Harness Engineering实践心得与高效技巧
引言
Harness Engineering 的概念在业内已讨论多时。宽泛的定义将其视为 Harness = Agent - Model——模型之外附加的所有能力,如 MCP、Skill、提示词等,皆属于 Harness 范畴。本文聚焦更具体的场景:Claude Plugins 中的 Harness 落地实践。虽与广义定义略有差异,核心目的一致——确保模型稳定输出结果。
为避免概念混淆,此处仅简析 Prompt、Context 与 Harness 三种范式的区别:
| 维度 | Prompt Engineering | Context Engineering | Harness Engineering |
|---|---|---|---|
| 核心定义 | 如何提问 | 让模型获知哪些信息 | 为模型构建执行环境 |
| 实现方式 | 自然语言描述 | 滑动窗口、向量库 | 管线搭建、状态管理、校验、外部API集成 |
| 层级关系 | 最内层:使用模型的基础手段 | 中间层:上下文信息管理 | 最外层:包含上下文工程与提示词工程 |
实践动机
先交代背景。团队开发了一款服务端代码改造影响面分析工具 Claude Plugins。起因是 Q2 起团队逐步向 AI 全栈转型,挑战在于用 AI 交付 Java 项目。当前多数模型在新项目中编写代码已较可靠,但对历史包袱沉重的遗留项目,模型仍会出现 Bug——即便采用了 Sdd/SuperPowers 等增强开发范式。
已上线的两个服务端项目中,有一个出现线上事故。根因是 AI 误改了本不该改动的业务入口。因此,为提升质量保障,工具箱中急需一款能精准评估改动影响面的工具。
对 AI 而言,分析代码影响面属于高复杂度任务:需要全局检索代码引用(已有 GodeGraph/GitNexus 深度优化)、梳理业务链路、评估风险、整理输出等。相信不少人有过类似经历:把复杂任务直接通过提示词交给模型,执行过程中模型可能偷懒或偏离方向。无论如何调整提示词,最终结果总与预期存在落差——这通常源于模型的认知局限与上下文窗口约束。而 Harness 的作用之一,正是尽可能减少此类偏差。
架构拆解
针对该分析插件,我们设计了多项工程化手段。以下说明具体做法及其背后的思考。
1. 整体工作流 —— 用确定性工程流程管控非确定性模型输出
如图所示,Plugin 通过暴露 Skill 作为用户交互入口。Skill 内嵌脚本驱动流程推进(Step1 → Step2 → …)。每个 Step 中的创造性任务仍交由模型处理,模型执行后,脚本将结果记录到 yaml(状态机)、校验成果,再进入下一 Step,直至结束。
此设计带来三点好处:
流程可控:由脚本决定“当前执行哪一步”“是否允许进入下一步”,避免模型提前收工或跳过步骤。若仅靠提示词告知 Step1/2/3,模型会因注意力稀释、追踪失效、位置偏差等问题导致结果不可靠。
状态可追溯:关键步骤执行结果以文件形式记录(此处使用
yaml)。以影响面分析Skill为例,让模型直接调用 py 脚本,在脚本中生成待处理任务列表,为每项任务标记status: WAITING/DONE/FAILED。脚本派发任务给模型并更新状态,执行期间可清晰获知任务完成情况。最终由脚本校验.yaml,针对未完成任务要求模型回补,确保处理全覆盖。上下文可控:模型在脚本执行后能拿到的上下文,完全由脚本决定。示例:
---name: xxdescription: xx--- ## 作用 xx... ## 执行步骤 ### Step1 - 生成改动任务 执行 ```bash python3 ${PLUGIN_ROOT}/script/generate.py" --create ``` ### Step2 - xxxxxx ...def main(): # ... print(f"脚本输出的内容会被模型读到,可以输出你想让模型知道的上下文") if __name__ == "__main__": main()模型能获取哪些上下文,完全由脚本中
print的内容决定。利用此机制可在脚本中精心设计——例如告知模型当前执行结果、下一步操作、所需参数等。
2. 状态机 —— 全/跨流程状态管理
状态机主要解决两个问题。
问题一:记录任务状态,兜底模型提前收工
面对任务繁多且复杂的场景,模型可能因上下文窗口限制、追踪失效等问题提前收工。典型表现为:模型在对话中宣称所有任务已完成,但人工审核发现仍有遗漏。反问“这块是否未处理”,模型才会意识到还有未完成项。
src/main/java/com/example/OrderService.java:
analysis_steps:
- step1_分析影响面: WAITING/PROCESSING/DONE/FAILED
- step2_分析风险点: WAITING/PROCESSING/DONE/FAILED
解决方案:为每项任务添加状态标记,任务执行后通过验证门校验状态,校验通过才放行,否则打回重新执行。工作流示意:
(图片占位)
问题二:跨 Agent(mainAgent、subAgent)协作时的任务状态追溯 / 跨 Skill 工作流时的任务状态追溯
将关键步骤的执行结果写入 .yaml 文件,其他 Agent/Skill 执行时也可读取该记录,实现跨 Agent/Skill 协作。设想一个全流程需求交付的 Plugin,从 PRD → 系分 → 开发 → 测试 各阶段对应不同 Skill,下一步必然依赖前一步的执行结果,此时状态管理不可或缺。
3. Diff 切割 —— 降低模型不确定性
核心思路:将模型容易产生幻觉的任务拆解出来,用脚本辅助完成,或直接全部由脚本完成,减少模型的不确定因素。
例如,本 Plugin 中识别“当前分支相比 origin/master 改动了哪些文件、哪些代码块”对模型来说容易产生幻觉。因此直接通过脚本梳理后再喂给模型。当每个模型可能出错的小任务都增加一层保障,整个工作流自然更稳定。
4. SubAgent 并发 —— 避免上下文窗口限制
执行复杂任务时,将所有任务交给单一 Agent 容易触发上下文窗口限制及注意力分散问题。Anthropic 也报告过类似现象:上下文达到一定量时(40%),模型会变得犹豫,甚至倾向提前收工,即使任务未完成。
一些 AI IDE 支持开启 subAgent,如 Claude Code、Cursor。因此我们采用另一种模式:由一个 mainAgent 控制主流程,复杂任务由 mainAgent 派发 subAgent 执行。这样 subAgent 拥有干净的上下文,不会出现提前收工问题,主流程也不会上下文溢出。
有人可能疑惑 mainAgent 如何获取 subAgent 的运行结果。IDE 会自动将 subAgent 的结果回传给 mainAgent,无需额外处理。但从稳定性和状态管理统一性的角度,通过回写状态机的方式更为稳妥。工作流示意:
(图片占位)
5. 验证门 —— 把控模型交付质量
验证机制是 Harness 理念中的关键层。在纯 Prompt 工程中,若想验证结果,可能写“Review 你的成果,识别有无问题”。但仔细想想,让本身就带有不确定性的模型来 Review 自己的产出,好比让人用同一双眼睛检查自己的作业。我们曾做过尝试:让模型生成一份方案文档,之后新建会话保持干净上下文,继续让模型 Review 该方案,重复此过程。最终结论是:每次 Review 都会发现新问题。让模型自我审查并非一个趋于稳定的方案。
本次实践中,验证部分的做法是将大任务进行拆解。例如在代码改动分析任务中,diff 里的每个文件可拆为小任务,而每个文件中的改动又有不同维度需分析(调用链、影响面、逻辑错误、性能风险等),每个维度也可继续拆解。任务类型也不同,有的只需执行即可,有的则要求模型分析全面。最终验证门需做两件事:
- 结合状态机中任务的 status 校验是否执行完
- 结合状态机中任务执行结果
.yaml文件校验模型执行是否到位
校验不通过则不允许放行,要求模型重新执行。实现方式仍通过脚本控制,只需在脚本执行后 print 结论,模型即可据此处理后续步骤。不过建议针对整个工程设计一套结构化的输出范式,强化模型对脚本执行结果的关注度。工作流与状态机类似,只是校验粒度更细,此处不再附图。
6. Skill Prompt 设计
尽管有了不少工程化手段,Prompt 设计依然重要。这方面我们踩过一些坑,总结出几点实践经验。
简单 Prompt vs 复杂 Prompt
尝试过两种风格。一种是详细版:列出各场景下正确的 case、错误的 case,用大量自然语言举例说明对错,并补充边界情况。另一种是极简版:整个 Prompt 仅包含要执行的 Step。最后发现 Prompt 越精简,模型执行越稳定——过多的 Prompt 会导致模型注意力分散。不过此结论可能因模型而异,各团队观点也不同,例如 OpenAI 倾向详细 Prompt,Anthropic 则建议定期精简 Prompt。
直接给代码 > 文字描述
原因简单:某些模型的编码、预训练数据、分词逻辑天然偏英文,解析中文可能产生偏差。测试表明,即使很短的描述也有概率出现幻觉,而代码是通用且无歧义的。
维度不宜太分散,否则模型每次执行重点不一致
最初在代码中放入一份包含 34 个风险点的 md 文档,要求模型逐一分析。多次测试后发现模型输出的风险点有时不一致,甚至偏差较大。排查后发现风险点过于分散。后来将 34 个具体风险点整合为 10 个,并从具体点转向为模型提供引导方向。
以下是几个主要 Skill 的最终提示词设计概览:
(Skill Prompt 模板占位)
7. 整体目录设计
Plugin 不同于单个 Skill,Plugin 可以是 Skill 的集合,因此目录设计需考虑通用性。
├── .claude-plugin/plugin.json # 插件元信息
├── .cc-version # 版本号 (1.0.0)
├── README.md # 项目文档
├── constants/
│ ├── analysis_diff_risk/
│ │ └── filter-rules.yaml # 文件过滤规则
│ └── common/
│ ├── runtime-constants.yaml # 公用运行时配置
│ └── output-messages.yaml # 输出消息模板
├── docs/
│ └── analysis_diff_risk_point.md # 风险检查文档
│ └── analysis-diff-risk-step2.md # 风险检查文档
├── script/analysis_diff_risk/
│ ├── analyze_diff.py # Git diff 解析 + YAML 生成
│ ├── execute_analysis.py # 状态机引擎(列表/收集/汇总)
│ └── cleanup_state.py # 清理临时状态
├── state/analysis_diff_risk/ # 运行时状态机
│ └── impact-analysis-${hash_id}.yaml # 待执行的任务列表
│ └── results_${hash_id}_batch.yaml # subAgent执行任务的结果
├── skills/fe-analysis-diff-risk/
│ └── SKILL.md # Skill 入口定义
└── template/
└── analysis_diff_risk_result.md # 输出报告模板
└── subagent_analyze_prompt.md # subagent提示词
踩过的坑
脚本输出执行结果时,
print中的文字描述必须准确,否则会导致模型偏离方向。最好附带下一步操作建议。能力一般的模型统计 diff 行数时易出现偏差。解决方案是直接用脚本计算每段 diff 的 start_line、end_line,再喂给模型。
改动量较大时,模型检索耗时会明显增加。因为模型常用的检索方式是
grep/ripgrep(rg),这些命令本质是用正则模糊匹配代码字符,匹配完成后还需模型判断是否属于正确场景。
举例:告诉模型“把项目中所有的 start 换成 star”,模型开始执行 grep 命令。因仅靠正则模糊匹配,它无法区分语境,可能匹配出各种情况。然后模型再逐一分析上下文,判断哪些是预期场景,循环下来拖慢检索速度。
好消息是社区已有开源库解决此问题,例如 codegraph。它先将代码解析成结构图(AST + 符号关系图),记录“谁是什么角色”。检索时可直接查索引,清楚哪些是变量定义、哪些是函数引用,甚至知道代码在哪些地方被实际使用。实测可减少约 30% 耗时,且比模型更稳定,后续计划集成到 Plugin 中。
效果与局限
- 测试了
Sonnet 4.6、Opus 4.8、deepseek-v4-pro、Qwen3.6 Plus,均能按预期执行。 - 服务端场景可能涉及跨上下游调用。此时上游或下游代码位于其他项目中,
Skill读取不到会造成上下文不完整。简易解决方式是将上下游项目放在同一文件目录下。
总结:Harness 的核心启示
- 我们当然期待更强的大模型能一键完成所有工作,但模型的稳定性与边界不可忽视。
- 除了会用模型,还应该探索模型更深层的能力,以提升未来竞争力。
- 在前端/服务端积累的工程化能力,在 AI 时代依然有效。日常需重视基础积累。