跳转到主内容

Claude Citations API 实战:让模型自动标注引用来源,RAG 准确率提升 15%

Claude Citations 让模型在回答中精确标注引用的文档片段,避免幻觉、便于审计。本文给出 PDF/Plain text/Custom 三种文档形式的完整代码,对比 Citations 与手写 prompt 引用的差异,并讲清楚 cited_text 不计费的省钱机制。

开发指南CitationsRAGcited_text反幻觉法律合规预计阅读10分钟
2026.05.20 发表
Claude Citations API 实战:让模型自动标注引用来源,RAG 准确率提升 15%

Claude Citations API 实战:让模型自动标注引用来源,RAG 准确率提升 15%

做 RAG(检索增强生成)的工程师都遇到过这种灵魂提问:

“你这个回答到底是从哪段文档里得出来的?”

这个问题之所以致命,是因为模型会自信地引用一段根本不存在的原文。在法律、医疗、金融、合规审计这些场景,引用错了不只是难看,可能直接出事故。

过去大家的解法是在 prompt 里写"请用方括号标注你引用了哪段文字",然后正则解析。这种方式至少有三个问题:

  1. 模型会编造看似真实但实际不存在的引用编号
  2. 直接输出原文片段,output token 飙升导致成本翻倍
  3. 解析逻辑脆弱,模型偶尔不按格式输出就全崩

Anthropic 在 2025 年初推出、2026 年正式 GA 的 Citations API 给出了官方答案——模型直接返回结构化的引用对象,包含字符级偏移、文档索引、原文片段,全部由 API 层保证。

本文给出 Citations 的完整接入方案、三种文档喂入方式、与传统 prompt 引用的对比、以及一个真实可跑的法律 RAG 例子。


一、Citations 到底是什么

Citations 是 Anthropic 在 messages API 上的一个文档块开关。你把文档作为 document content block 传给 Claude,并打开 citations.enabled = true,模型回答时就会自动在每个论点后附带一个citation 对象,结构如下:

{
  "type": "char_location",
  "cited_text": "...被引用的原文片段...",
  "document_index": 0,
  "document_title": "annual-report-2025.pdf",
  "start_char_index": 1024,
  "end_char_index": 1180
}
{
  "type": "char_location",
  "cited_text": "...被引用的原文片段...",
  "document_index": 0,
  "document_title": "annual-report-2025.pdf",
  "start_char_index": 1024,
  "end_char_index": 1180
}

四个关键属性:

字段 含义
cited_text 被引用的原文片段,不计入 output token
document_index 第几个文档(0-indexed)
start_char_index / end_char_index 在该文档中的字符偏移
type 引用粒度类型(见下表)

三种引用粒度

粒度 适用场景 类型字段
char_location 纯文本文档(最精确) char_location
page_location PDF 文档(页级) page_location
content_block_location 自定义文档块(最灵活) content_block_location

为什么比 prompt 工程更靠谱

Anthropic 自家的对比评测里,Citations 内置版相比 prompt-engineered 版本在召回准确率上提升最高 15%。原因有三个:

  1. 引用指向是 API 强制保证的——start_char_index/end_char_index 由系统计算,不是模型生成的字符串
  2. cited_text 不计入 output token——你不再需要为了让模型"输出原文"而支付额外的 token 费用
  3. 模型不会再编造——所有 citation 都映射到你提供文档的真实位置

二、三种文档喂入方式

方式 1:Plain Text(字符精确引用)

最适合:你已经把文档解析为纯文本,需要字符级精确定位。

import anthropic

client = anthropic.Anthropic(
    api_key="sk-你的ClaudeAPI密钥",
    base_url="https://gw.claudeapi.com"
)

doc_text = open("contract.txt", "r", encoding="utf-8").read()

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "text",
                    "media_type": "text/plain",
                    "data": doc_text,
                },
                "title": "甲方乙方合作协议-2026.txt",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "这份合同的违约金条款是怎么规定的?"
            }
        ]
    }]
)
import anthropic

