법제처를 에이전트에 물리다
이 세팅기의 목적은 변호사 업무에 도움되는 에이전트를 만드는 데 있다. 그런데 로컬 모델은 프론티어 모델보다 환각이 더 자주 나타난다. 조문 인용 등에서는 환각이 일어나면 안 되기에, 법제처에 직접 물어볼 통로가 필요했다.
마침 GitHub에서 korean-law-mcp라는 오픈소스 프로젝트를 발견했다. 법제처 국가법령정보 Open API 42개를 MCP 도구 17개로 묶어놓은 것이다. 17개 도구의 구성은 대략 이렇다.
- 기본 조회 도구: 법령 검색(
search_law), 조문 전문 조회(get_law_text), 별표·서식 조회(get_annexes) - 결정례 통합 검색: 판례·헌법재판소 결정·조세심판·행정심판·법령해석례 등 여러 결정례 도메인을 도구 두 개(
search_decisions,get_decision_text)로 검색·조회 - 체인 도구 8종: 종합 리서치, 법체계 분석, 처분 근거 확인, 쟁송 대비, 개정 추적, 조례 비교, 절차·서식 안내, 계약서 리스크 분석처럼 여러 조회를 묶어 한 번에 돌리는 도구
- 검증·분석 도구: 텍스트 속 조문 인용을 법제처 DB와 교차검증해 가짜 조문을 잡아내는
verify_citations, 특정 조문을 인용한 판례·해석례를 역추적하는impact_map - 메타 도구: 그 밖의 전문 도구를 찾아 실행하는
discover_tools,execute_tool
법령 업무에 필요한 조회는 대부분 커버되는 구성이라, 바로 붙이기로 했다.
설치 방식 선택
설치 방법이 두 갈래였다. 개발자가 운영하는 공개 서버에 인증키만 붙여 쓰는 방식과, 패키지를 내 머신(ATOM)에서 직접 실행하는 방식이다.
전자는 내 질문이 내가 아닌 다른 사람의 서버를 거쳐 간다는 점이 마음에 들지 않았다. 그래서 후자 방법을 선택하기로 했다. npx로 실행해서, 실행할 때마다 최신판을 확인해 가져오게 했다.
사전 준비는 법제처 Open API 인증키(OC) 하나다. 법제처 Open API 신청 페이지에서 무료로 발급받을 수 있다.
등록
17화에서 게시판 MCP를 붙일 때와 같은 구조다.
openclaw mcp set koreanlaw '{"command":"npx","args":["-y","korean-law-mcp@latest"],"env":{"LAW_OC":"본인인증키"}}'
인증키를 명령 안의 env 블록에 직접 적은 데에는 이유가 있다. 보통 터미널에서 export LAW_OC=...처럼 환경변수를 설정하는 방법을 떠올리기 쉬운데, 그 방식은 그 터미널 창 안에서만 유효하다. 오픈클로는 터미널과 무관하게 백그라운드에서 늘 돌아가는 프로그램이라, 내가 터미널에 설정해둔 값을 보지 못한다. 그래서 설정 파일 자체에 키를 적어둬야, 오픈클로가 이 서버를 띄울 때마다 키를 확실히 전달할 수 있다.
등록 후 MCP 목록을 확인했다.
openclaw mcp list
MCP servers (/home/rocklassic/.openclaw/openclaw.json):
- koreanlaw
- legalt3ch
기존의 legalt3ch 옆에 koreanlaw가 새로 떴다. 연결 과정은 이것으로 끝이다.
민법 제1조를 물어봤다
openclaw agent --agent main --message "민법 제1조 조문 내용을 알려줘"

