<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[JD Armada - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[JD Armada - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/kr/search-labs/author/jd-armada</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/author/jd-armada</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/author/jd-armada.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Tue, 22 Sep 2026 17:48:16 GMT</lastBuildDate>
  <item>
    <title><![CDATA[상위 Elastic 에이전트 빌더 프로젝트와 Cal Hacks 12.0에서 얻은 교훈]]></title>
    <description><![CDATA[Cal Hacks 12.0에서 최고의 Elastic 에이전트 빌더 프로젝트를 살펴보고 서버리스, ES|QL 및 에이전트 아키텍처에 대한 기술적 요점을 자세히 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>몇 주 전, 전 세계에서 2,000명 이상의 참가자가 모인 대규모 오프라인 해커톤 중 하나인 <a href="https://cal-hacks-12-0.devpost.com/">Cal Hacks 12.0을</a> 후원할 수 있는 놀라운 기회를 가졌습니다. 서버리스에서 Elastic 에이전트 빌더를 가장 잘 활용할 수 있는 전용 상금 트랙을 제공했는데, 그 반응은 놀라웠습니다. 단 36시간 만에 산불 인텔리전스 도구 구축부터 StackOverflow 유효성 검사기까지 에이전트 빌더를 창의적인 방식으로 사용한 29개의 제출물이 접수되었습니다.</p><p>인상적인 프로젝트 외에도 Cal Hacks 12.0의 경험은 Stack을 처음 접하는 개발자들의 빠르고 여과 없는 피드백이라는 또 다른 가치 있는 것을 얻게 해 주었습니다. 해커톤은 촉박한 일정, 사전 정보 부족, 예측할 수 없는 장애물(악명 높은 와이파이 중단 등)이 있는 독특한 압박 테스트입니다. 이를 통해 개발자 환경의 장점과 개선이 필요한 부분을 정확히 파악할 수 있습니다. 개발자들이 점점 더 LLM 기반 워크플로우를 통해 새로운 방식으로 Elastic Stack과 상호 작용함에 따라 이는 이제 더욱 중요해졌습니다. 이 블로그 게시물에서는 참가자들이 에이전트 빌더로 구축한 내용과 그 과정에서 배운 점을 자세히 살펴봅니다.</p><h2>수상 프로젝트</h2><h3>1등 에이전트 오버플로</h3><p>스택 오버플로가 LLM 및 에이전트 시대에 맞게 재구축되었습니다.</p><p><a href="https://devpost.com/software/agentoverflow">여기에서</a> 에이전트오버플로에 대해 자세히 알아보세요.</p><p>에이전트오버플로는 대부분의 AI 개발자가 직면하는 문제를 해결합니다: LLM이 환각을 일으키고, 채팅 기록이 사라지고, 개발자가 동일한 문제를 다시 해결하느라 시간을 낭비하는 등의 문제를 해결합니다.</p><p>에이전트오버플로는 실제 문제 해결 쌍을 캡처, 검증 및 재표출하여 개발자가 환각의 소용돌이를 끊고 더 빠르게 출시할 수 있도록 지원합니다.</p><h4>작동 방식:</h4><p><strong>1. JSON - "솔루션 스키마" 공유.</strong></p><p>Claude 공유에서 클릭 한 번으로 다음을 포함하는 구조화된 형식인 공유 솔루션 JSON을 스크랩, 추출 및 조립합니다:</p><ul><li><p>문제</p></li><li><p>컨텍스트</p></li><li><p>코드</p></li><li><p>태그</p></li><li><p>검증된 솔루션 단계.</p></li></ul><p>유효성 검사기(LAVA)가 구조를 검사하고 적용하면 사용자가 한 줄의 추가 컨텍스트를 추가하면 Elasticsearch 내에 저장되고 색인됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte7bc35b6d54921e8/6a17f0176df73162760a0fe6/45a3e96f4474050a855419628c2a7338bb12c706-1600x877.png" alt="&quot;솔루션 공유&quot;를 클릭하면 관련 메타데이터와 함께 현재 세션이 스크랩됩니다." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9967f52007fff99e/6a17f019ec0f8987c45a6701/2d65cb154d8ee32fc96ff17dfa5b0bf2636e3777-1600x1002.png" alt="사용자가 웹 프론트엔드를 통해 추가 컨텍스트를 제공하면 JSON이 Elasticsearch에서 색인됩니다." /><p><strong>2. 솔루션 찾기</strong></p><p>문제가 발생하면 <code>Find Solution</code> 을 클릭하면 AgentOverflow가 현재 대화를 스크랩하여 쿼리를 작성하고 이를 사용하여 하이브리드 Elasticsearch 검색을 실행하여 결과를 표시합니다:</p><ul><li><p>순위가 매겨진 커뮤니티 검증 수정 사항</p></li><li><p>원래 문제를 해결한 정확한 안내 메시지</p></li></ul><p>이를 통해 개발자는 현재 세션을 빠르게 복사, 붙여넣기, 차단 해제할 수 있습니다.</p><p><strong>3. MCP - LLM을 위한 컨텍스트 주입</strong></p><p>MCP(모델 컨텍스트 프로토콜)를 통해 Elasticsearch 내에 저장된 구조화된 솔루션에 연결하면 별도의 노이즈 없이 런타임에 높은 신호 컨텍스트(코드, 로그, 구성, 이전 수정 사항)를 LLM에 공급할 수 있습니다.</p><p>에이전트 오버플로에서는 에이전트 빌더와 Elasticsearch를 사용해 관련 컨텍스트를 LLM에 주입하는 구조화된 메모리 계층으로 사용합니다. 이를 통해 수동적인 챗봇에서 상황 인식 문제 해결사로 변신합니다.</p><h3>준우승 MarketMind</h3><p>6개의 Elastic 에이전트가 제공하는 시장 에너지에 대한 실시간 해석 가능한 보기.</p><p>마켓마인드에 대한 자세한 내용은 <a href="https://devpost.com/software/marketmind-b6cy2q">여기를</a> 참조하세요.</p><p>MarketMind는 초보 트레이더에게 파편화된 시장 데이터를 명확한 실시간 신호로 변환하는 플랫폼을 제공함으로써 자리를 잡았습니다. 가격 움직임, 펀더멘털, 심리, 변동성을 여러 도구에 분산하는 대신 MarketMind는 이 모든 정보를 하나의 플랫폼에 통합하여 트레이더가 실행 가능한 인사이트를 얻을 수 있도록 도와줍니다. 이 프로젝트는 에이전트를 구축할 때 복잡한 ES|QL 쿼리를 사용하기도 했습니다.</p><h4>작동 방식:</h4><p><strong>1. 실시간 시장 데이터 수집</strong></p><p>MarketMind는 가격 움직임, 펀더멘털, 심리, 변동성, 위험 지표를 야후 파이낸스로부터 가져옵니다. 이 데이터는 수집되어 여러 개의 Elasticsearch 인덱스로 구성됩니다.</p><p><strong>2. 6명의 전문 에이전트가 시장을 분석합니다.</strong></p><p>에이전트 빌더로 구축된 각 에이전트는 시장의 다른 계층에 초점을 맞춥니다. 이들은 Elasticsearch 인덱스에서 읽고, 자체 도메인별 메트릭을 계산하고, 점수와 추론이 포함된 표준화된 JSON 출력을 생성합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4ba582f9872b65b/6a17f01b7f6f15c2d8c09c1c/7d9716cca06a047a2b3584378b5c7e592a785ba1-1284x878.png" alt="시장을 분석하는 6가지 전문 GOOGL AI 에이전트" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd86ed3bfe4b8bd2b/6a17f01c5ea30f868164b6ba/5aac6a833347c0d2e596c02049ec4b4d3aae5cd7-794x764.png" alt="볼륨 이상 및 재해 감지 분석 기능을 갖춘 GOOGL 전문 에이전트" /><p><strong>3. 통합된 '시장 에너지' 모델로 신호 통합</strong></p><p>합산된 결과는 각 종목 주위에 빛나는 펄스로 표시되어 모멘텀이 형성되고 있는지, 리스크가 상승하고 있는지, 심리가 변화하고 있는지를 보여줍니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5af7c7c838308275/6a17f01e42022917b629f6ca/46b3da8e3d528c5dd4e2829416c5446098acb3aa-744x718.png" alt="GOOGL 전문 에이전트의 통합된 '시장 에너지' 모델" /><p><strong>4. 인사이트 시각화</strong></p><p>프론트엔드는 타입스크립트, SVG 물리 기반 비주얼, 라이브 캔들스틱 차트를 위한 <a href="https://github.com/chartjs">Chart.js를 사용하여</a> <a href="https://github.com/vercel/next.js">React와 Next.js로 구축되었습니다.</a> 이렇게 하면 원시 분석이 실시간으로 실행 가능한 피드백으로 전환됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt775e1880aa7afacc/6a17f01f1d1b83ce1f93e528/3f000c043117b77ed4127202be5a49c12e3682ba-1600x930.png" alt="GOOGL 전문 상담원 분석의 인사이트를 시각화하는 방법" /><h2>기타 흥미로운 프로젝트:</h2><p>다음은 스택의 여러 부분에서 Elastic을 사용한 다른 강력한 경쟁자들입니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltffe292009e446a70/6a17f0216df731068c0a0fea/76c49a853426844f475cd6b2a74999e60af20e8c-926x1080.png" alt="" /><p><a href="https://cal-hacks-12-0.devpost.com/submissions/search?utf8=%E2%9C%93&amp;prize_filter%5Bprizes%5D%5B%5D=91882">여기에서</a> 트랙에 제출된 프로젝트의 전체 목록을 확인하세요.</p><h2>개발자로부터 배운 점</h2><ul><li><p><strong>에이전트 빌더는 사용자 친화적입니다:</strong></p></li></ul><p>대부분의 팀은 이전에 Elastic을 사용해 본 적이 없었지만 적은 지원으로도 신속하게 에이전트를 구축할 수 있었습니다. 더 많은 안내가 필요한 분들을 위해 워크숍을 열었지만, 대부분 데이터를 수집하고 해당 데이터에 대한 작업을 수행하는 에이전트를 구축할 수 있었습니다.</p><ul><li><p><strong>LLM은 </strong><strong><code>kNN</code></strong><strong> 쿼리에 탁월하지만 여전히 ES|QL 생성에 대한 지침이 필요합니다:</strong></p></li></ul><p>ChatGPT-5에 ES|QL 쿼리를 생성하도록 요청하면 ES|QL과 SQL이 혼합된 잘못된 정보가 반환되는 경우가 많았습니다. 마크다운 파일로 문서를 LLM에 공급하는 것은 실행 가능한 해결 방법인 것 같았습니다.</p><ul><li><p><strong>스냅샷 전용 ES|QL 함수가 문서로 유출되었습니다:</strong></p></li></ul><p>곧 출시될 <code>FIRST</code> 및 <code>LAST</code> 집계 함수는 의도치 않게 ES|QL 문서에 포함되었습니다. 이러한 문서를 ChatGPT에 제공했기 때문에 모델은 아직 서버리스에서 사용할 수 없는 기능임에도 불구하고 이러한 기능을 충실히 사용했습니다. 그룹의 피드백에 힘입어 엔지니어링 팀은 게시된 문서에서 기능을 제거하는 수정 사항을 신속하게 공개하고 병합했습니다<a href="https://github.com/elastic/elasticsearch/pull/137341">(PR #137341)</a>.</p><ul><li><p><strong>서버리스 관련 안내가 누락되었습니다:</strong></p></li></ul><p>한 팀이 조회 모드로 생성되지 않은 인덱스에서 <code>LOOKUP JOIN</code> 을 활성화하려고 시도했습니다. 오류 메시지는 서버리스에 존재하지 않는 명령을 쫓는다는 메시지를 보냈습니다. 이 사실을 제품 팀에 전달했고, 제품 팀은 즉시 서버리스와 관련된 실행 가능한 메시지에 대한 수정 사항을 공개했습니다. 장기적으로는 재색인 복잡성을 완전히 숨기는 것이 비전입니다<a href="https://github.com/elastic/elasticsearch-serverless/issues/4838">(이슈 #4838)</a>.</p><ul><li><p><strong>대면 이벤트의 가치:</strong></p></li></ul><p>온라인 해커톤도 훌륭하지만, 빌더와 어깨를 맞대고 디버깅할 때 얻을 수 있는 빠른 피드백을 따라올 수 있는 것은 없습니다. 다양한 사용 사례에서 에이전트 빌더를 통합하는 팀을 지켜보면서 ES|QL을 사용하는 개발자 경험을 개선할 수 있는 부분을 발견하고 비동기 채널을 통해 문제를 해결하는 것보다 훨씬 더 빠르게 문제를 해결했습니다.</p><h2>결론</h2><p>Cal Hacks 12.0은 주말 동안 멋진 데모를 보여줬을 뿐만 아니라 새로운 개발자들이 Elastic Stack과 어떻게 상호작용하는지에 대한 인사이트도 제공했습니다. 단 36시간 만에 팀들이 에이전트 빌더를 선택하고, Elasticsearch로 데이터를 수집하고, 멀티 에이전트 시스템을 설계하고, 다양한 방식으로 기능을 테스트하는 것을 보았습니다. 이번 행사를 통해 대면 이벤트가 중요한 이유를 다시 한 번 깨달았습니다. 빠른 피드백 루프, 실제 대화, 실습 디버깅을 통해 현재 개발자의 요구 사항을 파악하는 데 도움이 되었습니다. 저희가 배운 내용을 엔지니어링 팀에 다시 전달할 수 있게 되어 기쁩니다. 다음 해커톤에서 뵙겠습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agent-builder-projects-learnings-cal-hacks-12-0</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agent-builder-projects-learnings-cal-hacks-12-0</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[에이전틱 AI]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0f079179be9832d4/6a17f023631730a69c585b6d/8ba034a6f19b50521f541b8131756a8acdb52975-1280x960.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Mastra와 Elasticsearch를 사용하여 시맨틱 리콜 기능을 갖춘 지식 에이전트 구축하기]]></title>
    <description><![CDATA[메모리 및 정보 검색을 위한 벡터 저장소로 Mastra와 Elasticsearch를 사용해 시맨틱 리콜 기능을 갖춘 지식 에이전트를 구축하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">컨텍스트 엔지니어링은</a> 안정적인 AI 에이전트 및 아키텍처를 구축하는 데 있어 점점 더 중요해지고 있습니다. 모델이 점점 더 개선됨에 따라 그 효과와 신뢰성은 학습된 데이터보다는 올바른 맥락에 얼마나 잘 근거를 두고 있는지에 따라 달라집니다. 가장 관련성 높은 정보를 적시에 검색하고 적용할 수 있는 에이전트가 정확하고 신뢰할 수 있는 결과물을 만들어낼 가능성이 훨씬 높습니다.</p><p>이 블로그에서는 <a href="https://mastra.ai/">Mastra를</a> 사용해 사용자가 말한 내용을 기억하고 나중에 관련 정보를 불러올 수 있는 지식 에이전트를 구축하는 데 Elasticsearch를 메모리 및 검색 백엔드로 사용하겠습니다. 동일한 개념을 실제 사용 사례로 쉽게 확장하여 지원 상담원이 과거의 대화와 해결 방법을 기억할 수 있어 특정 사용자에게 맞춤형 응답을 제공하거나 이전 컨텍스트를 기반으로 더 빠르게 해결책을 제시할 수 있다고 생각하면 됩니다.</p><p>여기를 따라 단계별로 구축하는 방법을 알아보세요. 길을 잃었거나 완성된 예제를 실행하고 싶다면 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">여기에서</a> 리포지토리를 확인하세요.</p><h2>마스트라란 무엇인가요?</h2><p>Mastra는 추론, 메모리 및 도구에 대한 교체 가능한 부품으로 AI 에이전트를 구축하기 위한 오픈 소스 TypeScript 프레임워크입니다. <a href="https://mastra.ai/docs/memory/semantic-recall">시맨틱 리콜</a> 기능을 통해 상담원은 메시지를 벡터 데이터베이스에 임베딩으로 저장하여 과거 상호작용을 기억하고 검색할 수 있습니다. 이를 통해 상담원은 장기적인 대화 맥락과 연속성을 유지할 수 있습니다. Elasticsearch는 효율적인 고밀도 벡터 검색을 지원하기 때문에 이 기능을 활성화하는 데 탁월한 벡터 저장소입니다. 시맨틱 리콜이 트리거되면 에이전트는 관련 과거 메시지를 모델의 컨텍스트 창으로 가져와서 모델이 검색된 컨텍스트를 추론 및 응답의 기초로 사용할 수 있도록 합니다.</p><h2>시작하기 위해 필요한 사항</h2><ul><li><p>노드 v18+</p></li><li><p>Elasticsearch(버전 8.15 이상)</p></li><li><p>Elasticsearch API 키</p></li><li><p><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI API 키</a></p></li></ul><p>참고: 데모에서는 OpenAI 공급자를 사용하므로 이 공급자가 필요하지만, Mastra는 다른 AI SDK 및 커뮤니티 모델 공급자를 지원하므로 설정에 따라 쉽게 교체할 수 있습니다.</p><h2>Mastra 프로젝트 구축</h2><p>Mastra에 내장된 CLI를 사용하여 프로젝트의 스캐폴딩을 제공하겠습니다. 명령을 실행합니다:</p>npm create mastra@latest<p>다음과 같은 일련의 프롬프트가 표시됩니다:</p><p>1. 프로젝트 이름을 지정합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87f941f654d03827/6a16f7af67045b214d45bfa1/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" alt="Mastra 앱에서 프롬프트 이름 지정하기" /><p>2. 이 기본값을 그대로 사용해도 되므로 비워두셔도 됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7dbb3d4f27435cac/6a16f7b0cdacbf29497d27de/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" alt="프롬프트 파일을 어디에 보관할지 마스트라에게 알려주기" /><p>3. 이 프로젝트에서는 OpenAI에서 제공하는 모델을 사용합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f654f6cb9397e94/6a16f7b2964cea899a08b942/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" alt="Mastra에서 OpenAI가 제공하는 모델 선택하기" /><p>4. 모든 환경 변수를 이후 단계에서 구성할 '.env' 파일에 저장하므로 '지금은 건너뛰기' 옵션을 선택합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff106117521a3519/6a16f7b3c1e8a5031af880d8/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" alt="OpenAI 키에 대해 지금은 건너뛰기를 선택합니다." /><p>5. 이 옵션을 건너뛸 수도 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda7d9c51c3878d7/6a16f7b450916809dbe1b892/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" alt="" /><p>초기화가 완료되면 다음 단계로 넘어갈 수 있습니다.</p><h3>종속성 설치</h3><p>다음으로 몇 가지 종속성을 설치해야 합니다:</p>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv<ul><li><p><code>ai</code> - 자바스크립트/타입스크립트에서 AI 모델, 프롬프트 및 워크플로를 관리할 수 있는 도구를 제공하는 핵심 AI SDK 패키지입니다. Mastra는 Vercel의 <a href="https://ai-sdk.dev/">AI SDK를</a> 기반으로 구축되었으므로 에이전트와의 모델 상호 작용을 활성화하려면 이 종속성이 필요합니다.</p></li><li><p><code>@ai-sdk/openai</code> - AI SDK를 OpenAI 모델(예: GPT-4, GPT-4o 등)에 연결하여 OpenAI API 키를 사용하여 API 호출을 가능하게 하는 플러그인입니다.</p></li><li><p><code>@elastic/elasticsearch</code> - <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js용 공식 Elasticsearch 클라이언트</a>, 인덱싱, 검색, 벡터 작업을 위해 Elastic Cloud 또는 로컬 클러스터에 연결하는 데 사용됩니다.</p></li><li><p><code>dotenv</code> - .env에서 환경 변수를 로드합니다. 파일을 process.env에 추가합니다, API 키와 Elasticsearch 엔드포인트와 같은 자격 증명을 안전하게 삽입할 수 있습니다.</p></li></ul><h3>환경 변수 구성</h3><p>프로젝트 루트 디렉터리에 <code>.env</code> 파일이 없는 경우 이 파일을 만듭니다. 또는 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">리포지토리에</a> 제공한 예제 <code>.env</code> 를 복사하여 이름을 바꿀 수 있습니다. 이 파일에서 다음 변수를 추가할 수 있습니다:</p>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<p>이것으로 기본 설정이 끝났습니다. 여기에서 이미 상담원 구축 및 오케스트레이션을 시작할 수 있습니다. 여기서 한 걸음 더 나아가 저장 및 벡터 검색 레이어로 Elasticsearch를 추가하겠습니다.</p><h2>벡터 저장소로 Elasticsearch 추가하기</h2><p><code>stores</code> 이라는 새 폴더를 만들고 그 안에 이 <a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">파일을</a> 추가합니다. Mastra와 Elastic이 공식 Elasticsearch 벡터 저장소 통합을 출시하기 전에 <a href="https://github.com/abhiaiyer91">Abhi Aiyer</a>(Mastra CTO)가 초기 프로토타입 클래스( <code>ElasticVector</code>)를 공유했습니다. 간단히 말해, Mastra의 메모리 추상화와 Elasticsearch의 고밀도 벡터 기능을 연결하여 개발자가 에이전트를 위한 벡터 데이터베이스로 Elasticsearch를 도입할 수 있습니다.</p><p>통합의 중요한 부분을 자세히 살펴보겠습니다:</p><h3>Elasticsearch 클라이언트 수집</h3><p>이 섹션에서는 <code>ElasticVector</code> 클래스를 정의하고 표준 배포와 서버리스 배포를 모두 지원하는 Elasticsearch 클라이언트 연결을 설정합니다.</p>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

export class ElasticVector extends MastraVector {
    private client: Client;
    private isServerless: boolean | undefined;
    private deploymentChecked: boolean = false;
    private readonly maxCountAccuracy: number;

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}<ul><li><p><code>ElasticVectorConfig extends ClientOptions</code>: 이렇게 하면 모든 Elasticsearch 클라이언트 옵션(예: <code>node</code>, <code>auth</code>, <code>requestTimeout</code>)을 상속하고 사용자 정의 속성을 추가하는 새로운 구성 인터페이스가 생성됩니다. 즉, 사용자는 서버리스 전용 옵션과 함께 유효한 모든 Elasticsearch 구성을 전달할 수 있습니다.</p></li><li><p><code>extends MastraVector</code>: 이를 통해 <code>ElasticVector</code> 은 모든 벡터 스토어 통합이 준수하는 공통 인터페이스인 Mastra의 기본 <code>MastraVector</code> 클래스에서 상속할 수 있습니다. 이렇게 하면 Elasticsearch가 에이전트의 관점에서 다른 모든 Mastra 벡터 백엔드처럼 작동합니다.</p></li><li><p><code>private client: Client</code>: Elasticsearch JavaScript 클라이언트의 인스턴스를 보관하는 개인 속성입니다. 이렇게 하면 클래스가 클러스터와 직접 대화할 수 있습니다.</p></li><li><p><code>isServerless</code> 및 <code>deploymentChecked</code>: 이러한 속성은 함께 작동하여 서버리스 또는 표준 Elasticsearch 배포에 연결되어 있는지 여부를 감지하고 캐시합니다. 이 감지는 처음 사용할 때 자동으로 수행되거나 명시적으로 구성할 수 있습니다.</p></li><li><p><code>constructor(config: ClientOptions)</code>: 이 생성자는 구성 객체(Elasticsearch 자격 증명 및 선택적 서버리스 설정이 포함된)를 가져와서 <code>this.client = new Client(config)</code> 줄에서 클라이언트를 초기화하는 데 사용합니다.</p></li><li><p><code>super()</code>: 이것은 Mastra의 기본 생성자를 호출하므로 로깅, 유효성 검사 헬퍼 및 기타 내부 훅을 상속합니다.</p></li></ul><p>이 시점에서 Mastra는 다음과 같은 새로운 벡터 스토어가 있다는 것을 알고 있습니다. <code>ElasticVector</code></p><h3>배포 유형 감지</h3><p>인덱스를 생성하기 전에 어댑터는 표준 Elasticsearch를 사용 중인지 아니면 Elasticsearch 서버리스를 사용 중인지 자동으로 감지합니다. 서버리스 배포에서는 수동 샤드 구성을 허용하지 않기 때문에 이 점이 중요합니다.</p>private async detectServerless(): Promise&lt;boolean&gt; {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

    // Use explicit configuration if provided
    if (this.isServerless !== undefined) {
        this.deploymentChecked = true;
        this.logger?.info(
            `Using explicit deployment type: ${this.isServerless ? 'Serverless' : 'Standard'}`
        );
        return this.isServerless;
    }

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}<p>무슨 일이 일어나고 있나요?</p><ul><li><p>먼저 구성에서 <code>isServerless</code> 을 명시적으로 설정했는지 확인합니다(자동 감지 건너뛰기).</p></li><li><p>Elasticsearch의 <code>info()</code> API를 호출하여 클러스터 정보를 가져옵니다.</p></li><li><p><code>build_flavor field</code> (서버리스 배포는 <code>serverless</code>)를 확인합니다.</p></li><li><p>빌드 플레이버를 사용할 수 없는 경우 태그 라인 확인으로 돌아가기</p></li><li><p>반복되는 API 호출을 방지하기 위해 결과 캐시</p></li><li><p>탐지에 실패하면 표준 배포로 기본 설정됩니다.</p></li></ul><p> 사용 예시:</p>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});<h3>Elasticsearch에서 "메모리" 저장소 만들기</h3><p>아래 함수는 임베딩을 저장하기 위한 Elasticsearch 인덱스를 설정합니다. 인덱스가 이미 존재하는지 확인합니다. 그렇지 않은 경우 임베딩 및 사용자 정의 유사성 메트릭을 저장할 <code>dense_vector</code> 필드가 포함된 아래 매핑을 사용하여 매핑을 생성합니다.</p><p>몇 가지 주의해야 할 사항:</p><ul><li><p><code>dimension</code> 매개변수는 각 임베딩 벡터의 길이로, 사용 중인 임베딩 모델에 따라 달라집니다. 이 경우, <code>1536</code> 크기의 벡터를 출력하는 OpenAI의 <code>text-embedding-3-small</code> 모델을 사용하여 임베딩을 생성하겠습니다. 이 값을 기본값으로 사용하겠습니다.</p></li><li><p>아래 매핑에 사용된 <code>similarity</code> 변수는 <code>metric</code> 매개변수의 값을 받아 선택한 거리 메트릭에 대해 Elasticsearch 호환 키워드로 변환하는 도우미 함수 c<code>onst similarity = this.mapMetricToSimilarity(metric)</code> 에서 정의됩니다.</p><ul><li><p>예를 들어: 예를 들어, Mastra는 <code>cosine</code>, <code>euclidean</code>, <code>dotproduct</code> 와 같은 벡터 유사성에 대한 일반적인 용어를 사용합니다. <code>euclidean</code> 메트릭을 Elasticsearch 매핑에 직접 전달하면, Elasticsearch는 <code>l2_norm</code> 키워드가 유클리드 거리를 나타낼 것으로 예상하기 때문에 오류가 발생합니다.</p></li></ul></li><li><p>서버리스 호환성: 서버리스 배포를 위한 샤드 및 복제본 설정은 Elasticsearch 서버리스에서 자동으로 관리되므로 코드에서 자동으로 생략됩니다.</p></li></ul>async createIndex(params: CreateIndexParams): Promise&lt;void&gt; {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

    try {
        const exists = await this.client.indices.exists({ index: indexName });

        if (exists) {
            try {
                await this.validateExistingIndex(indexName, dimension, metric);
                this.logger?.info(`Index "${indexName}" already exists and is valid`);
                return;
            } catch (validationError) {
                throw new Error(
                    `Index "${indexName}" exists but does not match the required configuration: ${
                        validationError instanceof Error ? validationError.message : String(validationError)
                    }`
                );
            }
        }

        const isServerless = await this.detectServerless();
        const similarity = this.mapMetricToSimilarity(metric);

        const indexConfig: any = {
            index: indexName,
            mappings: {
                properties: {
                    vector: {
                        type: 'dense_vector',
                        dims: dimension,
                        index: true,
                        similarity: similarity,
                    },
                    metadata: {
                        type: 'object',
                        enabled: true,
                        dynamic: true, // Allows flexible metadata structures
                    },
                },
            },
        };

        // Only configure shards/replicas for non-serverless deployments
        // Serverless manages infrastructure automatically
        if (!isServerless) {
            indexConfig.settings = {
                number_of_shards: 1,
                number_of_replicas: 0, // Increase for production HA deployments
            };
        }

        await this.client.indices.create(indexConfig);

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}<h3>상호 작용 후 새 메모리 또는 노트 저장하기</h3><p>이 함수는 각 상호 작용 후에 생성된 새 임베딩을 메타데이터와 함께 가져온 다음 Elastic의 <code>bulk</code> API를 사용하여 인덱스에 삽입하거나 업데이트합니다. <code>bulk</code> API는 여러 개의 쓰기 작업을 단일 요청으로 그룹화하여 인덱싱 성능을 개선함으로써 에이전트의 메모리가 계속 증가함에 따라 업데이트가 효율적으로 유지되도록 합니다.</p>async upsert(params: UpsertVectorParams): Promise&lt;string[]&gt; {
    const { indexName, vectors, metadata = [], ids } = params;

    try {
        // Generate unique IDs if not provided
        const vectorIds = ids || vectors.map((_, i) =&gt; 
            `vec_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 9)}`
        );

        const operations = vectors.flatMap((vec, index) =&gt; [
            { index: { _index: indexName, _id: vectorIds[index] } },
            {
                vector: vec,
                metadata: metadata[index] || {},
            },
        ]);

        const response = await this.client.bulk({
            refresh: true,
            operations,
        });

        if (response.errors) {
            const erroredItems = response.items.filter((item: any) =&gt; item.index?.error);
            const erroredIds = erroredItems.map((item: any) =&gt; item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) =&gt; ({
                id: item.index?._id,
                error: item.index?.error?.reason || item.index?.error,
                type: item.index?.error?.type
            }));
            
            const errorMessage = `Failed to upsert ${erroredIds.length}/${vectors.length} vectors`;
            console.error(`${errorMessage}. Sample errors:`, JSON.stringify(errorDetails, null, 2));
            this.logger?.error(errorMessage, { 
                failedCount: erroredIds.length, 
                totalCount: vectors.length,
                sampleErrors: errorDetails 
            });
            
            // Still return successfully inserted IDs
            const successfulIds = vectorIds.filter((id, idx) =&gt; 
                !erroredIds.includes(id)
            );
            
            if (successfulIds.length === 0) {
                throw new Error(`${errorMessage}. All operations failed. See logs for details.`);
            }
            
            return successfulIds;
        }

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}<h3>시맨틱 리콜을 위해 유사한 벡터 쿼리하기</h3><p>이 기능은 시맨틱 리콜 기능의 핵심입니다. 에이전트는 벡터 검색을 사용하여 인덱스 내에서 유사한 저장된 임베딩을 찾습니다.</p>async query(params: QueryVectorParams&lt;any&gt;): Promise&lt;QueryResult[]&gt; {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

    try {
        const knnQuery: any = {
            field: 'vector',
            query_vector: queryVector,
            k: topK,
            num_candidates: Math.max(topK * 10, 100), // Search more candidates for better recall
        };

        // Apply metadata filters if provided
        if (filter) {
            knnQuery.filter = this.buildElasticFilter(filter);
        }

        const sourceFields = ['metadata'];
        if (includeVector) {
            sourceFields.push('vector');
        }

        const response = await this.client.search({
            index: indexName,
            knn: knnQuery,
            size: topK,
            _source: sourceFields,
        });

        const results = response.hits.hits.map((hit: any) =&gt; ({
            id: hit._id,
            score: hit._score || 0,
            metadata: hit._source?.metadata || {},
            vector: includeVector ? hit._source?.vector : undefined,
        }));

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}<p>내부를 들여다보세요:</p><ul><li><p>Elasticsearch에서 <code>knn</code> API를 사용하여 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a> (k-nearest neighbors) 쿼리를 실행합니다.</p></li><li><p>입력 쿼리 벡터와 유사한 상위 K개의 벡터를 검색합니다.</p></li><li><p>선택적으로 메타데이터 필터를 적용하여 결과 범위를 좁힐 수 있습니다(예: 특정 카테고리 또는 시간 범위 내에서만 검색).</p></li><li><p>문서 ID, 유사도 점수, 저장된 메타데이터를 포함한 구조화된 결과를 반환합니다.</p></li></ul><h2>지식창고 만들기</h2><p>이제 <code>ElasticVector</code> 통합을 통해 Mastra와 Elasticsearch 간의 연결을 확인했으니, 지식 에이전트 자체를 생성해 보겠습니다.</p><p><code>agents</code> 폴더 안에 <code>knowledge-agent.ts</code> 이라는 파일을 만듭니다. 환경 변수를 연결하고 Elasticsearch 클라이언트를 초기화하는 것으로 시작할 수 있습니다.</p>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;

//Error check for undefined credentials
if (!ELASTICSEARCH_ENDPOINT || !ELASTICSEARCH_API_KEY) {
  throw new Error('Missing Elasticsearch credentials');
}

//Check to see if a connection can be established
const testClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { 
    apiKey: ELASTICSEARCH_API_KEY 
  },
});

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});<p>여기, 우리:</p><ul><li><p><code>dotenv</code> 을 사용하여 <code>.env</code> 파일에서 변수를 로드합니다.</p></li><li><p>Elasticsearch 자격 증명이 올바르게 주입되고 있는지 확인하면 클라이언트에 성공적으로 연결할 수 있습니다.</p></li><li><p><code>ElasticVector</code> 생성자에 Elasticsearch 엔드포인트와 API 키를 전달하여 앞서 정의한 벡터 저장소의 인스턴스를 생성합니다.</p></li><li><p>선택적으로 Elasticsearch 서버리스를 사용하는 경우 <code>isServerless: true</code> 을 지정합니다. 이렇게 하면 자동 감지 단계를 건너뛰고 시작 시간이 단축됩니다. 이 옵션을 생략하면 처음 사용할 때 어댑터가 자동으로 배포 유형을 감지합니다.</p></li></ul><p>다음으로 Mastra의 <code>Agent</code> 클래스를 사용하여 에이전트를 정의할 수 있습니다.</p>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

        //embedder used to create embeddings for each message
        embedder: 'openai/text-embedding-3-small',

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});<p>정의할 수 있는 필드는 다음과 같습니다:</p><ul><li><p><code>name</code> 및 <code>instructions</code>: 아이덴티티와 기본 기능을 부여합니다.</p></li><li><p><code>model</code>: <code>@ai-sdk/openai</code> 패키지를 통해 OpenAI의 <code>gpt-4o</code> 를 사용하고 있습니다.</p></li><li><p><code>memory</code>:</p><ul><li><p><code>vector</code>: Elasticsearch 저장소를 가리키므로 임베딩이 저장되고 거기에서 검색됩니다.</p></li><li><p><code>embedder</code>: 임베딩 생성에 사용할 모델</p></li><li><p><code>semanticRecall</code> 옵션은 리콜 작동 방식을 결정합니다:</p><ul><li><p><code>topK</code>: 검색할 의미적으로 유사한 메시지 수입니다.</p></li><li><p><code>messageRange</code>: 각 경기에 포함할 대화 분량입니다.</p></li><li><p><code>scope</code>: 메모리 경계를 정의합니다.</p></li></ul></li></ul></li></ul><p>거의 다 끝났습니다. 새로 생성된 에이전트를 Mastra 구성에 추가하기만 하면 됩니다. <a href="http://index.ts/"><code>index.ts</code></a> 라는 파일에서 지식 에이전트를 가져와서 <code>agents</code> 필드에 삽입합니다.</p>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});<p>다른 필드에는 다음이 포함됩니다:</p><ul><li><p><code>storage</code>: 실행 기록, 통합 가시성 메트릭, 점수 및 캐시를 위한 Mastra의 내부 데이터 저장소입니다. Mastra 스토리지에 대한 자세한 내용은 <a href="https://mastra.ai/docs/server-db/storage">여기를</a> 참조하세요.</p></li><li><p><code>logger</code>: Mastra는 경량 구조화된 JSON 로거인 <a href="https://github.com/pinojs/pino">Pino를</a> 사용합니다. 상담원 시작 및 중지, 도구 호출 및 결과, 오류, LLM 응답 시간 등의 이벤트를 캡처합니다.</p></li><li><p><code>observability</code>: 상담원의 AI 추적 및 실행 가시성을 제어합니다. 추적합니다:</p><ul><li><p>각 추론 단계의 시작/종료</p></li><li><p>어떤 모델 또는 도구를 사용했는지.</p></li><li><p>입력 및 출력.</p></li><li><p>점수 및 평가</p></li></ul></li></ul><h3>Mastra Studio로 에이전트 테스트</h3><p>축하합니다! 여기까지 왔다면 이 에이전트를 실행하여 시맨틱 리콜 기능을 테스트할 준비가 된 것입니다. 다행히도 Mastra는 기본 제공 채팅 UI를 제공하므로 자체적으로 구축할 필요가 없습니다.</p><p>Mastra 개발 서버를 시작하려면 터미널을 열고 다음 명령을 실행합니다:</p>npm run dev<p>서버를 처음 번들링하고 시작하면 플레이그라운드의 주소가 제공됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5f857fddc74ffc9/6a16f7b6a6c2b995d5e794c0/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" alt="Playground의 서버 주소" /><p>이 주소를 브라우저에 붙여넣으면 마스트라 스튜디오로 이동합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc7fdda6ce46ce068/6a16f7b7b0367d4f7672bacf/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" alt="플레이그라운드 주소를 붙여넣어 마스트라 스튜디오에 액세스하기" /><p><code>knowledgeAgent</code> 옵션을 선택하고 채팅을 시작합니다.</p><p>모든 것이 올바르게 연결되었는지 간단히 테스트하려면 "팀은 10월의 판매 실적이 12% 증가했다고 발표했습니다%, 주로 기업 리뉴얼에 힘입은 것입니다."와 같은 정보를 입력합니다. 다음 단계는 미드 마켓 고객으로 범위를 넓히는 것입니다." 다음으로 새 채팅을 시작하고 "다음에 어떤 고객 세그먼트에 집중해야 한다고 했나요?"와 같은 질문을 하세요. 지식 상담원은 첫 번째 채팅에서 제공한 정보를 기억할 수 있어야 합니다. 다음과 같은 응답이 표시되어야 합니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfec3266e81a7213b/6a16f7b92b835f6f70f4afe2/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" alt="Mastra Studio에서 지식 상담원과 채팅하기 - 상담원이 정보를 불러올 수 있습니다." /><p>이와 같은 응답을 보면 에이전트가 이전 메시지를 Elasticsearch에 임베딩으로 성공적으로 저장하고 나중에 벡터 검색을 사용하여 검색했다는 뜻입니다.</p><h3>상담원의 장기 기억 저장소 검사하기</h3><p>Mastra Studio의 상담원 구성에서 <code>memory</code> 탭으로 이동합니다. 이를 통해 상담원이 시간이 지남에 따라 학습한 내용을 확인할 수 있습니다. Elasticsearch에 포함되고 저장되는 모든 메시지, 응답, 상호 작용은 이 장기 기억의 일부가 됩니다. 과거 상호작용을 의미론적으로 검색하여 상담원이 이전에 학습한 정보나 컨텍스트를 빠르게 찾을 수 있습니다. 이는 기본적으로 에이전트가 시맨틱 리콜 중에 사용하는 것과 동일한 메커니즘이지만, 여기서 직접 검사할 수 있습니다. 아래 예시에서는 '판매'라는 용어를 검색하여 판매에 관한 내용이 포함된 모든 상호작용을 반환하고 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte428134d7bf2a43a/6a16f7bbb0367d185872bad3/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" alt="지식 에이전트 장기 기억 저장소를 검사하는 방법" /><h2>결론</h2><p>Mastra와 Elasticsearch를 연결하면 컨텍스트 엔지니어링의 핵심 계층인 메모리를 에이전트에게 제공할 수 있습니다. 시맨틱 리콜을 통해 상담원은 시간이 지남에 따라 컨텍스트를 구축하여 학습한 내용을 기반으로 응답할 수 있습니다. 이는 보다 정확하고 안정적이며 자연스러운 상호작용을 의미합니다.</p><p>이 초기 통합은 시작에 불과합니다. 여기서 동일한 패턴으로 과거 티켓을 기억하는 지원 상담원, 관련 문서를 검색하는 내부 봇, 대화 중에 고객 세부 정보를 기억할 수 있는 AI 어시스턴트 등을 만들 수 있습니다. 또한, 가까운 시일 내에 이 페어링이 더욱 원활하게 이루어질 수 있도록 공식적인 Mastra 통합을 위해 노력하고 있습니다.</p><p>여러분이 다음에 무엇을 만들지 기대가 됩니다. 한 번 사용해보시고 <a href="https://mastra.ai/">Mastra와</a> 그 메모리 기능을 살펴보고 발견한 내용을 커뮤니티와 자유롭게 공유하세요.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
    <category><![CDATA[에이전틱 AI]]></category>
    <category><![CDATA[개발자 경험]]></category>
    <category><![CDATA[통합]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09afdbff05603865/6a16f7bd839dfabbf2dcfcb5/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch 인덱스의 필드를 표시하는 방법]]></title>
    <description><![CDATA[맵핑 및 _검색 API, 하위 필드, 합성 _소스 및 런타임 필드를 사용하여 Elasticsearch 인덱스의 필드를 표시하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>이 문서에서는 Elasticsearch 인덱스의 필드를 표시하는 방법에 대해 설명합니다. 이는 데이터 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 유용할 수 있습니다. 다음 주제를 다룰 예정입니다:</p><ol><li><p><code>_mapping</code> API를 사용하여 필드 정보 검색하기</p></li><li><p><code>_search</code> API를 사용하여 필드 값 표시</p></li><li><p>하위 필드 표시</p></li><li><p>Synthetic _source</p></li><li><p>런타임 필드</p></li></ol><h2>1. 맵핑 API를 사용하여 필드 정보 검색하기</h2><p><code>_mapping</code> API를 사용하면 인덱스 또는 여러 인덱스에 대한 매핑 정의를 검색할 수 있습니다. 여기에는 필드, 데이터 유형 및 기타 속성에 대한 정보가 포함됩니다. 특정 인덱스에 대한 매핑을 검색하려면 다음 요청을 사용하세요:</p>GET /&lt;index_name&gt;/_mapping<p>예를 들어 <code>my_index</code> 이라는 인덱스가 있는 경우 다음 요청으로 해당 인덱스의 매핑을 검색할 수 있습니다:</p>GET /my_index/_mapping<p>응답에는 필드 및 해당 속성에 대한 정보가 포함된 인덱스에 대한 매핑 정의가 포함됩니다.</p><p>특정 필드에 대한 매핑을 검색할 수도 있습니다. 매핑이 상당히 크고 특정 필드에만 집중하려는 경우 유용할 수 있습니다. 특정 필드의 매핑을 검색하려면 다음 요청을 사용하세요:</p>GET /my_index/_mapping/field/my_field<p>다음 요청에서와 같이 쉼표로 이름을 구분하여 여러 필드의 매핑을 검색할 수도 있습니다:</p>GET /my_index/_mapping/field/my_field_1,my_field_2,my_field_3<h2>2. search API를 사용하여 필드 값 표시하기</h2><p>Elasticsearch 인덱스의 필드 값을 표시하려면 <code>_search</code> API를 사용하면 됩니다. <code>_search</code> API는 반환되는 필드를 제어할 수 있는 다양한 방법을 제공하며, 두 가지 주요 방법은 다음과 같습니다:</p><ol><li><p><strong><code>_source</code></strong>: <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field"><code>_source</code></a> 필드에는 수집 파이프라인이나 전처리 단계에 의해 변경된 사항을 포함하여 색인된 그대로의 원본 JSON 문서 본문이 포함되어 있습니다. 소스 문서의 특정 필드를 표시하려면 아래에서 설명하는 대로 소스 필터링을 구현합니다.</p></li><li><p><strong><code>fields</code></strong>: <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields"><code>fields</code></a> 매개변수를 사용하면 색인 매핑을 기반으로 검색을 수행할 때 문서에서 특정 필드를 검색할 수 있습니다. <code>_source</code> 과 달리 <code>fields</code> 은 <code>_source</code> 을 참조하지 않고 저장된 필드, 문서 값 또는 런타임 필드의 값을 반환할 수도 있지만 문서 값이나 저장된 설정이 없는 표준 필드의 경우 <code>_source</code> 으로 되돌아갑니다. 이는 아래에서 살펴보겠지만 성능 등 많은 이점을 가져올 수 있습니다.</p></li></ol><h3>소스필드 사용</h3><p>기본적으로<code> _search</code> API는 색인된 원본 JSON 문서가 포함된 <code>_source</code> 필드를 반환합니다. 특정 필드를 표시하려면 검색 요청의 <code>_source </code>매개변수에 필터를 추가할 수 있으며, 이를 소스 필터링이라고 합니다.</p><p>다음은 <code>my_index</code> 인덱스에 있는 문서에 대한 <code>title </code>및 <code>author</code> 필드 값을 반환하는 검색 요청의 예입니다:</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title", "author"]
}<p>이 예제에서 <code>_source</code> 매개변수는 반환할 필드를 지정합니다.</p><p>더 많은 제어가 필요한 경우 <code>_source</code> 객체의 <code>includes</code> 및 <code>excludes </code>속성을 사용할 수 있습니다. 예를 들어 아래 쿼리는 최상위 수준 <code>title</code> 필드와 <code>author</code> 의 <code>author.description</code> 을 제외한 모든 하위 필드를 반환합니다.</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": {
     “includes”: [“title”, “author.*],
     “excludes”: [“author.description”]
  }
}<p>이 예제에서는 <code>author.* </code>패턴을 사용하여 <code>author </code>객체의 모든 직접 하위 필드를 검색합니다. 그런 다음 <code>author.description </code>을 명시적으로 제외하여 다른 작성자 필드만 반환되도록 합니다. 이 경우에도 여전히 소스 JSON을 로드하고 구문 분석해야 하므로 성능이 향상되지는 않지만 네트워크를 통해 전송되는 응답의 크기를 줄일 수 있다는 점에 유의하세요.</p><h3>필드 매개변수 사용</h3><p><code>fields</code> 매개변수를 사용하여 검색 응답에 반환되는 필드를 필터링할 수 있습니다. <code>_source</code> 대신 <code>fields</code> 을 사용하면 다음과 같은 여러 가지 이점이 있습니다:</p><ul><li><p><strong>성능 개선: </strong><code>fields </code>은 전체 <code>_source</code> 을 로드할 필요 없이 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">저장된 필드</a> 또는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values">문서 값에서</a> 직접 값을 반환할 수 있으므로 응답 페이로드 크기가 더 작아집니다.</p></li><li><p><strong>형식화된 출력:</strong> 표준 필드의 경우<code> fields</code> 은 <code>_source</code> 으로 되돌아가 값을 가져올 수 있지만, 인덱스 매핑을 확인하여 형식이 지정된 날짜와 같은 출력의 형식을 적절히 지정하여 집계 및 정렬에 사용되는 것과 일관성을 유지합니다.</p></li><li><p><strong>런타임 필드에 대한 액세스:</strong> <code>fields</code> 은 원본 <code>_source</code> 에 없는 런타임 필드를 반환할 수 있습니다.</p></li><li><p>더 많은 혜택은 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#search-fields-param">여기에서</a> 확인할 수 있습니다.</p></li></ul><p>예를 들어 <code>my_index</code> 인덱스에서 <code>title</code> 및 <code>author</code> 필드만 반환하려면 다음 검색 요청을 사용할 수 있습니다:</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author"],
  "_source": false
}<p>위의 쿼리에서는 소스 문서를 반환하지 않도록 <code>_source </code>필드를 false로 설정했습니다. 이렇게 하면 응답의 페이로드 크기를 크게 최소화할 수 있지만 <code>title</code> 및 <code>author</code> 필드가 <code>keyword </code>필드 유형이고 기본적으로 <code>doc_values</code> 이 활성화되어 있기 때문에 작동한다는 점을 기억하세요. 필드에 <code>doc_values</code> 가 활성화되어 있지 않고 <code>_source</code> 가 false로 설정되어 있으면, Elasticsearch는 이를 검색할 방법이 없으며 응답에서 건너뛰게 됩니다.</p><p><code>fields</code> 응답은 값이 하나만 있는 경우에도 항상 각 필드에 대한 값 배열을 반환한다는 점에 유의하세요. 이는 Elasticsearch에 전용 배열 유형이 없고 모든 필드에 여러 개의 값이 있을 수 있기 때문입니다. Elasticsearch의 배열에 대한 자세한 내용을 보려면 <a href="http://elastic.co/docs/reference/elasticsearch/mapping-reference/array">여기를</a> 클릭하세요.</p><h3>필드를 검색하는 다른 방법</h3><p><code>_source</code> 또는 <code>fields</code> 을 사용하여 필드를 검색하는 것이 권장되는 방법이지만, 특정 사용 사례에 따라 다음과 같은 다양한 방법을 사용할 수 있습니다:</p><p><strong>문서 값 필드:</strong> <code>_source</code> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#docvalue-fields"><code>docvalue_fields</code></a>매개 변수를 사용하여 검색할 수 있습니다. 문서 값은 <code>_source</code> 과 동일한 필드 값을 저장하지만 정렬 및 집계에 최적화된 온디스크 데이터 구조로 저장합니다.</p><p><code>_source</code> 에 저장된 값과는 별개이므로 전체 <code>_source</code> 를 로드하지 않고도 특정 필드를 요청할 수 있습니다. 이 기능은 대규모 문서를 쿼리하지만 문서 값을 지원하는 작은 필드 몇 개만 필요한 경우에 유용합니다. <code>docvalue_fields </code>사용의 또 다른 사용 사례는 아래 예제에서 볼 수 있듯이 <code>date</code> 및 <code>numeric</code> 필드에 사용자 지정 서식을 사용하려는 경우입니다.</p><p><code>doc_values</code> 을 활성화한 필드 또는 <code>keyword</code>, <code>date</code>, 숫자 유형 및 <code>boolean</code> 과 같이 기본적으로 활성화된 필드 유형에 대해서만 작동하며, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/text"><code>text</code></a> 또는 <a href="https://www.elastic.co/docs/reference/elasticsearch/plugins/mapper-annotated-text-usage"><code>annotated_text</code></a> 에는 작동하지 않습니다.</p><p>이 예에서는 <code>docvalue_fields</code> 매개변수를 사용하여 전체 <code>_source</code> 문서를 로드하지 않고 <code>title</code>, <code>author</code>, <code>published</code> 필드를 검색합니다:</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "docvalue_fields": [
    "title",
    "author",
    {
      "field": "published",
      "format": "epoch_millis"
    }
  ],
  "_source": false
}<p>이 쿼리가 실행되면 Elasticsearch는 각 문서에 대해 <code>_source </code>을 참조하는 대신 온디스크 컬럼형 저장소에서 직접 값을 가져옵니다. <code>published</code> 필드는 쿼리에 제공된 <code>format</code> 매개변수 덕분에 기본 형식이 아닌 <code>epoch_millis</code> 형식으로 반환됩니다.</p><p><strong>저장된 필드:</strong> 매핑에서 특정 필드를 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">저장된</a> 것으로 명시적으로 표시한 경우 <code>stored_fields</code> 매개변수를 사용하여 해당 필드를 필터링할 수 있습니다. 특정 필드에 대해서만 가벼운 응답을 원하거나 나중에 검색할 수 있도록 의도적으로 저장한 필드에 대해 이 기능을 사용하면 유용합니다. <code>_source</code> 과 별도로 저장되므로 이 방법은 <code>_source</code> 을 로드할 필요가 없는 경우에도 유용합니다.</p><p>이 옵션은 기본적으로 꺼져 있으며 일반적으로 권장되지 않는다는 점에 유의하세요. 원본 소스 문서의 특정 하위 집합을 반환하려면 대신 소스 필터링을 사용하세요.</p><p>아래 예제 쿼리에서는 <code>stored_fields</code> 매개 변수를 사용하여 "<code>store”: true</code>" 인덱스 매핑 구성이 있는 <code>summary</code> 필드를 검색합니다.</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "stored_fields": ["summary"]
}<p>이 쿼리가 실행되면 Elasticsearch는 이 필드가 <code>”store”: true</code> 로 표시되어 있는지 확인하며, 이 필드를 찾지 못하면 필드를 완전히 건너뜁니다.</p><h2>3. 하위 필드 표시</h2><p>인덱스에 하위 필드가 포함된 경우 점 표기법을 사용하여 <code>fields</code> 매개변수에서 필드 경로를 지정할 수 있습니다. 하위 필드는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/nested">중첩된 필드 유형과</a> 다르다는 점에 유의하세요. 예를 들어 <code>address.city</code> 이라는 이름의 하위 필드가 있는 경우 다음과 같이 검색 응답에 포함할 수 있습니다:</p>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author", "address.city"],
  "_source": false
}<p>이 예제에서는 검색 응답에 <code>title</code>, <code>author</code>, <code>address.city</code> 필드의 값이 포함됩니다.</p><h2>4. 합성 _소스</h2><p><code> _source</code> 사용 기능을 유지하면서 디스크 공간도 절약하려면 인덱스 매핑에 합성 <code>_source</code> 을 사용하는 옵션이 있습니다. <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field#synthetic-source">합성 </a><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field#synthetic-source"><code>_source</code></a> 은 <code>_source</code> 이 비활성화되어 있는 경우에도 Elasticsearch가 저장된 필드 및 문서 값과 같은 기존 데이터로부터 <code>_source</code> 을 재구성할 수 있도록 하는 기능입니다. 이렇게 하면 재구성이 즉시 이루어지므로 쿼리 시 속도가 약간 느려지는 대신 저장 공간을 많이 절약할 수 있습니다. 인덱스 설정에서 아래 값을 사용하여 이 기능을 사용하도록 설정합니다:</p>PUT idx
{
  "settings": {
    "index": {
      "mapping": {
        "source": {
          "mode": "synthetic"
        }
      }
    }
  }
}<p><code>_search</code> API를 사용할 때 전체 문서 표시, 소스 필터링, <code>_source</code> 를 사용할 수 있을 것으로 기대하는 Kibana와 같은 다른 기능 및 도구와의 호환성, 전체 <code>_source</code> 문서를 저장할 필요가 없는 것 등이 합성 <code>_source </code>사용의 몇 가지 이점입니다.</p><h2>5. 런타임 필드</h2><p>런타임 <a href="https://www.elastic.co/docs/manage-data/data-store/mapping/runtime-fields">필드를</a> 사용하면 쿼리 시 또는 런타임 블록 아래의 인덱스 매핑에서 스크립트 필드를 정의할 수 있습니다. 이러한 필드는 색인화되지 않으므로 런타임 필드를 추가해도 색인 크기가 증가하지는 않지만 <code>_source</code> 에 표시되지 않습니다. 매핑에 정의된 런타임 필드는 영구적이며 모든 쿼리에서 사용할 수 있는 반면, 쿼리 시점에 정의된 런타임 필드는 임시적이며 해당 검색 요청에서만 사용할 수 있습니다.</p><p>런타임 필드 사용의 주요 이점은 이미 수집한 후 문서에 필드를 추가할 수 있어 매핑 결정을 간소화할 수 있다는 점입니다. 런타임 필드는 문자열 서식 지정이나 점수 계산과 같이 원본 문서에는 없지만 스크립트를 사용하여 생성된 값으로 문서를 보강하는 데도 유용합니다.</p><p>또한 런타임 필드는 결과 집합의 모든 문서에 대해 스크립트를 실행해야 하므로 성능이 저하될 수 있다는 점도 유의할 필요가 있습니다. <a href="https://www.elastic.co/docs/manage-data/data-store/mapping/retrieve-runtime-field">런타임 필드를 검색하려면</a> <code>_search</code> API에서 <code>fields</code> 매개 변수를 사용할 수도 있습니다.</p><h2>결론</h2><p>Elasticsearch 인덱스의 필드를 표시하는 방법은 인덱스 매핑 또는 <code>_source</code> 을 사용하여 단순히 값을 검색하는 것부터 <code>fields</code>, <code>docvalue_fields</code> 또는 제어 및 효율성을 높이기 위한 런타임 필드를 사용하는 고급 방법까지 다양합니다. 검색 환경을 최적화하려면 다양한 방법 간의 장단점을 이해하는 것이 중요합니다. 페이로드를 최적화하든, 문서를 보강하든, 저장 공간을 절약하기 위해 합성 <code>_source</code> 을 사용하든, Elasticsearch는 필요한 데이터를 필요한 방식으로 찾을 수 있는 여러 가지 도구와 기능을 제공합니다. 이러한 기법은 데이터 구조를 이해하고, 특정 필드를 식별하고, 문제를 해결하는 데 도움이 될 수 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-index-show-fields</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-index-show-fields</guid>
    <category><![CDATA[인덱스 데이터]]></category>
    <category><![CDATA[매핑]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd041e871a8935448/6a17de320b0bedf404dd34ab/23b96aaa1a38b1f4747b4a87695d816f24c0cf70-720x421.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[JavaScript, Mastra, Elasticsearch로 에이전트 RAG 어시스턴트 구축하기]]></title>
    <description><![CDATA[JavaScript 에코시스템에서 AI 에이전트를 구축하는 방법 알아보기]]></description>
    <content:encoded><![CDATA[<p>이 아이디어는 열띤 경쟁이 벌어지는 판타지 농구 리그에 참여하던 중 떠올랐습니다. 궁금했습니다: <em>주간 매치업에서 우위를 점하는 데 도움이 되는 AI 에이전트를 만들 수 있을까요? 물론 가능합니다!</em></p><p>이 글에서는 <a href="https://mastra.ai/en/docs">Mastra와</a> 경량 JavaScript 웹 애플리케이션을 사용하여 에이전트 RAG 어시스턴트를 구축하여 상호 작용하는 방법을 살펴봅니다. 이 에이전트를 Elasticsearch에 연결하면 구조화된 플레이어 데이터에 액세스하고 실시간 통계 집계를 실행할 수 있는 기능을 제공하여 플레이어 통계에 기반한 추천을 제공할 수 있습니다. GitHub <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">리포지토리로</a> 이동하여 애플리케이션을 직접 복제하고 실행하는 방법에 대한 지침을 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">읽어보세요</a>. </p><p>모든 것이 합쳐지면 어떤 모습일까요?</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63ea3e7a09306fbf/6a17f1d97f6f150e22c09c50/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" alt="" /><p>참고: 이 블로그 게시물은 "<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">AI SDK와 Elastic으로 AI 에이전트 구축하기</a>"를 기반으로 합니다. AI 에이전트를 처음 접하고 어떤 용도로 사용할 수 있는지 궁금하다면 여기서부터 시작하세요.
</p><h2><strong>아키텍처 개요</strong></h2><p>시스템의 핵심에는 에이전트의 추론 엔진(두뇌) 역할을 하는 대규모 언어 모델(LLM)이 있습니다. 사용자 입력을 해석하고, 호출할 도구를 결정하며, 관련 응답을 생성하는 데 필요한 단계를 오케스트레이션합니다.</p><p>에이전트 자체는 자바스크립트 에코시스템의 에이전트 프레임워크인 Mastra에 의해 스캐폴드됩니다. Mastra는 LLM을 백엔드 인프라로 래핑하여 API 엔드포인트로 노출하고 도구, 시스템 프롬프트 및 에이전트 동작을 정의하기 위한 인터페이스를 제공합니다.</p><p>프론트엔드에서는 <a href="https://vite.dev/guide/">Vite를</a> 사용하여 에이전트에게 쿼리를 보내고 응답을 받기 위한 채팅 인터페이스를 제공하는 React 웹 애플리케이션을 빠르게 스캐폴딩합니다.</p><p>마지막으로 에이전트가 쿼리하고 집계할 수 있는 플레이어 통계 및 매치업 데이터를 저장하는 Elasticsearch가 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte13f09493f217047/6a17f1db1d1b83178d93e546/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" alt="" /><h2><strong>배경</strong></h2><p>몇 가지 기본 개념을 살펴보겠습니다:</p><h3><strong>에이전트 RAG란 무엇인가요?</strong></h3><p>AI 에이전트는 다른 시스템과 상호 작용하고, 독립적으로 작동하며, 정의된 매개변수에 따라 작업을 수행할 수 있습니다. 에이전틱 RAG는 AI 에이전트의 자율성과 검색 증강 생성의 원칙을 결합하여 LLM이 응답을 생성하기 위해 호출할 도구와 컨텍스트로 사용할 데이터를 선택할 수 있도록 합니다. <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">여기에서</a> RAG에 대해 자세히 알아보세요.</p><h3><strong>프레임워크 선택, 왜 AI-SDK를 넘어야 할까요?</strong></h3><p>사용 가능한 AI 에이전트 프레임워크는 여러 가지가 있으며, <a href="https://www.elastic.co/search-labs/blog/using-crewai-with-elasticsearch">CrewAI</a>, <a href="https://www.elastic.co/search-labs/blog/using-autogen-with-elasticsearch">AutoGen</a>, <a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph와</a> 같은 인기 있는 프레임워크에 대해 들어보셨을 것입니다. 이러한 프레임워크는 대부분 다양한 모델 지원, 도구 사용, 메모리 관리 등 공통된 기능 세트를 공유합니다.</p><p>다음은 해리슨 체이스(LangChain의 CEO)가 작성한 프레임워크 <a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">비교표입니다</a>.</p><p>Mastra에 관심을 갖게 된 이유는 풀스택 개발자가 에이전트를 에코시스템에 쉽게 통합할 수 있도록 구축된 자바스크립트 우선 프레임워크라는 점입니다. Vercel의 AI-SDK도 이 작업의 대부분을 수행하지만, 프로젝트에 더 복잡한 에이전트 워크플로가 포함될 때 Mastra가 빛을 발합니다. Mastra는 AI-SDK에서 설정한 기본 패턴을 개선하며, 이 프로젝트에서는 이 두 가지를 함께 사용할 예정입니다.</p><h3><strong>프레임워크 및 모델 선택 고려 사항</strong></h3><p>이러한 프레임워크는 AI 에이전트를 빠르게 구축하는 데 도움이 될 수 있지만 고려해야 할 몇 가지 단점이 있습니다. 예를 들어, AI 에이전트나 일반적인 추상화 계층이 아닌 다른 프레임워크를 사용하면 제어권을 약간 잃게 됩니다. LLM이 도구를 올바르게 사용하지 않거나 원치 않는 작업을 수행하는 경우 추상화로 인해 디버깅이 더 어려워집니다. 하지만 제 생각에는 이러한 프레임워크가 모멘텀을 얻고 있고 지속적으로 반복되고 있기 때문에 구축할 때 얻을 수 있는 쉽고 빠른 속도를 고려하면 이러한 절충안은 가치가 있습니다.</p><p>다시 말하지만, 이러한 프레임워크는 모델에 구애받지 않으므로 다양한 모델을 플러그 앤 플레이할 수 있으며, 학습된 데이터 세트에 따라 모델이 달라지고 그에 따라 응답도 달라진다는 점을 기억하세요. 일부 모델은 도구 호출도 지원하지 않습니다. 따라서 여러 모델을 전환하고 테스트하여 어떤 모델이 가장 좋은 응답을 제공하는지 확인할 수 있지만 각 모델에 대해 시스템 프롬프트를 다시 작성해야 할 가능성이 높다는 점에 유의하세요. 예를 들어, Llama3.3 사용 보다 훨씬 더 많은 프롬프트와 구체적인 지침을 통해 원하는 응답을 얻을 수 있습니다.</p><h3><strong>NBA 판타지 농구</strong></h3><p>판타지 농구는 친구 그룹과 함께 리그를 시작하는 것으로, 보통 약간의 돈을 걸고 진행합니다(그룹의 경쟁 정도에 따라 우정의 상태에 영향을 미칠 수 있습니다). 그런 다음 각자 10명의 플레이어로 팀을 구성하여 매주 번갈아 가며 다른 친구의 10명의 플레이어와 경쟁합니다. 전체 점수에 기여하는 포인트는 주어진 주에 각 플레이어가 상대 플레이어를 상대로 어떤 성적을 거두었는지에 따라 결정됩니다.</p><p>팀에 소속된 선수가 부상, 출장 정지 등을 당한 경우 팀에 추가할 수 있는 자유 계약 선수 목록이 있습니다. 판타지 스포츠에서는 픽업 횟수가 제한되어 있고 모두가 최고의 선수를 뽑기 위해 끊임없이 노력하기 때문에 이 부분에서 많은 고민이 일어납니다.</p><p>특히 어떤 선수를 뽑을지 빠르게 결정해야 하는 상황에서 NBA AI 어시스턴트가 빛을 발하는 부분입니다. 어시스턴트는 플레이어가 특정 상대와의 경기력을 수동으로 조회할 필요 없이 해당 데이터를 빠르게 찾아 평균을 비교하여 정보에 입각한 추천을 제공합니다.</p><p>이제 에이전트 RAG와 NBA 판타지 농구에 대한 기본 사항을 알아봤으니 실제로 확인해 보겠습니다.</p><h2><strong>프로젝트 구축</strong></h2><p>어느 지점에서 막히거나 처음부터 빌드하고 싶지 않다면 <a href="https://github.com/jdarmada/nba-ai-assistant-js.git">리포지토리를</a> 참조하세요.</p><h3><strong>다룰 내용</strong></h3><ol><li><p><strong>프로젝트 발판 마련하기:</strong></p><ol><li><p><strong>백엔드(마스트라):</strong> 백엔드를 스캐폴드하고 에이전트 로직을 정의하려면 npx create mastra@latest를 사용하세요.</p></li><li><p><strong>프런트엔드(Vite + React):</strong> 상담원과 상호작용할 수 있는 프론트엔드 채팅 인터페이스를 구축하려면 npm create vite@latest를 사용하세요.</p></li></ol></li><li><p><strong>환경 변수 설정</strong></p><ol><li><p>환경 변수를 관리하려면 dotenv를 설치하세요.</p></li><li><p>.env 파일 만들기 파일을 열고 필요한 변수를 입력합니다.</p></li></ol></li><li><p><strong>Elasticsearch 설정</strong></p><ol><li><p>(로컬 또는 클라우드에서) Elasticsearch 클러스터를 스핀업합니다.</p></li><li><p>공식 Elasticsearch 클라이언트를 설치합니다.</p></li><li><p>환경 변수에 액세스할 수 있는지 확인합니다.</p></li><li><p>클라이언트에 연결을 설정합니다.</p></li></ol></li><li><p><strong>NBA 데이터를 Elasticsearch로 대량 수집하기</strong></p><ol><li><p>적절한 매핑으로 인덱스를 생성하여 집계를 활성화합니다.</p></li><li><p>CSV 파일에서 플레이어 게임 통계를 Elasticsearch 인덱스로 일괄 수집합니다.</p></li></ol></li><li><p><strong>Elasticsearch 집계 정의</strong></p><ol><li><p>특정 상대에 대한 과거 평균을 계산하기 위한 쿼리입니다.</p></li><li><p>특정 상대에 대한 시즌 평균을 계산하기 위한 쿼리입니다.</p></li></ol></li><li><p><strong>플레이어 비교 유틸리티 파일</strong></p><ol><li><p>도우미 기능과 Elasticsearch 집계를 통합합니다.</p></li></ol></li><li><p><strong>에이전트 구축하기</strong></p><ol><li><p>상담원 정의 및 시스템 프롬프트를 추가합니다.</p></li><li><p>zod를 설치하고 도구를 정의합니다.</p></li><li><p>CORS를 처리하기 위한 미들웨어 설정을 추가합니다.</p></li></ol></li><li><p><strong>프론트엔드 통합</strong></p><ol><li><p>AI-SDK의 useChat을 사용하여 상담원과 상호 작용합니다.</p></li><li><p>적절한 형식의 대화를 담을 수 있는 UI를 만듭니다.</p></li></ol></li><li><p><strong>애플리케이션 실행</strong></p><ol><li><p>백엔드(Mastra 서버)와 프론트엔드(React 앱)를 모두 시작하세요.</p></li><li><p>샘플 쿼리 및 사용법.</p></li></ol></li><li><p><strong>다음 단계 에이전트의 지능 향상</strong></p><ol><li><p>시맨틱 검색 기능을 추가하여 더욱 통찰력 있는 추천을 지원합니다.</p></li><li><p>검색 로직을 Elasticsearch MCP(모델 컨텍스트 프로토콜) 서버로 이동하여 동적 쿼리를 활성화합니다.</p></li></ol></li></ol><h3><strong>필수 구성 요소</strong></h3><ul><li><p><strong>Node.js 및 npm</strong>: 백엔드와 프론트엔드 모두 Node에서 실행됩니다. Node 18+ 및 npm v9+가 설치되어 있는지 확인합니다(Node 18+에 번들로 제공됨).</p></li><li><p><strong>Elasticsearch 클러스터:</strong> 로컬 또는 클라우드에 있는 활성 Elasticsearch 클러스터입니다.</p></li><li><p><strong>OpenAI API 키</strong>: <a href="https://platform.openai.com/api-keys">OpenAI 개발자 포털의</a> API 키 페이지에서 생성하세요.</p></li></ul><p></p><h3><strong>프로젝트 구조</strong></h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt749baa120552e4ab/6a17f1dd1d1b83bfe993e54a/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" alt="" /><h4><strong>1단계: 프로젝트 발판 마련</strong></h4><ol><li><p>먼저 nba-ai-assistant-js 디렉토리를 생성하고 다음을 사용하여 내부로 이동합니다: </p></li></ol>mkdir nba-ai-assistant-js &amp;&amp; cd nba-ai-assistant-js<p><strong>백엔드:</strong></p><ol><li><p>다음 명령과 함께 Mastra 생성 도구를 사용합니다: </p></li></ol>npx create-mastra@latest<p>2. 터미널에 몇 가지 메시지가 표시되는데, 첫 번째 메시지는 프로젝트 백엔드의 이름을 지정하는 것입니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt65abf68fe588e968/6a17f1de63baff2814741d5b/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" alt="" /><p>3. 다음으로, Mastra 파일을 저장하는 기본 구조를 유지하므로 <code>src/</code> 을 입력합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89bd829fcf0ae6b9/6a17f1e04b055dd30e432302/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" alt="" /><p>4. 그런 다음 기본 LLM 공급자로 OpenAI를 선택합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd167cc77a40b9a8/6a17f1e11480099e29b48863/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" alt="" /><p>5. 마지막으로 OpenAI API 키를 입력하라는 메시지가 표시됩니다. 지금은 건너뛰는 옵션을 선택하고 나중에<code> .env</code> 파일로 제공하겠습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12654151ed495370/6a17f1e22f4a5c0f84fa89f9/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" alt="" /><p><strong>프론트엔드:</strong></p><ol><li><p>다시 루트 디렉토리로 이동하여 다음 명령을 사용하여 <a href="https://vite.dev/guide/">Vite 생성 도구를</a> 실행합니다: <code>npm create vite@latest frontend -- --template react</code></p></li></ol><p>이렇게 하면 React용 특정 템플릿이 포함된 <code>frontend</code> 이라는 이름의 경량 React 앱이 생성됩니다.</p><p>모든 것이 정상적으로 진행되었다면 프로젝트 디렉토리 내부에 Mastra 코드가 있는 백엔드 디렉토리와 React 앱이 있는 <code>frontend</code> 디렉토리가 있을 것입니다.</p><p></p><h4><strong>2단계: 환경 변수 설정</strong></h4><ol><li><p>민감한 키를 관리하기 위해 <code>dotenv</code> 패키지를 사용하여 .env에서 환경 변수를 로드합니다. 파일을 만듭니다. 백엔드 디렉토리로 이동하여 <code>dotenv</code> 을 설치합니다:</p></li></ol>cd backend
npm install dotenv --save<p>2. 백엔드 디렉토리에 적절한 변수를 입력할 수 있는 example.env 파일이 제공됩니다. 직접 생성하는 경우 다음 변수를 반드시 포함하세요:</p># OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here

# Elasticsearch Configuration
ELASTIC_ENDPOINT=your_elasticsearch_endpoint_here
ELASTIC_API_KEY=your_elasticsearch_api_key_here
<p></p><p>참고: <code>.gitignore</code> 에 <code>.env</code> 을 추가하여 이 파일이 버전 관리에서 제외되었는지 확인하세요.</p><h4><strong>3단계: Elasticsearch 설정하기</strong></h4><p>먼저 활성 Elasticsearch 클러스터가 필요합니다. 두 가지 옵션이 있습니다:</p><ul><li><p><strong>옵션 A: Elasticsearch Cloud 사용</strong></p><ul><li><p><a href="https://cloud.elastic.co/registration">Elastic Cloud에</a>가입하기</p></li><li><p>새 배포 만들기</p></li><li><p>엔드포인트 URL 및 API 키(인코딩된) 가져오기</p></li></ul></li><li><p><strong>옵션 B: 로컬에서 Elasticsearch 실행하기</strong></p><ul><li><p>로컬에 Elasticsearch 설치 및 실행</p></li><li><p>엔드포인트로 http://localhost:9200 사용</p></li><li><p>API 키 생성</p></li></ul></li></ul><p></p><p><strong>백엔드에 Elasticsearch 클라이언트 설치하기:</strong></p><ol><li><p>먼저, 백엔드 디렉터리에 공식 Elasticsearch 클라이언트를 설치합니다:</p></li></ol>npm install @elastic/elasticsearch<p>2. 그런 다음 재사용 가능한 함수를 저장할 디렉터리 라이브러리를 만들고 그 안으로 이동합니다:</p>mkdir lib &amp;&amp; cd lib<p>3. 내부에 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js라는</a> 새 파일을 생성합니다. 이 파일은 Elasticsearch 클라이언트를 초기화하고 프로젝트 전체에서 사용할 수 있도록 노출합니다.</p><p>4. ECMAScript 모듈(ESM)을 사용하고 있으므로 __dirname and __파일명을 사용할 수 없습니다. .env 파일에서 환경 변수가 올바르게 로드되었는지 확인하려면 파일에서 이 설정을 파일 맨 위에 추가합니다:</p>import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { Client } from '@elastic/elasticsearch';

// Grab current directory and load .env from backend folder
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = join(__dirname, '../.env');

// Load environment variables from the correct path
config({ path: envPath });<p>5. 이제 환경 변수를 사용하여 Elasticsearch 클라이언트를 초기화하고 연결을 확인합니다:</p>//Elastic client Initialization, make sure environment variables are being loaded in correctly
const config= {
    node: `${process.env.ELASTIC_ENDPOINT}`,
    auth: {
        apiKey: `${process.env.ELASTIC_API_KEY}`,
    },
};

export const elasticClient = new Client(config);

//Check if the client is connected
async function checkConnection() { 
    try {
        const info = await elasticClient.info();
        console.log('Elasticsearch is connected:', info);
    } catch (error) {
        console.error('Elasticsearch connection error:', error);
    }
}

checkConnection();
<p>이제 이 클라이언트 인스턴스를 Elasticsearch 클러스터와 상호 작용해야 하는 모든 파일로 가져올 수 있습니다.</p><p></p><h4><strong>4단계: NBA 데이터를 Elasticsearch로 대량 수집하기</strong></h4><p><strong>데이터 세트:</strong></p><p>이 프로젝트에서는 리포지토리의 <a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">백엔드/데이터</a> 디렉터리에서 사용 가능한 데이터 세트를 참조하겠습니다. NBA 어시스턴트는 이 데이터를 지식 베이스로 사용하여 통계 비교를 실행하고 추천을 생성합니다.</p><ul><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/sample_nba_data.csv">sample_player_game_stats.csv</a> - 선수의 전체 NBA 커리어에 대한 경기당 득점, 리바운드, 스틸 등 선수별 게임 통계 샘플입니다. 이 데이터 집합을 사용하여 집계를 수행합니다. (참고: 이 데이터는 데모 목적으로 미리 생성된 모의 데이터이며 공식 NBA 소스에서 가져온 것이 아닙니다.)</p></li><li><p><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a> - 에이전트가 플레이어 및 팀 이름을 ID와 일치시킬 수 있도록 일반적으로 API 호출을 통해 제공되는 플레이어 및 팀 메타데이터를 대체합니다. 샘플 데이터를 사용하기 때문에 외부 API에서 가져오는 데 따른 오버헤드를 피하기 위해 에이전트가 참조할 수 있는 몇 가지 값을 하드코딩했습니다.</p></li></ul><p></p><p><strong>구현:</strong></p><ol><li><p><code>backend/lib</code> 디렉터리에서 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">플레이어데이터인입.js라는</a> 파일을 생성합니다.</p></li><li><p>가져오기를 설정하고, CSV 파일 경로를 확인하고, 구문 분석을 설정합니다. 다시 말하지만, ESM을 사용하고 있으므로 <code>__dirname</code> 을 재구성하여 샘플 CSV의 경로를 확인해야 합니다. 또한 <a href="http://node.js/">Node.js의</a> 기본 제공 모듈인 <code>fs</code> 및 <code>readline</code> 을 사용하여 주어진 CSV 파일을 한 줄씩 구문 분석합니다.</p></li></ol>import fs from 'fs';
import readline from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
import { elasticClient } from './elasticClient.js';

const indexName = 'sample-nba-player-data'; //Replace with your preferred index name

//Since we are using ES modules __dirname and __filename don't exist, so this is a workaround that allows us to use the absolute file path for our sample data.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.resolve(__dirname, '../data/sample_nba_data.csv');<p>이렇게 하면 대량 수집 단계에 도달했을 때 CSV를 효율적으로 읽고 구문 분석할 수 있습니다.</p><p>3. 적절한 매핑으로 인덱스를 생성합니다. Elasticsearch는 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">동적 매핑을</a> 통해 필드 유형을 자동으로 추론할 수 있지만, 여기서는 각 통계가 숫자 필드로 처리되도록 명시적으로 처리하고자 합니다. 이 필드는 나중에 집계에 사용할 것이므로 중요합니다. 또한 득점, 리바운드 등과 같은 통계에는 <code>float </code>유형을 사용하여 소수점 값을 포함하도록 합니다. 마지막으로, 매핑 속성 <code>dynamic: 'strict'</code> 을 추가하여 Elasticsearch가 인식할 수 없는 필드를 동적으로 매핑하지 않도록 하려고 합니다. 
</p>// Function to create an index with mappings
async function createIndex() {
    try {
        // Check if the index already exists
        const exists = await elasticClient.indices.exists({ index: indexName });

        if (exists) {
            console.log(`Index "${indexName}" already exists, deleting it now.`);
            await elasticClient.indices.delete({ index: indexName });
            console.log(`Deleted index "${indexName}".`);
        }
        // Create the index with mappings
        const response = await elasticClient.indices.create({
            index: indexName,
            body: {
                mappings: {
                    dynamic: 'strict', // Prevent dynamic mapping
                    properties: {
                        game_id: { type: 'integer' },
                        game_date: { type: 'date' },
                        player_id: { type: 'integer' },
                        player_full_name: { type: 'text' },
                        player_team_id: { type: 'integer' },
                        player_team_name: { type: 'text' },
                        home_team: { type: 'boolean' },
                        opponent_team_id: { type: 'integer' },
                        opponent_team_name: { type: 'text' },
                        points: { type: 'float' },
                        rebounds: { type: 'float' },
                        assists: { type: 'float' },
                        steals: { type: 'float' },
                        blocks: { type: 'float' },
                        fg_percentage: { type: 'float' },
                        minutes_played: { type: 'float' },
                    },
                },
            },
        });

        console.log('Index created:', response);
        return true;
    } catch (error) {
        console.error('Error creating index:', error);
        return false;
    }
}
<p>4. CSV 데이터를 Elasticsearch 인덱스에 대량 수집하는 기능을 추가합니다. 코드 블록 내부에서는 헤더 줄을 건너뜁니다. 그런 다음 각 줄 항목을 쉼표로 분할하고 문서 개체에 밀어 넣습니다. 이 단계는 또한 이들을 정리하고 올바른 유형인지 확인합니다. 다음으로, 문서를 인덱스 정보와 함께 bulkBody 배열에 밀어넣고, 이 배열은 Elasticsearch로의 대량 수집을 위한 페이로드 역할을 하게 됩니다.</p>async function bulkIngestCsv(filePath) {
    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
        input: readStream,
        crlfDelay: Infinity,
    });

    const bulkBody = [];
    let lineNum = 0;

    //Skip the header line
    let headerLine = true;
    for await (const line of rl) {
        if (headerLine) {
            headerLine = false;
            continue;
        }
        lineNum++;

        // Split the line by comma and remove whitespace
        const [
            game_id,
            game_date,
            player_id,
            player_full_name,
            player_team_id,
            player_team_name,
            home_team,
            opponent_team_id,
            opponent_team_name,
            points,
            rebounds,
            assists,
            steals,
            blocks,
            fg_percentage,
            minutes_played,
        ] = line.split(',');

        // Create a document object
        const document = {
            game_id: parseInt(game_id),
            game_date: game_date.trim(),
            player_id: parseInt(player_id),
            player_full_name: player_full_name.trim(),
            player_team_id: parseInt(player_team_id),
            player_team_name: player_team_name.trim(),
            home_team: home_team.trim() === 'True', // Converts True/False into a boolean
            opponent_team_id: parseInt(opponent_team_id),
            opponent_team_name: opponent_team_name.trim(),
            points: parseFloat(points),
            rebounds: parseFloat(rebounds),
            assists: parseFloat(assists),
            steals: parseFloat(steals),
            blocks: parseFloat(blocks),
            fg_percentage: parseFloat(fg_percentage),
            minutes_played: parseFloat(minutes_played),
        };

        // Prepare the bulk operation format
        bulkBody.push({ index: { _index: indexName } });
        bulkBody.push(document);
    }

    console.log(`Parsed ${lineNum} lines from CSV`);
<p>5. 그런 다음 <code>elasticClient.bulk()</code> 와 함께 Elasticsearch의 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">대량 API를</a> 사용하여 단일 요청으로 여러 문서를 수집할 수 있습니다. 아래의 오류 처리는 수집에 실패한 문서 수와 수집에 성공한 문서 수를 알려주는 구조로 되어 있습니다.</p>try {
        // Perform the bulk request
        const response = await elasticClient.bulk({ body: bulkBody });

        if (response.errors) {
            console.log('Bulk Ingestion had some hiccups:');

            // Count successful vs failed operations
            let successCount = 0;
            let errorCount = 0;
            const errorDetails = [];

            response.items.forEach((item, index) =&gt; {
                const operation = item.index || item.create || item.update || item.delete;
                if (operation.error) {
                    errorCount++;
                    errorDetails.push({
                        document: index + 1,
                        error: operation.error,
                    });
                } else {
                    successCount++;
                }
            });

            console.log(`Successfully indexed: ${successCount} documents`);
            console.log(`Failed to index: ${errorCount} documents, here are the details`, errorDetails);

        } else {
            console.log(`Bulk Ingestion fully successful!`);
        }

    } catch (error) {
        console.error('Error performing bulk ingestion:', error);
    }
}
<p>6. 아래 <code>main()</code> 함수를 실행하여 <code>createIndex()</code> 및 <code>bulkIngestCsv()</code> 함수를 순차적으로 실행합니다.</p>// Run this function
async function main() {
    const result = await createIndex();
    if (!result) {
        console.error('Index setup failed. Aborting.');
        return;
    }

    await bulkIngestCsv(filePath);
    console.log('Bulk ingestion completed!');
}

