제 9 장

근거를 제시하는 RAG

검색과 생성을 붙여 놓고 보니 화면이 제법 그럴듯했다. 답변 아래 출처가 붙고 문서 이름을 누르면 원문으로 넘어갔다. 되돌린 뒤로 한 달 넘게 붙들고 있던 것이 한 화면에 다 들어와 있어서, 개발팀은 이제 됐다고 생각했다.

그런데 겉모습만으로는 알 수 없는 것이 있다. 교육비 문서를 인용하면서 출장 숙박비 한도를 답하는 시스템도 화면은 똑같이 생겼다. 개발팀은 화면이 정말 그 출처대로 답하는지 확인하기로 하고 검색·문맥 구성·생성·인용 검증을 한 요청 경로로 연결해 실제 로컬 모델로 돌려 봤다.

요청 경로

직원이 물으면 검색기가 사내 문서에서 관련된 것을 찾고, 구성기가 그중 그 직원이 읽어도 되는 것만 골라 질문 뒤에 붙이고, 모델이 붙은 자료만 보고 답을 쓴다.

RAG는 검색한 외부 자료를 생성에 넣는 접근이다. 모델 가중치 안의 지식만 쓰는 대신 요청에 필요한 자료를 찾아 입력으로 준다. 모델을 고치는 것이 아니라 모델이 보는 입력을 고치는 일이다. 그래서 정책이 바뀌면 문서 한 건만 갈아 끼우면 되고 누가 그 문서를 읽을 수 있는지도 검색 단계에서 사람마다 다르게 정할 수 있다. 가중치 학습으로는 둘 다 어렵다.

바뀌지 않는 것도 있다. 모델은 여전히 다음 토큰을 예측할 뿐이라, 입력에 든 문장을 그대로 옮겨 적을 의무가 없다. 근거를 넣어 줬다고 모델이 그 근거대로 답하지는 않는다. 원 논문은 신경 검색기와 생성 모델을 함께 학습하는 방법을 다루지만, 이 책의 구현은 사전 학습된 임베딩·생성 모델을 검색과 문맥 전달로 연결하는 애플리케이션 파이프라인이다. RAG 원 논문

respond의 요청 경로질문 검사질문 임베딩허용·유효문서 검색청크 현재상태 확인문맥 예산선택 근거재확인생성구조·인용검사근거 현재상태 재확인상태·출처반환굵은 칸은 근거의 현재 상태를 다시 보는 자리다. 이 경로에는 문서 수정도 티켓 등록도 없다
역할하는 일
검색기사용자가 볼 수 있고 기준일에 유효한 후보를 찾는다
문맥 구성기후보 중 생성 입력에 들어갈 자료를 고른다
생성기질문과 자료를 읽어 답변이나 유보 상태를 만든다
검증기출력 구조와 실제 전달한 근거의 인용을 확인한다

로컬 생성 모델은 문서가 입력에 있어도 답을 못 찾았고, 검색기는 답을 담지 않은 문서에도 높은 점수를 줬고, 저장소는 특정 시점의 권한과 리비전을 검사했지만 요청 도중의 변경은 고정하지 못했다. 통합은 이런 단계를 없애는 일이 아니라, 각 단계의 결과와 실패를 다음 단계가 제대로 받도록 만드는 일이다. 오래된 규정과 새 규정을 함께 넣으면 모델이 둘을 섞으니, 원본·시행일·권한 규칙을 먼저 정하고 그 위에서 검색과 생성 품질을 잰다.

검색 기준일은 호출자가 정한다. 시스템의 오늘 날짜를 쓸지 사용자가 요청한 과거 날짜를 쓸지는 제품 규칙이다. 후보 수에는 상한을 둔다. 실제로 모델에 가는 근거 수는 이보다 적을 수 있다. 문맥 구성기가 전체 입력 예산을 보고 들어갈 후보를 고르기 때문이다. 검색 점수를 정수 우선순위로 반올림하지 않고 점수순으로 정렬된 결과에 서로 다른 우선순위를 붙여 순서를 지킨다.

후보가 하나도 없으면 생성기를 호출하지 않고 insufficient_evidence를 돌려준다. 현재 사용자와 기준일로 쓸 근거가 없다는 뜻이고 문서가 시스템 어딘가에 있는지는 말하지 않는다. 후보는 있었지만 예산 안에 하나도 못 들어갔으면 context_failed다. 둘을 합치면 운영자는 문맥 예산 오류를 못 찾고 사용자는 문서가 없는 줄 안다.

세는 쪽은 완성된 입력의 토큰 수를 세고, 보내는 쪽은 그 입력을 그대로 생성기에 넘긴다. 셀 때는 시스템 지시문·출력 스키마·고정 작업 문장까지 넣은 실제 입력으로 센다. 개발팀은 실제로 돌려 보면서 생성 결과의 입력 토큰 수가 직전에 센 값과 같은지 확인했다. 로컬에서 세면 외부 전송이 없지만 외부 세기 API로 바꾸면 세는 일 자체가 자료를 보내는 일이라 그 단계의 권한 시점도 따로 정한다.

별칭과 출처 연결

