건축 법규검토 AI에서 “4층 이하"와 “4층 이상"을 혼동하면 어떻게 될까? 높이 상한이 뒤집혀 불법 건축물이 합법으로 판정된다. 이 글은 그 한 글자 차이를 잡기 위한 여정이다.

문제: PDF 표가 검색되지만 신뢰하기 어렵다

건축 법규검토 시스템은 지구단위계획 고시, 설계 지침서 등 건축 관련 PDF를 분석하여 건폐율, 용적률, 높이제한 등의 기준을 추출한다. PDF 전처리 파이프라인은 Docling을 사용해 문서를 파싱하고, 텍스트를 청킹한 후 임베딩을 생성하여 하이브리드 검색(키워드 + 시맨틱)을 지원한다.

Docling의 HierarchicalChunker는 표 내용도 마크다운 형태로 청킹하여 검색 인덱스에 포함한다. 표가 아예 빠지는 건 아니다. 문제는 그 마크다운의 품질이었다.

  • 병합 셀 구조가 깨지면서 “건폐율 60%“가 어떤 가구번호에 해당하는지 관계가 사라진다
  • OCR 오류(“이하” → “이상”, “커” → “키”)가 검색 결과에 그대로 노출된다
  • 에이전트가 “주1 건폐율"을 검색해서 청크를 찾아도, 그 값이 맞는지 신뢰할 수 없다

지구단위계획 고시의 건폐율/용적률/높이 기준은 대부분 복합 표에 존재하기 때문에, 이는 치명적인 문제였다.

테스트 대상: 대구연호 공공주택지구 지구계획 고시

테스트에 사용한 PDF는 「국토교통부고시 제2024-598호」(54페이지)로, Docling이 추출한 표는 총 109개, 54개 전 페이지에 분포한다.

아래는 핵심 표가 포함된 36페이지의 실제 PDF 이미지다:

page_36 그림 1. 고시 PDF 36페이지 — 가구별 건폐율/용적률/높이 기준표. 병합 셀이 복잡하게 얽혀 있다.

Docling 마크다운의 한계

Docling은 PDF의 표를 2D 그리드(행×열 배열)와 마크다운으로 추출한다. 단순한 표에서는 잘 동작하지만, 건축 고시 PDF의 복합 표에서는 병합 셀 관계가 손실되고, 한국어 OCR 오류(“커” → “키”)가 발생하며, 어떤 값이 어떤 가구에 해당하는지 관계가 불분명해진다.

접근: Vision + OCR 하이브리드

왜 Vision인가?

LLM의 Vision 기능은 이미지를 직접 보고 해석한다. PDF 페이지를 이미지로 렌더링하면, 사람이 표를 읽는 것과 동일한 방식으로 병합 셀의 시각적 경계를 인식하고, 행과 열 간의 논리적 관계를 파악하여 구조화된 JSON으로 출력할 수 있다.

하지만 Vision만으로는 부족했다.

Vision-only의 한계: 체계적 오류

Bedrock Claude Haiku 4.5로 Vision-only 테스트를 진행했을 때, 높이와 용적률 필드에서 체계적인 “이하” → “이상” 오류가 발생했다.

36페이지 실제 Vision-only 결과 (오류 부분 발췌):

{
  "구분": "공1, 공2",
  "건폐율": "60% 이하",
  "용적률": "400% 이하",
  "높이": "20층 이상"   // ← 원본: "20층 이하"
},
{
  "구분": "공3, 공4",
  "건폐율": "60% 이하",
  "용적률": "400% 이하",
  "높이": "10층 이상"   // ← 원본: "10층 이하"
},
{
  "구분": "초1",
  "건폐율": "60% 이하",
  "용적률": "200% 이하",
  "높이": "5층 이상"    // ← 원본: "5층 이하"
},
{
  "구분": "키1 ~ 키2",  // ← 원본: "커1, 커2"
  "건폐율": "60% 이하",
  "용적률": "400% 이하",
  "높이": "8층 이상"    // ← 원본: "8층 이하"
}

37페이지에서도 용적률 1건의 오류가 발생했다:

{
  "구분": "기타1",
  "건폐율": "60% 이하",
  "용적률": "200% 이상",  // ← 원본: "200% 이하"
  "높이": "4층 이하"
}

흥미로운 점은 건폐율은 모두 정확했다는 것이다. “이하"와 “이상"의 시각적 유사성이 문제인데, 특히 높이 필드에서 집중적으로 오류가 발생했다.

하이브리드: 이미지 + OCR 텍스트 교차 검증

핵심 아이디어: Docling OCR은 텍스트 인식에 강하고, Vision은 구조 인식에 강하다. 둘을 결합하면?

능력 Docling OCR Vision
텍스트 인식 (“이하”/“이상”) 강함 약함
표 구조 파악 (병합 셀) 약함 강함
행-열 관계 이해 약함 강함

Docling이 이미 추출한 마크다운을 OCR 텍스트로 함께 제공하고, Vision에게 “이미지의 구조를 보되, 텍스트는 OCR과 교차 검증하라"고 지시하면 된다.

프롬프트 엔지니어링: 한 글자의 차이를 잡다

하이브리드 방식을 도입했지만, 첫 번째 프롬프트에서는 여전히 “이하” → “이상” 오류가 발생했다. 프롬프트에 “이미지의 시각 정보를 우선하되"라고 썼기 때문이다. Haiku는 이 지시를 충실히 따라 OCR의 올바른 “이하"를 무시하고 이미지의 “이상"을 채택했다.

Claude 프롬프트 엔지니어링 가이드를 참고하여 프롬프트를 재설계했다.

적용한 원칙들

1. <role> + WHY (왜 정확해야 하는지)

<role>
지구단위계획 고시 PDF에서 표를 구조화 JSON으로 추출하는 전문가입니다.
이미지와 OCR 텍스트가 함께 제공됩니다.
건축 법규검토에 사용되므로 "이하"와 "이상"의 구분이 정확해야 합니다.
</role>

단순히 “정확하게 하라"가 아니라, 정확해야 하는지(건축 법규검토)를 명시했다. Claude(LLM)는 맥락이 주어졌을 때 더 환각(Hallucination)을 줄일 수 있다.

2. <workflow> + 도메인 힌트

<workflow>
1. 이미지에서 표의 구조(행/열/병합)를 파악합니다.
2. 각 셀의 텍스트를 읽되, OCR 텍스트와 교차 검증합니다.
   건폐율/용적률/높이 셀에서 "이하"와 "이상"이 나오면,
   OCR 텍스트의 해당 셀과 반드시 비교하세요.
   지구단위계획에서 건폐율·용적률·높이는 상한 규제이므로 "이하"가 일반적입니다.
3. 아래 JSON 스키마에 맞게 출력합니다.
</workflow>

핵심은 두 가지다:

  • 교차 검증 지시: “반드시 비교하세요"로 OCR 텍스트를 참조하도록 강제
  • 도메인 힌트: “상한 규제이므로 이하가 일반적” — 모델이 확신이 없을 때 올바른 방향으로 기울게 함

3. <examples> — Good/Bad 대비

<examples>
<good_example>
OCR: " 이하 60%"  →  "건폐율": "60% 이하"
OCR: " 층 이하 4"  →  "높이": "4층 이하"
</good_example>
<bad_example>
OCR에 "이하"인데 "이상"으로 출력
→ 건축 규제 방향이 반대가 되어 법규검토 오류 발생
</bad_example>
</examples>

Bad example에서 **결과(consequence)**를 명시한 것이 중요하다. “법규검토 오류 발생"이라는 실질적 피해를 알려주면 모델이 해당 패턴을 더 강하게 회피한다.

결과: 오류 6건 → 0건

실제 테스트 결과 (Bedrock Claude Haiku 4.5):

페이지 표 유형 Vision-only 오류 하이브리드 오류
36 건폐율/용적률/높이 높이 4건 + 가구번호 1건 0건
37 건폐율/용적률/높이 용적률 1건 (“이상”) 0건
38 면적조서 0건 0건
7 토지공급계획 0건 0건
40 도로현황 0건 0건

36페이지의 실제 추출 결과를 나란히 비교하면 차이가 명확하다:

Vision-only — 높이 필드 4건 전부 “이상” 오류, 가구번호 “커”→“키” 오류