main();
<p>대량 수집이 성공했다는 콘솔 로그가 표시되면, Elasticsearch 인덱스에서 빠른 확인을 수행하여 문서가 실제로 성공적으로 수집되었는지 확인합니다.</p><h4><strong>5단계: Elasticsearch 집계 정의 및 통합하기</strong></h4><p>이는 플레이어의 통계를 서로 비교하기 위해 AI 에이전트의 도구를 정의할 때 사용되는 주요 기능이 될 것입니다.</p><p>1. <code>backend/lib</code> 디렉터리로 이동하여 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js라는</a> 파일을 생성합니다.</p><p>2. 아래 쿼리를 추가하여 특정 상대에 대한 플레이어의 기록 평균을 계산합니다. 이 쿼리는 <code>player_id</code> 과 일치하는 조건과 <code>opponent_team_id</code> 의 두 가지 조건이 포함된 <code>bool</code> <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">필터를</a> 사용하여 관련 게임만 검색합니다. 우리는 문서를 반환할 필요가 없고 집계만 중요하므로 <code>size:0</code> 로 설정합니다. <code>aggs</code> 블록에서는 <code>points, rebounds, assists, steals, blocks</code> 및 <code>fg_percentage</code> 같은 필드에서 여러 메트릭 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">집계를</a> 병렬로 실행하여 평균값을 계산합니다. LLM은 계산에 적중하거나 실패할 수 있으며, 이렇게 하면 해당 프로세스가 Elasticsearch로 오프로드되어 NBA AI 어시스턴트가 정확한 데이터에 액세스할 수 있습니다.</p>export async function getHistoricalAveragesAgainstOpponent(player_id, opponent_team_id) {
    try {
        //Query for Historical Averages
        const historicalQuery = await elasticClient.search({
            index: 'sample-nba-player-data', 
            size: 0,
            query: {
                bool: {
                    must: [
                        {
                            term: {
                                player_id: {
                                    value: player_id,
                                },
                            },
                        },
                        {
                            term: {
                                opponent_team_id: {
                                    value: opponent_team_id,
                                },
                            },
                        },
                    ],
                },
            },
            aggs: {
                avg_points: { avg: { field: 'points' } },
                avg_rebounds: { avg: { field: 'rebounds' } },
                avg_assists: { avg: { field: 'assists' } },
                avg_steals: { avg: { field: 'steals' } },
                avg_blocks: { avg: { field: 'blocks' } },
             avg_fg_percentage: { avg: { field: 'fg_percentage' } },
            },
        });

        return {
            points: historicalQuery.aggregations.avg_points.value || 0,
            rebounds: historicalQuery.aggregations.avg_rebounds.value || 0,
            assists: historicalQuery.aggregations.avg_assists.value || 0,
            steals: historicalQuery.aggregations.avg_steals.value || 0,
            blocks: historicalQuery.aggregations.avg_blocks.value || 0,
            fgPercentage: historicalQuery.aggregations.avg_fg_percentage.value || 0,
        };
    } catch (error) {
        console.error('Query error from getHistoricalAveragesAgainstOpponent function:', error);
        return { error: 'Queries failed in getting historical averages against opponent.' };
    }
}
<p>3. 특정 상대를 상대로 한 플레이어의 시즌 평균을 계산하기 위해 과거 기록과 거의 동일한 쿼리를 사용합니다. 이 쿼리의 유일한 차이점은 <code>bool</code> 필터에 <code>game_date</code> 에 대한 추가 조건이 있다는 것입니다. <code>game_date</code> 필드는 현재 NBA 시즌 범위 내에 있어야 합니다. 이 경우 범위는 <code>2024-10-01</code> 에서 <code>2025-06-30</code> 사이입니다. 아래의 추가 조건은 다음 집계에서 이번 시즌의 게임만 격리되도록 합니다.
</p>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
<h4><strong>6단계: 플레이어 비교 유틸리티</strong></h4><p>코드를 모듈식으로 유지 관리하기 위해 메타데이터 도우미 함수와 Elasticsearch 집계를 통합하는 유틸리티 파일을 만들겠습니다. 이렇게 하면 상담원이 사용하는 기본 도구에 전원이 공급됩니다. 이에 대해서는 나중에 자세히 설명합니다:</p><p>1. <code>backend/lib</code> 디렉토리에 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">비교플레이어.js</a> 파일을 새로 만듭니다.</p><p>2. 아래 함수를 추가하여 메타데이터 헬퍼와 Elasticsearch 집계 로직을 에이전트가 사용하는 기본 도구를 강화하는 단일 함수로 통합하세요.
</p>import { playersByName } from '../data/playerAndTeamInfo.js';
import { teamsByName } from '../data/playerAndTeamInfo.js';
import { upcomingMatchups } from '../data/playerAndTeamInfo.js';
import { getHistoricalAveragesAgainstOpponent } from './elasticAggs.js';
import { getSeasonAveragesAgainstOpponent } from './elasticAggs.js';

