921. 사내 규정집 RAG 챗봇 제작기
프로젝트 개요와 최종 아키텍처
- 사내 문서 기반 RAG 챗봇 웹앱의 전체 구조를 이해하고 직접 재현할 수 있다.
- 검색 방식·임베딩 모델·청킹 전략의 대안들을 비교하고, 상황에 맞는 선택 기준을 세울 수 있다.
- 프롬프트만으로 부족한 환각 방지를 구조(원문 발췌 렌더링)로 해결하는 방법을 익힌다.
무엇을 만들었나
회사 HR 규정집 4종을 지식으로 삼아, 규정에 없는 말은 하지 않고 답변마다 원문 근거를 그대로 발췌해 보여주는 사내 규정 안내 챗봇이다. UI는 ChatGPT.com과 같은 사용감을 목표로 했다.
| 요구사항 | 구현 |
|---|---|
| ChatGPT 스타일 UI | 사이드바(새 채팅·대화 목록), 토큰 스트리밍, 라이트/다크, Enter 전송 |
| 지식 4종 | 회사 인사 및 복무 규정 · 경조사 규정 · 전결 규정 · 출장 및 여비 규정 |
| 규정 선택 대화 | 사이드바 체크박스 → 선택된 규정만 검색 대상(검색 수준에서 차단) |
| 내 파일과 대화 | 📎 업로드(pdf/docx/md/txt) → 세션 스코프 색인 → 즉시 질문 가능 |
| 환각 방지 | RAG 강화 프롬프트 + 근거 임계값 거부 + 원문 발췌는 서버가 렌더링 |
| 원문 근거 블록 | 답변 하단 접이식 카드: 규정명 > 조항 경로 + 원문 그대로 + 관련도 |
| LLM | OpenAI gpt-5-mini (환경변수로 교체 가능) |

