에이전트 메모리를 위해 벡터 데이터베이스가 반드시 필요한 것은 아닙니다
요약
AI 에이전트의 메모리 구현 시 벡터 데이터베이스가 항상 정답은 아닙니다. 저자는 의미론적 메모리와 이름 지정 메모리를 구분하여, 정확한 사실과 상태를 저장하는 데는 벡터 검색보다 키-값 방식의 접근이 더 효율적임을 설명합니다.
핵심 포인트
- 의미론적 메모리와 이름 지정 메모리의 개념적 분리 필요
- 벡터 검색은 유사도 기반 검색에 적합하며 정확한 사실 저장에는 부적합할 수 있음
- 에이전트의 컨텍스트 유지를 위해 키-값 형태의 메모리 설계 고려 권장
AI 에이전트에게 메모리를 부여하는 모든 가이드는 동일한 방식으로 시작됩니다. 임베딩 모델 (embedding model)을 선택하고, 벡터 데이터베이스 (vector database)를 구축하며, 데이터를 청킹 (chunking)하고, 검색 (retrieval)을 튜닝하는 것입니다. 저 또한 정확히 그렇게 할 것이라고 가정했습니다. 하지만 단 한 줄의 코드도 작성하기 전에, 제가 실제로 해결하려고 하는 문제를 면밀히 살펴보았고, 그 과정에서 이 중 그 어떤 것도 필요하지 않은 부분을 발견했습니다.
이것이 바로 제가 벡터 데이터베이스도, 임베딩 (embeddings)도, 모델이 루프에 포함되지도 않는 에이전트용 메모리 API를 구축하게 된 논리적 근거입니다. 또한 저는 그 결정이 언제 틀리는지에 대해서도 정확히 말씀드릴 것입니다. 왜냐하면 그 결정이 틀리는 경우가 빈번하기 때문이며, 여러분은 저의 아키텍처를 포함하여 그 누구의 설계도 복제하기 전에 자신이 선의 어느 쪽에 있는지 알아야 하기 때문입니다.
아무도 묻지 않았던 질문
에이전트들이 실제로 계속 실패하고 있는 지점은 바로 이것입니다. "이것과 의미상 유사한 것을 찾아줘"가 아닙니다. 그들은 "지난 세션에서 내가 말했던 특정한 것을 기억해줘"라는 요청에서 실패했습니다.
이 글을 읽는 대부분의 사람들이 경험했을 코딩 에이전트의 사례를 들어보겠습니다. 월요일에 당신을 도와주었던 에이전트는 화요일이 되면 당신이 이미 Postgres를 사용하기로 결정했다는 사실도, 배포 (deploy) 단계가 특정 스크립트를 통해 실행된다는 점도, 클라이언트의 이름이 특정한 방식으로 철자가 구성되어 있다는 점도 전혀 알지 못합니다. 모든 세션은 제로(zero) 상태에서 시작됩니다. 당신은 매일 아침 똑같은 컨텍스트 (context)를 다시 설명해야 합니다.
이것은 검색 (search) 문제가 아닙니다. 여기에는 모호한(fuzzy) 요소가 전혀 없습니다. 당신은 되돌려 받고 싶은 정확한 내용을 알고 있으며, 그것을 무엇이라 불러야 하는지도 알고 있습니다. 그것은 키 (key), 값 (value), 그리고 나중에 변경되지 않은 상태로 그것을 읽을 수 있는 능력입니다.
벡터 검색 (Vector search)은 다른 질문에 답합니다: "이 쿼리 (query)와 의미상 가장 유사한 것들은 무엇인가." 그것은 진정으로 어렵고 진정으로 유용한 능력입니다. 하지만 그것은 "이 프로젝트를 위해 Postgres를 선택했다는 것을 기억해줘"라는 요구사항에 필요한 것이 아닙니다. 그 요구사항에는 이름 아래에 저장하고, 나중에 그 이름으로 신뢰할 수 있게 다시 가져오는 기능이 필요합니다.
혼동되는 두 가지 종류의 메모리
일단 이 둘을 분리하고 나니, 전체적인 설계가 자연스럽게 도출되었습니다.
의미론적 메모리 (semantic memory)가 있습니다. 이는 찾고자 하는 정확한 항목은 모르지만, 대략적으로 무엇에 관한 것인지만 알고 있는 방대한 텍스트 본문에서 정보를 검색하는 것입니다. "설계 문서에서 속도 제한 (rate limits)에 대해 뭐라고 했지?"와 같은 경우입니다. 정확한 명칭을 말할 수 없더라도 관련 구절을 찾고 싶은 것이죠. 이것이 바로 임베딩 (embeddings)과 벡터 데이터베이스 (vector databases)가 쓰이는 용도이며, 이들은 이 작업에 매우 능숙합니다.
그다음은 이름이 지정된 메모리 (named memory)입니다. 이는 지칭할 수 있는 사실과 상태를 의미합니다. "사용자는 미터법을 선호한다.", "이 프로젝트는 금요일에 배포된다.", "마지막 인보이스 번호는 1043이었다."와 같은 것들입니다. 여기서는 의미를 통해 검색하는 것이 아닙니다. 무언가 구체적인 것을 저장했고, 그것을 이름(명칭)으로 다시 불러오고 싶은 것입니다.
사람들이 흔히 "에이전트 메모리 (agent memory)"라고 부르는 것 중 상당수는 첫 번째 종류의 옷을 입고 있는 두 번째 종류입니다. 튜토리얼에서 시맨틱 검색 (semantic search)을 보여주기 때문에 본능적으로 시맨틱 검색을 선택하게 되지만, 실제 필요한 것은 이름이 지정된 사실들을 저장하고 다시 읽어올 수 있는 신뢰할 수 있는 장소입니다.
벡터 데이터베이스가 적절한 선택인 경우
이 부분이 중요한 지점이며, 제가 나머지 글을 솔직하게 쓸 수 있는 이유이기도 합니다.
만약 여러분의 문제가 비정형 코퍼스 (unstructured corpus)에 대한 검색이라면, 임베딩 (embeddings)이 필요합니다. 만약 에이전트가 인덱싱 방법을 전달받지 못한 수백 개의 문서에 대해 질문에 답해야 하거나, 재현율 (recall)이 모호해도 괜찮거나, "관련된 것을 찾아줘"가 작업의 전부라면, 벡터 데이터베이스가 올바른 도구이며 키-값 저장소 (key-value store)는 잘못된 선택입니다. 단순함만을 강조하는 설득에 속아 올바른 아키텍처 (architecture)를 포기하지 마세요. 만약 여러분의 문제가 그 형태라면, 이 탭을 닫고 가서 임베딩을 설정하십시오. 그 편이 더 행복할 것입니다.
실수는 벡터 검색을 사용하지 않는 것이 아닙니다. 실수는 처음부터 시맨틱 (semantic)하지 않았던 문제에 대해 벡터 검색을 기본값으로 설정하는 것입니다.
벡터 데이터베이스를 추가하지 않음으로써 얻는 것
내 문제가 시맨틱 메모리가 아니라 이름이 지정된 메모리라고 결정함으로써, 기계 장치의 한 카테고리 전체를 고려 대상에서 제외할 수 있었습니다. 모든 쓰기 작업 시 임베딩 단계가 필요 없습니다. 튜닝해야 할 인덱스 (index)도 없습니다. 모델을 변경할 때 다시 임베딩할 필요도 없습니다. 당연히 나와야 할 검색 결과가 왜 3위로 밀려났는지 디버깅할 필요도 없습니다. 실행하고 비용을 지불해야 할 두 번째 인프라 (infrastructure)도 필요 없습니다.
남은 것은 가장 좋은 방식으로 지루합니다. 키를 지정하고 값을 저장한 다음, 남겼던 그대로 읽어옵니다. 원한다면 TTL(Time To Live)을 설정하여 사실들이 스스로 만료되게 할 수도 있습니다. 에이전트가 무엇을 알고 있는지 목록으로 확인하고 그것에 대해 문자열 검색을 수행할 수 있습니다. 이것은 거의 전체 표면적(surface area)에 가깝습니다.
그리고 그 단순함 속에 숨겨진 신뢰라는 지점이 있습니다. 이것이 메모리입니다. 에이전트가 세상에 대해 믿는 것이 바로 이 것입니다. 저는 제가 완전히 검사하고 확실하게 추론할 수 있는 저장소가, 제가 심문해야 하는 유사성 순위(similarity ranking)보다 훨씬 낫다고 생각합니다. 이 특정 작업의 경우, 지루하지만 검사 가능한 버전은 타협이 아닙니다. 그것은 기능입니다.
전환점: 이것이 여러분의 Postgres 테이블을 능가하는 이유
이 시점에서 솔직한 반론은 이렇습니다. 좋습니다. 만약 단순히 키와 값이라면, 제가 이미 실행하고 있는 Postgres에 왜 테이블을 만들지 않느냐고요? 단일 에이전트에게는 그렇게 하는 것이 맞습니다. 그것은 비즈니스가 아니라 CREATE TABLE입니다.
여러 개의 에이전트가 서로의 쓰기(write) 내용을 읽어야 하는 순간 상황이 바뀝니다. 플래너(planner)가 실행자(executor)에게 작업을 넘기고, 리서치 에이전트가 작가 에이전트(writer agent)를 위해 발견한 내용을 남기는 식입니다. 이제 여러분은 메모리를 저장하는 것이 아니라 프로세스 전반에 걸쳐 상태(state)를 공유하고 있으며, 갑자기 지루하지만 어려운 부분들—에이전트들이 서로 충돌하지 않도록 하는 네임스페이스 지정(namespacing), 올바른 에이전트가 올바른 메모리를 보도록 하는 스코핑(scoping), 권한 모델, 키 관리—을 직접 구축하고 있는 것입니다. 이것이야말로 다시 작성할 가치가 없는 부분입니다. 에이전트 간의 공유 메모리(Shared memory across agents)를 제공받는 것은, 이미 가지고 있는 테이블의 컬럼보다는 여기 서비스(service)를 사용해야 하는 실제 이유입니다.
내구성(Durability)과 TTL은 일반적인 기능입니다. 접근 모델이 이미 해결된 공유 상태(Shared state)는 그렇지 않습니다.
의도적으로 아닌 것, 그리고 다음 단계
이것은 의미 기반 검색(semantic search)을 수행하지 않으며, 그렇게 할 것처럼 가장하지도 않습니다. 문서를 읽고 무엇이 중요한지 결정하지 않습니다. 여러분이 기억될 것을 지시합니다. 만약 코퍼스(corpus)에 대한 의미 기반 검색을 원한다면, 이것은 잘못된 계층이며 저는 그렇게 말씀드릴 것입니다.
제가 알고 있는 한 가지 부족한 점은—누군가 공개적으로 저를 압박했기에 알게 된 사실이지만—메모리의 생명주기 상태(lifecycle state)입니다. 즉, 사실(fact)을 삭제하는 대신 '은퇴(retired)' 상태로 표시하고, 그것을 대체한 정보와 연결함으로써 에이전트가 어떤 메모리가 여전히 하중을 견디고 있는지(load-bearing), 그리고 어떤 것이 단순한 과거 기록인지를 구분할 수 있게 하는 방법입니다. 이것은 의미론적(semantic) 문제가 아니라 시간적(temporal) 문제이며, 바로 그렇기 때문에 임베딩(embeddings)을 끌어들이지 않고도 이러한 저장소(store)에 속해야 하는 것입니다. 이것이 제가 다음에 실제로 구축하고 있는 것입니다.
만약 에이전트 메모리의 필요성이 진정으로 의미론적이라면, 벡터 데이터베이스(vector database)를 사용하십시오. 만약 그것이 이름이 지정된 사실(named facts)과 공유 상태(shared state)에 관한 것이라면, 당신은 문제에서 요구하는 것보다 훨씬 더 많은 장치(machinery)를 찾으려 했던 것일 수 있습니다. 그것이 깨달음의 전부였으며, 저는 모든 튜토리얼이 가리키는 도구 대신 제가 직접 찾았기를 바랐던 도구를 만들었습니다.
저는 동의를 얻기보다는 이 추론이 어디서 어긋나는지 지적받기를 바랍니다. 그러니 허점이 보인다면 말씀해 주십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기