Claude API PDF 文档问答实战:从原生解析到分页引用的完整方案
拿到一份 60 页的供应商合同,老板要你"半小时内告诉我违约条款怎么写、付款节点几个、有没有自动续约"。你打开 PDF 想用 Ctrl+F,发现合同里关键条款都用了"该方"、"前述事项"这种含糊指代——搜索失效。
这种场景用 Claude API 是甜区。Claude 原生支持 PDF 输入,不需要先跑 OCR、不需要先切 chunk、不需要自建 RAG。但很多人接进来后还是踩坑:100 页的硬上限、32MB 请求体限制、citations 用了但定位不到原页、同一份文档每次都重新上传烧 token。
本文用一份合同问答的真实流程串起来,把 PDF 上传的四种方式、citations 分页引用、prompt caching 复用、超长文档切分都讲清楚,给出可以直接复制运行的代码。所有示例使用 claudeapi.com 的国内接入点,避免 api.anthropic.com 被防火长城拦截的超时问题。
一、Claude 的 PDF 支持到底是怎么回事
很多人以为 Claude 的 PDF 处理是"后台跑 OCR 提取文字再喂给模型"——不是。Claude 把 PDF 当作视觉 + 文本的混合输入:每一页既被作为图像理解(保留版式、表格、签章、手写笔迹),也提取出文本层(可选择性引用具体文字段落)。这一点对合同、财报、论文这种"版式承载语义"的文档至关重要——表头、签字栏、脚注、跨页表格都能被读懂。
代价是单页消耗比纯文本高得多。一份 100 页的 PDF,文本量可能只有 3 万 token,但当成 PDF 输入会消耗 7-10 万 token(每页约 700-1000 token,含图像 token)。所以不要为了"省事"就把已经是纯文本的内容塞成 PDF,能用 text/markdown 喂的就别用 PDF 喂。
硬限制(截至 2026-05)
| 限制项 | 数值 | 来源 |
|---|---|---|
| 单份 PDF 最大页数 | 100 页 | Anthropic 官方文档 |
| Messages API 请求体上限 | 32 MB | 标准同步接口 |
| Batch API 请求体上限 | 256 MB | 异步批处理 |
| Files API 单文件上限 | 500 MB | 文件持久化接口 |
| 组织存储总上限 | 100 GB | Files API 全局配额 |
100 页是模型层的硬约束,不是网关的限制——换 endpoint 解决不了。超过 100 页必须自己切分,本文第四节给出处理方案。
数据来源:Anthropic PDF support、Files API docs。
二、四种 PDF 上传方式怎么选
Claude API 提供四种把 PDF 喂进模型的姿势,差异主要在单次开销、复用成本、适用规模。
| 方式 | 适用场景 | 单文件上限 | 跨请求复用 | 推荐度 |
|---|---|---|---|---|
| Base64 内联 | 一次性问答、文档 < 10 MB | 32 MB(含整个请求体) | 不能 | 临时验证 |
| URL 引用 | PDF 已托管在可公网访问的地址 | 32 MB | 不能 | 一次性场景 |
| Files API + file_id | 同一份文档反复问答、多用户共享 | 500 MB | 能 | 生产首选 |
| Batch API | 批量处理多份文档、可接受异步 | 256 MB | 不能 | 离线分析 |
90% 的生产场景应该用 Files API:先上传一次拿 file_id,后续每次问答只引用 ID,省掉重复的网络传输和 base64 编码开销。下面分别给出最常用的两种姿势。
2.1 base64 内联(适合一次性问答)
import anthropic
import base64
client = anthropic.Anthropic(
api_key="sk-你的ClaudeAPI密钥",
base_url="https://gw.claudeapi.com"
)
with open("contract.pdf", "rb") as f:
pdf_data = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": pdf_data,
},
},
{
"type": "text",
"text": "这份合同的违约责任在第几条?逐条列出关键义务。",
},
],
}
],
)
print(response.content[0].text)
import anthropic
import base64
client = anthropic.Anthropic(
api_key="sk-你的ClaudeAPI密钥",
base_url="https://gw.claudeapi.com"
)
with open("contract.pdf", "rb") as f:
pdf_data = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": pdf_data,
},
},
{
"type": "text",
"text": "这份合同的违约责任在第几条?逐条列出关键义务。",
},
],
}
],
)
print(response.content[0].text)
注意:base64 编码会让数据体积膨胀约 33%,所以原始 PDF 接近 24 MB 时就要小心 32 MB 请求体上限。
2.2 Files API 上传 + file_id 复用(生产首选)
第一步:上传文件,拿到 file_id。
import anthropic
client = anthropic.Anthropic(
api_key="sk-你的ClaudeAPI密钥",
base_url="https://gw.claudeapi.com",
default_headers={"anthropic-beta": "files-api-2025-04-14"},
)
with open("contract.pdf", "rb") as f:
uploaded = client.beta.files.upload(
file=("contract.pdf", f, "application/pdf")
)
print(uploaded.id)
# 形如 file_011CNha8iCJcU1wXNR6q4V8w
import anthropic
client = anthropic.Anthropic(
api_key="sk-你的ClaudeAPI密钥",
base_url="https://gw.claudeapi.com",
default_headers={"anthropic-beta": "files-api-2025-04-14"},
)
with open("contract.pdf", "rb") as f:
uploaded = client.beta.files.upload(
file=("contract.pdf", f, "application/pdf")
)
print(uploaded.id)
# 形如 file_011CNha8iCJcU1wXNR6q4V8w
第二步:后续所有问答都引用这个 ID,不再传文件本身。
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "file",
"file_id": "file_011CNha8iCJcU1wXNR6q4V8w",
},
},
{
"type": "text",
"text": "付款节点有几个?分别是哪一条款?",
},
],
}
],
)
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "file",
"file_id": "file_011CNha8iCJcU1wXNR6q4V8w",
},
},
{
"type": "text",
"text": "付款节点有几个?分别是哪一条款?",
},
],
}
],
)
关键提醒:Files API 仍在 beta 阶段,必须带上 anthropic-beta: files-api-2025-04-14 头部。文件持久保留直到调用 DELETE /v1/files/{file_id} 显式删除,不用了记得清理释放 100 GB 配额。
2.3 Node.js 版本
import Anthropic, { toFile } from "@anthropic-ai/sdk";
import { createReadStream } from "node:fs";
const client = new Anthropic({
apiKey: "sk-你的ClaudeAPI密钥",
baseURL: "https://gw.claudeapi.com",
defaultHeaders: { "anthropic-beta": "files-api-2025-04-14" },
});
const uploaded = await client.beta.files.upload({
file: await toFile(createReadStream("contract.pdf"), "contract.pdf", {
type: "application/pdf",
}),
});
const response = await client.messages.create({
model: "claude-opus-4-7",
max_tokens: 4096,
messages: [
{
role: "user",
content: [
{ type: "document", source: { type: "file", file_id: uploaded.id } },
{ type: "text", text: "这份合同的违约责任在第几条?" },
],
},
],
});
console.log(response.content[0].text);
import Anthropic, { toFile } from "@anthropic-ai/sdk";
import { createReadStream } from "node:fs";
const client = new Anthropic({
apiKey: "sk-你的ClaudeAPI密钥",
baseURL: "https://gw.claudeapi.com",
defaultHeaders: { "anthropic-beta": "files-api-2025-04-14" },
});
const uploaded = await client.beta.files.upload({
file: await toFile(createReadStream("contract.pdf"), "contract.pdf", {
type: "application/pdf",
}),
});
const response = await client.messages.create({
model: "claude-opus-4-7",
max_tokens: 4096,
messages: [
{
role: "user",
content: [
{ type: "document", source: { type: "file", file_id: uploaded.id } },
{ type: "text", text: "这份合同的违约责任在第几条?" },
],
},
],
});
console.log(response.content[0].text);
2.4 cURL 验证
# 上传
curl https://gw.claudeapi.com/v1/files \
-H "x-api-key: sk-你的ClaudeAPI密钥" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14" \
-F "[email protected];type=application/pdf"
# 引用 file_id 提问
curl https://gw.claudeapi.com/v1/messages \
-H "x-api-key: sk-你的ClaudeAPI密钥" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-7",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": [
{"type": "document", "source": {"type": "file", "file_id": "file_xxx"}},
{"type": "text", "text": "概述本文档的核心条款"}
]
}]
}'
# 上传
curl https://gw.claudeapi.com/v1/files \
-H "x-api-key: sk-你的ClaudeAPI密钥" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14" \
-F "[email protected];type=application/pdf"
# 引用 file_id 提问
curl https://gw.claudeapi.com/v1/messages \
-H "x-api-key: sk-你的ClaudeAPI密钥" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: files-api-2025-04-14" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-7",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": [
{"type": "document", "source": {"type": "file", "file_id": "file_xxx"}},
{"type": "text", "text": "概述本文档的核心条款"}
]
}]
}'
三、开启 citations:让答案带页码出处
合同问答最怕"模型说有就有"——读者无法核验。Claude 的 citations 功能强制让答案中的每个事实片段附带原文出处(页码 + 文本块),把"信我"变成"看原文"。
只需要在 document 块加一个 citations: {"enabled": true}:
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "file_xxx"},
"citations": {"enabled": True},
},
{
"type": "text",
"text": "请逐条列出付款节点,并标注原文位置。",
},
],
}
],
)
for block in response.content:
if block.type == "text":
print(block.text)
if block.citations:
for c in block.citations:
print(f" ↳ 出处: 第 {c.start_page_number}-{c.end_page_number} 页")
print(f" 原文片段: {c.cited_text[:80]}...")
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "file_xxx"},
"citations": {"enabled": True},
},
{
"type": "text",
"text": "请逐条列出付款节点,并标注原文位置。",
},
],
}
],
)
for block in response.content:
if block.type == "text":
print(block.text)
if block.citations:
for c in block.citations:
print(f" ↳ 出处: 第 {c.start_page_number}-{c.end_page_number} 页")
print(f" 原文片段: {c.cited_text[:80]}...")
输出会形如:
首付款节点为合同签署后 7 个工作日内,金额为合同总价的 30%。
↳ 出处: 第 12-12 页
原文片段: 甲方应于本合同签订之日起七(7)个工作日内,向乙方支付合同总金额的百分之三十...
首付款节点为合同签署后 7 个工作日内,金额为合同总价的 30%。
↳ 出处: 第 12-12 页
原文片段: 甲方应于本合同签订之日起七(7)个工作日内,向乙方支付合同总金额的百分之三十...
业务侧的好处:前端可以直接把页码渲染成跳转链接,用户点击就翻到原文位置——这套体验在法律、金融、医疗文档问答里属于标配。
四、超过 100 页怎么办
100 页是模型硬上限,但实战中合同附件、年报、技术规范动辄两三百页。处理思路有三种,按优先级排序:
思路一:语义切分,而不是机械切 100 页
简单粗暴地按页切会把同一条款劈成两半,导致问答时上下文断裂。更好的做法是按章节、附件、目录边界切——先用一次便宜的调用让 Sonnet 输出"目录结构 + 每章起止页",再按章节切。代价是多一次调用,但单文档只切一次,后续都按 file_id 复用。
思路二:Batch API + 索引文件
把 PDF 切成多份(每份 ≤ 100 页),都通过 Files API 上传,维护一个 {section_name: file_id} 的本地索引。提问时先用一次轻量调用让 Haiku 4.5 判断"这个问题落在哪一章",再只调用对应章节的 file_id。这种"路由 + 精确召回"的方式比把整文档塞给模型省 70% 以上 token。
思路三:RAG 作为兜底
如果文档结构完全无规律(扫描件、混合文档),或者要做跨文档检索,还是要回到经典 RAG——把每页跑 embedding 入向量库,问答时召回 top-k 页再让 Claude 阅读。Files API 不替代 RAG,而是让"小规模文档问答"省掉建索引的成本。
五、Prompt Caching:把重复读取的成本砍到 1/10
如果同一份合同会被反复提问(法务团队场景),每次都把 PDF 内容塞进上下文会非常烧钱。开启 prompt caching 后,文档内容的 token 只在首次问答时全价计费,后续 5 分钟内的复用按 1/10 计价。
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "file_xxx"},
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": "违约责任条款在第几页?",
},
],
}
],
)
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=4096,
extra_headers={"anthropic-beta": "files-api-2025-04-14"},
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "file_xxx"},
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": "违约责任条款在第几页?",
},
],
}
],
)
注意:cache_control 放在 document 块上,不是 text 上。cache 的最小命中粒度是 1024 token,普通 PDF 一页几百 token,放心整页缓存。
六、5 个高频踩坑
坑 1:把 PDF 里的扫描件当文字 PDF 处理
如果 PDF 是扫描件(纯图片,无文本层),Claude 仍能读懂(用视觉能力),但页面 token 消耗会更高,且对小字体、低分辨率扫描件准确率下降。生产环境建议先用 pdftotext -layout 试一下,确认文本层质量再决定是用 PDF 输入还是先 OCR 再喂文本。
坑 2:base64 编码后忘了算 33% 膨胀
24 MB 的 PDF 经过 base64 编码变成约 32 MB,正好顶到 Messages API 上限。文件超过 20 MB 就直接走 Files API,不要纠结。
坑 3:Files API 没带 beta header
{"error": {"type": "invalid_request_error", "message": "..."}}
{"error": {"type": "invalid_request_error", "message": "..."}}
90% 的概率是漏了 anthropic-beta: files-api-2025-04-14 头部。Python SDK 用 default_headers 或 extra_headers,Node.js 用 defaultHeaders。
坑 4:同一个 PDF 反复上传导致存储爆炸
100 GB 配额看着大,但每个用户每次问答都上传一次的话很快就爆。生产环境应该用文件 hash(SHA-256)做去重,本地维护 {file_hash: file_id} 映射,同一份文件只上传一次。
坑 5:citations 启用但前端不渲染
模型已经返回了 citations,但接业务侧只取 text 字段,等于白花 token。citations 一旦启用就要在前端把"段落 ↔ 页码"的可视化做出来,否则关掉省钱。
七、模型选型建议
| 场景 | 推荐模型 | 输入价 / 输出价 | 理由 |
|---|---|---|---|
| 合同/法律/财报问答 | Opus 4.7 | ¥20 / ¥100 (每 M token) | 长上下文推理强,容错低 |
| 普通报告/论文/手册问答 | Sonnet 4.6 | ¥4 / ¥20 | 性价比最高,90% 场景够用 |
| 简单抽取(发票/表单) | Haiku 4.5 | ¥1 / ¥5 | 极速且便宜,适合结构化抽取 |
| 跨章节路由判断 | Haiku 4.5 | ¥1 / ¥5 | 第四节"思路二"里的路由调用 |
价格来自 claudeapi.com 当前定价,按量计费,无最低消费。
小结
PDF 问答的工程要点就三条:用 Files API 做持久化(避免重复上传)、开启 citations(让答案可核验)、对超 100 页文档做语义切分(而不是机械切片)。剩下的 prompt caching、模型选型都是在这三个骨架上做优化。
国内开发者直接调用 Anthropic 官方 API 会被防火长城拦截,推荐通过 claudeapi.com 接入:base_url 替换为 https://gw.claudeapi.com 即可,SDK 代码完全不动,支持支付宝/微信充值,人民币结算可开票。控制台地址 console.claudeapi.com,Files API 配额、调用日志、token 消耗都能实时看到。