민사에 관하여 법률에 규정이 없으면 관습법에 의하고 관습법이 없으면 조리에 의한다.
적용 순서는: 1. 법률 → 2. 관습법 → 3. 조리 …
정확한 답이다. 그런데 이 답은 도구가 작동한다는 증거가 못 된다. 민법 제1조는 모델이 학습 때 수없이 봤을 조문이라, 법제처에 다녀오지 않고 기억으로 답해도 똑같은 답이 나오기 때문이다.
나에게는 도구 호출 여부를 곧이곧대로 믿지 않게 된 경험이 하나 있다. 구글 캘린더 관련 스킬을 만들었을 때 모델이 스킬을 제대로 호출하지 못해 한참 고생했다(이 실패기와 교정기는 기회가 되면 따로 쓸 예정이다). 그래서 “답이 맞다”와 “도구를 썼다”를 등치하지 않기로 했다. 검증이 필요했다.
존재하지 않는 법률을 검색시키다
14화에서 마커 파일로 환각과 실제 실행을 갈랐던 발상을 빌려, 이번에는 답이 존재할 수 없는 질문을 설계했다. 프롬프트에 도구 사용도 명시했다.
openclaw agent --agent main --message "koreanlaw의 search_law 도구로 '우주항행자 보호에 관한 법률'을 검색해줘. 반드시 도구를 호출할 것."
‘우주항행자 보호에 관한 법률’은 내가 지어낸, 세상에 없는 법이다. 도구를 진짜 호출했다면 “없음”이 나와야 하고, 기억으로 답한다면 그럴듯한 가짜를 지어낼 것이다. “없다”는 외워서 답할 수 없기 때문이다.
검증 장면을 보려고 터미널을 하나 더 열어 로그도 걸어뒀다. 도구를 부르면 로그에 찍힐 것이라 생각했다.
tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log
tail -f는 파일 끝에 새로 추가되는 내용을 실시간으로 화면에 흘려주는 명령이다. $(date +%Y-%m-%d)는 오늘 날짜로 자동 치환되므로, 결국 “오늘 자 오픈클로 로그를 생중계로 보여달라”는 뜻이다.
답이 왔다.