//Simple helper functions to simulate API calls for player and team metadata. These reference the hardcoded values from playerAndTeamInfo.js in the data directory
export function getPlayerInfo(playerFullName) {
    return playersByName[playerFullName];
}

export function getTeamID(teamFullName) {
    return teamsByName[teamFullName];
}

export function getUpcomingMatchups(teamId) {
    return upcomingMatchups[teamId];
}

//Main function used by the 'playerComparisonTool' agent tool
export async function comparePlayersForNextMatchup(player1Name, player2Name) {
    //Get Player Info
    const player1Info = getPlayerInfo(player1Name);
    const player2Info = getPlayerInfo(player2Name);

    //Get upcoming matchups
    const player1NextGame = getUpcomingMatchups(player1Info.team_id)[0];
    const player2NextGame = getUpcomingMatchups(player2Info.team_id)[0];

    //Get season and historical averages against next opponent for player 1
    const player1SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );
    const player1HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );

    //Get season and historical averages against next opponent for player 2
    const player2SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );
    const player2HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );

    const player1 = {
        name: player1Name,
        playerId: player1Info.player_id,
        teamId: player1Info.team_id,
        nextOpponent: {
            teamId: player1NextGame.opponent_team_id,
            teamName: player1NextGame.opponent_team_name,
            home: player1NextGame.home,
        },
        stats: {
            seasonAverages: player1SeasonAverages,
            historicalAverages: player1HistoricalAverages,
        },
    };

    const player2 = {
        name: player2Name,
        playerId: player2Info.player_id,
        teamId: player2Info.team_id,
        nextOpponent: {
            teamId: player2NextGame.opponent_team_id,
            teamName: player2NextGame.opponent_team_name,
            home: player2NextGame.home,
        },
        stats: {
            seasonAverages: player2SeasonAverages,
            historicalAverages: player2HistoricalAverages,
        },
    };

    return [player1, player2];
}
<h4><strong>7단계: 에이전트 구축하기</strong></h4><p>이제 프론트엔드 및 백엔드 스캐폴딩을 생성하고, NBA 경기 데이터를 수집하고, Elasticsearch에 대한 연결을 설정했으므로 에이전트 구축을 위해 모든 조각을 조합하기 시작할 수 있습니다.</p><p><strong>상담원 정의하기</strong></p><p>1. <code>backend/src/mastra/agents</code> 디렉터리 내의 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a> 파일로 이동하여 상담원 정의를 추가합니다. 다음과 같은 필드를 지정할 수 있습니다:</p><ul><li><p><strong>이름:</strong> 이름: 프런트엔드에서 상담원을 호출할 때 참조할 수 있는 이름을 지정합니다.</p></li><li><p><strong>지침/시스템 프롬프트: </strong>시스템 프롬프트는 상호 작용 중에 따라야 할 초기 컨텍스트와 규칙을 LLM에 제공합니다. 사용자가 채팅창을 통해 보내는 메시지와 비슷하지만, 이 메시지는 사용자가 입력하기 전에 먼저 표시됩니다. 이 역시 선택한 모델에 따라 달라집니다.</p></li><li><p><strong>모델:</strong> 사용할 LLM(Mastra는 OpenAI, Anthropic, 로컬 모델 등을 지원합니다).</p></li><li><p><strong>도구:</strong> 도구: 상담원이 호출할 수 있는 도구 기능의 목록입니다.</p></li><li><p><strong>메모리:</strong> (선택 사항) 상담원이 대화 기록 등을 기억하도록 하려는 경우 선택합니다. 간단하게 하기 위해 영구 메모리 없이 시작할 수 있지만, Mastra는 이를 지원합니다.</p></li></ul><p></p>import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { playerComparisonTool } from '../tools';

