지식베이스 하나 만드는데 세 번을 엎었다

English version: Three Failures Before a Knowledge Base That Held

흩어진 과제 문서들이 빛나는 원장으로 빨려 들어가 정돈된 지식 그래프로 나오고, 손 하나가 노드를 고치는 그림

언어 모델 위에 뭔가를 얹어 보려는 시도가 여기저기서 이어지고 있는데요, 다만 그 재료가 되는 문서는 대개 깔끔하지 않습니다. 연구과제를 하다 보면 특히 문서가 쌓이거든요. 사업계획서, 협약서, 연차보고서, 정산 서류, 출장 신청서와 복명서. 그것도 몇 년치가, 그해에 정리한 사람 마음대로 만들어진 폴더 구조 속에 흩어져 있습니다.

정작 필요한 정보는 다 그 안에 있습니다. 이 과제가 어느 사업에 속하는지, 어느 부처 소관이고 어떤 규정을 따르는지, 참여기관은 어디어디이고 각 기관 담당자가 누구인지, 처음에 내건 목표가 뭐였고 실제로 뭐가 나왔는지, 그 출장은 어느 과제로 다녀온 건지.

어느 파일을 열어야 하는지만 알면 찾는 건 어렵지 않습니다. 문제는 대개 그걸 모른다는 거구요, “24년 독일 출장은 어떤 과제였지?” 같은 단순한 질문의 답이 서로를 전혀 참조하지 않는 문서 네 개에 나눠 담겨 있다는 점입니다.

그래서 이 문서 더미 위에 지식베이스를 올려 보기로 했습니다. 사실을 뽑고, 엮고, 계속 최신으로 유지하는 것이죠.

사실 이 주변을 맴돈 게 처음은 아닙니다. 얼마 전에는 반나절 동안 마크다운 파일과 작은 스킬로 개인 지식 베이스를 만들어 본 적이 있고(그 글은 영문으로만 있습니다), 이틀 뒤에는 그게 삐걱거리다 결국 그래프가 되고 싶어 하는 이야기를 쓰기도 했습니다. 그때는 마음 편한 편이었던 것 같아요. 코퍼스가 머릿속에 들어올 만큼 작았고, 설계가 마음에 안 들면 그냥 버리면 그만이었거든요.

이번 이야기는 같은 생각을 진짜 문서 더미에 적용해 봤을 때 벌어진 일입니다. 그리고 지금까지 세 번을 엎었습니다. 아래는 각 시도가 무엇을 놓쳤는지, 그리고 네 번째는 어떤 모습인지 정리해 본 기록입니다.

첫 번째: 파일에서 데이터베이스까지 한 줄로

제일 먼저 떠오르는 설계입니다. 폴더를 훑고, 파일을 읽고, 내용을 뽑아서, DB에 씁니다. 앞에서 뒤까지 직렬로 이어진 파이프라인 하나죠.

한 번은 잘 돌았습니다. 문제는 두 번째부터였구요.

지식베이스는 한 번 뽑아 놓고 덮어 두는 보고서가 아닙니다. 새 문서를 계속 받아들여야 하고, 이미 있던 문서도 계속 바뀝니다. 파일이 하나 새로 생기고, 하나 지워지고, 누군가 사업계획서를 열어서 숫자 하나를 고쳐 저장하기도 하죠. 이런 일이 생길 때마다 DB에는 낡았거나 서로 어긋나는 행이 남을 위험이 생기는데, 직렬 파이프라인에는 그 처리를 넣을 자리가 마땅히 없습니다. 그래서 사방에 퍼지게 됩니다. 읽는 쪽에도 방어 코드, 쓰는 쪽에도 방어 코드, 정합성 맞추는 패스, 삭제 감지.

