| |
| """3단계 검증: 5케이스(라우팅·환각방지·대화기억) + 측정(JSON 깨짐률·노드 지연).""" |
| import json |
|
|
| from rag.graph import build_graph |
| from rag.search import Index |
| from rag import common as C |
|
|
| CKPT = C.ROOT / "rag" / "checkpoints_validate.db" |
|
|
|
|
| def brief(state): |
| a = state.get("answer", {}) |
| r = state.get("route", {}) |
| return { |
| "rewritten": state.get("rewritten"), |
| "qtype": r.get("qtype"), "collections": r.get("collections"), |
| "retrieved": [h["ref_key"] or h["doc_no"] for h in state.get("retrieved", [])], |
| "answer": (a.get("answer") or "")[:160], |
| "used_refs": a.get("used_refs"), "has_grounds": a.get("has_grounds"), |
| "verified": [v["ref"] for v in state.get("verified", [])], |
| "trace": state.get("trace", []), |
| } |
|
|
|
|
| def collect_json_flags(trace): |
| return [(t["node"], t.get("json_ok")) for t in trace if "json_ok" in t] |
|
|
|
|
| def main(): |
| print("인덱스 로드 중...", flush=True) |
| idx = Index() |
| graph = build_graph(idx, checkpoint_path=CKPT) |
| json_flags, latencies = [], [] |
|
|
| def run(q, thread): |
| st = graph.invoke({"question": q}, |
| {"configurable": {"thread_id": thread}}) |
| b = brief(st) |
| json_flags.extend(collect_json_flags(b["trace"])) |
| latencies.append({t["node"]: t.get("latency_ms") for t in b["trace"]}) |
| return b |
|
|
| print("\n########## 케이스 1: 단기리스 연장 (라우팅+검증) ##########") |
| b = run("1년 임차 후 1년 연장하면 단기리스 면제 되나?", "c1") |
| print(f" 라우팅: {b['qtype']} → {b['collections']}") |
| print(f" 검색: {b['retrieved']}") |
| print(f" 답변: {b['answer']}") |
| print(f" used_refs: {b['used_refs']} | verify원문: {b['verified']}") |
| hit_qa = any("40677" in x for x in b["retrieved"]) |
| hit_std = any("제1116호" in x for x in b["retrieved"]) |
| print(f" ▶ 40677 검색={hit_qa}, 제1116호 근거={hit_std}, verify반환={len(b['verified'])>0}") |
|
|
| print("\n########## 케이스 2: 파생상품 정의 (정의조회 라우팅) ##########") |
| b = run("전환사채 풋옵션이 파생상품 정의를 충족하는지", "c2") |
| print(f" 라우팅: {b['qtype']} → {b['collections']}") |
| print(f" 검색: {b['retrieved']}") |
| print(f" 답변: {b['answer']}") |
| hit = any("40670" in x for x in b["retrieved"]) |
| hit_term = any("용어의 정의" in x or "제1109호" in x for x in b["retrieved"]) |
| print(f" ▶ 40670 검색={hit}, 제1109호/용어정의 근거={hit_term}") |
|
|
| print("\n########## 케이스 3: 중소기업 특례 (KGAAP 라우팅) ##########") |
| b = run("중소기업 회계처리 특례를 적용하지 않다가 다시 적용할 수 있나?", "c3") |
| print(f" 라우팅: {b['qtype']} → {b['collections']}") |
| print(f" 검색: {b['retrieved']}") |
| print(f" 답변: {b['answer']}") |
| kgaap = any(c.startswith("qa_kgaap") or c == "kgaap_standards" for c in b["collections"]) |
| print(f" ▶ KGAAP 라우팅={kgaap}, 검색근거 {len(b['retrieved'])}건") |
|
|
| print("\n########## 케이스 4: 근거없는 질문 (환각 방지) ##########") |
| b = run("미국 세법상 감가상각 내용연수는?", "c4") |
| print(f" 라우팅: {b['qtype']} → {b['collections']}") |
| print(f" 검색: {b['retrieved']}") |
| print(f" 답변: {b['answer']}") |
| refused = (not b["has_grounds"]) or ("근거를 찾지 못" in (b["answer"] or "")) |
| print(f" ▶ 환각 방지(근거없음 명시)={refused}") |
|
|
| print("\n########## 케이스 5: 대화기억 (같은 thread 2턴) ##########") |
| b1 = run("1년 임차 후 1년 연장하면 단기리스 되나?", "c5") |
| print(f" 1턴 재작성: {b1['rewritten']}") |
| b2 = run("그럼 리스부채는 어떻게 되나?", "c5") |
| print(f" 2턴 원질문: 그럼 리스부채는 어떻게 되나?") |
| print(f" 2턴 재작성: {b2['rewritten']}") |
| print(f" 2턴 답변: {b2['answer']}") |
| expanded = ("리스" in b2["rewritten"] and len(b2["rewritten"]) > len("그럼 리스부채는 어떻게 되나?")) |
| print(f" ▶ 후속질문 독립화(맥락 반영)={expanded}") |
|
|
| print("\n########## 측정 ##########") |
| n = len(json_flags) |
| broken = [f for f in json_flags if f[1] is False] |
| print(f" JSON 출력: {n}회 중 깨짐 {len(broken)}건 ({100*len(broken)/max(n,1):.0f}%) {broken}") |
| from collections import defaultdict |
| agg = defaultdict(list) |
| for lat in latencies: |
| for node, ms in lat.items(): |
| if ms is not None: |
| agg[node].append(ms) |
| print(" 노드별 평균 지연(ms):", |
| {k: int(sum(v) / len(v)) for k, v in agg.items()}) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|