검색을 고치려 시작했다가 기억을 고치고 말았다
TL;DR
62행에서 631행으로 커지자, 검색 품질의 핵심은 검색 코드보다 무엇을 어떻게 기억시키느냐에 있었다.
- 긴 메모리는 키워드 과매칭·벡터 입력 절단·그래프 길이 편향을 함께 일으켰다. 파라미터 조정만으로 해결할 수 없는 코퍼스 문제였다.
- 낡아서 잘못된 행동을 유도하는 기억 47건을 정리했고, id를 보존하는 revise와 수정·삭제 전 이력 보관으로 되돌릴 길을 마련했다.
- 쓰기 규칙 도입 뒤 유입 84건에서 상태 스냅샷은 약 2건, 핸드오프 체인은 0건이었다. 검색을 바꾸지 않은 기간에 hybrid R@1도 벡터 단독을 넘었다.
- 그래프 자동 확장은 측정 후 기각했다. 다음 과제는 긴 기억의 분리, 명시 링크와 정정 간선이며, 검색 한 번의 침묵을 기억의 부재로 판단하지 않는 것이다.
[260821 TIL] 우리들의 추억(aka 기억보관소) mcp 구축기을 쓴 게 8월 21일이다.
그때 코퍼스는 62행, 툴은 remember / recall / list_memories / forget 넷이었다.
6주가 지난 오늘(10-04) 숫자는 이렇다.
| 08-21 | 10-04 | |
|---|---|---|
| 행 수 | 62 | 631 |
| 툴 | 4 | 9 ( list_tags neighbors related_tags retag revise 추가) |
| 테스트 | 없음 ( db:eval 회귀 하네스만) |
vitest 유닛 + db:eval + 라이브 DB 롤백 테스트 |
docs/ 측정 기록 |
0 | 14편 |
첫 번째 기록은 대부분 검색에 관한 이야기였다.
바이그램, 벡터, RRF, 리전 같은 것들.
그 뒤 6주를 돌아보면 검색 코드를 고친 건 상수 몇 줄뿐이고
나머지 시간은 거의 전부 안에 들어 있는 기억 자체를 들여다보는 데 썼다.
행이 62개일 때 문제는 검색이었고 600개가 되니 쓰기였다.
1. 지난번 숙제 채점
첫 기록의 부록 C에 재검토 예정 세 개를 적어 뒀다.
#18 근접 중복 임계값 0.92 — "0.88~0.90으로 내리는 것이 유력"이었다. 틀렸다.
380행 전수로 재 보니 방향이 정반대였다.
| 짧은 쪽 길이 | 평균 cos | 0.92 이상 비율 |
|---|---|---|
| <1,000자 | 0.708 | 0.04% |
| 1,000~2,000자 | 0.738 | 0.21% |
| 2,000자+ | 0.786 | 2.17% |
긴 문서일수록 서로 더 비슷해진다.
2,000자 넘는 쌍이 0.92에 걸릴 확률이 짧은 쌍의 54배다.
긴 메모리들은 공통 프로젝트명, 공통 어휘가 겹쳐서 코사인이 밀려 올라간다.
0.92는 "한 문장짜리 메모리의 패러프레이즈"(0.932~0.934)로 정한 값이었는데
그 사이 메모리가 한 문장이 아니게 됐다.
내렸으면 위양성이 늘었을 것이다.
그런데 이 숙제는 결국 임계값을 고르는 문제가 아니었다는 걸로 끝났다.
10월 2일에 놓친 건 하나가 정확히 0.918이었다(뒤에서 다시 나온다).
임계값을 어디에 두든 0.002 차이로 비껴가는 건 생긴다.
그래서 임계값 하나 대신 0.85 이상 상위 3건을 두 구간으로 보여 주는 쪽으로 갔다.
"재는 도구부터 만든다"고 응답에 점수를 실어 둔 게 이 결론의 근거가 됐다.
#11 벡터 인덱스 — 아직 안 붙였다.
631행이고 지난번에 적은 재검토 조건("수천 행")에 한참 못 미친다.
#20 LangGraph — 여전히 아니다.
다만 재밌는 게 하나 있었다.
지난번에 정한 재검토 신호 중 하나가 "사람 승인을 중간에 끼워야 할 때"였는데
실제로 그게 생겼다.
그런데 생긴 곳은 이 서버 밖, 클라이언트 쪽이었다
(eunoh-dev 어드민의 fourplay 메모리 에이전트가 제안 → 다이얼로그 카드로 사람이 승인한다).
서버는 여전히 요청 하나 = 왕복 하나다.
"서버는 에이전트가 아니다"가 맞았다는 쪽의 증거로 본다.
2. 파라미터는 코퍼스 크기의 함수였다
이건 복잡하니 요약만.
- 62행에서 정한
RRF_K = 60이 265행에서 무너졌다. K=60은 순위를 평탄하게 만들어서 "양쪽 leg에 어중간하게 걸친 긴 문서"가 "벡터 최상위"를 이긴다. K 60 → 10,ts_rank정규화 0 → 1. - 537행이 되자 이번엔 hybrid R@1(9)이 벡터 단독(11) 밑으로 내려갔다. 94회 스윕해서 벡터:키워드 2:1 → 4:1로 옮겼다.
그런데 9월 26일 재측정의 진짜 결론은 4:1이 아니었다.
긴 문서에 강한 페널티를 주는 정규화(2 = 길이로 나눔)에서만 R@1이 벡터 수준으로 올라왔다.
즉 R@1을 끌어내린 건 가중치가 아닌 문서 길이였다.
정규화 2는 긴 정답을 키워드 leg에서 86위로 밀어내서 기각했다(임베딩이 죽어 키워드만 남는 강등 경로에서 못 찾게 된다).
4:1은 증상 완화로 기록했다.
검색 파라미터를 아무리 돌려도 코퍼스가 정한 상한은 못 넘는다.
이걸 알고 나서야 시선이 검색 코드에서 코퍼스로 옮겨 갔다.
3. "한 문장" 계약이 무너져 있었다
remember 툴 설명에는 "완결된 한 문장으로"라고 적혀 있다.
9월 14일 전수 측정 결과는 이랬다.
| 주 | 길이 중앙값 |
|---|---|
| 08-17 | 420자 |
| 08-24 | 1,373자 |
| 09-07 | 2,108자 |
한 달 만에 5배다.
LLM들이 세션 끝에 "오늘 한 일 정리"를 통째로 remember에 넣고 있었다.
핸드오프, 진행 상황, PR 번호, 다음 할 일, 그리고 그 사이사이에 진짜 남겨야 할 함정이나 실측값까지.
여기서 첫 기록 6장의 침묵 하나가 다시 돌아왔다.
잘린 벡터
임베딩 입력 예산은 4,000자다.
넘으면 뒤쪽을 우리가 먼저 자르고 로그를 남긴다 - 지난번에 그렇게 고쳤다.
그런데 로그는 서버에만 남고 저장하는 쪽은 모른다.
저장은 성공한다.
뒤쪽은 벡터에 없고 근접 중복 검사도 그 부분을 못 본다.
최악은 7,830자 중 3,831자가 벡터에서 안 보이는 메모리였다.
처방은 두 개였다.
remember응답에주의: content N자 중 뒤쪽 M자가 …경고. 막지도 자르지도 않고 알리기만 한다.- 이미 넘친 12건은 62조각으로 분할.
분할은 "기억해라고 한 것을 잃지 않는다"는 불변식에 정면으로 닿는 작업이라 안전장치를 많이 걸었다.
원본 전문을 git에 백업하고 조각을 전부 저장·검증한 뒤에야 원본을 지우고
원본에서 패러프레이즈가 불가능한 토큰(숫자, 백틱 인용, 코드 식별자)만 뽑아 조각에 다 남았는지 대조했다.
1,460개 중 누락 0.
표본은 거짓말을 한다
이 측정은 핸드오프 문서 하나에서 출발했다.
그 문서는 list_memories 100건 상한 때문에 최근 100건 표본으로 쟀었다.
전수로 다시 재니 세 군데가 틀렸다.
| 표본 | 전수 | |
|---|---|---|
| 길이 중앙값 | 2,052자 | 990자 |
| 상태성 메모리 비중 | 49% | 20% |
| "번들이라 중복 감지가 죽었다" | 가설 | 반증 (오히려 반대) |
최근 100건은 하필 제일 나빴던 한 주에 몰려 있었다.
"코퍼스의 절반"은 실은 "최근 유입의 절반"이었다.
지우지 말고 떼어내라
상태성 태그(next-step, status, handoff …)가 붙은 7일 넘은 메모리 29건을 전부 읽었다.
| 분류 | 비율 |
|---|---|
| 순수 상태 (지워도 무손실) | 6.9% |
| 혼재 (상태 + 남을 원리) | 65.5% |
| 오분류 (태그만 상태성, 내용은 영구 지식) | 27.6% |
status 태그만 보고 지웠다면 93%에서 뭔가를 같이 잃었을 것이다.
빌드 플레이크 재현 조건, 배포 순서 규칙, 외부 API 차단 사실 같은 것들이 "오늘 여기까지 했음" 사이에 섞여 있었다.
그래서 삭제 대신 분리를 처방했다.
4. 그래프 - 만들었지만, 자동으로는 안 쓰기로 했다
9월 18일엔 "메모리를 그래프로 다룰 수 있나"를 503행 전수로 쟀다.
엣지 후보는 이미 두 개 있었다. 태그 공유, 그리고 임베딩 kNN.
둘이 얼마나 겹치는지가 가장 중요했다.
| kNN top-4 엣지 중 태그도 공유하는 것 | 43% |
| 태그 공유 엣지 중 kNN 이웃이기도 한 것 | 17% |
두 신호가 거의 안 겹친다.
태그는 임베딩이 모르는 걸 알고 그 반대도 참이다.
그리고 나쁜 숫자도 하나.
kNN에서 2,000자 넘는 문서가 500자 미만 문서보다 2.2배 자주 남의 이웃으로 뽑혔다.
이건 길이 아티팩트다(3장의 "긴 문서끼리 비슷해진다"와 같은 현상).
짧은 메모리의 18.4%는 아무의 top-4에도 안 들어가서 그래프로는 영원히 도달할 수 없었다.
하필 짧고 구체적인 쪽이.
1-hop 확장 - 기각
recall 상위 결과의 이웃을 끌어와 다시 융합하면 회수가 오르는가.
| weight | R@1 | R@10 |
|---|---|---|
| 없음 | 11 | 16 |
| 0.05 ~ 0.3 | 11 | 16 |
| 0.4 | 7 | 16 |
| 0.5 | 4 | 16 |
R@10은 어느 가중에서도 안 움직이고 0.4부터 R@1이 무너진다.
seed(상위 5건)는 확장 후보에서 빠지지만 6~20위는 안 빠진다.
그래서 그 행들이 양쪽에서 점수를 두 번 받는다.
R@10을 막고 있던 두 질의는 애초에 1-hop 이웃 안에 정답이 없었다.
같은 이웃 신호도 neighbors 툴로 주면 유용한데 융합에 넣으면 손해다.
누가 언제 부르느냐가 갈랐다.
모델은 "이 동네를 더 보고 싶다"를 알고 부르지만 융합은 모든 질의에 일괄로 건다.
도움이 되는 질의는 소수인데 비용은 전부가 낸다.
확장은 자동 재랭킹 대신 모델이 부르는 도구의 모양으로 가기로 했다.
그래서 neighbors(의미 이웃 + 태그 이웃을 나란히)와 related_tags를 툴로 냈고
eunoh-dev 어드민에 그래프 뷰도 붙였다(엣지는 서버가 계산한다. 화면에 505행 × 1536차원을 보낼 수는 없으니까).
엣지는 전역 임계값 대신 노드당 top-k로 뽑는다.
임계값 0.85 → 0.80에서 엣지가 3.7배 뛰는 벼랑이 있었고 그 벼랑은 코퍼스가 자라면 움직인다.
2장과 같은 교훈이다.
절대값은 코퍼스 크기와 같이 낡는다.
5. 사건 - "유실됐다, 경위 불명"
9월 20일, 맥미니에 옵시디언 자동화 잡을 설치하던 세션.
launchctl list에 잡이 12개뿐이었다. 문서는 13개라고 했다.com.eunoh.personas-prewarm이 없었다.
에이전트는 recall을 한 번 던졌다.
"맥미니 macmini obsidian 리뷰 승격 review promote launchd"
결과에 철거 기록이 없자 "plist가 유실됐다, 경위 불명" 이라고 판단했고
볼트 문서 세 곳과 장기 메모리 두 건에 그렇게 적었다.
사실은 사흘 전에 내가 의도적으로 철거한 거였다.
그 기록은 메모리 안에 멀쩡히 있었다ㅠㅠ
측정해 보니 이랬다.
| 질의 | 정답 순위 |
|---|---|
| 실제로 던진 주제 질의 | MISS |
personas-prewarm |
1위 |
com.eunoh.personas-prewarm |
1위 |
| 1-hop 확장 (seed 10 × 이웃 10) | 0/100 |
정답은 "personas 검색 지연" 이야기 안에 들어 있었다.
에이전트는 "맥미니 자동화"라는 자기 작업의 주제로 물었다.
두 메모리를 잇는 건 personas-prewarm이라는 식별자 하나뿐이었다.
임베딩도 태그도 "무엇에 관한 것인가"의 축이라 식별자 동일성은 어느 쪽에도 없다.
코퍼스에 답이 있었고 검색도 답을 낼 수 있었다. 질문이 틀렸다.
처방은 호출자 쪽에 들어갔다.
서버는 손대지 않고 CLAUDE.md에 두 줄을 넣었다.
- 구체적 식별자(잡 라벨, 파일 경로, 테이블명…)를 쥐었으면 주제가 아니라 그 식별자로 먼저 묻는다.
recall한 번이 비었다고 "없다"고 판단하지 않는다. 표현을 바꿔 2~3회, 필요하면neighbors로 넓힌다.
그리고 이 사건이 구조적 공백 하나를 드러냈다.
지금 그래프의 간선은 전부 "닮았다"다.
그런데 필요했던 건 "이것이 저것을 무효화한다"였다.
비대칭이고 시간 순서가 있고 내용 유사도와 무관하다.
오히려 주제가 멀수록 위험하다.
가까우면 recall이 둘을 같이 끌어오지만 멀면 낡은 쪽만 나온다. 정정은 안 나온다.
6. 침묵은 측정 장치에도 있었다
지난 기록의 결론이 "적은 에러가 아니라 침묵"이었다.
이번 6주에 만난 침묵들은 대부분 검색을 재는 쪽에 있었다.
사라진 gold.
평가셋 정답 하나가 코퍼스에서 지워져 있었다.
없는 id는 조용히 영구 MISS가 되고 달성 가능한 천장이 8에서 7로 내려간다.
그런데 게이트가 GREEN 이었다.
게이트가 전부 상대 비교(hybrid ≥ vector 같은)라서...
이게 좀 아팠다.
9월 5일 기록에서 "상대 단정이 코퍼스 드리프트를 잡아 줬다"고 칭찬했었다.
바로 그 성질이 이번엔 측정 장치의 고장을 숨겼다.
거짓 경보를 막아 주던 설계가 이번엔 진짜 고장을 숨긴 거다.
그래서 dangling gold는 이제 즉시 FAIL이다.
품질 문제가 아니라 장치 고장이니까.
\s가 s가 됐다.
그래프 뷰의 미리보기에서 공백을 축약하려고 regexp_replace(..., '\s+', ' ')를 drizzle sql 태그 안에 썼다.sql 태그는 raw가 아니라 cooked 문자열을 쓴다.
cooked에서 \s는 그냥 s다.
그래서 Postgres에는 's+'가 갔고 몇 주 동안 모든 미리보기에서 소문자 s를 지우고 있었다.
520행 전부 소문자 s 0개, 대문자 S는 멀쩡히 살아 있었다.
에러는 당연히 없었다. 백슬래시 없는 [[:space:]]+로 바꿨다.
타입 선언은 검증이 아니다.neighbors가 거의 100% 죽어 있었다.
e.createdAt.toISOString is not a function.
의미 이웃은 drizzle select라 Date가 오고 태그 이웃은 db.execute<T>() 원시 SQL이라 문자열이 왔다.
<T>는 런타임 검증 없는 캐스트다.
유닛 테스트 픽스처가 new Date(...)로 손으로 만든 거라 영원히 통과했다.
회귀 테스트의 픽스처는 드라이버가 실제로 주는 모양이어야 한다.
세 건 다 같은 모양이다.
값은 그럴듯하고 테스트는 GREEN.
지난번에 "실행해보지 않고 통과시킨 것은 대체로 통과한 게 아니었다"고 적었는데
이번엔 거기에 "재는 도구도 재 봐야 한다"가 붙었다.
7. 1차 평가 - 낡아서 틀린 기억 47건
9월 26일 1차 평가에선 "낡아서 틀린" 메모리를 셌다.
날짜가 오래된 것과 틀린 것은 다르다.
기준은 지금 읽은 사람이 잘못된 행동을 하는가였다.
537행 → 후보 98건 → 전수 판정 → 47건(8.8%).
그리고 그 47건이 한 덩어리가 아니었다.
| 클래스 | 건수 | 예 | 맞는 처방 |
|---|---|---|---|
| A. 상태 스냅샷 | 35 | "PR #48 열려 있음", "다음 세션은 여기서 시작" | 무효화 간선 아님. 애초에 저장하지 않기 |
| B. 지시·판단의 반전 | 12 | "pooler로 되돌려라"(반대가 맞다), "임계값을 내리는 게 유력"(1장) | 무효화 간선의 진짜 표적 |
A가 무서운 건 정정이 영영 안 생겨서다.
"PR이 머지됐다"를 메모리로 남기는 사람은 없다.
정본은 GitHub이고 메모리는 그냥 낡는다.
B는 실행이 싸서 무섭다.
"임계값을 0.88로 내려라"는 상수 한 줄이다.
읽은 에이전트가 바로 고칠 수 있다.
판정하면서 설계 제약도 넷 나왔는데 둘이 특히 기억에 남는다.
- 부분 무효화. 1,569자에 사실 6개가 든 메모리에서 마지막 한 줄만 낡았다. 그런데 나머지 중 하나가 바로 5장 사건의 그 철거 기록이었다. 문서 단위로 "무효"를 찍으면 5장 사건을 그대로 다시 만든다.
- 작성일이 정정보다 늦다. 09-14에 저장된 메모리가 09-07에 이미 폐기된 내용이었다(지난 시점을 소급 정리한 핸드오프). "나중에 쓴 것이 이긴다"는 이 코퍼스에서 틀린다.
둘 다 뿌리가 3장의 입자 문제다.
사실 하나가 메모리 하나였다면 생기지 않았다.
정리 - 그리고 직접 SQL을 친 이유
47건을 처분했다. 삭제 9, 수정 38.
A 33건 중 순수 상태는 5건뿐이었고 나머지는 머리글과 "다음" 절만 낡았다.
그래서 대부분은 낡은 절만 걷어내는 수정으로 처리했다.
수정은 id를 유지해야 했다. 대상 id를 본문에서 참조하는 메모리가 22건이었다.forget + remember로 갈면 id가 바뀌고 그 참조가 전부 끊긴다.
(실제로 9월 14일 분할 이후 5건이 사라진 id를 가리키고 있었다.)
그런데 MCP 툴엔 본문 수정이 없었다.
그래서 라이브 DB에 직접 SQL을 쳤다.
전문 백업을 먼저 뜨고 content 교체 + embedding = NULL → 백필.
8. 2주 전의 나를 뒤집었다 - revise
9월 14일, retag 툴을 만들면서 이렇게 적었다.
content update는 만들지 않는다.
update는 "갱신"으로 읽혀 덜 경계되면서 실제로는 덮어쓰기다.
조용한 손실의 새 입구가 된다.
9월 25일에 revise(id, content)를 만들었다.
2주 전에는 "덮어쓰기는 되돌릴 수 없다"는 이유로 반대했다.
그래서 이번엔 그 전제를 먼저 없앴다.
revise든 forget이든 이전 본문을 별도 표 memory_history에 옮겨 둔 다음에 바꾼다.
(archived_at 컬럼 대신 별도 표로 간 건 memories 읽기 경로 25곳을 건드리지 않으려고.)
그리고 반대편 비용이 커졌다.
id를 못 지키니 사람이 SQL을 치고 있었고
CLAUDE.md는 이미 에이전트에게 "이전 핸드오프는 forget하라"고 시키고 있었다.
에이전트가 비가역 삭제를 자율로 하고 있었던 거다.
되돌릴 수 있어야 맡길 수 있다.
오늘 기준 memory_history에 29행이 있다.
아직 복구 툴은 없다.
필요해지면 만든다.
9. 판단을 넘겼으면, 판단의 품질이 곧 시스템의 품질이다
첫 기록 8장의 결론이 "서버는 에이전트가 아니다. 판단을 반대편으로 넘긴다"였다.
6주 지나고 보니 그 문장에는 뒷면이 있었다.
반대편이 판단을 잘못하면 그게 그대로 코퍼스가 된다.
3장의 거대 번들, 7장의 상태 스냅샷 35건이 전부 반대편의 판단이었다.
그래서 9월 26일 CLAUDE.md에 쓰기 규칙을 넣었다.
- 외부 시스템이 정본인 상태("PR #44 열려 있음")는 저장하지 않는다. 남길 건 상태가 아니라 결정과 근거.
- 사실 하나당 메모리 하나.
- 일부가 낡으면
forget+remember가 아니라revise. - 핸드오프 체인을 쌓지 않는다.
- 태그는 기기가 아니라 내용으로 (
work/personal).
규칙은 권고다. 모델이 따를지는 모른다.
그래서 재발률로 쟀다.
정리 후 6.5일, 새로 들어온 84건을 전부 읽었다.
| 1차 평가 (537건 누적) | 이후 유입 84건 | |
|---|---|---|
| A. 상태 스냅샷 | 35 | ≈2 |
| B. 정정 없이 남은 반전 | 12 | 1 |
| 핸드오프 체인 | 다수 | 0 |
| 일별 길이 중앙값 | 1,344자 (09-21주) | 412~514자 |
revise 사용 |
— | 26 |
꽤 먹혔다!!
그리고 검색 품질이 따라왔다.
612행에서 hybrid R@1이 처음으로 벡터 단독을 넘었다(11 vs 9).
짧은 메모리가 늘면서 긴 문서의 바이그램 과매칭 비중이 줄어든 것 같다.
2장에서 "R@1의 원인은 문서 길이"라고 했던 게 반대 방향으로 확인된 셈이다.
검색을 하나도 안 고쳤는데 검색이 좋아졌다.
그런데 새 문제 셋이 나왔다.
- 놓친 B형 1건이 원본과 코사인 0.918이었다. 근접 중복 임계값 0.92를 0.002 차이로 비껴갔다. → 1장의 결론, "임계값이 아니라 상위 k".
revise가 덧붙이기로 번졌다. 439자 → 1,541자. 틀린 걸 고치지 않고 "구현 완료" 같은 새 사실을 붙였다. 원자적 메모리가 다시 뭉친다.- CLAUDE.md를 안 읽는 쓰기 경로가 있었다. eunoh-dev의 fourplay 메모리 에이전트. AI SDK로 도는 서버 쪽 에이전트라 지시문이 따로 있고 제안 종류가
remember | retag둘뿐이었다. 태그 누락 10건이 전부 이 경로의 한 세션에서 나왔고 놓친 B형 1건은 "겹치지만 새 사실이 있으면 새 부분만remember하라"는 그 지시문을 정확히 따른 결과였다.
3번이 제일 오래 남을 것 같다.
"판단을 반대편으로 넘긴다"고 할 때 나는 반대편을 Claude Code와 ChatGPT 정도로 상상했다.
실제 반대편은 내가 만든 다른 앱의 에이전트까지였고 그쪽은 규칙을 한 줄도 몰랐다.
그래서 10월 2일 작업은 양쪽이었다.
- 서버:
remember응답에 0.85 이상 상위 3건을 두 구간으로. 0.92 이상 「거의 같음」(중복이면 forget, 갱신이면 기존 걸 revise), 0.85~0.92 「같은 주제」(정정이면 기존 걸 revise, 새 사실이면 그대로 두고 덧붙이지 말 것). 0.85라는 하한도 84건의 최근접 코사인 분포에서 나왔다. - 클라이언트: fourplay 지시문에
revise제안 종류와 쓰기 규칙.
서버는 규칙을 강제할 수 없다.
따르기 쉽게 만들 수 있을 뿐이다.
저장하는 바로 그 순간이 낡음을 잡기 가장 싼 때고 그때 후보가 눈앞에 있으면 모델은 꽤 잘 고른다.
그게 지난번 "판단은 넘기되 판단 재료(점수)는 응답에 싣는다"의 연장선이다.
10. 아직 갈 길
목표를 둘로 정리해 뒀다.
| 목표 | 현실적 기대 | |
|---|---|---|
| G1 | recall 뒤에 그래프를 탐색하듯 연결된 기억으로 건너간다 | 만들면 되는 기능이다 |
| G2 | 에이전트가 recall → remember → revise 순환을 돌려 사람이 DB를 직접 고치는 일을 줄인다 | 줄일 수 있지만 0은 안 된다 |
G2는 "검수가 필요 없는 상태"가 아니라 47건짜리 대청소를 가끔 하는 가벼운 점검으로 줄이는 게 목표다.
측정 없이 됐다고 말하지 않기로 했다.
남은 순서.
- 입자 (게이트 650행, 오늘 631행). 새 유입은 이미 짧다. 실제 대상은 9월 21일 이전의 긴 문서들이다. 오늘 기준 2,000자 넘는 메모리가 아직 95건이다.
- 명시 링크 간선. 본문의 id 참조를 파싱해
neighbors에 "나가는 링크 + 백링크"로. 분할처럼 id를 바꾸는 연산이 들어오는 참조도 같이 고쳐야 한다. - 정정 간선 → recall 응답에 표시. 5장의 공백. "이 행에는 정정이 붙어 있다"가 응답에 보여야 모델이
neighbors를 부를 이유가 생긴다.
하지 않기로 한 것도 적어 둔다.
1-hop 자동 확장은 세 번 재서 세 번 기각했다.
다시 제안하지 않는다.
11. 남는 것
첫 기록의 세 문장은 여전히 유효하다. 이번엔 이렇게 셋.
- 검색 품질의 상한은 코퍼스가 정한다.
- RRF를 94번 돌려서 R@1 하나를 올렸고 검색 코드를 안 건드린 쓰기 규칙으로 또 하나가 올랐다. 앞의 것은 증상 완화였고 원인을 건드린 건 뒤의 것이었다. 파라미터는 코퍼스 크기의 함수고 그 함수의 입력은 쓰는 쪽이 만든다.
- 판단을 넘겼으면, 판단의 재료와 되돌릴 길까지 넘겨야 한다.
- 점수와 후보를 응답에 싣고(재료) 이력을 남긴다(되돌릴 길). 규칙은 권고일 뿐이라 재발률로 잰다. 그리고 반대편은 생각보다 넓다.
- 재는 도구도 재 봐야 한다.
- 사라진 gold, cooked 문자열, 캐스트일 뿐인
<T>. 상대 게이트는 드리프트를 잡아 준 그 성질로 고장을 숨겼다. 같은 설계가 양날이다.
- 사라진 gold, cooked 문자열, 캐스트일 뿐인
그리고 하나 더.
5장에서 에이전트는 recall 한 번이 비자 "유실됐다"고 적었다.
한 번의 침묵을 부재의 증거로 삼지 않는다.
지난번엔 내 코드가 에러를 삼키는 침묵이었다.
이번엔 검색 결과가 비어 있었다.
둘 다 조용히 틀린 결론으로 이어졌다.
역시 적은 침묵이다..
부록 A. 결정 기록 (첫 기록 #1~#20에 이어서)
| # | 결정 | 시점 | 검토한 대안 | 근거 | 상태 |
|---|---|---|---|---|---|
| 21 | recall 응답에 leg별 순위·코사인·검색 모드 표기 | 09-05 | 현행(결과만) | 융합 CTE가 계산한 값을 버리고 있었다. 원인을 정반대로 짚었다 | 유효 |
| 22 | RRF_K 60 → 10, ts_rank 정규화 0 → 1 |
09-05 | K 5, 정규화 2 | 265행에서 hybrid < vector. K=60은 순위 평탄화. 정규화 2는 긴 정답을 강등 경로에서 밀어냄 | 유효 |
| 23 | vitest 도입. 유닛은 포맷·SQL 구조, 실 DB는 db:eval |
09-05 | db:eval만 |
응답 계약을 DB 없이 고정 | 유효 |
| 24 | 임베딩 입력 절단은 막지 않고 응답에 알린다 | 09-14 | 거부, 자동 절단 | 저장은 성공해야 한다(불변식). 저장하는 쪽이 몰랐다 | 유효 |
| 25 | 예산 초과 12건 → 62조각 분할, 큐레이션 없이 | 09-14 | 방치 | 최대 3,831자가 벡터에서 안 보임. 하드토큰 1,460개 보존 100% | 완료 |
| 26 | 상태성 메모리는 삭제가 아니라 분리 | 09-14 | 태그 기준 일괄 삭제, expires_at |
29건 중 93%가 혼재·오분류 | 유효 |
| 27 | retag: tags만, 증분(add/remove) |
09-14 | 전체 교체, content update | 전체 교체는 안 읽으면 태그가 날아감. content update는 조용한 손실의 입구 | 유효 (content 쪽은 #36이 번복) |
| 28 | 읽기 전용 JSON API, GET만 | 09-17 | MCP 응답 파싱 | 사람용 문장을 정규식으로 되돌리면 포맷 변경마다 깨진다 | 유효 |
| 29 | 그래프 엣지는 노드당 top-k, 태그·kNN 둘 다 | 09-18 | 전역 임계값, 한 종류만 | 0.85→0.80 벼랑 3.7배. 겹침 43%/17% | 유효 |
| 30 | 1-hop 자동 확장 기각, neighbors/related_tags 툴로 |
09-18 | recall에 융합 | R@10 불변, 0.4부터 R@1 붕괴(중복 가산) | 유효 |
| 31 | 복수 gold(any-of), 사라진 gold는 즉시 FAIL | 09-18 | 단일 gold | 상대 게이트가 장치 고장을 숨겼다 | 유효 |
| 32 | 식별자 우선 질의, 1회 침묵을 부재로 보지 않기 (CLAUDE.md) | 09-20~26 | 서버 검색 개선 | 주제 질의 MISS, 식별자 질의 1위. 그래프 0/100 | 유효 |
| 33 | 벡터:키워드 2:1 → 4:1 | 09-26 | 정규화 2, 벡터 단독 | 537행 R@1 하락. 원인은 길이라 이건 증상 완화 | 유효 (키워드 기여가 게이트에 걸쳐 있음) |
| 34 | 정리는 id 보존 제자리 수정 | 09-25~26 | forget + remember | 대상 id를 참조하는 메모리 22건 | 완료 |
| 35 | CLAUDE.md 쓰기 규칙 (상태 스냅샷 금지·원자성·revise·체인 금지·태그) | 09-26 | 서버 강제 | 47건 중 35건이 상태 스냅샷 | 유효 (재발률로 측정) |
| 36 | revise + forget은 memory_history에 원문 보관 |
09-25 | #27 유지 | 되돌릴 수 있어야 에이전트에게 맡길 수 있다 | 유효 |
| 37 | 대체 후보: 임계값 하나 → 0.85 이상 상위 3건, 두 구간 | 10-02 | 임계값 조정 | 놓친 정정이 0.918. 하한은 84건 최근접 분포 | 유효 (#18 종결) |
| 38 | fourplay 쓰기 경로에도 revise·쓰기 규칙 |
10-02 | 서버 응답만 수정 | fourplay는 응답을 안 받는다. 태그 누락 10건 전부 이 경로 | 유효 |
부록 B. 타임라인
08-24 sanitizeDbError가 cause 체인까지 훑는다 (Supavisor 장애)
09-05 recall 출처 표기 · tags 필터 · list_tags · vitest · RRF K 60→10
09-14 절단 경고 · 전수 측정(표본 정정 3건) · 12건 → 62조각 분할 · retag
09-17 읽기 전용 JSON API (eunoh-dev 어드민 기억 모니터)
09-18 그래프 베이스라인 503행 · neighbors / related_tags · gold 감사 · 1-hop 확장 기각 · 그래프 뷰 API
09-20 personas-prewarm 사건 · neighbors createdAt 버그
09-21 graph preview가 글자 s를 지우던 문제
09-25 RRF 2:1 → 4:1 · revise · forget 이력 보관
09-25~26 1차 평가(47건) · 정리(삭제 9, 수정 38) · CLAUDE.md 쓰기 규칙
10-02 재발률 측정(84건) · remember 같은 주제 후보 · fourplay 쓰기 경로
10-04 631행 · 이 기록
부록 C. 재검토 예정
- 입자 — 650행 게이트. 대상은 09-21 이전의 2,000자+ 문서(현재 95건)
- #33 4:1 — 다음 재측정에서 "키워드 leg 기여"가 0이 되면 다시 본다
- 재발률 2차 — B형(정정을 새 메모리로 저장)이 0이 되는지, revise 덧붙이기가 줄었는지
- #11 벡터 인덱스 — 여전히 수천 행에서
- #20 LangGraph — 여전히 같은 조건. 다만 "사람 승인"은 서버 밖(클라이언트)에서 먼저 생겼다
'TIL' 카테고리의 다른 글
| [260930 TIL] vgpu로 블랙홀 세 번째 시도 (0) | 2026.09.30 |
|---|---|
| [260928 TIL] terraform provider 공유 캐시 사용 (0) | 2026.09.30 |
| [260917 TIL] ANALYZE는 왜 한 시간이 걸렸나 (0) | 2026.09.17 |
| [260913 TIL] Next 16 캐시컴포넌트 - 이게 좋은건지... (1) | 2026.09.13 |
| [260830 TIL] 배럴 export 와 실행 환경 경계 (0) | 2026.08.30 |