결국 추출하는 코드보다 DB를 변경으로부터 지키는 코드가 더 많아졌습니다. 존재 이유 자체가 ‘계속 갱신되는 것’인 지식베이스를 만들면서 그 유지보수 비용을 감당하는 건 아무리 봐도 수지가 맞지 않았구요. 자기가 쓰일 상황을 견디지 못하는 설계였던 셈이에요. 그럼 일을 나누면 좀 나아질까요?

두 번째: 생성자와 소비자의 분리

다음 설계에서는 일을 자연스러워 보이는 경계를 따라 나눴습니다. 한쪽은 문서를 읽어서 특성을 뽑는 생성자로, 주요 폴더 단위로 돕니다. 다른 쪽은 생성된 결과를 읽어서 특성들 사이의 관계를 정리하는 소비자로, 관계를 갱신하고 정돈합니다.

구조적으로는 훨씬 나았습니다. 생성자는 소비자가 그걸로 뭘 하든 상관하지 않고, 소비자는 특성이 어디서 왔는지 신경 쓰지 않습니다. 폴더 하나 바뀌면 그 폴더만 다시 생성하면 되구요.

이 설계는 비용 때문에 무너졌습니다. 추출한다는 건 결국 문서 텍스트를 모델에 밀어 넣는다는 뜻이고, 코퍼스는 크고, 다시 돌릴 때마다 그 값을 또 내야 하거든요. 전량 한 바퀴 도는 토큰 비용이 부담스러운 수준이라 반복 실험 자체가 어려워졌습니다. 두 번 돌릴 엄두가 안 나는 설계는 디버깅을 할 수 없는 설계에 가깝다고 생각합니다. 구조는 멀쩡했는데 경제성이 발목을 잡은 셈이죠.

세 번째: 로컬 LLM과 Claude의 분담

비용 문제의 해법은 뻔해 보였습니다. 양 많고 비싼 부분을 이미 갖고 있는 하드웨어로 내리면 되니까요. 로컬 LLM이 문서를 읽어 특성을 뽑고, 그걸 훨씬 잘하는 Claude가 그 위에서 관계를 짭니다. 이 엔티티들이 어떻게 연결되는지, 이 언급이 어느 기관을 가리키는지.

한동안은 답인 것 같았습니다. 로컬 추출은 사실상 공짜였고 관계 추출 품질도 괜찮았구요.

그러다 슬슬 밀리기 시작했습니다. 코퍼스에 새로운 종류의 관계가 나올 때마다 그걸 처리할 프롬프트가 필요했고, 그 프롬프트들이 계속 쌓였습니다. Claude로 보내야 하는 컨텍스트도 같이 불어났구요. 지시문이 늘고, 딸려 보내는 추출 결과가 늘고, 설명해야 할 관계 종류가 늘고. 하나하나는 다 그럴 만한 증가였는데, 합이 문제였습니다. 비싼 쪽으로 가는 컨텍스트가 계속 커지더니, 추출을 로컬로 돌려서 아낀 몫을 조용히 다 갉아 먹었습니다.

비용을 없앤 게 아니라 옮겼을 뿐이었던 거죠. 그리고 이 하이브리드 구조에는 비싼 쪽 절반이 커지는 걸 막을 장치가 없었습니다. 그럼 무엇을 바꿔야 했을까요?

다시 처음부터: 원장 중심의 시스템

네 번째는 질문을 바꾸는 데서 시작했습니다. ‘어떻게 하면 잘 뽑을까’가 아니라, ‘계속 바뀌는 상황을 견디려면 무엇이 갖춰져야 하나’로요.

절차별 명세와 파일별 원장 기록: 각 절차를 명세하고, 파일마다 원장에 한 줄을 줍니다. 재설계의 핵심은 문서마다 지금 어느 단계까지 왔고 결과가 뭐였는지를 적어 두는 평범한 표 하나입니다. 로그가 아니라 원장이고, 각 단계는 여기서 읽습니다. 단계끼리는 서로를 부르지 않구요. 각 단계가 원장에 “남은 일이 뭐냐” 물어보고, 하고, 상태를 다시 써 넣습니다. 한 단계가 멈춰도 나머지는 돌아가니, 장애가 타고 번질 통로 자체가 없는 셈이에요. 앞선 설계들이 실패한 이유 중 하나는 분류 단계가 추출 단계를 직접 호출하는 구조라서, 하나가 막히면 전부 막혔다는 점이었습니다.

