포티투마루
경력근거: 고객 내부 데이터를 활용한 RAG 파이프라인 구축 및 검색 품질 최적화
※ 실제 산출물이 아닌 AI 예측 시안입니다.
포티투마루는 LLM 과 RAG 를 직접 개발하면서 금융권과 방산·국방 분야에 실제 진출을 진행 중이고, 이 자리는 그 규제 산업 고객의 최전선에서 폐쇄망·온프레미스·에어갭 환경에 모델을 배포하고 고객 내부 데이터로 RAG 파이프라인을 세워 검색 품질을 올리는 일입니다. 온사이트 상주 비중이 60% 이상이라 고객 자료를 밖으로 빼지 못하는 상태에서 그 자리에서 도는 물건을 만들어 보여야 하고, 프리세일즈 단계에서 시나리오를 발굴해 PoC 를 설계하는 것도 같은 사람의 몫입니다. 이 포트폴리오는 그 순서를 그대로 밟아 바깥 연결 없이 색인하고 근거 조항을 찾아 주는 도구를 먼저 돌린 다음, 검색 품질을 숫자로 비교해 설정을 고르고, 마지막에 반입 번들 하나로 묶어 네트워크를 끊은 머신에서 다시 세웁니다. 다만 이 포트폴리오가 보여주는 것은 검색 파이프라인 구축과 검색 품질 측정, 오프라인 설치 구성 기술이며 국방·방산·공공 조달 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
이 자리는 규제 산업 고객의 최전선에서 폐쇄망·온프레미스·에어갭 환경에 모델을 올리고 망분리와 접근통제 요건을 충족하는 구조를 직접 세우는 자리입니다. 그래서 이 포트폴리오도 화면을 예쁘게 만드는 대신, 네트워크를 끊은 머신에서 한 번에 올라가 실제로 도는지를 증거로 삼습니다.
고객 내부 데이터로 RAG 파이프라인을 구축하고 검색 품질을 최적화하는 것이 주요업무에 그대로 적혀 있습니다. 문단을 어떤 길이로 쪼개고 단어 검색과 의미 검색을 어떤 비중으로 섞을지에 따라 결과가 갈리므로, 감이 아니라 질문 정답셋으로 비교해 설정을 고르는 과정을 남깁니다.
필수 역량에 바이브 코딩을 포함한 직접 구현 역량이 걸려 있고, 시나리오를 발굴해 PoC 를 설계하고 제안서까지 쓰는 것이 같은 사람의 몫입니다. 시연 가능한 작은 도구 하나를 끝까지 돌려 두면 프리세일즈 자리에서 말 대신 물건으로 설명하게 됩니다.
고객사가 반입 절차를 거쳐 서버에 올린 규정집·지침·과업지시서·개정 공문을 색인해 두고, 담당자가 터미널에서 `docq ask "용역 대금을 언제 지급하도록 정해 두었지"` 처럼 평소 말로 묻는 CLI입니다. 도구는 질문에서 뽑은 검색어와 조건을 먼저 표로 보여 주고, 그 조건으로 찾은 문단을 문서명·쪽수·조항 번호를 달아 원문 그대로 순위대로 찍습니다. 답 문장을 새로 쓰지 않으므로 문서에 근거가 없으면 근거 없음으로 끝내고 어떤 자료를 더 반입해야 하는지 되묻습니다. 색인과 검색과 순위 계산은 그 서버 안에서 끝나고, 질문을 검색어로 바꾸는 한 단계만 모델 어댑터를 거치며, 이 어댑터를 사내에 올린 오픈웨이트 모델로 바꿔 끼우면 바깥으로 나가는 호출이 한 건도 남지 않습니다.
고객사에서 사업 관리를 맡은 담당자입니다. 과업지시서와 내부 지침, 그 사이에 끼어든 개정 공문이 수백 쪽인데 비용을 이렇게 집행해도 되는지 확인하려면 어느 조항이 근거인지 찾아야 하고 그 조항이 아직 살아 있는지도 봐야 합니다. 지금은 파일을 하나씩 열어 단어를 찾다가 결국 오래 계신 분께 물어보고, 그분이 자리에 없으면 하루가 밀립니다. 평소 쓰는 말로 물으면 근거 조항과 쪽수가 바로 나오고 그 조항이 개정으로 바뀌었는지 같이 보이기를 바랍니다.
담당자의 질문은 대개 어느 문서의 어느 조항이 근거인지를 찾는 일이라서, 답 문장을 만들지 않고 문단을 찾아 원문과 위치를 그대로 보여 주기만 해도 확인 시간이 줄어듭니다. 모델은 질문을 검색어와 조건으로 바꾸는 변환만 맡고 순위와 인용은 색인이 결정하므로, 틀린 결과가 나와도 검색어 표에서 어디가 어긋났는지 바로 짚이고 없는 근거가 지어지지 않습니다.
반입한 문서를 문단으로 쪼개 색인하고, 담당자 질문을 검색어와 조건으로 바꿔 표로 확인시킨 뒤 근거 문단을 문서명·쪽수·조항 번호를 달아 순위대로 찍는 CLI입니다. 본문은 이미 텍스트로 뽑혀 나온 반출 파일(쪽 정보가 붙은 문단 목록)을 입력으로 가정하고, 스캔본 글자 판독과 사내망 실제 배포는 뒤 주차로 미뤄 1주에 혼자 완결합니다.
담당자가 실제로 물을 법한 질문 40개와 그 정답 문단을 만들어, 문단을 쪼개는 길이와 단어 검색·의미 검색의 비중을 바꿔 가며 정답이 상위 다섯 개 안에 들어온 비율과 몇 번째로 나왔는지를 표로 비교하고, 근거를 들어 설정 하나를 고르는 단계입니다.
2주차에서 고른 설정 그대로 모델 가중치와 컨테이너 이미지, 색인을 반입 번들 하나로 묶고 Helm 으로 올린 뒤, 바깥 연결을 막은 상태에서 같은 질문에 같은 근거가 나오는지 확인하고 반입 목록과 외부 호출 차단 검사 결과를 문서로 남기는 단계입니다.
담당자가 물을 법한 질문 12개를 그대로 치면 뽑아낸 검색어 표와 근거 문단이 문서명·쪽수·조항 번호를 달고 몇 초 안에 터미널에 나오는 걸 시연합니다
일부러 만든 까다로운 입력 30종(조항 번호가 없는 문단, 표가 한 줄로 이어붙은 문단, 글자가 안 잡힌 스캔 문단, 문서에 없는 용어를 묻는 질문)에도 출력 형식이 한 번도 깨지지 않고, 글자가 안 잡힌 문단은 검색에서 빠졌다고 알립니다
문서에 근거가 없는 질문 8개에서 답 문장을 만들어 낸 사례가 0건입니다. 상위 점수가 기준선에 못 미치면 근거 없음으로 끝나는 경로를 자동 테스트로 보여줍니다
직접 만든 질문 정답 40개에서 정답 문단이 상위 다섯 개 안에 들어온 비율(recall@5, 정답을 놓치지 않은 비율)이 0.85, 정답이 몇 번째로 나왔는지를 평균 낸 값(MRR, 1에 가까울수록 위쪽에서 찾음)이 0.71 입니다. 단, 문서와 정답을 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건에서의 동작입니다
위 수치는 전부 작성자가 만든 더미 문서 자가채점이며, 실제 검색 품질은 고객사 반입 문서와 담당자가 실제로 던지는 질문이 있어야 측정 가능합니다. 이 도구는 그 측정을 반입 첫날 바로 돌릴 수 있는 상태로 만듭니다.
이건 고객·실데이터 없이 혼자 만든 포트폴리오입니다. 모든 수치는 합성 문서 자가채점이라 성능 증명이 아닙니다. 면접에서 데모 조건 동작과 실검증 준비 상태로 솔직히 설명합니다
정답 문단을 작성자가 직접 지정했으니 잘 맞는 게 당연합니다. 조항 번호가 빠진 문단, 표가 한 줄로 뭉개진 문단, 개정으로 폐지된 조항, 같은 뜻의 다른 표기, 문서에 아예 없는 질문을 일부러 섞어 쉬운 문제만 푸는 게 아님을 보여줍니다
검색어가 잘못 뽑히면 엉뚱한 조항이 위로 올라옵니다. 검색 전에 검색어 표를 사람이 확인하게 막고, 검색어마다 근거가 된 질문 어절을 붙이며, 순위는 결정론 점수 계산이 정하므로 모델이 답을 지어내지 못합니다
고객사마다 문서 종류와 조항 번호 표기가 제각각이고, 개정 공문이 본문 밖에서 조항을 바꿔 놓는 일이 흔합니다. 문서 종류 목록과 용어 사전을 코드 밖 파일로 분리하고 대체 관계를 못 찾은 조항은 추측하지 말고 확인이 필요하다고 표시합니다. 실제 도입 시에는 이 사전과 질문 예시를 고객사 기준으로 다시 써야 합니다
폐쇄망 배포는 여기서 네트워크를 끊은 개인 머신으로 흉내 낸 것이라, 실제 고객사의 망분리 구성과 반입 심사 절차는 다릅니다. 반입 목록과 외부 호출 차단 검사까지가 이 포트폴리오의 범위이고 보안 적합성 판정은 담당 기관의 몫입니다
이 포트폴리오가 보여주는 것은 검색 파이프라인 구축과 검색 품질 측정, 오프라인 설치 구성 기술이며 국방·방산·공공 조달 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사가 반입한 규정집·지침·과업지시서·개정 공문을 문단 단위로 쪼갠 표입니다. 검색이 실제로 걸리는 대상이며, 현장 문서답게 조항 번호 결측·표가 뭉개진 문단·글자가 안 잡힌 스캔 문단·개정으로 폐지된 조항을 일부러 섞어 쉬운 데이터만 다루는 게 아님을 보여줍니다.
| chunk_id | doc_id | doc_title | doc_type | page | clause_no | text | effective_from | superseded_by | extract_quality |
|---|---|---|---|---|---|---|---|---|---|
| K001 | D01 | 정보시스템 구축 용역 과업지시서 | 과업지시서 | 12 | 제7조 제2항 | 용역 대금은 착수금, 중도금, 잔금으로 나누어 지급하며 착수금은 계약… | 2026-01-05 | — | text |
| K002 | D01 | 정보시스템 구축 용역 과업지시서 | 과업지시서 | 14 | — | 구분 착수금 중도금 잔금 비율 30% 40% 30% 지급시기 계약일+… | 2026-01-05 | — | table_flattened |
| K003 | D02 | 용역 대금 집행 지침 | 지침 | 3 | 제3조 | 산출물 검수는 검수요청서 접수일부터 14일 이내에 완료하며 보완 요구… | 2025-07-01 | — | text |
| K004 | D02 | 용역 대금 집행 지침 | 지침 | 4 | 제4조 | 선급금은 계약 금액의 20%를 초과하여 지급할 수 없다. | 2025-07-01 | K011 | text |
| K005 | D03 | 지침 일부 개정 알림 공문 | 개정공문 | — | — | 용역 대금 집행 지침 제4조의 선급금 한도를 20%에서 30%로 상향… | 2026-04-01 | — | text |
아래 JSON Schema 에 맞는 반입 문서 문단 목록 JSON 배열을 60건 생성하세요. 문서는 5~8종이고 한 문서에 여러 문단이 이어집니다.
{"type":"object","properties":{"chunk_id":{"type":"string"},"doc_id":{"type":"string"},"doc_title":{"type":"string"},"doc_type":{"type":"string","enum":["규정","지침","과업지시서","개정공문","회의록","매뉴얼"]},"page":{"type":["integer","null"]},"clause_no":{"type":["string","null"]},"text":{"type":"string"},"effective_from":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"extract_quality":{"type":"string","enum":["text","table_flattened","ocr_low","empty"]}},"required":["chunk_id","doc_id","doc_title","doc_type","page","clause_no","text","effective_from","superseded_by","extract_quality"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 조항 번호가 없는 문단(clause_no=null) 8건과 쪽 정보가 없는 개정 공문(page=null) 3건, (2) 표가 한 줄로 뭉개진 문단(extract_quality=table_flattened) 4건과 글자가 잘못 잡힌 문단(ocr_low) 3건, 본문이 빈 문단(text="", extract_quality=empty) 2건, (3) 개정으로 대체된 조항(superseded_by 에 새 chunk_id) 3쌍을 넣고 새 조항은 effective_from 을 뒤 날짜로 둘 것, (4) 같은 뜻을 다르게 적은 표기('선급금'과 '선금', '검수'와 '납품검사')를 서로 다른 문단에 섞을 것, (5) 시행일이 없는 회의록·매뉴얼 문단(effective_from=null) 5건. 모든 항목은 위 스키마를 통과해야 하며 JSON 배열만 출력합니다.{"type":"object","properties":{"chunk_id":{"type":"string"},"doc_id":{"type":"string"},"doc_title":{"type":"string"},"doc_type":{"type":"string","enum":["규정","지침","과업지시서","개정공문","회의록","매뉴얼"]},"page":{"type":["integer","null"]},"clause_no":{"type":["string","null"]},"text":{"type":"string"},"effective_from":{"type":["string","null"]},"superseded_by":{"type":["string","null"]},"extract_quality":{"type":"string","enum":["text","table_flattened","ocr_low","empty"]}},"required":["chunk_id","doc_id","doc_title","doc_type","page","clause_no","text","effective_from","superseded_by","extract_quality"]}담당자가 실제로 물을 법한 질문과 그 정답 문단입니다. 검색 결과를 규칙으로 자동 채점하는 기준이며, 문서에 근거가 없어 답하면 안 되는 질문과 기준일에 따라 정답이 갈리는 질문을 함께 담습니다.
| q_id | question | asked_by | gold_chunk_ids | gold_doc_type | as_of | expect_unanswerable | expect_terms |
|---|---|---|---|---|---|---|---|
| Q01 | 용역 대금을 언제 지급하도록 정해 두었지 | 사업관리 | ["K001","K002"] | 과업지시서 | — | false | ["용역 대금","지급"] |
| Q02 | 선금은 계약 금액의 몇 퍼센트까지 줄 수 있어 | 회계 | ["K011"] | 지침 | 2026-05-01 | false | ["선급금","한도"] |
| Q03 | 작년 기준으로 선급금 한도가 얼마였어 | 회계 | ["K004"] | 지침 | 2025-12-31 | false | ["선급금","한도"] |
| Q04 | 납품검사는 며칠 안에 끝내야 해 | 사업관리 | ["K003"] | 지침 | — | false | ["검수","기한"] |
| Q05 | USB 를 업무망에 꽂으려면 무슨 절차를 밟아야 하지 | 보안 | ["K006","K012"] | 규정 | — | false | ["저장매체","반입","승인"] |
아래 JSON Schema 에 맞는 질문 정답셋 JSON 배열을 40건 생성하세요. 질문은 doc_chunks 의 문단 내용에서만 답이 나와야 합니다.
{"type":"object","properties":{"q_id":{"type":"string"},"question":{"type":"string"},"asked_by":{"type":"string","enum":["사업관리","회계","보안","현장운영"]},"gold_chunk_ids":{"type":"array","items":{"type":"string"}},"gold_doc_type":{"type":["string","null"]},"as_of":{"type":["string","null"]},"expect_unanswerable":{"type":"boolean"},"expect_terms":{"type":"array","items":{"type":"string"}}},"required":["q_id","question","asked_by","gold_chunk_ids","gold_doc_type","as_of","expect_unanswerable","expect_terms"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 문서에 근거가 없어 답하면 안 되는 질문 8건은 gold_chunk_ids 를 빈 배열로 두고 expect_unanswerable=true 로 둘 것, (2) 조항 문구 그대로가 아니라 담당자가 쓰는 말로 물을 것('선급금'을 '선금', '산출물 검수'를 '납품검사', '저장매체 반입'을 'USB 꽂기'), (3) 기준일에 따라 정답이 갈리는 질문 6건은 as_of 를 개정 전후로 나눠 서로 다른 chunk_id 를 정답으로 둘 것, (4) 정답 문단이 둘 이상인 질문 6건, (5) 조항 번호를 직접 대는 질문 4건('지침 제3조가 뭐야'). 모든 항목은 위 스키마를 통과해야 하며 JSON 배열만 출력합니다.{"type":"object","properties":{"q_id":{"type":"string"},"question":{"type":"string"},"asked_by":{"type":"string","enum":["사업관리","회계","보안","현장운영"]},"gold_chunk_ids":{"type":"array","items":{"type":"string"}},"gold_doc_type":{"type":["string","null"]},"as_of":{"type":["string","null"]},"expect_unanswerable":{"type":"boolean"},"expect_terms":{"type":"array","items":{"type":"string"}}},"required":["q_id","question","asked_by","gold_chunk_ids","gold_doc_type","as_of","expect_unanswerable","expect_terms"]}담당자가 쓰는 말과 문서에 적힌 용어를 잇는 사전입니다. 질문에서 검색어를 넓힐 때 근거가 되며, 고객사가 바뀌면 이 표만 갈아끼웁니다. 어디로도 잇지 못한 말은 추측하지 않고 미매핑으로 남겨 되묻기 재료가 됩니다.
| term | maps_to | kind | source_doc_id | deprecated |
|---|---|---|---|---|
| 선금 | 선급금 | 구어 | D02 | false |
| 납품검사 | 산출물 검수 | 별칭 | D02 | false |
| 검수조서 | 기성 검사 조서 | 별칭 | D02 | false |
| USB | 외부 저장매체 | 구어 | D04 | false |
| 망분리 | 업무망과 외부망의 물리적 분리 | 별칭 | D04 | false |
아래 JSON Schema 에 맞는 용어 사전 JSON 배열을 40건 생성하세요. doc_chunks 의 본문에 실제로 나오는 용어와 담당자가 쓰는 말을 잇습니다.
{"type":"object","properties":{"term":{"type":"string"},"maps_to":{"type":["string","null"]},"kind":{"type":"string","enum":["약어","별칭","구어","단위"]},"source_doc_id":{"type":["string","null"]},"deprecated":{"type":"boolean"}},"required":["term","maps_to","kind","source_doc_id","deprecated"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 문서 용어를 가리키는 구어 표현을 2~3개씩 묶을 것('선금'과 '착수금 선지급'이 모두 선급금), (2) 어디로도 잇지 못하는 말(maps_to=null) 5건을 넣어 되묻기 재료로 삼을 것, (3) 지금은 쓰지 않는 옛 표기(deprecated=true) 4건, (4) 기간·비율 단위를 뜻하는 항목(kind=단위) 4건, (5) 출처 문서를 특정할 수 없는 항목(source_doc_id=null) 5건. 모든 항목은 위 스키마를 통과해야 하며 JSON 배열만 출력합니다.{"type":"object","properties":{"term":{"type":"string"},"maps_to":{"type":["string","null"]},"kind":{"type":"string","enum":["약어","별칭","구어","단위"]},"source_doc_id":{"type":["string","null"]},"deprecated":{"type":"boolean"}},"required":["term","maps_to","kind","source_doc_id","deprecated"]}| Python | 문단 분할, 색인 적재, 검색 순위 계산, 채점 스크립트까지 도구 전체를 쓰는 주 언어입니다 |
| RAG | 질문을 검색어와 조건으로 바꿔 문단을 찾아 근거로 붙이는 뼈대이며, 답 문장 생성 단계를 일부러 빼 인용만 남깁니다 |
| 벡터DB | 문단 임베딩을 담아 의미가 비슷한 문단을 찾는 저장소이며, 서버 안에서만 도는 Qdrant 를 컨테이너로 띄웁니다 |
| BM25 | 조항 번호나 고유 용어처럼 글자가 그대로 맞아야 하는 질문을 잡는 단어 검색이며, 의미 검색과 점수를 섞어 최종 순위를 만듭니다 |
| 임베딩 모델 | 한국어 문단을 벡터로 바꾸는 BGE-m3 를 로컬에서 돌려 외부 임베딩 API 없이 색인합니다 |
| Reranker | 상위 스무 개 문단을 질문과 다시 견줘 순서를 바로잡는 경량 교차 인코더이며 2주차 비교 대상 중 하나입니다 |
| Docker Compose | 검색 API·벡터DB·색인 워커를 한 묶음으로 띄워 1주차 개발 환경을 그대로 재현합니다 |
| Kubernetes | 고객사 온프레미스 클러스터를 가정한 3주차 배포 대상이며 자원 제한과 볼륨 마운트를 여기서 정합니다 |
| Helm | 반입 번들을 값 파일 하나만 바꿔 올리는 설치 단위이며 색인 초기화 작업까지 차트에 담습니다 |
| vLLM | 3주차에 오픈웨이트 모델을 사내에서 서빙해 질문 변환 어댑터의 외부 호출을 없애는 서빙 계층입니다 |
| Claude | 1주차에 질문을 검색어와 조건으로 바꾸는 변환 한 단계에만 쓰고 답 문장 작성에는 관여시키지 않습니다 |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우고 싶으면 2주차로 넘어가 질문 정답셋으로 검색 설정을 비교합니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우면 3주차로 넘어가 고른 설정을 반입 번들로 묶어 네트워크를 끊은 머신에서 다시 세웁니다.
질문 한 건을 (1) 넓힌 검색어와 그 근거가 된 질문 어절, (2) 문서 종류와 기준일 조건, (3) 확인을 거친 뒤 실행한 검색의 결과 문단과 점수, (4) 근거가 모자라 답하지 않은 경우의 되묻기로 풀어 담습니다. 답 문장 필드가 아예 없어서 모델이 문장을 지어낼 자리가 남지 않습니다.
{"type":"object","properties":{"question":{"type":"string"},"asked_at":{"type":"string"},"expanded_terms":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"from_phrase":{"type":"string"},"source":{"type":"string","enum":["질문","사전"]}},"required":["term","from_phrase","source"]}},"filters":{"type":"object","properties":{"doc_type":{"type":["string","null"]},"as_of":{"type":["string","null"]}},"required":["doc_type","as_of"]},"confirmed":{"type":"boolean"},"retrieval_config":{"type":"object","properties":{"chunk_chars":{"type":"integer"},"bm25_weight":{"type":"number"},"vector_weight":{"type":"number"},"top_k":{"type":"integer"},"score_floor":{"type":"number"}},"required":["chunk_chars","bm25_weight","vector_weight","top_k","score_floor"]},"hits":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"integer"},"chunk_id":{"type":"string"},"doc_title":{"type":"string"},"page":{"type":["integer","null"]},"clause_no":{"type":["string","null"]},"snippet":{"type":"string"},"score":{"type":"number"},"matched_terms":{"type":"array","items":{"type":"string"}},"superseded_note":{"type":["string","null"]}},"required":["rank","chunk_id","doc_title","page","clause_no","snippet","score","matched_terms","superseded_note"]}},"no_evidence":{"type":["object","null"],"properties":{"reason":{"type":"string"},"ask":{"type":"string"},"top_score":{"type":"number"}},"required":["reason","ask","top_score"]}},"required":["question","asked_at","expanded_terms","filters","confirmed","retrieval_config","hits","no_evidence"]}[
{
"input_ref": "질문 \"선금은 계약 금액의 몇 퍼센트까지 줄 수 있어\" (기준일 2026-05-01, 담당자가 검색어 표를 확인하고 승인한 경우)",
"output": {
"question": "선금은 계약 금액의 몇 퍼센트까지 줄 수 있어",
"asked_at": "2026-05-01 10:12",
"expanded_terms": [
{
"term": "선급금",
"from_phrase": "선금",
"source": "사전"
},
{
"term": "한도",
"from_phrase": "몇 퍼센트까지",
"source": "질문"
},
{
"term": "계약 금액",
"from_phrase": "계약 금액",
"source": "질문"
}
],
"filters": {
"doc_type": null,
"as_of": "2026-05-01"
},
"confirmed": true,
"retrieval_config": {
"chunk_chars": 800,
"bm25_weight": 0.5,
"vector_weight": 0.5,
"top_k": 5,
"score_floor": 0.32
},
"hits": [
{
"rank": 1,
"chunk_id": "K011",
"doc_title": "용역 대금 집행 지침(개정)",
"page": 4,
"clause_no": "제4조",
"snippet": "선급금은 계약 금액의 30%를 초과하여 지급할 수 없다.",
"score": 0.81,
"matched_terms": [
"선급금",
"계약 금액"
],
"superseded_note": null
},
{
"rank": 2,
"chunk_id": "K004",
"doc_title": "용역 대금 집행 지침",
"page": 4,
"clause_no": "제4조",
"snippet": "선급금은 계약 금액의 20%를 초과하여 지급할 수 없다.",
"score": 0.79,
"matched_terms": [
"선급금",
"계약 금액"
],
"superseded_note": "2026-04-01 개정으로 K011 이 대신합니다. 기준일이 개정일 이후라 아래로 내렸습니다."
},
{
"rank": 3,
"chunk_id": "K005",
"doc_title": "지침 일부 개정 알림 공문",
"page": null,
"clause_no": null,
"snippet": "용역 대금 집행 지침 제4조의 선급금 한도를 20%에서 30%로 상향하며 2026년 4월 1일부터 적용한다.",
"score": 0.74,
"matched_terms": [
"선급금",
"한도"
],
"superseded_note": null
}
],
"no_evidence": null
}
},
{
"input_ref": "질문 \"출장비 정산 기준이 어떻게 돼\" (반입 문서에 근거가 없는 경우)",
"output": {
"question": "출장비 정산 기준이 어떻게 돼",
"asked_at": "2026-05-01 10:31",
"expanded_terms": [
{
"term": "출장비",
"from_phrase": "출장비",
"source": "질문"
},
{
"term": "정산",
"from_phrase": "정산 기준",
"source": "질문"
}
],
"filters": {
"doc_type": null,
"as_of": null
},
"confirmed": true,
"retrieval_config": {
"chunk_chars": 800,
"bm25_weight": 0.5,
"vector_weight": 0.5,
"top_k": 5,
"score_floor": 0.32
},
"hits": [],
"no_evidence": {
"reason": "상위 문단 점수가 0.18 로 기준선 0.32 에 못 미쳐 근거로 쓰지 않았습니다. 반입된 문서에 출장비를 다룬 문단이 없습니다.",
"ask": "여비 규정이나 복무 지침이 아직 반입되지 않은 듯합니다. 해당 문서를 반입 신청해 주시겠습니까",
"top_score": 0.18
}
}
},
{
"input_ref": "질문 \"장비 켜기 전에 확인할 게 뭐였지\" (글자가 잘못 잡힌 스캔 문단이 걸린 경우)",
"output": {
"question": "장비 켜기 전에 확인할 게 뭐였지",
"asked_at": "2026-05-01 11:05",
"expanded_terms": [
{
"term": "전원",
"from_phrase": "켜기 전에",
"source": "사전"
},
{
"term": "확인",
"from_phrase": "확인할 게",
"source": "질문"
}
],
"filters": {
"doc_type": "매뉴얼",
"as_of": null
},
"confirmed": true,
"retrieval_config": {
"chunk_chars": 800,
"bm25_weight": 0.5,
"vector_weight": 0.5,
"top_k": 5,
"score_floor": 0.32
},
"hits": [
{
"rank": 1,
"chunk_id": "K009",
"doc_title": "장비 운용 매뉴얼",
"page": 22,
"clause_no": null,
"snippet": "전원 인가 전 접지 상태를 확인 하고 이상 시 즉시 담딩자에 게 연락 한다",
"score": 0.66,
"matched_terms": [
"전원",
"확인"
],
"superseded_note": "글자 인식 품질이 낮은 문단입니다. 원문 22쪽을 눈으로 확인해 주세요."
}
],
"no_evidence": null
}
}
][자동 검증 — 스크립트가 매번 돌립니다] 1. JSON Schema 통과 여부. 필드가 빠지거나 자료형이 어긋난 출력을 걸러 냅니다. 2. expanded_terms 의 from_phrase 가 모두 질문 원문에 그대로 있는 어절인지 검사합니다. 질문에 없는 말로 검색어를 만들어 내면 실패로 처리합니다. 3. hits 의 chunk_id·doc_title·page·clause_no·snippet 이 색인 원문과 글자 단위로 같은지 대조합니다. 문단을 요약하거나 고쳐 쓴 흔적이 있으면 실패입니다. 4. no_evidence 가 채워졌으면 hits 가 비어 있고 top_score 가 score_floor 미만인지 확인해, 근거 없이 결과를 내보내는 경로를 막습니다. 5. 검색 채점은 gold_chunk_ids 가 상위 다섯 개 안에 들어왔는지와 몇 번째에 나왔는지를 규칙으로 계산하며 부분 점수를 따로 주지 않습니다. 6. as_of 가 있는 질문은 시행일이 지난 조항이 1위로 오면 실패로 보고, 대체 조항이 있는데 superseded_note 가 비어 있어도 실패로 처리합니다. [수동 검증 — 사람이 눈으로 봅니다] 1. 되묻기 문구가 담당자가 실제로 답할 수 있는 요청인지 40건을 읽어 확인합니다. 2. 글자 인식 품질이 낮은 문단이 결과에 섞였을 때 안내 문구가 오해를 부르지 않는지 봅니다. 3. 이 확인은 작성자가 대신 하므로 합성 기준이며, 실제로는 고객사 담당자가 봐야 결론이 납니다.