跳转至

第三章 Prompt 与上下文工程

导读

很多人把 Prompt 当"咒语"——觉得某个特定写法能让模型"开窍"。这种心态是错的。Prompt 不是咒语,是**条件概率引导**——它通过约束模型看到的输入来影响 P(output | input)。本章从概率视角解释 Prompt 本质,然后展开提示策略(Zero-shot / Few-shot / CoT / Self-Consistency / ToT)、角色与对话组织、结构化输出、Prompt 工程实战。读完本章你应该能在工程上写出可维护、可测试、可迭代的 Prompt,而不是"试到能用就提交"的玄学咒语。

3.1 Prompt 的本质

理解 Prompt 最根本的视角是概率——LLM 是条件概率分布 P(output | input),input 就是 Prompt,output 就是模型生成的 token 序列。改 Prompt 不是"激活某个开关",是改变条件概率的形状,让模型把质量分布压在你想要的输出区域。

下一 token 预测视角

LLM 生成时一步步预测:给定已有 token 序列 x_1, x_2, ..., x_n,模型输出下一个 token x_{n+1} 的概率分布 P(x_{n+1} | x_1...x_n)。整个输出序列的概率是每步条件概率的乘积。Prompt 就是这个序列的前缀——它决定了后续 token 的条件概率。

举例:你输入 "中国的首都是",模型预测下一个 token 是 "北" 的概率极高(>99%),然后是 "京"。但如果你输入 "中国有哪些城市",模型预测下一个 token 是 "北京"、"上海"、"广州" 之一的概率都较高,采样会从中选一个。Prompt 的微妙变化("是" vs "有哪些")改变了条件概率的形状——前者收窄到唯一答案,后者展开到多种可能。

这就是为什么"Prompt 工程"是有意义的——同一种任务用不同 Prompt 表述,效果差几倍。不是因为某种表述"激活了模型能力",是因为更精确的表述让模型把概率压在正确答案附近,减少了采样的随机性。

Prompt 概率视角的几个工程推论

从概率视角能推出几个工程结论。第一,Prompt 越具体,输出方差越小。"翻译成英文"比"翻译"具体,"翻译成美式英文,保持正式语气"更具体——越具体模型输出范围越窄、越稳定。第二,Prompt 措辞与训练分布匹配的更好。模型预训练时见过大量"问题→答案"格式,"Q: ... A: ..."这种格式让模型熟悉;写成 "请回复如下问题:..." 模型反而不熟。第三,Prompt 的尾部影响最大。生成时下一个 token 的条件概率主要看最近的 token——所以 prompt 末尾放最重要信息(任务、示例),开头放背景。第四,Prompt 改一个字可能让效果差几倍。因为模型对某些 token 的条件分布敏感,"列举 5 个城市"与"列举五个城市"可能让输出准确率差 5%——这就是为什么 Prompt 要 A/B 测试。

为什么 System Prompt 重要

System Prompt(系统提示)是消息列表里的一个特殊角色,OpenAI 与 Anthropic 都把它放在最高优先级位置。它的作用是定义"模型这次的默认行为"——你是客服、是编程助手、是法律顾问、是数据分析师。

System Prompt 之所以"重要"而不是"装饰",原因有两个。第一,位置优先。在 attention 里,序列前面的 token 对后面 token 的输出影响更大(causal attention 让前面看到后面,但生成时只看前面)——System Prompt 在最前面,对所有后续生成都有影响。第二,重复曝光。多轮对话里 user 与 assistant 来回多轮,但 system prompt 每轮都在 prompt 顶部,相当于"反复提醒"模型自己的角色。这就是为什么 system prompt 写"你是一位资深律师"会让模型回答更专业。

为什么 Few-shot 有效

Few-shot(少样本示例)是在 Prompt 里给几个"输入 → 输出"对,让模型从示例里学到任务模式。这在工程上叫 In-Context Learning(上下文学习)——模型不更新权重,但能从 prompt 里的示例"学到"做什么。

