Long Documents, Multimodal Input, and a First Look at the API: Using Large Context, Saving Money With Prompt Caching, PDF and Image Input, Citation-Backed Answers; a Minimal Messages API Call
Make your first code call to Claude: feed it a whole PDF and get back a summary with page-numbered citations, then use prompt caching to cut the cost of repeated questions by an order of magnitude.
今日目标
- 能用官方 SDK 发出一次 Messages API 调用,并读懂返回里的内容块与 usage
- 能把 PDF 或图片作为内容块传给 Claude,并开启 citations 拿到带页码的引用
- 能说清 prompt caching 省在哪、什么情况下反而不省,并从 usage 字段验证缓存是否命中
昨天学的是怎么跟搭档交代事情。今天开始给它递材料——而且是一整本几十页的材料。读完做完实验,回到顶部勾掉三条目标。
小白版讲解
Messages API 的最小调用:一次请求里有哪几样东西
昨天的代码已经悄悄用过一次 API,今天正式拆开看。你和搭档的每一次交流,在 API 层面就是一次对 Messages API 的 HTTP 请求。请求里最少有四样东西:用哪个模型(model)、最多让它说多少(max_tokens)、名片(system,可选)、对话记录(messages)。响应回来也不是一段文字,而是一个内容块数组(content),每个块有自己的 type:文本是 text,模型要调工具时是 tool_use,开了思考时还会有 thinking 块。你要的那句话,是数组里 type 为 text 的块。
响应末尾还挂着一个 usage 对象,记录这次请求进了多少输入 token、出了多少输出 token。这是今天最重要的字段,因为今天讲的每一个技巧,最后都要靠它来验证「到底省了没有」。先把最小调用写出来,把 usage 打印出来看一眼:
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic() // 读 ANTHROPIC_API_KEY
const res = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 512,
system: '你是一名耐心的技术写作者,回答不超过三句话。',
messages: [{ role: 'user', content: '一句话解释什么是上下文窗口。' }],
})
// content 是块数组,不是字符串
const text = res.content
.filter((b) => b.type === 'text')
.map((b) => b.text)
.join('')
console.log(text)
// usage 是今天的主角:每个技巧最后都靠它验证
console.log(`input=${res.usage.input_tokens} output=${res.usage.output_tokens}`)
console.log(`stop_reason=${res.stop_reason}`) // end_turn / max_tokens / tool_use …import anthropic
client = anthropic.Anthropic() # 读 ANTHROPIC_API_KEY
res = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
system="你是一名耐心的技术写作者,回答不超过三句话。",
messages=[{"role": "user", "content": "一句话解释什么是上下文窗口。"}],
)
# content 是块列表,不是字符串
text = "".join(b.text for b in res.content if b.type == "text")
print(text)
# usage 是今天的主角:每个技巧最后都靠它验证
print(f"input={res.usage.input_tokens} output={res.usage.output_tokens}")
print(f"stop_reason={res.stop_reason}") # end_turn / max_tokens / tool_use …两个细节现在就记住。第一,stop_reason 告诉你模型为什么停:end_turn 是说完了,max_tokens 是被你设的上限截断了——摘要被截断一半却当成完整结果,是新手最常见的静默错误。第二,max_tokens 不要抠得太小,它是硬上限不是目标长度,模型不会因为你给了 512 就把话说得更精炼,只会被剪断。
大上下文不等于免费:窗口越大,越要算每次请求搬了多少
Claude 当前主力模型的上下文窗口(context window)是一百万 token 级别,一份几十页的 PDF、几万行代码整本塞进去都放得下。很多人第一反应是「那就不用管长度了」——这是今天要纠正的第一个直觉。
回到搭档的类比:你给他一箱材料,他每回答你一个问题,都得把整箱材料从头到尾重新翻一遍。模型是无状态的,每次请求都从零开始读你发过去的所有东西,输入 token 按次计费。一份 60 页的 PDF,每页大约要 1500 到 3000 个 token,整本大约 10 万 token;你围着它问十个问题,就是 100 万输入 token——即使单价便宜,也是一笔很实在的钱,更别说每次都要等它把 10 万 token 读完才开始回答。
窗口大解决的是「放得下」,没有解决「每次都要重读」。所以大上下文时代的基本功不是「怎么塞更多」,而是算清每次请求搬了多少、哪些是重复搬的。今天后面两个技巧——citations 和 prompt caching——一个让你搬得有据可查,一个让重复搬的部分只付零头。
PDF 与图片作为内容块:模型看到的不只是文字
上一节说「把 PDF 塞进去」,具体怎么塞?不是先用工具把 PDF 转成纯文本再贴进提示词——那样表格会散、图表会丢。Messages API 允许你在 messages 的内容里直接放一个 document 块,source 是 PDF 的 base64;图片同理,放一个 image 块。Claude 对 PDF 的每一页既读文字也看版面,所以图表、表格、手写批注它都能理解。
几个硬性限制要记住:单次请求最大 32 MB,最多 600 页(上下文窗口不到一百万 token 的模型是 100 页),不能是加密的 PDF。特别密的 PDF(小字、复杂表格、大量图片)可能不到页数上限就把窗口填满,这时要按章节切开。另一个建议来自昨天的「材料在前、指令在后」:document 块放在 text 块前面,先让模型看材料,再看问题。
同一个 PDF 会反复问,所以每次都传 base64 也很浪费带宽。官方另有 Files API 可以先上传拿到 file_id,之后请求里只传 id——今天的实验用 base64 是为了少一个概念,生产里两种都行。
带引用回答:citations 让每句话都能指回页码
搭档读完 60 页给你一段摘要,你最关心的问题是:「这句结论是哪一页说的?」如果他只是凭记忆复述,你得自己翻回去核对;如果他在每句话后面标了页码,你只需要抽查。
以前做「带引用」只有一条路:在提示词里要求模型「引用原文并注明页码」,然后期待它老实抄。问题是模型抄的时候会「顺手改写」,页码也可能记错——你没法区分「它真的引用了」和「它觉得自己引用了」。Claude 的 citations 功能把这件事从「提示词约定」变成了「API 特性」:给 document 块加上 citations: { enabled: true },返回的文本块就会被切成多段,每段带一个 citations 数组,里面有被引用的原文(cited_text)、在第几份文档(document_index),以及位置——对 PDF 来说是 page_location,带 start_page_number 和 end_page_number(页码从 1 开始,结束页不包含)。
这些引用是 API 在服务端解析并核对过的,指向的一定是文档里真实存在的段落;而且 cited_text 不计入输出 token,比让模型自己抄原文还便宜。把它接到代码里:
import fs from 'node:fs'
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic()
const pdf = fs.readFileSync('report.pdf').toString('base64') // base64 不能有换行
const res = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 2048,
messages: [
{
role: 'user',
content: [
{
type: 'document', // 材料在前
source: { type: 'base64', media_type: 'application/pdf', data: pdf },
title: 'report.pdf',
citations: { enabled: true }, // 打开引用
},
{ type: 'text', text: '用五句话总结这份报告的核心结论,每句都要有依据。' }, // 指令在后
],
},
],
})
// 开了 citations 后文本会被切成多个 text 块,每块可能带 citations
for (const block of res.content) {
if (block.type !== 'text') continue
const pages = (block.citations ?? [])
.filter((c) => c.type === 'page_location')
.map((c) => `p.${c.start_page_number}`) // end_page_number 不包含,单页引用直接看 start
process.stdout.write(block.text + (pages.length ? ` [${pages.join(', ')}]` : ''))
}import base64
import anthropic
client = anthropic.Anthropic()
pdf = base64.standard_b64encode(open("report.pdf", "rb").read()).decode() # 不能有换行
res = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
messages=[
{
"role": "user",
"content": [
{
"type": "document", # 材料在前
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf},
"title": "report.pdf",
"citations": {"enabled": True}, # 打开引用
},
{"type": "text", "text": "用五句话总结这份报告的核心结论,每句都要有依据。"}, # 指令在后
],
}
],
)
# 开了 citations 后文本会被切成多个 text 块,每块可能带 citations
for block in res.content:
if block.type != "text":
continue
pages = [
f"p.{c.start_page_number}" # end_page_number 不包含,单页引用直接看 start
for c in (block.citations or [])
if c.type == "page_location"
]
print(block.text + (f" [{', '.join(pages)}]" if pages else ""), end="")一个限制昨天提过:citations 和结构化输出不能同时开。所以「带页码的摘要」和「填一张 JSON 表」是两条不同的路——需要引用就用 citations 拿文本块自己拼,需要严格结构就放弃 API 级引用、在 schema 里留一个 page 字段让模型自己填(可靠性会差一档)。
prompt caching:把不变的前缀存起来,反复提问只付零头
回到「每次都重新翻整箱材料」的问题。搭档翻第一遍的时候如果顺手做了索引,第二次、第三次再问就不用从头读了。提示缓存(prompt caching)就是这份索引:你在请求里标出一个位置说「到这里为止的内容请缓存」,服务端会把这段前缀处理后的状态存下来,接下来几分钟内再发一模一样的前缀,就直接复用,只付零头。
关键词是前缀。缓存匹配的是「从请求开头到 cache_control 标记为止的每一个字节」,顺序是工具定义、system、messages。任何一个字节变了,后面的缓存全部失效。这就解释了昨天说的「名片上不要写日期」:system 开头放一个每次都变的时间戳,前缀永远对不上,缓存一次都命中不了。正确的摆法是:不变的放前面(system、文档),会变的放后面(这一轮的问题),缓存标记打在不变部分的末尾。
价格上,写入缓存的那一次是正常输入价的 1.25 倍,之后每次命中只收 0.1 倍。所以它不是白送的:一份文档你只问一个问题,开缓存反而多花 25%;问三次以上才开始净省。缓存默认存 5 分钟,每次命中会刷新;还有一个 1 小时档,写入价 2 倍,适合间隔更长的场景。另外有一个最小长度门槛(当前主力模型是 1024 token,Haiku 4.5 是 4096),前缀太短不会缓存也不会报错——这是「明明开了缓存却没命中」最常见的原因之一。
把缓存标记打在 document 块上:
import fs from 'node:fs'
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic()
const pdf = fs.readFileSync('report.pdf').toString('base64')
async function ask(question: string) {
const res = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'document',
source: { type: 'base64', media_type: 'application/pdf', data: pdf },
cache_control: { type: 'ephemeral' }, // 缓存到这里为止:文档不变,问题在后面变
},
{ type: 'text', text: question },
],
},
],
})
const u = res.usage
// 第一次:cache_creation 大、cache_read 为 0;第二次起:cache_read 大、cache_creation 为 0
console.log(
`write=${u.cache_creation_input_tokens} read=${u.cache_read_input_tokens} uncached=${u.input_tokens}`
)
}
await ask('这份报告的核心结论是什么?')
await ask('报告里提到的风险有哪些?') // 同一前缀,命中缓存import base64
import anthropic
client = anthropic.Anthropic()
pdf = base64.standard_b64encode(open("report.pdf", "rb").read()).decode()
def ask(question: str) -> None:
res = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "base64", "media_type": "application/pdf", "data": pdf},
"cache_control": {"type": "ephemeral"}, # 缓存到这里为止:文档不变,问题在后面变
},
{"type": "text", "text": question},
],
}
],
)
u = res.usage
# 第一次:cache_creation 大、cache_read 为 0;第二次起:cache_read 大、cache_creation 为 0
print(f"write={u.cache_creation_input_tokens} read={u.cache_read_input_tokens} uncached={u.input_tokens}")
ask("这份报告的核心结论是什么?")
ask("报告里提到的风险有哪些?") # 同一前缀,命中缓存用 usage 字段验证:缓存到底命中了没有
前面讲了「应该省」,怎么知道「真的省了」?答案回到第一节的 usage。开了缓存之后它多出两个字段:cache_creation_input_tokens 是这次写进缓存的 token 数,cache_read_input_tokens 是这次从缓存读出来的 token 数;原来的 input_tokens 变成「没被缓存覆盖的那部分」。三者相加才是这次请求真正处理的输入总量。
一次健康的「先写后读」应该是这样:第一次请求 cache_creation 约等于文档大小、cache_read 为 0;第二次请求 cache_read 约等于文档大小、cache_creation 为 0、input_tokens 只剩问题那几十个 token。如果第二次 cache_read 还是 0,逐项排查:前缀里有没有会变的东西(时间戳、随机 id、没排序的 JSON);两次请求的模型是不是同一个;前缀有没有过最小长度门槛;两次间隔有没有超过 5 分钟;工具列表的顺序是不是一致。这五条覆盖了九成的「缓存失效」。
把这套验证写进代码是个好习惯:在日志里打出这三个字段,做一个「缓存命中率」的指标,上线后看一眼就知道前缀设计有没有被谁改坏。这是今天实验的最后一步,也是你以后接任何长文档需求时的第一道自检。
源码导读
动手实验
动手之前先确认两件事:你有一份不加密、几页到几十页的 PDF(没有就用 lab 目录里自带的 sample.pdf);密钥放在 .env 的 ANTHROPIC_API_KEY,没有密钥就全程用 MOCK=1。卡住了先看 README 的「常见坑」。
- 在 starter 目录 pnpm install 后跑 MOCK=1 pnpm start sample.pdf,看到一段带 [p.N] 标记的模拟摘要,确认骨架能跑。
- 完成练习 1:把 PDF 读成 base64 装进 document 块,开启 citations,document 块放在 text 块之前。
- 完成练习 2:遍历返回的内容块,把每条 page_location 的起始页码拼到对应句子后面,并在末尾打印 stop_reason。
- 完成练习 3:给 document 块加 cache_control,连问两个问题,打印三个缓存相关的 usage 字段并对比。
- 把摘要的问题换成三个不同的问题再跑,观察 cache_read 是否稳定命中;如果没有,按正文的五条清单排查。
面试题
今天 3 道题在下方题库区,侧重上下文窗口为什么是最稀缺资源、prompt caching 的前缀语义、citations 与自己让模型抄原文的区别。展开后先看「分析过程」再看要点——照着推导练,比背要点管用。标注「国内高频 / 海外高频」方便按目标市场取舍。
检查清单与明日预告
- 能用官方 SDK 发出一次 Messages API 调用,并读懂返回里的内容块与 usage
- 能把 PDF 或图片作为内容块传给 Claude,并开启 citations 拿到带页码的引用
- 能说清 prompt caching 省在哪、什么情况下反而不省,并从 usage 字段验证缓存是否命中
- 能背出「缓存不命中」的五条排查清单
- 实验的 5 条验收标准全部通过
- 3 道面试题不看要点也能答出至少 2 道
明天(D3)我们离开 API,进入 Claude Code——一个会自己读文件、跑命令、改代码的搭档。你会装好它,给自己的项目写第一份 CLAUDE.md,用 Plan Mode 走完「先探索、再计划、再动手」,并且第一次把示例任务「给 TODO API 补校验和测试」真的交给它做。今天讲的「上下文是最稀缺的资源」在那里会变成每天都要面对的现实:Claude Code 读的每个文件、跑的每条命令都在消耗同一个窗口,所以 D3 有一半篇幅在讲怎么管它。
Interview questions
Why is the context window called the scarcest resource in LLM applications? With million-token windows, does that still hold?为什么说上下文窗口是 LLM 应用里最稀缺的资源?窗口已经有一百万 token 了,这个说法还成立吗?
Common in ChinaCommon overseasBasic#context-window#costHow to reason about it · think before answering
- The second sentence is the point. 'The window has a limit' is a dated answer; explain why scarcity survives large windows.
- Three causal chains: models are stateless so every request re-reads the whole input and bills it, a big window only solves fitting, not re-sending; longer context means more latency and diluted attention, so adherence to early instructions degrades as the window fills; and in agent workflows every file read and command output lands in the same window, filling it far faster than chat does.
- Conclusion: scarcity shifted from 'won't fit' to 'every token costs money and attention', so the discipline becomes active management — include only what is needed, cache the stable prefix, delegate research to subagents with their own context, and clear between tasks.
- Production math: a 60-page PDF is roughly 100k tokens; ten questions about it are a million input tokens; caching versus not caching is an order of magnitude apart.
- Follow-up: when should context accumulate? While deep in one complex problem where the history is still load-bearing; the test is whether the next step will use it.
分析过程 · 先想清楚再作答
- 题眼在第二句。只答「窗口有上限」已经过时了,面试官想听的是「窗口变大之后为什么还稀缺」。
- 从三条因果链推:一、模型无状态,每次请求都把全部输入重读一遍,输入 token 按次计费——窗口大只解决了放得下,没解决每次都要重搬;二、上下文越长,延迟越高、注意力越稀释,模型对早期指令的遵守度会下降,也就是「性能随填充度下降」;三、Agent 场景里每读一个文件、每跑一条命令的输出都进同一个窗口,填得比聊天快得多。
- 结论:窗口大了,稀缺性从「放不下」变成了「每一 token 都在花钱和稀释注意力」,所以管理手段变成了主动管:只放必要的、把不变的缓存起来、把查资料的活派给独立上下文的子代理、该清就清。
- 生产视角:算一笔账——60 页 PDF 约 10 万 token,围着它问 10 个问题就是 100 万输入 token;不用缓存和不用缓存的差价是一个量级。
- 可预期的追问:那什么时候应该让上下文积累?在一个复杂问题里深挖时历史是有价值的;判据是「这段历史下一步还会不会用到」。
Key points
- Models are stateless: every request re-reads and bills the full input; a large window solves fitting, not re-sending
- Longer context raises latency and dilutes attention; adherence to early instructions drops
- Agent workflows dump every file read and command output into the same window
- Tactics: include only what's needed, cache the stable prefix, isolate research in subagents, clear between tasks
答题要点
- 模型无状态,每次请求重读全部输入并计费;窗口大只解决放得下,不解决每次重搬
- 上下文越长延迟越高、注意力越稀释,早期指令遵守度下降
- Agent 场景每次读文件、跑命令的输出都进窗口,填得比聊天快得多
- 对策:只放必要的、缓存不变前缀、用子代理隔离查资料、任务之间清空
Where does prompt caching save money, when does it cost more, and how do you debug a zero cache-hit rate in production?prompt caching 省在哪?什么情况下反而不省?线上发现缓存命中率是零,你怎么排查?
Common in ChinaCommon overseasIntermediate#prompt-caching#costHow to reason about it · think before answering
- Three questions, three layers: mechanism, boundaries, debugging. The third layer is what shows production experience.
- Mechanism: the cache matches the exact byte prefix from the start of the request to the cache_control marker (tools, then system, then messages). A hit bills that prefix at 0.1x input price; the write costs 1.25x (2x for the one-hour TTL).
- When it costs more: a prefix used only once (+25%); volatile content inside the prefix — timestamps, random ids, unsorted JSON, user names — so every call writes a cache nothing will read; a prefix below the minimum (1024 tokens on current flagship models, 4096 on Haiku 4.5) that silently never caches; requests spaced beyond the TTL.
- Debug order by likelihood: dynamic content at the head of system or tool definitions; model id mismatch between calls; prefix under the minimum; gap over five minutes; unstable tool ordering. The single signal is usage.cache_read_input_tokens greater than zero.
- Follow-up: where do breakpoints go? At the end of stable sections — tools, system, the long document, the second-to-last message in a multi-turn chat — at most four; a breakpoint on per-turn content is a wasted write.
分析过程 · 先想清楚再作答
- 三问对应三层:原理、边界、排查。只答第一层是背文档,第三层才体现有没有真的上过线。
- 原理一句话:缓存匹配的是请求开头到 cache_control 标记为止的精确前缀(顺序是工具、system、messages),命中时这段只收正常输入价的 0.1 倍;代价是写入那一次收 1.25 倍(1 小时档 2 倍)。
- 不省的情况由此推出:同一前缀只用一次(多付 25%);前缀里有每次都变的内容(时间戳、随机 id、未排序 JSON、用户名),导致每次都在写永远用不上的缓存;前缀短于最小门槛(主力模型 1024 token,Haiku 4.5 是 4096)根本不会缓存;两次请求间隔超过 TTL。
- 排查清单按发生概率排:一看 system 或工具定义开头有没有动态内容;二看两次请求的模型 id 是否一致;三看前缀长度是否过门槛;四看间隔是否超 5 分钟;五看工具列表顺序是否稳定。判据只有一个字段:usage.cache_read_input_tokens 是否大于 0。
- 可预期的追问:断点应该打在哪?不变的末尾——工具定义末尾、system 末尾、长文档末尾、多轮对话倒数第二条消息,最多四个;打在每轮都变的内容上等于白写。
Key points
- Matches the exact prefix (tools → system → messages up to the marker); hits bill 0.1x, writes 1.25x
- Costs more when the prefix is used once, contains volatile content, is under the minimum length, or requests exceed the TTL
- Debug: dynamic content, model mismatch, length, gap, tool ordering; verify via cache_read_input_tokens
- Place breakpoints at the end of stable sections, at most four
答题要点
- 匹配精确前缀(工具 → system → messages 到标记为止);命中 0.1 倍,写入 1.25 倍
- 不省:前缀只用一次、前缀含动态内容、前缀短于最小门槛、间隔超过 TTL
- 排查:动态内容、模型不一致、长度不够、间隔太久、工具顺序变了;看 cache_read_input_tokens
- 断点打在不变部分的末尾,最多四个
How do API citations fundamentally differ from prompting the model to quote sources with page numbers, and when can't you use them?citations 和在提示词里要求模型「引用原文并注明页码」有什么本质区别?什么场景下不能用 citations?
Common in ChinaCommon overseasIntermediate#citations#groundingHow to reason about it · think before answering
- The question is about where trust comes from. 'Citations are more convenient' is surface; the real difference is who guarantees the quote is real.
- With prompting, both the quote and the page number are free text the model generates — it may paraphrase, it may misremember the page, and you cannot tell a real quote from an imagined one. With citations, the model emits citation intent in a standard format, the API parses and verifies it server-side, cited_text is guaranteed to exist in the document, and page_location comes from the API. Fidelity is enforced by the API rather than promised by the model.
- Two side benefits: cited_text does not count toward output tokens, so it is cheaper than asking the model to copy; and the result is structured content blocks your UI can highlight and jump to without regex guessing.
- When you can't: citations are incompatible with structured outputs (JSON Schema) — enabling both returns a 400. Either drop API-level citations and add a page field to the schema (one notch less reliable), or split into two calls: citations for facts, structured output for shaping.
- Follow-ups: page semantics — start_page_number is 1-indexed and end_page_number is exclusive; document_index distinguishes sources. Can you audit citation quality? Yes — string-match cited_text against the source, or sample manually.
分析过程 · 先想清楚再作答
- 这题考的是「可信度从哪来」。答成「citations 更方便」是表面;本质区别是谁来保证引用的真实性。
- 拆法:提示词方案里,引用和页码都是模型生成的自由文本——它可能顺手改写原文、可能记错页码,你无法区分「真引用」和「自以为引用」。citations 方案里,模型内部以标准格式输出引用意图,API 在服务端解析并核对,返回的 cited_text 一定是文档里真实存在的段落,page_location 的页码由 API 给出。真实性由 API 保证而不是由模型自觉保证。
- 附带的两点好处:cited_text 不计入输出 token,比让模型抄原文便宜;返回是结构化的内容块,程序可以直接高亮、跳转,不用正则去猜「第 3 页」出现在哪。
- 不能用的场景:与结构化输出(JSON Schema)不兼容,二者同开会报 400;此时要么放弃 API 级引用、在 schema 里留 page 字段让模型自己填(可靠性差一档),要么分两步:先 citations 拿事实,再用结构化输出整理。
- 可预期的追问:页码字段的语义?start_page_number 从 1 开始,end_page_number 不包含;多文档时 document_index 区分来源。再追问「能否验证引用质量」——能,用 cited_text 与原文做字符串比对,或抽样人工核对。
Key points
- Prompted quotes are free text the model generates — it may paraphrase or misplace pages, and you can't tell
- Citations are parsed and verified server-side; cited_text is guaranteed to exist and page numbers come from the API
- cited_text is free of output-token cost and the structured blocks enable highlighting and navigation
- Mutually exclusive with structured outputs; split into two calls or add a page field to the schema
答题要点
- 提示词引用是模型生成的自由文本,可能改写、记错页码,无法区分真假
- citations 由 API 在服务端解析核对,cited_text 一定存在于文档中,页码由 API 给出
- cited_text 不计输出 token,返回结构化便于高亮跳转
- 与结构化输出互斥;需要两者时分两步或在 schema 留 page 字段
Comments
Sign in to join the discussion
No comments yet — be the first.