1-page 아키텍처 deep-dive
외부 LLM 없이 extractive grounded answer를 생성하고, 공개 fixture smoke + private/internal eval split으로 검증되는 RFP DocAgent. 핵심 결정은 ADR로 추적되며 매 평가 호출마다 baseline 컬럼이 자동 측정된다.
파이프라인 (ADR 라벨 포함)
flowchart TD
Q[User Query] --> A["Query Analyzer<br/>entities + query_type"]
A --> P["Planner<br/>metadata-first<br/>comparison-balanced top_k<br/>[ADR 0002]"]
P --> RD["Dense channel<br/>MiniLM cosine"]
P --> RB["Lexical channel<br/>BM25 [ADR 0010]"]
RD --> FU{retrieval_backend}
RB --> FU
FU -->|dense| W["Weighted fusion"]
FU -->|hybrid| RRF["RRF k=60"]
W --> E[Evidence Aggregator]
RRF --> E
E --> EB["Evidence boundary<br/>neutralize injection [ADR 0008]"]
EB --> V["Verifier / Retry<br/>strict -> relaxed [ADR 0004]"]
V --> G["Answer Generator<br/>extractive default [ADR 0003]<br/>llm_synthesis ablation [ADR 0011]"]
G --> F["Final Response<br/>claims + citations + status"]
F --> EV["Eval<br/>public fixture smoke [ADR 0005]<br/>private/internal aggregate"]
classDef highlight fill:#fffbdd,stroke:#d4a017,stroke-width:2px,color:#000
class P,V,G highlight
단계별 한 줄 요약
| 단계(Stage) | 역할 | 핵심 코드 | ADR | 측정 신호 |
|---|---|---|---|---|
| Ingestion | HWP CSV fallback · 메타 6컬럼 | ingestion.py, visual_ingestion.py |
— | ingestion_report.json |
| Chunking | fixed(baseline) vs section | rag_core.py, rag_indexing.py |
— | chunk_seq_in_section |
| Query analyzer | 엔터티/유형/모호성 추출 | rag_query.py |
— | analyze_query trace |
| Planner | metadata-first · comparison-balanced top_k | rag_core.py, rag_retrieval.py |
0002 | citation precision |
| Retriever | dense · BM25 · RRF k=60 | rag_retrieval.py |
0010 | recall@k |
| Evidence boundary | 외부 chunk의 prompt injection 무력화 | rag_verifier.py |
0008 | prompt injection regression test |
| Verifier / Retry | strict -> relaxed, partial-topic 모드 | rag_core.py, rag_verifier.py |
0004 | retry_trigger_reason |
| Answer generator | extractive 기본 / LLM 합성 ablation | rag_answer.py, rag_synthesis.py |
0003, 0011 | answer_format_compliance |
| Eval | 공개 fixture smoke + private/internal aggregate | eval/run_eval.py, scripts/run_real_eval_delta.py |
0005 | metrics split + latency |
평가 경계
이 레포지토리는 공개 가능한 작은 fixture를 smoke test 용도로만 사용합니다. 실제 성능 평가는 레포지토리에 커밋하지 않는 private/internal eval set을 기준으로 수행하는 것을 전제로 합니다.
공개 fixture smoke는 eval/fixtures/smoke_rfp/raw/와 eval/config.yaml로 실행되며, 목적은 평가 프레임워크가 private data 없이 deterministic하게 동작하는지 확인하는 것이다. 성능 claim의 source of truth는 private/internal eval aggregate다.
평가 지표는 다음 축으로 분리한다.
| 축 | 예시 metric | 해석 |
|---|---|---|
| 검색(retrieval) 품질 | chunk_recall@k, MRR, nDCG |
관련 chunk를 찾는가 |
| 답변 품질(answer quality) | accuracy, groundedness, abstention outcome | 근거 기반 답변/보류가 맞는가 |
| 인용/근거(citation/evidence) | citation precision, claim-citation alignment | claim과 evidence가 맞물리는가 |
| 지연시간(latency) | p50/p95, stage latency, retry cost | 품질 개선이 latency 비용을 정당화하는가 |
핵심 결정 4가지
| 결정 | 본문 위치 | 한 줄 왜 |
|---|---|---|
| Extractive를 기본값으로 | ADR 0001 | advanced component는 latency·complexity·regression surface를 동반 → baseline 옆에 두지 않으면 질 개선인지 실패 모드 이동인지 판단 불가 |
| Metadata-first retrieval | ADR 0002 | RFP는 메타데이터(발주기관·사업명)가 진정한 anchor — dense top-k 단독은 비교 질의에서 starvation 발생 |
| Abstention을 1급 status로 | ADR 0003 | 근거 부족을 정직하게 인정하는 것이 RFP 도메인에서 모호한 답변보다 가치 큼 |
| 평가 surface 분리 | ADR 0005 | 공개 fixture 재현성과 실제 성능 신호를 하나의 surface로 동시에 충족할 수 없음 |
더 읽을 거리
- README §평가 스토리
- 모듈 맵 — 파이프라인 단계별 코드 소유권과 호출 순서
- 설계 배경 — 핵심 의사결정과 reviewer 판독 경로
- ADR 인덱스
- Engineering governance