UrbanLaw가 한 건의 검토를 만들기까지 어떤 데이터를 모으고, 어떻게 정리하고, 어떤 검색·검증 절차를 거치는지 전부 적어두었습니다. 신뢰는 결과만으로 만들 수 없다고 봅니다. 과정을 봐주세요.
"AI가 제대로 알고 있나?"라는 질문의 시작은 결국 데이터입니다. 우리는 일반적인 LLM의 사전학습 지식을 신뢰하지 않고, 실제 법령 원문을 자체 DB에 적재한 뒤 그 안에서만 인용하도록 설계했습니다.
법제처의 공식 OpenAPI(law.go.kr, OC=lawsearch6165)를 통해 원문을 직접 크롤링합니다. 가공·요약된 2차 자료가 아닌 법제처 정본입니다.
data/rechunk_crawl_cache/ 저장urbanlaw_precedents 컬렉션 (2024.3 ~ 현재)일반 RAG처럼 "원문을 그냥 임베딩"하지 않습니다. 검색 정확도를 위해 임베딩 텍스트를 3중으로 구성합니다.
┌─ 임베딩 입력 텍스트 ─────────────────────────┐
│ [법령명 | 장 | 절 | 조문제목] ← prefix
│ LLM이 생성한 context_summary ← 상위법 위임 관계만
│ 조문 원문 그대로 ← original_text
└──────────────────────────────────────────────┘
모델: OpenAI text-embedding-3-large (3,072차원)
최대 길이: 6,000자 (EMBED_MAX_CHARS)
각 조문에 대해 Gemini Flash로 1~2문장 요약을 생성하지만, 환각을 막기 위해 다음 4개의 절대 규칙을 프롬프트에 박아넣었습니다.
검증 결과 (2026-03-17): 무작위 10건 교차검증 → 할루시네이션 0건 확인.
parent_id·parent_text를 메타데이터로 들고 있음검토 대상지의 토지정보·규제·건축물대장은 우리가 미리 저장해둔 데이터가 아니라, 검토할 때마다 정부 OpenAPI를 호출해 그 시점의 최신 정보를 받아옵니다.
api.vworld.kr/req/search) — 도로명·지번 모두 지원
getLandCharacteristics · getLandUseAttr — 연도별 공시지가 시계열 포함
BLD_API_KEY) — 표제부·총괄표제부·층별 정보 (PNU 11번째 자리로 대지/산 자동 분기)
98.33.2.225:6080/.../UPIS/20200526_WFS · 도면번호·결정고시·특별계획구역까지 추출
lt_c_upisuq161 fallback (gpkg 미수록 지역용)
data/usage.db에 사용 로그로 기록됩니다(개인정보 제외).단일 벡터검색은 토지/규제처럼 키워드가 정확히 떨어지는 영역에서 자주 핵심 조문을 놓칩니다. 그래서 BM25 + 임베딩 + AI 플래너 + 룰 기반 매칭을 한꺼번에 돌립니다.
zone_bpr)ms-marco-MiniLM-L-6-v2 재정렬 + Track A 필수 보존track_a_bm25, zone_bpr_priority, regulation_direct, regulation_direct_byulpyoAI 플래너가 토지정보 + 질문을 보고 3개 트랙으로 검색 키워드를 생성합니다.
그리고 다음을 동시에 강제 포함합니다.
_bm25_metas 전체와 대조 → 겹치는 법령 강제 검색1차 검색 결과 안에서 다음 패턴을 탐지하면 자동으로 후속 검색을 돌립니다.
령 제XX조 → 시행령 조문 추가법 제XX조 → 법률 조문 추가조례로 정한다 / 정하는 → 해당 지자체 조례 검색별표 X → 별표 조문 추가[위계:3 📘 조례] [조문번호] 제목 (장/절) + [맥락] 상위법 위임관계 1~2줄 + 원문단일 LLM 응답을 그대로 신뢰하지 않습니다. 역할이 다른 전문가 페르소나가 각자의 관점에서 따져보고, 반박 전문가가 반대 입장을 일부러 만들어 부딪치게 합니다.
standard 위에 다음을 추가합니다.
소스: core/agents.py · multi_agent_consult() · _recursive_deep_dive() · verify_precedent_citations()
"법령 인용한 척"하는 LLM의 흔한 패턴을 막기 위해, 모델이 만든 답변에서 인용된 조문/판례를 거꾸로 우리 DB에 다시 조회합니다.
같은 모델에게 "이 답변에 누락이나 오류가 있다면?"이라고 다시 묻고, 빠진 쟁점이 있으면 보강 답변을 생성합니다.
답변에서 인용한 법령·조문을 추출 → DB에서 다시 검색 → 누락된 조문이 있으면 자동 보강합니다.
verify_precedent_citations()가 답변 텍스트에서 사건번호 패턴(예: 2018두12345)을 추출 → ChromaDB urbanlaw_precedents 컬렉션에서 실제 존재 여부 조회.
법규 검토 영역에서 LLM이 가장 자주 틀리는 패턴이 조문 번호 환각과 판례 인용 환각입니다. 그래서 우리는 두 가지를 정한 뒤 출시했습니다 — (1) DB에 없는 법령은 인용하지 않는다, (2) 검증 결과는 답변 안에 그대로 노출한다. 깔끔한 답변보다 거짓을 줄인 답변이 우선이라고 봅니다.
가장 많이 쓰이는 검토 유형입니다. 주소 하나만으로 토지정보부터 법령·판례까지 한 번에 검토합니다.
POST /api/search-address (VWorld 주소검색)POST /api/land-info (PNU 확정 + 데이터 수집)
getLandCharacteristics / getLandUseAttr → 공시지가·토지특성POST /api/analyze SSE 스트림 → 위 03·04·05 섹션의 검색·전문가 패널·검증 절차 그대로 수행/api/chat/stream) + 재분석(/api/reanalyze) + 내보내기(/api/export/docx) 가능소스: routes/land.py, routes/analyze.py
한 필지가 아니라 여러 필지를 한꺼번에 봐야 하는 사업형 검토용입니다.
POST /api/block-parcels)POST /api/multi-parcel-zoning · POST /api/block-zoning 호출
landInfo 재구성 (대지면적 합산, 평균 용적률 등)POST /api/analyze 또는 /api/block-analyze 사용소스: routes/block.py (707 LOC)
"역세권 청년주택의 인센티브 한도?"처럼 토지 맥락이 없는 제도 자체에 대한 질문 모드입니다.
land_info=None으로 분석 진입_parse_general_planner)가 질문을 법령 검색용 쿼리로 분해2026-04-17 이전까지 후속 채팅(/api/chat/stream)에서 법령 검색 결과가 항상 0건으로 반환되는 버그가 있었습니다. search_laws() 반환 타입을 dict로 잘못 가정한 코드가 try/except로 숨겨져 있던 것이 원인이었고, 발견 즉시 수정·배포했습니다. 이후로 후속 질문도 정상 인용됩니다.
소스: routes/analyze.py · core/search.py
설계공모 지침서, 사업 설명서 PDF를 올리면 핵심 조건·법적 한도·인센티브·리스크를 자동으로 정리합니다.
POST /api/competition/analyzeget_land_info로 대상지 토지정보까지 보강search_laws_for_guideline()로 관련 법령 대량 검색search_legal_limits()로 지침서 수치 vs 법적 한도 비교 (예: 지침서 "용적률 600%" vs 법령 한도)search_incentives()로 적용 가능한 인센티브 후보 수집generate_risk_flags()로 누락·재확인 필요 항목 생성POST /api/competition/chat으로 같은 지침서에 대해 후속 질의소스: routes/competition.py (1,205 LOC)
지침서/대지 조건을 바탕으로 건폐율·용적률·높이·인센티브·최대 규모를 계산합니다. UI는 별도 탭이지만, 백엔드는 지침서 분석(competition)의 구조화 데이터를 재활용합니다.
POST /api/scale-review — 지침서/토지정보 + 사용자 입력 수치 → 법적 한도 비교 → 인센티브 적용 시 가능 규모 시뮬레이션소스: routes/competition.py · scale 관련 로직
필지 단위가 아닌 구역 단위 가능성 검토용. 정비사업·재개발·도시재생을 염두에 둔 모드입니다.
POST /api/devreview/analyzeroutes.analyze.api_analyze()에 넘겨 AI 종합 분석 수행 (검색·전문가 패널·검증 모두 동일)소스: routes/devreview.py (275 LOC)
"내부적으로 뭐 쓰는지" 묻는 분들을 위해 그대로 적습니다. 숨길 이유가 없습니다.
text-embedding-3-large (3,072차원)ms-marco-MiniLM-L-6-v2 (numpy 1.26.4 고정)잘 되는 것만 적으면 신뢰가 안 쌓인다고 봐서, 우리가 알고 있는 한계도 같이 적습니다.
static/app.js가 매우 크고, 탭 간 결합도가 높아 회귀 버그 위험이 상존합니다. 리팩토링 진행 중.설명을 신뢰하기보다, 직접 결과를 확인하시는 게 가장 빠릅니다. 단일 획지 검토는 약 30~60초면 끝납니다.