跳到主要内容

LLM 应用的测试驱动开发:类比成立与失效之处

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个团队使用 Claude 构建了一个 AI 研究助手。他们对 Prompt 进行了三周的迭代,向利益相关者演示了该助手,并满怀信心肠发布了它。两个月后,他们发现该助手在大约 30% 的输出中悄悄地产生虚假引用(幻觉)—— 这种失败模式之前没有人测试过,因为评估套件是在 Prompt 在演示中“感觉对了”之后才建立的。

这种模式是常态,而非例外。LLM 开发行业在很大程度上采用了测试驱动开发(TDD)的词汇 —— 评估(Evals)、回归套件、黄金数据集、LLM-as-judge —— 却忽略了 TDD 建立的最重要规则:在实现之前编写测试,而不是在实现之后。

以下是如何正确执行此操作的方法,以及 TDD 类比在哪些地方失效得非常严重,以至于字面上照搬它会让你的系统变得更糟。

在 Prompt 之后编写的评估在结构上是薄弱的

传统的 TDD 之所以有效,是因为失败的测试在任何实现存在之前就定义了成功。在实践中,大多数 LLM 团队的做法恰恰相反:先编写 Prompt,看到看起来可以接受的输出,然后编写测试来验证系统已经产生的结果。

其结果是一个评估套件在无意识中围绕系统现有的优势进行设计,并被调整为忽略其盲点。最终你会发现,那些本就不会失败的测试拿了高分,而对于你尚未想到的失败模式,则完全没有信号。

“评估优先”的工作流将此反转。在编写任何 Prompt 之前,先定义 10–20 个覆盖成功案例和预期失败案例的场景。为每个场景指定正确输出的样子,并选择一种评估方法:确定性输出使用精确匹配(Exact match),质量评估使用基于标准的评分(Rubric-based scoring),主观特质使用 LLM-as-judge。构建一个可以根据这些场景对输出进行评分的最简框架 —— 在这个阶段,甚至电子表格也可以。

然后通过编写 Prompt 来满足这些评估。

为什么这很重要的具体证据:一个团队使用这种方法构建了一个基于 Claude 的研究技能,并运行自动化的评估周期。从预设评估场景的 41% 准确率开始,他们在四个隔夜周期内达到了 92% —— 如果没有一个预定义的衡量标准为反馈循环提供优化目标,这种轨迹是不可能实现的。

在 Prompt 之后编写的评估将被校准为 Prompt 在初始准确率水平上已经产生的结果。其天花板会低得多。

让反馈循环快到足以产生影响

只有当你能快速运行评估并根据结果采取行动时,“评估优先”的方法才有效。以小时计的反馈循环会扼杀迭代动力。目标是每个周期几分钟。

使其可实现的实用策略:

从比你认为需要的更少的示例开始。 50 个精选示例比等待构建一个 500 个示例的数据集能更快提供有意义的信号。一个团队在分析了 100 条生产链路追踪(Traces)后发现,三个问题解释了大多数失败 —— 这个结果是通过对一个小规模、具有代表性的样本进行标注得出的,而不是通过穷举式的覆盖。随着你发现新的失败模式再扩展数据集,而不是预先扩展。

按成本对评估套件进行分层。 在每次提交时运行廉价的确定性评估(精确匹配、正则表达式、JSON Schema 验证)。在重大 Prompt 更改或发布前运行昂贵的 LLM-as-judge 评估。这既能保持持续反馈循环的快速,又能为高风险决策保留彻底的覆盖。

并行化测试执行。 LLM 调用是 IO 密集型的,且极易并行。例如,从顺序执行切换到异步并发执行(如 @pytest.mark.asyncio_cooperative 模式),可以使拥有多个模型调用的套件的总评估运行时间减少 30–40%。在大规模情况下, 10 分钟和 4 分钟评估周期之间的这种差异会显著累加。

