기본 모델을 바꾼 뒤로 /ingest를 실사용하다가 이상한 느낌이 들었다.
새 원본이 들어오면 거의 항상 노트 하나로만 요약되고, 분할은 드물게만
일어났다. 착각인지 확인하려고 실제 원본 하나를 다시 들여다봤다.
다리오 아모데이와, 그 옆의 다니엘라
한 뉴스 기사를 ingest한 결과를 보니 “다리오 아모데이와 앤트로픽의 AI 안전 리더십”이라는 노트 하나가 만들어져 있었다. 그런데 그 기사 안에는 다리오의 여동생이자 앤트로픽 공동창업자인 다니엘라 아모데이도 별도 박스로 다뤄지고 있었다 — 명백히 구분되는 인물인데, 노트는 하나로만 나왔다.
배치 크기를 의심했다
같은 원본을 혼자 넣었을 때는 10번 중 10번 다 다니엘라가 별도 노트로 분할됐다. 근데 무관한 원본 2개, 5개와 함께 묶어서 넣으니 분할 비율이 7/10, 4/10으로 떨어졌다. 원본을 몇 개씩 묶어서 한 번에 처리하는 배치 크기 설정이 6이었는데, 이게 판단력을 흐리고 있다는 증거였다.
/refactor의 제안 단계도 같은 문제가 있는지 확인해봤는데, 이쪽은
무죄였다 — 카탈로그(제목·태그·목차 같은 짧은 메타데이터만) 크기를
0개에서 20개까지 늘려도 15번 중 15번 다 정확했다. 문서 본문을
직접 쓰는 부담이 없어서 배치가 커져도 안 흔들린 거였다.
근데 애초에 볼 수가 없었다
suggest가 무죄라는 걸 확인하고 나서, 실제로 병합됐던 그 노트의
속성을 다시 열어봤다. 태그가 technology, ai, anthropic, dario_amodei, ai_safety, claude뿐이었다 — 다니엘라의 이름은 태그에도
목차(헤딩)에도 전혀 없었다. /refactor 제안 단계는 문서 본문 전체가
아니라 경로/제목/분류/태그/목차 같은 짧은 메타데이터만 보고
판단하는데, 그 메타데이터 어디에도 다니엘라라는 이름이 없으면
아무리 정확한 판단 로직이라도 그녀의 존재 자체를 볼 방법이 없다.
”태그에 강제로 넣으면 되지 않나”는 다른 이유로 기각됐다
그럼 태그에 언급된 인물 이름을 다 넣도록 프롬프트를 강화하면 되지 않을까 하는 생각이 먼저 들었다. 근데 다시 보니 이상했다 — 태그는 원래 “이 노트가 다루는 핵심 주제”를 뜻하는 건데, 다니엘라는 전체 기사의 한 문단짜리 곁가지였다. 그런 걸 태그로 넣으면 “다니엘라_아모데이” 태그를 보고 찾아온 사람이 사실상 다리오 얘기만 잔뜩 보게 되는 셈이라, 태그라는 신호 자체가 오염된다. 태그를 억지로 채우는 건 증상을 가리는 임시방편이지 제대로 된 해법이 아니었다.
”얇으니까 나중에 쪼개자”는 거꾸로였다
그다음 든 생각은 “다니엘라 관련 정보가 아직 얇으니 지금 안 쪼개도 된다”였다. 근데 이것도 정정을 받았다 — 컴파운딩 위키가 실제로 작동하려면, 얇아도 먼저 쪼개서 그 개체의 이름을 가진 노드(앵커)를 만들어둬야 한다. 그래야 나중에 그 인물을 다루는 다른 원본이 들어왔을 때 그 앵커로 병합되면서 밀도가 쌓인다. 지금처럼 병합된 채로 남으면, 미래의 원본은 병합할 대상 자체를 찾지 못하고 새 중복 노트를 만들거나 기존 노트를 오염시키게 된다. 정보 밀도는 쪼갠 다음 시간이 지나면서 채워지는 거지, 쪼개기 전에 미리 다 채워져 있어야 하는 게 아니었다.
더 큰 질문 — ingest가 너무 많은 걸 한 번에 하고 있었다
여기서 논의가 한 단계 더 올라갔다. 지금 /ingest는 한 번의 판단
안에서 세 가지를 동시에 한다 — 그냥 요약할지, 분할하며 요약할지,
기존 노트로 병합할지. 병합 판단은 위키 카탈로그 전체를
대상으로 관련 문서를 찾는 별도의 API 호출까지 거쳐야 하는 무거운
작업이다.
두 가지 재구성 방향이 나왔다.
하나는 /refactor를 split 전용과 merge 전용으로 아예 나누는
것이었는데, 이건 기각했다 — suggest 단계가 카탈로그 크기와
무관하게 이미 병합·분할 판단을 둘 다 정확히 해내고 있다는 게
방금 확인된 상태라, 굳이 쪼갤 근거가 없었다. 관찰되지 않은
문제를 미리 해결하려는 셈이었다.
다른 하나는 /ingest를 요약+분할로만 좁히고, 병합은 전부
/refactor에게 위임하는 것이었다. 이건 오늘 나온 증거들과 잘
맞았다 — /ingest가 내려야 하는 판단이 “이 원본이 몇 개체를
다루는가” 하나로 단순해지고, 이건 “한 번에 여러 판단을 시키면
정확도가 떨어진다”는 패턴과 정반대 방향이었다.
코드를 더 넓게 봤더니, 두 가지가 더 나왔다 — API 이중 호출(초경량 라우터)
이 재구성이 실제로 뭘 건드리는지 코드를 더 넓게 감사해봤다.
첫째, ingest는 청크 하나를 처리할 때 실제로 API를 두 번 호출하고 있었다 — 병합 대상을 찾는 “초경량 라우터” 호출을 먼저 하고, 그 결과를 넣어서 본 ingest 호출을 한 번 더 한다. 병합을 아예 빼면 이 라우터 호출 자체가 필요 없어져서, 비용과 속도가 추가로 개선될 여지가 있었다.
둘째, 이건 사실 얼마 전에 반대 방향으로 작업했던 걸 되돌리는
셈이었다 — 예전에 /ingest의 병합 기준을 /refactor와 통일시키는
작업을 따로 한 적이 있는데, 이번엔 반대로 “ingest에서 병합 자체를
없애서” 통일하는 거였다. 방향은 정반대지만 목표(두 컴포넌트의
병합 판단 불일치 제거)는 같았다.
실측해보니, 진짜로 좋아졌다
말로만 정리하지 않고 프롬프트에서 병합 관련 지시를 전부 빼고 실측했다. (여기서 뺀 건 “이미 존재하는 문서로 병합”하는 것만이다 — 같은 배치 안에서 원본 여러 개가 우연히 같은 새 주제를 다뤄서 한 노트로 자연스럽게 통합되는 것까지 막는 건 아니다.) 같은 다니엘라 케이스로 배치 크기별 분할 비율을 다시 쟀다.
| 조건 | 배치 1개 | 배치 2개 | 배치 3개 | 배치 6개 |
|---|---|---|---|---|
| 병합 포함(원래) | 10/10 | — | 7/10 | 4/10 |
| 병합 제거 | 5/5 | 10/10 | 9/10 | 10/10 |
병합 판단 하나를 뺐을 뿐인데, 배치 6개에서 4/10이던 게 10/10으로 올라갔다.
모델을 하나 더 비교해보니
같은 실험을 예전 기본 모델(더 저렴한 하위 티어)로도 돌려봤다. 이쪽도 병합을 빼니 개선은 됐지만, 양상이 달랐다 — 배치 2개에서 7/10으로 여전히 흔들렸고, 결과물도 산만했다. 다니엘라뿐 아니라 클로드, 샘 올트먼, 미토스, 심지어 “CODE”라는 방법론 약어까지 전부 별도 노트로 쪼개는, 과분할에 가까운 경향을 보였다. 같은 처방이 두 모델에 다르게 작동한 셈이다 — 싼 모델은 판단을 단순하게 해줘도 여전히 배치 크기에 더 민감하고, 결과의 일관성 자체도 떨어졌다.
그럼 배치를 낮추면 속도는 얼마나 느려지나
정확도만 보면 배치 크기를 최대한 낮추는 게 맞아 보였다. 근데 그러면 비용과 속도는 어떻게 될까 — 감으로 말하지 않고 실제로 쟀다. 원본 6개를 기준으로, 한 번에 6개를 묶어 처리할 때와 하나씩 따로 처리할 때를 비교했다.
| 구성 | 토큰(입력/출력/합계) | 소요 시간 |
|---|---|---|
| 배치 6개(1회 호출) | 5,830 / 3,417 / 12,796 | 40.6초 |
| 배치 1개씩(6회 순차 호출, 청크간 대기 포함) | 19,820 / 4,377 / 24,197 | 73.6초 |
비용은 약 1.8배, 속도는 순차 처리라 1.8~2.2배 느려졌다. 비용은 정확도를 위해서라면 감수할 만했는데, 속도는 무시하기 어려운 차이였다.
대기 시간을 줄이는 대신, 다른 걸 줄이기로 했다
첫 반응은 “청크 사이 대기 시간(레이트 리밋 방지용 설정)을 줄이면 되지 않나”였다. 근데 이건 채택하지 않기로 했다 — 지금 쓰는 API 티어가 넉넉하다고 해서 그 값을 낮추는 걸 기본값으로 삼으면, 나중에 더 낮은 티어를 쓰는 다른 사용자에게는 위험한 설정이 될 수 있다. 특정 사용자의 상황에 기본값을 맞추면 안 된다는 판단이었다.
대신 개별 호출 시간을 다시 뜯어봤다 — 6번의 개별 호출이 각각 8초, 23초, 8초, 7초, 9.5초, 5.4초 걸렸다. 순차로 처리하면 합계 61초지만, 동시에 병렬로 쐈다면 제일 느린 것 하나(23초)만 기다리면 됐을 것이다. 순차 61초 대비 이론상 2.6배 빠르다.
요청은 병렬로, 기록은 순서대로
그래서 나온 설계가 이거였다 — 청크의 “응답을 기다리는 부분”과 “응답을 실제로 파일에 쓰는 부분”을 분리한다. 요청은 새 설정값 (동시 실행 개수, 기본값은 보수적으로 낮게)만큼 동시에 나가고, API 티어에 따라 사용자가 직접 조절할 수 있게 한다. 반면 실제 파일 쓰기와 로그 기록은 어느 응답이 먼저 돌아오든 상관없이 완전히 순서대로 하나씩만 처리하는 큐로 처리한다 — 얼마 전 log.md에 생긴 유령 링크 버그를 고치면서 만든 로직이 동시에 여러 곳에서 호출되면 다시 깨질 수 있어서다.
이렇게 하면 같은 배치 안에서 “방금 다른 청크가 막 만든 문서”로
병합되는 효과는 사라진다(순서가 안 보장되니까). 근데 이건 이미
병합 자체를 ingest에서 뺀 뒤라 상관없어졌다 — 병합은 전부
/refactor가 카탈로그 전체를 보고 나중에 처리하기로 했으니까.
남는 생각
이번에 확인한 건 세 겹이었다. 제일 얕은 층은 실용적인 것 —
배치 크기가 크면 판단이 흐려진다는 패턴이 분할 판단에도 그대로
적용된다는 것. 그 밑에는 더 근본적인 것 — /ingest가 너무 많은
걸 한 번에 판단하려고 하는 게 문제의 뿌리였고, 그 판단 하나
(병합)를 통째로 다른 컴포넌트에 위임하니 나머지 판단(분할)의
정확도까지 같이 좋아졌다는 것. 그리고 제일 밑에는 설계
전반에 걸친 것 — 정확도를 올리는 선택(배치를 낮추는 것)이
공짜가 아니라는 걸 실측으로 확인하고, 그 대가(속도)를 사용자
환경을 해치지 않는 방식(요청만 병렬화)으로 다시 줄이는 것까지
이어졌다.
중간에 여러 번 방향을 틀었다 — 태그를 강제로 채우자는 생각도, refactor를 둘로 나누자는 생각도, 대기 시간을 줄이자는 생각도 전부 한 번씩 떠올랐다가 각자 다른 이유로 기각됐다. 매번 “이게 왜 안 되는지”를 구체적으로 짚고 넘어갔기 때문에, 최종적으로 남은 방향(병합 위임 + 요청 병렬화 + 기록 직렬화)이 그냥 처음 떠오른 아이디어가 아니라 여러 대안을 실제로 걸러낸 결과라는 확신이 든다.