client = anthropic.Anthropic(
    api_key="sk-你的ClaudeAPI密钥",
    base_url="https://gw.claudeapi.com"
)

doc_text = open("contract.txt", "r", encoding="utf-8").read()

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "text",
                    "media_type": "text/plain",
                    "data": doc_text,
                },
                "title": "甲方乙方合作协议-2026.txt",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "这份合同的违约金条款是怎么规定的?"
            }
        ]
    }]
)

返回结构(精简版):

for block in response.content:
    if block.type == "text":
        print(block.text)
        for cit in block.citations or []:
            print(f"  └─ 引自 {cit.document_title}")
            print(f"     字符 [{cit.start_char_index}:{cit.end_char_index}]")
            print(f"     原文:{cit.cited_text[:80]}...")
for block in response.content:
    if block.type == "text":
        print(block.text)
        for cit in block.citations or []:
            print(f"  └─ 引自 {cit.document_title}")
            print(f"     字符 [{cit.start_char_index}:{cit.end_char_index}]")
            print(f"     原文:{cit.cited_text[:80]}...")

输出示例:

违约方需向守约方支付合同总金额 20% 的违约金,并在 30 日内一次性支付。
  └─ 引自 甲方乙方合作协议-2026.txt
     字符 [4128:4280]
     原文:第十二条 违约责任:违约方应当向守约方支付合同总金额百分之二十的违约金...
违约方需向守约方支付合同总金额 20% 的违约金,并在 30 日内一次性支付。
  └─ 引自 甲方乙方合作协议-2026.txt
     字符 [4128:4280]
     原文:第十二条 违约责任:违约方应当向守约方支付合同总金额百分之二十的违约金...

方式 2:PDF(页级引用)

最适合:扫描件、含图表的财报、合同原件。模型会自动 OCR + 视觉理解,引用粒度精确到 PDF 的页号。

import base64

with open("annual-report.pdf", "rb") as f:
    pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=2048,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "base64",
                    "media_type": "application/pdf",
                    "data": pdf_b64,
                },
                "title": "财报-2025.pdf",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "现金流量表中经营性净现金流是多少?"
            }
        ]
    }]
)
import base64

with open("annual-report.pdf", "rb") as f:
    pdf_b64 = base64.standard_b64encode(f.read()).decode("utf-8")

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=2048,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "base64",
                    "media_type": "application/pdf",
                    "data": pdf_b64,
                },
                "title": "财报-2025.pdf",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "现金流量表中经营性净现金流是多少?"
            }
        ]
    }]
)

PDF 模式下 citation 会带 start_page_number / end_page_number(1-indexed):

经营性净现金流为 28.5 亿元,同比增长 12%。
  └─ 引自 财报-2025.pdf 第 32-32 页
经营性净现金流为 28.5 亿元,同比增长 12%。
  └─ 引自 财报-2025.pdf 第 32-32 页

如果文档已经上传到 Files API,可以用 file_id 引用,详见 Files API 完全指南

方式 3:Custom Content(最灵活,RAG 必选)

最适合:你自己分了块的 RAG 场景,想精确控制每个 chunk 的边界。

chunks = [
    "第一条 本协议由甲方与乙方于 2026 年 1 月 1 日签订...",
    "第十二条 违约责任:违约方应当向守约方支付合同总金额百分之二十的违约金...",
    "第十五条 争议解决:因本协议产生的任何争议..."
]

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "content",
                    "content": [
                        {"type": "text", "text": chunk} for chunk in chunks
                    ]
                },
                "title": "合同条款-命中片段",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "违约金有多少?争议怎么解决?"
            }
        ]
    }]
)
chunks = [
    "第一条 本协议由甲方与乙方于 2026 年 1 月 1 日签订...",
    "第十二条 违约责任:违约方应当向守约方支付合同总金额百分之二十的违约金...",
    "第十五条 争议解决:因本协议产生的任何争议..."
]

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "document",
                "source": {
                    "type": "content",
                    "content": [
                        {"type": "text", "text": chunk} for chunk in chunks
                    ]
                },
                "title": "合同条款-命中片段",
                "citations": {"enabled": True}
            },
            {
                "type": "text",
                "text": "违约金有多少?争议怎么解决?"
            }
        ]
    }]
)

