Prompt 与评测:面试速记 + 从零学习
这份文档分两层用:
| 场景 | 读哪里 |
|---|---|
| 面试前 10 分钟 | 第一部分速记页:稳定性四层 + 高频考点表 |
| 面试前一天 | 第二部分易混对照 + 第三部分口述稿,出声念一遍 |
| 平时学习 | 第四部分详解,按”从能跑到能上线”的顺序排的 |
| 想动手 | 第五部分:两个实测实验,都不需要 API key |
| 检验是否真会 | 第六部分闭卷自测 |
全文围绕一个问题:demo 能跑,凭什么说它能上线——你怎么让它稳定,又怎么证明它变好了。
这不是凑出来的题目。54 条岗位里有一条(孚若科技)把要求写得很直白: 结构化输出、工具调用、上下文管理、失败兜底、回归验证。 前两项在 Agent 篇,后三项就是这一篇。
第一部分 速记页
1.1 从”能跑”到”能上线”缺的那段
flowchart TB
A["demo:跑通了一次"] --> B{"换个输入还对吗"}
B -->|"不一定"| C["稳定性:结构化输出、参数校验、重试、兜底"]
C --> D{"改了之后变好还是变差"}
D -->|"不知道"| E["评测:测试集、指标、一次只改一个变量"]
E --> F{"线上真实表现如何"}
F -->|"看不见"| G["可观测:Trace、日志、成本、线上指标"]
G --> H["能上线的系统"]
看图,记四句话:
- “跑通了一次”和”能上线”之间隔着三道:稳定性、可度量、可观测。大部分个人项目死在第一道。
- 稳定性不靠把 prompt 写得更花哨,靠结构化输出 + 程序校验 + 失败兜底——这是工程问题不是话术问题。
- 没有评测集就没有”变好”这回事,只有”我觉得变好了”。
- 线上和离线是两套指标,离线测能力,线上测真实分布下的表现和成本。
1.2 稳定性的四个层次
从弱到强,面试时按这个顺序讲显得有体系:
| 层次 | 做法 | 能挡住什么 |
|---|---|---|
| 1. 提示约束 | prompt 里写”只输出 JSON,不要解释” | 大部分情况,但没有保证 |
| 2. 格式强制 | JSON mode / 约束解码 / Tool Calling schema | 语法层面的非法输出 |
| 3. 程序校验 | 解析 + schema 校验 + 业务规则检查 | 语法对但语义错的(枚举值不存在、数字越界) |
| 4. 失败兜底 | 校验失败时重试、降级、走人工、返回明确的失败 | 前三层都没挡住的 |
关键认知:第 1 层是建议,第 3、4 层才是保证。 安全和正确性不能建在 prompt 上——模型永远有概率不听话。
1.3 高频考点一页表
Prompt 工程
| 考点 | 一句话答案 | 别答错 |
|---|---|---|
| Prompt 工程是什么 | 不是话术技巧,是把不确定的自然语言输出,变成可校验、可复现、可回归的工程产物 | 不要背”你是一个专家”这类模板 |
| 结构化输出怎么保证 | 四层递进:提示约束 → 格式强制 → 程序校验 → 失败兜底 | 只说”让它输出 JSON”不够 |
| 模型输出的 JSON 解析失败怎么办 | 容错解析(剥 markdown 围栏、截取花括号、补尾括号、去尾逗号);还不行就带着错误信息让它重出一次 | 不要直接抛异常 |
| few-shot 有用吗 | 格式和风格类任务很有用;知识类任务基本无用。例子要覆盖边界情况 | 例子越多越好是错的,会挤占上下文 |
| CoT(让它先想再答)什么时候用 | 推理类任务有效;现在的推理模型内置了,再手写反而可能干扰 | |
| temperature 设多少 | 结构化输出、工具调用设 0 或接近 0;创意生成才调高 | |
| prompt 改了效果变差怎么办 | 说明你没有评测集。prompt 要像代码一样版本化 + 回归测试 | |
| 长 prompt 里关键信息放哪 | 开头或结尾,中间容易被忽略 |
评测
| 考点 | 一句话答案 | 别答错 |
|---|---|---|
| 为什么必须有评测集 | 没有它,每次改动都是赌博;有了它,改动才有方向 | |
| 测试集多大够用 | 50-100 条真实问题就能起步,覆盖简单/复杂/边界/无答案四类 | 不是越大越好,标注成本才是瓶颈 |
| 测试集怎么造 | 从真实数据里挑,人工标注期望结果。模型生成的要人工过一遍 | 全用模型生成会生成一堆伪问题 |
| 单次评测能下结论吗 | 不能。小测试集 + 模型随机性,单次结果可能完全反过来(见 5.2 实测) | 这题答出来很加分 |
| 怎么让评测可复现 | 固定 seed + temperature=0 + 固定 prompt 版本,但仍不保证完全一致,所以要多跑取统计量 | |
| LLM-as-Judge 靠谱吗 | 相对比较(A/B 谁好)比绝对打分靠谱;要固定评分 prompt、抽样人工校验、注意位置偏见 | 不能完全替代人工 |
| 位置偏见是什么 | Judge 倾向于选先出现的那个。要交换顺序各跑一次取平均 | |
| 离线好线上差,为什么 | 测试集分布和真实分布不一致;真实输入更脏更长更奇怪 |
可观测与兜底
| 考点 | 一句话答案 | 别答错 |
|---|---|---|
| 要记录什么 | 每步:输入、输出、工具调用、耗时、token、模型版本、prompt 版本 | 只打日志不叫可观测 |
| Trace 是什么 | 用一个 trace id 把一次请求里所有模型调用、工具调用、检索串起来,能完整回放 | |
| 线上该看哪些指标 | 成功率、P95 延迟、每次请求成本、工具失败率、兜底触发率、用户反馈 | |
| 模型服务挂了怎么办 | 多模型 fallback:主模型超时或报错切备用,prompt 要能跨模型复用 | |
| 怎么防成本失控 | 单次任务 token 上限 + 用户配额 + 异常消耗告警 | |
| 灰度怎么做 | prompt 和模型都要版本化,按比例放量,对比两版的线上指标 |
什么时候不要用 Agent
| 场景 | 该用什么 |
|---|---|
| 步骤固定(文档入库 → 切片 → 向量化) | 固定工作流,不要让模型决定下一步 |
| 需要强一致和可审计(财务、合规) | 固定流程 + 模型只做单点判断 |
| 步骤不确定、需要试错(编译修复、排障) | Agent |
| 需要人工把关 | 工作流 + 关键节点人工确认 |
54 条岗位里工作流/workflow 出现 17 次(31%),比想象的高。能说清”我在哪几步用了 Agent、哪几步写死了、为什么”,比吹自主性强。
第二部分 易混对照
2.1 Prompt 技巧 vs Prompt 工程
| 技巧 | 工程 | |
|---|---|---|
| 关注 | 怎么措辞让它这次答得好 | 怎么让它每次都在可接受范围内 |
| 产物 | 一段话 | 版本化的模板 + schema + 校验 + 测试集 + 回归流程 |
| 改动依据 | 试了感觉好 | 评测集数字 |
| 能不能交接 | 靠个人手感 | 能,因为有测试集兜底 |
网上大量内容在教前者。面试里值钱的是后者,因为公司要的是能上线不出事的系统。
2.2 保证结构化输出的三种做法
| 做法 | 原理 | 保证程度 | 代价 |
|---|---|---|---|
| prompt 里要求 | ”只输出 JSON” | 无保证 | 零 |
| JSON mode / 约束解码 | 服务端在生成时限制 token,只能吐合法 JSON | 语法保证 | 需要服务端支持 |
| Tool Calling schema | 按你给的 JSON Schema 生成参数 | 语法 + 字段名保证 | 同上 |
注意三者都只保证语法。{"build_type": "调试模式"} 是合法 JSON、字段名也对,但值不在枚举里。语义正确必须靠程序校验,这是第三层存在的理由。
2.3 评测 vs 测试
| 传统测试 | LLM 评测 | |
|---|---|---|
| 期望 | 精确相等 | 大多数时候没有唯一正确答案 |
| 结果 | 通过/失败,确定 | 分数,有方差 |
| 单次能否下结论 | 能 | 不能,要多跑取统计量 |
| 失败怎么处理 | 修复到通过 | 看整体指标是否回退,允许个别退化 |
因为有方差,LLM 评测的门禁不能设成”必须全过”,而是”整体指标不低于基线 X%“。
2.4 离线评测 vs 线上监控
| 离线评测 | 线上监控 | |
|---|---|---|
| 测什么 | 能力上限 | 真实分布下的表现 |
| 数据 | 精选的测试集 | 真实流量 |
| 什么时候跑 | 改动前后 | 持续 |
| 典型指标 | 准确率、Recall@K | 成功率、P95 延迟、成本、兜底触发率 |
离线好线上差是常态,因为真实输入比测试集脏得多。线上兜底触发率突然上升,通常是遇到了测试集没覆盖的输入形态——这些应该被捞回来补进测试集,形成闭环。
2.5 LLM-as-Judge 的三个坑
| 坑 | 表现 | 对策 |
|---|---|---|
| 位置偏见 | 倾向于选先出现的那个 | 交换顺序各跑一次取平均 |
| 长度偏见 | 倾向于选更长的答案 | 评分标准里明确”简洁不扣分” |
| 自我偏好 | 倾向于选和自己风格像的 | 用不同厂商的模型做 Judge |
还有一条:绝对打分(给 1-5 分)比相对比较(A 和 B 哪个好)不稳定得多。能做成 A/B 就别做打分。
2.6 重试 vs 兜底 vs 降级
| 什么时候 | 做什么 | |
|---|---|---|
| 重试 | 瞬时失败(超时、限流、格式错) | 原样再来一次,或带着错误信息让它改 |
| 降级 | 主路径不可用 | 换备用模型、换更简单的流程、返回缓存结果 |
| 兜底 | 都不行 | 返回明确的失败信息,不要编一个假答案 |
最差的设计是失败了还硬答。用户宁可看到”暂时无法处理”,也不要一个看起来像模像样的错误答案。
第三部分 口述稿
3.1 你怎么保证输出稳定(90 秒)
“我分四层,越往后保证越强。
第一层是 prompt 里写清楚要什么格式,这只是建议,模型有概率不听。
第二层是格式强制,用 Tool Calling 的 schema 或者 JSON mode,让服务端在生成时就限制只能吐合法结构。这保证了语法。
第三层是程序校验,这层才是真正的保证。因为前两层只管语法——{"build_type": "调试模式"} 是合法 JSON、字段名也对,但值不在我的枚举里。所以解析之后要做 schema 校验和业务规则检查。
第四层是兜底。校验没过的,带着具体错误信息让模型重出一次;还不行就降级或者返回明确失败,绝不硬编一个假结果。
核心认知是:安全和正确性不能建在 prompt 上,模型永远有概率不听话,保证必须在代码里。“
3.2 模型输出的 JSON 解析不了怎么办(60 秒)
“实际会遇到七八种畸形:套着 markdown 围栏、前面带一句’好的我来执行’、尾逗号、单引号、括号没闭合、一次吐两个对象。
我写了个容错解析:先剥 markdown 围栏,再从第一个左花括号开始做括号配对截取,缺的右括号补上,去掉尾逗号,还不行就把单引号换成双引号再试。我实测七种畸形输入,直接 json.loads 六种失败,容错解析全过。
容错之后还是失败的,就带着报错信息让模型重出一次——把’你上次输出的第 3 行有语法错误’告诉它,成功率很高。
但要注意这是补救。根本解法是用 Tool Calling 或 JSON mode,从生成阶段就约束,而不是事后修。“
3.3 你怎么知道改动是变好了(90 秒)
“靠评测集,而且要注意单次结果不可信。
我做过一个对照实验说明这个问题:假设真实水平是 A 70%、B 75%,用 50 条的测试集各跑五轮,结果有两轮 A 的分数反而比 B 高——如果只跑一次就下结论,40% 的概率会选错。跑二十轮取均值才收敛到真实值,标准差大概 7 个百分点。
所以我的做法是:测试集 50 到 100 条,覆盖简单、复杂、边界、无答案四类;temperature 设 0、固定 seed;一次只改一个变量;每个配置多跑几轮取均值,差距小于一倍标准差就不认为有差异。
这样每一轮改动都有个可信的数字,最后能连成一条’从 X% 到 Y%‘的线,而不是’我觉得变好了’。“
3.4 Bad Case 怎么分析(60 秒)
“关键是归类,不是一个个修。逐个改 prompt 去救个案,改完这个坏那个,永远收敛不了。
我的流程是:把失败案例导出来,按失败原因打标签——比如检索没召回、召回了没用上、格式错、工具选错、理解偏差。打完标签看分布,通常会发现百分之六七十集中在一两类上。
然后对着最大的那一类做系统性改动,改完跑全量评测看整体有没有涨。个案修好但整体没涨,说明改错了方向。
我在项目里把失败分类和数字都记下来了,面试问’剩下的错是什么’,我能直接答出来是哪几类、为什么难修——这比说’准确率 85%’ 更有说服力。“
3.5 LLM-as-Judge 怎么用(60 秒)
“能用,但有三个坑要处理。
位置偏见最明显:给 Judge 看 A 和 B,它倾向选先出现的。所以要交换顺序各跑一次,两次结论一致才算数。还有长度偏见,倾向选更长的;自我偏好,倾向选和自己风格像的,所以最好用另一个厂商的模型当 Judge。
用法上,让它做相对比较比让它打分稳定得多。‘A 和 B 哪个更好’比’给这个答案打 1 到 5 分’可靠。
而且必须抽样人工校验,比如每次抽 20 条人工复核,确认 Judge 和人的判断一致率。一致率太低,Judge 的结论就不能用。
它省的是人力,不是判断责任。“
3.6 上线之后怎么监控(60 秒)
“离线评测测能力上限,线上测真实表现,两套指标。
线上我会看:成功率、P95 延迟、单次请求成本、工具失败率、兜底触发率。兜底触发率是最有用的一个——它突然上升说明遇到了测试集没覆盖的输入形态,这些请求应该捞回来补进测试集,形成闭环。
实现上用 trace id 把一次请求里所有模型调用、工具调用、检索串起来,每条记录带上模型版本和 prompt 版本。出问题能完整回放当时的决策链,而不是只看到一条’失败了’的日志。
prompt 和模型都要版本化,改动灰度放量,对比两版的线上指标再全量。“
3.7 什么时候不该用 Agent(45 秒)
“步骤固定的不该用。比如文档入库这条链路——解析、切片、向量化、写库,顺序是确定的,让模型每步都决定一次既慢又贵还不可靠,直接写成固定工作流。
需要强一致或者可审计的场景也不该用,模型的自主决策没法审计。这类可以让模型只做单点判断,流程本身写死。
Agent 的价值在步骤不确定、需要试错的场景,比如编译报错修复——下一步做什么取决于上一步报了什么错,没法预先写死。
实际生产系统里大部分’AI 应用’是工作流加几个模型节点,真正需要自主决策的环节很少。能说清楚我在哪几步用了 Agent、哪几步写死了、为什么,比强调自主性有说服力。“
第四部分 逐个机制详解
4.1 Prompt 工程不是话术
先破一个误区。网上流传的”你是一个资深专家""请深呼吸一步步思考”这类咒语,在现在的模型上边际收益很小,而且没有任何保证。
真正的 Prompt 工程关心四件事:
| 关心什么 | 具体做法 |
|---|---|
| 输出能不能被程序消费 | 结构化输出 + schema |
| 改动能不能被验证 | 版本化 + 评测集 + 回归 |
| 失败能不能被兜住 | 校验 + 重试 + 降级 |
| 成本能不能被控制 | 长度控制 + caching + 模型分级 |
一个能上线的 prompt 应该长这样(结构,不是措辞):
[角色和任务] 一句话说清楚做什么
[输入格式说明] 资料怎么给的、怎么标号
[输出格式约束] 要什么字段、什么类型、枚举值有哪些
[边界规则] 什么情况回答"无法处理",不要编
[少量示例] 覆盖正常 + 边界,2-3 个够
边界规则那一条最容易漏,也最值钱。明确告诉它”资料里没有就说不知道""参数不全就报错而不是猜”,能砍掉一大块不可控输出。
4.2 结构化输出:四层保证
第 1 层:提示约束(无保证)
只输出 JSON,不要任何解释文字,不要 markdown 代码块。
有用,但模型有概率不听。不能把正确性建在这层。
第 2 层:格式强制(语法保证)
优先用 Tool Calling 的 schema(见 Agent 篇 4.4),或者服务端的 JSON mode。原理是在生成阶段限制候选 token,只能吐出合法结构。
第 3 层:程序校验(语义保证)
这层才是真保证。因为前两层只管语法:
{"build_type": "调试模式"} # 合法 JSON,字段名也对,但值不在枚举里
{"timeout": -5} # 类型对,业务上非法
{"path": "../../etc/passwd"} # 格式完全合法,但越界
校验要覆盖三种:
def validate(data, schema):
# 1. 结构校验:字段在不在、类型对不对
jsonschema.validate(data, schema)
# 2. 值域校验:枚举、范围、格式
if data["timeout"] <= 0:
raise ValueError("timeout 必须为正数")
# 3. 业务规则:越界、权限、状态机合法性
if not is_inside_workdir(data["path"]):
raise ValueError("路径越界")
第 4 层:失败兜底
校验没过,按顺序尝试:
- 带错误信息重试:把具体哪里错了告诉模型,让它重出一次。成功率很高
- 降级:换更简单的输出格式,或者换备用模型
- 明确失败:返回”无法处理”,绝不编一个假结果
重试要设次数上限,否则就是另一种死循环。
容错解析:补救措施
模型输出的 JSON 常见畸形有七八种。实测(代码和完整输出见 5.1):
| 畸形 | 直接 json.loads | 容错解析 |
|---|---|---|
| 正常 | OK | OK |
| 套 markdown 围栏 | 失败 | OK |
| 前面带”好的,我来执行:“ | 失败 | OK |
| 尾逗号 | 失败 | OK |
| 单引号 | 失败 | OK |
| 括号没闭合 | 失败 | OK |
| 一次吐两个对象 | 失败 | OK |
七种输入,直接解析六种失败,容错全过。但要记住这是补救,不是解法——根本解法是第 2 层的格式强制。
4.3 让输出可复现
| 手段 | 效果 | 局限 |
|---|---|---|
temperature=0 | 大幅降低随机性 | 仍不保证完全一致 |
固定 seed | 进一步降低 | 服务端批处理、模型版本更新都会破坏 |
| 固定 prompt 版本 | 消除人为变量 | 要有版本管理 |
| 固定模型版本 | 消除模型更新的影响 | 供应商可能下线旧版本 |
四条都做了,仍然不能保证两次结果完全相同。 这是 LLM 系统和传统系统的本质差异,也是为什么评测必须多跑取统计量(见 4.7)。
面试被问”怎么保证可复现”,答”设 temperature 和 seed”只能得一半分,把上面这个局限说出来才完整。
4.4 Bad Case 分析:归类,不要逐个修
逐个案例改 prompt 是新手最常见的陷阱:改好这个坏那个,永远收敛不了,因为你在用全局手段修局部问题。
正确流程:
flowchart LR
A[跑全量评测] --> B[导出所有失败案例]
B --> C["人工打标签<br/>按失败原因分类"]
C --> D["看分布<br/>通常 60-70% 集中在 1-2 类"]
D --> E["针对最大那类做系统性改动"]
E --> F["跑全量评测<br/>看整体有没有涨"]
F -->|"涨了"| G[记录数字,进入下一轮]
F -->|"没涨"| H["方向错了,回退"]
失败分类的例子(以 RAG + Agent 系统为例):
| 标签 | 含义 | 对应改动 |
|---|---|---|
| 检索未召回 | 正确资料没进 topK | 混合检索、query 改写 |
| 召回未采纳 | 资料给了但没用 | 调顺序、减 topK、强调只依据资料 |
| 格式错误 | 输出结构不对 | 加 schema 约束 |
| 工具选错 | 该用 A 用了 B | 改工具 description |
| 参数错误 | 工具选对参数填错 | 加枚举、拆工具 |
| 理解偏差 | 问题本身理解错了 | 改 prompt 或加示例 |
| 数据问题 | 知识库里压根没有 | 补语料 |
“个案修好但整体没涨,说明改错了方向” ——这句话面试里说出来,对方就知道你真做过。
4.5 评测集怎么造
四类必须覆盖
| 类别 | 占比参考 | 作用 |
|---|---|---|
| 简单直给 | 40% | 基本能力,回归时最先发现退化 |
| 需要多步/多片 | 30% | 真实难度 |
| 边界情况 | 20% | 歧义、超长输入、脏数据 |
| 无答案 | 10% | 测它会不会硬编 |
无答案那一类最容易被忽略,但它是测幻觉的唯一手段。没有这类,你的系统可能在”什么都敢答”的状态下拿到高分。
规模
50-100 条起步就够。瓶颈不是数量而是标注质量——每条都要人工确认期望结果是什么。一百条高质量标注比一千条模型生成的强。
模型生成要不要用
可以用来省力(让模型基于文档生成候选问题),但必须人工过一遍。模型生成的问题有两个典型毛病:照着原文抄,答案就在问题里;问法太书面,和真实用户的口语差很远。
版本化
测试集本身也要版本化。加了题目之后,历史数字就不可比了——要么标明版本,要么重跑基线。
4.6 指标怎么选
| 层 | 指标 | 能不能自动算 |
|---|---|---|
| 检索 | Recall@K、MRR | 能(标注了正确片 id) |
| 结构 | 格式合法率、schema 通过率 | 能 |
| 工具 | 工具选择准确率、参数正确率 | 能(标注了期望工具) |
| 端到端 | 任务成功率 | 看任务 |
| 生成质量 | 正确性、忠实度 | 要 Judge 或人工 |
| 成本 | 平均 token、平均步数、平均耗时 | 能 |
“端到端成功率能不能自动算”是选题时最该关心的事。 编译通过与否、测试跑没跑绿、SQL 执行结果对不对——这类有客观信号的任务,评测成本低一个数量级,结论也可信得多。
这正是 CrossBuild Agent 那个项目的核心优势(见 docs/crossbuild-agent-plan.md)。
4.7 方差:为什么单次评测会骗你
这是最容易被忽略、面试又很能体现深度的一点。
小测试集加上模型随机性,单次评测的结论可能完全反过来。实测(代码见 5.2):真实水平 A=70%、B=75%,测试集 50 条,各跑五轮:
第1轮: A=84% B=70% -> A 更好 <- 结论反了
第2轮: A=84% B=64% -> A 更好 <- 结论反了
第3轮: A=62% B=76% -> B 更好
第4轮: A=68% B=80% -> B 更好
第5轮: A=76% B=80% -> B 更好
跑 20 轮取统计量:
A: 均值 70.0% 标准差 6.6% 范围 58%-82%
B: 均值 74.8% 标准差 7.3% 范围 60%-90%
五轮里两轮结论反了。 如果只跑一次就下结论,四成概率选错方向。
实践规则:
- 每个配置至少跑 3-5 轮取均值
- 差距小于一倍标准差就别认为有差异
- 想看出 5 个百分点的差距,50 条测试集是不够的——要么加大测试集,要么多跑几轮
- 报告数字时带上轮数和波动范围,只报一个数是不负责任的
面试里能说出”我跑了 N 轮取均值,标准差多少”,比报一个光秃秃的准确率强得多。
4.8 LLM-as-Judge
什么时候用
人工评判太贵、又没有客观标准答案时(比如”这个回答好不好”)。有客观信号就别用 Judge。
三个偏见
| 偏见 | 对策 |
|---|---|
| 位置偏见(选先出现的) | 交换顺序各跑一次,两次一致才算 |
| 长度偏见(选更长的) | 评分标准里写明”简洁不扣分” |
| 自我偏好(选风格像自己的) | 用不同厂商的模型做 Judge |
用法
相对比较 > 绝对打分。 “A 和 B 哪个更好”的一致性远高于”给这个答案打 1-5 分”。
评分 prompt 要包含:明确的评分维度、每个维度的标准、要求先给理由再给结论(理由能暴露它的误判)。
必须做的校验
每轮抽 20 条人工复核,算 Judge 和人的一致率。一致率低于某个阈值(比如 80%),Judge 的结论就不能用。 Judge 省的是人力,不是判断责任。
4.9 回归测试与发布
prompt 要当代码管:
| 做法 | 说明 |
|---|---|
| 版本化 | prompt 存在代码仓库里,不要硬编在业务逻辑中间 |
| 模板化 | 变量用占位符,避免拼字符串出错 |
| 回归门禁 | 改动前后跑评测集,整体指标不低于基线才允许合入 |
| 灰度 | 按比例放量,对比线上指标 |
| 可回滚 | prompt 版本和模型版本都要能一键切回 |
门禁不能设成”必须全过”(因为有方差),而是”整体指标不低于基线 X 个百分点”,并且允许个别用例退化。
4.10 可观测:出了问题能不能复原现场
要记录的字段:
| 字段 | 为什么 |
|---|---|
| trace_id | 把一次请求的所有调用串起来 |
| 每次模型调用的输入输出 | 复现问题的唯一依据 |
| 工具调用的参数和结果 | 定位是模型错还是工具错 |
| 模型版本、prompt 版本 | 排除”换版本导致的”这类原因 |
| token 用量、耗时 | 成本和性能分析 |
| 兜底是否触发 | 最有价值的线上信号 |
兜底触发率值得单独说:它上升通常意味着遇到了测试集没覆盖的输入形态。把这些请求捞回来、补进测试集,就形成了”线上发现问题 → 离线补测试 → 改进 → 灰度”的闭环。这个闭环是”能上线的系统”和”跑通的 demo”最大的区别。
4.11 兜底与降级
flowchart TB
A[执行] --> B{成功?}
B -->|是| Z[返回结果]
B -->|否| C{可重试的错?}
C -->|"格式错/超时/限流"| D["重试<br/>带上错误信息"]
D --> E{重试次数超限?}
E -->|否| A
E -->|是| F
C -->|"不可重试"| F{有降级路径?}
F -->|"有"| G["换备用模型<br/>或简化流程<br/>或返回缓存"]
F -->|"没有"| H["明确失败<br/>不要编假答案"]
三条原则:
- 重试要有上限,否则是另一种死循环
- 降级要预先设计,不能临时想
- 失败要明确,最差的设计是失败了还硬答
4.12 工作流编排:什么时候不要用 Agent
54 条岗位里工作流相关出现 17 次(31%),Dify、Coze、n8n 这类平台也在其中。这说明企业实际在做的很多是编排,不是自主 Agent。
判断标准很简单:下一步做什么,是确定的还是取决于上一步的结果?
| 场景 | 下一步确定吗 | 用什么 |
|---|---|---|
| 文档入库:解析→切片→向量化→写库 | 确定 | 固定工作流 |
| 表单审核:抽取字段→校验→入库 | 确定 | 工作流 + 模型做单点抽取 |
| 编译报错修复 | 取决于报了什么错 | Agent |
| 客服:意图识别→分支处理 | 大部分确定 | 工作流 + 少量 Agent 节点 |
混合是常态:主干写死,个别节点交给模型,需要试错的子任务才用 Agent。 这样可预测、可审计、成本低,出问题也好定位。
第五部分 动手实验
两个实验都不需要 API key,纯本地可跑,输出是真跑出来的。
5.1 实验一:模型输出的 JSON 有多少种坏法
先看七种真实会遇到的畸形输入:
SAMPLES = [
'{"name": "run_command", "command": "cmake --build ."}', # 正常
'```json\n{"name": "run_command", "command": "cmake --build ."}\n```', # markdown 围栏
'好的,我来执行:\n{"name": "run_command", "command": "cmake --build ."}', # 前面带话
'{"name": "run_command", "command": "cmake --build .",}', # 尾逗号
"{'name': 'run_command', 'command': 'cmake --build .'}", # 单引号
'{"name": "run_command", "command": "cmake --build ."', # 没闭合
'{"name": "run_command"} {"name": "read_file"}', # 吐了两个
]
容错解析:
import json, re
def repair(s):
s = s.strip()
m = re.search(r"```(?:json)?\s*(.*?)```", s, re.S) # 剥 markdown 围栏
if m:
s = m.group(1).strip()
start = s.find("{") # 找到第一个 {
if start == -1:
raise ValueError("找不到 JSON 起始")
depth, end = 0, None
for i, ch in enumerate(s[start:], start): # 括号配对截取
if ch == "{": depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
s = s[start:] + "}" * depth if end is None else s[start:end] # 补缺失的右括号
s = re.sub(r",(\s*[}\]])", r"\1", s) # 去尾逗号
try:
return json.loads(s)
except json.JSONDecodeError:
return json.loads(re.sub(r"'", '"', s)) # 单引号兜底
实测输出:
1. {"name": "run_command", "command": "cmake --bu 直接解析=OK 容错=OK
2. ```json\n{"name": "run_command", "command": "c 直接解析=JSONDecodeError 容错=OK
3. 好的,我来执行:\n{"name": "run_command", "comman 直接解析=JSONDecodeError 容错=OK
4. {"name": "run_command", "command": "cmake --bu 直接解析=JSONDecodeError 容错=OK
5. {'name': 'run_command', 'command': 'cmake --bu 直接解析=JSONDecodeError 容错=OK
6. {"name": "run_command", "command": "cmake --bu 直接解析=JSONDecodeError 容错=OK
7. {"name": "run_command"} {"name": "read_file"} 直接解析=JSONDecodeError 容错=OK
七种输入,直接 json.loads 六种失败,容错解析全过。
跑完这个实验,3.2 那段口述稿你就有实感了。但别忘了结论:这是补救,根本解法是用 Tool Calling 或 JSON mode 从生成阶段约束。
5.2 实验二:为什么单次评测会骗你
这个实验不需要任何模型,用随机数就能说明问题——因为问题出在统计,不出在模型。
import random, statistics
random.seed(7)
TRUE_A, TRUE_B = 0.70, 0.75 # 假设 B 真的比 A 好 5 个百分点
N = 50 # 测试集 50 条
def run(p, n=N):
return sum(1 for _ in range(n) if random.random() < p) / n
for i in range(1, 6):
a, b = run(TRUE_A), run(TRUE_B)
print(f"第{i}轮: A={a:.0%} B={b:.0%}")
实测输出:
真实水平: A=70% B=75% 测试集 50 条
单次评测各跑 5 轮:
第1轮: A=84% B=70% -> A 更好 <- 结论反了
第2轮: A=84% B=64% -> A 更好 <- 结论反了
第3轮: A=62% B=76% -> B 更好
第4轮: A=68% B=80% -> B 更好
第5轮: A=76% B=80% -> B 更好
5 轮里有 2 轮得出错误结论
跑 20 轮取统计量:
A: 均值 70.0% 标准差 6.6% 范围 58%-82%
B: 均值 74.8% 标准差 7.3% 范围 60%-90%
读懂这个结果:B 确实更好,但单轮 50 条的测试里,有 40% 的概率会得出相反结论。单看第 1 轮,你会以为 A 比 B 好 14 个百分点,然后回滚一个本来正确的改动。
跑 20 轮之后均值收敛到真实值,但标准差有 6-7 个百分点——这意味着两个配置差距小于 7 个点时,单次比较基本没有意义。
实践规则:每个配置跑 3-5 轮取均值;差距小于一倍标准差不算有差异;报数字时带上轮数和波动范围。
5.3 实验三:评测跑批框架
阶段 4 要用的骨架,先跑通框架再接真实系统:
import json, statistics
from pathlib import Path
def load_testset(path):
return [json.loads(line) for line in Path(path).read_text().splitlines() if line.strip()]
def evaluate(system_fn, testset, rounds=3):
per_round = []
all_failures = []
for r in range(rounds):
ok, failures = 0, []
for case in testset:
try:
result = system_fn(case["input"])
if check(result, case["expect"]):
ok += 1
else:
failures.append({"id": case["id"], "got": result, "want": case["expect"]})
except Exception as e:
failures.append({"id": case["id"], "error": f"{type(e).__name__}: {e}"})
per_round.append(ok / len(testset))
all_failures.extend(failures)
return {
"mean": statistics.mean(per_round),
"stdev": statistics.stdev(per_round) if rounds > 1 else 0.0,
"rounds": per_round,
"failures": all_failures,
}
测试集用 JSONL,一行一条,便于增量追加和 diff:
{"id": "t001", "input": "怎么让 cmake 进入交叉编译模式", "expect": {"chunk_ids": ["c_042"]}, "type": "简单"}
{"id": "t002", "input": "musl 和 glibc 有什么区别", "expect": {"chunk_ids": ["c_017"]}, "type": "简单"}
{"id": "t003", "input": "今天天气怎么样", "expect": {"no_answer": true}, "type": "无答案"}
注意最后一条:无答案的用例要占一成左右,专门测它会不会硬编。
失败案例导出来之后按 4.4 的分类打标签,看分布,针对最大那类改。
5.4 排查手册
| 现象 | 先查哪里 |
|---|---|
| 输出格式时好时坏 | temperature 是不是没设 0;有没有用 schema 约束 |
| JSON 偶尔解析失败 | 有没有容错解析;根本解法是换 Tool Calling |
| 字段名对但值非法 | 缺第 3 层程序校验;枚举值有没有写进 schema |
| 改了 prompt 这个好了那个坏了 | 没有评测集,在用全局手段修局部问题 |
| 评测结果每次都不一样 | 正常。看轮数够不够、差距是否小于标准差 |
| 离线 90% 线上一堆投诉 | 测试集分布和真实不一致;看线上兜底触发率 |
| Judge 打分和人工差很远 | 位置偏见(换顺序重跑)、评分标准太模糊 |
| 成本突然飙升 | 看每步 token;大概率是历史没压缩或工具结果没截断 |
| 换了模型效果掉了 | prompt 过拟合到旧模型;回归测试就是防这个的 |
第六部分 闭卷自测
Prompt 工程
- Prompt 工程和 prompt 技巧的区别是什么?(2.1 / 4.1)
- 一个能上线的 prompt 有哪五个结构块?哪块最容易漏?(4.1)
- 保证结构化输出的四层分别是什么?哪层才是真保证?(1.2 / 4.2)
{"build_type": "调试模式"}通过了 JSON mode,问题出在哪?(2.2 / 4.2)- 模型输出的 JSON 有哪几种坏法?容错解析怎么写?(4.2 / 5.1)
- 容错解析是根本解法吗?根本解法是什么?(4.2)
- temperature=0 加固定 seed,能保证两次结果一样吗?(4.3)
- few-shot 什么时候有用,什么时候没用?(1.3)
Bad Case 与评测
- 为什么不能逐个案例改 prompt?(4.4)
- Bad Case 分析的正确流程是什么?(4.4)
- “个案修好但整体没涨”说明什么?(4.4)
- 测试集要覆盖哪四类?哪类最容易被忽略、为什么重要?(4.5)
- 测试集 50 条够吗?瓶颈是数量还是什么?(4.5)
- 用模型生成测试题有什么典型毛病?(4.5)
- 端到端指标能不能自动算,为什么这件事在选题时最重要?(4.6)
方差与统计
- 真实差距 5 个百分点、测试集 50 条,单次评测出错的概率大概多少?(4.7 / 5.2)
- 每个配置该跑几轮?什么情况下认为”没有差异”?(4.7)
- 报告评测结果时,除了均值还要报什么?(4.7)
Judge
- LLM-as-Judge 的三个偏见分别是什么?各自怎么对付?(2.5 / 4.8)
- 相对比较和绝对打分,哪个更稳定?(2.5 / 4.8)
- 用 Judge 必须配套做什么校验?(4.8)
上线工程
- 回归门禁为什么不能设成”必须全过”?(4.9)
- 可观测要记录哪些字段?为什么要记 prompt 版本?(4.10)
- 兜底触发率为什么是最有价值的线上信号?(4.10)
- 重试、降级、兜底分别在什么时候用?最差的设计是什么?(2.6 / 4.11)
- 离线好线上差,可能是什么原因?(2.4)
架构判断
- 判断”该用 Agent 还是工作流”的核心标准是一句什么话?(4.12)
- 举两个不该用 Agent 的场景。(1.3 / 4.12)
- 生产系统里 Agent 和工作流通常是什么关系?(4.12 / 3.7)
附:三篇的关系
| 篇 | 覆盖 | 对应 JD 要求(54 条样本) |
|---|---|---|
| Agent 从零开始图解 | Tool Calling、Agent Loop、工具执行、上下文、成本 | Agent 61%、工具调用 20% |
| RAG 从零开始图解 | 切片、embedding、检索、rerank、降幻觉 | RAG 48%、向量库 28% |
| 本篇 | 结构化输出、Bad Case、评测、可观测、兜底、工作流 | Prompt 48%、工作流 31%、评测回归 |
三篇读完,54 条岗位里的技术要求基本覆盖。剩下的是各家自己的业务栈(Java/Spring 43%、前端 30%、数据库、Docker/K8s),那部分按你原有的工程经验答即可。
深挖回 ai/02-Prompt与上下文/ 和 ai/05-Eval与观测/(7 篇 2252 行,是所有专题里最厚的一块)。