尽早构建特定领域的工具。 一个工程团队发现,一个能在单个屏幕上显示完整对话上下文的自定义工具是“值得投资的” —— 如果没有它,错误分析需要跨标签页进行过多的上下文切换,导致速度太慢。对工具的投资通过显著加快失败诊断速度而获得了回报。

在自动评审员中使用二元评分。 使用 1–5 分李克特量表(Likert scale)评分的 LLM-as-judge 评估会产生不一致的结果:同一个评审员在一次运行中可能给某个输出打 3 分,而在另一次运行中可能打 4 分。二元 通过/失败(PASS/FAIL,或者最多三个选项)消除了这种不一致性,并使阈值决策变得直接。只有当评分可靠时,反馈循环才会快速移动。

TDD 类比失效的地方

非确定性是结构性的,而不是待修复的 Bug

传统的单元测试断言精确的输出。给定输入 X,代码必须精确地产生 Y。LLM 在根本上打破了这一点 —— 而且这种打破比大多数从业者意识到的还要深。

设置 temperature=0.0 并不保证确定性的输出。2025 年发布的研究表明,由于并行矩阵运算中的浮点舍入和批处理伪影,相同的 Prompt、相同的推理设置和相同的硬件配置在不同运行之间仍然会产生输出差异。非确定性是结构性的,无法通过配置消除。

必要的适配是概率规范(Probabilistic specification)。与其定义“这必须通过”,不如定义“这必须在至少 80% 的运行中通过”。像 LangEvals 这样的库通过 @pytest.mark.pass_rate(0.8) 等模式明确地对此进行了编码。底层哲学发生了转变:测试 LLM 并非为了证明正确性,而是为了记录可靠性阈值。

这对测试设计产生了二阶效应。在第一次失败时停止套件(标准的 pytest 行为)会给人一种系统健康状况的误导。在 LLM 评估套件中,单次运行中的单次失败可能是噪声。所有结果都应该在完整运行中进行记录和汇总,提供一个基于百分比的健康得分,而不是二元的通过/失败判定。

提示词错误没有编译时报错

在传统软件中,代码中的拼写错误或损坏的导入会立即且明显地导致失败。在代码运行之前,开发者就能获得即时的反馈,意识到出了问题。

提示词工程(Prompt engineering)没有对应的机制。微妙的词句变化——一个词的转换、句子结构的调整、指令顺序的修改——都可能在无形中改变模型的行为,而这些变化往往只会在生产环境规模下的边缘情况(edge cases)中显现。“编译”在推理过程中无声无息地发生,而故障模式可能直到系统处理了数千条输入后才会出现。

这就是为什么提示词版本化和回归测试不是可选的基础设施。它们是捕捉那些原本应由“编译步骤”捕捉的问题的唯一机制。如果团队将提示词视为非正式的字符串,而不是版本化的资产,就会失去识别哪项更改导致了哪项退化的能力。当模型升级无声无息地将语气得分从 0.85 降低到 0.72 时,拥有评估(eval)基础设施的团队能在 20 分钟内发现它;而没有该设施的团队则会在两周后通过客户投诉得知此事。

地面真值是一个分布,而不是一个点

传统的 TDD 假设每个测试用例只有一个正确答案。对于大多数 LLM 任务来说,正确的输出构成一个分布。一封精心撰写的邮件可以有数十种有效的形式。一条详尽的代码审查评论可以用十种不同的方式表达。不存在一个单一的“正确”输出来进行断言。

这迫使我们做出一个在传统 TDD 中没有对应项的设计决策:

  • 缩小输出规范,直到确定性匹配奏效。这对于结构化任务(如 JSON 提取、分类、实体识别)是可行的。对于生成式任务,这无法充分描述重要的细节。
  • 接受模糊评估,使用带有经过良好校准的评分标准(rubric)的 LLM 作为评判者(LLM-as-judge)。这种方式具有可扩展性,但引入了第二个模型,而该模型本身可能会失败、发生漂移或表现出偏见(例如偏好冗长的回答、偏好与评判者同系列的模型的回答、在成对比较中偏好首先呈现的项目)。
  • 使用人工标注作为地面真值(Ground Truth)。这虽然成本高、速度慢且风险大,但是唯一不继承另一个模型偏好的真值来源。