가운데 원장 슬래브를 분류, 추출, 연결, 사람 검토 화면의 네 블록이 둘러싸고 각각 원장과 이어져 있으며, 검토에서 교정이 되돌아가는 고리가 있는 아이소메트릭 도식

다른 단계가 아닌 원장과만 통신하는 각 단계, 그리고 각 절차로 되먹여지는 검토 교정

독립적인 절차로의 분할: 일을 진짜로 독립적인 절차들로 쪼갭니다. 이 문서가 무슨 종류인지 판단하는 부분, 문서에서 필드를 뽑아내는 부분, 뽑힌 엔티티들을 서로 연결하는 부분. 각각 따로 돌릴 수 있고, 따로 다시 돌릴 수 있고, 따로 디버깅할 수 있습니다. 그리고 각각이 자기한테 맞는 모델을 고릅니다. 로컬 LLM이 필요한 단계도 있지만, 언어 모델을 아예 안 쓰고 규칙이나 간단한 회귀로 판정하는 게 나은 단계가 생각보다 훨씬 많더라구요. 더 싸고 더 예측 가능하거든요.

사람이 참여하는 루프와 원장 위의 UI: 사람을 루프 안에 넣고, 원장 위에 UI를 얹습니다. 이게 그동안 통째로 빠져 있던 부분이었는데요, 이제 파이프라인이 내린 판단을 들여다보고 고치는 것만 하는 웹 인터페이스가 있습니다. 잘못 분류된 문서, 뽑히긴 했는데 해석이 틀린 필드, 같은 기관으로 합쳐졌어야 하는데 따로 남은 두 레코드 같은 것들이죠. 제가 고친 값이 이 시스템에서 가장 높은 권위를 가지고, 뒤에 오는 어떤 단계도 그걸 덮어쓰지 못합니다.

중요한 건 교정이 그 행 하나만 고치고 끝나지 않는다는 점입니다. 교정 결과가 각 단계의 절차에 되먹여집니다. 고쳐 준 별칭은 다음번 검증기가 쓰는 규칙이 되고, 고쳐 준 분류는 분류기가 참고하는 근거가 됩니다. 제가 코드를 다시 짜야 좋아지는 게 아니라, 쓰면 쓸수록 좋아지는 구조인 셈이에요.

질의와 판정의 분리: 묻는 것과 판정하는 것을 나눕니다. 이번에 얻은 것 중 제일 쓸모 있었던 구분이라고 생각합니다. 필드 정의에는 두 가지 측면이 있습니다. 하나는 모델에게 보내는 프롬프트를 이루는 쪽(이름, 타입, 설명), 다른 하나는 돌아온 답을 판정하는 데 쓰는 쪽(허용 별칭, 정규화, 필수 여부)입니다. 앞엣것을 바꾸면 다시 추출해야 하고 GPU 시간을 씁니다. 뒤엣것을 바꾸면 이미 받아 둔 출력을 다시 판정만 하면 되구요. 모델 호출 0회.

이 둘을 뭉뚱그리면 눈에 잘 안 띄는 방식으로 비싸집니다. 첫 시스템에서 어떤 필드 하나의 허용값 목록이 너무 좁게 잡혀 있었는데요, 문서에는 그 단어의 더 긴 공식 표기가 적혀 있는데 목록이 그걸 몰라서, 모델이 제대로 읽어 온 답을 검증기가 버리고 있었습니다. 그 필드 하나가 전체 거부의 72.5%였습니다. 고치는 건 데이터 한 줄이면 되는 일이었죠. 그런데 그 수정이 추출 경로에 엉켜 있어서, 적용하려면 코퍼스를 통째로 다시 뽑아야 했습니다. 그래서 안 고쳤고, 알고 있던 이슈 284건 중 279건이 그대로 남았습니다.