返回的 citation 会带 start_block_index / end_block_index,告诉你引用的是数组里第几块。这种模式特别适合:

  • 向量检索后给 top-K 个 chunk:每个 chunk 是一个 block,模型回答时直接告诉你它引用了哪个 block
  • 多文档汇总:可以把不同来源的文档片段合并成一个 document,引用结构清晰

三、Node.js / cURL 等价实现

Node.js / TypeScript

import Anthropic from "@anthropic-ai/sdk";
import fs from "fs";

const client = new Anthropic({
  apiKey: "sk-你的ClaudeAPI密钥",
  baseURL: "https://gw.claudeapi.com",
});

const docText = fs.readFileSync("contract.txt", "utf-8");

const response = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  messages: [{
    role: "user",
    content: [
      {
        type: "document",
        source: { type: "text", media_type: "text/plain", data: docText },
        title: "甲方乙方合作协议.txt",
        citations: { enabled: true },
      },
      { type: "text", text: "违约金条款是?" }
    ]
  }]
});

for (const block of response.content) {
  if (block.type === "text") {
    console.log(block.text);
    for (const cit of (block as any).citations || []) {
      console.log(`  └─ ${cit.document_title} [${cit.start_char_index}:${cit.end_char_index}]`);
    }
  }
}
import Anthropic from "@anthropic-ai/sdk";
import fs from "fs";

const client = new Anthropic({
  apiKey: "sk-你的ClaudeAPI密钥",
  baseURL: "https://gw.claudeapi.com",
});

const docText = fs.readFileSync("contract.txt", "utf-8");

const response = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  messages: [{
    role: "user",
    content: [
      {
        type: "document",
        source: { type: "text", media_type: "text/plain", data: docText },
        title: "甲方乙方合作协议.txt",
        citations: { enabled: true },
      },
      { type: "text", text: "违约金条款是?" }
    ]
  }]
});

for (const block of response.content) {
  if (block.type === "text") {
    console.log(block.text);
    for (const cit of (block as any).citations || []) {
      console.log(`  └─ ${cit.document_title} [${cit.start_char_index}:${cit.end_char_index}]`);
    }
  }
}

cURL

curl https://gw.claudeapi.com/v1/messages \
  -H "x-api-key: sk-你的ClaudeAPI密钥" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-4-7",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": [
        {
          "type": "document",
          "source": {"type": "text", "media_type": "text/plain", "data": "本协议自 2026 年 1 月 1 日生效..."},
          "title": "合同.txt",
          "citations": {"enabled": true}
        },
        {"type": "text", "text": "协议什么时候生效?"}
      ]
    }]
  }'
curl https://gw.claudeapi.com/v1/messages \
  -H "x-api-key: sk-你的ClaudeAPI密钥" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-4-7",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": [
        {
          "type": "document",
          "source": {"type": "text", "media_type": "text/plain", "data": "本协议自 2026 年 1 月 1 日生效..."},
          "title": "合同.txt",
          "citations": {"enabled": true}
        },
        {"type": "text", "text": "协议什么时候生效?"}
      ]
    }]
  }'

四、Citations vs Prompt 工程引用:六维对比

维度 传统 Prompt 引用 Citations API
引用可靠性 模型可能编造 API 保证位置真实
Output token 成本 引用文本计入输出(昂贵) cited_text 不计费
字符级偏移 需自己解析 内置 start_char_index
多文档支持 需要复杂 prompt 描述 自动 document_index
实现复杂度 自写正则解析 SDK 直接拿对象
召回准确率 基线 +15%(Anthropic 内测数据)

对法律、医疗、金融、政府类客户,这六个维度里前两个就足够说服技术选型。


五、Web Search 自带 Citations(不需要开关)