최종 검색 파이프라인
질문 하나가 답변이 되기까지 7단계를 거친다. 핵심은 넓게 모아서(하이브리드) 정밀하게 거르고(리랭커) 근거를 원문 그대로 보여주는 것이다.
설계 결정 — 대안과 선택의 이유
벡터 임베딩은 필수인가
답: 필수는 아니다. 그러나 이 요구사항에서는 최선이다. 임베딩 없이 돌아가는 것처럼 보였던 사례의 실체부터 확인해야 한다. 바이브코딩 에이전트는 grep·파일읽기 도구로 직접 탐색하는 에이전틱 검색이고, GPTs는 임베딩을 안 하는 게 아니라 OpenAI file_search가 뒤에서 대신(청킹→임베딩→벡터검색) 해주는 것이다. 직접 만들 때는 그 부분이 우리 몫이 된다.
| 후보 | 장점 | 단점 | 판정 |
|---|---|---|---|
| 풀 컨텍스트 주입 검색 없음 | 구현 최소. 규정 4개(~수만 토큰)면 실제로 동작 | 매 턴 전체 토큰 비용 · 대용량 업로드에 취약 · 원문 발췌를 모델 생성에 의존(변형 위험) | 탈락 |
| BM25 + 리랭커 임베딩 없음 | 로컬 모델 1개, 색인 단순 | 유의어 질문("부친상"→"직계가족 사망")을 후보에 못 올릴 수 있음 | 차선 |
| 하이브리드 + 리랭커 | 어휘·의미 상호 보완. 2026년 실무 표준 구성 | 임베딩 모델 운영 필요 | 채택 |
"원문을 그대로 인용하라"는 지시만으로는 모델이 인용문을 미묘하게 바꾸는 것을 막을 수 없다. 이 프로젝트의 핵심 설계는 인용문 생성을 모델에게 맡기지 않는 것이다.
답변 끝에 SOURCES: [1,3] — 사용한 발췌 번호만 보고한다.
번호를 검색 인덱스의 원문 청크(규정명·조항 경로·본문)로 매핑한다.
근거 블록의 모든 글자는 인덱스 원문 — 모델이 만든 문장이 0%다.
스트리밍 중에는 SOURCES 마커 접두어를 보류하는 필터가 마지막 줄이 화면에 노출되는 것을 막는다. 사용자는 답변만 보고, 근거는 별도 블록에서 원문으로 확인한다.
임베딩 모델 선택
웹 리서치 후보 탐색 및 선택 — 2026년 중반 기준.
| 후보 | 특징 | 판정 |
|---|---|---|
| BGE-M3 (2024) | 다국어 100+ · dense+sparse+multi-vector · MIT 라이선스. "2026년에도 프로덕션 RAG 스택의 기본값"으로 통용 | 채택 |
| KURE-v1 | BGE-M3의 한국어 특화 파인튜닝(고려대). 한국어 검색 벤치마크 최상위권 — 모델명 1줄 교체로 전환 가능하게 구현 | 유력 대안 |
| Qwen3-Embedding (2025) | MTEB multilingual 리더보드 1위권(8B, 70.58). 0.6B도 강력 — 성능 실험용 | 실험 후보 |
| OpenAI text-embedding-3 | API라 운영 단순, 로컬 모델 불필요. 단 오픈소스 아님 · 색인마다 과금 | 보류 |
| multilingual-e5 | 과거 표준. 신규 프로젝트 우선순위 낮음 | 세대교체 |
~/.cache/huggingface)로 받아진다. 사용자 전역 캐시라 여러 프로젝트가 공유하며, 프로젝트 폴더에 두고 싶으면 HF_HOME 환경변수만 바꾸면 된다. 파이썬 패키지는 프로젝트 venv에 격리 설치했다.리랭커·청킹·저장소 — 나머지 결정들
리랭커: 하이브리드에서 멈추지 않는다
1차 검색은 "넓게 모으는" 단계다. 크로스인코더 리랭커(bge-reranker-v2-m3)는 질문과 청크를 한 쌍으로 함께 읽고 "이 질문에 진짜 답하는 조항인가"를 재판정한다. 금액·일수·승인권자처럼 정답이 하나인 질문에서 체감 효과가 가장 크고, 업계 리서치에서도 "가장 ROI 높은 개선"으로 꼽힌다. 부가 효과로 리랭커 점수를 근거 충분성 임계값(sigmoid 0.15)으로 재활용해 미달 시 거부 경로로 보낸다.
청킹: 문단이 아니라 조항 단위
규정 문서는 제N조 (제목) 구조가 명확하다. 마크다운 헤딩 최하위 단위로 잘라 조항 1개 = 청크 1개를 만들고, 각 청크에 『규정명』 > 장 > 절 컨텍스트 헤더를 붙여 색인한다(Anthropic Contextual Retrieval의 경량판 — 원 실험에서는 리랭킹과 결합 시 검색 실패 67% 감소 보고). 긴 조항은 빈 줄 경계로만 2차 분할해 표가 중간에서 잘리는 일이 없다.
벡터 DB를 쓰지 않은 이유
Qdrant·pgvector·Elasticsearch도 검토했지만 numpy 인메모리 + 디스크 캐시를 채택했다. 청크 58개 규모에서 벡터 DB는 운영 부담만 더한다 — emb @ q_emb 한 줄이면 전량 코사인 검색이 끝난다. 문서가 수만 청크로 늘어나는 시점에 도입하는 것이 옳은 순서다.
저장 위치는 backend/.cache/emb-<해시>.npy 파일 하나다. BGE-M3로 58개 청크를 전부 임베딩한 58×1024 float32 행렬 그 자체(약 232KB — 58×1024×4바이트)이며, 파일명 해시를 모델명+청크 내용으로 만들어 규정이 바뀌면 자동으로 재계산된다. 서버 시작 시 이 파일을 메모리에 올려두고, 질문마다 내적 한 번으로 58개 전부와 코사인 유사도를 구한다. 그래도 벡터 DB라고 부르지 않는 이유는 "벡터 DB"가 데이터가 아니라 시스템을 가리키는 말이기 때문이다.
| 벡터 DB가 제공하는 것 | 이 프로젝트의 numpy 방식 |
|---|---|
| ANN 인덱스 (HNSW 등 근사 검색) | 없음 — 58개 전량 완전탐색. 이 규모에선 마이크로초 단위라 근사가 불필요 |
| CRUD (벡터 개별 추가·삭제·수정) | 없음 — 문서가 바뀌면 통째로 재색인 (내용 해시 캐시가 자동 감지) |
| 메타데이터 필터링 | 없음 — 규정 선택 체크박스 필터는 파이썬 마스크로 처리 |
| 서버 프로세스 (동시성·스케일링) | 없음 — FastAPI 프로세스 메모리에 상주 |
나머지 결정 요약
| 결정 항목 | 선택 | 이유 |
|---|---|---|
| BM25 구현 | rank_bm25 (계획은 bm25s) | 순수 파이썬 — Windows 호환 확실. 이 규모에서 속도 차이 무의미 |
| 형태소 분석 | kiwipiepy | 한국어 BM25의 전제조건. 명사·동사·숫자·외래어 태그만 토큰화 |
| 웹 스택 | FastAPI + React(Vite) | 형태소분석·임베딩 생태계가 Python에 있음. UI 유지보수는 React |
| LLM | gpt-5-mini (교체 가능) | 비용/품질 균형. gpt-5 계열은 temperature 미지원 → reasoning_effort로 제어 |
| 멀티턴 | 질의 재작성 단계 추가 | "그럼 경조금은?" 같은 후속질문을 독립 검색 질의로 변환 |
| 대화 이력 | localStorage | 단일 사용자 로컬 앱 — 서버 DB는 과함 |
| 업로드 색인 | 세션 스코프 인메모리 | 개인 파일이 공용 지식과 섞이지 않음. 재시작 시 소멸(의도됨) |
지식 베이스 구축
규정집 4종 — 정합성이 생명
지식 베이스는 규정집 4종이다. 챗봇이 여러 규정을 동시에 검색해 답을 합성하므로 규정 간 수치가 서로 모순되면 그대로 오답이 된다. 4종은 수치·담당자·승인 절차가 서로 맞물리도록 설계했다.
docs/ 최상위 *.md만 자동 색인한다. 하위 폴더(이 교재가 있는 docs/교재/ 등)는 제외 — 교재가 규정으로 검색되는 사고를 막는다. 파일 내용 해시로 임베딩을 캐시해 내용이 바뀐 경우에만 재계산한다.핵심 구현
조항 단위 청킹 + 컨텍스트 헤더
원문(text)과 색인용(search_text)을 분리 저장하는 것이 포인트다. 발췌 블록에는 원문이, 검색 인덱스에는 경로가 붙은 버전이 쓰인다.
path_titles = [t for lv, t in stack if lv >= 2]
section_path = " > ".join(path_titles) # "제2장 경조휴가 > 제4조 (경조휴가 일수)"
header = f"『{doc_title}』 > {section_path}"
chunks.append(Chunk(
...,
text=part, # 원문 그대로 → 근거 발췌 블록용
search_text=f"{header}\n{part}", # 컨텍스트 헤더 부착 → BM25/임베딩 색인용
))하이브리드 검색 → RRF → 리랭킹
bm25_scores = shard.bm25.get_scores(q_tokens) # 어휘 검색
dense_scores = shard.emb @ q_emb # 의미 검색 (정규화 벡터 내적 = 코사인)
# 방식별 순위를 RRF로 융합: score = Σ 1/(60 + rank)
for rank, (key, score) in enumerate(bm25_scored[:20]):
if score > 0:
rrf[key] = rrf.get(key, 0.0) + 1.0 / (60 + rank + 1)
for rank, (key, _) in enumerate(dense_scored[:20]):
rrf[key] = rrf.get(key, 0.0) + 1.0 / (60 + rank + 1)
# 상위 12개만 크로스인코더로 정밀 재정렬
pairs = [(query, candidates[key].search_text) for key, _ in fused]
logits = get_reranker().predict(pairs)
...
has_evidence = results[0].score >= 0.15 # 근거 충분성 판단allowed_doc_ids). 프롬프트 수준이 아니라 검색 수준에서 차단하므로 "선택 안 한 규정으로 답하는" 사고가 구조적으로 불가능하다.환각 방지 시스템 프롬프트
SOURCES 스트리밍 필터와 멀티턴 재작성
토큰을 실시간으로 흘리면서도 마지막 줄 SOURCES: [1,3]은 화면에 노출하지 않아야 한다. 누적 버퍼 끝이 마커의 접두어와 겹치는 길이만큼 전송을 보류하는 필터를 구현했다.
def _held_suffix_len(pending: str) -> int:
"""pending 끝부분이 SOURCES: 마커의 접두어와 겹치는 길이 (스트리밍 중 보류)."""
for k in range(min(len(_SOURCE_MARKER), len(pending)), 0, -1):
if _SOURCE_MARKER.startswith(pending[-k:]):
return k
return 0멀티턴 처리: "부친상 휴가 며칠?" → (답변) → "그럼 경조금은?" 같은 후속질문은 그대로 검색하면 실패한다. 직전 대화 6턴을 넣고 "마지막 질문을 독립적인 검색 질의 한 문장으로 재작성하라"는 소형 GPT 호출(reasoning_effort: minimal)을 검색 전에 수행하고, 재작성된 해석을 [질문 해석]으로 생성 프롬프트에도 전달한다.


