模型不是函数:claude-cookbooks/misc 十四篇的公共底层

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 秒 都是给状态空间爆炸设的闸
minimummaxLength 这类约束不支持 数值区间和长度不是正则 / CFG 能表达的
enum 大小写不保证 说明它不是纯字面量匹配,token 边界和大小写变体上有松弛
语法只作用于直接输出,各段之间状态重置 自动机是分段挂载的,不是全程挂着
和 citations 互斥 citation block 要和文本交错插入,会打断状态连续性

其中 minimum: 100 那条最能说明边界在哪:它不被支持不是产品偷懒,是语法层面表达不了。

于是 SDK 那层就不只是格式转换,它会改你的 schema :删掉不支持的约束(minimummaxLength 之类)、把被删的约束改写进字段 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 做过对照实验:只把 reasoningverdict 后面挪到前面,边界样例上的判决全部翻转。

顺带记住一条: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_memorylast_summarized_indextokens_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,看一眼知道它在那儿就够了。它的价值不在代码,在提醒你:入口处的失真是不可逆的。

相关推荐
冬奇Lab1 小时前
开源项目第189期:DeepTutor — Agent 原生的终身个性化学习工作台,三层记忆+多引擎RAG+Partners
人工智能·开源·资讯
王莹月1 小时前
生图API 出问题怎么定位?给调用加 traceId 和结构化日志(nano-banana-pro)
gpt·ai·chatgpt·ai作画·aigc·agi
小柯南敲键盘1 小时前
电商图片翻译工具推荐,批量处理主图视频字幕,免费试用
大数据·人工智能·python·音视频
一次旅行1 小时前
2026.08.16 AI产业深度解读|国产大模型全面突围,算力硬件/智能安全/人形机器人四大趋势附落地方案
人工智能·安全·机器人
观远数据1 小时前
零售连锁BI选型清单:什么样的场景适合ChatBI,什么样的场景不适合
大数据·人工智能·零售
IT_陈寒2 小时前
Vue的双向绑定把我坑惨了,原来这个场景不能用
前端·人工智能·后端
阿甘编程点滴2 小时前
多角色对话AI配音工具技术解析与功能实测汇总
人工智能
IT爱学堂2 小时前
尚硅谷 - 2025年3月Java+AI大模型应用开发
java·开发语言·人工智能
水镜AI2 小时前
LangGraph 结构化输出:为什么 with_structured_output 一遇到 list 就翻车?
人工智能