export const basketballAgent = new Agent({
    name: 'Basketball Agent',
    instructions: `
      You are a NBA Basketball expert.
      Your primary function is to compare two NBA players and recommend which one is the better fantasy pickup.

      Only compare players from the following list:
      - LeBron James
      - Stephen Curry
      - Jayson Tatum
      - Jaylen Brown
      - Nikola Jokic
      - Luka Doncic
      - Kyrie Irving
      - Anthony Davis
      - Kawhi Leonard
      - Russell Westbrook

      Input Handling Rules:
      - If the user asks about a player that is not on this list, respond with the list of available players for comparison.
      - If the user only inputs one player, ask the user to add another player from the list provided.
      - If the user inputs a player with the wrong spelling or capitalizations, infer from the list of available players provided.
      - IMPORTANT: If the user asks a question or asks you to generate a response about anything outside of basketball or the scope of this project, DO NOT answer and affirm you can only talk about basketball.

      Tool Usage:
      - Extract and standardize player names to match the list exactly.
      - Use the playerComparisonTool, passing both names as strings.
      - The tool will return an object with game information, stats, and analysis.

      Format your response using Markdown syntax. Use:

        Example output format:

       
        #### Next Game Info
        - ***LeBron James** vs Warriors, May 24 (Home)  
        - ***Stephen Curry** vs Lakers, May 24 (Away)


        #### Stats Comparison  
        \`\`\`  
        Stat                  LeBron James (vs Warriors)    Stephen Curry (vs Lakers)  
        --------------------  -----------------------------  ----------------------------  
        Historical Points     28.3                          30.3  
        Historical Assists    6.7                           8.7  
        Season Points         28.8                          23.3  
        Season Assists        6.2                           4.7  
        \`\`\`

        #### Fantasy Recommendation  
        Explain which player is the better fantasy pickup and why.
      
    `,
    model: openai('gpt-4o'),
    tools: { playerComparisonTool },
});
<p><strong>
도구 정의</strong></p><ol><li><p><code>backend/src/mastra/tools</code> 디렉터리 내의 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a> 파일로 이동합니다.</p></li><li><p>명령을 사용하여 Zod를 설치합니다:</p></li></ol>npm install zod<p>3. 도구 정의를 추가합니다. <code>comparePlayers.js</code> 파일 내의 함수를 에이전트가 이 도구를 호출할 때 사용할 기본 함수로 가져옵니다. Mastra의 <code>createTool()</code> 기능을 사용하여 <code>playerComparisonTool</code> 을 등록합니다. 필드에는 다음이 포함됩니다:</p><ul><li><p><code>id</code>: 상담원이 도구의 기능을 이해하는 데 도움이 되는 자연어 설명입니다.</p></li><li><p><code>input schema</code>: 도구의 입력 형태를 정의하기 위해 Mastra는 타입스크립트 스키마 유효성 검사 라이브러리인 <a href="https://zod.dev/">Zod</a> 스키마를 사용합니다. Zod는 에이전트가 올바른 구조의 입력을 입력하는지 확인하고 입력 구조가 일치하지 않으면 도구가 실행되지 않도록 방지하여 도움을 줍니다.</p></li><li><p><code>description</code>: 상담원이 언제 전화를 걸어 도구를 사용해야 하는지 이해하는 데 도움이 되는 자연어 설명입니다.</p></li><li><p><code>execute</code>: 도구가 호출될 때 실행되는 로직입니다. 저희의 경우 가져온 헬퍼 함수를 사용하여 성능 통계를 반환하고 있습니다.</p></li></ul>import { comparePlayersForNextMatchup } from '../../../lib/comparePlayers.js'
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const playerComparisonTool = createTool({
    id: "Compare two NBA players",
    inputSchema: z.object({
        player1:z.string(),
        player2:z.string()
    }),
    description: "Use this tool to compare two players given in the user prompt.",
    execute: async ({ context: { player1, player2 } }) =&gt; {
        return await comparePlayersForNextMatchup(player1, player2);
      },
})<p><strong>CORS 처리를 위한 미들웨어 추가</strong></p><p>Mastra 서버에 미들웨어를 추가하여 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS를</a> 처리합니다. 인생에서 피할 수 없는 세 가지가 있다면 죽음, 세금, 그리고 웹 개발자에게는 CORS라고 합니다. 간단히 말해, 교차 출처 리소스 공유는 프론트엔드에서 다른 도메인이나 포트에서 실행되는 백엔드에 요청을 하지 못하도록 차단하는 브라우저 보안 기능입니다. 로컬호스트에서 백엔드와 프론트엔드를 모두 실행하지만 서로 다른 포트를 사용하므로 CORS 정책이 트리거됩니다. 백엔드에서 프론트엔드의 요청을 허용할 수 있도록 <a href="https://mastra.ai/en/docs/server-db/middleware">Mastra 문서에</a> 지정된 미들웨어를 추가해야 합니다.</p><p>1. <code>backend/src/mastra</code> 디렉터리 내의 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a> 파일로 이동하여 CORS에 대한 구성을 추가합니다:</p><ul><li><p><code>origin: ['http://localhost:5173']</code></p><ul><li><p>이 주소(Vite 기본 주소)에서만 요청을 허용합니다.</p></li></ul></li><li><p><code>allowMethods: ["GET", "POST"]</code></p><ul><li><p>허용되는 HTTP 메서드입니다. 대부분의 경우 POST를 사용합니다.</p></li></ul></li><li><p><code>allowHeaders: ["Content-Type", "Authorization", "x-mastra-client-type, "x-highlight-request", "traceparent"],</code></p><ul><li><p>요청에 사용할 수 있는 사용자 정의 헤더를 결정합니다.</p></li></ul></li></ul><p></p>import { Mastra } from '@mastra/core/mastra';
import { basketballAgent } from './agents';

console.log('Starting Mastra server...');

export const mastra = new Mastra({
  agents: { basketballAgent },
  server:{
    timeout: 10 * 60 * 1000, // 10 minutes
    cors: {
      origin: ['http://localhost:5173'],
      allowMethods: ["GET", "POST"],
      allowHeaders: [
        "Content-Type",
        "Authorization",
        "x-mastra-client-type",
        "x-highlight-request",
        "traceparent",
      ],
      exposeHeaders: ["Content-Length", "X-Requested-With"],
      credentials: false,
    },
  },

});

console.log('Mastra server configured.'); // Log after server configuration
<h4><strong>8단계: 프론트엔드 통합</strong></h4><p>이 React 컴포넌트는 <code>@ai-sdk/react</code> 에서 <a href="https://mastra.ai/en/docs/frameworks/agentic-uis/ai-sdk#using-the-usechat-hook">useChat()</a> 훅을 사용하여 Mastra AI 에이전트에 연결되는 간단한 채팅 인터페이스를 제공합니다. 또한 이 훅을 사용하여 토큰 사용량, 도구 호출을 표시하고 대화를 렌더링할 것입니다. 위의 시스템 프롬프트에서 상담원에게 마크다운으로 응답을 출력하도록 요청하므로 <code>react-markdown</code> 을 사용하여 응답 형식을 올바르게 지정합니다.</p><p></p><p>1. 프론트엔드 디렉터리에서 @ai-sdk/react 패키지를 설치하여 useChat() 훅을 사용합니다.</p>npm install @ai-sdk/react<p>2. 같은 디렉토리에 있는 동안 에이전트가 생성하는 응답의 형식을 올바르게 지정할 수 있도록 React Markdown을 설치합니다.</p>npm install react-markdown<p>3. <code>useChat()</code> 을 구현합니다. 이 훅은 프론트엔드와 AI 에이전트 백엔드 간의 상호작용을 관리합니다. 메시지 상태, 사용자 입력, 상태를 처리하고 통합 가시성을 위한 라이프사이클 훅을 제공합니다. 전달되는 옵션에는 다음이 포함됩니다:</p><ul><li><p><code>api:</code> 이는 Mastra AI 에이전트의 엔드포인트를 정의합니다. 기본값은 포트 4111이며 스트리밍 응답을 지원하는 경로도 추가하려고 합니다.</p></li><li><p><code>onToolCall</code>: 상담원이 도구를 호출할 때마다 실행되며, 상담원이 어떤 도구를 호출하는지 추적하는 데 사용합니다.</p></li><li><p><code>onFinish</code>: 상담원이 전체 응답을 완료한 후에 실행됩니다. 스트리밍을 활성화하더라도 <code>onFinish</code> 은 각 청크가 아닌 전체 메시지가 수신된 후에 실행됩니다. 여기서는 토큰 사용량을 추적하는 데 사용하고 있습니다. 이는 LLM 비용을 모니터링하고 최적화할 때 유용할 수 있습니다.</p></li></ul><p>4. 마지막으로 <code>frontend/components</code> 디렉토리에 있는 <a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a> 컴포넌트로 이동하여 대화를 담을 UI를 만듭니다. 그런 다음 응답을 <code>ReactMarkdown</code> 컴포넌트로 래핑하여 상담원의 응답 형식을 올바르게 지정합니다.</p>import React, { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import ReactMarkdown from 'react-markdown';

export default function ChatUI() {
    const [totalTokenUsage, setTotalTokenUsage] = useState(0);
    const [promptTokenUsage, setPromptTokenUsage] = useState(0);
    const [completionTokenUsage, setCompletionTokenUsage] = useState(0);
    const [toolsCalled, setToolsCalled] = useState([]);

    const { messages, input, handleInputChange, handleSubmit, status } = useChat({
        api: 'http://localhost:4111/api/agents/basketballAgent/stream', //Replace with your own endpoint for your agent
        id: 'my-chat-session',

        //Optional parameter to check agent tool calls
        onToolCall: ({ toolCall }) =&gt; {
            setToolsCalled((prev) =&gt; [...prev, toolCall.toolName]);
        },

        //Optional parameter to check token usages
        onFinish: (message, { usage }) =&gt; {
            setTotalTokenUsage((prev) =&gt; prev + usage.totalTokens);
            setPromptTokenUsage((prev) =&gt; prev + usage.promptTokens);
            setCompletionTokenUsage((prev) =&gt; prev + usage.completionTokens);
        },

        //Optional parameter for error handling
        onError: (error) =&gt; {
            console.error('Agent error:', error);
        },
    });

    return (
        &lt;div&gt;
            &lt;div className="agent-info"&gt;
                &lt;h4 className="stats-title"&gt;What's My Agent Doing?&lt;/h4&gt;

                &lt;div className="stats-box"&gt;
                    &lt;strong className="stats-sub-title"&gt;Tools Called:&lt;/strong&gt;
                    &lt;ul className="tool-list"&gt;
                        {toolsCalled.map((tool, idx) =&gt; (
                            &lt;li key={idx}&gt;{tool}&lt;/li&gt;
                        ))}
                        {toolsCalled.length === 0 &amp;&amp; &lt;li&gt;No tools called yet.&lt;/li&gt;}
                    &lt;/ul&gt;

                    &lt;div className="usage-stats"&gt;
                        &lt;p&gt;Prompt Token Usage: {promptTokenUsage}&lt;/p&gt;
                        &lt;p&gt;Completion Token Usage: {completionTokenUsage}&lt;/p&gt;
                        &lt;p&gt;Total Token Usage: {totalTokenUsage}&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;strong&gt;Conversation:&lt;/strong&gt;
            &lt;div className="convo-box"&gt;
                {messages.map((msg) =&gt; (
                    &lt;div key={msg.id} className="message-item"&gt;
                        &lt;strong className="message-role"&gt;{msg.role === 'assistant' ? 'Basketbot' : 'You'}:&lt;/strong&gt;
                        &lt;ReactMarkdown&gt;{msg.content}&lt;/ReactMarkdown&gt;
                    &lt;/div&gt;
                ))}
            &lt;/div&gt;

            &lt;form onSubmit={handleSubmit}&gt;
                &lt;input
                    type="text"
                    value={input}
                    onChange={handleInputChange}
                    placeholder="Input two players you want to compare."
                    className="input-box"
                /&gt;
                &lt;button type="submit" disabled={status === 'streaming'}&gt;
                    {status === 'streaming' ? 'Thinking...' : 'Send'}
                &lt;/button&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    );
}<h4><strong>9단계: 애플리케이션 실행</strong></h4><p>축하합니다! 이제 애플리케이션을 실행할 준비가 되었습니다. 백엔드와 프론트엔드를 모두 시작하려면 다음 단계를 따르세요.</p><ol><li><p>터미널 창에서 루트 디렉토리에서 시작하여 백엔드 디렉토리로 이동하여 Mastra 서버를 시작합니다:</p></li></ol>cd backend

npm run dev<p>2. 다른 터미널 창에서 루트 디렉토리에서 시작하여 프론트엔드 디렉토리로 이동하고 React 앱을 시작합니다:</p><p></p>cd frontend

npm run dev<p></p><p>3. 3. 브라우저로 이동하여 다음으로 이동합니다:</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>채팅 인터페이스가 보일 것입니다. 이 샘플 프롬프트를 사용해 보세요:</p><ul><li><p>"르브론 제임스와 스테픈 커리 비교"</p></li><li><p>"제이슨 테이텀과 루카 돈치치 중 누구를 선택해야 하나요?"</p></li></ul><p></p><h3><strong>다음 단계 에이전트의 지능 향상</strong></h3><p>어시스턴트의 에이전트 기능과 추천 기능을 더욱 강화하기 위해 다음 버전에서는 몇 가지 주요 업그레이드 기능을 추가할 예정입니다.</p><p></p><p><strong>NBA 뉴스 시맨틱 검색</strong></p><p>플레이어 성과에 영향을 줄 수 있는 요소는 무수히 많으며, 그 중 상당수는 원시 통계에 나타나지 않습니다. 부상 보고서, 라인업 변경 또는 경기 후 분석과 같은 내용은 뉴스 기사에서만 볼 수 있습니다. 이러한 추가적인 맥락을 파악하기 위해 시맨틱 검색 기능을 추가하여 에이전트가 관련 NBA 기사를 검색하고 해당 내러티브를 추천에 반영할 수 있도록 할 것입니다.</p><p></p><p><strong>Elasticsearch MCP 서버를 사용한 동적 검색</strong></p><p>MCP(모델 컨텍스트 프로토콜)는 상담원이 데이터 소스에 연결하는 방법의 표준으로 빠르게 자리 잡고 있습니다. 에이전트가 우리가 제공하는 사전 정의된 검색 기능에 의존하지 않고 동적으로 쿼리를 작성할 수 있도록 검색 로직을 Elasticsearch MCP 서버로 마이그레이션할 것입니다. 이를 통해 보다 자연스러운 언어 워크플로우를 사용할 수 있고 모든 검색 쿼리를 수동으로 작성할 필요성을 줄일 수 있습니다. <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">여기에서</a> Elasticsearch MCP 서버와 에코시스템의 현재 상태에 대해 자세히 알아보세요.</p><p></p><p>이러한 변경 사항은 이미 진행 중이니 계속 지켜봐 주세요!</p><h3><strong>결론</strong></h3><p></p><p>이 블로그에서는 JavaScript, Mastra 및 Elasticsearch를 사용하여 판타지 농구 팀에 맞춤형 추천을 제공하는 에이전트 RAG 어시스턴트를 구축했습니다. 저희가 다뤘습니다:</p><ul><li><p><strong>에이전트 RAG의 기본</strong> 사항과 AI 에이전트의 자율성과 RAG를 효과적으로 사용할 수 있는 도구를 결합하여 보다 미묘하고 역동적인 에이전트를 만드는 방법에 대해 알아보세요.</p></li><li><p>데이터 저장 기능과 강력한 기본 집계 기능을 갖춘 <strong>Elasticsearch가 </strong>어떻게 LLM의 지식 베이스로서 훌륭한 파트너가 되는지 알아보세요.</p></li><li><p>자바스크립트 에코시스템에서 개발자를 위해 이러한 에이전트 구축을 간소화하는 <strong>Mastra </strong>프레임워크와 그 방법을 소개합니다.</p></li></ul><p>농구광이든, AI 에이전트를 구축하는 방법을 모색 중이든, 저처럼 두 가지 모두에 관심이 있는 분이라면 이 블로그가 시작을 위한 몇 가지 기본 요소를 제공했으면 합니다. 전체 리포지토리는 <a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub에서</a> 사용할 수 있으며, 자유롭게 복제하고 수정할 수 있습니다. 이제 판타지 리그에서 우승하세요!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agentic-rag</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agentic-rag</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[에이전틱 AI]]></category>
    <category><![CDATA[Javascript]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ffd561836a4cb20/6a17f1e47b54f978588b39e4/8132ed781c1ea5d46ca244182f421ed5c721f23b-1200x628.png" length="0" type="image/png"/>
    <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[MCP(모델 컨텍스트 프로토콜)의 현재 상태]]></title>
    <description><![CDATA[MCP, 프로젝트 업데이트, 기능, 보안 문제, 새로운 사용 사례, Elastic의 Elasticsearch MCP 서버를 다루는 방법에 대해 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>최근 샌프란시스코에서 열린 <a href="https://mcpdevsummit.ai/">MCP 개발자 서밋에</a> 참석했는데, 모델 컨텍스트 프로토콜(MCP)이 AI 에이전트 및 컨텍스트가 풍부한 AI 애플리케이션의 기본 구성 요소로 빠르게 자리 잡고 있다는 것을 알 수 있었습니다. Elastic에서는 <a href="https://www.elastic.co/kr/elasticsearch/agent-builder">에이전트 빌더에서</a> 직접 MCP 서버를 노출함으로써 이러한 방향으로 나아가고 있으며, Elasticsearch를 모든 MCP 호환 에이전트를 위한 최고 수준의 컨텍스트 및 도구 제공자로 만들고 있습니다. 이 포스팅에서는 이벤트의 주요 업데이트, 새로운 사용 사례, MCP의 향후 예정 사항, 에이전트 빌더를 사용하여 에이전트가 MCP를 통해 Elasticsearch를 사용할 수 있도록 하는 방법에 대해 살펴보겠습니다.</p><h2>모델 컨텍스트 프로토콜(MCP)이란 무엇인가요?</h2><p>익숙하지 않은 분들을 위해 설명하자면, <a href="https://modelcontextprotocol.io/introduction">모델 컨텍스트 프로토콜은</a> 다양한 데이터 소스 및 도구에 AI 모델을 연결하는 구조화된 양방향 방법을 제공하여 보다 관련성 있고 정보에 입각한 응답을 생성할 수 있도록 하는 개방형 표준입니다. 일반적으로 "<a href="https://modelcontextprotocol.io/introduction">AI 애플리케이션용 USB-C 포트</a>"라고 불립니다.</p><p>다음은 양방향성을 강조하는 아키텍처 다이어그램입니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ff0e141b5dfda29/6a17e7ffe8fbcee5263a1946/5eba1e59514eb58a5220bb92bb49e6328ee83cd7-674x466.png" alt="모델 컨텍스트 프로토콜(MCP) 아키텍처" /><p>AI 애플리케이션을 확장하는 데 있어 주요 과제 중 하나는 각각의 새로운 데이터 소스에 대한 사용자 지정 통합을 구축해야 하는 것이므로, 이는 AI 실무자에게 중요한 변화입니다. MCP는 모델에 컨텍스트를 관리하고 제공하기 위한 지속 가능하고 재사용 가능한 아키텍처를 제공합니다. 모델에 구애받지 않고 서버에 구애받지 않으며 완전한 오픈 소스입니다.</p><p>MCP는 애플리케이션 간의 통합을 표준화하기 위한 일련의 API 사양 중 가장 최신 버전입니다. 과거에는 RESTful 서비스를 위한 OpenAPI, 데이터 쿼리를 위한 GraphQL, 마이크로 서비스 통신을 위한 gRPC를 사용했습니다. MCP는 이러한 구형 사양의 구조화된 엄격함을 공유할 뿐만 아니라 이를 제너레이티브 AI 설정에 적용하여 사용자 지정 커넥터 없이도 에이전트를 다른 시스템에 쉽게 연결할 수 있습니다. 여러 면에서 MCP는 HTTP가 웹에서 했던 일을 AI 에이전트에서 하는 것을 목표로 합니다. HTTP가 브라우저와 웹사이트 간의 통신을 표준화한 것처럼, MCP는 AI 에이전트가 주변 데이터 세계와 상호 작용하는 방식을 표준화하고자 합니다.</p><h2>MCP와 다른 에이전트 프로토콜 비교</h2><p>상담원 프로토콜 환경은 빠르게 확장되고 있으며, 상담원 상호 작용 방식을 정의하기 위해 경쟁하는 12개 이상의 새로운 표준이 등장하고 있습니다. LlamaIndex의 <a href="https://x.com/seldo">로리 보스는</a> 에이전트 간 대화에 초점을 맞춘 에이전트 간 프로토콜과 LLM에 구조화된 컨텍스트를 전달하는 데 초점을 맞춘 MCP와 같은 컨텍스트 지향 프로토콜의 두 가지 유형으로 대부분 분류할 수 있다고 설명합니다.</p><p>Google의 <a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/">A2A</a> (에이전트 간 프로토콜), Cisco와 IBM의 <a href="https://agentcommunicationprotocol.dev/introduction/welcome">ACP</a> (에이전트 통신 프로토콜), <a href="https://agoraprotocol.org/">아고라와</a> 같은 다른 인기 프로토콜은 에이전트 간 협상, 연합 구축, 심지어 탈중앙화된 신원 시스템을 가능하게 하는 것을 목표로 합니다. MCP는 상담원이 서로 대화하는 방식이 아니라 툴과 데이터에 액세스하는 방식에 초점을 맞추기 때문에 좀 더 실용적인 접근 방식을 취합니다(향후 다양한 방식으로 이를 지원할 수도 있지만).</p><p>현재 MCP를 차별화하는 요소는 견인력과 추진력입니다. 초창기 프론트엔드 프레임워크의 React와 마찬가지로 MCP도 틈새 문제에서 시작하여 현재는 가장 많이 채택되고 확장 가능한 에이전트 프로토콜 중 하나로 자리 잡았습니다.</p><h2>서밋 요약: MCP의 진화하는 우선순위</h2><p>이 서밋에는 Anthropic, Okta, OpenAI, AWS, GitHub 등의 기여자들이 연사로 참여했습니다. 이번 회담에서는 핵심 프로토콜 개선부터 실제 구현에 이르기까지 다양한 논의가 이루어졌으며, 즉각적인 우선순위와 장기적인 우선순위에 대한 윤곽이 드러났습니다. 이 강연은 초기 실험과 단순한 도구 호출에서 벗어나 MCP를 기반으로 신뢰할 수 있고 확장 가능한 모듈식 AI 시스템을 구축하는 것으로의 전환을 반영했습니다.</p><p>여러 연사가 MCP가 단순한 프로토콜 배관을 넘어 AI 네이티브 웹의 기반이 될 수 있는 미래에 대해 이야기했습니다. 자바스크립트를 통해 사용자가 웹 페이지를 클릭하고 상호 작용할 수 있었던 것처럼, MCP를 통해 에이전트가 우리를 대신하여 동일한 작업을 수행할 수 있습니다. 예를 들어, 이커머스에서는 사용자가 쇼핑을 위해 웹사이트를 수동으로 탐색하는 대신 상담원에게 로그인하여 특정 제품을 찾아 장바구니에 추가하고 결제하라고 간단히 지시할 수 있습니다.</p><p>페이팔은 이번 서밋에서 이러한 에이전트 커머스 경험을 가능하게 하는 새로운 에이전트 툴킷과 MCP 서버를 선보였습니다. MCP가 도구와 데이터 소스에 대한 안전하고 안정적인 액세스를 제공하므로 상담원은 웹을 읽는 데 그치지 않고 이를 기반으로 조치를 취할 수 있습니다. 오늘날 MCP는 이미 강력한 추진력을 갖춘 표준으로 자리 잡았으며, 향후에는 웹 전반에서 AI를 활용한 사용자 상호작용의 표준이 될 수 있습니다.</p><h2>MCP 프로젝트 업데이트: 운송, 도출 및 구조화된 툴링</h2><p>MCP의 핵심 기여자인 <a href="https://x.com/JeromeSwannack">제롬 스와낵은</a> 지난 6개월 동안 프로토콜 사양에 대한 몇 가지 업데이트를 공유했습니다. 이러한 변경의 주요 목표는 다음과 같습니다:</p><ol><li><p>스트리밍 가능한 HTTP를 추가하여 원격 MCP 활성화하기</p></li><li><p>발화 및 도구 출력 스키마를 추가하여 더욱 풍부한 상담원 상호작용 모델을 사용하려면 다음과 같이 하세요.</p></li></ol><p>MCP가 오픈 소스이기 때문에 개발자는 이미 스트리밍 가능한 HTTP와 같은 변경 사항을 구현할 수 있습니다. 유도 및 도구 출력 스키마는 현재 공개되지 않았으며 초안 단계에 있으며 발전할 수 있습니다.</p><p><strong>스트리밍 가능한 HTTP </strong><a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports">(2025년 3월 26일 출시</a>)<strong>:</strong> 영향력 있는 기술 업데이트는 새로운 전송 메커니즘으로 스트리밍 가능한 HTTP를 도입한 것입니다. 이는 서버 전송 이벤트(SSE)를 확장성이 뛰어난 양방향 모델로 대체하여 단일 HTTP 연결을 통해 청크 전송 인코딩과 점진적 메시지 전달을 지원합니다. 이를 통해 AWS Lambda와 같은 클라우드 인프라에 MCP 서버를 배포하고 오래 지속되는 연결이나 폴링이 필요 없는 엔터프라이즈 네트워크 제약을 지원할 수 있습니다.</p><p><strong>유도 </strong><a href="https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation">(06-18-2025에 출시</a>)<strong>:</strong> 도출을 통해 서버는 클라이언트로부터 컨텍스트를 구조화하는 방식에 대한 스키마를 정의할 수 있습니다. 기본적으로 서버는 필요한 사항과 기대하는 입력의 종류를 설명할 수 있습니다. 여기에는 몇 가지 의미가 있습니다: 서버 빌더의 경우 더 복잡한 에이전트 상호작용을 구축할 수 있습니다. 클라이언트 빌더의 경우 이러한 스키마에 맞게 조정되는 동적 UI를 구현할 수 있습니다. 그러나 사용자로부터 민감하거나 개인 식별이 가능한 정보를 추출하는 데 유도 기능을 사용해서는 안 됩니다. 개발자는 <a href="https://modelcontextprotocol.io/specification/draft/client/elicitation#security-considerations">모범 사례를</a> 따라 유도 프롬프트가 안전하고 적절하게 유지되도록 해야 하며, 특히 MCP가 성숙해짐에 따라 더욱 그렇습니다. 이는 이 글의 뒷부분에서 논의할 더 광범위한 보안 문제와 관련이 있습니다.</p><p><strong>도구 출력 스키마 </strong><a href="https://modelcontextprotocol.io/specification/draft/server/tools#output-schema">(06-18-2025에 출시</a>)<strong>: </strong>이 개념은 클라이언트와 LLM이 도구 출력 모양을 미리 알 수 있게 해줍니다. 도구 출력 스키마를 통해 개발자는 도구가 반환할 것으로 예상되는 내용을 설명할 수 있습니다. 이러한 스키마는 직접 도구 호출의 주요 한계 중 하나인 컨텍스트 창을 비효율적으로 사용하는 문제를 해결합니다. 컨텍스트 창은 LLM으로 작업할 때 가장 중요한 리소스 중 하나로 간주되며, 도구를 직접 호출하면 LLM의 컨텍스트에 완전히 푸시되는 원시 콘텐츠를 반환합니다. 도구 출력 스키마는 MCP 서버가 구조화된 데이터를 제공하도록 허용하여 토큰과 컨텍스트 창을 더 잘 활용할 수 있도록 도와줍니다. 다음은 일반적인 도구에 대한 몇 가지 <a href="https://modelcontextprotocol.io/specification/draft/server/tools#security-considerations">모범 사례입니다</a>.</p><p>이러한 새로운 업데이트와 향후 추가 기능을 통해 MCP는 더욱 모듈화되고 유형화되어 프로덕션에 바로 사용할 수 있는 에이전트 프로토콜이 될 것입니다.</p><h2>잘 사용되지 않는 전원 기능: 샘플링 및 루트</h2><p>MCP 사양이 새로운 것은 아니지만, 기조연설에서 샘플링과 루트가 모두 강조되었습니다. 이 두 가지 기본 요소는 현재 간과되고 잘 알려지지 않았지만 에이전트 간의 더욱 풍부하고 안전한 상호 작용에 크게 기여할 수 있습니다.</p><p><strong>샘플링 - 서버가 클라이언트에 완료를 요청할 수 있습니다: </strong><a href="https://modelcontextprotocol.io/docs/concepts/sampling">샘플링을</a> 사용하면 MCP 서버가 클라이언트 측 LLM에 완료를 요청할 수 있습니다. 이는 서버가 요청에 응답만 하는 것이 아니라 클라이언트 모델에 응답을 생성하도록 요청할 수 있는 프로토콜의 양방향 특성을 추가합니다. 이를 통해 클라이언트는 비용, 보안 및 MCP 서버가 사용하는 모델을 완벽하게 제어할 수 있습니다. 따라서 사전 구성된 모델이 있는 외부 MCP 서버를 사용하는 경우 서버가 이미 클라이언트에 연결된 모델을 프롬프트하기만 하면 되므로 자체 API 키를 제공하거나 해당 모델에 대한 자체 구독을 구성할 필요가 없습니다. 이를 통해 보다 복잡하고 인터랙티브한 상담원 동작이 가능해집니다.</p><p><strong>루트 - 리소스에 대한 범위 지정 액세스: </strong><a href="https://modelcontextprotocol.io/docs/concepts/roots">루트는</a> 클라이언트가 집중할 관련 리소스 및 작업 공간에 대해 서버에 알릴 수 있는 방법을 제공하기 위해 설계되었습니다. 이는 서버가 작동하는 범위를 설정하는 데 유용합니다. 루트는 "<a href="https://modelcontextprotocol.io/docs/concepts/roots#how-roots-work">정보 제공을 위한 것이지 엄격하게 강제하는</a>것은 아니다"라는 점에 유의하세요. 즉, MCP 서버나 에이전트에 대한 자격이나 권한을 정의하지 않습니다. 즉, 서버나 에이전트가 특정 도구를 실행하거나 쓰기 작업을 수행하는 것을 막기 위해 루트에만 의존할 수 없습니다. 루트를 사용하면 사용자 승인 메커니즘을 통해 클라이언트 측에서 권한을 처리해야 합니다. 또한 개발자는 루트에 의해 설정된 경계를 존중하고 <a href="https://modelcontextprotocol.io/docs/concepts/roots#best-practices">모범 사례를</a> 사용하도록 설계된 서버를 사용해야 한다는 점을 염두에 두어야 합니다.</p><h2>상담원을 위한 인증: OAuth 2.1 및 보호된 메타데이터</h2><p>이 섹션에서는 안전하지 않은 플로우를 제거하고 모범 사례를 통합한 OAuth 2.0의 최신 버전인 OAuth 2.1에 중점을 둡니다.</p><p>특히 보안과 확장성이 MCP가 에이전트와 툴을 연결하는 표준이 되는 데 걸림돌이 되는 주요 장애물로 여겨지는 만큼 OAuth 지원은 매우 기대되는 주제였습니다. <a href="https://x.com/aaronpk">Aaron Parecki</a> (Okta의 OAuth 2.1 편집자이자 ID 표준 전문가)는 MCP가 서버 개발자의 복잡성을 대부분 덜어주는 깔끔하고 확장 가능한 OAuth 흐름을 채택하는 방법에 대해 설명했습니다. 공식 OAuth 2.1 인증 사양은 최근 <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">6-18-2025에</a> 최신 프로토콜 개정판에 게시되었습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d53e7bb091b4f43/6a17e80163baff80dc741c56/2ea159116fe5e03ff800f077adf16d6ca9f1c1d1-1594x1280.png" alt="상담원을 위한 MCP 인증" /><p>이 구현에서는 OAuth 책임을 MCP 클라이언트와 서버로 나눌 수 있습니다. 대부분의 인증 흐름은 MCP 클라이언트에서 시작되고 처리되며, 마지막에 서버가 보안 토큰을 수신하고 확인하는 역할만 수행합니다. 이러한 분할은 개발자가 모든 연결을 구성할 필요 없이 여러 도구에서 인증하는 방법의 중요한 확장 문제를 해결하는 데 도움이 되며, MCP 서버 개발자가 OAuth 전문가가 될 필요가 없도록 보장합니다.</p><p>강연의 두 가지 주요 하이라이트는 다음과 같습니다:</p><ol><li><p><a href="https://datatracker.ietf.org/doc/rfc9728/"><strong>보호된 리소스 메타데이터</strong></a>: MCP 서버는 목적, 엔드포인트 및 인증 방법을 설명하는 JSON 파일을 게시할 수 있습니다. 이를 통해 클라이언트는 서버 URL만으로 OAuth 플로우를 시작할 수 있으므로 연결 프로세스가 간소화됩니다. 자세히 알아보세요: <a href="https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol">MCP에서 OAuth 수정하기</a></p></li><li><p><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13"><strong>IDP 및 SSO 지원</strong></a>: 기업은 ID 공급업체를 통합하여 중앙에서 액세스를 관리할 수 있습니다. 이는 사용자 경험과 보안 모두에 도움이 됩니다. 사용자는 10개의 서로 다른 동의 화면을 클릭할 필요가 없으며 보안팀은 각 연결에 대한 가시성을 확보할 수 있습니다.</p></li></ol><p>OAuth 로직을 클라이언트로 푸시하고 서버의 메타데이터에 의존함으로써 MCP 에코시스템은 큰 병목 현상을 피할 수 있습니다. 이를 통해 MCP는 오늘날 프로덕션 환경에서 최신 API를 보호하는 방식과 더욱 밀접하게 연계됩니다.</p><p>추가 읽기: <a href="https://aaronparecki.com/oauth-2-simplified/">OAuth 2 간소화</a>.</p><h2>컴포저블 에코시스템의 보안 과제</h2><p>새로운 개발에는 새로운 공격 표면도 수반됩니다. Cisco의 Arjun Sambamoorthy는 MCP 환경의 몇 가지 주요 위협을 다음과 같이 나열합니다:</p><p>위협</p><p>설명</p><p>해결 방법 &amp; 모범 사례</p><p>프롬프트 주입 &amp; 공구 중독</p><p>LLM 시스템 컨텍스트 또는 도구 설명에 악성 프롬프트를 삽입하여 LLM이 파일 읽기 또는 데이터 유출과 같은 의도하지 않은 작업을 수행하도록 하는 방법입니다.</p><p>MCP 스캔과 같은 도구를 사용하여 도구 메타데이터를 확인합니다. 설명과 매개변수를 프롬프트에 포함하기 전에 유효성을 검사합니다. 마지막으로 고위험 도구에 대한 사용자 승인을 구현하는 것을 고려하세요. 자세한 내용은 표 아래의 추가 읽기 목록에서 OWASP 프롬프트 주입 가이드를 참조하세요.</p><p>샘플링 공격</p><p>MCP의 맥락에서 샘플링은 MCP 서버가 LLM에 대한 즉각적인 인젝션 공격을 수행할 수 있는 문을 열어줍니다.</p><p>신뢰할 수 없는 서버에 대한 샘플링을 사용하지 않도록 설정하고 샘플링 요청에 대한 인적 승인을 추가하는 것을 고려하세요.</p><p>악성 MCP 서버</p><p>현재 MCP 서버 모음에서는 안전을 보장하기 위해 모든 서버를 일일이 검사하기 어렵습니다. 로그 서버는 사용자의 데이터를 은밀하게 수집하여 악의적인 공격자에게 노출시킬 수 있습니다.</p><p>신뢰할 수 있는 레지스트리 또는 내부 목록의 MCP 서버에만 연결합니다. 샌드박싱을 사용하여 컨테이너에서 타사 서버를 실행하세요.</p><p>악성 MCP 설치 도구</p><p>명령줄 설치 프로그램과 스크립트는 MCP 서버나 도구를 빠르게 구현하는 데 편리하지만, 확인되지 않은 손상된 코드가 설치될 수 있습니다.</p><p>샌드박스 환경에 설치하고 패키지 서명의 유효성을 검사하세요. 확인되지 않은 출처의 자동 업데이트는 절대 하지 마세요.</p><p>이러한 문제를 해결하기 위해 Arjun은 모든 검증을 처리할 수 있는 신뢰할 수 있는 MCP 레지스트리(자세한 내용은 아래 읽기 목록의 상위 두 항목을 참조하세요)와 이 <a href="https://github.com/slowmist/MCP-Security-Checklist">보안 체크리스트를</a> 사용할 것을 제안합니다.</p><p>추가 읽기:</p><ul><li><p><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices">공식 MCP 보안 모범 사례</a></p></li><li><p><a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP LLM 애플리케이션 상위 10위</a></p></li><li><p><a href="https://hiddenlayer.com/innovation-hub/">히든레이어 위협 연구</a></p></li><li><p><a href="https://github.com/invariantlabs-ai/mcp-scan">MCP 스캔</a></p></li><li><p><a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/">OWASP 프롬프트 주입 가이드</a></p></li></ul><h2>다음 단계 레지스트리, 거버넌스 및 에코시스템</h2><p>중앙 집중식 MCP 레지스트리가 개발 중이며 이번 서밋에서 가장 지속적으로 논의된 주제 중 하나였습니다. 현재 서버 에코시스템은 파편화, 낮은 신뢰도 및 검색 가능성으로 어려움을 겪고 있습니다. 특히 메타데이터가 불완전하거나 스푸핑될 수 있는 탈중앙화된 생태계에서는 개발자가 MCP 서버를 찾고, 그 기능을 확인하고, 안전하게 설치하는 것이 어렵습니다.</p><p>중앙 집중식 레지스트리는 신뢰할 수 있는 정보 소스 역할을 하고 검색 가능성을 개선하며 서버 메타데이터의 무결성을 보장하고 악성 도구 설치 위험을 줄임으로써 이러한 문제점을 직접 해결합니다.</p><p>MCP 레지스트리의 목표는 다음과 같습니다:</p><ul><li><p>서버 메타데이터(서버의 기능, 인증, 설치 및 호출 방법)에 대한 단일 소스 제공</p></li><li><p>불완전한 타사 레지스트리와 파편화를 제거하여 서버가 등록을 원할 때 인터넷의 다른 모든 레지스트리를 업데이트할 필요가 없도록 합니다.</p></li><li><p>앞서 언급한 메타데이터가 포함된 CLI 도구와 server.json 파일을 포함하는 서버 등록 플로우를 제공합니다.</p></li></ul><p>더 큰 희망은 신뢰할 수 있는 레지스트리가 생태계를 안전하게 확장하여 개발자가 자신 있게 새로운 도구를 만들고 공유할 수 있도록 돕는 것입니다.</p><p>거버넌스는 앤트로픽이 가장 중요하게 생각하는 또 다른 문제였습니다. MCP는 개방적이고 커뮤니티 주도로 유지되어야 하지만 거버넌스 모델을 확장하는 것은 여전히 진행 중인 작업이라는 점을 분명히 했습니다. 현재 해당 분야에서 도움을 구하고 있으며, 오픈소스 프로토콜 거버넌스에 대한 경험이 있는 분이라면 누구든 연락해 달라고 요청하고 있습니다. 이것은 제가 언급하고 싶었던 다른 주제로 이어집니다. 행사 내내 연사들은 생태계는 내부 개발자들의 기여가 있어야만 성장할 수 있다고 강조했습니다. MCP를 새로운 웹 표준으로 만들고 다른 인기 있는 에이전트 프로토콜과 차별화하기 위해서는 집중적인 노력이 필요합니다.</p><h2>실제 세계에서의 MCP: 사례 연구 및 데모</h2><p>여러 조직에서 이미 MCP가 실제 애플리케이션에서 어떻게 사용되고 있는지 공유했습니다:</p><ul><li><p><strong>페이팔 - 에이전트 커머스를 위한 MCP 서버: </strong>PayPal은 사용자의 쇼핑 경험을 근본적으로 바꿀 수 있는 새로운 <a href="https://github.com/paypal/agent-toolkit/">에이전트 툴킷과</a> MCP 서버를 선보였습니다. 소셜 미디어를 뒤져 상품을 찾고, 가격을 비교하고, 결제하는 대신 PayPal MCP 서버에 연결된 상담원과 채팅하여 이러한 모든 작업을 처리할 수 있습니다.
</p></li><li><p><strong>EpicAI.pro - 자비스:</strong> MCP의 발전으로 실제 자비스형 비서가 점점 더 가까워지고 있습니다. 아이언맨 영화에 익숙하지 않은 분들을 위해 설명하자면, 자비스는 자연어를 사용하고, 다중 모드 입력에 응답하며, 응답 대기 시간이 없고, 사용자의 요구를 사전에 예측하고, 자동으로 통합을 관리하고, 기기와 위치 간에 컨텍스트 전환이 가능한 AI 어시스턴트입니다. 자비스를 실제 로봇 비서로 상상한다면, MCP는 자비스에게 '손' 또는 복잡한 작업을 처리할 수 있는 능력을 부여합니다.
</p></li><li><p><strong>Postman - </strong><a href="https://www.postman.com/explore/mcp-generator"><strong>MCP 서버 생성기</strong></a><strong>: </strong>다양한 API 요청을 선택하고 장바구니에 담아 전체 장바구니를 MCP 서버로 다운로드할 수 있는 API 요청에 대한 장바구니 환경을 제공합니다.
</p></li><li><p><strong>블룸버그 - </strong>블룸버그는 엔터프라이즈 GenAI 개발의 주요 병목 현상을 해결했습니다. 약 10,000명의 엔지니어를 보유한 이 회사는 팀 전체에 걸쳐 도구와 에이전트를 통합할 수 있는 표준화된 방법이 필요했습니다. MCP를 통해 내부 툴을 상담원이 통합 인터페이스에서 쉽게 호출할 수 있는 원격 우선의 모듈식 구성 요소로 전환했습니다. 이를 통해 엔지니어는 조직 전체에 툴을 기여할 수 있었고, AI 팀은 사용자 지정 통합 대신 에이전트 구축에 집중할 수 있었습니다. 이제 블룸버그는 확장 가능하고 안전한 에이전트 워크플로우를 지원하여 MCP 에코시스템과의 완전한 상호운용성을 확보할 수 있습니다. 블룸버그는 어떤 공개 자료도 연결하지 않았지만, 정상 회담에서 공개적으로 발표한 내용은 이렇습니다.
</p></li><li><p><strong>블록 - </strong>블록은 MCP를 사용하여 직원들이 엔지니어링, 영업, 마케팅 등의 업무를 자동화할 수 있도록 지원하는 내부 AI 에이전트인 <a href="https://github.com/block/goose?tab=readme-ov-file">Goose를</a> 구동합니다. 이들은 매일 사용하는 시스템과 자연어 상호 작용을 지원하기 위해 Git, Snowflake, Jira 및 Google Workspace와 같은 도구를 위한 60개 이상의 MCP 서버를 구축했습니다. 이제 Block의 직원들은 코드를 작성하지 않고도 Goose를 사용하여 데이터 쿼리, 사기 탐지, 인시던트 관리, 내부 프로세스 탐색 등의 작업을 수행할 수 있습니다. MCP는 블록이 단 2개월 만에 여러 직무에 걸쳐 AI 도입을 확장할 수 있도록 지원했습니다.
</p></li><li><p><strong>AWS - </strong><a href="https://github.com/awslabs/mcp"><strong>AWS MCP 서버</strong></a><strong>: </strong>AWS는 주사위 굴리기를 시뮬레이션하고, 과거 주사위 굴림을 추적하고, 스트리밍 가능한 HTTP를 사용하여 결과를 반환하는 재미있는 던전 앤 드래곤 테마의 MCP 서버를 선보였습니다. 이 경량 예시는 Lambda 및 Fargate와 같은 AWS 도구와 인프라를 사용하여 MCP 서버를 구축하고 배포하는 것이 얼마나 쉬운지를 강조합니다. 또한 MCP 서버와 상호 작용하는 멀티모달 에이전트를 구축하기 위한 오픈 소스 툴킷인 <a href="https://aws.amazon.com/blogs/opensource/introducing-strands-agents-an-open-source-ai-agents-sdk/">Strands SDK도</a> 소개했습니다.</p></li></ul><h2>Elastic 에이전트 빌더의 MCP 지원</h2><p>데이터 위에 직접 에이전트를 구축하는 가장 쉬운 방법인 <a href="https://www.elastic.co/kr/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Elastic 에이전트 빌더를</a> 사용해 지금 바로 MCP 실험을 시작할 수 있습니다. 에이전트 빌더를 사용하면 Elasticsearch 기반 도구를 MCP 호환 에이전트에 노출할 수 있으며, 다음과 같은 강력한 기본 제공 도구가 이미 포함되어 있습니다:</p><ul><li><p><code>platform.core.search</code> - 전체 Elasticsearch 쿼리 DSL을 사용하여 검색을 실행합니다.</p></li><li><p><code>platform.core.list_indices</code> - Elasticsearch 내에서 사용 가능한 모든 인덱스를 나열합니다(에이전트가 어떤 데이터가 존재하는지 검색하는 데 도움이 됨).</p></li><li><p><code>platform.core.get_index_mapping</code> - 특정 인덱스에 대한 필드 매핑을 검색합니다(상담원이 데이터의 형태와 유형을 이해하는 데 도움이 됨).</p></li><li><p><code>platform.core.get_document_by_id</code> - ID로 특정 문서 가져오기(정확한 검색을 위해)</p></li></ul><p>이러한 도구만 있으면 신뢰할 수 있는 AI 에이전트 구축의 핵심인 엔터프라이즈 수준의 검색 및 정확도로 에이전트를 무장시킬 수 있습니다.</p><p>에이전트 빌더를 더욱 강력하게 만드는 것은 애플리케이션의 요구 사항에 맞는 사용자 지정 도구를 정의하고 노출할 수 있는 기능입니다. 이는 상담원이 매번 해당 로직을 다시 발견할 필요 없이 특정 인덱스에 대해 특정 유형의 검색을 수행하도록 하려는 독단적이거나 반복적인 워크플로우에 특히 유용합니다. 동일한 결론에 도달하기 위해 계획과 추론에 토큰을 사용하는 대신, 그 의도를 도구에 직접 인코딩하여 에이전트가 더 빠르고 안정적이며 비용 효율적으로 작업할 수 있습니다.</p><p>다음은 에이전트 빌더 UI 내에서 ES|QL을 사용하는 사용자 지정 도구 정의의 예입니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltca9e3d0a4e7031c0/6a17e803faa913d8c393c897/c1f6405a374b707e8e6fa36b9e21db5f3c7cd127-1376x864.png" alt="상담원 빌더 UI" /><p>사용자 지정 도구를 정의한 후에는 <code>Manage MCP</code> 드롭다운을 클릭하고 MCP 서버 URL을 복사하여 MCP를 사용하여 해당 도구(기본 제공 기본 도구 포함)를 노출할 수 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf7d8b29b06c08f94/6a17e805033c8d07f06bb1b6/9f39588525ca2643475de557ea54a6bcf5c150f6-1282x616.png" alt="MCP 도구" /><p>이제 이 MCP 엔드포인트를 MCP를 사용하는 모든 클라이언트로 가져와서 에이전트 빌더에 연결하고 사용 가능한 모든 도구에 대한 액세스 권한을 부여할 수 있습니다. 자세한 내용은 <a href="https://www.elastic.co/kr/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">상담원 빌더에</a> 대한 소개를 참조하세요.</p><h2>결론</h2><p>MCP 개발자 서밋은 MCP가 이러한 AI 에이전트가 서로 상호 작용하고 주변 데이터 세계와 상호 작용하는 방식을 형성하고 있음을 분명히 했습니다. 에이전트를 엔터프라이즈 데이터에 연결하든 완전 자율 에이전트를 설계하든 MCP는 표준화되고 구성 가능한 통합 방법을 제공하여 대규모로 빠르게 유용해지고 있습니다. 전송 프로토콜과 보안 패턴에서 레지스트리와 거버넌스에 이르기까지 MCP 생태계는 빠르게 성숙하고 있습니다. MCP는 계속해서 개방적이고 커뮤니티 주도로 운영될 예정이므로 지금 바로 개발자가 MCP의 진화를 주도할 수 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/mcp-current-state</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/mcp-current-state</guid>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2f63f23bbecd2a18/6a17e8066317302039585aa7/02b8c8672ffa129e0ed91a92d6cab612a01d27f2-1200x628.png" length="0" type="image/png"/>
    <pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>