PERSONAL LAB / interview/从零开始图解
Interview 从零开始图解

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["能上线的系统"]

看图,记四句话:

  1. “跑通了一次”和”能上线”之间隔着三道:稳定性、可度量、可观测。大部分个人项目死在第一道。
  2. 稳定性不靠把 prompt 写得更花哨,靠结构化输出 + 程序校验 + 失败兜底——这是工程问题不是话术问题。
  3. 没有评测集就没有”变好”这回事,只有”我觉得变好了”。
  4. 线上和离线是两套指标,离线测能力,线上测真实分布下的表现和成本。

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 层:失败兜底

校验没过,按顺序尝试:

  1. 带错误信息重试:把具体哪里错了告诉模型,让它重出一次。成功率很高
  2. 降级:换更简单的输出格式,或者换备用模型
  3. 明确失败:返回”无法处理”,绝不编一个假结果

重试要设次数上限,否则就是另一种死循环。

容错解析:补救措施

模型输出的 JSON 常见畸形有七八种。实测(代码和完整输出见 5.1):

畸形直接 json.loads容错解析
正常OKOK
套 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%

五轮里两轮结论反了。 如果只跑一次就下结论,四成概率选错方向。

实践规则:

  1. 每个配置至少跑 3-5 轮取均值
  2. 差距小于一倍标准差就别认为有差异
  3. 想看出 5 个百分点的差距,50 条测试集是不够的——要么加大测试集,要么多跑几轮
  4. 报告数字时带上轮数和波动范围,只报一个数是不负责任的

面试里能说出”我跑了 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/>不要编假答案"]

三条原则:

  1. 重试要有上限,否则是另一种死循环
  2. 降级要预先设计,不能临时想
  3. 失败要明确,最差的设计是失败了还硬答

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 工程

  1. Prompt 工程和 prompt 技巧的区别是什么?(2.1 / 4.1)
  2. 一个能上线的 prompt 有哪五个结构块?哪块最容易漏?(4.1)
  3. 保证结构化输出的四层分别是什么?哪层才是真保证?(1.2 / 4.2)
  4. {"build_type": "调试模式"} 通过了 JSON mode,问题出在哪?(2.2 / 4.2)
  5. 模型输出的 JSON 有哪几种坏法?容错解析怎么写?(4.2 / 5.1)
  6. 容错解析是根本解法吗?根本解法是什么?(4.2)
  7. temperature=0 加固定 seed,能保证两次结果一样吗?(4.3)
  8. few-shot 什么时候有用,什么时候没用?(1.3)

Bad Case 与评测

  1. 为什么不能逐个案例改 prompt?(4.4)
  2. Bad Case 分析的正确流程是什么?(4.4)
  3. “个案修好但整体没涨”说明什么?(4.4)
  4. 测试集要覆盖哪四类?哪类最容易被忽略、为什么重要?(4.5)
  5. 测试集 50 条够吗?瓶颈是数量还是什么?(4.5)
  6. 用模型生成测试题有什么典型毛病?(4.5)
  7. 端到端指标能不能自动算,为什么这件事在选题时最重要?(4.6)

方差与统计

  1. 真实差距 5 个百分点、测试集 50 条,单次评测出错的概率大概多少?(4.7 / 5.2)
  2. 每个配置该跑几轮?什么情况下认为”没有差异”?(4.7)
  3. 报告评测结果时,除了均值还要报什么?(4.7)

Judge

  1. LLM-as-Judge 的三个偏见分别是什么?各自怎么对付?(2.5 / 4.8)
  2. 相对比较和绝对打分,哪个更稳定?(2.5 / 4.8)
  3. 用 Judge 必须配套做什么校验?(4.8)

上线工程

  1. 回归门禁为什么不能设成”必须全过”?(4.9)
  2. 可观测要记录哪些字段?为什么要记 prompt 版本?(4.10)
  3. 兜底触发率为什么是最有价值的线上信号?(4.10)
  4. 重试、降级、兜底分别在什么时候用?最差的设计是什么?(2.6 / 4.11)
  5. 离线好线上差,可能是什么原因?(2.4)

架构判断

  1. 判断”该用 Agent 还是工作流”的核心标准是一句什么话?(4.12)
  2. 举两个不该用 Agent 的场景。(1.3 / 4.12)
  3. 生产系统里 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 行,是所有专题里最厚的一块)。

Related · 从零开始图解
⎇ main interview/从零开始图解 56 节 253 notes UTF-8