본문으로 건너뛰기

© 2026 Molayo

Lobste.rs헤드라인2026. 06. 19. 04:44

Elasticsearch 기반의 에이전트 메모리: 하이브리드 검색(Hybrid Retrieval) 및 DLS

요약

Elasticsearch를 활용하여 에이전트의 장기 메모리(Long-term memory)를 구축하는 아키텍처를 소개합니다. 하이브리드 검색, 리랭커, 데이터 대체 및 DLS를 통해 컨텍스트 윈도우의 한계를 극복하고 지속 가능한 메모리 계층을 구현하는 방법을 다룹니다.

핵심 포인트

  • 컨텍스트 윈도우의 한계를 극복하기 위한 장기 메모리 시스템의 필요성
  • RRF와 리랭커를 활용한 하이브리드 회상(Hybrid Recall) 구현
  • 정보 모순 해결을 위한 대체(Supersession) 메커니즘 적용
  • 사용자별 데이터 격리를 위한 DLS(Document Level Security) 활용
  • QA 평가에서 R@10 평균 0.89의 높은 성능 기록

Agent Builder가 현재 GA(General Availability)로 출시되었습니다. Elastic Cloud Trial을 통해 시작해 보시고, Agent Builder에 대한 문서는 여기에서 확인하십시오.

Elasticsearch에서 에이전트 메모리 구축하기

세 개의 인덱스, 리랭커(Reranker)를 활용한 하이브리드 회상(Hybrid Recall), 대체(Supersession), 감쇠(Decay), 그리고 DLS. 에이전트를 위한 지속 가능한 메모리 계층(Persistent Memory Layer)의 아키텍처와 그 수치적 근거.

Sarah의 스마트 전구는 흰색만 표시됩니다. 그녀의 스마트 홈 어시스턴트는 허브를 재설정할 것을 제안합니다. 그녀는 지난 3월에 그렇게 했고, 지난주에도 다시 했습니다. 하지만 두 번의 재설정 모두 아무것도 해결하지 못했습니다. 에이전트는 그 사실을 모르며, 개가 센서 케이블을 씹어 놓았다는 사실도 모릅니다. 중요했던 이력, 무엇이 작동했고 무엇이 작동하지 않았는지, 그리고 Sarah가 누구인지에 대한 정보는 각 세션이 끝날 때마다 사라졌습니다.

표준적인 해결책은 이전의 컨텍스트(Context)를 컨텍스트 윈도우(Context Window)에 밀어 넣는 것입니다. 하지만 이는 비용, 지연 시간(Latency), 그리고 모델이 프롬프트의 가장자리에서 멀리 떨어진 사실을 무시하는 것으로 잘 알려진 "중간에서 길을 잃는(lost in the middle)" 현상 때문에 한계에 부딪힙니다. 1M 토큰의 컨텍스트 윈도우는 임시 메모장(Scratchpad)일 뿐, 메모리 시스템이 아닙니다.

컨텍스트 윈도우는 단기 메모리(Short-term memory)입니다. 즉, 단일 추론(Inference)을 위한 활성 추론 공간입니다. 부족한 것은 장기 메모리(Long-term memory)입니다. 세션 종료 후에도 유지되고, 수년간의 상호작용으로 확장 가능하며, 콘텐츠, 시간, 사용자별로 사실을 검색할 수 있는 지속 가능한 저장소(Persistent Store)가 필요합니다.

이 포스트는 Elasticsearch를 기반으로 구축되었으며, 인지 과학의 세 가지 범주를 중심으로 구조화된 실제 에이전트 메모리 시스템의 아키텍처에 대해 다룹니다. 이 시스템은 RRF(Reciprocal Rank Fusion)와 크로스 인코더 리랭커(Cross-encoder Reranker)를 사용한 하나의 하이브리드 회상 쿼리, 모순을 해결하기 위한 대체(Supersession), 그리고 사용자별 DLS 격리(Isolation)로 구성됩니다. 168개의 질문에 대한 QA 스타일 평가에서, 교차 테넌트 유출(Cross-tenant leaks) 없이 R@10 평균 0.89를 기록했습니다.

