Claude가 PDF를 전사하지 못하는 이유 — 그리고 그 대안
요약
Claude가 PDF 내용을 그대로 전사하려고 할 때 발생하는 '재현 방지 가드레일(anti-regurgitation guard)' 문제와 그 해결책을 다룹니다. 모델이 텍스트를 직접 출력하게 하는 대신, 파서를 작성하게 하여 파일을 직접 생성하는 아키텍처를 권장합니다.
핵심 포인트
- Claude의 출력 측 분류기는 저작권 여부와 상관없이 축자적 재현을 차단함
- API 400 에러(Output blocked by content filtering policy)로 나타남
- 모델 출력에 소스 텍스트를 포함하지 말고 파서를 작성하게 하는 것이 해결책임
우리는 보트 에이전트(boat agent)를 위한 MCP 서버를 지원하기 위해, 도로 항해 규칙인 COLREGS 규칙 텍스트를 담은 공개적이고 기계 판독 가능한 저장소가 필요했습니다. 명백한 계획은 Claude에게 USCG의 Navigation Rules and Regulations Handbook PDF를 전달하고, 각 규칙을 깔끔한 마크다운(markdown) 파일로 전사(transcribe)하도록 요청하는 것이었습니다. 이 텍스트는 퍼블릭 도메인(public domain)에 속하는 미국 정부 저작물로, 복제와 재배포가 자유롭습니다.
Claude는 PDF 전체를 읽고 작성을 시작했지만, 곧 Anthropic API가 이를 강제로 차단했습니다:
API Error: 400 Output blocked by content filtering policy
차단되었습니다. 또 차단되었습니다. 전사를 완전히 포기하기 전까지 두 세션에 걸쳐 11번의 차단된 응답이 발생했습니다. 이 포스트는 그 막다른 길에 대한 기록입니다 — 시도 과정, 반복되는 오류, 그리고 결과적으로 더 나은 아키텍처(architecture)임이 밝혀진 해결책: 소스 텍스트를 모델 출력(model output)을 통해 전달하지 마세요. 모델이 파서(parser)를 작성하게 하고, 그 파서가 파일을 작성하게 하세요.
문제 (Problem)
증상은 작업 도중 Messages API에서 발생하는 400 에러이며, 모델을 구동하는 요소(Claude Code, SDK 호출, 에이전트 루프 등)를 통해 다음과 같이 나타납니다:
API Error: 400 Output blocked by content filtering policy
이 오류는 모델이 소스 문서를 읽고 출력을 시작한 후, 즉 '늦게' 발생합니다. 읽기 작업은 성공합니다. 생성(generation) 단계에서 문제가 생기는 것입니다. 그리고 이 문제는 지속적입니다. 일단 작업이 "이 문서를 읽고 그 텍스트를 작성하라"가 되면, 문서 텍스트를 전혀 포함하지 않는 답변을 포함하여 세션 내의 모든 후속 응답이 차단되는 경향이 있습니다.
이것은 속도 제한(rate limit)도 아니고, 컨텍스트 길이(context-length) 오류도 아니며, 잘못된 요청(malformed request)도 아닙니다. 이것은 **출력 측 분류기(output-side classifier)**입니다. Anthropic은 자사의 개인정보 보호 센터(privacy center)에 이를 명확히 문서화해 두었습니다:
Claude의 목적은 새로운 콘텐츠와 아이디어를 생성하는 것이지, 이미 존재하는 콘텐츠를 재현하는 것이 아닙니다.
이것은 재현 방지 가드레일(anti-regurgitation guard)입니다. 시스템은 소스 자료의 지속적인 축자적 재현(verbatim reproduction)이 있는지 출력(output) 스트림을 감시하며, 이를 발견하면 작동합니다. 이러한 설계로 인해 두 가지 결과가 발생하며, 두 가지 모두 우리에게 문제를 일으켰습니다:
- 라이선스를 식별하지 못합니다 (It is license-blind). 분류기 (Classifier)는 출처나 라이선스를 확인할 수 없습니다. 퍼블릭 도메인(Public-domain)인 미국 해안경비대(USCG) 규정 텍스트도 저작권이 있는 산문과 똑같이 분류기를 작동시킵니다. "하지만 이것은 복사해도 법적으로 문제가 없습니다"라는 논리는 필터가 알아들을 수 있는 논거가 아닙니다.
- API 레벨에서 작동하므로 우회할 수 없습니다. 서브 에이전트 (Subagents), 서로 다른 세션, 재시도 (Retries) 등 모두 동일한 분류기에 걸리게 됩니다. 트리거는 일시적인 현상이 아니라, 작업의 형태 (출력물에 긴 내용을 그대로 복사하여 넣는 행위) 그 자체입니다.
이 문제를 겪고 있다면 여러분만 그런 것이 아닙니다. 이는 Claude Code에서 지속적으로 발생하는 오탐 (False positives)의 원인입니다 — LICENSE 파일 생성, Mozilla 라이선스 추가, Contributor Covenant 행동 강령, 동료 검토(Peer-reviewed) 논문 추출, 심지어 무해한 마크다운(Markdown) 편집까지 포함됩니다. 이 모든 사례의 공통점은 모델이 기존 텍스트와 밀접하게 일치하는 긴 구간을 출력하려 했다는 점입니다.
진단 (Diagnosis)
해결책을 찾는 데 핵심이 되는 사고 모델은 다음과 같습니다: 필터는 사용자의 의도가 아니라 출력물(Output)에 주목합니다. 해당 텍스트가 퍼블릭 도메인이라는 사실은 중요하지 않습니다. 당신 앞에 파일이 열려 있다는 사실도 중요하지 않습니다. 당신이 텍스트를 추출했고 모델에게 단순히 그것을 다시 말해달라고 요청하는 것 또한 중요하지 않습니다. 기존 자료의 긴 verbatim (축자적) 구간이 모델의 출력물을 통해 흘러나오면, 분류기가 작동할 수 있습니다.
사람들이 놓치는 부분은 바로 마지막 지점입니다. 우리는 어시스턴트가 진행 상황을 요약하는 동안 이미 추출된 vault 텍스트를 단순히 채팅창에 다시 출력했을 때 한 번 트리거를 작동시킨 적이 있습니다. 해당 텍스트는 이미 다른 수단을 통해 PDF에서 추출된 상태였으며, 모델은 단지 대화 중에 그것을 인용하고 있었을 뿐입니다. 출력은 출력입니다. 텍스트가 어디에서 왔는지는 분류기에게 보이지 않습니다.
따라서 "이 PDF를 마크다운(markdown)으로 전사(transcribe)해줘"라는 문구는 거의 완벽한 트리거(trigger)입니다. 이는 정의상 기존 문서의 긴 축어적 구간(verbatim span)을 출력하라는 요청입니다. 수행하려는 작업과 필터가 차단하는 작업이 동일한 것입니다.
우리가 시도한 것 (그리고 실패한 이유)
시도 1 — 그냥 전사하기
모델에게 PDF를 가리키고 규칙에 따라 깔끔한 마크다운(markdown)을 요청했습니다. 모델이 생성한 작업 목록을 재구성한 버전은 다음과 같습니다 (구조는 실제이며, 문구는 축어적 전사가 아닌 재구성된 것입니다):
TODO
[ ] 규칙 1–13 전사 → rules/international/rule-01.md … rule-13.md
[ ] 규칙 14–31 전사
...
모델은 첫 번째 작업을 포착하고, 페이지를 읽고, 쓰기를 시작했습니다 — 그리고 다음과 같은 결과가 나왔습니다:
API Error: 400 Output blocked by content filtering policy
한 세션에서 차단벽이 세워지기 전까지 11개의 규칙 파일이 생성되었고, 나머지는 중단되었습니다. 이후 전용 전사 세션에서는 상황이 더 나빴습니다 — 6번의 응답 차단, 생성된 파일 0개. 첫 번째 작업을 포착한 이후의 모든 응답이 차단되었습니다.
시도 2 — 재시도
400 에러가 발생했을 때의 본능은 재시도하는 것입니다. 그래서 우리는 재시도했습니다. 동일한 작업, 새로운 응답:
API Error: 400 Output blocked by content filtering policy
여기서 재시도는 절대 통하지 않으며, 이제 그 이유를 알게 되었습니다. 트리거는 일시적인 오류가 아니라 작업의 형태 그 자체입니다. 동일한 입력, 동일한 분류기(classifier), 동일한 차단입니다. 우리는 심지어 답변에 문서 텍스트를 포함하지 않은 일반 채팅으로 모델에게 _"작동 중인가요, 아니면 막혀 있는 건가요?"_라고 물어보았습니다 — 그리고 그 질문조차 차단되었습니다. 왜냐하면 해당 세션이 이미 축어적 재현(verbatim-reproduction) 작업에 고정되었기 때문입니다.
시도 3 — 서브에이전트(subagent) 파견
더 엄격한 프롬프트(prompt)를 가진 새로운 서브에이전트(subagent)라면 다르게 행동할지도 모릅니다. 우리는 규칙 파일을 작성하는 것만을 임무로 하는 서브에이전트를 파견했습니다. 하지만 해당 에이전트 역시 두 번의 차단을 겪었습니다:
API Error: 400 Output blocked by content filtering policy
필터는 API 레벨에서 작동합니다. 서브에이전트(subagent)는 단지 더 많은 Messages API 호출일 뿐이며, 정확히 동일한 분류기(classifier)를 거쳐 라우팅됩니다. 필터링되지 않는 "내부" 모델이란 존재하지 않습니다.
두 세션에 걸친 합계: 11번의 응답 차단. 첫째 날 5번(그중 2번은 파견된 하위 에이전트(subagent) 내부에서 발생, 1번은 어시스턴트가 이미 추출된 텍스트를 단순히 _되풀이(echo)_했을 때 발생), 전용 재시도 세션에서 진전 없이 6번 발생. 마침내 교훈을 얻었습니다: 더 정중하게 요청하거나, 다른 에이전트를 사용하거나, 한 번 더 시도한다고 해서 이 작업이 해결되지는 않습니다. 차단되는 것은 작업 자체가 아니라, 그 작업 내용 그 자체입니다.
해결책
모델의 출력(output)을 통해 소스 텍스트를 보내는 것을 중단하십시오. 모델이 파서(parser)를 작성하게 하십시오. 그리고 파서가 파일을 작성하게 하십시오. 이제 모델의 출력은 Python입니다. 즉, 필터가 문제 삼지 않는 새로운 코드입니다. 그리고 규칙 본문은 pdftotext 및 XML 파싱(parsing)을 통해 파일 대 파일로 흐르며, 모델의 출력 스트림(output stream)에는 전혀 닿지 않습니다.
이 볼트(vault)를 위해 세 개의 결정론적 추출기(deterministic extractors)와 하나의 오케스트레이터(orchestrator)를 구성했습니다. PDF보다 구조화된 권위 있는 소스가 있다면 그것을 우선시하십시오. 출처(provenance)가 더 명확할 뿐만 아니라 기계 판독(machine-parseable)도 가능하기 때문입니다:
# scripts/build_vault.py — Claude가 작성했습니다. 모델이 아닌 이 스크립트가 추출을 수행합니다.
for part in ECFR_PARTS: # 33 CFR 83–88, eCFR 버전 관리 API XML
xml_text = ecfr_path(sources, part).read_text()
...
기계 판독이 가능하도록 각각 선택된 세 가지 소스:
미국 내륙(US Inland) → eCFR 버전 관리 API XML (33 CFR Parts 83–88) → rules/inland/*.md
캐나다(Canadian) → Justice Laws XML (C.R.C., c. 1416) → rules/canadian/*.md
국제(International) → USCG 핸드북의 텍스트 레이어에 pdftotext 적용 → rules/international/*.md
명령어 하나로 전체 볼트를 다시 구축합니다:
uv run python scripts/build_vault.py \
--handbook "/path/to/Nav Rules Handbook_Corrected_08-12-2024.pdf"
결과: 생성된 파일 135개, 필터 오류 0건 — 모델 출력을 통해 전달했을 때 11번이나 차단되었던 것과 정확히 동일한 자료를 처리했습니다. 각 파일은 구조화된 프론트매터(frontmatter) 블록과 규칙 본문(rule body)을 포함하는 작은 마크다운(markdown) 문서입니다 (프론트매터 형태만 포함하며, 규칙 본문은 포함하지 않음):
---
number: '5'
regime: international
...
이것이 작동하는 단 한 줄의 이유는 다음과 같습니다: 모델의 출력값이 파서(parser, 새로운 코드)이며, 코퍼스(corpus) 텍스트는 모델에 의해 생성(emitted)되지 않고 파일 간에 이동할 뿐이라는 점입니다.
이것이 중요한 이유 / 주의사항
어쨌든 이것이 안전이 중요한(safety-critical) 코퍼스를 위한 더 나은 아키텍처입니다. 필터링 과정에서 다음과 같은 업그레이드가 강제되었습니다:
- 환각(hallucination) 또는 의역(paraphrase) 위험 없음. 결정론적(deterministic) 파서는 바이트(bytes)를 복사합니다. 규칙을 "대체로" 재현하는 것이 아닙니다. 양보 우선순위를 결정하는 'not' 단어 하나가 누락될 수 있는 항해 규칙(navigation rules)의 경우, 이는 단순히 편의의 문제가 아닙니다.
- 바이트 단위로 동일한 재구축(Byte-identical rebuilds). 소스(sources)를 고정하고 다시 실행하면 동일한 저장소(vault)를 얻을 수 있습니다. 여기서의 빌드(build)는 전부가 아니면 전무(all-or-nothing) 방식입니다. 즉, 단 하나의 파일도 쓰기 전에 전체 규칙 세트(regime별 예상 규칙 번호, 비어 있지 않은 산문, 인쇄 아티팩트(print artifacts) 없음, 타당한 제목 등)를 검증합니다.
- 인간의 검토 작업 감소. 전사된 모든 줄을 교정하는 대신, 검토자는 샘플을 통해 파서(parser)를 점검(spot-check)합니다. 결정론적 추출(deterministic extraction)은 깨끗한 샘플 하나가 전체 배치(batch)를 검증할 수 있음을 의미합니다.
우리가 겪었던 몇 가지 함정들이 있으며, 이 방식을 선택할 경우 유용할 것입니다:
- 출력하여 확인하지 말고, 차이점(diff)과 샘플링으로 검토하세요. 추출된 산문(prose)을 대화창에 다시 출력하는 행위는 깨끗하게 빌드된 후에도 필터(filter)를 다시 작동시킵니다. 검증(Verification)은 구조 — 개수, 규칙 번호, 제목, 첫 글자 등 — 를 확언하는 것이지, 긴 산문 문자열을 확인하는 것이 아닙니다. 예를 들어, 캐나다의 파서(parser)는 충실도 불변량(fidelity invariant)을 강제합니다. 즉, 조항을 평탄화(flattening)할 때 단어 문자가 손실되면 오류를 발생시킵니다(
입력값과 출력값에 대해 re.sub(r"\W", "", ...)를 적용했을 때 일치해야 함). 따라서 정확성은 사람이 텍스트를 읽는 것이 아니라, 코드의 어설션(assertion)을 통해 확인됩니다. - 실제 소스 XML에는 방언(dialects)이 존재합니다. eCFR 테이블은 다른 정부 XML에서 흔히 볼 수 있는 CALS 방식의
ROW/ENT가 아니라 HTML 스타일의TR/TD를 사용합니다. 우리는 검토 과정에서 이를 발견하기 전까지 테이블 하나를 통째로 누락시킨 적이 있습니다.EXTRACT블록은 중첩되어 있어 재귀(recursion)가 필요합니다. 핸드북의 텍스트 레이어는 섹션 제목을 줄 중간에 감싸고 있어, 마치 콘텐츠인 것처럼 위장한 파편들을 생성합니다. 결정론적(Determinism)이라는 것이 사소하다는 뜻은 아닙니다. 그것은 _디버깅 가능함(debuggable)_을 의미합니다. 즉, 실패의 원인이 이번 턴의 모델의 기분(mood)에 있는 것이 아니라, 읽고 테스트할 수 있는 코드에 있다는 뜻입니다. - 실패할 때는 소리 높여 명확하게 실패합니다. 처리되지 않은 요소를 만난 파서는 조용히 의역(paraphrase)을 내뱉는 대신, 태그 이름을 명시하며 오류를 발생시켜 사용자가 확인하도록 유도합니다. 이는 모델이 어색하다고 느낀 텍스트를 기꺼이 "수정"해버리는 전사(transcription) 실패 모드와는 정반대의 방식입니다.
단순한 규칙을 넘어선 일반적인 원칙은 다음과 같습니다: "문서로부터 구조화된 코퍼스(corpus)를 구축하는" 모든 작업 — 가이드북을 다시 입력하거나, 장비 매뉴얼을 사양서(spec cards)로 변환하는 작업 등 — 은 전사(transcribe)하는 것이 아니라 결정론적으로 추출(extract)해야 합니다. 소스에 저작권이 걸려 있다면 필터가 더 강력하게 작동할 것이며, 충실도(fidelity)를 위해 결과와 상관없이 결정론적인 파이프라인을 원하게 될 것입니다. 출력 필터가 LICENSE 파일에 대해 오탐(false-positive)을 일으키면 짜증스럽겠지만, 코퍼스 입력(corpus-ingestion) 작업에서 필터가 작동한다면 그것은 당신이 처음부터 선택했어야 할 아키텍처를 가리키고 있는 것입니다.
끝
이것은 모든 것이 전기 구동되는 차터 카타마란(charter catamaran)을 위한 AI ops 레이어를 구축하는 과정에서 나온 결과물입니다. 로컬 LLM 뒤에 MCP 서버 스택과 Home Assistant 음성 프런트엔드를 배치했으며, 그중 하나는 항해 규칙(navigation rules of the road)에 관한 질문에 답변합니다. 이 볼트(vault)와 빌드 파이프라인은 오픈 소스입니다: github.com/sailingnaturali/colregs-vault ([결정론적(deterministic) scripts/build_vault.py 포함), 이는 colregs-mcp에 의해 소비됩니다. 만약 전사(transcription) 작업 중에 Output blocked by content filtering policy 오류와 싸우고 있었다면, 해결책은 동일합니다. 전사를 멈추고, 파싱(parsing)을 시작하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기