为什么 in-context learning 能工作?学术界还没完全搞清楚,但有几个解释。第一,示例作为模板:模型看到几对"输入 → 输出"后,会"模仿"这种模式生成。第二,示例作为分布约束:示例让条件概率 P(output | input, examples) 收窄到与示例同分布的空间。第三,示例触发了模型在预训练时学到的某种能力——可能模型在预训练时见过类似的"任务 → 示例 → 续写"模式。

工程上的直觉:few-shot 在格式固定、任务边界清晰的任务上效果好(分类、抽取、改写),在需要复杂推理的任务上效果不如 CoT——因为示例只能教"做什么",不能教"怎么想"。

Few-shot 与 Fine-tune 的边界

工程师经常纠结"上 few-shot 还是上 fine-tune"。判断标准是几个维度。第一,任务稳定性:few-shot 准确率到 90%+ 就别上 fine-tune,加 fine-tune 边际收益小、维护成本大;few-shot 准确率 60% 且加示例无效,再考虑 fine-tune。第二,示例数量上限:如果 few-shot 要 20+ 个示例才有效,prompt token 爆炸,此时 fine-tune 更经济(把示例"压缩"进权重)。第三,任务变化频率:任务每周都变 → few-shot(改示例即可);任务稳定 6 个月以上 → fine-tune(一次训练长期受益)。第四,延迟敏感度:fine-tune 让小模型达到大模型水平,延迟降一个量级——延迟敏感场景必上 fine-tune。

3.2 提示策略

下面五种策略从简单到复杂递增,每种适用不同场景。

提示策略对比

Zero-shot

最朴素的策略:直接给任务,不给示例。例如 "翻译成英文:今天天气真好"。Zero-shot 适合模型已经熟悉的简单任务——翻译、改写、摘要、分类——这些任务在预训练 + SFT 阶段见过大量数据,模型默认就会。

何时用:任务简单、模型强、追求 prompt 短(省 token)。何时不用:输出格式特殊、任务新颖、需要严格格式控制——这些场景 zero-shot 准确率会断崖式下降,加几条 few-shot 能提 10-30 个百分点。

Zero-shot 的工程注意事项

Zero-shot 听起来简单,工程上仍有几个细节。第一,任务描述要明确。"翻译"不明确(译成什么语言?),"翻译成英文"明确——任务描述越具体,输出范围越窄越稳。第二,输出格式要显式:要求 JSON 输出就明说"输出 JSON 格式",不要让模型猜。第三,Zero-shot 配 JSON mode 是最省 token 的组合:结构化任务上"短 prompt + JSON mode" 比"长 prompt with few-shot" 省几倍 token,准确率不差——能上 JSON mode 就上。

Few-shot

给 1-5 个示例,让模型从示例里学到任务模式。例如:

正面 → 积极
负面 → 消极
中性 → ?

Few-shot 适合新任务、定制格式、领域术语。何时用:要稳定输出格式、要做新任务(模型预训练没见过类似)、要引导某种风格。何时不用:示例会过长的任务(一个示例几百 token,prompt 直接爆),或任务太复杂几条示例学不会(要换 CoT)。

工程上的细节:few-shot 示例顺序影响结果——模型对最近的示例记忆更深(recency bias),把"想要"的输出风格放在最后效果更好;示例数量 3-5 个通常够,加更多收益递减;示例要保证质量,一个错示例能毁掉一批调用。

Few-shot 的高阶用法

工程师用 few-shot 时有几个进阶技巧。第一,动态 few-shot:根据当前输入从示例库召回最相关的几条作为 few-shot——本质是"用 RAG 给 prompt 选示例"。这种做法能让 few-shot 在不同输入下都贴合,准确率比固定 few-shot 高 5-15 个百分点。第二,示例去重与多样性:示例之间要保证多样性(覆盖不同输入分布),不要 5 个示例都是"情感分类"的正面例子——模型会以为只能输出"积极"。第三,示例与真实输入的格式严格匹配:示例的输入字段顺序、字段类型要和真实输入完全一致,否则模型会困惑。

