코딩 에이전트의 메모리를 위해 벡터 검색을 버렸다. FTS5의 승리.
요약
코딩 에이전트의 메모리 구현 시 벡터 검색 대신 SQLite의 FTS5 전문 검색을 사용하는 것이 더 효율적임을 설명합니다. 로그, 에러 코드, 스택 트레이스 등 키워드 중심의 데이터 검색에는 의미론적 유사도보다 리터럴 매칭이 더 정확하기 때문입니다.
핵심 포인트
- 코딩 에이전트 쿼리는 의미론적 유사도보다 키워드 일치가 중요함
- 벡터 검색은 에러 코드나 특정 로그 라인 같은 정밀한 검색에 취약함
- SQLite FTS5를 사용하면 임베딩 모델이나 벡터 DB 없이도 구현 가능
- BM25 알고리즘을 통해 별도 설정 없이도 높은 관련성 점수 제공
제가 읽어본 모든 "에이전트에게 메모리를 부여하는 방법" 튜토리얼은 모두 동일한 스택을 제안합니다. 문서를 청크(chunk)로 나누고, 임베딩(embedding)한 뒤, 벡터를 데이터베이스에 넣고, 쿼리 시점에 코사인 유사도(cosine similarity)를 계산하는 방식이죠. 그래서 저의 코딩 에이전트가 모델의 컨텍스트 윈도우(context window)에 원문을 그대로 쏟아붓지 않으면서도, 인덱싱된 도구 출력, git 로그, 가져온 문서들을 검색할 수 있게 만들어야 했을 때, 저 역시 벡터 스토어(vector store)를 구축해야 한다고 가정했습니다.
하지만 저는 그렇게 하지 않았습니다. 대신 SQLite의 FTS5 전문 검색(full-text search)을 사용했는데, 이 특정 작업에 있어서 그것은 타협안이 아니라 더 나은 도구였습니다.
실제 문제가 무엇이었나
제가 만든 도구(context-mode, 대규모 명령 출력 및 API 응답을 모델의 컨텍스트 외부로 라우팅하기 위한 도구)는 다음과 같은 쿼리에 답해야 합니다:
- "실패하는 테스트 (failing tests)"
- "HTTP 500 에러 (HTTP 500 errors)"
- "비동기 라우트 핸들러 (async route handlers)"
이 쿼리들은 임의의 셸 출력, JSON 응답, 가져온 웹 페이지를 대상으로 수행되며, 한 번 인덱싱되면 세션이 필요할 때마다 얼마든지 검색할 수 있습니다. 초보적인 방식은 모든 것을 컨텍스트에 쏟아붓고 모델이 그것을 읽게 만드는 것입니다. 하지만 출력 내용이 50KB의 테스트 로그가 되고, 단 세 줄이면 충분할 요약을 위해 컨텍스트 윈도우의 절반을 태워버리기 전까지만 그 방식이 유효합니다.
벡터가 단순한 대안이 아니라, 여기서 잘못된 기본값인 이유
벡터 검색(Vector search)은 "이것과 의미론적으로 유사한 것이 무엇인가"라는 질문에 답하기 위해 만들어졌습니다. 지원 티켓, 문서, 채팅 기록처럼 동일한 아이디어가 서로 다른 단어로 표현되어, "비밀번호를 어떻게 재설정하나요"라는 질문이 "계정 복구 단계"라는 제목의 문서와 매칭되어야 하는 산문(prose)을 검색할 때는 올바른 도구입니다.
코딩 에이전트 (Coding-agent)의 쿼리는 대부분 그런 성격이 아닙니다. "HTTP 500 errors"는 제가 근사치를 구하고 싶은 모호한 의미론적 (semantic) 개념이 아닙니다. 이는 더 나은 랭킹 (ranking)을 가진 리터럴 (literal) grep에 더 가깝습니다. 검색 대상이 되는 콘텐츠 또한 스택 트레이스 (stack traces), 로그 라인 (log lines), JSON 키 (keys), 에러 코드 (error codes)와 같이 구조화되어 있고 키워드가 밀집되어 있습니다. 스택 트레이스를 임베딩 (embedding)하고 코사인 유사도 (cosine similarity)를 비교하는 방식은, "이 두 단락은 유사한 주제에 관한 것이다"라는 점에는 능숙하지만 "이 라인에 ECONNREFUSED 문자열이 포함되어 있다"라는 점에는 서툰 벡터 표현 (vector representation)을 위해, 실제로 중요한 것(리터럴 예외 이름, 리터럴 라인 번호)을 버리는 행위입니다.
FTS5는 정확히 이를 위해 구축되었습니다. 토큰화 (tokenized)되고 인덱싱 (indexed)된, 정확하거나 거의 정확한 용어 일치에 대한 전체 텍스트 검색 (full-text search)을 제공하며, 별도의 설정 없이도 BM25 스타일의 관련성 점수 (relevance scoring)를 지원합니다.
실제 구현 모습
임베딩 모델 (embedding model)도, 벡터 데이터베이스 (vector database)도, 임베딩 계산을 위한 네트워크 왕복 (network round-trip)도 필요 없습니다. 표준 라이브러리 (stdlib)만 있으면 됩니다:
import sqlite3
conn = sqlite3.connect("index.db")
...
이것이 엔진의 전부입니다. snippet()은 매칭된 부분 주변의 하이라이트된 컨텍스트 (context)를 무료로 제공합니다. rank는 BM25 정렬을 무료로 제공합니다. 인덱싱된 테스트 출력 배치에 대해 "HTTP 500 errors"를 쿼리하면, 의미론적으로 가장 가까운 단락이 아니라, 용어 빈도 (term frequency)와 희귀도 (rarity)에 따라 정렬된 500과 error를 포함하는 실제 라인들을 반환합니다.
이 방식이 실패할 지점 — 그리고 왜 여기서는 그렇지 않은가
만약 쿼리가 진정으로 의미론적 매칭 (semantic matching)을 필요로 한다면 FTS5는 나쁜 선택입니다. 예를 들어 "비밀번호 재설정에 관한 문서를 찾아줘"라는 쿼리는 "계정 복구 (Account Recovery)"와 매칭되어야 하는데, 임베딩 없이는 어떤 토큰화 (tokenization)로도 여기에 도달할 수 없습니다. 만약 제가 용어가 일관되지 않은 산문 형태의 문서 지식 베이스 (knowledge base)를 대상으로 검색을 구축한다면, 벡터를 사용할 것이며, 아마도 하이브리드 (hybrid) 방식(재현율 (recall)을 위한 BM25와 의미론적 재정렬 (semantic re-ranking)을 위한 벡터)을 사용할 것입니다.
하지만 에이전트 자체의 도구 출력(tool output), 오류 로그(error logs), 그리고 가져온 API 응답은 여러분(또는 에이전트)이 해당 용어를 염두에 두고 쿼리를 작성했기 때문에, 검색하려는 실제 용어들로 밀집되어 있습니다. '테스트 실패(Failing tests)'라는 쿼리는 FAIL, AssertionError, 테스트 이름 등 — 실제로 로그에 존재하는 단어들과 함께 공존할 것입니다. 임베딩을 정당화하는 의미론적 격차는 이 도메인에서는 대부분 존재하지 않습니다.
일반화 가능한 교훈
'의미론적 검색 추가(Add semantic search)'는 '캐시 추가(add a cache)'나 '큐 추가(add a queue)'처럼 반사적인 반응이 되었습니다. 즉,
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기