Prompt调优实战:Harness工程驱动AI性能提升
基于评测驱动的 Prompt 调优:Harness 工程
文 | AI编程实践
很多团队调 Prompt 的方式都很熟悉:线上出现一个 badcase,马上加一句规则;第二天又出问题,再补一个反例。Prompt 越来越长,内部约束开始打架,原本正常的请求也被误伤。
最麻烦的不是某个 case 没修好,而是团队无法回答三个基本问题:
这次修改究竟改善了哪些问题?
以前成功的 case 有没有变差?
当前版本比上一个版本好多少?
如果回答不了,所谓 Prompt 调优,本质上仍然是凭感觉改文案。
Harness 工程解决的就是这件事:把 badcase、评测集、单点实验、全量回归和版本基线串成一套可重复执行的验证系统。
💡关键洞察:Harness 可以理解为一套围绕模型运行的“测试台架”。它固定输入、模型、Prompt、工具和评分标准,让每次修改都有证据、有对照,也有退路。
一、为什么 Prompt 越改越长,效果反而不稳定
一个常见场景是:模型在分析订单时使用了错误的时间范围。
团队发现问题后,在 Prompt 末尾补上一句:
新增规则: 请合理使用时间,不要选择错误的时间范围。这句话几乎没有提供可验证的信息。
什么叫“合理”?应该使用用户指定时间、系统当前时间,还是数据表中的业务时间?如果缺少时间字段,是拒绝回答、请求补充,还是使用默认值?
规则看似增加了,模型面对真实输入时仍然需要猜。
下一次出现类似问题,团队可能继续加规则:
请谨慎处理时间。 必须保证时间准确。 不得使用不合理的时间。 回答前检查时间是否正确。文字变多了,决策标准并没有变清楚。
💡关键洞察:Prompt 调优不是继续堆叠形容词,而是把模糊问题改写成可以复现、执行和评分的行为标准。
二、第一步:收集 badcase,但不要只记一句“结果不对”
badcase 不是一条抱怨,而是一份可以重新运行的失败证据。
至少要保存三部分:
真实输入: 用户实际给了 AI 什么内容? 运行时还注入了哪些上下文、知识和工具结果? 当前输出: 模型实际返回了什么? 具体哪一句、哪个字段或哪个动作有问题? 期望结果: 正确输出应该是什么? 哪些内容必须出现,哪些内容禁止出现?如果条件允许,还应保存完整运行环境:
case_id: time_range_017 case_type: failure source: production model: model-version prompt_version: v1.8.3 temperature: 0 tools: - order_query_v2 retrieved_context: context_snapshot_017 expected_behavior: - 优先使用用户明确指定的时间范围 - 未指定结束时间时使用系统当前时间 - 不得把订单创建时间替换为支付时间 failure_tags: - 时间字段选择错误 - 未遵守用户约束这里有一个很常见的坑:团队只保存用户问题和模型回答,却没有保存模型版本、Prompt 版本、RAG 召回内容和工具返回结果。几天后再回放,同一个输入已经无法复现原来的错误。
2.1 把“感觉不对”改成可判定的问题
下面这些描述都不适合直接进入评测集:
模糊描述 | 可评测描述 |
|---|---|
这条数据不行 | 输出使用了 2024 年数据,但用户要求查询 2025 年 |
回答不够专业 | 回答缺少风险等级、判断依据和处理建议三个字段 |
格式有问题 | items应为数组,实际返回了字符串 |
工具调用错误 | 用户只要求读取数据,模型却调用了写入接口 |
回答不完整 | 三个子问题中只回答了前两个 |
出现幻觉 | 输出包含检索结果和工具结果中均不存在的订单状态 |
写得越具体,后面的归因、修复和自动评分就越容易。
三、第二步:先归因,再决定改哪里
看到 badcase 就改 Prompt,是 Harness 工程中最典型的误区。
模型输出由多层系统共同产生:
最终输出 = 模型能力 + Prompt约束 + 输入数据 + RAG召回 + Few-shot示例 + Workflow状态 + 工具能力 + 输出解析与程序校验任何一层出问题,都可能表现为“模型回答错了”。如果归因错了,Prompt 会逐渐变成所有系统缺陷的垃圾桶。
3.1 badcase 修复分流表
问题类型 | 典型表现 | 优先修复位置 |
|---|---|---|
指令不明确 | 同类输入下行为摇摆 | Prompt |
正确行为难以描述 | 模型不知道什么算合格 | Few-shot |
缺少事实或私有数据 | 模型只能猜业务信息 | RAG |
某类能力长期不足 | 大量同类样本都失败 | SFT |
行为偏好难靠规则稳定表达 | 输出能完成任务,但策略选择持续不符合偏好 | RL / Reward |
通用知识或底层能力不足 | 大规模领域语料缺失 | 继续预训练或更换基础模型 |
多步骤任务经常漏步 | 中间状态混乱、失败后不会恢复 | Workflow / 状态机 |
工具结果错误 | 参数、权限、超时或接口语义异常 | 工具与程序 |
Schema 不稳定 | 少字段、错类型、非法枚举 | 结构化输出与程序校验 |
规则重复或冲突 | 修复新 case 后旧 case 退化 | Prompt 重构 |
延迟和成本异常 | 重复前缀过长、缓存命中率低 | Prompt 结构与缓存策略 |
3.2 几类容易误判的问题
Schema 异常不应只靠 Prompt 修。
如果下游必须接收固定 JSON,就不能只写“请严格按照 JSON 输出”。应优先使用模型的结构化输出能力,再配合 Schema 校验、自动重试和降级处理。
缺少事实不等于模型能力差。
业务数据每天变化,应该由 RAG、数据库或工具提供。把数据写进 Prompt,既难更新,也无法追踪来源。
复杂任务漏步不一定需要更长的 Prompt。
当任务包含查询、判断、审批、写入和异常恢复时,应该拆成 Workflow 或状态机。每一步明确输入、输出和失败分支,比让模型一次完成所有操作稳定得多。
Few-shot 不是随便塞几个成功案例。
示例应覆盖最容易混淆的决策边界,明确展示什么输入对应什么输出。错误示例、过时示例和互相矛盾的示例,会直接污染模型判断。
四、第三步:基于 badcase 建立评测集
评测集不能只有失败案例。
如果全部样本都来自最近发生的问题,团队会不知不觉地针对局部失败过拟合。新问题可能修好了,历史能力却开始退化。
一个可用的评测集至少应包含:
Case 类型 | 作用 |
|---|---|
正常 case | 守住主流程和高频需求 |
历史失败 case | 验证已经修复的问题不会复发 |
边界 case | 测试空值、极值、歧义和冲突输入 |
对抗 case | 测试提示注入、越权和绕过约束 |
工具失败 case | 测试超时、空结果、权限不足和返回异常 |
Schema case | 测试字段、类型、枚举和嵌套结构 |
长上下文 case | 测试信息遗漏、位置偏差和上下文截断 |
建议为每个 case 定义清楚的验收条件:
case_id: order_query_042 category: tool_failure input: user_query: 查询最近一个月已支付订单 mock_tool_result: status: timeout expected: behavior: - 不得编造订单数据 - 明确说明查询暂时失败 - 提供重试建议 forbidden: - 返回具体订单数量 - 声称查询已经完成 scoring: tool_failure_handling: required hallucination: zero_tolerance4.1 不要只看一个总分
平均分很容易掩盖真实退化。
假设新版本的综合得分从 86 分提高到 88 分,但工具调用成功率从 96% 降到 82%。如果这项能力正好位于核心业务链路,新版本就不能上线。
Harness 应同时记录:
任务正确率
结构化输出通过率
工具调用成功率
事实引用或依据覆盖率
幻觉率
安全与权限违规数
P50、P95 延迟
输入、输出 Token 消耗
单次请求成本
各类 case 的通过率
确定性问题优先用程序评分,例如 JSON 是否可解析、字段是否缺失、工具参数是否正确。语义质量再使用模型裁判,并保留人工抽检。
五、第四步:单点修改,先做小范围回测
一次实验只修改一个主要变量。
例如,只调整时间处理规则,不要同时更换模型、增加 Few-shot、修改 RAG 参数和升级工具版本。多个变量一起变化,即使得分提高,也无法判断真正有效的修改是什么。
一次单点实验可以这样记录:
experiment_id: exp_time_rule_001 baseline: prompt_v1.8.3 candidate: prompt_v1.8.4 hypothesis: 将“合理使用时间”改为明确的时间字段优先级, 可以修复时间选择错误,同时不影响未指定时间的正常请求。 change: - 用户明确指定时间时,严格使用用户时间 - 用户未指定时,使用系统注入的 current_date - 缺少必要时间且无法推断时,请求用户补充 - 禁止自行替换业务时间字段 target_cases: - time_range_017 - time_range_021 - time_missing_004 acceptance: target_pass_rate: 100% normal_case_regression: 0先回测目标 badcase 和相邻边界 case。确认假设有效后,再进入全量回归。
如果单点修改没有改善,就撤回修改,重新检查归因。不要因为已经花了时间,就继续往同一条错误路径上叠规则。
六、第五步:全量回归,建立 Benchmark 基线
单个 badcase 通过,只能说明局部问题可能修好了。
上线前还需要运行完整 Benchmark,覆盖历史成功、历史失败、边界输入、工具异常和安全场景。
每次基线至少记录:
项目 | Baseline v1.8.3 | Candidate v1.8.4 |
|---|---|---|
正常 case 通过率 | 94.2% | 94.4% |
历史 badcase 通过率 | 81.6% | 89.7% |
边界 case 通过率 | 78.3% | 84.1% |
工具调用成功率 | 96.1% | 96.0% |
Schema 通过率 | 98.8% | 99.2% |
幻觉率 | 2.4% | 1.8% |
P95 延迟 | 3.2 秒 | 3.3 秒 |
平均输入 Token | 2860 | 2740 |
这里记录的不只是“成功过的 case”。
失败过的、边界上的、工具异常的,以及过去修复后重新通过的 case,都应该留下。它们共同构成系统的能力边界。
6.1 最常见的回归事故
只针对失败 case 加规则。
新规则让目标 case 通过,却误伤正常请求。解决办法不是再加一条例外,而是补全数据来源、操作步骤、决策优先级和失败处理。
只比较总分。
平均分提高,但某个关键分类明显下降。应为核心指标设置不可退化门槛,而不是允许其他维度的提升抵消它。
评测集和 Prompt 一起改。
修改评分标准后,新旧版本失去可比性。评测集变更也需要版本号,并保留一组长期稳定的核心 Benchmark。
没有冻结运行环境。
模型、温度、工具、RAG 索引同时变化,最后无法定位波动来自哪里。
七、十类常见修复方法,分别该怎么用
7.1 把要求写成可验证行为
不要写“谨慎”“合理”“专业”“高质量”。
应写清触发条件、操作顺序、数据来源、输出要求和失败处理。
当用户指定时间: 使用用户提供的开始时间和结束时间。 当用户未指定结束时间: 使用系统变量 current_date。 当开始时间缺失且无法从上下文确认: 请求用户补充,不得自行猜测。7.2 Schema 异常交给工程侧兜底
Prompt 可以说明字段含义,但不能承担全部格式可靠性。
使用结构化输出、类型校验、枚举约束、重试机制和解析失败降级。涉及资金、权限或自动执行时,程序必须做最终检查。
7.3 用 Few-shot 定义“什么才算正确”
Few-shot 适合展示难以用一句规则描述的判断边界。
示例应包含真实输入、标准输出和选择理由,并覆盖容易混淆的正反案例。不要只放几个相似的成功样本。
7.4 修复任务流程,而不是反复加例外
如果模型不知道从哪里取数、先做什么、失败后怎么办,就补充数据来源、操作步骤和失败分支。
当 Prompt 中出现大量“如果……但是……除非……”时,通常意味着它已经需要重构。
7.5 用 RL 处理稳定的行为偏好
如果模型基本会做任务,但在大量场景中持续选择了不符合业务偏好的策略,可以考虑增加 Reward 规则并重新训练。
Reward 必须能够稳定判断,不能只是“回答更好”“更像专家”这类模糊目标。上线前还要检查模型是否为了刷奖励而钻评分规则的空子。
7.6 区分 SFT、继续预训练和 RAG
方法 | 更适合解决的问题 |
|---|---|
SFT | 固定任务格式、领域行为模式、重复出现的能力缺口 |
继续预训练 | 大规模领域语言和底层知识缺失 |
RAG | 经常变化、需要引用、属于私有系统的事实数据 |
简单判断:需要模型“学会怎么做”,优先看 SFT;需要模型“临时知道什么”,优先看 RAG。
7.7 复杂任务拆成 Workflow 或状态机
把一个长任务拆成明确节点,每个节点只负责一个决定。
需要保存中间状态,定义成功条件、失败条件、重试次数和人工接管入口。否则模型一旦在中间步骤出错,后续输出往往只是继续错下去。
7.8 工具问题就修工具
参数描述含糊、接口返回不稳定、错误码不可读、权限边界混乱,这些都不是 Prompt 能根治的问题。
工具应提供清楚的参数 Schema、可区分的错误类型、幂等机制,以及足够模型判断下一步的返回信息。
7.9 输出格式需要硬约束
对于机器消费的结果,使用“结构化模型输出 + 程序校验”的组合。
校验失败后,可以自动重试、局部修复或进入人工处理,但不能把非法数据直接交给下游。
7.10 定期清理 Prompt 内部结构
Prompt 需要像代码一样重构。
检查是否存在互相矛盾的约束、重复规则、已经失效的 Few-shot,以及散落在不同位置的优先级说明。稳定且高复用的内容放在前缀,动态内容放在后面,避免无意义的前缀变化降低 KV Cache 命中率。
八、一个最小可用的 Harness 应该包含什么
不需要一开始就建设庞大的评测平台。最小版本能完成下面几件事,就已经比人工抽查可靠得多:
1. 保存可复现的 badcase 2. 为 case 添加分类、来源和失败原因 3. 固定模型、Prompt、工具与数据快照 4. 同时运行 baseline 和 candidate 5. 支持确定性评分、模型评分和人工抽检 6. 输出分类指标与失败明细 7. 标记新增失败和历史回归 8. 保存每次 Benchmark 基线 9. 为高风险指标设置上线门槛 10. 将线上新 badcase 持续回流评测集真正重要的不是平台有多少功能,而是团队能否稳定回答:
为什么改? 具体改了什么? 修复了哪些case? 破坏了哪些case? 指标变化是否可接受? 出现问题能否回退?九、结语:Prompt 也要进入工程化生命周期
Prompt 上线之后会遇到新的输入、新的工具、新的数据和新的业务约束。它不可能靠一次编写永久稳定。
但这并不意味着团队只能不停地追加规则。
更可靠的做法是把每个 badcase 变成测试资产:先保存失败现场,再归因到正确层级;用小范围回测验证修复假设,最后运行完整 Benchmark,确认历史能力没有退化。
当修改有实验记录,版本有基线,失败能复现,回归能被及时发现,Prompt 调优才真正从“经验活”变成工程。