{"구분": "공1, 공2", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "20층 이상"},
{"구분": "공3, 공4", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "10층 이상"},
{"구분": "초1",      "건폐율": "60% 이하", "용적률": "200% 이하", "높이": "5층 이상"},
{"구분": "키1 ~ 키2", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "8층 이상"}

하이브리드 — 전 항목 정확, 가구번호도 “커"로 올바르게 추출

{"구분": "공1, 공2", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "20층 이하"},
{"구분": "공3, 공4", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "10층 이하"},
{"구분": "초1",      "건폐율": "60% 이하", "용적률": "200% 이하", "높이": "5층 이하"},
{"구분": "커1, 커2", "건폐율": "60% 이하", "용적률": "400% 이하", "높이": "8층 이하"}

OCR 텍스트가 텍스트 인식의 기준점(Anchor) 역할을 수행하며, 높이와 가구번호의 오류가 모두 교정되었다.

page_37 그림 2. 37페이지 — 종교시설/기타 가구의 규모 기준. Vision-only에서 용적률 “200% 이상” 오류가 발생했으나, 하이브리드에서는 정확히 “200% 이하"로 추출되었다.

비용 비교 (실측)

모델 방식 페이지 토큰 (in+out) 비용 소요시간
Haiku 4.5 Vision-only 36 2,020+1,402 $0.0090 11.9s
Haiku 4.5 하이브리드 36 7,463+1,478 $0.0149 11.3s
Haiku 4.5 Vision-only 37 2,020+1,519 $0.0096 12.4s
Haiku 4.5 하이브리드 37 3,918+945 $0.0086 9.8s

하이브리드는 OCR 텍스트가 추가되어 입력 토큰이 늘지만, 출력 정확도가 높아 재시도가 불필요하다. 페이지당 평균 비용은 $0.009~0.015 수준이다. (Haiku 4.5 공식 단가: $1.00/MTok input, $5.00/MTok output)

참고로 Sonnet 4.5 Vision-only도 테스트했다. “이하”/“이상"은 정확했지만 가구번호 “커"를 “기"로 오인식했고, 비용은 $0.028/페이지(Haiku의 3배), 소요시간은 18.3초(1.5배)였다. 프롬프트 엔지니어링과 OCR 하이브리드를 결합하면, 이 작업에서는 Haiku만으로도 충분했다.

아키텍처: 검색 레이어를 건드리지 않는 통합

핵심 설계

Docling의 HierarchicalChunker가 이미 표 마크다운을 청킹하고 있으므로, Vision이 생성한 고품질 flat_text를 추가 청크로 같은 인프라(docling_chunks + docling_embeddings)에 저장한다.

Docling 마크다운 청크   → docling_chunks (기존, 품질 낮음)
Vision 추출 flat_text  → docling_chunks (추가, 구조화된 고품질)
                       → docling_embeddings (임베딩 벡터)

이렇게 하면:

  • Docling 청크와 Vision 청크가 공존 — 검색 시 더 정확한 Vision 청크가 상위에 랭킹
  • 기존 키워드/시맨틱 검색 코드 변경 0줄
  • Vision이 실패해도 Docling 청크가 fallback으로 동작

구조화 JSON은 docling_tables.structured_data에 별도 저장하여, 향후 프로그래밍 방식의 값 추출이나 프론트엔드 표 렌더링에 활용한다.

파이프라인

PDF 업로드
  ├─ 1. S3 다운로드 (pdf_bytes 보존)
  ├─ 2. Docling 변환 (OCR + 레이아웃 분석)
  ├─ 3. 테이블 추출 (DoclingTable + 마크다운)
  ├─ 4. 이미지 추출
  ├─ 5. ★ Vision 표 구조화 (NEW)
  │     │
  │     ├─ 페이지별 그룹핑 (같은 페이지의 표들 묶음)
  │     ├─ PyMuPDF로 페이지 → PNG 이미지
  │     ├─ Docling 마크다운을 OCR 텍스트로 사용
  │     ├─ Bedrock Converse API (Haiku 4.5) 호출
  │     │
  │     ├─ structured_data → docling_tables에 저장
  │     ├─ flat_text → docling_chunks에 저장
  │     └─ embedding → docling_embeddings에 저장
  ├─ 6. HierarchicalChunker 텍스트 청킹
  ├─ 7. 임베딩 생성 (텍스트 + Vision 표 통합)
  └─ 8. DB 저장

