전편에서 법령 RAG를 만들었다면, 이제 위험은 만드는 쪽이 아니라 바꾸는 쪽에 있습니다. 법령이 개정되어 문서를 다시 넣고, 답이 딱딱하다고 프롬프트를 다듬고, 더 좋은 모델이 나와 교체하는 — 이 평범한 운영 행위 하나하나가 어제까지 잘 나오던 답을 소리 없이 바꿉니다. 화면에 오류는 안 뜨는데 조문 인용이 슬쩍 빠지는 식이라, 검증 없이는 아무도 모릅니다.
운영 3종
| 장치 | 무엇을 지키나 | 언제 작동하나 |
|---|---|---|
| 1 골든셋 | "정답이 무엇인지"의 기준 | 만들 때 한 번 + 문서 개정 때 갱신 |
| 2 회귀 평가 | 변경 전후의 품질 비교 | 프롬프트·문서·모델을 바꿀 때마다 |
| 3 가드레일 | 개별 답변 한 건의 사고 | 매 응답, 실시간 |
평가는 평균을 지키고, 가드레일은 한 건을 막습니다. 역할이 달라서 둘 다 필요합니다.
1. 골든셋 — 자주 묻는 질문 20개부터
골든셋은 "이 질문엔 이 근거로 이렇게 답해야 한다"의 목록입니다. 거창하게 시작할 것 없이, 실제로 자주 들어온 질문 20개면 충분합니다.
- 실제 질문 로그에서 고르되, 개인정보 점검을 거쳐 신고자·주소 등은 제거합니다.
- 질문마다 정답 근거(조문·출처) 와 기대 답의 요점을 사람이 적습니다 — 이 한 줄이 평가의 기준점이 됩니다.
- 파일로 저장해 버전 관리합니다. 법령이 개정되면 골든셋도 함께 개정하고 확인일을 갱신합니다.
# goldenset.yaml — 질문 · 정답 근거 · 기대 요점 (법령 개정 시 함께 갱신) - q: 소화기 점검 주기는 어떻게 되나요? ground_truth: 소방시설법 시행령 제○조 (출처 링크) must_cite: "제○조" # 답에 반드시 인용돼야 하는 조문 checked_on: 2026-07-03 - q: 자체점검 결과 보고 기한은? ground_truth: 소방시설법 제○조 제○항 (출처 링크) must_cite: "제○조" checked_on: 2026-07-03
2. 회귀 평가 — 두 겹으로
겹 1 · Ragas 점수 — 전체 품질의 온도계
전편의 4단 검증을 운영용으로 확장합니다. 핵심은 점수 자체가 아니라 변경 전후의 비교와 하한선입니다.
from ragas import evaluate from ragas.metrics import faithfulness, context_precision # eval_dataset: 골든셋의 question / answer / contexts / ground_truth result = evaluate(dataset=eval_dataset, metrics=[faithfulness, context_precision]) df = result.to_pandas() # 하한선: 이 밑으로 내려가면 배포하지 않는다 (수치는 자기 골든셋으로 정한다) assert df["faithfulness"].mean() >= 0.80, "충실도 하락 — 변경 반영 중단" assert df["context_precision"].mean() >= 0.70, "검색 정밀도 하락 — 청킹·인덱스 확인" # 전후 비교: 지난 결과를 저장해 두고, 점수가 '떨어진 질문'부터 사람이 본다 df.to_csv("eval-2026-07-03.csv", index=False)
겹 2 · promptfoo 단언 — 문장 수준의 안전핀
점수는 평균이라 "이 질문엔 반드시 이 조문이 인용돼야 한다" 같은 개별 규칙을 놓칠 수 있습니다. promptfoo는 질문마다 단언(assert)을 붙여 통과/실패로 끊어 줍니다.
# promptfooconfig.yaml — `promptfoo eval` 로 실행 (npm install -g promptfoo) prompts: - file://rag-prompt.txt providers: - file://rag-answer.py # 내 RAG 파이프라인을 호출하는 스크립트 defaultTest: assert: - type: llm-rubric # 모든 질문 공통: 단정 금지 원칙 value: 처분·과태료·의무 여부를 단정하지 않고 조문 확인을 안내한다 tests: - vars: { query: "소화기 점검 주기는 어떻게 되나요?" } assert: - type: contains # 이 질문엔 이 조문이 반드시 나와야 한다 value: "제○조" - vars: { query: "우리 서 인사 발령 알려줘" } assert: - type: llm-rubric # 범위 밖 질문은 거절해야 정답 value: 문서에 없는 내용이라 답할 수 없다고 안내한다
언제 돌리나 — ① 법령·SOP 문서를 갱신했을 때 ② 프롬프트를 한 글자라도 고쳤을 때 ③ 모델을 교체했을 때 ④ 아무것도 안 바꿔도 주 1회(모델 제공자 쪽 변경 감지). 둘 다 명령 한 줄이라 습관의 문제이지 기술의 문제가 아닙니다.
3. 가드레일 — 마지막 한 건을 막는 문지기
평가를 통과한 시스템도 개별 답변은 빗나갈 수 있습니다. 가드레일은 답이 사용자에게 나가기 직전 위험 신호를 기계적으로 검사해, 걸리면 답 대신 "확인 필요" 안내를 내보냅니다.
import re BLOCK_RULES = [ (re.compile(r"(과태료|처분|벌금).{0,20}(입니다|됩니다|해야 합니다)"), "단정 표현"), (re.compile(r"확인일이 지난 근거"), "낡은 근거"), ] def guardrail(answer: str, cited: list) -> tuple[bool, str]: if not cited: # 근거 없는 답은 내보내지 않는다 return False, "근거 없음" for pattern, reason in BLOCK_RULES: if pattern.search(answer): return False, reason return True, ""
규칙 기반으로 시작해도 충분하고, 커지면 안전한 AI 운영 도구의 Guardrails·NeMo Guardrails 같은 전용 도구로 옮깁니다. 개인정보가 답에 섞여 나오는 것까지 막으려면 개인정보 점검의 마스킹을 출력 쪽에도 겁니다.
운영 루틴 한 장
| 언제 | 무엇을 | 통과 기준 |
|---|---|---|
| 문서·프롬프트·모델 변경 시 | Ragas + promptfoo 전체 회귀 | 하한선 이상 + 단언 전부 통과 |
| 주 1회 정기 | 동일 회귀 (변경 없어도) | 지난주 대비 하락 질문 0건 |
| 매 응답 | 가드레일 | 근거 있음 + 차단 규칙 미해당 |
| 분기 1회 | 골든셋 자체 점검 | 개정 법령 반영·낡은 질문 교체 |
사람 검토 체크리스트
- [ ] 골든셋의 질문·정답 근거를 사람이 작성했고, 개인정보는 제거했습니다. - [ ] faithfulness·context precision 하한선을 우리 골든셋 기준으로 정했습니다. - [ ] 문서·프롬프트·모델 변경 시 회귀 평가를 돌리는 절차가 문서화돼 있습니다. - [ ] 점수가 떨어진 질문은 배포 전에 사람이 원인을 확인했습니다. - [ ] 가드레일에 걸린 답변 사례를 주기적으로 검토해 규칙을 보강합니다. - [ ] 법령 개정 시 골든셋 갱신 담당자가 지정돼 있습니다.