三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Prompt调优实战:Harness工程驱动AI性能提升

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_tolerance

4.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 调优才真正从“经验活”变成工程。

← 返回列表