법제처 API와 Groq, 거짓말 안 하게 섞기 — RAG 그라운딩 실전기 (3장)
진짜 데이터를 주는 것과, 그 데이터를 쓰게 만드는 것
LLM한테 그냥 "임금체불 당했는데 어떻게 해야 하나요?"라고 물으면, 그럴싸한 답이 나온다. 문제는 "그럴싸함"이지 "정확함"이 아니라는 것. 법률 서비스에서 조문 번호를 지어내거나, 이미 개정된 옛날 조항을 인용하면 그건 그냥 사고다.
그래서 세운 원칙은 하나였다. LLM이 답을 만들어내게 하지 말고, 법제처에서 실제로 검색한 법조문·판례를 던져주고 그걸 그대로 인용하게 시키자. 말은 쉬운데, 실제로 해보니 세 가지 층위의 문제가 연달아 나왔다.
1단계 — 검색은 빠르게, 병렬로
사용자가 질문을 입력하면 법령 조문과 판례를 각각 검색해서 AI 프롬프트에 끼워 넣는 구조를 짰다. 순서대로 하나씩 검색하면 느리니까, 동시에 실행했다.
articles_task = search_articles(req.message, law_name=law_name, display=5)
prec_task = search_chat_precedents(req.message, effective_category, limit=2)
articles, prec_results = await asyncio.gather(
articles_task, prec_task, return_exceptions=True
)
# 키워드 매칭으로 조문을 못 찾으면, 카테고리별로 미리 정리해둔 핵심 조문으로 대체
if not articles and effective_category:
articles = await get_category_key_articles(effective_category)
여기서 return_exceptions=True를 넣은 게 나중에 꽤 중요했다 — 법령 검색이 실패해도 판례 검색까지 같이 죽지 않게, 서로 독립적으로 실패하게 만들었다.

2단계 — LLM이 자꾸 도망갔다
데이터를 프롬프트에 넣어줘도 Groq가 자꾸 이런 식으로 답했다. "정확한 내용은 국가법령정보센터에서 확인하시기 바랍니다." 데이터를 줬는데 안 쓰고 사용자를 다시 검색으로 떠미는 거다. LLM 입장에서는 "확실하지 않으면 안내만 하자"는 안전한 선택이었겠지만, 서비스 입장에서는 있으나 마나였다.
7월 1일, 프롬프트에 이 지시문을 명시적으로 박아넣었다.
[관련 판례 — 판결요지·판례이유의 핵심 문장을 직접 인용할 것.
절대 링크나 사이트 안내 금지]
"~하지 마라"를 이렇게 직접적으로 써야 했다는 게 좀 웃기지만, 실제로 이게 먹혔다. LLM에게 데이터를 주는 것과, 그 데이터를 실제로 쓰게 만드는 것은 완전히 다른 문제였다.
3단계 — 요약만 주면 인용을 못 한다
판례 인용도 처음엔 요약(판시사항 500자)만 줬는데, "직접 인용"을 시켜도 어색하게 바꿔 말하기만 했다. 원문이 없으니 당연했다. 그래서 판례이유 전문 일부(1,200자)까지 프롬프트에 포함시켰다.
prec_lines.append(f"판시사항: {p['issues'][:500]}")
prec_lines.append(f"판결요지: {p['holding'][:800]}")
prec_lines.append(f"판례이유(전문일부): {p['full_content'][:1000]}")
동의어 사전도 이 시기에 19개에서 45개로 늘렸다. 사용자가 "임금 안 줘요"라고 물으면 이걸 법제처가 검색할 수 있는 "임금체불", "체불" 같은 키워드로 바꿔줘야 하는데, 노무·주거·가사·소비자·교통·지식재산·투자 도메인별로 계속 빠진 게 나왔다. 하나씩 채워 넣는 식으로 늘어났다.
그런데 검색 자체가 가끔 0건이었다
이 파이프라인을 다 짜놓고도, 정작 검색 결과가 0건 나오는 경우가 있었다. "임금체불"로 판례를 검색하면 0건. 그런데 "임금 체불"(띄어쓰기만 다름)로 검색하면 7건이 나왔다.
원인은 법제처 판례 검색 API의 숨겨진 스펙이었다. 띄어쓰기가 AND 연산자로 동작한다. 붙여쓴 복합어는 검색 인덱스에 정확히 일치하는 게 없으면 그냥 0건을 반환하는데, 이게 공식 문서 어디에도 안 적혀 있었다. 사용자가 "지금도 임금체불로 검색하면 0건 나온다"고 재차 신고하고 나서야, 자동으로 복합어를 띄어쓰기로 분리하는 정규화 로직을 만들었다. 35개 법률 용어를 매핑해뒀다.
_PREC_SPLIT_MAP = {
"임금체불": "임금 체불",
"부당해고": "부당 해고",
"직장내괴롭힘": "직장 괴롭힘",
... # 총 35개
}

이 장에서 남는 것
RAG(검색해서 근거를 주는 방식)를 구현하는 건 "검색 붙이고 프롬프트에 넣기"로 끝나는 일이 아니었다. 검색이 실제로 맞는 결과를 주는지, LLM이 그 결과를 실제로 쓰는지, 사용자가 실제로 입력하는 방식이 검색 스펙과 맞는지 — 세 군데 전부 따로따로 어긋날 수 있었고, 실제로 셋 다 한 번씩 어긋났다.
이 세 가지 검증 지점을 개념적으로 정리한 내용은 PM실무사전 3편(RAG, 환각, 그라운딩)에서 별도로 다뤘다.
다음 장 예고
공짜로 메꿔보려 두 번 걸었다 — 사회적기업 인증 탈락과 해커톤 도전, 그리고 철회.
이 글은 개인 개발 프로젝트의 기록이며, 실제 코드는 일부 축약·각색되었습니다. 서비스 이용이 법률 자문을 대체하지는 않습니다.