长文档、多模态与 API 初见:大上下文怎么用、prompt caching 省钱、PDF 与图片输入、带引用回答;Messages API 最小调用
第一次用代码调 Claude:把一份 PDF 整本喂进去,让它给出带页码引用的摘要,并用 prompt caching 把反复提问的成本降一个量级。
今日目标
- 能用官方 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 有一半篇幅在讲怎么管它。
面试题库
为什么说上下文窗口是 LLM 应用里最稀缺的资源?窗口已经有一百万 token 了,这个说法还成立吗?Why is the context window called the scarcest resource in LLM applications? With million-token windows, does that still hold?
国内高频海外高频基础#context-window#cost分析过程 · 先想清楚再作答
- 题眼在第二句。只答「窗口有上限」已经过时了,面试官想听的是「窗口变大之后为什么还稀缺」。
- 从三条因果链推:一、模型无状态,每次请求都把全部输入重读一遍,输入 token 按次计费——窗口大只解决了放得下,没解决每次都要重搬;二、上下文越长,延迟越高、注意力越稀释,模型对早期指令的遵守度会下降,也就是「性能随填充度下降」;三、Agent 场景里每读一个文件、每跑一条命令的输出都进同一个窗口,填得比聊天快得多。
- 结论:窗口大了,稀缺性从「放不下」变成了「每一 token 都在花钱和稀释注意力」,所以管理手段变成了主动管:只放必要的、把不变的缓存起来、把查资料的活派给独立上下文的子代理、该清就清。
- 生产视角:算一笔账——60 页 PDF 约 10 万 token,围着它问 10 个问题就是 100 万输入 token;不用缓存和不用缓存的差价是一个量级。
- 可预期的追问:那什么时候应该让上下文积累?在一个复杂问题里深挖时历史是有价值的;判据是「这段历史下一步还会不会用到」。
How 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.
答题要点
- 模型无状态,每次请求重读全部输入并计费;窗口大只解决放得下,不解决每次重搬
- 上下文越长延迟越高、注意力越稀释,早期指令遵守度下降
- Agent 场景每次读文件、跑命令的输出都进窗口,填得比聊天快得多
- 对策:只放必要的、缓存不变前缀、用子代理隔离查资料、任务之间清空
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
prompt caching 省在哪?什么情况下反而不省?线上发现缓存命中率是零,你怎么排查?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#cost分析过程 · 先想清楚再作答
- 三问对应三层:原理、边界、排查。只答第一层是背文档,第三层才体现有没有真的上过线。
- 原理一句话:缓存匹配的是请求开头到 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 末尾、长文档末尾、多轮对话倒数第二条消息,最多四个;打在每轮都变的内容上等于白写。
How 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.
答题要点
- 匹配精确前缀(工具 → system → messages 到标记为止);命中 0.1 倍,写入 1.25 倍
- 不省:前缀只用一次、前缀含动态内容、前缀短于最小门槛、间隔超过 TTL
- 排查:动态内容、模型不一致、长度不够、间隔太久、工具顺序变了;看 cache_read_input_tokens
- 断点打在不变部分的末尾,最多四个
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
citations 和在提示词里要求模型「引用原文并注明页码」有什么本质区别?什么场景下不能用 citations?How do API citations fundamentally differ from prompting the model to quote sources with page numbers, and when can't you use them?
国内高频海外高频进阶#citations#grounding分析过程 · 先想清楚再作答
- 这题考的是「可信度从哪来」。答成「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 与原文做字符串比对,或抽样人工核对。
How 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 由 API 在服务端解析核对,cited_text 一定存在于文档中,页码由 API 给出
- cited_text 不计输出 token,返回结构化便于高亮跳转
- 与结构化输出互斥;需要两者时分两步或在 schema 留 page 字段
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
评论
登录后即可参与讨论
还没有评论,来说第一句。