改 prompt 修一个弄坏十个:LLM 应用评测避坑

我们有个 LLM 驱动的智能助手核心逻辑全靠一段精心打磨的 prompt。某天有用户反馈某类问法助手答得不对,我一看确实是 bug,熟练地打开 prompt 加了几句话调了几个措辞把这个 case 修好了,本地试了完美便上线,满以为只是一次修一个 bug 的常规操作。可没过两天反馈像雪片飞来:好几个原本一直好好的功能突然开始出错——我修好了一个 case 却在不知不觉中弄坏了十个。复盘后背发凉:我对 prompt 那几处看似无害的修改确实修好了目标 case,却同时悄悄改变了模型在其它许多场景下的行为,而我对这些副作用一无所知,因为我手里根本没有一套能告诉我这次改动对全部场景影响如何的东西。这就是 LLM 工程化里极深又极易被低估的坑:prompt 是牵一发动全身的脆弱资产,一处局部改动会引发大范围行为回归,没有评测集来量化每次改动的全局影响,每次优化都是蒙眼裸奔。这篇文章从这次修一个 bug 弄坏十个的事故出发,讲透 LLM 应用评测:prompt 全局耦合改一处影响全局、建评测集把感觉变数字、bug 修好后加入评测集防复发、开放式回答用 LLM-as-judge、把 prompt 当代码版本化并把评测纳入 CI、多维度评测看清权衡、输出概率性要多跑取平均,以及评测集要持续生长。

我们有个 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 就从"蒙眼裸奔"变成"有数据护航的工程操作"。最后,拧成几条可直接照做的铁律:

  1. prompt 是全局耦合的,改一处会影响所有场景, 别用"改代码"的直觉去"改 prompt"。
  2. 建一套覆盖各场景的评测集,每次改动都跑, 用量化分数代替"感觉变好了"。
  3. 线上 bug 修好后, 把该 case 加进评测集,确保它永不复发(回归测试精髓)。
  4. 开放式回答用 LLM-as-judge,结合精确匹配、规则校验、人工抽检, 多法并用。
  5. 把 prompt 版本化, 评测纳入 CI,让"过评测"成为上线前不可绕过的硬门槛。
  6. 多维度评测,看清每次改动在准确、简洁、安全等维度上的真实权衡。
  7. 输出有随机性, 评测要多跑取平均,既看平均表现、也看稳定性。

一张 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

忘了一个 await:TS 浮动 Promise 乱序吞错避坑

2026-5-31 2:25:55

技术教程

本地能读换环境就崩:Python 字符编码避坑复盘

2026-5-31 2:50:52

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索