별칭에서 원문 구간으로모델 입력의 별칭s1이번 요청의 대응표서버가 보관한다실제 청크 ID3f9a…문서 · 리비전 · 구간edu · v2 · 120–184s2 · 예산 때문에 빠진 후보✕unknown_source · 이번 생성에 준 근거가 아니다별칭은 요청마다 새로 만든다. 다른 요청의 s1은 다른 문서다. 영구 출처 ID로 저장하지 않는다

저장소의 청크 식별자는 긴 해시라서 모델에는 s1, s2처럼 짧은 별칭을 준다. 응답의 sources에는 별칭과 함께 실제 chunk_id, doc_id, revision, start, end, 인용 구절을 돌려준다. URL은 서버가 만든다. 문서 열기 기능을 붙인다면 서버가 문서 ID를 신뢰하는 경로로 바꾸고 현재 권한을 확인한다.

검증에 쓰는 근거 집합은 검색 후보 전체가 아니라 구성기가 실제로 고른 문서다. 검사는 앞에서 만든 검증기를 그대로 쓴다. 필수 필드, 타입, 상태와 본문의 일관성, 실제 별칭, 원문의 연속 구절을 보고, 중복 JSON 키·추가 필드·없는 구절은 거부한다. 실패하면 원본 생성문은 응답에 남기지 않고 오류 코드와 사용량만 돌려준다.

“교육비 한도는 분기당 30만 원이다”라는 구절은 원문에 있다. 이 구절을 붙여 “숙박비 한도는 30만 원이다”라고 답하면 문자열 검사는 통과한다. 주장의 대상·조건·수치가 근거와 맞는지까지는 문자열 검사로 알 수 없다. 이것을 따지는 일을 의미 평가라고 부른다. 인용 품질을 다룬 ALCE 연구도 유창성, 답변 정확성, 인용 품질을 나눠 평가한다. 이 서비스의 answered는 출력 상태와 인용 검사가 통과했다는 뜻이고 모든 주장이 사실이라는 인증이 아니다. 개발팀은 잘못된 300만 원 답변이 실제 30만 원 구절을 인용하면 구조 검사를 통과하는 사례를 테스트에도 일부러 남겼다. 나쁜 답을 허용하려는 것이 아니라 검증기가 무엇을 보장하지 않는지 분명히 하기 위해서다. 인용을 포함하는 언어 모델 응답의 평가 연구

근거 부족과 상충 문서

상황처리누가 정하나
후보가 없다생성 없이 insufficient_evidence서버
후보는 있지만 답이 없다모델이 insufficient_evidence로 유보해야 한다모델 (지시만으로는 보장 안 됨)
질문에 조건이 빠졌다needs_clarification · 답변과 인용을 비우고 되묻는다모델
문서 사이에 충돌이 있다결론을 정할 수 없으면 유보 · 권위 규칙은 메타데이터로지시는 있지만 검출기는 없다

“얼마까지 지원되나요?”만으로는 교육비인지 출장비인지 알 수 없다. 근거는 충분한데 질문 대상이 불명확한 상황을 자료 부족과 섞지 않는다. 같은 문서의 시행일 충돌은 수집에서 막았지만 서로 다른 문서 사이의 충돌은 남는다. 규정과 공지가 같은 날짜에 다른 금액을 말할 때 점수가 높은 쪽이 권위가 높은 것이 아니다. 규정 우선, 명시적 개정 공지 우선 같은 규칙이 있으면 메타데이터와 선택 로직으로 표현한다. 이번 구현은 문서 사이의 권위를 자동으로 판정하지 않는다.

한 요청 안의 근거 재확인 세 번검색문맥 구성생성 (오래 걸린다)검증반환① 청크 현재 상태② 선택 근거 재확인③ 근거 재확인권한 회수 · 문서 개정③이 잡는다 → evidence_changed③ 직후의 변경은 잡지 못하고, 이미 보낸 자료는 되돌릴 수 없다. 허용할 변경 시점과 취소·재시도 규칙은 따로 정한다

저장소는 고른 청크의 현재 권한·시행일·원문과 출처 메타데이터를 다시 조회한다. 검색 뒤, 생성 직전, 생성 뒤 세 번이다. 그 사이 삭제·갱신·권한 회수가 있으면 evidence_changed로 답변과 출처를 버린다. 질문의 기준일은 요청 내내 유지한다. 그 사이 문서가 바뀌면 바뀐 쪽을 읽는다. 원문 오류가 정정돼 청크가 바뀌었으면 같은 과거 날짜의 질문이라도 진행 중인 답변을 다시 본다. 긴 모델 호출 동안 DB 쓰기 잠금을 잡는 방식은 비용이 커서 쓰지 않는다.

실패 상태

결과 상태뜻다음에 볼 것
retrieval_failed질문 임베딩 또는 저장소 검색 실패입력 한도, 모델·저장소 상태
context_failed입력 구성·세기 실패 또는 모든 근거의 예산 초과실제 지시문·스키마·문맥 예산
generation_failed생성 예외 또는 완료되지 않은 출력모델 상태, 출력 한도, 종료 정보
contract_failedJSON·필드·상태·인용 검사 실패원본 실험 출력과 오류 분류
evidence_changed선택 근거를 현재 권한·판으로 쓸 수 없음삭제·개정·권한 변경
insufficient_evidence쓸 근거가 없거나 모델이 유보원본 자료와 질문 범위