第四,示例的"答案"要可靠:一条错示例能让模型学到错误模式,污染整批调用——所以 few-shot 示例要做"质检",最好人工 review 过、有 golden label 的样本。第五,few-shot 不是越多越好:OpenAI 与 Anthropic 都做过实验,3-5 个示例后准确率开始平台期,加更多示例 prompt token 涨但准确率不涨甚至下降(因为模型注意力被稀释)。一般 3-5 个示例是甜点。

CoT (Chain-of-Thought)

CoT 是 Wei 等 2022 年提出(arXiv:2201.11903),核心是让模型"分步推理再答"——在 prompt 里加 "Let's think step by step" 触发,或在 few-shot 示例里展示推理过程。

CoT 在数学、逻辑、多步推理任务上能提 10-30 个百分点。原理:LLM 单次前向只能算有限"计算步",分步推理相当于把复杂问题拆成多个简单预测,每步只用模型擅长的事。例如 "23 + 47 × 3 = ?" 直接算容易错,先 "47 × 3 = 141",再 "23 + 141 = 164" 就对了。

何时用:数学题、逻辑题、多步推理、规划任务。何时不用:简单任务(翻译、分类)反而降准确率——因为模型在简单任务上本来就直接答对,加 CoT 反而让它在思考链里走偏。这是工程上的常见误用:以为"加 CoT 总比不加好",其实在简单任务上效果相反。

CoT 的几种变体

CoT 有几个工程上常用的变体。第一,Zero-shot CoT:直接在 prompt 末尾加 "Let's think step by step" 触发——最简单但效果不如有示例的 few-shot CoT。第二,Few-shot CoT:在 few-shot 示例里展示完整推理链,模型模仿这种推理风格。第三,Auto-CoT:让模型自动给未标注数据生成推理链作为 few-shot 示例,省人工标注。第四,Self-Consistency:CoT 跑 N 次投票选众数,进一步提准确率。

工程上选哪个变体的标准是:任务难度越高越用复杂变体。简单数学用 zero-shot CoT 即可;难题(奥赛级)用 few-shot CoT + self-consistency;超难题(如 ARC challenge)才上 ToT。每个变体的成本是前一档的 5-10 倍,盲目上重链是浪费。

Self-Consistency

CoT 的进阶版(arXiv:2203.11171):同一个问题让模型用 CoT 跑 N 次(温度 >0),从 N 个答案里投票选众数。核心思路是:单次 CoT 可能走错,但 N 次推理中正确的路径大概率占多数。

工程上:N=5-10 次是甜点,再多收益递减但成本线性涨;只对"答案是离散值"的任务(多选题、yes/no、数值计算)有效,开放生成任务没有"众数"概念;成本是单次 CoT 的 N 倍,要权衡准确率提升 vs 成本翻倍。

Self-Consistency 的几个细节

Self-Consistency 工程上的几个细节。第一,温度不能太低:温度 0 时 N 次输出都一样,self-consistency 退化成单次 CoT。建议温度 0.5-0.8,让 N 次输出有差异但不太乱。第二,投票方式有讲究:简单任务用众数投票;数值任务用均值或中位数;多选题可以用加权投票(按模型自己给的置信度)。第三,N 选多大:成本敏感场景 N=3 也有效(比单次提升 5-10 个百分点);准确率优先场景 N=10 是甜点,超过 10 边际收益快速递减。第四,结果不一致是信号:N 次答案都不一样,说明任务难度高或 prompt 不清——这种情况要重新审视 prompt 而不是盲目加大 N。第五,只对离散答案有效:开放生成(写诗、写总结)没有"众数"概念,self-consistency 无意义;要做开放生成的一致性,得换成"让裁判选最好的一个"而不是投票。

ToT (Tree-of-Thoughts)