如果你用的是 Anthropic 原生 web_search 工具(web_search_20260209 是 2026 年的最新版本,支持 dynamic filtering),citations 是默认强开的——你不需要在文档块里加 citations.enabled,每个网页结果都会自带 web_search_result_location 类型的 citation。

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=2048,
    tools=[{"type": "web_search_20260209", "name": "web_search"}],
    messages=[{"role": "user", "content": "2026 年 Anthropic 估值是多少?给出来源。"}]
)
response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=2048,
    tools=[{"type": "web_search_20260209", "name": "web_search"}],
    messages=[{"role": "user", "content": "2026 年 Anthropic 估值是多少?给出来源。"}]
)

Anthropic 的服务条款明确:直接把 API 输出展示给最终用户时,必须保留 citation 指向原始来源——这条对做搜索/问答类产品的团队尤其重要,别因为美观需求把 url 字段藏起来。

cited_text、title、url 字段同样不计入 token 计费


六、踩坑清单

坑 1:Citations 与 streaming 一起用要小心累积。 流式返回时,每个 text delta 可能携带 citation_delta,需要在客户端按 block 累积,不能简单拼接字符串。SDK 已封装好,自写客户端要注意。

坑 2:document 必须有 title。 实测不写 title 也能跑通,但所有 citation 的 document_title 字段会为空,前端展示会很难看。养成传 title 的习惯

坑 3:Custom content 模式下,block 太碎会影响召回。 把一份合同拆成 200 个 8 字 block,模型反而会"看不清"。建议每个 block 保持 100-500 字的语义完整段落,太碎反而降低准确率。

坑 4:不是所有模型都返回引用粒度一致。 Opus 4.7 / Sonnet 4.6 / Haiku 4.5 都支持 Citations,但 Haiku 在多文档复杂场景下偶尔会漏引用。重要场景请用 Opus 4.7 或 Sonnet 4.6。

坑 5:Citations 不等于"防幻觉"。 模型仍然可能误读文档语义。Citations 只保证"引用位置真实存在",不保证"引用解读正确"。生产环境建议二次校验:把 cited_text 和模型回答做语义一致性检查。

坑 6:国内直连 api.anthropic.com 不通。base_url 改成 https://gw.claudeapi.com 即可使用 Citations 全部能力,无需额外配置。


七、一个完整的法律 RAG 端到端示例

把上面的 Custom Content 模式串起来,做一个最小可跑的法律咨询助手:

import anthropic
from typing import List

client = anthropic.Anthropic(
    api_key="sk-你的ClaudeAPI密钥",
    base_url="https://gw.claudeapi.com"
)

# 假设你有一个向量数据库 (Pinecone / Weaviate / Chroma)
def retrieve_chunks(query: str, k: int = 5) -> List[dict]:
    """返回 top-k 法条片段"""
    # ... 你的向量检索代码
    return [
        {"law": "民法典", "article": "第585条", "text": "当事人可以约定一方违约时..."},
        {"law": "合同法", "article": "第114条", "text": "约定的违约金低于造成的损失..."},
        # ...
    ]

def answer_with_citations(question: str):
    chunks = retrieve_chunks(question)

    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=2048,
        system="你是一个严谨的法律咨询助手。回答必须仅基于提供的法条片段,不允许引用未提供的法律。",
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "document",
                    "source": {
                        "type": "content",
                        "content": [
                            {"type": "text",
                             "text": f"【{c['law']} {c['article']}{c['text']}"}
                            for c in chunks
                        ]
                    },
                    "title": "命中的法律条文",
                    "citations": {"enabled": True}
                },
                {"type": "text", "text": question}
            ]
        }]
    )

    # 结构化输出,附带引用
    result = {"answer": "", "citations": []}
    for block in response.content:
        if block.type == "text":
            result["answer"] += block.text
            for cit in (block.citations or []):
                idx = cit.start_block_index
                result["citations"].append({
                    "law": chunks[idx]["law"],
                    "article": chunks[idx]["article"],
                    "cited_text": cit.cited_text,
                })
    return result

print(answer_with_citations("约定违约金过低能否调整?"))
import anthropic
from typing import List