전체 구현 코드는 GitHub에 공개되어 있습니다. 이 포스트는 이 시스템이 이러한 형태로 설계되었는지에 대해 다룹니다.

에이전트 메모리 저장소가 수행해야 할 일

사용자가 "지난번에 어떤 해결책을 시도했었지?"라고 묻는다면, 이는 정확한 일치(exact-match) 제약 조건이 있는 시간적 쿼리(temporal query)입니다. 또는 "왜 내 스마트 전구는 흰색으로만 보이지?"라고 묻는다면, 개인적인 메모리와 공유된 카탈로그(catalog)가 혼합되어야 합니다. 메모리 자체는 균일하게 작동하지 않습니다. 사용자가 겪은 사건, 사용자에 대한 안정적인 사실, 그리고 단계별 플레이북(playbooks)은 모두 서로 다른 쓰기 속도(write rates)와 노후화 규칙(aging rules)을 가지므로, 저장소는 유형을 인식하고 각각을 적절하게 처리해야 합니다. 또한 멀티 유저 배포(multi-user deployment) 환경에서는 각 사용자의 메모리가 다른 모든 사용자에게 보이지 않도록 유지되어야 합니다. 새로운 이벤트는 매우 빠르게 축적되므로 이를 내구성이 있는 유형으로 통합하지 않으면 인덱스(index)가 건초더미(haystack)처럼 변해버립니다. 사용자가 회상된 사실과 모순되는 내용을 말할 경우, 감사 추적(audit trail)을 유지하기 위해 이전 버전은 삭제되는 대신 대체되어야 합니다. 오래된 사실이 최신 사실보다 우선순위가 높아서는 안 되며, 사용자가 자주 접하는 사실이 뒤처져서도 안 됩니다. 그리고 전체 메모리 레이어(memory layer)는 특정 에이전트 런타임(agent runtime)에 종속되지 않고, MCP를 지원하는 모든 클라이언트가 접근할 수 있어야 합니다.

이러한 기능들을 벡터 저장소(vector store), 키워드 엔진(keyword engine), 감사 레이어(audit layer), 그리고 별도의 인증 서비스(auth service)로 나누는 것은 고장 날 수 있는 네 가지 요소를 늘리는 것이며, 매 회상(recall) 시마다 추가적인 라운드 트립(round-trips)을 발생시킵니다. 요구 사항들은 검색 엔진(search engine)의 특성을 설명하고 있으므로, 본 구현에서는 검색 엔진을 사용합니다. 이 포스트의 나머지 부분에서는 각 요소를 자세히 살펴봅니다.

에이전트 메모리의 세 가지 유형: 에피소드식, 의미적, 절차적 메모리

첫 번째 설계 결정은 어떤 범주의 메모리를 저장할 것인가입니다. 모든 것을 단순히 저장하는 것은 신호 없는 건초더미를 만드는 것과 같습니다. COALA 프레임워크에서 LLM 에이전트를 위해 제시된 에피소드식(episodic), 의미적(semantic), 절차적(procedural) 메모리 사이의 인지 심리학적 구분은 이미 적절한 범주를 갖추고 있으며, 이는 세 개의 Elasticsearch 인덱스(indices)로 깔끔하게 매핑됩니다.

일화 기억 (Episodic memory). 타임스탬프가 찍힌 이벤트: 추출이나 해석이 이루어지기 전, 사용자의 각 턴(turn)이 발생하는 그대로의 상태입니다. 대부분은 수명이 짧으며, 항상 보관할 가치가 있는 것은 아닙니다. 다만 일부 항목은 나중에 지속적인 사실에 대한 증거가 됩니다.

의미 기억 (Semantic memory). 사용자에 대해 정제되고 안정적인 주장들입니다.
Sarah는 Lumio Hub v2를 소유하고 있습니다. Sarah는 iOS 17.4를 사용 중입니다. Sarah의 허브는 3월에 초기화되었습니다.
이러한 정보는 세션 전반에 걸쳐 유지되며, 에이전트가 근거를 삼는 대상이 됩니다.

