들어가며
요즘 AI 코딩 에이전트들도 세션 안에서는 대화 맥락을 잘 붙잡고 있고, 최근엔 프로젝트 파일이나 이전 대화 일부를 요약해 다음 세션에 넘겨주는 기능도 하나둘 생기고 있습니다. 그런데 실제로 매일 써보면 이런 지점에서는 여전히 아쉬움이 남습니다.
- 어제 분명히 "이 프로젝트는 FastAPI + asyncpg로 간다"고 정해놨는데, 오늘 새 세션을 열면 그 결정을 다시 설명해야 하는 경우가 많다
- 어떤 버그를 30분 걸려 해결했는데, 다음 주에 비슷한 문제가 또 나오면 그 해결 과정이 자연스럽게 이어지지 않는다
- 프로젝트가 여러 개인데, 세션을 옮길 때마다 "지금 이거 어떤 프로젝트였지?"부터 다시 맞춰야 한다
즉 지금의 에이전트들이 "완전히 기억을 못 한다"기보다는, 세션을 넘어서는 장기 기억이나 여러 세션·프로젝트 간의 기억 공유까지는 아직 손이 잘 안 닿는 영역이라는 느낌이었습니다.
이 부분을 좀 더 채워보고 싶어서 Hindsight라는 에이전트 메모리 시스템을 Codex에 붙여봤는데, 생각보다 설정 과정에서 삽질할 지점이 많았습니다...
이번 글에서는 Hindsight가 어떤 원리로 동작하는지, 그리고 실제로 붙이면서 겪었던 문제들과 검증 과정을 정리해보려고 합니다.
Hindsight란?
한 줄로 요약하면 에이전트를 위한 장기 기억 엔진입니다.
다만 "기억"이라는 말 때문에 오해하기 쉬운데, Hindsight는 대화 로그를 통째로 저장했다가 다시 꺼내주는 방식이 아닙니다. 사람이 기억하는 방식에 더 가깝습니다.
우리가 어떤 대화를 기억할 때는
- 문장 그대로 녹음해서 저장하는 게 아니라
- "핵심이 뭐였는지"를 요약하고
- 그게 다른 기억들과 어떻게 연결되는지(누구랑 있었지, 언제였지, 무슨 일 때문이었지)를 같이 저장합니다
Hindsight도 똑같은 방식으로 동작합니다. 대화 트랜스크립트를 그대로 쌓아두는 게 아니라, narrative fact라는 자기완결적인 사실 단위로 재구성해서 저장합니다. "사용자가 X 버그를 Y로 해결했다"처럼, 여러 턴에 걸친 맥락이 하나의 사실 안에 압축되는 식입니다.
이 핵심 동작이 세 가지 연산으로 나뉩니다.
| 연산 | 하는 일 |
|---|---|
| Retain | 대화를 구조화된 사실로 변환해서 저장 |
| Recall | 지금 필요한 맥락에 맞는 기억을 검색해서 가져옴 |
| Reflect | 여러 기억을 종합해서 새로운 인사이트나 의견을 형성 |
Retain — 대화가 기억으로 바뀌는 과정
Retain은 마치 택배 포장의 역과정 같습니다. 택배가 여러 겹의 포장(계층)을 거치며 목적지로 가듯, Hindsight는 날것의 대화 텍스트를 여러 단계를 거쳐 구조화된 기억으로 압축합니다.
- Fact 추출: LLM이 대화를 읽고 "이 안에서 기억할 만한 사실이 뭐지?"를 판단해서 뽑아냅니다. 단순 문장 분리가 아니라, 여러 턴에 걸친 맥락을 하나의 사실 안에 보존합니다.
- 엔티티 해소: "그 프로젝트", "아까 그 버그"처럼 다르게 표현된 대상을 하나의 개체로 통일합니다.
- 임베딩 생성: 나중에 검색할 수 있도록 각 사실을 벡터로 변환합니다.
- 그래프 링크 구성: 시간적으로 가까운 사실끼리, 의미적으로 비슷한 사실끼리, 같은 엔티티를 공유하는 사실끼리 서로 연결합니다.
특히 흥미로웠던 부분은 시간을 두 축으로 따로 기록한다는 점이었습니다. "그 일이 실제로 언제 일어났는지"와 "그 얘기를 언제 들었는지"를 구분해서 저장하는 건데, 이게 있어야 "지난주에 결정했다고 들었지만, 실제로는 한 달 전 결정이었다" 같은 미묘한 시간 관계도 놓치지 않습니다.
실제 저장 구조를 조금 더 찾아보니, 이 모든 게 banks(은행) 아래 memory_units(추출된 fact), documents(원본 문서), entities/entity_links(엔티티와 그 관계) 같은 테이블로 나뉘어 저장된다고 합니다. 제가 로그에서 봤던 "temporal links 140개, semantic links 102개"가 결국 이 entity_links 계열 테이블에 쌓이는 데이터였던 셈입니다.
Recall — 지금 필요한 기억을 꺼내오는 과정
Recall은 단순 키워드 검색이나 벡터 유사도 검색 하나로 끝내지 않습니다. 아래 네 가지를 동시에 병렬로 돌린 뒤 결과를 합칩니다.
- Semantic: 의미가 비슷한 기억 (벡터 유사도)
- BM25: 키워드가 겹치는 기억
- Graph: 그래프 상에서 연결된 기억 (직접 매칭되지 않아도 관련 있는 것까지 추적)
- Temporal: 시간적으로 가까운 기억
이 네 결과를 RRF(Reciprocal Rank Fusion)로 합친 다음, cross-encoder로 한 번 더 정밀하게 재채점해서 최종적으로 몇 개만 골라 프롬프트에 끼워 넣습니다. 벡터 검색만 썼다면 놓쳤을 "간접적으로 연결된 기억"까지 그래프 순회로 찾아낼 수 있다는 게 이 구조의 핵심입니다.
참고로 이번 글에서는 다루지 않지만, Hindsight는 Retain/Recall 위에 Reflect라는 연산을 하나 더 두고 있습니다. 검색된 기억을 그냥 반환하는 게 아니라, "요약된 판단(mental model) → 중간 통찰(observation) → 원본 fact" 순으로 단계적으로 근거를 찾아가며 에이전트의 성향(disposition)까지 반영해 응답을 재구성하는 방식이라고 하는데, 이 부분은 기회가 되면 따로 다뤄보려고 합니다.
실제로 Codex에 붙여보기
원리는 이해했으니, 이제 실제로 설치해서 붙여봤습니다.
curl -fsSL https://hindsight.vectorize.io/get-codex | bash
설치 스크립트가 훅 스크립트(SessionStart / UserPromptSubmit / Stop)를 심어주고, ~/.codex/config.toml과 ~/.codex/hooks.json까지 자동으로 설정해줍니다. 여기까지는 순조로웠는데, 실제로 동작을 검증하는 과정에서 삽질이 좀 있었습니다.
삽질 1 — JSON 문법 오류
개인 설정 파일(~/.hindsight/codex.json)을 수정하다가 쉼표 하나를 빼먹었습니다.
{
"hindsightApiUrl": "http://127.0.0.1:9077"
"dynamicBankId": true
}
이러면 JSON 파싱 자체가 실패하는데, 문제는 에러가 아무 데도 안 뜬다는 거였습니다. 훅 스크립트가 이 파일을 못 읽으니 조용히 기본 설정으로 폴백해버려서, 겉으로는 아무 이상 없어 보이는데 원하는 설정(프로젝트별 기억 분리)이 전혀 반영이 안 되고 있었습니다. python3 -c "import json; json.load(...)"로 파싱을 직접 검증해보고 나서야 원인을 잡았습니다.
삽질 2 — localhost vs 127.0.0.1
로컬 데몬은 분명 정상 실행 중이라고 나오는데, curl http://localhost:9077/health는 계속 연결 실패였습니다. 원인은 macOS에서 localhost가 IPv6(::1)로 먼저 풀리는데, 서버는 IPv4로만 바인딩되어 있었던 것. 127.0.0.1로 직접 찍으니 바로 됐습니다.
curl http://127.0.0.1:9077/health
# {"status":"healthy","database":"connected"}
이후로는 설정 파일에도 localhost 대신 127.0.0.1을 명시적으로 박아뒀습니다.
삽질 3 — 은행(bank)이 안 나뉘는 문제
여러 프로젝트를 오가며 작업하는데, 기억이 전부 codex라는 은행 하나에 몰려서 저장되고 있었습니다. 알고 보니 dynamicBankId가 기본값 false라서, 어떤 프로젝트에서 작업하든 정적으로 지정된 bankId: "codex" 하나만 쓰고 있었던 것. 아래처럼 바꾸고 나서야 프로젝트 경로 기준으로 은행이 자동 분리됐습니다.
{
"dynamicBankId": true,
"dynamicBankGranularity": ["project"]
}
이게 진짜 동작하는지 검증하는 방법
설정을 끝냈다고 끝난 게 아니었습니다. "Recall이 검색을 했다"와 "그 결과를 에이전트가 실제로 참고했다"는 다른 이야기라서, 몇 단계로 나눠서 검증했습니다.
- Retain 로그 확인: 세션 종료 후 daemon 로그에서 fact 추출 개수, 그래프 링크 생성 개수가 찍히는지 확인
- Recall 로그 확인: 새 세션에서 프롬프트를 보냈을 때 관련 기억이 몇 건 검색됐는지(
N facts) 확인 - 세션 트랜스크립트 확인: Codex가 세션을 JSONL로 저장해두는데, 이 파일 안에
hindsight_memories태그가 실제로 포함되어 있는지 grep으로 검색 — 이게 "검색된 기억이 실제로 프롬프트에 주입됐다"는 가장 직접적인 증거였습니다 - 행동 테스트: 한 세션에서 특정 정보를 말해두고, 완전히 새 세션에서 그 정보를 직접 언급하지 않은 채 관련 질문을 던져서 에이전트가 알아서 그 맥락을 참고하는지 확인
이 네 단계를 다 통과하고 나서야 "설정이 됐다"가 아니라 "실제로 동작한다"고 확신할 수 있었습니다.
개발자가 실무에서 체감하는 포인트
1) 완전 자동은 아니다
Retain이 뭘 기억할지는 LLM이 판단합니다. retainMission으로 "기술적 결정, 코드 패턴, 디버깅 솔루션은 남기고 인사말은 무시해라" 같은 지침을 줄 수 있지만, 100% 정확하게 걸러지진 않습니다. 중요한 결정은 여전히 문서(AGENTS.md 같은)로 남겨두는 게 안전합니다. Hindsight는 그 문서화되지 않은 암묵적 맥락을 보완하는 역할로 보는 게 맞는 것 같습니다.
2) 응답 속도에 지연이 붙는다
Recall 쿼리 하나당 대략 0.1~0.3초, Retain(fact 추출)은 LLM 호출이 들어가다 보니 세션 종료 시 10초 이상 걸리는 경우도 있었습니다. 다만 retain은 백그라운드로 처리되기 때문에 응답 자체를 막지는 않았습니다.
3) 프로젝트/에이전트 단위 격리 설계가 중요하다
같은 프로젝트 안에서는 세션이 여러 개여도 기억이 섞이는 게 오히려 유리하고, 프로젝트가 다르면 반드시 분리되어야 합니다. dynamicBankGranularity를 어떻게 잡느냐에 따라 이 경계가 정해지기 때문에, 처음 설정할 때 이 부분을 명확히 정하고 시작하는 게 좋습니다.
4) Codex 전용이 아니다
Hindsight는 Codex 말고도 Claude Code, Cursor, LangGraph, CrewAI 등 다양한 에이전트/프레임워크에 동일한 방식으로 붙일 수 있습니다. 심지어 MCP 서버로도 노출되기 때문에, MCP를 지원하는 클라이언트라면 어디서든 같은 기억 저장소를 공유하는 것도 가능합니다. 프로젝트 경로 기준으로 은행이 분리되도록 설정해두면, Codex에서 학습된 기억을 Claude Code 세션에서도 그대로 이어받는 구조를 만들 수 있습니다.
마치며
에이전트에게 "기억"을 붙인다는 게 처음엔 단순히 대화 로그를 DB에 쌓는 정도로 생각했는데, 실제로 뜯어보니 fact 추출 → 엔티티 해소 → 그래프 구성 → 다중 전략 검색 → 재랭킹까지, 사람이 무언가를 기억하고 떠올리는 과정을 흉내 내려는 시도에 가까웠습니다.
설정 과정에서 겪은 삽질(쉼표 하나, IPv4/IPv6 문제, 은행 분리 누락)은 다 사소한 실수였지만, 그만큼 "설정했다"와 "제대로 동작한다"는 꼼꼼히 검증하지 않으면 구분이 안 된다는 것도 느꼈습니다. 특히 조용히 폴백되는 케이스(JSON 파싱 실패)는 겉으로 아무 에러도 안 보여서 가장 찾기 까다로웠습니다.
앞으로 더 써보면서 recall이 실제로 유용한 맥락을 잘 가져오는지, 아니면 노이즈가 더 많은지 체감해보고 recallBudget이나 retainMission 같은 튜닝 포인트를 조정해볼 생각입니다.
읽어주셔서 감사합니다.