ToT(arXiv:2305.10601)把 CoT 的"线性思考"扩展成"树状思考"——生成多个候选思路,评估每个思路的好坏,选最优的继续展开,必要时回溯。本质是把 LLM 当搜索引擎用,用"评估器"打分引导搜索方向。

ToT 适合搜索型任务(解谜、规划、创意生成),但成本远高于 CoT——一次任务可能调几十次 LLM。何时用:任务有明确的"成功状态"(如解 24 点、走迷宫),失败成本高(值得多花算力换准确率)。何时不用:常规任务(聊天、问答、翻译)——成本太高,提升却有限。

提示策略的工程选型表

工程师选提示策略时建议按这个顺序:先 Zero-shot 跑 baseline;准确率不够加 Few-shot;仍不够看任务类型——简单格式任务加更多 few-shot 或换 fine-tune;复杂推理任务加 CoT;CoT 不够稳定上 Self-Consistency;搜索型任务才上 ToT。每次升级都要评测准确率/成本/延迟的 trade-off——盲目上重链是工程上的浪费,朴素策略能解决问题就别升级。

3.3 角色与对话组织

LLM API 不接受单一字符串作为输入,它接受"消息列表"——每条消息有 role 与 content。这是为了让模型理解多轮对话与不同发言者。

四种角色

  • system:系统提示,定义模型行为。优先级最高,每轮都在 prompt 顶部。例:"你是一位资深律师,回答专业法律问题。"
  • user:用户消息,发起请求。例:"劳动合同法对加班费的规定?"
  • assistant:模型回复。多轮对话里 assistant 的历史回复会被包含在后续 prompt 里,让模型"记得"自己之前说过什么。
  • tool(OpenAI)/ tool_result(部分框架):工具调用结果。Function Calling 工具返回的数据用这个角色塞回 prompt,让模型基于工具结果继续生成。

OpenAI 与 Anthropic 消息格式差异

两边 API 格式有几个工程差异要注意。第一,system 位置不同。OpenAI 把 system 作为 messages 数组的第一条(role: "system");Anthropic 把 system 单独作为顶层字段(不在 messages 数组里)。第二,content 类型。OpenAI content 是字符串或多模态数组(图文混合);Anthropic content 必须是数组,每个元素有 type(text/image/tool_use/tool_result)。第三,tool 调用返回。OpenAI 用 role: "tool" + tool_call_id;Anthropic 用 role: "user" + content type: "tool_result" + tool_use_id。

工程上的影响:写跨平台兼容代码时要用适配层,比如 LangChain 的 BaseMessage 抽象、或自己写转换函数。不要硬编码 OpenAI 格式——换 Anthropic 时会出错。

多轮对话模板

一个完整多轮对话的消息列表示例:

[
  {"role": "system", "content": "你是有用的助手"},
  {"role": "user", "content": "什么是 RAG?"},
  {"role": "assistant", "content": "RAG 是检索增强生成..."},
  {"role": "user", "content": "它解决了什么问题?"},
  {"role": "assistant", "content": "知识截止与私有数据..."}
]

每轮调用 API 时把完整历史传过去——这就是为什么多轮对话 token 成本随轮数线性涨。Agent 系统里跑 50 步的对话历史可能 50K+ token,每步调用都要重传,是成本爆炸的根源。解决方案见第九章"记忆与上下文工程"。

多轮对话的几个工程陷阱

工程师做多轮对话产品时容易踩的坑有几个。第一,assistant 历史不能漏 tool_calls 字段。如果上一轮 assistant 调用了工具,消息体里要有 tool_calls 字段;只存 content 字符串会丢失工具调用上下文,模型会困惑"我之前调过工具吗"。第二,tool 消息的 tool_call_id 要匹配。OpenAI 与 Anthropic 都要求 tool result 与 assistant 的 tool call 通过 id 关联——id 不匹配会被 API 拒绝。第三,多轮 token 成本管理。一个 50 轮对话如果每轮 prompt 都重传完整历史,单轮 input token 涨到几十 K——要做历史压缩(旧消息摘要)或滑窗(只留最近 N 轮),但要注意滑窗会让模型"忘"早期对话。