flat_text: 어떤 표든 검색 가능한 평문으로

Vision이 출력하는 구조화 JSON은 표 유형마다 다르다. 이를 재귀적으로 평문화하는 structured_json_to_flat_text 함수가 모든 구조를 처리한다:

def _flatten_dict(obj, lines, depth=0):
    indent = "  " * depth
    if isinstance(obj, dict):
        for key, val in obj.items():
            if isinstance(val, (dict, list)):
                lines.append(f"{indent}{key}:")
                _flatten_dict(val, lines, depth + 1)
            else:
                lines.append(f"{indent}{key}: {val}")
    elif isinstance(obj, list):
        for item in obj:
            _flatten_dict(item, lines, depth)

36페이지 하이브리드 추출의 실제 flat_text 출력:

[변경 - 제20869호 관보 2024-11-08]

위치: 주1 ~ 주5
용도:
  허용용도: 「주차장법」 제2조 제1호의 노외주차장, ...
  불허용도: 허용용도 이외의 용도, ...
규모:
  구분: 주1
  건폐율: 60% 이하
  용적률: 200% 이하
  높이: 4층 이하

  구분: 주2
  건폐율: 70% 이하
  용적률: 500% 이하
  높이: 12층 이하

키워드 “건폐율 60%”, “높이 4층” 등으로 검색하면 해당 청크가 매칭된다.

Graceful Degradation

Vision API 호출은 외부 서비스 의존성이므로, 실패에 대비한 fallback을 설계했다:

if extract_tables and file_result.tables and pdf_bytes:
    try:
        vision_chunks, vision_embeddings, updated_tables = refine_tables_with_vision(
            pdf_bytes=pdf_bytes,
            tables=file_result.tables,
            source_id=source_id,
            file_id=file_id,
        )
        # Vision 결과 반영
    except Exception as e:
        logger.warning(f"[Vision] 표 구조화 실패 (Docling 원본 유지): {e}")

Vision이 실패하면:

  • Docling의 원본 마크다운이 그대로 유지
  • 표가 검색에서 누락되는 기존 동작으로 fallback
  • 텍스트 청킹/임베딩은 정상 진행

즉, Vision은 **추가 가치(additive)**이지 **필수 의존성(dependency)**이 아니다.

비용 분석

테스트 PDF(54페이지, 표 125개, 61개 고유 페이지) 기준 예상 비용:

항목 산출 근거 비용
Docling 처리 (CPU) 로컬 실행 ~$0
Vision 표 추출 (Haiku) 54페이지 × $0.011/페이지 ~$0.59
텍스트 임베딩 (Cohere) 기존 파이프라인 ~$0.05
합계 ~$0.64/PDF

표가 포함된 페이지에서만 Vision을 호출하므로, 텍스트 위주의 PDF에서는 비용이 거의 발생하지 않는다.

결론

  • 문제: Docling 마크다운의 낮은 표 품질(병합 셀 구조 손실, OCR 오류 “이하”→“이상”)
  • 해결: 멀티모달 Vision + Docling OCR 하이브리드 → 구조화 JSON + 검색용 flat_text
  • 핵심: 프롬프트 엔지니어링으로 Haiku급 모델에서도 오류 0건 달성
    • <role> + WHY: “건축 법규검토에 사용되므로”
    • <workflow> + 도메인 힌트: “상한 규제이므로 이하가 일반적”
    • <examples>: Good/Bad + consequence
  • 교훈: 단순히 이미지를 신뢰하기보다, OCR과의 교차 검증을 강제하고 도메인 맥락(Why)을 주입하는 것이 훨씬 효과적이다.

검색 레이어를 건드리지 않고, 기존 파이프라인에 한 단계를 추가하는 것만으로 PDF 표의 검색 품질이 근본적으로 개선되었다. 가장 우아한 해결책은 때로 최소한의 코드 변경만으로 완성된다.