在实践中,正确的做法是结合使用:通过人工标注来验证评判者,通过大规模评判来控制部署,对高风险或新颖的任务类型进行人工复核。

评估在不同模型间不可移植

在软件开发中,如果一个测试套件通过了某个接口的一种实现,那么它也应该通过任何正确的实现。LLM 的评估并不具备这种可迁移性。在 GPT-3.5-turbo、GPT-4o 和 Llama-3-70b 上测试相同的提示词,会揭示出每个模型不同的失败模式。一个在某个模型上达到 87% 准确率的提示词,在评估标准完全相同的情况下,在另一个模型上可能会下降到 61%。

实际的影响是:评估套件在一定程度上是特定于模型的,更换模型总是需要重新运行评估,而不是假设其可以移植。将提示词与模型配置一起进行版本管理,并将评估结果绑定到特定的(提示词版本、模型、设置)三元组的基础设施,才能正确处理这种情况。像 HumanEval 这样的共享“语言”基准测试提供的信号不足——模型在标准基准测试上可以超过 90%,但在你的应用所需的特定任务上却可能产生无法接受的输出。

黄金数据集是核心产物

在 LLM 驱动的 TDD 中,最重要的单一产物是黄金数据集(golden dataset):这是一组精心挑选的输入和预期输出(或预期的评估标准),它定义了你特定系统的成功标准。

数据集就是规范。当数据集不完整或仅覆盖理想路径(happy paths)时,规范就是不完整的。当数据集陈旧且不再根据生产环境的失败案例进行更新时,规范就与现实发生了偏离。

实际的规模指导:50–100 个案例可以使系统达到一个有意义的最小可行性评估;200–500 个案例可以很好地覆盖主要用例和边缘情况,足以作为生产环境的准入标准;对于成熟的系统,1,000 个以上的案例是合适的,此时可以定期发现新的失败模式并从生产流量中添加进来。

获取策略与规模同样重要。按质量大致排序如下:

  1. 经过隐私过滤的真实生产追踪(traces),提取自真实用户行为而非虚构的输入
  2. 由领域专家编写的“必须通过”的场景,并带有明确的验收标准,用于失败后果最严重的案例
  3. 边缘情况、模糊输入和对抗性示例,用于压力测试规范的边界
  4. 经过领域专家审查和提升的合成数据

数据集是一个活产物,而不是一次性的构建。每一个评估未能预测的生产失败,都是规范存在差距的证据。维持从生产失败到数据集更新的反馈循环,是保持评估套件长期校准到现实世界行为的关键。

结语

TDD 对 LLM 开发最有价值的贡献不是“先测试后代码”的工作流,而是将测试视为规范的文化。当评估在提示词之前编写时,团队被迫在投入实现之前具体地定义成功。当评估在之后编写时,它们衡量的是舒适区,而不是未知的领域。

这种类比在非确定性、缺乏编译时错误、主观的地面真值以及模型可移植性方面失效了。在这些方面,盲目地套用 TDD 会让情况变得更糟——例如在随机推理下断言精确的输出,将基准测试得分视为生产环境的指标,或者假设通过了一个模型评估的提示词可以干净地迁移到另一个模型。

有效的适配方式是:使用概率性规范而不是精确断言,使用基于百分比的健康得分而不是二进制的通过/失败,使用绑定到版本化提示词资产的特定模型评估配置,并将黄金数据集视为活的规范而不是一次性的构建。

其纪律与 TDD 相同:在编写任何实现之前,先定义什么是“完成”。不同的只是工具和对不确定性的容忍度。

References:Let's stay in touch and Follow me for more thoughts and updates