第四,assistant 历史里有幻觉内容会污染后续生成。如果模型上一轮答错了,下一轮它会"基于自己的错误答案继续推理"——多轮幻觉会滚雪球。生产系统要做"事实核查"或"置信度评估",低置信度的回答触发澄清或人工接管。第五,System Prompt 在多轮里要稳定。不要每轮都改 system prompt——会让模型对自己的角色认知混乱。要改角色就开新对话。

3.4 结构化输出

很多场景模型要输出可解析的结构化数据——JSON、表格、SQL——而不是自然语言。结构化输出的核心问题是:LLM 是概率模型,没法保证输出 100% 符合 schema。工程上要靠多种手段逼近"必出结构化"。

结构化输出

三种实现方式

第一,Prompt 引导:在 prompt 里说"输出 JSON,字段如下...",给 few-shot 示例。最简单但不可靠——模型偶尔输出带解释的 JSON 或字段缺失。第二,JSON Mode:OpenAI 与 Anthropic 都提供 response_format={"type": "json_object"},强制模型输出合法 JSON(但不保证 schema)。第三,Function Schema / Tool Use:把 schema 定义为 tool,让模型填 tool 参数——这种方式 schema 强校验,模型生成错误 schema 会被 API 拒绝。

结构化输出的工程要点

工程上写结构化输出有几个要点。第一,Schema 要尽量"窄":字段越少越好、枚举值越封闭越好。例如 severity 字段用 enum [1, 2, 3, 4, 5] 比用 int 范围更稳——模型只能从 5 个值里选,不会出 6 或 0。第二,字段说明要详细:每个字段在 schema description 里说清楚含义、单位、示例——模型对说明详细字段输出更准。第三,避免嵌套太深:JSON 嵌套超过 3 层模型容易出错——能拍平就拍平。第四,模型选型要支持 JSON mode:GPT-4o / Claude 3.5 都支持,部分开源模型 JSON mode 不稳定,要测试。第五,做重试 fallback:schema 校验失败时重试 1-2 次,仍失败 fallback 到规则系统或人工接管——不要无限重试烧 token。

为什么结构化输出要配 schema 校验

工程上的铁律:结构化输出必设 schema 校验。原因有几个。第一,模型会"幻觉"字段。即使 prompt 明确说"输出 user/order_id/amount 三个字段",模型偶尔会加个 timestamp 字段或漏 amount。第二,类型不可靠。amount 字段 prompt 说数字,模型可能输出 "99 元" 这种字符串。第三,模型输出顺序漂移。同一 prompt 多次调用,字段顺序可能不同——如果下游代码用位置解析就会出错。

工程上的标准做法是用 Pydantic 定义 schema,模型输出后用 Pydantic 校验:

from pydantic import BaseModel, ValidationError

class RefundRequest(BaseModel):
    user: str
    order_id: str
    amount: float
    reason: str

try:
    data = RefundRequest.model_validate_json(llm_output)
except ValidationError as e:
    # 重试或 fallback
    ...

校验失败时工程上有两种策略:一是重试(带"上次输出错误"反馈让模型修正),二是 fallback 到规则系统(用正则从原文本抽字段)。生产系统建议两层都要——重试 1-2 次失败就 fallback,不要无限重试烧 token。

XML 与 JSON 的选择

工程界有 JSON vs XML 的争议。JSON 优点是生态成熟、Pydantic 校验强、API 原生支持;XML 优点是模型容易包裹长文本(<answer>...</answer>),适合"长文本 + 结构化字段混合"场景。Anthropic Claude 在内部测试中发现 XML 标签能让模型输出更稳定——这是为什么 Claude 的 prompt 模板常用 <example><instructions> 这种 XML 风格。

工程上的选择标准:纯结构化数据用 JSON;混合内容(长文本 + 标注)用 XML 或 Markdown 标题;Agent 工具调用用 JSON Schema(OpenAI / Anthropic 都支持)。