배운 것들

산출물이 아닌 과정으로서의 지식베이스: 위의 실수는 거의 다 ‘만들 수 있는 것’을 설계하고 ‘유지할 수 있는 것’을 설계하지 않아서 생겼다고 봅니다. 어떤 설계든 첫 버전은 돌아가거든요. 그보다는 열 번째 실행이 어떤 모습일지를 물어보는 것이 의미가 있을 듯 합니다.

흐름보다 상태: 파이프라인이라는 비유는 그럴듯한데, 정작 중요한 걸 가리는 것 같아요. 각 항목이 지금 어디에 있는지, 하나가 걸리면 무슨 일이 벌어지는지. 순서가 아니라 원장이 시스템의 중심이 되고 나니, 그동안 붙들고 있던 변경 관리 코드 대부분이 그냥 필요 없어졌습니다.

옳은 일에 매겨진 잘못된 비용: 옳은 일의 비용이 잘못 매겨지면 아무도 옳은 일을 하지 않습니다. 계속 곱씹게 되는 교훈인데요, 가장 강한 증거는 실패가 아니라 회피였습니다. 언젠가 규칙대로면 스키마 버전을 올려야 하는 상황에서 안 올린 적이 있습니다. 도구가 “이 변경이 실제로 영향 주는 문서만 다시 해라”를 표현할 방법이 없어서, 규칙대로 하면 아무 상관 없는 파일들까지 재작업하게 되기 때문이었죠. 판단은 옳았고, 도구가 규율의 가격을 잘못 매긴 거였다고 생각합니다. 변경 비용을 실제 영향 범위에 비례하게 만들지 않으면 사람은 그 절차를 우회하게 되고, 그게 합리적인 선택이 되어 버립니다.

규칙으로 충분한 일은 규칙으로: 규칙으로 되는 일을 언어 모델에게 맡길 이유는 크지 않습니다. 연차별 예산의 합이 총액과 맞는지 보는 건 산수이고, 지출 날짜가 과제 종료일 뒤인지 보는 건 날짜 비교입니다. 이걸 LLM에 보내면 열 줄짜리 코드보다 느리고, 비싸고, 덜 정확하구요. 모델은 진짜 판단이 필요한 곳에만 쓰는 편이 나은 것 같아요.

모를 때 모른다고 말하는 시스템: 첫 버전의 가장 큰 문제는 틀린 답이 아니라 자신 있는 답이었습니다. 전체 문서의 절반 이상이 포괄 분기 하나로 뭉뚱그려졌고, 모델이 “문서에 명시되어 있지 않습니다”라고 답한 것까지 추출 성공으로 기록되고 있었거든요. 불확실한 결과는 스키마가 슬그머니 값으로 바꿔 버릴 게 아니라, 검토 큐로 보내는 정식 결과로 다루는 것이 바람직하다고 봅니다.

최후의 수단이 아닌 구성 요소로서의 사람 교정: 설계를 세 번 하는 동안 수동 수정을 ‘자동화가 실패했다는 증거’로 취급했고, 그래서 그걸 받아 줄 자리를 아예 안 만들었습니다. 결과적으로는 교정 인터페이스가 이 시스템에서 제일 값진 부분이 됐는데요, 파이프라인이 나빠서가 아니라, 파이프라인이 자기가 뭘 틀렸는지 알게 되는 통로가 거기 하나뿐이라서입니다.

특별할 것 없는 이야기입니다. 배우려는 만큼만 천천히 찾아오는 평범한 교훈에 가깝다고 생각해요. 언어 모델 위에 뭔가를 얹을 때 어려운 부분은 대개 모델이 아니라 그 주변의 모든 것이라는 점.

공유 LinkedIn X Facebook 이메일