client = anthropic.Anthropic(
    api_key="sk-你的ClaudeAPI密钥",
    base_url="https://gw.claudeapi.com"
)

# 假设你有一个向量数据库 (Pinecone / Weaviate / Chroma)
def retrieve_chunks(query: str, k: int = 5) -> List[dict]:
    """返回 top-k 法条片段"""
    # ... 你的向量检索代码
    return [
        {"law": "民法典", "article": "第585条", "text": "当事人可以约定一方违约时..."},
        {"law": "合同法", "article": "第114条", "text": "约定的违约金低于造成的损失..."},
        # ...
    ]

def answer_with_citations(question: str):
    chunks = retrieve_chunks(question)

    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=2048,
        system="你是一个严谨的法律咨询助手。回答必须仅基于提供的法条片段,不允许引用未提供的法律。",
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "document",
                    "source": {
                        "type": "content",
                        "content": [
                            {"type": "text",
                             "text": f"【{c['law']} {c['article']}{c['text']}"}
                            for c in chunks
                        ]
                    },
                    "title": "命中的法律条文",
                    "citations": {"enabled": True}
                },
                {"type": "text", "text": question}
            ]
        }]
    )

    # 结构化输出,附带引用
    result = {"answer": "", "citations": []}
    for block in response.content:
        if block.type == "text":
            result["answer"] += block.text
            for cit in (block.citations or []):
                idx = cit.start_block_index
                result["citations"].append({
                    "law": chunks[idx]["law"],
                    "article": chunks[idx]["article"],
                    "cited_text": cit.cited_text,
                })
    return result

print(answer_with_citations("约定违约金过低能否调整?"))

输出结构示例:

{
  "answer": "根据法律规定,约定违约金低于造成损失的,可以请求法院或仲裁机构予以增加。",
  "citations": [
    {
      "law": "合同法",
      "article": "第114条",
      "cited_text": "约定的违约金低于造成的损失的,当事人可以请求人民法院或者仲裁机构予以增加..."
    }
  ]
}
{
  "answer": "根据法律规定,约定违约金低于造成损失的,可以请求法院或仲裁机构予以增加。",
  "citations": [
    {
      "law": "合同法",
      "article": "第114条",
      "cited_text": "约定的违约金低于造成的损失的,当事人可以请求人民法院或者仲裁机构予以增加..."
    }
  ]
}

这样的输出可以直接喂给前端做"鼠标悬停展示原文"或者"导出审计报告",工程友好度远超 prompt 工程版本。


八、为什么用 ClaudeAPI.com 跑 Citations

Citations 是 Anthropic API 原生能力,因此你必须用 Anthropic 原生路径——OpenAI 兼容接口(chat/completions)不支持 Citations。

国内开发者通过 claudeapi.com 接入的优势:

维度 说明
协议兼容 100% 兼容 Anthropic 原生 messages API,Citations、Files、Batch 全部支持
接入点 https://gw.claudeapi.com,国内直连,免梯子
计费 人民币结算,支付宝/微信,按 token 量按需付费
模型 Citations 推荐的 Opus 4.7 / Sonnet 4.6 同价同步

定价参考 claudeapi.com 价格页。RAG 场景常用 Sonnet 4.6(输入 4 元/M,输出 20 元/M),Citations 的 cited_text 不计费这点尤其能省钱。


小结

Citations 是 Anthropic 官方对"防幻觉 + 可审计"需求的标准答案,比 prompt 工程引用更可靠、更省钱、更易工程化:

  • 三种粒度:char_location(纯文本)、page_location(PDF)、content_block_location(自定义 chunk)
  • 不计费的省钱细节:cited_text 不计入 output token
  • 生产建议:重要场景用 Opus 4.7 / Sonnet 4.6,document 一定要传 title,custom block 保持语义完整段落

国内开发者通过 claudeapi.combase_url 改成 https://gw.claudeapi.com 即可使用全部 Citations 能力,注册支付宝充值起步只需几分钟。


参考资料:Citations API 官方文档Anthropic Citations 发布博客Web Search Tool

相关文章