结构化输出与 Function Calling 的关系

工程师常问:结构化输出与 Function Calling 是不是同一件事?技术上不是,但工程上常混用。Function Calling 是"模型生成 tool_use 块(含 tool name + arguments JSON)",arguments 是结构化的——所以 Function Calling 是结构化输出的一种特殊形式(schema 在 tool definition 里定义)。OpenAI 的 response_format={"type":"json_schema", "json_schema":{...}} 是直接 schema 强约束的纯结构化输出,不绑定 tool。

工程上选哪个:纯数据抽取(如分析评论 → JSON)用 response_format;要触发后续动作(如调用 API、查询数据库)用 Function Calling——后者的 schema 会自动校验且能触发下游 tool。Agent 系统几乎都用 Function Calling,因为它能自然衔接"模型决策 → 工具执行 → 结果返回"。

3.5 Prompt 工程实战

写 Prompt 不难,难的是"管理 Prompt"——多版本、多语言、A/B、评测、回滚。这一节讲工程化 Prompt 的几个要点。

模板化(Jinja2)

不要把 Prompt 硬编码在代码里——把 Prompt 文本放在 .j2 模板文件,用 Jinja2 渲染。这样 Prompt 改动不用改代码,PM/运营可以改模板不必走研发流程。模板里变量用 {{user_name}}、条件用 {% if debug %}...{% endif %},逻辑清晰。

版本化(git track prompt files)

Prompt 文件要进 git 仓库,与代码同版本管理。每个 Prompt 改动都是一个 commit,commit message 要说明"为什么改"——这个改动是为了修什么 bug?提升什么场景的准确率?这样后续 Prompt 退化时可以 git blame 找到原因。千万不要把 Prompt 写在数据库里——没有 git 历史的 Prompt 是不可维护的。

A/B 测试

Prompt 改动必须 A/B——不要凭直觉改完就上线。工程上的标准流程:定义评测集(golden set,几百到几千条带标注答案)→ 在旧 Prompt 上跑一遍记录 baseline → 在新 Prompt 上跑一遍 → 对比准确率/格式合法率/成本/延迟 → 显著优才上线。A/B 时建议先用便宜模型(GPT-4o mini)快速跑通,再上正式模型验证。

评测(evaluation)

Prompt 工程的核心反馈信号是评测。评测要分两类。第一,规则评测:格式合法率、字段完整率、JSON 解析成功率——能自动化的指标。第二,语义评测:答案对不对、是否符合用户意图——这种要靠 LLM-as-Judge(用一个 LLM 当裁判打分)或人工抽样。LLM-as-Judge 不完美(模型会有偏好),但比纯人工快、比规则全——工程上是事实标准。

LLM-as-Judge 的工程实践

LLM-as-Judge 是 2023 年后事实标准的 Prompt 评测方法。工程上有几个要点。第一,裁判模型要强:用 GPT-4o 或 Claude 3.5 Sonnet 当裁判,不要用小模型——小模型评不准。第二,打分要 rubric 化:不要说"给这个回答打 1-10 分",要说"按以下 rubric 打分:准确性(1-5)、相关性(1-5)、流畅性(1-5)"。rubric 化让分数更稳定可比。第三,pairwise 比 absolute 稳定:让裁判比较两个回答哪个更好(pairwise),比让裁判单独打分(absolute)稳定——人类更擅长比较,模型也是。第四,做抽样人工校验:随机抽 10% 让人工再评一次,看 LLM-as-Judge 与人工的相关性,相关性低说明裁判模型有偏,要换。

第五,建立 golden set 持续维护:评测集要几百到几千条,每条有标准答案。每次 Prompt 改动前后都在 golden set 上跑一遍,准确率提升显著才上线。golden set 要定期更新——任务分布会漂移,旧 golden set 失效后 Prompt 改进信号就失真。

Prompt 工程的几条反模式