검증 — 스모크 테스트와 E2E
검색 품질 스모크 테스트
서버 없이 검색 파이프라인만 검증하는 CLI(backend/smoke_test.py)를 먼저 만들었다. 검색이 틀리면 그 뒤는 전부 무의미하기 때문에 검색부터 검증하는 순서가 중요하다.
| 질문 | 1위 검색 결과 | 리랭커 점수 |
|---|---|---|
| 부친상 때 며칠 쉬나요? | 경조사 규정 > 제4조 (경조휴가 일수) | 0.503 |
| 배우자 출산 축하금 얼마 받아요? | 인사·복무 규정 > 2.2 경조금 지급 | 0.725 |
| 해외 출장은 누구 승인을 받아야 하나요? | 출장여비 규정 > 제3조 (출장 신청 및 승인) | 0.730 |
| 과장 국내 출장 숙박비 한도는? | 출장여비 규정 > 제4조 (국내 출장 여비 지급 기준) | 0.729 |
| 연장근무 월 20시간 넘으면 승인은? | 인사·복무 규정 > 1.1 근무 시간 (2위: 전결 규정 제6조) | 0.715 |
Playwright E2E — 12개 시나리오
실제 브라우저(번들 Chromium)로 사용자 시나리오 전체를 자동 검증했다: 앱 로드 → 질문/근거 블록 → 멀티턴 후속질문 → 규정 선택 필터(거부) → 파일 업로드 질문 → 다크 모드 → 대화 이력 복원. 아래 스크린샷이 그 기록이다.