사용자는 답을 못 받았다는 한 가지 현상을 보지만 운영자는 실패 지점을 나눠야 한다. 임베딩 생성이 실패했는데 프롬프트를 바꾸거나, JSON 필드가 빠졌는데 검색 개수를 늘리면 원인은 그대로다. 운영 로그에는 원문을 무조건 남기지는 않는다. 질문·문서·생성문은 모두 민감할 수 있어서 결과 객체는 실패 원문을 지운다. 이번 실험은 가상 자료라서 원본을 남겨 뒀지만 그 방식을 사용자 서비스의 로그 정책으로 옮길 수는 없다. 정상 상태에서도 정답 근거가 후보에 있었는가, 예산을 거쳐 실제로 전달됐는가, 생성기가 그 근거를 제대로 썼는가, 인용이 주장을 뒷받침하는가를 나눠 본다.

첫 통합 실험

개발팀은 E5 임베딩과 Qwen3-0.6B 생성 모델을 캐시에서 올리고 임시 저장소에 가상 교육비 문서를 넣었다. 외부 API 계정은 없다. CPU float32 4스레드, 샘플링과 사고 모드 없음, 출력 상한 512토큰이다. 로컬에서는 출력 스키마를 지시문에 적어 보낼 뿐 제약 디코딩은 하지 않는다. 구조를 요청한 뒤 검증기로 검사한다.

첫 통합 실험 · Qwen3-0.6B · 사례당 1회프롬프트 v1v2 (예시 추가)사례내용형식내용형식서비스 결과R01교육비 한도와 신청 순서✓✕△✕contract_failedR02해외 출장 숙박비 (근거 없음)✕✕✕✕contract_failedR03상충하는 두 문서✕✕✕✕contract_failedR04권한 회수 뒤 질문––––insufficient_evidence△ = 내용 일부 누락 (승인 절차) · – = 생성을 부르지 않음. 입력 토큰은 v1 574·577·598 → v2 784·787·808 (+210)v2는 코드 블록만 없앴다. 형식을 고쳐도 R02·R03의 내용은 그대로 틀렸다
사례v1의 관찰v2의 관찰
R01 교육비분기당 30만 원·팀장 승인·포털 신청을 답했지만 clarification 필드가 없고 코드 블록을 붙임코드 블록은 사라졌지만 필드 누락, 승인 절차도 빠짐
R02 숙박비후보에 교육비 문서만 있는데 숙박비 한도를 30만 원이라고 답하고 교육비 원문을 인용근거에 없는 150,000원을 새로 만듦
R03 상충 문서같은 기준일에 30만 원·50만 원인 두 문서에서 유보 없이 50만 원 선택50만 원 선택
R04 권한 없음두 문서의 권한을 회수한 뒤 질문. 후보가 없어 생성 호출 없음같음

R01의 실패는 형식이고 R02·R03의 실패는 내용이다. 형식을 고쳐도 내용은 그대로 틀려 있다. R04는 모델의 유보 능력이 아니라 서버의 권한·빈 후보 경로가 만든 결과다. v2에는 필드를 모두 채운 답변 예시와 근거 부족 예시를 추가했고(소재는 가상의 도서 대출), 모델·문서·질문·출력 상한·검증 규칙은 그대로 뒀다. 그런데도 clarification 누락은 남았고 R02는 금액을 재사용하는 대신 없는 금액을 만들었다. 프롬프트를 길게 했다고 개선이라고 할 수 없다. 생성 함수 시간은 v1 약 6.91·5.50·4.35초, v2 약 5.33·4.88·3.38초였지만 v2의 출력이 더 짧고 통제된 반복 실험도 아니어서 속도 개선으로 읽을 수 없다.

개발팀은 사례마다 한 번씩만 돌렸고 예시도 그 첫 결과를 보고 추가했다. 최종 평가에는 이 집합을 쓸 수 없다. 이 모델·설정은 출력 검사와 근거 사용의 채택 기준을 넘지 못했다.

코드 블록을 지우고 빠진 필드를 자동으로 채우면 R01은 형식 검사를 통과할 수 있다. 그런데 같은 복구가 R02의 잘못된 숙박비 답도 다음 단계로 넘긴다. 출력 복구가 필요하면 허용하는 변환을 명시하고 변환 전후를 검증한다. 실패한 실험을 통과시키려고 필수 필드를 선택 필드로 바꾸거나 의미 기준을 낮추지 않았다. 생성 모델 교체, 제약 생성 기능, 작업에 맞는 프롬프트가 다음 후보고, 그때도 숙박비 오답과 충돌 사례는 회귀 집합에 남는다.

이제 됐다고 생각했던 그 화면은 R01부터 R04까지를 그대로 보여 줬다. 출처가 붙었다고 그 출처로 답한 것은 아니라는 사실을 개발팀은 실행 기록에서 처음 눈으로 봤다.

문서를 지식으로 연결하기9 / 21