Claude Files API로 PDF·이미지를 한 번 올리고 멀티턴 대화에서 재사용하기
멀티턴 챗봇을 만들다 보면 어느 순간 이런 상황을 마주치게 됩니다. 사용자가 50페이지짜리 계약서를 놓고 질문을 10번쯤 던지는데, 그 매 요청마다 같은 PDF를 통째로 Base64 인코딩해서 실어 보내고 있는 겁니다. 요청당 페이로드가 수 MB씩 나가고, 레이턴시는 늘어나고, CPU는 인코딩으로 바쁩니다. 저도 처음에는 "어차피 토큰 비용이 메인이니까 네트워크 오버헤드는 그냥 참자"고 생각했는데, 세션 동시성이 올라가면서 생각이 달라졌습니다.
2025년 4월 Anthropic이 베타로 출시한 Files API는 이 문제를 정면으로 다룹니다. 파일을 한 번 업로드하면 반환되는 file_id를 이후 모든 요청에서 참조만 하면 됩니다. 파일 본문을 다시 전송할 필요가 없습니다. 다만 짚고 넘어갈 게 있습니다. Files API는 토큰 비용을 자동으로 낮춰주지 않습니다. Claude는 file_id를 받아도 내부적으로 원본 콘텐츠를 처리하고 토큰 수 기준으로 과금합니다. 입력 토큰 비용까지 잡으려면 Prompt Caching을 같이 써야 합니다. 이 차이를 모르고 도입하면 기대치가 어긋나니, 처음에 짚고 가는 게 맞습니다.
이 글에서는 Files API의 동작 구조, Python SDK를 이용한 통합 패턴, 그리고 Prompt Caching과의 조합까지 실무 관점에서 풀어봅니다.
왜 Files API가 필요한가
매 요청마다 파일을 통째로 보내는 비용
Claude API에서 PDF를 다루는 기존 방식은 base64 인코딩된 파일 본문을 메시지 콘텐츠 블록에 직접 포함하는 것이었습니다. 단발성 요청이라면 문제없습니다. 그런데 멀티턴 대화나 다중 사용자 시나리오에서는 이야기가 달라집니다.
동일한 50페이지 문서를 10명의 사용자가 각각 10번씩 질의한다면, 같은 파일이 100번 전송됩니다. 각 요청의 페이로드 크기, 인코딩 CPU, 네트워크 왕복 시간이 모두 곱해집니다.
Files API가 바꾸는 것
Files API 도입 후 흐름은 이렇게 바뀝니다.
파일 본문 전송은 최초 1회뿐입니다. 이후 요청에서는 file_id 문자열만 포함하면 됩니다. 네트워크 오버헤드와 Base64 인코딩 비용이 제거됩니다. file_id는 조직 내 어디서나 참조할 수 있어서, 여러 사용자 세션이 같은 파일을 공유하는 시나리오에 특히 유용합니다.
기본 사용법
지원 파일 형식
2026년 8월 기준(베타) Files API가 지원하는 주요 형식은 다음과 같습니다.
| 형식 | MIME 타입 | 비고 |
|---|---|---|
| application/pdf | 문서 블록으로 참조 | |
| 평문 텍스트 | text/plain | .txt |
| Markdown | text/markdown | |
| 이미지 | image/jpeg, image/png, image/gif, image/webp | 이미지 블록으로 참조 |
| 소스 코드 | text/plain 등 | 언어별 확장자 |
DOCX, XLSX 같은 오피스 포맷은 Claude가 어느 방식으로도 직접 처리하지 않아서 PDF로 변환 후 업로드해야 합니다. 파일당 크기 제한과 조직 스토리지 쿼터는 플랜별로 다르니 공식 Files API 문서에서 최신 값을 확인하세요.
Python SDK로 업로드하고 멀티턴 대화에서 재사용하기
모든 요청에 베타 헤더가 필요합니다. Python SDK에서는 betas=["files-api-2025-04-14"] 파라미터로 처리됩니다.
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY 환경변수
# 1단계: 파일 업로드 (최초 1회)
with open("contract.pdf", "rb") as f:
file_obj = client.beta.files.upload(
file=("contract.pdf", f, "application/pdf"),
)
file_id = file_obj.id
print(f"업로드 완료: {file_id}")
def extract_text(response) -> str:
# content 블록 순서는 상황에 따라 달라질 수 있어서 타입으로 골라냄
return next(b.text for b in response.content if b.type == "text")
# 2단계: 멀티턴 대화 — 첫 턴에만 문서 블록을 넣고, 이후엔 텍스트만 추가
conversation_history = []
def ask(question: str) -> str:
if not conversation_history:
user_content = [
{
"type": "document",
"source": {"type": "file", "file_id": file_id},
"title": "계약서",
"citations": {"enabled": True},
},
{"type": "text", "text": question},
]
else:
user_content = [{"type": "text", "text": question}]
conversation_history.append({"role": "user", "content": user_content})
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=conversation_history,
betas=["files-api-2025-04-14"],
)
answer = extract_text(response)
conversation_history.append({"role": "assistant", "content": answer})
return answer
print(ask("이 계약서의 계약 기간은 얼마인가요?"))
print(ask("해지 조건은 어떻게 되나요?"))
print(ask("위약금 조항을 요약해줘."))여기서 오해하기 쉬운 지점이 있습니다. 두 번째 턴부터 user_content에 텍스트만 넣는다고 해서 "이후 요청은 아무 파일 정보도 안 보낸다"는 뜻이 아닙니다. conversation_history에는 첫 턴의 document 블록(file_id 포함)이 남아 있고, 이 히스토리 전체가 매 요청마다 API로 전송됩니다. 네트워크가 가벼워지는 실체는 "파일 본문 바이트 대신 file_id 문자열이 히스토리에 남는다"는 점입니다. 파일 본문 자체가 반복 전송되지 않는 것이지, 히스토리가 사라지는 게 아닙니다.
이미지 참조
이미지는 "type": "image"로 동일한 패턴을 씁니다.
with open("product_screenshot.png", "rb") as f:
img_obj = client.beta.files.upload(
file=("product_screenshot.png", f, "image/png"),
)
img_id = img_obj.id
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "file", "file_id": img_id},
},
{"type": "text", "text": "이 UI에서 개선할 수 있는 부분을 알려줘."},
],
}
],
betas=["files-api-2025-04-14"],
)
print(next(b.text for b in response.content if b.type == "text"))파일 생명주기 관리
업로드한 파일 목록 조회와 삭제 API도 제공됩니다.
# 목록 조회
files = client.beta.files.list()
for f in files.data:
print(f.id, f.filename, f.created_at)
# 삭제
client.beta.files.delete(file_id)스토리지 쿼터 관리를 위해 불필요한 파일은 주기적으로 정리하는 루틴을 두는 게 좋습니다. 보존 기간이나 자동 만료 관련 정책이 있는지는 공식 문서에서 최신 상태를 확인하는 편이 안전합니다.
Prompt Caching과 함께 써야 비용까지 잡힌다
솔직히 처음에 저도 헷갈렸습니다. Files API를 쓰면 토큰 비용도 같이 줄어드는 거 아닌가 싶었거든요. 그런데 아닙니다. Files API는 네트워크 전송 비용을 줄이는 거고, 입력 토큰 과금은 그대로 발생합니다. 같은 파일 내용을 매 요청마다 Claude가 처리하면 토큰 수는 동일합니다.
여기서 Prompt Caching이 들어옵니다. 자주 반복되는 콘텐츠 블록에 cache_control: {"type": "ephemeral"}을 붙여두면, 캐시 히트 시 해당 블록에 대한 입력 토큰 비용이 크게 낮아집니다. 모델별 캐시 읽기 단가와 할인율은 Anthropic 공식 가격 페이지에서 최신 값을 확인하세요. claude-sonnet-4-6의 단가와 캐시 할인율은 이전 세대 모델과 다를 수 있습니다.
두 가지를 함께 쓰는 패턴입니다. 2026년 기준 Prompt Caching은 이미 GA로 전환된 상태이므로, 별도 베타 헤더 없이 cache_control만 지정하면 됩니다(사용 시점의 SDK 릴리스 노트로 재확인 권장).
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=[
{
"type": "text",
"text": "당신은 법무 문서 분석 전문가입니다.",
"cache_control": {"type": "ephemeral"},
}
],
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": file_id},
"cache_control": {"type": "ephemeral"},
},
{"type": "text", "text": "핵심 조항을 세 줄로 요약해줘."},
],
}
],
betas=["files-api-2025-04-14"],
)Files API가 전송 계층을, Prompt Caching이 토큰 계층을 커버합니다. 두 층이 만나는 지점에서 비로소 프로덕션에서 체감할 만한 절감이 나옵니다.
배치 처리와 지식 베이스 패턴
수백 개 PDF를 일괄 처리하는 파이프라인
연구 논문 분석이나 재무 보고서 요약 자동화 같은 시나리오에서는 Message Batches API와 결합할 수 있습니다. 아래는 업로드부터 배치 제출, 결과 회수까지 이어지는 개념적 예시입니다.
import time
from pathlib import Path
import anthropic
client = anthropic.Anthropic()
# 1단계: PDF 일괄 업로드
pdf_dir = Path("./reports")
file_ids = {}
for pdf_path in pdf_dir.glob("*.pdf"):
with open(pdf_path, "rb") as f:
file_obj = client.beta.files.upload(
file=(pdf_path.name, f, "application/pdf"),
)
file_ids[pdf_path.stem] = file_obj.id
# 2단계: 각 file_id로 배치 요청 구성
requests = []
for doc_name, fid in file_ids.items():
requests.append({
"custom_id": doc_name,
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": [
{"type": "document", "source": {"type": "file", "file_id": fid}},
{"type": "text", "text": "핵심 수치와 결론을 200자 이내로 요약해줘."},
],
}
],
},
})
# 3단계: 배치 제출
batch = client.beta.messages.batches.create(
requests=requests,
betas=["files-api-2025-04-14"],
)
# 4단계: 완료 대기 및 결과 스트리밍 회수
while True:
status = client.beta.messages.batches.retrieve(batch.id)
if status.processing_status == "ended":
break
time.sleep(30)
for result in client.beta.messages.batches.results(batch.id):
if result.result.type == "succeeded":
message = result.result.message
text = next(b.text for b in message.content if b.type == "text")
print(result.custom_id, "->", text)배치 API의 요금 체계, 결과 포맷, 상태값은 Message Batches 문서에서 확인하세요.
공용 지식 베이스 어시스턴트
공통 매뉴얼이나 FAQ 문서를 한 번 업로드하고, 여러 사용자 세션이 동일한 file_id를 공유하는 패턴입니다. 문서 버전이 바뀔 때만 재업로드합니다.
KNOWLEDGE_BASE = {
"user_manual": "file_abc123",
"faq": "file_def456",
"policy": "file_ghi789",
}
def answer_support_query(user_query: str, doc_key: str) -> str:
fid = KNOWLEDGE_BASE[doc_key]
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": fid},
"cache_control": {"type": "ephemeral"},
},
{"type": "text", "text": user_query},
],
}
],
betas=["files-api-2025-04-14"],
)
return next(b.text for b in response.content if b.type == "text")트레이드오프
한눈에 보는 비교
| 항목 | 기존 Base64 인라인 방식 | Files API |
|---|---|---|
| 파일 전송 | 매 요청마다 | 최초 1회 |
| 페이로드 크기 | 파일 크기 × 요청 수 | 소형 (file_id만) |
| Base64 CPU 비용 | 매 요청마다 발생 | 없음 |
| 입력 토큰 비용 | 매 요청마다 동일 | 매 요청마다 동일 (변화 없음) |
| 세션 간 파일 공유 | 불가 | file_id로 가능 |
| 인용(Citations) | 지원 | 지원 |
| 오피스 포맷 지원 | PDF 변환 필요 | PDF 변환 필요 (동일 제약) |
| 안정성 | GA | 베타 (인터페이스 변경 가능) |
실무에서 자주 만나는 함정
"토큰 비용이 줄었겠지"라는 착각 Files API를 도입했는데 청구서가 거의 그대로라서 당황할 수 있습니다. 위에서 설명했듯, 토큰 과금은 변하지 않습니다. Prompt Caching 병행이 필수입니다.
베타 헤더 누락
betas=["files-api-2025-04-14"] 없이 요청하면 에러가 납니다. 환경별로 헤더 설정을 공통화해두면 실수를 줄일 수 있습니다.
스토리지 쿼터 소진 대용량 파일을 자주 올리는 파이프라인에서는 쿼터에 부딪힐 수 있습니다. 목록 API로 오래된 파일을 주기적으로 정리하는 잡을 두는 게 좋습니다.
멀티리전 레이턴시 파일은 업로드된 리전에 저장됩니다. 글로벌 서비스라면 사용자 위치와 파일 스토리지 리전이 다를 때 레이턴시 영향이 있을 수 있어서, 리전 설계를 미리 고려하는 편이 안전합니다.
대용량 단일 파일 파일당 크기 한도를 넘으면 논리적 분할 업로드를 고려해야 합니다. 스캔 해상도가 높은 대형 PDF가 이 케이스에 해당하기 쉽습니다.
어떤 상황에서 먼저 도입해야 하나
Files API의 이점이 실제 페이로드 감소로 이어지려면 "같은 파일을 여러 번 참조"라는 재사용 축이 필요합니다. 판단 기준을 단순화하면 이렇습니다.
간단히 정리하면, 한 세션에서 같은 문서를 서너 번 이상 참조하기 시작하는 지점부터 Files API의 네트워크 절감이 체감됩니다. 여기에 프롬프트 앞단이 안정적으로 유지된다면 Prompt Caching을 얹어서 토큰 계층까지 잡고, 오프라인 대량 처리라면 Message Batches로 마무리하는 순서가 자연스럽습니다.
베타 상태라는 점은 여전히 남아 있는 리스크입니다. 프로덕션 배포 전에 Anthropic 릴리스 노트를 확인하고, 베타 헤더와 응답 스키마 변경에 대응할 수 있는 얇은 래퍼를 한 겹 두면 나중이 편합니다.