misc/ 是 claude-cookbooks 里最像杂物抽屉的目录。它没有 capabilities/ 那样的主题、没有 claude_agent_sdk/ 那样的编号顺序,14 个 notebook 从 2023 年 8 月排到 2026 年 1 月,涉及:批处理、JSON 输出、SQL、PDF、网页摘要、缓存、评测、元提示词。
它是整个仓库唯一一个横切所有方向的目录。做 RAG 要用它的缓存,做 agent 要用它的压缩,做分类要用它的评测,做任何东西之前都要用它的合成测试数据。之所以放不进任何一个 capability,恰恰是因为它比 capability 低一层。
这一层是什么?把这 14 篇通读一遍,会发现它们在回答同一个问题的四个侧面:当你把模型当一个函数调用时,会崩在哪里。
普通函数有几个隐含承诺:给定输入,返回值的类型是确定的;调用成本可忽略;输入长度没有硬上限;返回对不对,看代码就知道。模型调用把这四条全部撕掉了------返回是自然语言,形状不受控;每次调用有真实价格,而且价格取决于你怎么组织上下文;输入输出都有硬窗口,撞上去就截断;返回对不对,没有任何机制自动告诉你。
misc 的 14 篇,就是这四条承诺各自失效之后的工程应对。加上一组关于"外部世界怎么进入上下文"的入口问题,正好把 14 篇分完。
| 崩点 | 对应篇目 | 共同的处理原则 |
|---|---|---|
| 输出形状不受控 | JSON mode、citations、moderation filter | 把约束从"请求配合"换成"机制上无法不配合" |
| 上下文是有价格的介质 | prompt caching、speculative caching、batch processing | 上下文布局是成本设计,不是事后优化 |
| 窗口是必然撞上的墙 | sampling past max tokens、session memory compaction | 设计撞墙之后的接续,而不是设计如何不撞 |
| 正确性没有自动裁判 | building evals、generate test cases、metaprompt | 判分权不能留在答题人手里 |
| 外部世界如何进入 | SQL queries、PDF upload、read web pages | 进入方式决定了失真程度和可追溯性 |
一、输出的形状不能靠叮嘱
how_to_enable_json_mode 开篇就把话说明白了:Claude 没有 constrained sampling 意义上的"JSON 模式"。这排除了一条最诱人的路------指望平台在采样层面替你保证格式。既然没有,那就得自己在接口层面造约束。
这篇给了四个手段,它们的差别不是"强度"而是生效环节:
| 手段 | 作用在哪 | 保证什么 | 失败模式 |
|---|---|---|---|
| 提示里要求输出 JSON | 输入侧 | 概率倾向 | 加开场白、格式跑偏 |
prefill 一个 { |
输出侧首位 | 第 1 个 token 是 { |
之后仍可能不合法;已作废 |
| stop sequence | 服务端采样循环 | 尾部没有多余话 | 前面和中间都不管 |
| XML 标签 + 正则抽取 | 事后定位 | 结果可被精确切出 | 标签可能不出现 |
第一行要走完"模型读到 → 分配注意力 → 理解意图 → 采样时倾向遵循"这条链,每环都是概率性的,会被上下文长度、冲突指令、模型"先说句开场白"的习惯削弱。它的结果是"第一个字符是 { 的概率是 0.9 还是 0.6"------高,但不是 1。这是一句叮嘱,存在"被忽略"这个动作空间。
后三行不在这条链上。prefill 利用的是"最后一条消息可以是未结束的 assistant",服务端不补 turn 终止符、直接从这里接着采样,所以那个 { 不是被采样出来的,是既成事实------模型无从忽略一个已经在序列里的 token。代价是思维链被掐掉 ,因为第一个 token 已经被占了,这是用格式确定性换推理质量。stop sequence 更硬,它甚至不在模型侧:模型可能"想"在 } 后补一句"以上就是您需要的 JSON",但一旦命中指定序列,循环直接停,那些 token 根本不会被生成。XML 标签严格说仍依赖模型配合(它得真的输出标签),但它把要求从"必须完美"放宽成"必须可定位"------哪怕前面啰嗦三段、后面又补总结,只要标签在就能切出来;它也是四者里唯一能处理"一个回复里多个 JSON"的方案,那种情况下搜 { 和 } 根本没法配对。
这个"叮嘱 vs 结构"的分野贯穿整组。building_moderation_filter 表面在讲内容审核,实际在讲同一件事的另一半:如何让一个主观判断产出可被程序消费的结果。它把分类体系(ALLOW / BLOCK 的定义和例子)整个搬进提示词,再要求"只返回 ALLOW 或 BLOCK"------审核规则于是变成可编辑的文本而不是硬编码逻辑,改规则不用改代码。随后加的两层增强作用不同:<thinking> 思维链提高判断质量,few-shot 压缩边界模糊地带的歧义。
using_citations 最能说明问题,因为它展示了同一需求的两代解法。第一代是纯提示技巧------让模型把引用的原文整段抄出来,notebook 直接列了三个毛病:抄原文推高 output token 因而推高成本;模型可以引用一个根本没给它的来源;召回和精度都不如后来的方案。第二代是 API 级 citations:文档以 document block 传入,模型返回带结构化坐标的引用,纯文本给字符位置、PDF 给页码、custom content 给内容块编号。差别在于第一代里"引用是否真实"依赖模型诚实,第二代里模型没有能力引用一个不存在的位置------坐标必须落在你传进去的 block 里。约束从道德问题变成了物理问题。
这篇还藏了两个精细设计。一是 context 字段:放进去的内容模型能读、能用来组织回答,但不能被引用。这是在文档内部划分"可作为证据的部分"和"仅供理解的部分"------元数据、检索时补的上下文、使用说明都该进 context,因为它们不是原始证据。能做出这个区分,说明设计者想清楚了引用的语义不是"我看过什么"而是"我的结论站在什么上面"。二是粒度:纯文本会被自动按句切分,你就只能拿到句级引用;想让整篇文章作为最小引用单位,得用 custom content 自己定义 chunk。粒度不是模型的选择,是你传参时就决定了的。
但这一节的答案不在 misc 里
上面四个手段今天只剩两个半:prefill 已作废(第六节细说),提示词要求仍是最弱一档,只有 stop sequence 和 XML 标签还站得住。
真正的答案在 capabilities/knowledge_graph/guide.ipynb(2026 年 3 月)------structured outputs:
python
class ExtractedGraph(BaseModel):
entities: list[Entity] # Entity.type 是 Literal["PERSON","ORGANIZATION",...]
relations: list[Relation]
response = client.messages.parse( # 不是 messages.create
messages=[{"role": "user", "content": PROMPT}],
output_format=ExtractedGraph, # 传的是类,不是对格式的描述
)
return response.parsed_output # 已经是 typed 对象
notebook 对它的保证写得毫不含糊:回复保证通过该 schema 的校验 ,以 typed 对象返回------不需要正则解析,不会有 JSON decode 错误,不必写防御性的 isinstance 检查。
前面四个手段都是事后修补 ------生成完了你去裁剪、抽取、校验;structured outputs 是事前约束 ------schema 参与解码,不合规的分支不会被走。所以你拿到的不是"一段大概是 JSON 的文本",而是一个已通过验证的对象。它顺手消灭了一类过去只能靠叮嘱的东西:Literal["PERSON",...] 这种枚举,用提示词写是"请从这五类里选"(可被违反),写进 schema 就是不可能出现第六类。枚举、嵌套、必填、类型,全从"说明"变成了"约束"。
SDK 只是糖,线上传的是 JSON Schema,包在 output_config.format 里(已 GA,不需要 beta header):
json
{
"messages": [...],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": { "name": {"type": "string"}, "demo_requested": {"type": "boolean"} },
"required": ["name", "demo_requested"],
"additionalProperties": false
}
}
}
}
字段位置有过迁移:output_format 已移到 output_config.format,老参数仍在过渡期;Python SDK 的 messages.parse() 收 output_format= 当便利参数、内部翻译,其他语言 SDK 要直接写 output_config。
为什么线上必须是 JSON Schema 而不能是一段自然语言说明?因为它要被编译成自动机。 官方给这个技术的名字叫 grammar-constrained sampling,实现细节没公开,但通用原理是公开的:每一步解码时,模型照常算出全词表 logits,约束引擎根据当前语法状态算出一个 mask,把会导致非法的 token 的 logit 设为 −∞,softmax 后概率归零,然后在剩下的合法 token 里按模型自己的分布采样,最后用选中的 token 更新语法状态。
这里有个容易被误解的地方:它没有削弱模型的判断。合法候选之间仍按原始概率竞争,模型的偏好完整保留;被拿掉的只是非法分支------它们根本不在候选集里。所以"合法"不是靠模型配合得来的,是靠候选集被裁剪得来的。而"语法状态"就是编译出来的那台自动机在跟踪的东西:括号深度、下一个位置该是 key 还是 value、当前允许哪些类型、required 字段收齐了没有。把 JSON Schema 编译成 grammar,本质是把你的 schema 变成一台能回答"下一个 token 合法吗"的自动机------一句"请输出 JSON"编译不出状态转移表,这就是线上必须是 schema 的原因。
顺带说,output_config.format 和工具定义上的 "strict": true 不是两个功能,官方明确它们共用同一条编译流水线。这解释了为什么两者的限制完全一致:同一份 JSON Schema 子集、共享同一份可选参数配额、同一个 24 小时 schema 缓存、同一条"schema 里不要放 PHI"的警告。
知道是编译成自动机之后,官方那些看起来零散的规定就全变成必然了:
| 规定 | 原理上的原因 |
|---|---|
| 首次有编译延迟,产物缓存 24 小时 | 自动机要先编译才能用,产物值得复用 |
改结构失效缓存,只改 name / description 不失效 |
前者改变自动机结构,后者不影响状态转移 |
| 每个可选参数让状态空间的一部分翻倍 | 可选字段意味着"有 / 无"两条分支,n 个就组合爆炸 |
union 类型(anyOf)特别贵 |
一个位置允许多种类型,自动机要同时保持多路可能 |
| 可选参数 ≤ 24、union ≤ 16、编译超时 180 秒 | 都是给状态空间爆炸设的闸 |
minimum、maxLength 这类约束不支持 |
数值区间和长度不是正则 / CFG 能表达的 |
| enum 大小写不保证 | 说明它不是纯字面量匹配,token 边界和大小写变体上有松弛 |
| 语法只作用于直接输出,各段之间状态重置 | 自动机是分段挂载的,不是全程挂着 |
| 和 citations 互斥 | citation block 要和文本交错插入,会打断状态连续性 |
其中 minimum: 100 那条最能说明边界在哪:它不被支持不是产品偷懒,是语法层面表达不了。
于是 SDK 那层就不只是格式转换,它会改你的 schema :删掉不支持的约束(minimum、maxLength 之类)、把被删的约束改写进字段 description、给所有 object 补 additionalProperties: false、过滤 string format,最后用你的原始 schema(含全部约束)验证回复 。真实分工是两段的------语法层合规由服务端 grammar 保证,语义约束层合规由客户端验证保证 。一个 minimum: 100 的字段发出去只是普通 integer,"至少 100"降级成描述里的一句话,回来由 SDK 卡住。知道这个分工,才不会误以为服务端替你兜住了一切。
自动机管的粒度也比"括号和引号闭合"大得多,它跟踪的是完整 schema 结构:当前在哪个对象的哪个位置、下一个该出 key 还是 value、该出哪个 key (候选被限定在声明的属性里,additionalProperties: false 让这个集合闭合)、这个 key 的 value 该是什么类型(声明 integer 时,引号直接被 mask 掉)、required 字段收齐了没有(没收齐时闭合的 } 是非法 token)。这是 grammar 而非 regex 的意义:它是结构化语法,括号闭合只是它顺带保证的最低层。
由此推出的必然结果是字段顺序被固定。要允许 key 任意顺序出现,自动机得记住"已经出过哪些 key",k 个 required key 就需要 2^k 个状态------所以实现上一律按声明序发射。Claude 文档也确认了这点,只有一个变形:required 字段先出、optional 后出,各自组内保持声明序。
而模型是自回归的、左到右不可回头,于是 schema 里的字段顺序就是模型的思考顺序:
python
# 坏:先被逼着表态,reasoning 变成事后找理由
class Judgment(BaseModel):
verdict: str
reasoning: str
# 好:先当草稿纸推演,再基于推演下结论
class Judgment(BaseModel):
reasoning: str
verdict: str
声明在答案之前的字段是草稿纸,声明在答案之后的字段是事后合理化。 有人拿 judge 做过对照实验:只把 reasoning 从 verdict 后面挪到前面,边界样例上的判决全部翻转。
顺带记住一条:schema-valid 不等于正确。 约束保证的是形状,不是内容,一个完全合规的对象里可以填着错的值------这正好接回第四节,评测省不掉。
几个不显眼但会踩的边角:
schema 本身有 token 成本。 用结构化输出时 Claude 会自动收到一段额外系统提示解释期望格式,输入 token 略增。更要紧的是它因此进入了 prompt 前缀 ------改 output_config.format 会让该会话的 prompt cache 命中不了(机制见第二节)。schema 看起来是输出侧的约束,实际有输入侧的实体存在。
保证有三个漏口。 refusal 时安全拒答优先于 schema,返回 200、照常计费、输出可能不合 schema;max_tokens 时输出截断因而不完整;还有个很容易踩的------enum 大小写不保证,schema 写 "Conversation topic 3" 可能返回 "Conversation Topic 3",无报错无特殊 stop_reason,所以比较 enum 要大小写不敏感,且别用只差大小写的枚举值。
和 Citations 互斥。 引用需要 citation block 和文本交错,与严格 JSON 约束冲突,同时启用直接 400。这一节最强的两个手段------不可伪造的引用坐标、不可违反的输出 schema------只能选一个。要兼得只能拆两次调用:一次带引用做检索与归因,一次用 schema 把结论结构化。
思维链没有被牺牲。 语法只作用于 Claude 的直接输出,不管 tool use、tool result 和 thinking 标签,且语法状态在各段之间重置 ------模型能在 thinking 里自由推理,最终回复仍受约束。这是它和 prefill 的本质区别:prefill 占掉输出首位、思维链就没了;structured outputs 不占 thinking 段。但要留意它和上面字段顺序那条的合力:语法不管 thinking,可你要是没开 extended thinking、又把结论摆在 schema 第一位,那是自己掐掉了推理空间。要么开 thinking,要么留一个前置的 reasoning 字段。
所以这一节今天的格局是:要提取、分类、评分、任何要落库的结构化结果,默认 structured outputs;模型还要决定调不调用哪个工具时,用 strict tool use("strict": true 加在工具定义上,tool_use/tool_use_with_pydantic.ipynb 是同一思想的对偶------入参和出参都不该是自由文本),两者可在同一请求里并用;XML 标签仍有位置,主要在两处------需要和 citations 共存时,以及 schema 复杂度撞上编译限制时。
二、上下文是有价格的介质
prompt_caching 会把"上下文是个容器,能装多少装多少"这个心智模型换掉:它是分层的、有价格的、有时效的存储。具体到数字------最短 1024 token(Sonnet)或 4096 token(Opus 和 Haiku 4.5)才够得上缓存;默认存活 5 分钟,每次命中续期,想要 1 小时得付 2 倍基础输入价;写入按 1.25 倍计费,读取按 0.1 倍计费;每个请求最多 4 个显式断点。
这些参数不是琐碎细节,它们直接决定架构。0.1 倍的读取价意味着"把一整本书放进每次请求"从荒谬变成合理;5 分钟 TTL 意味着高频短会话受益、低频长间隔无效;1024 token 门槛意味着小 prompt 缓存了也没用。
notebook 给了两种用法。自动缓存是在请求顶层加一个 cache_control,系统自己找位置放断点,多轮对话里断点会自己往前挪------你只管往 messages 列表里追加,不用管标记。显式断点是把 cache_control 放在具体的 content block 上,换来的是最多 4 个独立断点、可以给不同段落不同 TTL、可以把系统提示词和对话内容分开缓存。它的建议很干脆:从自动缓存开始,只在真需要细粒度控制时才转显式。这个建议本身值得留意------多数情况下"够用的默认"比"完全可控"更值。
speculative_prompt_caching 把同一个机制用出了一个反直觉的花样。用户在输入框里打字要花几秒,这几秒里请求还没发出,服务端什么也没做。那就在用户开始打字的瞬间,先发一个只要 1 个 token 输出的请求,把大上下文写进缓存。等用户真正提交时,缓存已经热了。省下来的不是钱,是首 token 延迟------把缓存创建的时间藏进了人的打字时间里。
这个手法的一般形式是:用户思考的时间是免费的算力窗口 。它成立的前提也说得很清楚:预热用的上下文必须和真实请求的上下文逐字相同 ,否则不命中;要用 cache_read_input_tokens 去验证真的命中了;还要加时间戳防止跨会话误共享缓存。(顺带一提,官方后来给了更干净的预热方式:max_tokens: 0,不产出内容、不计输出 token;但它和 structured outputs 不兼容,用了 schema 就只能回到 1-token 那条老路。)
"逐字相同"是理解缓存的钥匙。缓存的键是前缀字节的累积哈希,不是内容的语义。 顺序固定为 tools → system → messages,写入只发生在断点处,一次请求只写一个条目,键是"从 prompt 开头到该 block"的哈希。所以断点之前任何一个字节变了,下次算出的哈希就不同。层级也由此而来:某一层变了,该层及其后所有层全部落空------tools 变则三层全废,system 变则 system 和 messages 全废。
这也解释了为什么改 output_format 会打掉一个几十轮长会话的缓存,哪怕对话内容一字未动:结构化输出会往 prompt 里注入一段系统提示,schema 一变、这段文本就变、system 层哈希就变,于是所有 messages 位置的条目全都寻址不到。开关 citations、开关 web search 是同一个机制的同一类表现------凡是被渲染进 prompt 的参数,都是缓存边界的一部分。
"失效"这个词容易让人以为条目被销毁了,其实它还在内存里 ,删除只由 TTL 触发。改参数导致的只是这次请求寻址不到------把参数改回去、只要还在 TTL 内,照样命中原条目;两条并行会话用不同 schema,也各自维护各自的条目互不干扰。所以代价是一次性的前缀重建(按 1.25 倍写一个新条目),不是持续流血。它像 git 的 commit hash:改一个早期 commit,后面所有 hash 全变,但旧对象并没被删,只是没人指向了。
batch_processing 是同一个成本意识在另一个维度上的表达。如果你的请求不需要立刻返回,走 Message Batches 异步提交,价格直接砍半。它示范了在同一个批次里混放不同形态的请求------普通消息、带系统提示的、多轮的、带图片的,以及怎么监控批次状态、怎么处理其中一部分失败。这篇的思想含量最低,但它补上了成本图景的第三个维度:除了"缓存复用"和"提前预热",还有"放弃实时性"。
三篇加起来给出的结论是:成本不是事后优化项,它决定了什么设计一开始就是可行的。 这句话有个具体的证据在别处------generate_test_cases 的结尾说,有了 prompt caching,往提示词里塞大量示例从来没有比现在更划算。成本结构一变,prompt 设计的最优解跟着变。少示例曾经是被价格逼出来的选择,不是效果上的选择。
三、窗口是必然会撞的墙
sampling_past_max_tokens 处理一个具体到有点土的问题:让模型写一篇长东西,写到 max_tokens 就被硬截断,第五个故事停在半句话。
解法也很土:把这段被截断的回复原样放回 assistant 消息里,让模型接着往下采样。它能无缝续上半句话。但这篇真正的价值在最后那段代价说明------续写要为提示词里的输入 token 付第二遍钱,上一轮那 4096 个输出 token 这次会作为输入 token 再算一次,好在它们不会被当成输出重复计费。
写清代价这件事,比给出技巧更重要。它把"这个方案能用吗"变成一道可以算的账,而不是一种感觉。
session_memory_compaction 是同一类问题在时间轴上的展开,也是 misc 里工程含量最高的一篇。长会话必然撞上下文上限,撞上就得压缩历史。朴素做法是等撞上了再生成摘要------notebook 实测这一下让用户干等了 40 多秒 。
它的替代方案叫即时压缩,核心是把两件事解耦:摘要的生成提前,摘要的启用不提前。
设一条比硬上限低的软阈值(notebook 里硬上限 12000、初始化阈值 7500、之后每新增 2000 token 更新一次)。一到软阈值,后台线程开始生成会话记忆,之后按增量持续更新。但要注意:后台线程写的是 self.session_memory 这个旁路变量,它不进入当前会话 。chat() 里照常把完整历史发出去,只有真撞上硬上限时才调 compact(),把攒好的摘要换进 messages。
这个设计乍看别扭------摘要都备好了为什么不用?因为摘要是有损的,早换进去等于早丢信息 。所以最优策略是:信息尽量晚丢,生成尽量早做。传统压缩把这两件事绑在同一刻(撞线才生成、生成完就换),代价就是那 40 多秒干等。即时压缩把生成挪到软阈值、把替换留在硬上限,用户等待归零不是因为压缩变快了,而是压缩挪进了用户看不见的时间里------和上一节 speculative caching 是同一个手法。
值得补一句的是,窗口涨到百万级之后,压缩的触发理由变了。装不下不再是主要矛盾:一是成本,拖着 80 万 token 每轮都要为它付费;二是延迟,decode 每一步都要读一遍整个 KV cache,上下文越长每个输出 token 越慢,这是持续的税;三是注意力质量,上下文里塞满几十轮的失败尝试和废弃路径,模型容易重试已经失败过的方案。所以现在压缩更多是降噪而不是腾空间------把 30 万 token 的过程噪声换成 3000 token 的结论。相应地,阈值也不该再按"距离硬上限多远"来定,而该按任务阶段:一个子任务收尾就压掉它的过程细节,这跟窗口剩多少无关。
线程部分有个细节值得看,因为它决定了这套东西会不会反而拖慢主线程。threading.Lock 保护的是三个共享字段:session_memory、last_summarized_index、tokens_at_last_update。冲突场景是后台线程正在生成摘要(一次几十秒的 API 调用)而主线程还在追加新消息,没有锁就可能读到"摘要写好了但索引还没更新"的中间态,导致消息被吞或被重复计入。而锁的圈法很讲究:
python
with self._lock: # 锁内只做快照读
current_session_memory = self.session_memory
last_index = self.last_summarized_index
new_memory = self._create_session_memory(...) # 锁外!几十秒的 API 调用
with self._lock: # 锁内做原子写
self.session_memory = new_memory
self.last_summarized_index = snapshot_index
慢操作必须在锁外 ,锁只护住"读一组字段"和"写一组字段"这两个瞬间。要是把 API 调用也圈进去,主线程会被堵几十秒,即时压缩就白做了。另外两道保护不靠锁:_trigger_background_update 先查 is_alive(),已有任务在跑就跳过,保证同时只有一个后台更新;传给线程的是 self.messages.copy() 快照而非引用,所以主线程的追加不会改动后台正在读的列表。daemon=True 则保证主程序退出时不被这个线程拖住。
还有一层是靠缓存省出来的:后台摘要器和主对话共用同一段对话前缀,所以给它加上 cache_control 就能直接命中主对话刚刚创建的缓存。后台更新只需为"请你总结"这条新指令付全价,成本降到大约十分之一,整体省下约 80%。会话越长,省得越多------而会话越长正是你越需要压缩的时候。
那份压缩提示词比代码更值得抄,尤其是它怎么处理"用户纠正"。代码里没有任何检测逻辑,全靠提示词,而且做了三层:
先分析,再动笔。 提示词要求模型先在 <think> 里回答六个问题,第三问就是"用户是否在某处纠正或改变了方向"。这是把"找纠正"变成显式的前置任务,而不是指望模型写摘要时顺手想起来。事后 remove_thinking_blocks() 会把 <think> 段删掉------推理过程被用掉,但不占摘要空间。
给它专属栏位和句式模板。 输出格式里有一节 ## Errors & Corrections,直接给了识别特征:"don't do X"、"actually I meant Y"、"that's wrong because...",并要求逐字捕获,理由写得很清楚:这些就是学到的偏好。给句式比给抽象定义可执行得多。
规定牺牲顺序。 preserve: user corrections > errors > active work > completed work。它承认压缩必然有损,然后预先排好了谁先被牺牲。用户纠正排第一,比"已完成的工作"更该留------因为不对称:已完成的工作丢了模型可以重做或重查,用户纠正丢了模型会退回被否决过的行为,然后用户得再纠正一遍,这是最伤信任的失败。
另一条经验是重的权重给近期,因为对话尾部才是当前工作区;同时明确要求丢掉客套和"Great question"之类的填充。
四、正确性没有自动裁判
前三组都在处理"怎么把事做出来",这一组处理"怎么知道做对了"。它是 misc 里最不像技巧、最像方法论的一组。
building_evals 把评测拆成四件东西:输入提示、模型输出、黄金答案、分数。然后指出真正的成本结构:写题是一次性成本,判分是永久成本。题目和黄金答案写一遍很少重写,但只要你还在改提示词,就要一遍遍重新判分。所以设计评测时应该围着"判分能不能又快又便宜"来做选择。
它给的不只是理念,是一副可以直接抄的四段骨架,三种判分方式共用:
python
# 1. 输入模板
def build_input_prompt(animal_statement): ...
# 2. 评测集:input + golden_answer(实际用 jsonl / csv)
eval = [
{"animal_statement": "The animal is a human.", "golden_answer": "2"},
{"animal_statement": "The animal is a snake.", "golden_answer": "0"},
]
# 3. 跑一遍
outputs = [get_completion(build_input_prompt(q["animal_statement"])) for q in eval]
# 4. 判分------只有这一块随方式变
grades = [grade_completion(o, q["golden_answer"]) for o, q in zip(outputs, eval)]
print(f"Score: {sum(grades) / len(grades) * 100}%")
换判分方式只换第 4 步,前三步一字不动。 这是它最实用的地方。
三种判分方式各有位置,而且差别不只在判分代码上。代码判分 的 grade 函数就一行 output == golden_answer,功夫全在题目设计上------任务是"数腿",prompt 里明确要求"只返回整数,别的都不要",max_tokens=5。题目被刻意设计成可精确匹配的形状,这才是"只差一点巧妙设计"的实指。三道题的选法也有讲究:人(2)、蛇(0)、"狐狸掉了一条腿又长回来还多长一条"(5)------一道常规、一道零值边界、一道需要真推理的构造题。
人工判分 换掉的不是判分代码,而是 golden_answer 的性质:它不再是答案,而是一份给人看的检查清单 ,连反例都写进去("要有 50 次以上的拉类腿部动作,比如硬拉,但深蹲不算,那是推类 ")。另一道题更妙:要求助手拒绝发邮件,并穷举了什么算错------可以给草稿,但不能尝试发送、不能调用发信函数、也不能反问该发到哪个邮箱。这是在测能力边界的自我认知,而且把判定标准落到了具体行为。
模型判分的关键洞察是:上面那份给人看的清单不用改,直接当 rubric 喂给 grader:
python
def build_grader_prompt(answer, rubric):
return f"""...
<answer>{answer}</answer>
<rubric>{rubric}</rubric>
An answer is correct if it **entirely** meets the rubric criteria, and is otherwise incorrect.
First, think through whether the answer is correct inside <thinking></thinking> tags.
Then output either 'correct' or 'incorrect' inside <correctness></correctness> tags."""
三个设计点:先 thinking 再结论 (判分本身也需要推理)、结论装进标签 便于正则抽取、判定写死为 entirely meets(不留"部分符合"这种模糊档)。抽取那段还特意在找不到标签时抛异常而非静默返回------判分器坏了必须炸出来,不能悄悄算成答错。
这里正好能看到第一节和这一节的接口:grader 用的就是 XML 标签方案。今天可以换成 structured outputs(Literal["correct","incorrect"]),但必须把 reasoning 字段放在 verdict 前面------判分器是最容易踩字段顺序那个坑的场景,顺序一反,判分就变成了先表态再找理由。
这篇最有杠杆的一句建议是:很多时候你和一个可自动化的评测之间只差一点巧妙的设计,常见手法是把开放题改写成选择题。注意这是在说,为了让停止判据可被机器核验,可以反过来调整题目的形式。判据的可判定性优先于题目的自然性。
它的另一条建议反直觉但正确:宁要高数量低质量的题,不要极少量高质量的题。低质量在这里不是说题目错,是说单题的精细度低。因为评测的作用是检测分布上的变化,样本量比单题打磨更重要。同时评测的分布应该尽量贴近真实场景里问题和难度的实际分布------不然你优化的是一个虚构的场景。
generate_test_cases 解决前一篇留下的空白:题从哪来。你手上有一个带变量的提示词模板,但没有真实数据,或者有但因隐私不能用。那就让 Claude 造。它把这件事做成了一串可复用的小函数------extract_variables() 用正则抽出 {``{VAR}}、construct_variables_block() 生成"每个变量一个 XML 槽位"的输出格式、construct_example_block() 把 {变量: 值} 字典转成 <example> 块。
它的生成提示词里有两点值得抄。一是让模型先在 <planning> 里想清楚:这个变量在生产环境里由谁提供? 人类终端用户写的、从网站下载的、还是从数据库抽的?除语义内容外还要考虑长度、格式、语气。这是在追问变量的来源 而不是内容------来源决定长度、格式和噪声,也就决定了合成数据像不像真的。二是给了示例时的额外要求:新样例要来自同一分布,但和已有样例足够不同以提供额外信号。这句正好对上前面那条"分布要贴近真实"。
这篇还有个容易被略过的细节:它把 Claude 的生成规划 和生成结果分开展示,然后示范去编辑那段规划------比如加一句"ACME 的文档使用编号行",重新生成,结果就带上了编号问答。改的不是输出,是产生输出的那套约束。 改输出只修一次,改规划修的是之后所有次。(实现上它用 prefill 把规划文本塞回 assistant 消息,4.6 之后要改成把要求追加成普通指令。)
而且这些合成用例有双重用途:既是评测题,也是提示词里的 multishot 示例。得到黄金答案的路径也给了:自己从头写,或者让 Claude 写一版然后你改。
metaprompt 是这组的极限形态------一个用来写提示词的提示词。它本身是一个塞了六七个优秀提示词范例的超长 multi-shot prompt,你输进任务描述和想要的变量名,它输出一个提示词模板,然后 notebook 会拿你给的示例值试跑一遍。它对自己的定位很清醒:解决"空白页问题",给你一个可迭代的起点,不承诺最优,只适合单轮问答不适合多轮。
五、外部世界如何进入上下文
剩下三篇------SQL、PDF、网页摘要------都在做同一件事:把外部数据喂给模型。并排放,能看出三种失真程度。
how_to_make_sql_queries 喂进去的不是数据,是数据库表结构 。它从 SQLite 查出表信息,拼成一段 CREATE TABLE EMPLOYEES (id INTEGER\nname TEXT ...) 的文本,再当普通字符串插进提示词,然后用自然语言提问、拿回 SQL、在真库上执行。(这个 schema 和第一节那个 JSON Schema 只是同名:这里的是喂进去的知识,作用在输入侧,模型完全可以编一个不存在的列名;那里的是套上去的约束,作用在解码侧,非法 token 根本不在候选集里。)
模型看到的是结构而不是内容,它的工作是翻译而不是检索。而生成的 SQL 要被 cursor.execute() 真实执行------列名错了、语法错了,当场抛异常。执行环境本身就是个免费的验证器,比任何自我声明都硬。至于怎么让 SQL 干净地出来,这篇用的还是一句叮嘱("Only output the SQL query and nothing else"),返回值直接喂给执行器,没做任何防护;今天该让它从 structured outputs 或一个 run_query(sql: str) 工具里出来。
pdf_upload_summarization 喂进去的是 base64 编码的 PDF,走 API 的 document block。notebook 特意点明这种方式对文字和视觉元素(图表)都有效------PDF 是作为文档整体进入的,不是被预先抽成纯文本。
read_web_pages_with_haiku 喂进去的是 requests 抓回来的网页文本。这是三者里最粗糙的一种:网页的结构、位置、层级在抓取那一刻就丢了,剩下一团文本。
失真的代价,using_citations 那篇已经替它们回答了:文档如果是以拼字符串的方式进入提示词的,它就没有可被引用的坐标------你事后无法说清某个结论落在原文哪一句、哪一页。以 document block 形式进入,才有 char location、page location 这些可追溯的位置。
所以这三篇的真正课题不是"怎么喂进去",而是你在入口处丢掉的东西,后面所有环节都拿不回来。选 requests 抓文本还是选 document block,不只是麻烦程度的差别,是这条链路末端有没有能力做溯源的差别。
六、过期的技巧与不过期的判断
misc 的时间跨度是这个目录的隐藏信息。pdf_upload_summarization 标注 2023 年 8 月,session_memory_compaction 标注 2026 年 1 月。中间两年半,好几篇里的具体手法已经作废了。
能力缺口会被平台补上,接口设计的判断不会过期。 引用从提示技巧变成 API 功能,压缩从手写线程变成 SDK 能力,JSON 从 prefill 兜住变成 schema 在解码层保证------被替换掉的都是实现手段,被保留下来的是那个判断:引用需要不可伪造的坐标,压缩需要提前而非临时发生,输出形状需要机制而非叮嘱来保证。
"输出形状"这一条的下沉轨迹尤其清楚,它一共换了三次载体:提示词要求 (输入侧,要经过"理解并遵循"这个概率环节)→ prefill (输出侧,直接写入序列首位,跳过那个环节)→ structured outputs(解码侧,不合 schema 的 token 在采样时就不在候选里)。每一次都比上一次更靠近底层,每一次都让"模型不配合"这件事更加不可能发生。判断没变,只是终于被放到了它该在的位置。
这三条判断,在 prefill 存在的时候成立,在 prefill 消失之后依然成立,而且正是因为它们成立,平台才会把它们做进 API。今天的 API 功能,是昨天正确的 workaround 的化石。 反过来说,你今天为某个缺口设计的 workaround,如果背后的判断是对的,它大概率会在某个版本里变成官方能力;如果判断是错的,它会在某个版本里彻底失效且无人替补。
所以读 misc 的正确姿势是读它的"为什么",别抄它的"怎么做"。抄的时候先查一遍:这个技巧依赖的机制还在吗?
收束
把 14 篇压到一句话:模型不是函数,它是一个有价格、有形状、有硬边界、且不自带裁判的接口;能不能用好它,取决于你在它之外补了多少不依赖它配合的结构。
这句话在四组里各自的样子是同一个形状。JSON 靠 stop sequence 和 XML 标签兜住,不靠"请你输出 JSON"这句请求;引用靠必须落在传入 block 上的坐标兜住,不靠模型不编造来源的自觉;上下文预算靠缓存布局和批处理管住,不靠提醒模型省一点;上限撞击靠预先算好的续写和后台备好的摘要接住,不靠祈祷不撞上;正确性靠代码判分和外部 grader 判定,不靠模型说自己答对了。
每一次都是同一个动作:把一件本来依赖模型自觉的事,挪成一件模型无法不配合的事。 模型的配合会随上下文变长而衰减,会因一次压缩而丢失,会在版本更替时改变------挪到外面的结构不会。
这也解释了 misc 这个名字为什么误导。它们不是别处放不下的边角料,它们是别处都要用的公共层。RAG 要用它的缓存和引用,agent 要用它的压缩和续写,任何一次提示词优化都要用它的评测和合成数据。它们之所以不属于任何一个 capability,是因为它们在 capability 底下。
真正的读法也就清楚了:不用按顺序读完 14 篇。先读 prompt_caching,因为成本结构决定了你后面所有设计的可行边界;再读 building_evals,因为没有判据的优化全是玄学;然后按你撞上的墙去挑------输出形状不稳去看 JSON mode 和 citations,长会话崩了去看 session memory compaction,没有测试数据去看 generate_test_cases。
至于 read_web_pages_with_haiku 这种四十几行的 notebook,看一眼知道它在那儿就够了。它的价值不在代码,在提醒你:入口处的失真是不可逆的。