절차 기억 (Procedural memory). 다단계 플레이북(playbooks)입니다.
Zigbee 연결 끊김 문제를 해결하는 방법.
사실이 아닌 프로세스입니다. 각 항목은 사용자가 해결 여부를 확인(fix worked or didn't)할 때 통합(consolidation) 과정을 통해 증가하는 success_countfailure_count를 포함합니다. 이 카운터들은 통합 LLM이 플레이북을 개선할지 또는 교체할지 결정할 때 컨텍스트(context)로서 제공됩니다.

각 카테고리는 서로 다른 *생애주기 (lifecycle)*를 갖습니다. 일화 기억은 끊임없이 기록되며 소멸합니다. 의미 기억은 사용자가 변화함에 따라 큐레이션되고, 중복이 제거되며, 새로운 정보로 대체됩니다. 절차 기억은 통합에 피드백을 제공하는 결과 피드백(success_count, failure_count)을 축적합니다. 하나의 버킷(bucket)으로는 이를 모델링할 수 없습니다. 메모리 유형당 하나씩 구성된 세 개의 인덱스(indices)를 사용하면, 각 인덱스를 결합(coupling)하지 않고도 고유한 쓰기 속도, 노화 규칙(aging rules), 업데이트 규칙을 따를 수 있습니다.

이 세 가지와 함께 네 번째 검색 표면(retrieval surface)이 존재합니다: 바로 Elasticsearch에 이미 존재하는 월드 데이터(카탈로그, 지식 베이스)입니다. 이는 인지적 의미에서의 "기억"은 아니지만, 에이전트가 동일한 하이브리드 검색 파이프라인(다음 섹션에서 다룸)을 통해 이를 읽기 때문에 동일한 그림의 일부로 간주됩니다.

회상 파이프라인: RRF와 리랭커(reranker)를 이용한 하이브리드 검색

기억은 2단계 하이브리드 검색을 통해 회상됩니다: BM25 + Jina v5 dense에 대한 RRF를 수행한 후, 병합된 후보군에 대해 크로스 인코더(cross-encoder) 리랭커를 적용합니다. 각 문서는 한 번의 쓰기로 두 가지 방식으로 인덱싱됩니다: 원문 텍스트는 BM25 역색인(inverted index)에 저장되고, copy_to를 통해 동일한 값이 semantic_text로 라우팅됩니다.

Jina v5 벡터를 자동으로 생성하는 필드입니다. 동일한 콘텐츠를 두 번 인덱싱하더라도 저장 공간(storage footprint)은 일정하게 유지됩니다. 즉, 단 한 번의 원천 데이터(source-of-truth) 쓰기로 두 가지 검색 경로(index mapping)를 모두 생성합니다.

각 경로는 서로 다른 문제를 해결합니다. BM25는 에이전트의 의역(paraphrase) 과정에서 소실될 수 있는 리터럴 토큰(literal-token) 일치 항목, 즉 버전 번호, 에러 코드, "Lumio Hub v2"와 같은 고유 명사(proper nouns)를 고정합니다. 밀집 벡터(Dense vectors)는 답변에 다른 단어가 사용되더라도 질문의 의미적 형태(semantic shape)를 포착합니다. 어느 한쪽의 경로만으로는 상대방이 처리할 수 있는 사례를 놓치게 되며, RRF는 코사인 유사도(cosine similarities)에 맞춰 BM25 점수를 조정할 필요 없이 두 경로의 순위를 결합합니다.

과다 추출 (Over-fetch). 리랭커(Reranker)는 자신이 보는 것만을 재정렬할 수 있으므로, 후보군(candidate pool)이 넓어야 합니다. 하이브리드 검색기(hybrid retriever)는 각 경로당 80개의 후보를 추출하며, rank_constant=30 설정으로 RRF 결합을 수행합니다(Elasticsearch의 기본값인 60보다 더 타이트하여 상위 순위 항목이 더 지배적인 영향을 미칩니다). (_rrf_fetch)

리랭커 (Reranker). Jina v2 교차 인코더(cross-encoder)가 병합된 후보들을 사용자 쿼리에 대해 점수화합니다. BM25와 바이-인코더(bi-encoder) 밀집 벡터가 쿼리와 문서를 독립적으로 점수화하는 반면, 교차 인코더는 쌍(pair) 전체에 대한 전체 어텐션(full attention)을 사용하여 이들을 공동으로 점수화하며, 이는 쌍당 비용은 더 높지만 더 강력한 관련성 신호(relevance signal)를 제공합니다. 이것이 바로 2단계 파이프라인(two-stage pipeline)을 사용하는 동기입니다. 즉, 하이브리드 검색기로 저렴하게 과다 추출(over-fetch)한 다음, 비용이 더 높은 점수 산출기(_rerank)로 적은 수의 후보군을 리랭킹하는 것입니다.

위 다이어그램에 나타난 한 가지 미묘한 차이점이 있습니다. 에이전트의 도구 키트(tool kit)에는 recall_memory(tools.py에 정의됨)가 포함되어 있으며, 모델은 턴(turn) 중에 이를 호출합니다. 단 한 번의 호출로 세 가지 메모리 인덱스(memory indices)와 카탈로그 전체를 동시에 훑습니다. 에이전트가 메모리 유형을 직접 선택하지 않는 이유는, 리트리버(retriever)의 랭킹(ranking)과 인덱스별 감쇠(per-index decay)가 에이전트를 대신하여 라우팅(routing)을 처리하기 때문입니다. 두 번째 미묘한 차이점은 패러프레이징(paraphrasing, 의역)입니다. 에이전트는 해당 도구를 사용하기 전에 거의 항상 사용자의 메시지를 재작성하며, 이 과정에서 BM25가 확인하기 전에 쿼리에서 문자 그대로의 버전 번호, 오류 코드, 고유명사를 제거합니다. 따라서 모든 턴은 사용자의 축자적(verbatim) 메시지에 대한 자동 사전 호출(pre-recall)로 시작되며, 이는 마치 에이전트가 직접 호출한 것처럼 대화에 주입됩니다 (agent.py).

에이전트 메모리 기록 및 통합

두 가지 작업이 "방금 일어난 일"을 "이 사용자에 대해 지속되는 것"으로 이동시킵니다.

쓰기(Write). 모든 사용자 턴은 LLM이 응답하기 전에 하나의 에피소드 이벤트(ID, 정확한 메시지, 타임스탬프 등)를 기록합니다. ID는 쓰기 시 Elasticsearch에 의해 할당되며, Sarah의 API 키를 사용한 DLS 쿼리는 이후 모든 호출(recall)에서 해당 문서를 그녀의 범위 내로 제한합니다. 타임스탬프는 아래의 시간 감쇠(time-decay) 함수가 새로운 이벤트와 비교하여 순위를 매길 때 읽는 값입니다. 에이전트의 답변은 저장되지 않습니다. 대화 기록(conversation history)이 이미 다음 호출로 답변을 전달하며, 답변의 길이는 사용자가 말한 짧고 사실이 풍부한 내용들을 묻히게 만들기 때문입니다. 핫패스(Hot-path) 쓰기는 의도적인 선택입니다. 처음에는 두 가지 대안이 그럴듯하게 들릴 수 있습니다. 컨텍스트 윈도우(context window)가 새로운 사실을 계속 전달하도록 하는 방식은 열려 있는 세션의 나머지 동안은 작동하지만, 세션이 종료되거나 충돌하는 순간 인컨텍스트 상태(in-context state)는 사라집니다. 세션 간 메모리(cross-session memory)를 구축하는 것이 바로 이 작업의 핵심 목적입니다.

세션 종료 시점에 쓰기 작업을 배치(Batching) 처리하면 세션 간 상태(cross-session state)를 보존할 수 있지만, 이 구현 방식이 의존하는 두 가지 동일 턴(same-turn) 패턴을 깨뜨리게 됩니다. 예를 들어, 사용자가 하나의 메시지 안에서 새로운 기기를 언급하며 자신의 기기 목록을 요청하는 경우, 해당 새로운 사실이 동일한 턴 내에서 나중에 실행되는 회상(recall) 과정에서 가시적이어야 합니다. 왜냐하면 도구 호출(tool calls)은 대화 기록이 아닌 인덱스(index)를 쿼리하기 때문입니다. 또한, 대체(supersession) 흐름은 하나의 도구 호출 배치 내에서 수정된 사실을 쓰고 이를 바탕으로 다시 회상합니다. 지연된 쓰기(deferred writes) 방식을 사용하면 두 패턴 모두 조용히 오작동하게 됩니다. 대신 우리가 지불하는 비용은 사용자 메시지당 한 번의 Elasticsearch 쓰기 작업이며, 이는 단일 대화가 생성하는 볼륨 수준에서 100ms 미만입니다.

어떤 조언이 효과가 있었는지는 응답 본문에 저장하는 것이 아니라, 절차적 인덱스(procedural index)의 success_count / failure_count를 통해 별도로 캡처됩니다.

사용자의 확인(

사용자가 수정을 확인했는지 여부에 따라 결정되며, 사용자가 동의하지 않은 경우 refined_steps를 통해 결정됩니다.

프롬프트는 모든 출력마다 supporting_episode_ids를 요구하므로, 희소한 턴(sparse turn)의 경우 빈 리스트를 반환하고 아무것도 작성하지 않습니다.

중복 제거(Dedup)는 에이전트가 회상(recall)을 위해 사용하는 것과 동일한 하이브리드 검색기(hybrid retriever)를 사용합니다. 각 후보 사실(candidate fact)에 대해 사용자의 시맨틱 인덱스(semantic index)를 대상으로 상위 K개(top-K) 하이브리드 검색을 수행하여 비교 세트를 좁히며, 의미 판단을 위해 오직 해당 후보들만이 LLM으로 전달됩니다. 출력에는 두 가지 추가적인 가드레일(guards)이 적용됩니다. 신뢰도 하한선(confidence floor) 미만의 후보는 제외되며, 가장 높은 유사도 점수가 ≥ 0.90을 기록하여 수락된 사실은 중복으로 처리됩니다. 이 구현체에서 중복 제거는 더 단순합니다. 가장 최근의 약 50개 사실이 "중복하지 마시오"라는 지침과 함께 통합 LLM(consolidation LLM)에 전달되며, LLM 이후의 신뢰도 및 유사도 가드레일은 아직 연결되지 않았습니다. 하이브리드 회상 경로와 브래킷 가드레일(bracketing guards)이 실제 운영 아키텍처(production architecture)입니다. 이 스냅샷은 코퍼스(corpus)가 직접 비교를 수행하기에 충분할 만큼 작기 때문에 LLM이 직접 비교를 수행하는 방식에 의존합니다.

success_countfailure_count는 플레이북(playbooks)에 대한 피드백 루프를 완성합니다. 충분한 대화가 쌓이면, "이것이 작동했다"를 기록하는 동일한 필드가 "다음에는 이것을 먼저 보여줘"라는 신호가 됩니다. 현재 이 카운트들은 기록은 되지만 아직 검색 순위(retrieval ranking)에 편향(bias)을 주지는 않습니다. 해결된 몇몇 티켓들을 살펴보면, 부스트(boost)는 통계적 노이즈 수준입니다. 실제 운영 환경에 연결되어 배포 밀도가 신호를 의미 있게 만들 수 있는 수준이 되면 작동할 것입니다.

에이전트 메모리가 모순과 대체(supersession)를 처리하는 방법

삭제 없이 오직 추가만 하는 메모리는 결국 잘못된 정보를 갖게 됩니다. 사용자가 "에든버러로 이사했어"라고 말하면, 에이전트는 새로운 사실을 기록합니다. 6개월 후에도 기존의 "브리스틀에 거주함"이라는 사실은 여전히 인덱스에 남아 있습니다. 두 정보 모두 회상(recall) 시마다 나타나며, 에이전트는 잘못된 것을 선택하거나 확답을 피하게 됩니다. 신뢰는 빠르게 무너집니다.

해결책은 새로운 도구를 추가하는 것이 아니라 시스템 프롬프트(전체 프롬프트)에 규칙 하나를 추가하는 것입니다. 삭제하는 대신, 에이전트는 '대체(supersedes)'합니다:

AI 자동 생성 콘텐츠

본 콘텐츠는 Lobste.rs AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0