我们有个 LLM 驱动的智能助手,核心逻辑全靠一段精心打磨的提示词(prompt)。某天,有用户反馈了一个具体的问题:某类问法,助手答得不对。我一看,确实是个 bug,于是熟练地打开那段 prompt,加了几句话、调了几个措辞,把这个 case 修好了——本地试了试,完美,上线。我满以为这只是一次"修一个 bug"的常规操作。可没过两天,反馈像雪片一样飞来:好几个原本一直好好的功能,突然开始出错了。我修好了一个 case,却在不知不觉中,弄坏了十个。
我盯着改动复盘,后背一阵发凉。我对 prompt 的那几处看似无害的修改,确实修好了目标 case,但它同时悄悄地改变了模型在其它许多场景下的行为——我加的那几句强调,让模型在另一些问法上"用力过猛";我调整的措辞,让它对某类输入的理解发生了偏移。可怕的是,我对这些"副作用"一无所知——因为我手里,根本没有一套能告诉我"这次改动,对全部各类场景的影响是什么"的东西。我就像一个蒙着眼睛的外科医生,自以为精准地切除了一个病灶,却不知道手术刀同时划伤了周围一大片健康的组织。
这就是 LLM 应用工程化里一个极其深刻、又极易被低估的坑:prompt(以及整个 LLM 系统)是一种"牵一发而动全身"的脆弱资产,任何一处看似局部的改动,都可能在你看不见的地方,引发大范围的行为回归(regression);而如果没有一套'评测集(eval)'来量化每次改动的全局影响,你的每一次'优化',都是在蒙眼裸奔。这篇文章,就从这次"修一个 bug 弄坏十个"的事故出发,把 LLM 应用为什么、以及如何建立评测体系,一次讲透。
先摆几个关于改 prompt 的想当然
动手复盘前,先把我自己曾经深信、后来被这次回归教育的几个念头摆出来。
| 想当然的念头 | 残酷的真相 |
|---|---|
| "改 prompt 修个 bug, 影响就在那一处" | prompt 是全局的, 一处改动会波及无数其它场景 |
| "本地试了目标 case 没问题, 就能上线" | 试了一个 case, 不代表没弄坏另外一百个 |
| "改 prompt 凭经验和感觉就行" | 没有量化评测, '感觉变好了'极可能是错觉 |
| "LLM 应用没法像代码那样写测试" | 能, 而且必须——用评测集做回归测试 |
| "换个更强的模型, 效果只会更好" | 换模型也是大改动, 不评测同样会引入回归 |
这些念头的共同病根,是把"改 prompt"当成了一种局部的、低风险的、凭感觉就能做好的小操作,却没意识到,在一个 LLM 系统里,prompt 是整个系统行为的"总开关",动它一下,影响的是模型在所有输入上的表现。要看清这次事故,得先理解 prompt 这种"全局耦合"的特性。
第一件事:prompt 是全局耦合的,改一处影响全局
要理解这个坑,得先认清传统代码和 prompt 在"修改影响范围"上的根本不同。传统代码是模块化的:你改一个函数,影响范围通常被限定在调用它的地方,边界相对清晰;只要它的输入输出契约不变,别处就不受影响。可一段 prompt 不一样——它是一个不可分割的整体,模型是把整段 prompt 作为一个统一的"上下文"来理解、来响应的。你在 prompt 里加的任何一句话、改的任何一个词,都会微妙地改变模型对整个任务的理解,从而影响它在所有输入上的行为,而非仅仅是你想修的那一个 case。
这就是为什么我"修一个 bug 却弄坏十个":我以为我在 prompt 里加的那句话,只对目标 case 起作用,可实际上,模型把这句话纳入了它对整个任务的理解,于是在很多别的场景下,它的行为也跟着变了——大多数我没预料到、也没去检查的变化,就成了"回归"。下面这张图,把传统代码改动和 prompt 改动的影响范围对比画出来:
看懂这张图,事故的根就清楚了:prompt 没有传统代码那种"模块边界"来约束改动的影响范围,它的每一处修改,影响都是弥散的、波及全局的。这意味着,你绝不能用"改传统代码"的直觉去"改 prompt"——前者改一处看一处,后者改一处必须看全部。而"看全部"这件事,靠人眼一个个试是不可能的,必须依靠一套系统化的评测。接下来,我们就看这套评测怎么建。
第二件事:建一套评测集(eval),把"感觉"变成"数字"
根治这个问题的核心,是建立一套评测集(evaluation dataset / eval)——一组覆盖了各类典型场景的"输入 + 期望表现"的测试用例;每次改动 prompt 或换模型后,都把整套评测集跑一遍,用一个量化的分数,告诉你"这次改动,让系统整体变好了还是变坏了"。这本质上,就是把传统软件里"单元测试 / 回归测试"的思想,搬到了 LLM 应用上。它把"我感觉好像变好了"这种靠不住的主观判断,变成了"评测分数从 82% 涨到了 85%"这种客观、可对比的事实。
# 评测集:一组覆盖各类场景的测试用例(输入 + 期望/判定标准)
eval_cases = [
{"input": "帮我查一下昨天的订单", "expect_intent": "query_order"},
{"input": "我要退货", "expect_intent": "return_goods"},
{"input": "你们几点下班", "expect_intent": "ask_hours"},
# ... 几十上百条, 覆盖核心场景 + 历史上出过 bug 的 case(防回归)
]
# 每次改 prompt / 换模型后, 跑全套评测, 得到量化分数
def run_eval(prompt_version):
passed = 0
failures = []
for case in eval_cases:
output = run_llm(prompt_version, case["input"])
if check(output, case): # 判定是否符合期望
passed += 1
else:
failures.append({"case": case, "got": output}) # 记下退化的 case
score = passed / len(eval_cases)
return score, failures
# 改动前后对比:分数降了, 或新增了失败 case, 就是引入了回归!
old_score, _ = run_eval(old_prompt)
new_score, fails = run_eval(new_prompt)
print(f"评测分: {old_score:.0%} -> {new_score:.0%}")
if new_score < old_score:
print("警告:本次改动引入了回归!", fails) # 别上线!
这套评测集的价值,是给了你一双"看见全局影响"的眼睛。我那次事故,如果当初有这套评测集,那么在我改完 prompt、准备上线前,跑一遍评测,就会立刻看到"分数从 90% 掉到了 75%"、并清清楚楚地列出那十个被弄坏的 case——我根本不会让这次改动上线。评测集把"改 prompt"从一场蒙眼的赌博,变成了一次有数据护航的、可验证的工程操作。它最关键的作用,是把'优化某个 case'和'保护所有其它 case 不退化'这两件事,同时纳入了你的视野。尤其要记得:每次线上出了 bug、修好之后,都要把那个 case 加进评测集——这样它就永远不会再悄悄地复发(这正是回归测试的精髓)。
第三件事:答案不唯一怎么评?用"LLM 当裁判"
建评测集时,会遇到一个传统测试没有的难题:LLM 的输出往往是自然语言,答案不唯一、无法用简单的"字符串相等"来判断对错。比如"今天天气怎么样"的回答,可以有一百种正确的措辞。那怎么自动判定一个开放式回答是好是坏?业界一个行之有效的方案,是"用 LLM 当裁判(LLM-as-a-judge)"——用另一个(通常更强的)大模型,根据你定的评判标准,去给被测模型的输出打分。
# LLM-as-judge:用一个更强的模型, 按标准给开放式回答打分
def llm_judge(question, answer, criteria):
judge_prompt = f"""你是一个严格的评分员。请根据以下标准, 给回答打分。
问题:{question}
回答:{answer}
评分标准:{criteria}
请只输出一个 1-5 的分数和简短理由, 格式: 分数|理由"""
result = strong_llm.complete(judge_prompt) # 用更强的模型当裁判
return parse_score(result)
# 在评测里用裁判模型来判定开放式输出
for case in open_ended_cases:
answer = run_llm(prompt_version, case["input"])
score = llm_judge(case["input"], answer,
criteria="是否准确、是否礼貌、是否答到点上")
# 把分数累加, 得到这一类开放题的平均得分
LLM-as-judge 巧妙地解决了"自然语言答案难以自动评判"的难题——它用 AI 的语言理解能力,去评判 AI 的输出,从而让开放式回答的评测也能自动化、规模化。当然它不是完美的(裁判模型自己也可能犯错、有偏见),所以实践中常常是多种判定方式结合:对有标准答案的(如意图分类),用精确匹配;对格式有要求的,用规则校验;对开放式的,用 LLM-as-judge;对最核心、最关键的少数 case,辅以人工抽检。核心思想是:无论用什么方式,都要让"系统表现好不好"这件事,变得可量化、可自动跑、可对比——这是 LLM 应用能够被工程化地持续优化的基石。
第四件事:把 prompt 当代码——版本化 + 评测纳入 CI
有了评测集,下一步是把整个流程工程化。核心思想是:把 prompt 当成和代码一样的一等资产来对待——纳入版本控制、改动要评审、上线前必须通过评测。很多团队把 prompt 散落在代码各处、甚至硬编码在某个字符串里,改了也没记录、没评审,这正是回归频发的温床。正确的做法,是把 prompt 集中管理、版本化,并把"跑评测"这一步,变成上线流程里一道自动的、强制的关卡(CI)。
# 把 prompt 版本化管理(像管代码一样), 并在 CI 里强制跑评测
# prompts/intent_v3.txt prompt 单独存文件, 纳入 git, 改动走 PR 评审
def ci_eval_gate(new_prompt, baseline_score, threshold=0.0):
score, fails = run_eval(new_prompt)
print(f"评测分: {score:.1%} (基线 {baseline_score:.1%})")
if score < baseline_score - threshold: # 比基线退步了
print("CI 失败:本次改动导致评测回归, 阻止上线!")
for f in fails: print(" 退化 case:", f["case"]["input"])
raise SystemExit(1) # CI 红灯, 挡住这次改动
print("CI 通过:无回归, 可以上线")
# 效果:任何会让整体表现退步的 prompt 改动, 在合并前就被自动挡下
这一步的意义,是把"防回归"从一件依赖人自觉去做的事,变成一道自动强制、无法绕过的关卡。就像传统代码的单元测试在 CI 里跑、不过就不让合并一样,LLM 应用的评测也该是上线前的硬门槛。当"改 prompt 必须先过评测"成为一条不可逾越的流程铁律,我那次"改完随手就上线"的事故,从机制上就不可能再发生了。把 prompt 当代码,把评测当测试,把这套搬进你已经成熟的工程流程里——这是 LLM 应用走向工程化、可靠化的关键一跃。
第五件事:别只看一个总分,要多维度评测
评测做深一点,会发现"一个总分"是不够的。一个改动,很可能让某个维度变好、另一个维度变坏——比如你让回答变得更详细了(信息量升),但也变得更啰嗦了(简洁性降);你让它更严谨了(准确性升),但也变得更刻板了(亲和力降)。如果只看一个笼统的总分,这些"此消彼长"的内部变化就被掩盖了。所以成熟的评测,要拆成多个维度分别打分。
# 多维度评测:别只看一个总分, 分维度打分, 看清此消彼长
def multi_dim_eval(prompt_version):
dims = {"准确性": [], "简洁性": [], "亲和力": [], "格式合规": [], "安全合规": []}
for case in eval_cases:
output = run_llm(prompt_version, case["input"])
dims["准确性"].append(judge(output, case, "准确性"))
dims["简洁性"].append(judge(output, case, "简洁性"))
dims["亲和力"].append(judge(output, case, "亲和力"))
# ... 各维度分别评
return {k: sum(v) / len(v) for k, v in dims.items()}
before = multi_dim_eval(old_prompt) # 准确性85 简洁80 亲和75 ...
after = multi_dim_eval(new_prompt) # 准确性90 简洁65 亲和75 ...
# 一眼看出:准确性升但简洁性降——这次改动是用"啰嗦"换了"准确", 是否值得?
多维度评测的价值,是让你看清每次改动的真实的、立体的代价,而不是被一个总分糊弄过去。它逼着你去思考权衡:为了提升某个维度,我牺牲了哪个维度?这个代价值不值?LLM 应用的优化,往往不是"单调地变好",而是在多个相互制约的维度间寻找平衡——而多维度评测,就是你做这种权衡时的仪表盘。你要监控的维度,通常包括:任务完成的准确性、回答质量、格式合规性,以及尤其重要的——安全合规性(别因为一次优化,让模型在某些输入下输出了不该输出的东西)。
第六件事:LLM 输出是概率性的——评测要多跑几次
最后一个容易被忽略、却很关键的点:LLM 的输出是概率性的——同样的输入,跑两次,结果可能不完全一样(尤其温度不为 0 时)。这意味着,你不能只跑一次评测就下结论:也许这次分数高,只是运气好;下次同样的 prompt,分数可能就低了。所以对于有随机性的场景,评测要对每个 case 多跑几次、取平均或看稳定性,才能得到可信的结论。
# LLM 输出有随机性, 评测每个 case 多跑几次, 看平均和稳定性
def robust_eval(prompt_version, runs=3):
scores = []
for _ in range(runs):
score, _ = run_eval(prompt_version)
scores.append(score)
avg = sum(scores) / len(scores)
# 不仅看平均分, 还要看波动:波动大说明输出不稳定, 本身就是个问题
spread = max(scores) - min(scores)
return avg, spread
# 评测抽取类/分类类任务时, 把 temperature 调低(求稳定), 再多跑取平均
# 既要看"平均表现好不好", 也要看"表现稳不稳定"
这一点呼应了 LLM 应用一个贯穿始终的特质:它的不确定性,不仅体现在线上,也体现在评测上。所以评测一个有随机性的 LLM 系统,本身就要用"统计"的眼光去看——不是"它这次答对了吗",而是"它有多大概率答对、表现稳不稳定"。把"多次运行取平均/看波动"纳入评测方法,你得到的结论才经得起推敲。到这儿,LLM 应用评测的方方面面就齐了。我把它收成一张决策图:
把这套体系建起来,改 prompt 就从"蒙眼裸奔"变成"有数据护航的工程操作"。最后,拧成几条可直接照做的铁律:
- prompt 是全局耦合的,改一处会影响所有场景, 别用"改代码"的直觉去"改 prompt"。
- 建一套覆盖各场景的评测集,每次改动都跑, 用量化分数代替"感觉变好了"。
- 线上 bug 修好后, 把该 case 加进评测集,确保它永不复发(回归测试精髓)。
- 开放式回答用 LLM-as-judge,结合精确匹配、规则校验、人工抽检, 多法并用。
- 把 prompt 版本化, 评测纳入 CI,让"过评测"成为上线前不可绕过的硬门槛。
- 多维度评测,看清每次改动在准确、简洁、安全等维度上的真实权衡。
- 输出有随机性, 评测要多跑取平均,既看平均表现、也看稳定性。
一张 LLM 应用评测速查表
把建立评测体系的要点汇成一张表,做 LLM 应用优化时对照着来。
| 要点 | 做法 | 解决什么 |
|---|---|---|
| 评测集 | 覆盖各场景的输入+期望用例 | 量化每次改动的全局影响 |
| 回归保护 | 线上 bug 修好后加入评测集 | 确保老问题永不复发 |
| 开放式判定 | LLM-as-judge + 人工抽检 | 评判答案不唯一的回答 |
| 版本化 + CI | prompt 入 git, 评测进流水线 | 不过评测不准上线, 强制防回归 |
| 多维度 | 准确/简洁/安全等分别打分 | 看清改动的真实权衡代价 |
| 多次运行 | 每个 case 跑多次取平均/看波动 | 应对输出的概率性 |
| 线上反馈回流 | 真实 case 持续补进评测集 | 让评测集越来越贴近现实 |
延伸一步:评测集本身,也要持续生长
评测体系建起来之后,还有一个让它持续保值的关键动作:让评测集"活起来"、不断生长。一套静态的、几个月不更新的评测集,会慢慢和真实世界脱节——用户的新问法、新场景、新的边界 case,不会自动进到你的评测集里。所以要建立一条"反馈回流"的通路:把线上真实遇到的、有代表性的 case(尤其是出过问题的、用户不满意的),持续地、不断地补充进评测集。这样,你的评测集就会越来越贴近真实的使用情况,它给出的分数,也就越来越能代表"用户真实体验到的质量"。
这件事的深层意义在于:评测集的质量,直接决定了你优化的方向对不对。如果评测集本身覆盖不全、和现实脱节,那么你辛辛苦苦把评测分刷得再高,也可能只是在"应试",而非真正地提升用户体验。所以,投入精力去维护一套高质量、持续更新、真正反映核心场景的评测集,本身就是 LLM 应用工程里一项极有价值的长期投资。可以说,在 LLM 应用的世界里,'你的评测集有多好',很大程度上决定了'你的产品能优化到多好'——评测集,是你优化这艘船的舵。
这也引出一个更宏观的视角:这整套"评测驱动"的方法,正是 LLM 应用领域逐渐成熟的标志。早期大家做 AI 应用,凭的是灵感和手感,改 prompt 像炼丹;而现在,越来越多的团队认识到,要让 AI 应用可靠、可持续地变好,必须把它拉回到"数据驱动、可度量、可回归"的工程轨道上来。从"炼丹"到"工程",中间隔着的,正是这套评测体系。
写在最后
这次"修一个 bug 弄坏十个"的事故,给我最深的触动,是它揭示了 LLM 应用开发与传统软件开发之间,一道既熟悉又陌生的鸿沟。说它熟悉,是因为"防回归"这件事,传统软件早就有了成熟的答案——单元测试、回归测试、CI 门禁,我们用了几十年;说它陌生,是因为当对象从"行为确定的代码"变成"行为概率化、且全局耦合的 prompt"时,那些老办法不能直接照搬,需要被重新发明——评测集取代了单元测试,LLM-as-judge 取代了断言,多次运行取平均应对了不确定性。这次事故让我明白,拥抱 LLM 这种新范式,不意味着抛弃过往所有的工程智慧,恰恰相反,是要把那些智慧的'内核'(比如'任何改动都要能被验证、被防回归')提炼出来,再用适应新范式的'形式'去重新实现它。
而这背后,是一个更值得深思的认知:AI 应用,终究是软件;让它可靠的,不是别的什么魔法,正是把它当成一个严肃的软件工程问题来对待的那份认真。大模型很神奇,prompt 很灵动,这很容易让人产生一种"它和传统软件不一样,不需要那些条条框框"的错觉,从而在兴奋中丢掉了工程的纪律——不写评测、不防回归、改完凭感觉就上线。可这次事故狠狠地提醒我:越是面对这种行为难以预测、影响弥散全局的"灵动"系统,我们就越需要用严谨的、数据驱动的工程方法,去为它套上确定性的缰绳。这,或许正是这一波 AI 浪潮里,真正能把"惊艳的 demo"沉淀成"可靠的产品"的团队,和那些始终停留在"炼丹"阶段的团队之间,最本质的分野。这次教训于我,是一次珍贵的提醒:无论技术的形态如何日新月异,那份"对每一次改动负责、用数据而非感觉说话"的工程之心,永远是构建可靠系统不变的基石。愿你我在这场激动人心的 AI 浪潮里,既保有探索新范式的好奇与勇气,也守住那份让一切真正可靠起来的、朴素而严谨的工程之心。
—— 别看了 · 2026