공부/인공지능

[세컨드브레인 개발기] 2. 20,000개 문서 앞에서 설계가 무너진 날

TechToast 2026. 8. 2. 05:40
반응형

[세컨드브레인 개발기] 2. 20,000개 문서 앞에서 설계가 무너진 날

앞 편에서, 개인 연구용 세컨드브레인을 벡터 RAG 없이 시작한 이야기를 썼습니다. 소스가 들어올 때 LLM이 한 번 읽고 링크 걸린 마크다운으로 다시 쓰는 방식(Karpathy의 LLM-Wiki 패턴)이고, 근거 중 하나가 "개인 규모에서는 사전합성이 RAG를 이긴다"였습니다. 2026년 6월 2일에 그게 엔드투엔드로 돌아가는 걸 확인했습니다.

 

같은 날 설계가 깨졌습니다. 이 편은 그 이야기고, 시리즈에서 제일 중요한 편이라고 생각합니다.

산수 한 줄로 끝났다

 

구글드라이브에는 대학 두 곳에서 일하며 만든 자료가 예전부터 쌓여 있었습니다. 브레인에 넣을 대상으로 그 폴더를 지정하고, 규모를 세어봤어요.

 

 

폴더 두 개를 합쳐 문서 20,500개에, 약 53GiB였습니다. 이하 이 글에서는 약 2만 개라고 부르겠습니다. 앞 편에서 내가 적어둔 전제는 "수백~수천 개"였는데, 실제 규모는 그 상한을 한참 넘어간 곳에 있었습니다.

 

LLM-Wiki 패턴은 문서 하나당 LLM 합성 한 번입니다. 소스를 읽고, 요약하고, 링크를 뽑아, 마크다운을 생성하는 일이라 짧게 끝나지 않는데, 당시 이 박스에서 잰 문서당 합성 시간이 약 70초였습니다. 20,500 곱하기 70초는 1,435,000초, 그러니까 16.6일입니다.

 

 

이 그림을 그리다가, 내 초고가 틀렸다는 걸 알았어요. "수천 개면 하룻밤"이라고 적어뒀는데 5,000개는 이미 4.1일이고, 하룻밤 안에 끝나는 건 1,000개 수준까지입니다. 문서당 70초에서 하루를 넘기지 않는 한계는, 정확히 1,234개입니다. 비용이 개수에 정비례하니, 이런 감각은 금방 어긋납니다.

 

16.6일은 그나마 연속으로 16일이고, 그동안 같은 vLLM이 다른 일을 못 합니다. 브레인의 다른 기능도 그 뒤에 줄을 서고, 회의록 정리도 서고, 논문 요약도 서요. 증분으로 나눠 돌린다 해도 초기 적재를 끝내지 못하면, 시스템 자체가 시작되지 않습니다.

 

여기서 중요한 건, 16일이 "느리다"는 문제가 아니라는 점입니다. GPU를 하나 더 붙이면 8.3일이 되지만, 그래도 답이 아니에요. 절반으로 줄여도 여전히 일주일을 넘고 네 배를 붙여도 나흘이니, 깨진 것은 속도가 아니라 전제였습니다.

 

문서당 LLM 1회라는 비용 구조는 소스 개수에 정비례합니다. 천 개에서는 하룻밤이고, 2만 개에서는 시스템이 성립하지 않아요. 같은 설계가 어느 구간에서는 최적이고 어느 구간에서는 무의미한데, 나는 그 경계를 넘었다는 걸 폴더 크기를 세고 나서야 알았습니다.

위키를 버리지 않고 층을 하나 얹었다

선택은 두 가지였습니다. LLM-Wiki를 버리고 표준 RAG로 전면 전환하거나, 위키는 남기고 대규모 소스를 감당할 검색 계층을 따로 얹거나.

 

두 번째를 골랐습니다. 위키가 사람이 읽는 층으로는 여전히 잘 작동했고, 링크를 따라가며 브라우징하는 건 벡터 검색이 대체할 수 없기 때문입니다. 반대로 2만 개 원본 문서에서 "그 숫자가 어디 있었지"를 찾는 일은, 위키가 못 합니다. 용도가 다르니 층을 나누는 게 맞다고 판단했습니다.

 

임베딩은 fastembed로 intfloat/multilingual-e5-large를 썼습니다. 1024차원에 ONNX 런타임에서 CPU로 돌렸어요. 벡터DB는 도커로 띄운 Qdrant를 쓰고, 처리 완료분은 매니페스트 파일에 기록해서 증분으로만 색인하게 했습니다.

 

 

CPU로 임베딩한 이유는, 앞 편의 그 119GB입니다. GPU 메모리에 임베딩 모델을 얹을 자리가 없었고, ONNX/CPU로 돌리면 느리지만 20코어가 놀고 있었으니 그쪽이 현실적이었어요.

 

e5를 고른 이유는 한국어였습니다. 자료가 대부분 한국어인데, 당시 실측에서 한국어 코사인 유사도가 0.8 수준으로 나왔고, 나는 그걸 메모에 장점으로 적어뒀습니다.

 

이 문장이 나중에 이 시리즈에서 제일 아픈 대목이 됩니다. 미리 한 줄만 적어두면, 무관한 문서끼리도 0.8이 나온다는 건 변별력이 없다는 뜻이었습니다. 당시엔 한국어를 잘 이해한다는 신호로 읽었어요. 후반부에서 수치와 함께 다시 나옵니다.

 

e5는 접두사도 요구합니다. 질의에는 query:, 문서에는 passage:를 붙여야 하는데, 안 붙이면 조용히 품질이 떨어집니다. 에러가 나지 않으니, 더 위험합니다.

 