工程上经常看到的 Prompt 反模式:第一,Prompt 太长——把所有规则都塞 prompt,模型反而记不住重点。每条 prompt 控制在 2000 token 内为佳,超长要拆。第二,Prompt 里堆叠"必须""一定要""绝对不能"——这些词反复出现说明 Prompt 没设计好,模型对否定约束执行不稳,正面陈述更好。第三,Prompt 里 few-shot 示例和实际输入分布不匹配——示例都是英文,实际输入是中文,效果差。第四,没有评测就改 Prompt——凭感觉改 Prompt 是新手最大的坑,每次改完都觉得"更好了",实际可能更差。

Prompt 工程的团队协作

Prompt 工程不只是工程师的事——产品经理、领域专家、运营都要参与。团队协作模式建议:领域专家写"任务定义与示例"(什么算对、什么算错、有什么边界情况),工程师写"模板与代码集成"(Jinja2 模板、API 调用、评测脚本),PM 写"产品要求与评测指标"(用户场景、准确率要求、成本上限)。三者分开但都要进 git——一个 Prompt 文件包含任务说明 + 示例 + 模板 + 评测,每次改动都要 PR review。这种"Prompt 是代码"的心态能让 Prompt 工程从玄学变成工程。

代码片段:结构化输出 + Few-shot + CoT

下面这段代码用 OpenAI API 做"客户投诉分类",结合 system prompt + few-shot + CoT 触发 + Pydantic 结构化校验:

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Complaint(BaseModel):
    category: str        # 产品质量 / 物流 / 售后 / 其他
    severity: int        # 1-5
    suggested_action: str

system = "你是一位客户投诉分类助手,仔细推理后给出结构化输出。"
few_shot = """
示例:
输入:购买的耳机三天就坏了,太失望了!
输出:{"category": "产品质量", "severity": 5, "suggested_action": "立即换新"}
"""
user = "物流太慢了,五天才到,包装还破了"
messages = [
    {"role": "system", "content": system},
    {"role": "user", "content": few_shot + "\n输入:" + user + "\n先一步步推理,再输出 JSON。"}
]
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    temperature=0,
    response_format={"type": "json_object"},
)
try:
    result = Complaint.model_validate_json(resp.choices[0].message.content)
    print(result)
except Exception as e:
    print(f"schema 校验失败: {e}")

这段代码示范了几个工程要点:system prompt 定角色、few-shot 引导格式、CoT 触发让模型先推理再答、response_format 强制 JSON、Pydantic 校验兜底。这是生产级结构化输出的最小骨架。

工程实战要点

  • CoT 在简单任务反而降准确率:CoT 适合数学/逻辑/多步推理;翻译/分类/改写这种简单任务加 CoT 反而让模型在思考链里走偏。先评测再决定是否加 CoT。
  • 结构化输出必设 schema 校验:模型会幻觉字段、漂移类型、漏字段——Pydantic / JSON Schema 校验是底线,校验失败要重试 + fallback。
  • Prompt 改动要 A/B:凭感觉改 Prompt 是新手最大的坑;定义评测集 → 跑 baseline → 跑新版 → 对比关键指标 → 显著优才上线。
  • Few-shot 例子顺序影响结果(recency bias):模型对最近的示例记忆更深;把想要"模仿"的示例放最后;3-5 个示例通常够,加更多收益递减。
  • System prompt 优先级高于 user prompt 但不能滥用:system prompt 定角色与全局规则,不要把每次都不同的内容塞 system;system prompt 越长,user 内容的相对权重越低。

小结

  • Prompt 不是咒语,是条件概率引导——它通过约束 P(output | input) 的形状影响模型输出。
  • 五种提示策略:Zero-shot(简单任务)/ Few-shot(定制格式)/ CoT(多步推理)/ Self-Consistency(提升准确率)/ ToT(搜索型任务)。
  • 四角色消息:system(定行为)/ user(提请求)/ assistant(历史回复)/ tool(工具结果)。OpenAI 与 Anthropic 消息格式有差异,跨平台要用适配层。
  • 结构化输出三种方式:Prompt 引导(不可靠)/ JSON Mode(合法 JSON 但不保证 schema)/ Function Schema(强校验)。必配 Pydantic / JSON Schema 校验。
  • Prompt 工程化四件套:Jinja2 模板化 / git 版本化 / A/B 测试 / 评测(规则 + LLM-as-Judge)。生产 Prompt 是工程产物不是玄学咒语。

