Prompt Engineering 与输出处理
指令怎么写、放哪儿,结果怎么收、怎么不信它
本章管一次调用的两个端口:入口——指令写成什么样、放在哪个组件里(TS 6.2);出口——结果怎么变成你代码里可以信任的数据(TS 6.3)。两端由同一条纪律串起来:你写进去的是意图,拿回来的是待校验的数据。中间那段「上下文里该放什么、怎么裁」不在本章,在 ch11。
vb: 登记。没有官方措辞支撑的工程做法(例如第七节的代码片段结构)会显式标注为「工程实践」,不冒充产品行为。一、指令住在哪:system、user,还是对话中途
TS 6.2 把 system vs. user placement 与「prompt/instruction 在组件间的放置」并列写进同一条 skill。这不是排版问题,是三个不同的生效时点。
| 指令的性质 | 放哪儿 | 依据 |
|---|---|---|
| 从会话第一轮就要生效的全局指令、角色设定 | 顶层 system 字段 | 官方:想加 system prompt 就用顶层 system 参数;messages 的第一条不能是带内容的 system message |
| 本轮的任务、数据、可变输入 | user turn | messages 是 user / assistant 交替的回合序列 |
| 对话进行到一半才需要生效的新指令 | mid-conversation system message | 可在一个 user turn 之后放 "role": "system" 的消息,它与顶层 system 同等权威 |
role 该放哪儿是有官方答案的:在 system prompt 里设定一个 role,能把 Claude 的行为与语气聚焦到你的用例上,哪怕只有一句话也有差别。所以「你是一位资深财务分析师」这种句子属于 system,不属于每一轮的 user turn——放进 user turn 你要么每轮重复一遍(浪费预算),要么只说一次然后被后面几十轮稀释。
system 会让 system 层及其之后的 messages 层一起失效。同一条指令放在两个位置,正确性一样、账单不一样。完整推演在 ch02 第六节。input sanitization:外来的字节要有自己的槽位
TS 6.2 还点了 input sanitization。在 prompt 构造这一层,它的落点很具体:用户提交的文本、抓来的网页、工单正文,都要包进一个明确标注的 tag 里,而不是拼接进你的指令句子。官方对 XML tag 的说明正是这个用途——XML tag 帮助 Claude无歧义地解析复杂 prompt,尤其当 prompt 混合了指令、上下文、示例与可变输入时;把每一类内容包进自己的 tag(例如 <instructions>、<context>、<input>)能减少误读。
这是 ch01 P2「外来的是数据」在 prompt 构造层的形态。但要说清楚边界:包 tag 降低的是「模型把数据当指令读」的概率,它不是一道闸门。真正的注入防护——把敏感动作收到 guardrails 与 hooks 后面——是 TS 7.1 的地界,见 ch14。本章只负责「别把不可信内容和你的规则并排摆在一起」。
二、把话说清楚:官方给的可操作做法
官方对 instruction clarity 的说法与它给的检验法都值得背下来:Claude 对清晰、明确的指令反应好;想要「超出基本要求」的行为就明确提出来,不要指望模型从含糊的 prompt 里推断。类比是「把 Claude 想成一个聪明但缺乏你们规范与工作流上下文的新员工」;检验法是那条 golden rule——把你的 prompt 拿给一个几乎没有该任务背景的同事看,如果他会困惑,Claude 也会。两条具体动作:对期望的输出格式与约束要具体;顺序或完整性重要时,用编号列表把指令写成顺序步骤。
加上「为什么」。官方说给指令附上背景或动机——比如解释为什么这种行为很重要——能帮 Claude 更好理解你的目标;而且 Claude 足够聪明,能从这个解释里泛化。文档给的例子很能说明问题:与其写 NEVER use ellipses,不如写「你的回应会被 TTS 引擎朗读,所以不要用省略号,因为 TTS 不知道怎么念它」。前者是一条孤立禁令,后者让模型能推广到你没列举的相邻情形。
控制输出格式的三条。① 告诉 Claude 该做什么,而不是不该做什么——与其「不要用 markdown」,不如「你的回应应由流畅的散文段落构成」;② 使用 XML 格式指示符;③ 让 prompt 的排版风格匹配你想要的输出风格——官方明说 prompt 里的排版风格可能影响回应风格,例如把 prompt 里的 markdown 去掉可以减少输出里的 markdown 量。第三条最反直觉,也最容易在题目里出现:你的 prompt 本身就是一个 few-shot 示例。
few-shot:示例是最可靠的格式控制手段
官方的定位是:示例是引导 Claude 输出格式、语气与结构最可靠的手段之一。三个要求——relevant(贴近真实用例)、diverse(覆盖边界情形,别让它学到你没想让它学的模式)、structured(用 <example> 包裹,多个示例外层用 <examples>);数量建议 3–5 个。zero-shot / single-shot / multi-shot 这条谱系与 token 代价的关系在 ch06 第九节。
三、iterative refinement:prompt 是要被迭代的构件
TS 6.2 把 iterative refinement 与 prompt adjustment 并列点名。这里要分清两个同名的东西。
- 开发期的迭代:改 prompt → 在一组样例上跑 → 看差异 → 再改。这是工程流程,落点是版本控制与评测集;prompt 作为有版本的构件该怎么管,在 ch04 第四节。
- 运行期的迭代:官方在减少 hallucination 的进阶手法里给了一条同名技术——iterative refinement:把 Claude 的输出作为后续 prompt 的输入,让它校验或展开此前的陈述,从而抓出并纠正不一致。
题目里要分清这两者。「上线后错误率高,怎么办」如果答案是「让模型再自我检查一遍」,那是运行期迭代——它有官方背书,但它产出的仍然是模型输出,仍然要过第五节那道校验(这一点第七节会再说一次)。如果题干给的是「我们改了三版 prompt,每次都靠人工看几个例子」,那缺的是开发期的评测集与版本轴,不是再加一轮自检。
四、structured output patterns:三条路,以及少了的那一条
TS 6.3 的第一项是 structured output patterns。官方目前提供的路径有三条,选哪条取决于你要约束的是「它说什么」还是「它怎么调你的函数」。
路一:直接要求(prompting)
把期望结构写清楚,配 XML 格式指示符。官方在讲 prefill 迁移时给了一句对这条路的定性:先试试直接要求模型遵循你的输出结构,新模型在被明确要求时能可靠匹配复杂 schema,配上重试尤其如此。代价是它没有保证,需要重试与校验兜底。
路二:structured outputs(约束「它说什么」)
官方定义:structured outputs 把 Claude 的回应约束成遵循某个 schema,从而保证下游能拿到合法可解析的输出;它通过 constrained decoding 保证 schema 合规——总是合法(不再有 JSON.parse() 错误)、类型安全(字段类型与必填字段有保证)、可靠(不需要为 schema 违规重试)。
请求形状要认得:字段是 output_config.format,type 取 json_schema、schema 放你的 JSON Schema;原先的 output_format 参数已经移到 output_config.format,而且不再需要 beta header。响应侧:Claude 的响应是匹配你 schema 的合法 JSON,放在响应的 text content block 里——注意它不是一个新的 block 类型,你的取文本逻辑不用改。
限制要记,因为它们正是干扰项的产地:支持标准 JSON Schema,但不支持递归 schema、enum 里的复杂类型、外部 $ref、数值约束(minimum / maximum / multipleOf)、字符串长度约束(minLength / maxLength);数组的 minItems 只支持 0 与 1。
minimum / maxLength 这类约束不能靠 schema 强制,意味着「金额必须为正」「SKU 不超过 32 字符」这类业务规则必须留在你自己的校验层。structured outputs 保证的是形状,不是取值——这正好是第五节存在的理由。路三:strict tool use(约束「它怎么调你的函数」)
官方把两者的分工写得很干净:JSON outputs 控制 Claude 的回应格式(它说什么);strict tool use(strict: true)校验工具参数(它怎么调你的函数)。两者可以独立使用,也可以在同一个请求里合用——合用时 Claude 既能用保证合法的参数调工具,又能返回结构化的 JSON 回应,官方说这在「既需要可靠工具调用、又需要结构化最终输出」的 agentic 工作流里有用。工具定义本身(input_schema 与 description 的写法)在 ch12。
少了的那一条:prefill 不再是选项
这一条是本章最需要更新记忆的地方。从 Claude 4.6 各模型与 Claude Mythos Preview 起,最后一条 assistant turn 上的 prefill(给出一段部分 assistant 消息让 Claude 续写)不再被支持,带 prefill 的请求返回 400。更早的模型仍然支持;在对话的其他位置添加 assistant 消息不受影响。官方给的理由是模型的智能与指令遵循已经进步到多数 prefill 用例不再需要它。
迁移路径官方也写明了:想强制输出结构,Structured Outputs 就是为此设计的;分类任务用带 enum 字段的 tool 或 structured outputs;想去掉「Here is…」这类开场白,用 system prompt 直接指示、或让模型输出在 XML tag 内、或用 structured outputs/tool calling,偶有漏网就在后处理里剥掉。
| 你要约束的 | 选 | 它保证什么 |
|---|---|---|
| 回应文本的结构 | output_config.format | 形状合法,不保证取值正确 |
| 工具入参的结构 | tool 的 strict: true | 工具名与入参通过 schema 校验 |
| 两者都要 | 同一请求里合用 | 官方支持的组合 |
| 只是想让它别说废话 | 指令 + XML tag + 后处理 | 倾向,非保证 |
五、defensive parsing:出口处的四道关
TS 6.3 点了 response validation 与 defensive parsing。这一节是本章唯一给代码的地方,因为它考的正是「你的集成层长什么样」。
先把原则说死:模型输出一旦进入你的进程,它就是一段来自外部的数据。这是 ch01 P7。下面这段是把它拆成四道关的一个可运行骨架——结构是本书的工程实践,不是产品行为;每一道关背后的产品事实各自挂了出处。
import json, jsonschema
REPLY_SCHEMA = {
"type": "object",
"required": ["invoice_id", "total_cents", "line_items"],
"properties": {
"invoice_id": {"type": "string"},
"total_cents": {"type": "integer"},
"line_items": {"type": "array", "items": {
"type": "object",
"required": ["sku", "qty", "unit_cents"],
"properties": {"sku": {"type": "string"},
"qty": {"type": "integer"},
"unit_cents": {"type": "integer"}}}},
},
}
def handle(resp):
# 关 1 · 先读 stop_reason,再读 content
# 取值 max_tokens 表示输出被截断——一段"看起来完整"的 JSON 也可能是半截
if resp.stop_reason == "max_tokens":
return Degraded("truncated", raw=resp)
# 关 2 · 解析。模型输出是外部数据,json.loads 一定要包在 try 里
text = "".join(b.text for b in resp.content if b.type == "text")
try:
data = json.loads(text)
except json.JSONDecodeError as err:
return Degraded("unparseable", raw=text, err=str(err))
# 关 3 · 校验形状。字段缺失/类型不对走降级分支,而不是让 KeyError 冒到调用方
try:
jsonschema.validate(data, REPLY_SCHEMA)
except jsonschema.ValidationError as err:
return Degraded("schema", path=list(err.absolute_path), err=err.message)
# 关 4 · 交叉校验。形状合法 != 数值自洽,这一关 schema 替你做不了
computed = sum(li["qty"] * li["unit_cents"] for li in data["line_items"])
if computed != data["total_cents"]:
return Degraded("inconsistent", computed=computed, claimed=data["total_cents"])
return Ok(data)
四道关各自对应一个真实的失败模式:
| 关 | 挡住的失败 | 依据 |
|---|---|---|
1 · stop_reason | 输出被截断,JSON 是半截 | 取值 max_tokens 表示触到了请求的 max_tokens 或模型上限;4.5 及更新模型上还有 model_context_window_exceeded |
| 2 · parse | 不是合法 JSON | 没开 structured outputs 时这是可能的;开了则官方保证「不再有 JSON.parse() 错误」 |
| 3 · schema | 字段缺失、类型不对 | 同上;structured outputs 保证必填字段与类型 |
| 4 · 交叉校验 | 形状合法但取值错 | schema 里连 minimum 都不支持,业务不变量只能你自己算 |
开了 structured outputs 之后,关 2 与关 3 还要不要留?要。理由有两层:其一,关 1 与关 4 不在 structured outputs 的保证范围内,而它们恰好是最贵的两类错误(半截数据入库、金额对不上);其二,schema 不支持数值与长度约束,业务不变量本来就得自己校。能删的是重试逻辑,不是校验逻辑。
Degraded,而不是抛同一个异常。原因码决定下一步:truncated 可以提高 max_tokens 重试;unparseable 值得重试一次并记录原文;schema 通常是 prompt 或 schema 的问题,重试无益,该进人工队列;inconsistent 最危险——它形状合法、能入库、错得安静,必须硬失败。「所有失败都重试」与「所有失败都报警」都是错的,分类才是。(本段为工程实践,无 vb: 出处。)六、skepticism toward confident output
blueprint 用的原话是「对自信输出保持怀疑」。官方文档里有一整页在讲它的机制面。
定性:连最先进的语言模型(包括 Claude)有时也会生成事实错误或与给定上下文不一致的文本,这个现象叫 hallucination,它会损害 AI 方案的可靠性。注意这句话里没有「模型会告诉你它不确定」这一层——语气的笃定程度与内容的正确程度不是同一件事,这正是 blueprint 那句 skepticism 的落点。
官方给的基础手法三条:
- 允许 Claude 说「我不知道」——明确给它承认不确定的许可,官方说这一条简单手法就能大幅减少虚假信息。工程含义:不给这个出口,模型面对信息不足时只能编。
- 用直接引用做事实锚定——长文档(>20k token)先让它逐字抽取原句,再基于原句执行任务。
- 用 citation 做校验——让它为每条断言引用出处;还可以在生成之后让它为每条断言找一条支撑原句,找不到就必须撤回该断言。
进阶四条:chain-of-thought verification(开 thinking 并设 display: "summarized",答案看起来不对时审阅 thinking block 里的推理摘要,暴露有缺陷的逻辑)、best-of-N verification(同一 prompt 跑多次比对输出,输出之间的不一致可能指示 hallucination)、iterative refinement(第三节讲过)、external knowledge restriction(明确要求只用所提供文档里的信息,不用通用知识)。
第一条的官方写法改过:本书 2026-08 登记时它还是「让 Claude 先逐步解释推理再给最终答案」,旧资料也多这么写。现在别这么做:官方要求读 thinking block 而不是在回应正文里索要推理,在 Claude Fable 5.1、Opus 5.5、Opus 5、Sonnet 5.5 与 Fable 5 上,试图让模型把内部推理写进正文的请求可能直接被拒,stop_details.category 为 "reasoning_extraction"。
而全章最该记住的收口是官方自己加的那条免责:这些技巧显著减少 hallucination,但并不能彻底消除它们;关键信息永远要校验,高风险决策尤其如此。
把它和第五节接起来:prompt 侧的所有手法都在降低概率,出口处的校验才是那道确定性的关。「让模型自己再检查一遍」不等于校验——那份检查结果仍然是模型输出,仍然要过同一道关。这是 ch01 P7 边界那一段的完整依据。
output_config.format 的字段形状与响应位置、structured outputs 的 JSON Schema 限制、JSON outputs 与 strict tool use 的分工、response validation 与 defensive parsing 的四道关与降级路径分类、skepticism toward confident output 的官方依据——hallucination 的定性、三条基础手法、四条进阶手法,以及「不能彻底消除、关键信息永远要校验」)。相邻考点:一次调用的解剖、stop_reason 与 caching 层级见 ch02;prompt versioning 与 model pinning 见 ch04;token 预算、context window 与 zero/few-shot 谱系见 ch06;上下文里放什么、怎么裁见 ch11;tool description 与 input_schema 见 ch12;注入防护与 guardrails 见 ch14;集成层与模型输出的源头隔离见 ch16。本章题组
output_config.format with a json_schema requiring total_cents (integer) and line_items (array of {sku, qty, unit_cents}). Parsing never fails. But finance reports that 1 in 400 invoices posts with a total_cents that does not equal the sum of its line items, and one had qty: -3. The team's handler does json.loads then writes straight to the ledger. What should they change?minimum / maximum / multipleOf)。所以交叉校验只能落在你自己的校验层——这正是本章第五节的「关 4」。minimum、maximum、multipleOf 逐个被点名为不支持。这一项之所以像真的,是因为这些关键字在普通 JSON Schema 里确实存在。{"label": " so the reply always continues into JSON. During a migration to claude-opus-5, every request now fails with a 400 before generation. The migration ticket says "response format must stay machine-parseable." What is the correct migration?output-format-legacy 这个 header 是本书自制的干扰项,没有官方出处。它像真的,是因为 Anthropic 确实用 beta header 门控新特性——但官方对 prefill 的表述是「不再被支持」,给的是迁移路径而不是兼容开关。messages with "role": "system", and the API rejects it. A developer's fix is to prepend the same 900 tokens to every user turn instead. Sessions run 30+ turns; the team also relies on a cached prefix for the tool definitions and knowledge base. What should they do?messages 的第一条不能是带内容的 system message,从头生效的指令要用顶层 system 字段。该放哪儿:官方明说在 system prompt 里设定 role 能聚焦 Claude 的行为与语气。为什么不是每轮重复:顶层 system 每个请求发一次,而不是每一轮各发一份。messages 层、每轮内容都变,缓存前缀按 tools → system → messages 逐层失效。system 是每个请求都完整送达的。max_tokens: 1024. Its handler does json.loads(text) inside a try block and, on JSONDecodeError, retries the same request up to three times before giving up. Ops sees a steady 2% of documents burning all four attempts, always the longest documents, and the logged raw text always ends mid-string. Retries never help. What is the fix?stop_reason 取值 max_tokens 的官方释义正是「触到了请求的 max_tokens 或模型上限」。这就是本章第五节「关 1 在关 2 之前」的理由:一段解析失败的文本,先要知道它是格式错还是被切断。JSON.parse() 错误」。但它约束的是生成出来的内容符合 schema,解决不了「输出在 1024 token 处被硬切」这个源头;额度不够时该修的是额度。这一项覆盖的是另一类失败。user message. Since a customer began pasting long formatted logs into tickets, the bot sometimes summarizes the log instead of triaging, and once followed a line in the ticket that read "ignore previous formatting rules." What is the right first change to the prompt construction?system 是对的一半——从头生效的指令本来就该在那儿。但「原始工单正文直接作为整条 user 消息、不需要任何标记」把另一半丢了:不可信内容仍然没有被标注成数据,模型无从判断这段字节是任务还是素材。Do not use markdown. Output still contains bold and bullets in roughly a third of responses. The prompt itself is a heavily formatted markdown document with headers, bold key terms, and bulleted rules. The team wants adherence up without moving to a schema. What should they try first?temperature 控制的是采样随机性,不是格式遵循度;「调高一点让它别被锚定」是给一个真实参数安上它没有的职责。更实际的是它在 Claude 4.7 及之后的模型上已 deprecated,设成非默认值直接返回 400。approve_expense(expense_id, amount_cents, reason) tool with well-formed arguments, and (2) return a final report object the dashboard renders. Today both go wrong occasionally: malformed tool arguments cause 422s from the internal service, and the final report sometimes comes back as prose. Which configuration matches the requirement?tool_calls 数组,是本书自制的干扰项,没有官方出处。工具调用走的是 tool_use content block 与 tool_result 的往返,不是响应 JSON 里的一个字段;而 output_config.format 的产物是放在 text content block 里的 JSON。strict: true 确实保证工具入参过 schema,这半句对。但把最终报告塞成工具的一个额外参数,是把「给用户的输出」错层承载在「给内部服务的调用」上——dashboard 要渲染的东西会跟着一次审批动作一起被发去内部服务。