법제처 Open API로 “우주항행자 보호에 관한 법률”을 두 번 검색해 보았으나, 두 경우 모두 결과가 존재하지 않았습니다.
- “우주항행자 보호에 관한 법률” → 없음
- “우주항행자” → 없음
결론: 법제처 법령 데이터베이스에 현재 “우주항행자 보호에 관한 법률”이라는 법령은 등록되어 있지 않습니다.
“없음.” 첫 검색이 빈손이라 키워드를 줄여 한 번 더 시도한 기록까지 있다. MCP 연결은 확실히 됐고, 검색이 성공했으니 인증키가 유효하다는 것도 같이 증명됐다.
그런데 로그가 비어 있다
로그 창을 확인했다. 그런데 도구 호출 기록이 없다. 준비 단계 기록 다음에 곧바로 답변 본문이 찍혀 있고, 그 사이에 search_law도, 호출을 나타내는 줄도 없다. 방금 도구가 호출된 것은 답 내용으로 확정인데(“없음”은 도구 없이 못 나오니까), 로그에는 흔적이 없는 것이다.
도구는 분명히 돌았는데 기록이 없다니, 대체 뭔가 싶어 한참 고민했다. 알아보니 이 로그는 시스템이 사람 보라고 흘리는 요약본이고, 도구 호출 여부는 trajectory라는 별도 파일을 확인해야 한다는 것을 알게 됐다.
여기서 살짝 멘붕이 왔다. 나는 지금까지 — 세팅기에 다 담지 못한 여러 시행착오 과정에서 — 로그 기록으로 도구 호출 여부를 판단해왔기 때문이다. “로그에 호출이 보이면 도구를 쓴 것, 안 보이면 안 쓴 것”이라는 전제였는데, 그 전제 자체가 잘못이었다는 것을 이제서야 깨달은 것이다. 이 전제로 어떤 답을 “도구를 안 썼으니 환각”이라고 판정했다면, 멀쩡한 답을 환각으로 몰았을 것이다. 검증 수단이라 믿었던 로그가 검증 수단이 아니었다.
trajectory: 에이전트의 블랙박스
trajectory는 오픈클로가 세션마다 남기는 원본 기록이다. 에이전트가 한 작업을 처리하며 거친 모든 발자취 — 사용자가 뭘 물었고, 모델이 어떤 도구를 어떤 인자로 불렀고, 도구가 뭘 돌려줬고, 최종적으로 뭐라 답했는지 — 가 전부 들어 있다. 도구 호출 여부는 여기를 봐야 한다.
find ~/.openclaw -name "*.trajectory.jsonl" -mmin -120
find는 파일을 찾는 명령이다. ~/.openclaw 폴더 아래에서, 이름이 *.trajectory.jsonl 패턴에 맞고(-name), 최근 120분 안에 수정된(-mmin -120) 파일을 찾으라는 뜻이다. 방금 작업한 세션 파일만 골라내기 위한 조건이다.
민법 제1조와 우주항행자 세션 파일들이 나왔다. 우주항행자는 더 볼 것이 없다 — 도구를 쓰라고 시켰고, 답 내용으로 호출이 확정됐기 때문이다. 궁금한 것은 민법 제1조였다. 명시적으로 도구를 쓰라고 지시하지 않았기 때문이다. 지시했을 때 도구를 쓰는 것은 확인했지만, 지시 없이도 17개의 도구 중 알맞은 것을 스스로 골라 쓸 수 있을까? 에이전트로서의 자질은 이쪽이다.
trajectory는 줄 하나가 기록 한 건이고, 작업 순서대로 쌓인다. 각 줄의 종류를 먼저 훑어보니 세션 시작 → 메타데이터 → 컨텍스트 구성 → 프롬프트 제출 → 모델 출력 → 산출물 → 세션 종료 순이었고, 도구 호출이 담기는 모델 출력은 5번째 줄이었다. 그래서 그 줄만 뽑아 도구 흔적을 걸렀다.
sed -n '5p' ~/.openclaw/agents/main/sessions/<세션ID>.trajectory.jsonl | jq '.' | grep -iE "toolCall|search_law|get_law"
세 명령을 파이프(|)로 이어 붙인 것인데, 역할이 각각 다르다.
sed -n '5p'— 파일에서 5번째 줄만 뽑는다(p는 print). 모델 출력이 담긴 줄이다.jq '.'— trajectory의 한 줄은 JSON이 통째로 압축돼 있어 그대로는 읽기 힘들다. jq가 이것을 들여쓰기된 형태로 펼쳐준다.grep -iE "toolCall|search_law|get_law"— 펼쳐진 내용 중 도구 관련 단어가 들어간 줄만 거른다.-i는 대소문자 무시,-E는 여러 단어를 “또는” 조건으로 묶는 옵션이다.
결과는 이랬다.
"type": "toolCall",
"name": "koreanlaw__search_law",
"arguments": { "query": "민법", "display": 5 }
...
"type": "toolCall",
"name": "koreanlaw__get_law_text",
"arguments": { "mst": "284415", "jo": "제1조" }
...
"text": "법령명: 민법\n...\n제1조 법원\n민사에 관하여 법률에 규정이 없으면 관습법에 의하고..."
한 줄씩 읽으면 이렇다.
"type": "toolCall"— 이 기록이 모델의 도구 호출이라는 표시다. 이 단어가 있다는 것 자체가 “도구를 불렀다”는 직접 증거다."name": "koreanlaw__search_law"— 호출한 도구의 이름. koreanlaw 서버의 search_law(법령 검색)를 불렀다."arguments": { "query": "민법", "display": 5 }— 모델이 도구에 넣은 인자. “민법”이라는 검색어로 결과 5건을 요청했다.- 두 번째
toolCall— 이어서get_law_text(조문 본문 조회)를 불렀다. 인자의mst: 284415는 첫 번째 검색이 돌려준 민법의 법령 마스터 번호이고,jo: 제1조는 그중 제1조만 달라는 뜻이다. "text": "법령명: 민법\n...제1조 법원..."— 도구가 돌려준 결과, 즉 법제처에서 온 실제 조문 본문이다.
지시하지 않았는데도 모델이 search_law로 민법을 검색해 마스터 번호를 받고, 그 번호로 get_law_text를 연달아 호출해 제1조 본문을 가져온 것이다. 두 단계 연쇄 호출을 스스로 설계했다. 그 정확했던 답은 기억이 아니라 법제처에서 실제로 가져온 것이었다. 호출 ID에 ollama_call_...이 붙어 있으니, 이 연쇄를 만든 것이 내 로컬 모델이라는 것도 확인된다.
에이전트는 처음부터 일을 제대로 하고 있었다. 못 미더웠던 것은 내가 보던 로그였다.
가짜 조문을 잡아낼 수 있는가
가짜 조문을 찾기 위한 verify_citations 도구가 잘 호출되는지 궁금했다. 진짜 조문과 가짜 조문을 섞어 던졌다.
openclaw agent --agent main --message "koreanlaw의 verify_citations 도구로 다음 문장의 인용을 검증해줘: '민법 제750조는 불법행위 손해배상을 규정하고, 형법 제9999조는 가중처벌을 정한다.'"
민법 제750조는 실존하고, 형법 제9999조는 없다(형법은 372조까지다). 결과는:

