跳转到主内容

Claude API PDF 文档问答实战:从原生解析到分页引用的完整方案

拿到一份几十页的合同、年报或论文,怎么用 Claude API 做问答?本文给出从 PDF 上传、原生视觉解析、分页 citations、prompt caching 复用到超 100 页文档切分的完整工程方案,附 Python + Node.js + cURL 三套可跑通代码

开发指南PDF合同问答文档问答预计阅读10分钟
2026.05.23 发表
Claude API PDF 文档问答实战:从原生解析到分页引用的完整方案

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 supportFiles 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_headersextra_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 消耗都能实时看到。

相关文章