디버깅 사례 — 후속질문이 거부당한 이유
트러블슈팅증상 — 근거가 있는데도 "확인되지 않는 내용입니다"
1차 E2E에서 후속질문("그럼 경조금은 얼마 받을 수 있어?")이 거부 응답을 받았다. 단계별로 재현한 결과: 질의 재작성 정상 → 검색 정상(컨텍스트에 "100만 원" 포함 확인) → 생성 단계에서 모델이 거부. 원인은 3중 조합이었다 — ① reasoning_effort: minimal(추론 거의 없음)로는 표 안의 행까지 못 찾고, ② 거부 규칙이 지나치게 엄격했으며("확인할 수 없으면 정확히 이렇게 답한다"가 쉬운 탈출구가 됨), ③ 후속질문의 생략된 주어("그럼 경조금은")를 해석해야 했다.
| 수정 | 내용 |
|---|---|
| 추론 수준 상향 | 답변 생성은 minimal → low (재작성은 minimal 유지 — 용도별 차등) |
| 질문 해석 전달 | 검색용으로 이미 계산한 재작성 질의를 [질문 해석]으로 생성 프롬프트에 재활용 |
| 거부 규칙 완화 | "관련 내용이 전혀 없을 때만" 거부. 부분 근거면 그 범위에서 답변 |
수정 후 같은 질문에 "부친(직계가족) 상의 경우 위로금 100만 원입니다 [1][3]"로 정확히 답했다. 교훈: RAG 실패는 검색 실패가 대부분이라지만, 검색이 완벽해도 생성 단계 설정(추론 수준·규칙의 강도)이 답을 망칠 수 있다. 단계별 재현으로 원인을 분리하라.
트러블슈팅 기록
| 문제 | 원인 | 해결 |
|---|---|---|
| PowerShell 한글 경로 파일 복사 실패 | 콘솔 인코딩(cp949)과 UTF-8 충돌 | Git Bash로 파일 작업 수행 |
| curl 한글 JSON 전송 시 파싱 오류 | msys→win32 경계의 인자 인코딩 변환 | UTF-8 파일로 저장 후 --data-binary @file |
| Chrome이 localhost 접속 실패 | 브라우저 HTTPS 강제 설정이 http:// 차단 | 사용자 브라우저 설정을 건드리지 않고 Playwright(번들 Chromium)로 E2E |
| Vite가 IPv6(::1)에만 바인딩 | Node 최신 버전의 localhost 해석 | server.host: '127.0.0.1' 명시 |
| gpt-5 계열 temperature 오류 | reasoning 모델은 temperature 미지원 | 모델명 분기로 reasoning_effort 사용 |
챗봇 작동 프로세스 설명
실제 질문 하나를 끝까지 추적한다
파이프라인 실측질문 "부친상 때 며칠 쉬나요?" 가 답변이 되기까지의 모든 중간 결과를 실측 그대로 보인다. 이 질문을 고른 이유: "부친상"이라는 단어는 규정집 어디에도 없다(규정에는 "직계가족 사망"이라 쓰여 있다). 단계마다 검색 기법이 어떻게 성공하고 실패하는지가 극명하게 드러난다. 아래 숫자는 예시가 아니라 실제 실행 결과다.
0단계 — 질문 형태소 토큰화 (kiwipiepy)
"부친상 때 며칠 쉬나요?" → ['부친', '상', '때', '며칠', '쉬']
BM25는 단어(형태소) 단위로 문서를 찾으므로 한국어에선 형태소 분석이 선행된다. "부친상"이 부친+상으로 분해된 것에 주목 — 이 분해가 다음 단계의 운명을 결정한다.
1단계 — BM25 키워드 검색: 사실상 실패
| 순위 | 청크 | BM25 점수 |
|---|---|---|
| 1 | 『전결 규정』 > 제2조 (용어의 정의) | 3.670 |
| 2 | 『인사 및 복무 규정』 > 잔업비 규정 | 2.300 |
| 3~ | (정답 조항 포함 나머지 전부) | 0.000 |
규정집에 "부친"이 없으므로 정답 조항(경조사 규정 제4조)은 0점. 상위 2개는 상·때 같은 조각이 우연히 걸린 무관한 문서다. 키워드 검색의 근본 한계 — 사용자의 어휘와 문서의 어휘가 다르면 못 찾는다. (반대로 "제7조"·"30만원"처럼 정확한 용어를 쓰는 질문에선 BM25가 벡터보다 강하다.)
2단계 — BGE-M3 벡터 검색: 의미로 구조
| 순위 | 청크 | 코사인 |
|---|---|---|
| 1 | 『인사 및 복무 규정』 > 1.2 휴가 정책 > 특별 휴가 | 0.5843 |
| 2 | 『인사 및 복무 규정』 > 1.1 근무 시간 | 0.5295 |
| 3 | 『인사 및 복무 규정』 > 연차휴가 | 0.5254 |
| 4 | 『경조사 규정』 > 제4조 (경조휴가 일수) | 0.5247 |
| 5 | 『인사 및 복무 규정』 > 2.3 복리후생 | 0.4925 |
임베딩 모델이 "부친상"↔"직계가족 사망", "며칠 쉬나요"↔"휴가 일수"의 의미적 유사성을 잡아냈다. 1위와 4위 모두 실제 정답("직계 가족 사망: 5일")을 담고 있다. 단 벡터도 완벽하진 않다 — "근무 시간"·"연차휴가" 같은 넓은 의미의 무관 문서도 섞여 있다.
3단계 — RRF 융합: 두 검색의 순위를 합친다
점수 스케일이 다른 두 검색을 순위만으로 결합한다: 점수 = Σ 1/(60+순위). BM25 0점 문서는 제외.
| 융합순위 | 청크 | BM25 순위 | 벡터 순위 | RRF 점수 |
|---|---|---|---|---|
| 1 | 인사·복무 > 잔업비 규정 | 2 | 8 | 0.0308 |
| 2 | 전결 > 제2조 (용어의 정의) | 1 | - | 0.0164 |
| 3 | 인사·복무 > 특별 휴가 | - | 1 | 0.0164 |
| 4~5 | 인사·복무 > 근무 시간 · 연차휴가 | - | 2·3 | 0.016 |
| 6 | 경조사 > 제4조 (경조휴가 일수) | - | 4 | 0.0156 |
| 7~12 | (복리후생, 전결 제6조, 경조사 제5·6조 등) |
융합 1위가 정답이 아니라 잔업비 규정이다(양쪽 검색에 모두 등장했다는 이유로). 정답은 6위. 융합까지도 완벽하지 않다 — 그래서 상위 12개를 '후보'로만 취급하고 다음 단계가 정밀 검증한다.
4단계 — 크로스인코더 리랭킹: 정답을 1위로 교정
임베딩(바이인코더)은 질문과 문서를 따로 벡터화해 거리를 재지만, 크로스인코더(bge-reranker-v2-m3)는 질문과 청크를 한 쌍으로 함께 읽고 판정한다. 느리므로 후보 12개에만 적용.
| 최종순위 | 청크 | 리랭커 점수 | 판정 |
|---|---|---|---|
| 1 | 경조사 > 제4조 (경조휴가 일수) | 0.503 | 컨텍스트 포함 |
| 2 | 인사·복무 > 특별 휴가 | 0.502 | 컨텍스트 포함 |
| 3 | 경조사 > 제6조 (경조휴가 신청 절차) | 0.501 | 컨텍스트 포함 |
| 4~6 | 경조사 제5조 · 연차휴가 · 근무 시간 | 0.500 | 컨텍스트 포함 |
| 7~12 | (융합 상위였던 잔업비·전결 용어정의 등 무관 문서) | 0.500 | 탈락 |
융합 1위였던 잔업비 규정이 밀려나고 정답 제4조가 1위로 올라왔다. 근거 충분성: 1위 0.503 ≥ 임계값 0.15 → 답변 생성으로 진행.
5단계 — GPT 답변 생성
최종 6개 청크에 [1]~[6] 번호를 붙여 환각 방지 프롬프트와 함께 gpt-5-mini에 전달했다. 실제 생성 결과 전문:
부친상(직계가족 사망) 시 유급 경조휴가 5일이 부여됩니다. 『경조사 규정』 제2장 제4조에 따른 내용입니다. [1]
증빙서류로는 사망진단서 및 가족관계증명서가 필요합니다. 『경조사 규정』 제2장 제4조에 명시되어 있습니다. [1]
신청은 인사 시스템을 통해 하여 소속 팀장의 승인을 받아야 하며, 긴급한 경우 팀장에게 유선 또는 메신저로 먼저 보고한 후 복귀 후 3일 이내에 증빙서류를 제출할 수 있습니다. 또한 증빙서류를 기한 내 제출하지 않으면 해당 휴가는 연차휴가로 대체 처리될 수 있습니다. 『경조사 규정』 제2장 제6조에 명시되어 있습니다. [3]
SOURCES: [1, 3] ← 이 줄은 스트리밍 필터가 화면에서 숨기고, 서버가 번호를 인덱스 원문 청크로 매핑해 '근거 원문' 블록으로 보낸다
한눈에 보는 교훈 — 각 단계는 서로의 약점을 보완한다
| 단계 | 이 질문에서의 성적 | 역할 |
|---|---|---|
| BM25 | 실패 (정답 0점) | 정확한 용어·조항번호·금액 질문에 강함 |
| 벡터 검색 | 정답을 후보에 올림 (4위) | 유의어·의미 질문에 강함 |
| RRF 융합 | 1위는 오답, 정답은 6위 | 두 검색을 안전하게 결합해 후보군 확보 |
| 리랭커 | 정답을 1위로 교정 | 후보 정밀 재판정 + 근거 충분성 게이트 |
| GPT 생성 | 5일 + 증빙 + 절차, 근거 번호 보고 | 발췌 안에서만 답하고 출처 표기 |
cd backend && .venv/Scripts/python trace_pipeline.py "아무 질문" — 어떤 질문이든 위와 같은 단계별 표가 출력된다. 교육 실습으로 각자 질문을 넣어 BM25가 이기는 질문과 벡터가 이기는 질문을 찾아보게 하면 좋다.확장 아이디어와 재현 체크리스트
더 해보기
도전 1검색 평가셋 구축
질문-정답조항 쌍 30개를 만들어 Recall@k·MRR을 자동 측정하라. 리랭커 임계값(0.15)·top-k(6) 튜닝이 감이 아니라 숫자로 가능해진다.
도전 2KURE-v1 A/B 테스트
indexer.py의 EMBED_MODEL_NAME을 nlpai-lab/KURE-v1로 바꾸고 평가셋으로 BGE-M3와 비교하라. 캐시 키에 모델명이 포함되어 있어 재색인은 자동이다.
도전 3표 행 단위 보조 청크
직급별 한도표를 행 단위로도 색인하면 "차장 숙박비"류 질문의 정밀도가 오른다. chunk_type 메타데이터를 추가해 보라.
도전 4운영 기능
대화 서버 저장(SQLite)·사용자 인증·답변 피드백(👍👎) 수집 → 실패 질문 로그로 규정집을 보완하는 운영 루프를 만들어 보라.
재현 체크리스트
- 조항 단위 청킹 + 컨텍스트 헤더가 적용되어 있는가 (표가 잘리지 않는가)
- BM25(형태소)와 벡터 검색이 모두 동작하고 RRF로 융합되는가
- 리랭커 점수 임계값 미달 시 거부 경로로 가는가
- 근거 블록이 모델 출력이 아니라 인덱스 원문을 렌더링하는가
- 체크박스 필터가 프롬프트가 아니라 검색 단계에서 적용되는가
- 후속질문이 질의 재작성을 거쳐 검색되는가
- 스모크 테스트(검색)와 E2E(전체 흐름)를 모두 통과했는가