지방계약 자료를 LLM Wiki와 RAG 지식베이스로 만든 기록
한 줄 요약
네이버 블로그 자료, HWPX 예규, Markdown 문서를 AI 코딩 에이전트와 함께 정리해서 개인 학술용 LLM Wiki와 MongoDB 기반 RAG 검색 파이프라인으로 바꿨습니다.
이런 분들께 도움돼요
법령, 예규, 판례, 매뉴얼처럼 긴 문서를 계속 찾아보는 분
Obsidian이나 Markdown으로 개인 지식베이스를 운영하는 분
RAG/vector DB를 해보고 싶은데 “문서를 어떻게 넣어야 하지?”에서 막히는 분
AI에게 단순 답변이 아니라 출처와 근거가 있는 질의를 시키고 싶은 분
시작점: 자료는 많은데, 매번 다시 찾고 있었다
저는 지방공기업에서 행정 업무를 하면서, 항상 지방자치단체를 당사자로 하는 계약에 관한 법률(이하 '지방계약법'이라 합니다)의 적용 문제로 고민했습니다.
지방계약법 관련 자료는 많지도 않은데다 법령, 행정규칙, 그리고 사례들이 흩어져 있었습니다. 법령 조문도 많지만, 문제는 '지방자치단체 입찰시 낙찰자 결정기준'과 '지방자치단체 입찰 및 계약집행기준'이 매우 많은 분량의 pdf/hwpx 문서로 되어 있어 찾기가 쉽지 않았습니다. 다만, 지방계약법에 대해 매우 잘 정리된 blog 글과 조달청, 국가계약법에 대한 유권해석을 수집해 놓은 사이트가 있어서 이를 참고할 수 있었습니다. 하지만 문제는 이 자료들이 각기 다른 곳에 흩어져 있다는 것이었습니다.
공개 블로그에는 해설 글이 많았고, 한글 문서에는 행정 기준 전문이 있었고, Obsidian에는 정리 중인 LLM Wiki가 있었습니다. 문제는 자료가 많을수록 “정확한 근거가 어디였지?”를 다시 찾는 시간이 늘어난다는 점이었습니다.
처음 목표는 단순했습니다.
사례사이트의 유권해석과 네이버 블로그 글을 Markdown으로 백업한다.
HWPX로 된 지방계약 기준 문서를 Markdown으로 바꾼다.
그 Markdown을 chunk로 나눠 RAG/vector DB에 넣는다.
LLM Wiki에서 질의하면 출처와 heading까지 같이 나오게 만든다.
여기서 중요한 전제는, 원문을 공개 재배포하려는 목적이 아니라 개인 학술 백업과 검색용 지식베이스를 만드는 것이었습니다. 그래서 초반에는 권리 확인 전까지 본문 전체를 복제하지 않고 metadata만 저장했고, 사용 권한을 확인한 뒤에야 full text 백업으로 확장했습니다.
만든 것
최종적으로 만든 것은 “지방계약 자료를 위한 로컬 AI 지식베이스 파이프라인”입니다.
구성은 이렇게 잡았습니다.
자료 수집
-> Markdown 변환
-> RAG chunk 생성
-> LLM Wiki source 등록
-> lint/query 자동화
-> MongoDB Local Atlas + Vector Search ingest
-> Ollama 기반 답변 생성실제 산출물은 다음과 같습니다.
네이버 블로그 Markdown 백업: 1,028개
HWPX -> Markdown 변환 문서: 2개
지방계약 RAG chunk: 1,304개
LLM Wiki Markdown 문서: 1,958개 기준 dry-run
MongoDB RAG 전체 chunk 예정: 약 14,610개"자료, schema, 자동화 스크립트, report를 분리한 LLM Wiki 구조"
사용한 도구와 역할
Codex: 파일 조사, 스크립트 작성, 오류 수정, 테스트, devlog 정리
insane-search: 네이버처럼 일반 fetch가 까다로운 사이트 접근 가능성 확인
hwp2md: HWPX 행정 문서를 Markdown으로 변환
Python: 블로그 백업, chunking, lint, query, MongoDB ingest 스크립트 작성
LLM Wiki: source, schema, workflow, glossary, question 문서 관리
MongoDB Local Atlas: RAG chunk 저장과 Vector Search
voyageai/voyage-4-nano: 로컬 embedding 모델Ollama
gemma4:e2b: 검색 결과 기반 답변 생성
AI에게 한 번에 “RAG 만들어줘”라고 맡긴 것이 아니라, 각 단계마다 역할을 나눴습니다. 자료 수집은 자료 수집대로, 변환은 변환대로, schema는 schema대로, 검증은 검증대로 분리했습니다. 이게 나중에 오류를 찾는 데 꽤 중요했습니다.
진행 과정
1. 네이버 블로그 글을 Markdown으로 백업
처음에는 네이버 블로그 전체 글 목록을 수집했습니다.
블로그의 RSS와 글 목록 API를 확인했고, 전체 글 수가 1,028개라는 것을 확인했습니다. 권리 확인 전에는 본문 전체를 저장하지 않고 제목, URL, 날짜, 글번호 같은 metadata만 저장했습니다.
그 다음 사용자가 개인 학술 백업 권한을 확인한 뒤, 각 글의 모바일 HTML 본문을 읽어 Markdown 파일로 갱신했습니다.
중간에 구형 네이버 글에서 본문이 너무 짧게 추출되는 문제가 있었습니다. 최신 글은 se-main-container를 쓰고, 구형 글은 __se_component_area를 쓰고 있었기 때문입니다. 본문 컨테이너 인식 규칙을 추가한 뒤 전체 1,028개 글이 full text 상태로 저장됐습니다.
검증 결과:
posts=1028
full_text=1028
missing=0"1,028개 블로그 글을 개인 백업용 Markdown으로 정리"
2. HWPX 행정 문서를 Markdown으로 변환
다음은 HWPX 문서였습니다.
외부에 있던 hwp2md 프로그램을 먼저 읽었습니다. 이 도구는 HWP/HWPX를 Markdown 초안으로 바꾸는 로컬 변환기였습니다. 테스트도 실행했습니다.
hwp2md 0.1.0
Ran 23 tests in 0.564s
OK그리고 현재 작업 폴더에서 바로 실행할 수 있도록 convert_hwp_here.bat를 만들었습니다. 더블클릭하면 현재 폴더의 .hwp, .hwpx 파일을 converted_hwp_md로 변환하는 방식입니다.
변환한 문서는 두 개였습니다.
지방자치단체 입찰시 낙찰자 결정기준
지방자치단체 입찰 및 계약집행기준변환 결과에는 .md, .report.json, .assets가 함께 생겼습니다. 리포트에는 layout 손실이나 이미지 위치 추정 같은 경고가 남았지만, 본문 Markdown 변환은 성공했습니다.
3. 법령 문서에 맞는 chunking 스크립트 작성
그 다음 단계가 핵심이었습니다. Markdown을 그냥 일정 글자 수로 자르면, 법령 문서에서는 검색 품질이 떨어집니다.
그래서 chunking 스크립트는 다음 metadata를 보존하도록 만들었습니다.
title
heading_path
heading
source_file
source_document
legal_refs
text특히 legal_refs에는 이런 패턴을 잡도록 했습니다.
제n장
제n절
제n조
별표
별지
부칙
시행령 제n조
시행규칙 제n조최종 chunk 통계는 이렇습니다.
input_md_files=2
chunks=1304
unique_ids=1304
min_chars=125
max_chars=2200
mean_chars=667.5검색 샘플도 확인했습니다.
query: 새로 공고
hit heading: 3. 새로 공고와 정정공고
legal_refs: 시행령제33조"법령 검색을 위해 heading_path와 legal_refs를 metadata로 보존"
4. LLM Wiki에 source와 schema를 넣었다
처음에는 chunk만 만들면 끝이라고 생각하기 쉽습니다. 그런데 실제로 운영하려면 “이 chunk가 어디서 왔는지”, “어떤 source에 속하는지”, “query에서 어떤 결과를 우선해야 하는지”를 계속 확인해야 합니다.
그래서 LLM Wiki 안에 source와 schema를 정리했습니다.
등록한 주요 source:
SRC-001: 지방자치단체 입찰시 낙찰자 결정기준
SRC-002: 지방자치단체 입 찰 및 계약집행기준
SRC-003: 지방계약 HWPX 변환 문서의 RAG chunk 산출물그리고 RAG chunk schema, ingest checklist, lint checklist, query workflow를 추가했습니다.
schema/rag-chunk-schema.md
schema/ingest-checklist.md
schema/lint-checklist.md
wiki/workflows/query-and-update.md이 단계에서 깨달은 점은, RAG는 “embedding을 넣는 일”보다 “나중에 믿고 운영할 수 있게 source와 검증 규칙을 남기는 일”이 더 중요하다는 것이었습니다.
5. lint와 query를 자동화했다
LLM Wiki가 커지면 사람이 매번 확인하기 어렵습니다. 그래서 lint와 query 도구를 만들었습니다.
lint는 source index, source summary, inbox 잔여물, 내부 링크, RAG JSONL/manifest 정합성을 검사합니다.
최종 lint 결과:
LLM Wiki lint: PASS
errors=0 warnings=0 links=80 sources=6 rag_sources=2query 도구는 Markdown 문서와 RAG JSONL을 함께 검색합니다.
검증한 질의는 이런 것들이었습니다.
물가변동 계약금액 조정
수의계약 배제사유
공동수급체 구성 제한
새로 공고 / 정정공고
부정당업자 제재사유예를 들어 “부정당업자 제재사유”를 물어봤을 때는 LLM Wiki의 SRC-002 chunk와 공식 법령 근거를 함께 확인해 답변했습니다. 답변에는 지방계약법 제31조, 지방계약법 시행령 제92조, 그리고 계약집행기준의 관련 chunk가 같이 쓰였습니다.
6. MongoDB RAG 파이프라인으로 확장
마지막으로 handoff 문서를 읽고, 기존 실습용 RAG 구조를 LLM Wiki vault에 맞게 적용했습니다.
목표는 이랬습니다.
Markdown 문서 읽기
-> Obsidian metadata 추출
-> heading-aware chunking
-> voyageai/voyage-4-nano embedding
-> MongoDB Local Atlas 저장
-> Vector Search index 생성
-> query와 Ollama answer 생성추가한 파일:
llm_wiki_rag.bat
scripts/index_llm_wiki.py
scripts/README.md
scripts/requirements-llm-wiki-rag.txt
scripts/llm_wiki_rag.env.exampleRAG 스크립트는 네 가지 명령을 지원하게 했습니다.
check
dry-run
ingest
query환경 확인 결과:
MongoDB Local Atlas: running
Ollama: gemma4:e2b, gemma4:e4b
Embedding model: voyageai/voyage-4-nano
Embedding dimensions: 2048dry-run 결과는 꽤 컸습니다.
Markdown docs=1958
chunks=약 14610작은 smoke ingest에서는 MongoDB 저장, Vector Search index, query, Ollama answer까지 확인했습니다.
막힌 점: YAML 날짜 때문에 MongoDB 저장이 실패했다
전체 ingest를 실행했더니 첫 저장 전에 멈췄습니다.
오류는 이랬습니다.
bson.errors.InvalidDocument:
cannot encode object: datetime.date(2026, 6, 5)원인은 단순했지만 중요했습니다. Markdown frontmatter의 updated: 2026-06-05 값이 PyYAML에서 datetime.date로 파싱되었고, MongoDB BSON encoder는 이 타입을 직접 저장하지 못했습니다.
그래서 mongo_safe() 함수를 추가했습니다.
처리한 타입:
datetime.date -> ISO date string
Path -> string
dict/list/tuple -> recursive conversion수정 후 검증:
Python py_compile: PASS
BSON smoke: docs=20 chunks=1412 bson_ok=1412
small ingest: docs=9 chunks=47 저장 성공