같이 미룬 것도 적어두겠습니다. 진짜 그래프 추출(LightRAG 류)은 이때 접었습니다. 엔티티와 관계를 뽑으려면 청크마다 LLM을 호출해야 하는데, 그건 문서당 70초와 정확히 같은 벽이고, 청크는 문서보다 많으니 더 심합니다. 대신 투입 대비 회수가 가장 큰 벡터 의미검색만 먼저 넣었어요. 색인된 게 하나도 없는 동안은 예전 위키 어휘검색으로 폴백하게 했는데, 새 계층이 비어 있는 기간에도 시스템은 동작해야 하니까.

곁가지 — 임베딩 데몬을 따로 띄운 이유

이건 같은 걸 만드는 사람에게 바로 쓸모있을 것 같아서, 따로 적어둡니다.

 

brain query를 처음 구현할 때는, 질의가 들어오면 임베딩 모델을 로드해 질의를 벡터로 바꾸고 Qdrant에 던졌습니다. 문제는 로드 시간이었습니다. 모델이 약 2GB인데 올리는 데 170초가 걸리고, 정작 질의 자체는 1초도 안 걸립니다.

 

질문 하나에 3분을 기다리는 도구는, 도구가 아닙니다. 그래서 임베딩만 담당하는 상주 데몬을 따로 뒀습니다. 부팅 때 한 번 모델을 올린 뒤 죽지 않고 떠 있으면서 색인과 질의 양쪽에 HTTP로 벡터를 내줍니다.

 

 

교훈은 단순합니다. 모델 로드 시간이 요청 처리 시간보다 크면, 프로세스 경계를 다시 그려야 합니다. CLI 한 방에 다 넣고 싶은 마음이 들지만 로드가 170초면 그 구조는 성립하지 않습니다. 그리고 이 판단은 앞 편의 메모리 제약과 붙어 있습니다. 상주 데몬을 두려면 메모리에 계속 앉아 있을 자리가 필요하고, 그 자리를 어디서 뺏어올지가 결국 같은 문제였습니다.

조건부 명제를 조건 없이 저장했다

정리하면 이렇습니다.

 

"사전합성이 RAG를 이긴다"는 규모 조건부 명제였습니다. 나는 조건을 근거 문단에만 적고, 설계 문서에는 적지 않았습니다.

 

 

기술 결정을 기록할 때 우리는 보통 결론을 적습니다. "벡터DB 없이 간다." 그리고 그 결론이 왜 맞는지 근거를 붙입니다. 그런데 근거 안에 조건이 숨어 있으면, 그 조건은 결론과 함께 저장되지 않아서, 나중에 문서를 다시 읽는 사람은 — 그게 3주 뒤의 나였습니다 — 결론만 봅니다.

 

지금은 설계 결정을 적을 때, 이런 줄을 같이 적습니다.

 

유효 범위: 소스 1,200개 이하
무효화 조건: 소스 개수 × 문서당 합성 시간 > 24시간

 

두 줄이 같은 수치를 가리켜야 합니다. 처음 이 규칙을 적을 때 나는 유효 범위에 5,000개라고 써놓고 무효화 조건은 24시간으로 뒀는데, 5,000개는 4일이니 두 줄이 서로를 부정합니다. 이 편의 그림을 그리다가 그걸 발견했어요. 수치를 도표로 옮기면, 앞뒤가 안 맞는 게 눈에 보입니다.

 

조건이 아니라 무효화 조건으로 적는 게 핵심입니다. "언제까지 맞다"보다 "무엇이 관측되면 틀린 것이다"가, 확인 가능한 문장이기 때문입니다. 이건 나중에 이 시리즈의 관통 주제가 됩니다. 선언을 적을 때 그 선언이 깨졌는지 확인하는 방법을 같이 적어두지 않으면, 선언은 그냥 오래된 낙관으로 남습니다.

후일담 — 곁가지가 본체가 됐다

한 달 뒤 시스템을 재설계하면서, 확인한 사실이 있습니다.

 

의미검색이 실제 백본이었습니다. 색인 작업은 원본 소스를 임베딩하고 있었고 LLM이 재합성한 위키는 색인에서 명시적으로 제외돼 있었으니, 질문에 답할 때 근거로 쓰인 건 위키가 아니라 원본 소스 쪽이었습니다. 그래서 재설계에서 자동 위키는 동결하고, 읽기전용 유물로 남겼습니다. 급하게 얹은 곁가지가 본체가 되고, 원래 본체가 유물이 된 셈입니다.

 

이게 앞 편에서 내가 자랑했던 것과 어긋난다는 점도 적어두겠습니다. 나는 lint로 위키를 점검할 수 있다는 걸 이 설계의 장점으로 꼽았는데, 실제로 답의 근거가 된 층은 위키가 아니라 벡터 쪽이었고 그쪽에는 점검할 방법이 없었습니다. 점검 가능한 층은 점검했고, 점검할 수 없는 층이 정작 일을 하고 있었습니다.

 

정직하게 하나 더. 이때 얹은 벡터 계층 자체도, 결국 망가진 채로 몇 주를 돌고 있었습니다. 위에서 장점이라고 적어둔 그 코사인 0.8이 원인이었고, 그 진단은 5부에서 수치와 함께 씁니다.

 

다음 편에서는 그 2만 개를 실제로 읽어들이는 이야기를 해 보겠습니다. 한국 대학 자료의 파일 포맷은, 상상보다 더 난장판입니다.

반응형