검증 결과입니다:
- 민법 제750조 ✅ 실존 — 불법행위 손해배상 책임에 관한 규정으로 정확함
- 형법 제9999조 ❌ 존재하지 않음 — 형법의 조문 범위는 제1조~제372조이며, 제9999조라는 조문은 존재하지 않음
⚠️환각 감지됨: “형법 제9999조는 가중처벌을 정한다”는 부분이 완전히 거짓 인용입니다. 원문에서 이 부분을 수정해야 합니다.
진짜는 통과시키고, 가짜는 존재 범위(제372조)까지 짚어서 차단했다. 372라는 숫자는 모델이 지어낼 수 있는 것이 아니라 법제처 DB가 돌려준 값이다. 에이전트 답변에 조문이 섞여 있으면 이 도구로 한 번 거른다 — 조문 환각에 대한 안전망이 생긴 것이다. 그것도 전부 로컬에서!
법을 물었는데 시행규칙이 왔다
별표 추출도 시험했다.
openclaw agent --agent main --message "산업안전보건법 별표 1의 내용을 조회해서 알려줘."
답이 왔다. 모델의 최종 출력은 이런 모습이었다.

산업안전보건법 별표 1
제목: 건설업체 산업재해발생률 및 산업재해 발생 보고의무 위반건수의 산정 기준과 방법 (제4조 관련)
2. 산업재해발생률 계산식
사고사망만인율(‱) = (사고사망자 수 ÷ 상시근로자 수) × 10,0004. 상시근로자 수 계산식 … (이하 항목별 정리)
그런데 뭔가 이상했다. 질문은 ‘산업안전보건법 별표 1’인데, 답은 시행규칙 별표 1을 알려줬다. 도구 호출은 제대로 됐는데 이상한 답이 나온 것이다.
법을 물었는데 왜 시행규칙이 왔을까. 모델이 멋대로 바꾼 것인지, 도구가 바꾼 것인지 가리러 trajectory를 다시 깠다. 방법은 민법 때와 같다 — 별표 질의 세션의 trajectory 파일에서 모델 출력 줄을 sed로 뽑아 jq로 펼쳤다. trajectory에는 호출과 그 결과가 일어난 순서대로 남아 있으므로, 질문부터 최종 답변까지의 과정 전체를 따라갈 수 있다.
1단계 — 모델의 법령 검색. 모델은 먼저 search_law로 “산업안전보건법”을 검색했다.
"name": "koreanlaw__search_law",
"arguments": { "query": "산업안전보건법", "display": 5 }
이 호출 바로 뒤에는 도구가 돌려준 결과가 text 필드로 붙어 있다.
"text": "검색 결과 (총 3건):
1. 산업안전보건법 [현행] - MST: 283449 - 구분: 법률
2. 산업안전보건법 시행령 [현행] - MST: 284771 - 구분: 대통령령
3. 산업안전보건법 시행규칙 [현행] - MST: 286657 - 구분: 고용노동부령"
검색 결과로 법률·시행령·시행규칙 세 건이 돌아왔다. 이 시점까지 모델은 내 질문 그대로 움직이고 있다.
2단계 — 모델의 별표 요청. 이어서 get_annexes(별표 조회)를 불렀다.
"name": "koreanlaw__get_annexes",
"arguments": { "lawName": "산업안전보건법 별표1", "annexNo": "1" }
인자를 보면 모델은 “산업안전보건법”, 즉 법률의 별표를 요청했다. “시행규칙”이라고 바꿔 넣지 않았다. 모델은 여기까지 정확했다.
3단계 — 도구의 응답. 이 get_annexes 호출 뒤에도 마찬가지로 도구가 돌려준 결과가 text 필드로 붙어 있다. 그 첫 줄이 이랬다.
■ 산업안전보건법 시행규칙 [별표 1] <개정 2021. 11. 19.>
건설업체 산업재해발생률 및 산업재해 발생 보고의무
위반건수의 산정 기준과 방법(제4조 관련)
법률을 요청받았는데 시행규칙 별표를 돌려줬다. 법률 본문에는 그 별표가 없으니 하위법령에서 찾아준 자동 라우팅이다. 대신 도구는 출처를 숨기지 않았다 — 첫 줄에 “산업안전보건법 시행규칙 [별표 1] <개정 2021. 11. 19.>“라고 어느 법령의 별표인지, 언제 개정본인지까지 명시해서 모델에게 넘겼다. 즉 모델은 답변을 만들기 전에 이 출처 표시를 분명히 받아 본 상태였다.
4단계 — 모델의 최종 답변. 그런데 모델이 이것을 받아 사용자용 답변으로 정리하면서, 제목을 ”## 산업안전보건법 별표 1”로 달았다. 도구가 분명히 표시해준 “시행규칙”이라는 세 글자를 빼먹은 것이다. 개정일 표시도 함께 사라졌다. 내 질문이 “산업안전보건법 별표 1”이었으니, 답의 라벨도 질문의 표현에 맞춰버린 것으로 보인다.
정리하면 이렇다.
| 단계 | 행위 | 평가 |
|---|---|---|
| 모델의 도구 호출 | “산업안전보건법” 그대로 요청 | 정확함 (명령 충실 이행) |
| 도구의 라우팅 | 법률에 없는 별표 → 시행규칙에서 탐색 | 과잉 친절 (자동 탐색) |
| 도구의 출처 표시 | “시행규칙 [별표 1] <개정 2021.11.19.>” 명시 | 정확함 (출처 명시) |
| 모델의 최종 요약 | “시행규칙”·개정일 누락, 법률 별표처럼 표기 | 출처 왜곡 (정보 누락 및 오표기) |
이것은 환각이 아니다. 내용은 전부 법제처에서 온 진짜다. 하지만 “진짜 데이터 + 어긋난 출처 라벨”이라는, 어떤 면에서는 환각보다 까다로운 오류다. 가짜 조문은 verify_citations가 잡아주지만, 법률 별표냐 시행규칙 별표냐 같은 위계 표기는 도구가 못 잡아준다. 이런 오류는 어떻게 잡을 것인지 고민이 필요하다.
오늘의 정리
- 법제처 MCP 연결: 아래 명령 한 줄로 끝난다. 인증키만 본인 것으로 바꾸면 된다.
openclaw mcp set koreanlaw '{"command":"npx","args":["-y","korean-law-mcp@latest"],"env":{"LAW_OC":"본인인증키"}}'
- 환각 발생 여부, 도구 호출 여부의 판단 기준은 로그 기록이 아니라 trajectory였다: 파일 로그는 도구 호출을 기록하지 않는 요약 채널이다. 호출·인자·결과가 전부 남는 원본은 세션별 trajectory 파일이고, 판정은 여기를 근거로 해야 한다.
- 법을 물었는데 시행규칙을 답한 문제: 내용은 법제처에서 온 진짜이므로 환각은 아니다. 도구의 자동 라우팅과 모델의 출처 라벨 누락이 겹친 별개 유형의 문제라, 해결 방안을 고민해볼 필요가 있다.
이로써 법제처 MCP 연결은 완료됐다. 에이전트가 법령을 기억이 아니라 법제처에서 직접 가져오게 됐고, 가짜 조문을 거를 안전망도 생겼다. 다만 출처 라벨 누락처럼 도구로는 못 잡는 문제가 새로 드러났으니, 숙제도 함께 남은 셈이다.