练习题

  1. 用一句话解释 Prompt 是如何影响 LLM 输出的。
  2. Few-shot 与 Fine-tune 的区别?
  3. CoT 适合什么任务?不适合什么?
  4. Self-Consistency 的核心思路?
  5. System / User / Assistant 三角色在对话中各自的作用?
  6. 为什么结构化输出要配 schema 校验?

参考答案

第 1 题答案 Prompt 通过约束模型看到的输入影响 P(output | input) 的条件概率形状——同一种任务用不同 Prompt 表述,模型把概率压在不同输出区域,效果差几倍。这不是"激活模型能力",是"让模型把采样压在正确答案附近",减少采样的随机性。例如"中国的首都"让概率压在"北京","中国有哪些城市"让概率展开到多个城市。
第 2 题答案 Few-shot 是 in-context learning——在 prompt 里给几个示例,模型不更新权重,从上下文"学到"任务模式;优点是无需训练、即时生效、可灵活切换;缺点是占 prompt token、上限受模型 in-context 能力限制、稳定性不如训练。Fine-tune 是更新模型权重——用 (input, output) 数据集训练模型;优点是稳定、可在小模型上达到大模型水平、推理时无 prompt 开销;缺点是要训练资源、迭代慢、切换不灵活。工程上能 few-shot 解决的不上 fine-tune,fine-tune 是 few-shot 不够稳定时的下一步。
第 3 题答案 CoT 适合需要多步推理的任务——数学题、逻辑题、规划、复杂决策。原理是把复杂问题拆成多个简单预测,每步只用模型擅长的事,整体准确率提升 10-30%。不适合简单任务——翻译、分类、改写这种任务模型本来就能直接答对,加 CoT 反而让模型在思考链里走偏,准确率下降。新手常见误用是"以为加 CoT 总比不加好",实际在简单任务上效果相反。要先评测再决定是否加 CoT。
第 4 题答案 Self-Consistency 核心思路:同一个问题让模型用 CoT 跑 N 次(温度 >0),从 N 个答案里投票选众数。原理是单次 CoT 可能走错推理路径,但 N 次推理中正确的路径大概率占多数——用多数投票过滤掉单次走错的推理。工程上 N=5-10 是甜点,再多收益递减但成本线性涨;只对"答案是离散值"的任务(多选题、yes/no、数值计算)有效,开放生成任务没有"众数"概念。
第 5 题答案 三角色在对话中各自作用:System 是系统提示,定义模型这次的角色与默认行为(如"你是律师"),优先级最高,每轮都在 prompt 顶部;User 是用户消息,发起请求;Assistant 是模型回复,多轮对话里 assistant 的历史回复会被包含在后续 prompt 里,让模型"记得"自己之前说过什么。三者协同:system 定基调、user 提请求、assistant 留历史。多轮对话里这三者交替出现,构成完整对话上下文。
第 6 题答案 结构化输出要配 schema 校验的原因:模型会"幻觉"字段(多加 timestamp 或漏 amount)、类型不可靠(prompt 说数字模型可能输出"99元"字符串)、字段顺序漂移(同 prompt 多次调用顺序可能不同)。Pydantic 或 JSON Schema 校验是底线——校验失败时重试(带"上次输出错误"反馈让模型修正)或 fallback 到规则系统(正则抽字段)。生产系统建议两层都要:重试 1-2 次失败就 fallback,不要无限重试烧 token。这是 LLM 工程区别于"调 API 试一试"的关键纪律。

下一章:第四章 推理 API 与部署