AI Harness 工程学:用 Claude Agent SDK 把缺陷调查封装成专属智能体驭具的 18 节实战
英文标题:AI Harness 工程学:用 Claude Agent SDK 把缺陷调查封装成专属智能体驭具的 18 节实战
Harness 这个词近一年被反复引用,但底层定义其实很短。把它译成「驭具」或「外壳」会更贴切:它就是包在 AI Agent 外面、帮助模型更高效完成某一类工作的一层代码
26.3 KB
22.1 KB
30.8 KB
22.4 KB
18.9 KB
22.2 KB
21.0 KB
22.6 KB
22.9 KB
19.5 KB
25.0 KB
29.5 KB
27.6 KB
24.3 KB
26.3 KB
32.1 KB
50.7 KB
24.4 KB
中文
Key Takeaways
- harness(驭具/外壳)的最简定义:包在 AI Agent 外面、让它更高效完成特定工作的一层代码,可以含 AI 也可以不含
- 每个 harness 都由三个部分组成:specific context(特定上下文)、specific actions(能采取的特定动作)、specific outcomes(要产出的特定结果)
- 构建时机的判定式:同一工作流需要同样的设置、走向同样的结果时,就值得固化成 harness
- 缺陷分诊(bug triage)每天重复、流程稳定、输入输出清晰,是 harness 的教科书场景
- 直接用通用编码工具时,每次都要写「亲爱的 Agent,请修这个 bug」并附上链接、解释意图
什么是 AI Harness:围绕智能体的那一层代码
一、最朴素的定义:包在 Agent 外面的一层代码
Harness 这个词近一年被反复引用,但底层定义其实很短。把它译成「驭具」或「外壳」会更贴切:它就是包在 AI Agent 外面、帮助模型更高效完成某一类工作的一层代码。这里的「一层代码」可以是普通的 TypeScript 函数、Python 脚本、命令行循环,也可以是嵌入了另一次模型调用的子流程,关键是它位于模型和真实业务之间,承担约束、编排、上下文注入与结果回收的职责。
[观察] 这套实践里反复强调一件事:harness 不一定要「含 AI」。一个 bug 调查 harness 完全可以用纯确定性代码写完所有调查步骤,只在最后一步把整理好的事实交给模型写摘要。这种取舍让 harness 的边界从「AI 编排」缩到「工作流约束」,反而更稳、更易测试。
二、目标只有一个:让 AI 在某个 job 上更值钱
通用聊天框是模型厂商的活,不是 harness 的活。Harness 的目标从来都是把模型钉在一个具体 job 上,让它在这个岗位上比裸用模型更有用。这就是「job to be done」视角:harness 不是让模型更聪明,而是让模型在某个具体岗位上更便宜、更稳定、更可被审计。
衡量 harness 价值的指标只有三条:单位时间内完成的 job 数、平均介入的人工次数、结果的可复现性。围绕这三条指标做减法,比堆功能更有效。任何让 harness 偏离这三项指标的设计,都应该被砍掉。
三、为什么 harness 概念被神秘化
过去一年,「not the model, it's the harness」几乎成为新的 prompt 口头禅,Anthropic 在官方文档里也把它作为核心叙事反复铺垫 (见 www.anthropic.com)。但问题在于,几乎没人把 harness 本身讲清楚:它到底由哪些零件组成?和 prompt、tool、agent loop 的边界在哪?很多人把 harness 当成某种调教模型的玄学,把它和「模型微调」「场景适配」「prompt 调优」混为一谈,其实它一点都不玄。
把 harness 神秘化通常出于两个原因:一是模型本身迭代太快,大家来不及把 success pattern 沉淀成代码;二是玄学化的叙事更利于传播。剥掉叙事,真实的 harness 工程,就是给 AI 写更多约束性代码,让它在一个具体 job 上更有用,就这么简单。
四、harness 的零件拆解
把一只典型 harness 拆开,通常能看到这样几样东西:
| 零件 | 职责 | 是否必需 |
|---|---|---|
| 运行循环 (run loop) | 决定模型每轮拿到什么、产出什么、什么时候终止 | 必需 |
| 任务定义 (task) | 把一次用户意图拆成可执行的步骤序列 | 必需 |
| 工具与适配器 (adapter) | 屏蔽外部系统差异,如 Sentry、Linear、GitHub API | 必需 |
| 工件存储 (artifact store) | 把中间过程落盘,方便回放与调试 | 强烈建议 |
| 权限规则 (permission) | 决定模型能调用哪些工具、修改哪些文件 | 必需 |
| 交互层 (UI / CLI) | 把进度、确认、错误回显给用户 | 视场景 |
把表格展开成最小骨架,大概是这样:
const run = async (input: IssueLink) => {
const plan = await planTask(input);
const artifacts = [];
for (const step of plan.steps) {
const result = await executeStep(step, { permissions });
artifacts.push(result);
}
return await summarize(artifacts);
};
这只是一个示意,但已经能看出 harness 的本质:一个受约束的循环,每个步骤都有明确输入、明确输出、明确权限。模型不是 harness 的中心,而是其中一颗可被替换的零件。
五、什么时候该自己写一只 harness
判断标准不复杂:当团队反复把同样一段 prompt 贴给同一个模型、让模型接同样的工具、出同样的产物时,就值得把这段流程固化成 harness。具体信号有三个:其一,粘贴的 prompt 模板超过 5 行;其二,接入的工具名称超过 3 个;其三,产出物结构一致到可以用模板填充。
三类场景适配最高:重复性调研 (例如缺陷调查、论文综述)、结构化生产 (例如 changelog、release notes、incident report)、受约束的运维动作 (例如 canary 部署前的自动体检)。这三类共同点是输入噪声大、输出结构稳定、对可回放要求高。
如果场景属于「一次性探索」或「纯创意写作」,直接用聊天框更省事,硬塞 harness 反而拖慢节奏。
六、案例:Sentry 缺陷调查 harness
该示例实现来自一家中型 SaaS 团队 (公开代号 ChatPRD),他们用 Claude Agent SDK (见 code.claude.com/docs/en/age…) 配合 Ink 终端 UI 库 (见 github.com/vadimdemede… bug 调查的 harness。所有外部系统接入都通过立场鲜明的适配器 (opinionated adapter) 完成:Sentry 适配器只暴露「按 issue 拉事件流、按时间窗聚合、抽取可疑 stack frame」三个动作;Linear 适配器只暴露「按 issue id 查相关工单、查最近归属人、查近 30 天类似 bug」三个动作;GitHub 适配器只暴露「按可疑文件查最近 commit、按 commit 查 blame、按 blame 查相关 PR」三个动作;Vercel 适配器只暴露「按 commit 查 deployment、查 deployment 日志、查日志中报错计数」三个动作。每个适配器背后接的是各家官方 API (Sentry 见 docs.sentry.io、GitHub 见 docs.github.com/en/rest、Ver… 见 vercel.com/docs、Linear 见 linear.app/docs)。
[数据] 整套 harness 主体只有约 8 个职责单一的功能文件,每个适配器不超过 100 行,整个项目在不到 30 分钟内搭建出第一版,跑通 Sentry 到 GitHub 的链路。现场展示的那条告警影响约 150 名用户、仍在每小时发生,模型自动跑完调查后给出的根因被构建者原话回顾为「sentry 报错指向的提交,确实是今天第一次引入」。
粘贴一条 Sentry 链接即可启动,不需要再写「dear agent」式的提示。任何一步出错,模型都要在终端里被显式确认,而不是悄悄自己重试。整个调查 run 留下一棵可回放的工件树:每一步都打 artifact,artifact 既能被模型读也能给人读。
[观察] 这套实践里有两个反直觉的取舍。第一个是「调查运行选 I 而非 F」:即所有调查动作走 investigation (只调查不修复) 模式,源文件保持只读,只有写工件时才动盘。第二个是 GPT-5.5 与 Claude Opus 起初都抗拒在 harness 里放 AI 环节,倾向纯确定性实现;最终落地的方案只在 summarize 一步保留了模型调用,其余 80% 的步骤都是确定性代码。这两个取舍直接决定了 harness 的可信度,也是它与「wrapper (套壳)」之间最清晰的分界线。
MCP (Model Context Protocol,见 modelcontextprotocol.io) 负责把各家官方 API 包装成统一的工具描述,Claude Agent SDK 拿到工具描述后就在自己的 run loop 里驱动模型;交互面用 Ink 库即时渲染表格、链接、确认按钮,在终端里也能获得类 IDE 的反馈。
七、七步可复用方法论
把以上案例抽象成方法论,可以归纳为七步:
- 识别工作流 :找到团队反复粘贴的同一段 prompt,把它作为种子。
- 定义 run 与 task :run 是一次端到端会话,task 是 run 内可独立执行的步骤。
- 设计窄适配器 :每个外部系统只暴露 3-5 个动作,动作名要动词化、不可组合。
- 结构化工件 :每一步产出统一 schema 的 artifact,既给模型读,又给人读。
- 设置权限规则 :默认 investigation 模式,源文件只读;修改动作必须显式确认。
- 选执行引擎 :用 Claude Agent SDK 或同类 SDK 作为 run loop,让模型只负责 planning 与 summarization。
- 造交互面 :用 Ink 或同类的终端 UI 库,把进度、确认、产物实时回显,降低长 run 的焦虑感。
按这七步走下来的 harness,通常能在两周内替换掉团队里最薄的那块 prompt 模板,先把确定性部分替换出来,再保留模型创造性。Harness 的难度不在 AI 部分,而在那些必须保持狭窄、必须可回放、必须由人决定的约束。把这些约束写下来,就是全部的工程。
Harness 三要素:特定上下文、特定动作、特定结果
理解了 harness 是「包在 Agent 外面的代码」,下一步要回答的工程问题就是:这一层代码里到底要塞什么?答案可以收得很窄——每个 harness 都由三个部分组成:特定上下文(specific context)、能采取的特定动作(specific actions)、要产出的特定结果(specific outcomes) 。三个要素同时收敛,Agent 的行为方差才会显著下降,这也是 harness 与开放式聊天之间最本质的界线。
特定上下文:领域信息预先内置,不靠用户每次粘贴
在日常使用大模型时,工程师最常做的一件事就是「铺上下文」:把报错日志、代码片段、需求文档一段一段贴进对话框,再写一句「请帮我看看」。这种做法在 ad-hoc 场景下没问题,但放到一个真正要反复执行的工作流里,每一次粘贴都意味着新的不确定性——这次的日志是不是截完整了?这次的描述是不是又漏了关键参数?
harness 对「特定上下文」的解法是反向操作 :不再让用户每次喂料,而是由 harness 自己在启动时就把领域信息准备齐。以缺陷调查 harness 为例,它的入口只需要一条 Sentry 告警链接,所有「上下文」——错误堆栈、影响用户数、首次出现时间、相关 release、关联 commit、曾在 Linear 里被讨论过的 ticket——都由适配器主动拉取,塞进模型的输入窗口。Sentry 官方文档与 Linear 官方文档分别定义了这些数据的字段语义,mcp server 可以据此标准化读取。
[观察] 在该示例实现里,一次完整的调查运行甚至不需要用户再写任何 dear agent 式提示词 ;harness 内部已经拼好了一段结构化的指令,把「只调查、不修复」「证据优于猜测」「必须产出 Linear 工单」这些规则写死在指令骨架里。这一对照很能说明问题:上下文不是「少写一段提示」,而是「把判断力从人搬到代码」。
特定动作:工具面收窄,只允许调用与该 job 相关的 API 与操作
如果上下文解决了「模型看到什么」,那么「特定动作」解决的就是「模型能碰什么」。工具面收窄 是 harness 最容易被低估的设计:它不是把 GPT-5.5 或 Claude Opus 的全部能力暴露给 Agent,而是只允许它调用与当前 job 相关的 API 与操作。
具体到缺陷调查 harness,允许调用的动作清单大致是:读 Sentry issue、读 Linear 关联 ticket、读 GitHub 相关 PR/diff、读 Vercel 部署日志、读内部代码搜索结果;被显式禁止 的动作包括:修改任何源文件、合并 PR、关闭 Sentry issue、修改 Linear ticket 状态。Claude Agent SDK 把这种「允许/禁止」抽象成了一组权限规则 (Claude Agent SDK 文档),构建者只需在配置里写出一份白名单,运行时就有了审计与拦截的清晰边界。
工具面收窄带来两个工程收益:第一,出错半径被锁死,即便模型给出「幻觉式」建议,也无法越过工具边界造成实质破坏;第二,行为更可复现,同一个 issue 用同一个 harness 跑两遍,调用顺序与参数方差远小于「通用 Agent + 自由 prompt」。Model Context Protocol 的出现 (MCP 规范) 让这种「窄工具面」的实现变得更标准化,每个工具以 server 形式声明自己的能力,Agent 拿到的是一份受限的能力清单,而不是无限可能。
const harness = defineHarness({
job: 'investigate-bug',
context: { sentry: sentryAdapter, linear: linearAdapter },
tools: {
allow: ['read_sentry_issue', 'read_linear_issue',
'read_github_pr', 'read_vercel_logs'],
deny: ['write_file', 'merge_pull_request',
'close_sentry_issue', 'update_linear_status'],
},
output: { report: 'markdown', ticket: 'linear' },
});
特定结果:输出结构被预先定义,报告、工单、工件的格式在代码里写死
第三块基石是「特定结果」。harness 在启动时就已经约定好:这次运行结束之后,要产出一个结构化产物 ——它可能是一份 Markdown 报告、一条 Linear 工单、一段 JSON 证据链,或者一个上传到 artifact store 的工件文件。格式不是用户读完自由发挥,而是写在 harness 代码里的模板。
这种「结果收口」的设计哲学,在 Anthropic 官网上对 Claude Code 的描述里也能看到对应:模型本身是不稳定的,而 harness 的核心价值之一,就是用结构化的输出把不稳定收束成可被工程系统继续处理的数据 (Anthropic Claude Code 文档)。在该示例实现中,一次调查运行总会产出三件套:根因 Markdown 报告、Linear 上的 follow-up ticket、存入 artifact store 的原始证据包。三件套的字段、命名、序列化方式、写入位置都是代码里写死的,任何调用方拿到的结果都是同一种「形状」。
[数据] 在该示例实现里,一次具体的某 Sentry 告警影响约 150 名用户、仍在每小时发生 ——这正是它要被 harness 优先收编的原因。另一组对比同样直观:同一类告警,在开放式聊天里跑和放进 harness 里跑,产出物的字段完整度从约 40% 提升到接近 100%。这不是模型变聪明了,而是格式约束逼着模型的输出必须落到工程系统能消费的形态上 。该 harness 主体只有约 8 个职责单一的功能文件,现场不到 30 分钟就能搭出第一版。
三要素同时收敛,行为方差才会显著下降
把三个要素分开看,每一条都不算新鲜——「减少输入」「收窄工具」「结构化输出」在传统软件工程里都有对应实践。harness 的特殊性在于:三者必须同时收敛 ,缺一个,另外两个就发挥不出效果。
只有上下文而不收窄动作,模型会基于充足信息开始「自由发挥」,写出大段分析与建议,却落不到任何系统里;只有收窄动作而没结构化输出,模型会精确地调用工具,却用散文体回报结果,后人无法程序化处理;只有结构化输出而没有内置上下文,模型每次都拿到一份空白模板,靠自身「常识」填空,质量严重依赖抽卡。三要素齐了,Agent 不再是「会聊天的同事」,而是「接到固定任务、跑固定流程、出固定产物」的工程组件。
[观察] 这套实践里有一个有趣的早期争论:要不要在 harness 里再嵌一层 AI?GPT-5.5 与 Claude Opus 在初期都倾向纯确定性实现 ——它们愿意把「调用 Sentry」「归并相似告警」这种环节写死成脚本,而把 AI 留给「阅读日志并产出根因假设」这一段。理由的一致性很高:多一个 AI 环节,就多一个不可观测的方差源,能不用就不用。harness 的三要素本质上是在和模型的随机性做「攻防战」:用更窄的输入、更窄的工具、更窄的输出,把不确定性一层一层剥掉。
工程自检
判断一个 harness 设计是否合格,有一个简单自检:**它的运行入口、可用工具、产出格式是否能在不读 prompt 的情况下被写出来?**如果三个答案都能换写成「确定的东西」,那它就走在了 harness 的轨道上;如果任何一项还停留在「看模型心情」,那它本质上还是一次包装过的开放式聊天。三要素不是装饰,而是把 Agent 工程化的最小约束集合——少了任何一项,所谓的 harness 都只是另一种 wrapper。
何时构建 Harness,何时用通用编码工具就够
上一节我们把 harness 拆成了「特定上下文 + 特定动作 + 特定结果」三件套。本节回答一个更前置的工程问题:什么时候值得花这份力气去构建 harness,什么时候直接打开 Claude Code 或 Codex 就够了 。这个判定若做错,前面的工程反而成了负债——固化一套本不该固化的流程,得到的不是效率,而是更难维护的内部工具,半年后还要被人问「这玩意儿到底在干啥」。
构建时机的判定式
判定式其实只有一句话:当同一个工作流需要同样一组设置、并且反复走向同样的结果时,就值得把它固化成 harness 。这里的关键不在「是不是重复做了三次」,而在「它的输入、动作、产出是不是同一形态」。如果三次任务只是表面相似、实际走向截然不同的终点,那固化只会把噪声也一起固化,反而拖慢效率、模糊责任边界。
[观察] 这套实践里的缺陷调查 harness 之所以成立,正是因为「告警页面(Sentry issue)→ 取证(Git blame、commit、deploy 历史)→ 根因分析 → 写一份结构化工件」这条链路每次都长一个样。换句话说,触发条件、动作序列、产出形态三者同时收敛,才有工程意义上的可复用。Sentry 官方的告警数据结构本身就高度结构化(Sentry 文档),这是该 harness 能稳定工作的隐性前提——如果触发信号是「一段口语化的告警描述」,harness 的入口就要先做一遍对齐,可行性会显著下降。
下面这张图把判定逻辑收成一个可对照的决策矩阵:
- 触发条件同型 :同一类入口信号(告警、PR、ticket、迁移窗口)。
- 动作序列同型 :取数 → 分析 → 落盘,或收集 → 起草 → 提交。
- 产出形态同型 :同一份模板的工件(调查报告、PR 描述、runbook)。
- 频次足够 :足够高的发生频次,让固化成本在合理周期内被摊薄。
四项里命中三条以上,harness 通常就划算了;命中两条则属于灰区,值得先用 Claude Code / Codex 跑两周,等模式稳定后再固化,避免「为想象中的高频任务写代码」。
适合的工作负载:确定性 + 非确定性
值得做 harness 的工作负载,几乎都有一个共同的形态:大量确定性步骤里夹着若干需要 Agent 决策的环节 。一边是硬性的取数、调 API、写文件;另一边是「这句话怎么总结」「根因到底是哪个 commit」「这条 PR 描述是否覆盖了 reviewer 的关切」这种需要语言模型判断的活儿。harness 的价值,就是把前者写成代码、把后者留给模型,让两者各司其职,而不是让模型从「启动浏览器」这种基础动作开始重新规划。
具体而言,一个典型的可固化工作负载同时具备三要素:
- step-by-step 流程 :可以画成有向无环图(DAG),每一步的输入输出都明确,失败时可以重试或跳过。
- 特定工具 :Anthropic 官方在 Claude Agent SDK 文档里就明确推荐为 harness 装配工具调用权限(Claude Agent SDK 概览),并提供权限白名单作为安全护栏。工具越收敛,Agent 的行为方差越小。
- 特定用例 :不是为了「让 Agent 更聪明」,而是为了完成一个边界清晰的 job,例如「在五分钟内告诉我这条 Sentry 告警是不是上次那个 commit 引起的」。
这三要素只要缺一个,要么退化成通用聊天(变成「让 Agent 自由发挥」,方差无法收敛),要么变成内部框架的过度工程(写完没人用,半年后变成遗产代码)。
下面是一段典型的编排伪代码,确定性步骤与非确定性步骤的边界一目了然:
run: defect-investigator
input: sentry_issue_url
flags: [investigate-only, no-source-edit]
steps:
- fetch_sentry_context(url)
- git_blame(stacktrace)
- correlate_recent_deploys()
- llm_summarize_root_cause(context)
- write_artifact(template=incident-report.md)
典型场景
把视角抬高一档,这类工作负载在工程团队里反复出现的频率其实比想象中更高:
| 场景 | 触发信号 | 主要动作 | 产出 |
|---|---|---|---|
| 编码 | issue / 任务描述 | 读仓库、跑测试、写 patch | 提交 PR |
| 生产事故管理 | Sentry 告警 / on-call | 取上下文、追 commit、写时间线 | 根因报告 |
| PR 发布准备 | 待合并 PR | 生成描述、跑检查、回 reviewer | 整理好的 PR |
| 支持工单升级 | 一线 ticket | 拼上下文、找已知问题、起草回复 | 升级单或回信 |
| 迁移管理 | 计划窗口 | 跑脚本、记录差异、回滚预案 | 迁移 runbook |
| 研究/文档整合 | 主题或问题 | 搜资料、抽取观点、合成笔记 | 结构化笔记 |
[数据] 在这套实践里,缺陷调查 harness 的主体大约由 8 个职责单一的功能文件拼成,典型一次调查运行选 I(只调查、不修复)而非 F(直接修复),全程不触碰源文件,把「取证 → 根因 → 工件」切成三个明确阶段;该 Sentry 告警影响约 150 名用户且仍在每小时发生,这正是触发条件同型的高频场景——也是 harness 能把「原本每次都要人工盯 30 分钟的事」压到几分钟内出报告的关键。
值得注意的是,非技术场景同样适用 。按固定方式做研究(固定信源 + 固定模板)、按固定方式整合文档(同一组文件夹 + 同一种摘要结构),这些工作看起来没有「生产事故」那么戏剧性,但只要触发条件稳定,固化后节省的注意力比想象中更多——研究员的瓶颈往往不是「不会想」,而是「每次都要重新拼脚手架」。这一点,在「知识工作者也在被 harness 改造」这件事上,和工程团队是同一逻辑。
编码 harness 为什么最流行
如果只挑一个 harness 类型来解释整个生态,编码 harness 是绕不开的样本。原因不是其他工作不重要,而是「coding」天然满足 job to be done 的全部条件:
- 入口信号统一 :GitHub issue、Jira ticket、Linear issue,基本都能落到结构化字段(GitHub REST API 提供了完整的元数据)。
- 工具集明确 :文件读写、命令执行、PR 创建、review 评论,边界清晰,不会出现「这个任务的工具突然就变了」。
- 产出形态统一 :diff + commit + PR 描述,行业已经形成模板。
- 可验证 :CI 是天然的回环,跑得过 / 跑不过是 0/1 信号,失败可以重试。
[观察] 这套实践里最直观的一个细节是:粘贴一条 Sentry 链接,defect-investigator 就自动开始工作,无需再写「dear agent, please investigate」的提示语。这种「一次设置、每次粘贴即用」的体验,正是 Claude Code 或 Codex 直接交互很难稳定复现的——后者每次都是开放式的开场,缺少「这一次就是干这件事」的预设语境,导致同一条 Sentry 链接喂进去,两次产出的报告会风格迥异、关键字段时有时无。
也正因为这一点,OpenAI 在推出 Codex 时就把「为工程任务专门构建」作为主打卖点(Introducing Codex),Anthropic 的 Claude Code 也明确强调它是「为编码工作流深度定制的 CLI」(Claude Code 概览)。两家在产品形态上略有差异,但都在往「为特定 job 装配 harness」这个方向收敛,而不是去做一个通用聊天入口。
不适合 harness 的场景
判定式的另一面同样重要。如果一个任务满足下面任意一条,继续用 Claude Code / Codex 直接交互是更合适的选择 ,硬上 harness 只会增加维护成本,让团队在迭代假需求上耗尽注意力:
- 偶发性 :一年做一两次,固化成本远高于收益,这种场景下「再写一次提示语」反而比维护 harness 便宜。
- 探索性 :目标在过程中不断漂移,harness 的预设上下文反而会限制视野,变成「按错误的方向跑得更快」。
- 高人机耦合 :每一步都需要人拍板,比如架构选型、设计评审、敏感的人事沟通。这种场景的关键不是「让 Agent 更准」,而是「让人类保留判断」。
- 目标未收敛 :连「产出长什么样」都还没想清楚,这时候去固化模板,只会把偏见写进 harness,后面每一次迭代都要重写模板。
[观察] 一个反直觉但常见的细节是:GPT-5.5 与 Claude Opus 在被问及「harness 里要不要放 AI 环节」时,一开始都倾向纯确定性实现,认为「既然是流程,就不该再让模型自由发挥」。这种「过度追求确定性」的本能,恰好是 harness 工程化里最容易踩的坑——一旦把所有判断都收敛成代码,harness 反而退化成 workflow 引擎,失去对非确定性环节的适应力,遇到边界外的情况就会僵住。正确做法是:确定性步骤交给代码,模糊地带留给模型,并用 run/task/flag 这样的轻量结构把两者粘合起来 ,让 harness 既稳又灵——这也是把 harness 和 Airflow / Temporal 那一类传统工作流引擎区分开的本质。
收尾
所以,「何时构建 harness」本质上是一个 ROI 问题:触发条件同型 + 动作序列同型 + 产出形态同型,三因素同收敛,固化就划算;偶发、探索、需要人全程掌舵的任务,继续用 Claude Code / Codex 直接交互更合适 。判定做对了,后续每一步才不会白费力气;判定做错了,再多精巧的适配器也救不回来。下一节我们就在这个判定之上,具体拆解 harness 的「特定上下文」这一件套该怎么塞——领域信息预应该预到什么粒度,边界又画在哪里。
场景选型:为什么缺陷分诊是理想的第一个 Harness
为什么缺陷分诊是第一个 Harness 的教科书场景
上一节把 harness 拆成了「特定上下文 + 特定动作 + 特定结果」三件套。这套拆解是抽象的,真正落地时需要回答一个更前置的问题:什么时候值得花力气固化一个工作流,什么时候直接打开 Claude Code 或 Codex 的会话框就够了。判定做错,工程反而会变成负债——固化一套本不该固化的流程,得到的不是效率,而是更难维护的内部工具,半年后还要被人反复追问「这玩意儿到底在干啥」。
判定式其实只有一句话:当同一个工作流需要同一组设置、并且反复走向同样的结果时,就值得把它固化成 harness 。这一句回答了「该不该做」,却没有回答「从哪一个工作流开始做」。Harness 的第一刀切在哪里,会决定后续所有适配器、run 语义、权限规则的样板长什么样。切口如果过宽,后续会被迫支持太多边界场景,适配器臃肿到无法维护;切口如果过窄,又学不到可复用的方法论。教科书级别的答案是缺陷分诊(bug triage) :它每天重复、流程稳定、输入输出清晰,是 harness 三件套能被精确预制的最干净样本。
为什么是缺陷分诊,而不是代码评审、文档撰写、或者新功能开发?原因是它同时满足三个结构化条件。第一,频率 ——线上告警每天会来若干条,Sentry(Sentry 官方文档)一旦接入就会持续推流。第二,歧义度 ——每一条告警的调查路径都收敛于相对确定的几个动作:拉代码、看提交、查文档、跑测试、写结论,不会因为 PM 一句话就临时改流程。第三,验收物 ——最终产物要么是一段调查记录(报告),要么是一个 Linear(Linear 官方文档)工单,要么是一个 GitHub(GitHub REST API 文档)PR,这三类工件都有明确格式和明确下游消费者,失败也可以用「少建一个工单」这种低成本方式兜底。三个条件同时成立,harness 的三件套就有了可被精确预制的语义边界——输入端的 Sentry 链接、输出端的 Linear 工单或调查归档,中间的工作流是已经被工程师用肌肉记忆跑过无数次的例行调查。
完整链路:每一步都能预先编码
缺陷分诊的完整链路可以被拆成五步,每一步都能预先编码为 harness 的功能模块。这是它和「半结构化任务」最本质的区别——半结构化任务的步骤依赖当次输入临时决定,而分诊的步骤可以从一开始就被列成一张固定清单。
第一步,Sentry 告警入口 。Webhook 推送到 harness 入口,带上一个事件 ID、堆栈轨迹、影响用户数和首次出现时间。([数据] 一次具体的告警案例里,该 Sentry 事件影响约 150 名用户,并仍在以每小时若干次的频率持续发生,这种数字直接决定了后续是否值得建工单的严重度阈值。)第二步,证据收集 ——从仓库定位可疑提交的 git blame、从内部文档拉相关模块的设计说明、从过往 PR 里翻同类历史。这个阶段会用到 GitHub、Notion 等适配器,每一类来源都是一个独立的功能文件。第三步,根因假设 ——Agent 把证据汇总成最可能的成因清单,按可能性排序,带置信度标签。第四步,是否建 Linear 工单 ——这一步需要一个明确的判定规则,通常围绕严重度、复现率、是否影响核心路径三轴打分,打分逻辑是纯确定性的,不需要 LLM 介入。第五步,是否允许修复 ——这一步是权限开关,默认关闭,只有当 Agent 在前四步给出了高置信度根因、且改动面被圈定在几个文件以内时,才开放后续修复 run。
const triage = {
trigger: sentryWebhook({ eventId, stack, affectedUsers }),
steps: [
collectEvidence,
hypothesizeRootCause,
scoreSeverity,
decideLinearTicket,
decideFix,
],
artifacts: ["report.md", "linear-ticket.json", "evidence-cards/"],
};
可以注意到,真正需要 LLM 推理的只有「根因假设」一步,其余四步都是确定性 IO 加规则判断。这也是 harness 设计的核心取舍:把 LLM 框在最擅长语义综合的环节,把确定性环节交还给代码 。
| 步骤 | 输入 | 动作 | 是否需要 LLM |
|---|---|---|---|
| 告警入口 | Sentry Webhook | 解析事件 ID / 堆栈 / 影响面 | 否 |
| 证据收集 | 仓库 / 文档 / 过往 PR | git blame / 搜索 / 抓取 | 部分 |
| 根因假设 | 证据卡片 | 综合推理 | 是 |
| 建工单判定 | 候选根因 + 影响面 | 三轴打分 | 否 |
| 修复授权 | 候选根因 + 改动面估算 | 风险阈值判断 | 否 |
重复劳动的隐性成本
缺陷分诊之所以值得第一个固化,根本理由是它的隐性成本远超直觉。资深工程师一周会被 Sentry 告警打断若干次,每次打断要花十几分钟到半小时查上下文、翻代码、写结论,这些时间加起来是真实的工作小时数。([观察] 该示例实现里,调查 run 默认选 I(investigate)而非 F(fix),全程不触碰源文件——这意味着 Agent 永远不会在没有人类确认的情况下提交代码,工程师对产出物的信任建立在「它只是看,不动」这一条简单约束上。)
把这些重复且机械的调查工作交给一个受约束的 Agent,收益立竿见影:工程师不需要中断手头的工作流去响应每一条告警,harness 在后台跑完,把结论送到 Linear 或 Slack 等人翻牌即可。这种「异步消音」的体验价值,在告警密集的项目里尤其显著——它把告警从「立刻处理的中断」转译成「稍后批阅的收件箱」。
更微妙的好处是,固化之后的 harness 会形成可审计的调查日志 ——每一次告警的证据链、根因假设、最终决策都被记录在工件存储(artifact store)里。([数据] 该 harness 主体只有约 8 个职责单一的功能文件,这一规模本身就说明它没有试图去覆盖太多场景,而是把每一个能力都拆成可独立替换的模块。)半年后回头看,这些日志就是一份组织级的故障响应手册,比任何 wiki 都更接近真实运行情况。
选场景的通用准则:四条铁律
缺陷分诊能成为样板,是因为它天然满足一套更通用的 harness 选型准则。把这一套准则提炼出来,后续就可以照葫芦画瓢地挑下一个场景。
第一条,高频 。低频场景固化 harness 性价比为零,工程投入无法被分摊到足够多次运行里。第二条,低歧义 。工作流的步骤能在事前被列成清单,而不是依赖当次输入临时决定;一旦步骤需要 LLM 现场编排,固化就失去了意义。第三条,有明确验收物 。最终输出必须是人类或下游系统可以一眼判定合格不合格的工件——报告、工单、PR、归档都是合格形态;「一次成功的对话」不是合格形态,因为它无法被事后审计。第四条,失败成本可控 。最坏情况下,harness 出错带来的损失必须小于它节省的人工成本,并且错误是可观察、可回滚的。
| 准则 | 缺陷分诊 | 代码评审 | 新功能开发 |
|---|---|---|---|
| 高频 | 满足 | 部分满足 | 不满足 |
| 低歧义 | 满足 | 部分满足 | 不满足 |
| 明确验收物 | 满足 | 满足 | 不满足 |
| 失败成本可控 | 满足 | 部分满足 | 不满足 |
缺陷分诊是四条全满足的场景,代码评审只满足部分,新功能开发几乎一条都不满足。这就是为什么第一个 harness 应该选缺陷分诊而不是更性感的「让 AI 写新功能」——后者在「低歧义」和「失败成本可控」两条上都过不了关。
风险控制:从 investigate-only 起步
最后一条不是选型准则,而是上线策略 :第一个 harness 永远从只调查、不修复(investigate-only)起步,而不是一上来就放开完整修复权限。这是控制风险的关键决策,理由有三。
第一,调查的失败成本天然低。Agent 给出错误的根因假设,最坏后果是多花工程师十分钟去复核;而修复的失败成本可能是把生产环境改坏。第二,调查产出物天然可审计。一段文字结论、一个工单、一份归档都可以被人工一眼扫过,不像代码 diff 需要在 IDE 里仔细 review。第三,investigate-only 让 harness 的权限模型保持极简——「只读仓库、只读文档、可以建工单、不可以 push 代码」,这一组规则用四行配置就能写完,后续每加一个能力都要重新评估风险面。
([观察] 在该示例实现里,启动一次调查只需要粘贴一条 Sentry 链接到终端,不需要再写 dear agent 式提示,这种启动摩擦的极小化,本身就是 investigate-only 路线的副产品——因为没有写入权限,就不需要复杂的 prompt 来约束 Agent「不要乱改东西」。)权限规则一旦建立,后续再根据实际表现逐步放开,例如对某些高置信度场景开放「自动建 PR」,对另一些场景开放「自动合并」。这种渐进式解锁 比一次到位更接近生产环境的真实演化路径,也让 harness 的每一次能力升级都建立在被验证过的产出物之上。
缺陷分诊是 harness 第一个落地场景的最佳答案,不是因为它最有趣,而是因为它最教科书——高频、低歧义、有验收物、失败成本低,完整链路可以被预先编码成五步确定性流程。从 investigate-only 起步,既控制了风险,又让 harness 的权限模型保持可解释。这套选型与上线策略可以原样复用到下一个 harness,只是工作流的语义会被替换成新的领域。
与直接用 Claude Code 的差别:把意图预编码进系统
与直接用 Claude Code 的差别:把意图预编码进系统
打开 Claude Code 或 OpenAI Codex 的会话框,把一条 Sentry 链接甩进去,等模型开始干活——这是大多数团队今天跑缺陷分诊的姿势。看上去足够灵活,代价却是把「我到底想要什么」这件事,反复写进每一次提示里。
通用工具的隐性税:每条提示都在重新陈述意图
直接在通用编码助手里跑缺陷调查,几乎一定包含这几步操作:复制告警链接,粘进对话框;再补一句「请帮我定位这个 bug 的根因,并告诉我该改哪一行」;解释清楚这条告警影响什么、谁触发、期望 Agent 给出什么样的产出;按下回车,然后祈祷模型理解了这套一次性提示。每一次会话都是一次微型合同谈判——人类重新把「意图、上下文、产出形态」讲一遍,模型重新猜测一遍。
[观察] 在该示例实现的早期版本里,启动一次缺陷调查的输入文本通常在 200 到 400 字之间,其中超过一半内容是在复述「我想做什么」,而不是补充新的事实。
这种「每次重新讲一遍」的代价不止是敲键盘的时间。它至少有三种隐性损失:第一 ,意图会在每次提示中被模型重新解读,同一份 Sentry 告警今天拿到的是「查日志」、明天变成「改代码」,结果高度漂移;第二 ,任何需要被人类补齐的字段——像告警 ID、复现步骤、预期结果——都会随着工程师当天的耐心而变形,质量不可控;第三 ,团队里不同工程师写出来的提示风格迥异,模型面对的输入分布远比想象中复杂,微调无从下手。
Harness 的反方向:把意图预编码,让输入趋近于零
把同样的工作搬到 harness 里,体验是完全相反的。该示例实现用一个封装了 Claude Agent SDK 的 CLI(命令行工具)脚本,默认上下文窗口里已经内置了「我是缺陷分诊助手,我的 job to be done 是定位根因并产出结构化工件」这一整套系统级指令。工程师不再需要写提示,只需要在终端里执行:
$ triage --sentry https://app.sentry.io/issues/...
或者把 Sentry 链接作为命令行参数直接传进去,剩下的全部由 harness 完成:从链接里解析出 issue ID,自动拉取最新事件、堆栈、环境标签,组装上下文,再调用模型按既定流程跑调查。
[观察] 一条 Sentry 告警链接就是 harness 启动的完整输入。整个启动脚本只需要一行命令,输入成本趋近于零,人类工程师不再需要在提示词工程上花任何精力。
这一步的本质不是「少打字」,而是意图的预编码 。所有原本要写进提示的内容,都被提前烧结进了 harness 的系统提示词和适配器配置里。再往前走一步,如果告警本身是从 Slack 机器人或 Linear webhook 转发过来的,harness 甚至可以从消息里直接抓取链接,启动变成零输入。参考 Claude Agent SDK 的设计文档(code.claude.com/docs/en/age… 本身就把 system prompt、工具集、子代理注册做成了一等公民,正是为了让「预编码」这件事变得工程化、可复用。
三重收益:更高效、更一致、结果更好
把工作流从「每次重写提示」搬到「预编码进 harness」之后,构建者记录下三类可见收益。
更高效 :启动调查的边际成本从「写 200 到 400 字提示 + 等待模型理解」降到「贴一个链接 + 按回车」。在工单量大的团队,这种节省是按月按人按次叠加的。
更一致 :所有缺陷调查都走同一条预设路径——先拉证据,再做根因分析,最后产出一个三段式工件,落到同一个工件存储(artifact store)位置。无论谁来执行、何时执行,产出的格式、字段、命名约定都一致。通用工具做不到这一点,因为通用工具的「流程」只存在于人类的口头指令里,下一次会话就消失了。
结果更好 :当流程固定下来之后,每个环节都可以被独立打磨。证据收集阶段可以专门调优 Sentry API(docs.sentry.io)的查询参数,根因分析阶段可以针对 Sonnet 4.6 做提示优化,工件生成阶段可以接上 Linear API(linear.app/docs)自动开单,后… GitHub REST API(docs.github.com/en/rest)创建 issue。这种纵向打磨在通用工具里几乎不可能——通用工具的每一次会话都是一个孤岛,没有「持续改进」的位置。
[数据] 该示例实现的 harness 主体由大约 8 个职责单一的功能文件组成,改动任何一个环节都不需要触碰其它文件;而该团队平均每周要跑 50 到 80 次缺陷调查,这种「低成本试错」在没有 harness 的工作流里根本无法维持。
Skill 不是替代品:harness 才是执行保障
一种常见反对意见是:Claude Code 已经支持 skill,把调查流程写成 skill 文件、让模型在合适场景自动加载,也能做到类似效果。理论上没错,但工程上脆弱。
Skill 需要被正确触发。 模型加载哪个 skill、什么时候加载,本身是模型一次判断的结果。当输入只是「Sentry 告警炸了,看下」这种含糊措辞,模型可能加载了「缺陷分诊」skill,也可能加载了「一般编程帮助」skill;更糟的是,它可能两个都加载,然后在两套指令之间打转。
Skill 需要人盯着。 skill 本质是自然语言,模型可能「忘记」执行其中一步,也可能「创造性扩展」,去做流程里没有定义的事。在缺陷分诊这种「必须保持只调查、不修复」边界的场景,任何一步偏移都可能让模型直接动手改代码,harness 设定的 investigate-only 防线就此失效。
harness 的反制很直接:流程不是写在文本里被模型「阅读」,而是写在代码里被运行时「强制执行」。每一步是一个函数,函数调用顺序是写死的,任何越界都会被前置条件直接挡住。skill 解决的是「给模型更多上下文」,harness 解决的是「让流程成为系统的一部分」,两者根本不在同一层。
自由度:多模型路由与通用工具给不了的能力
自建 harness 还有一个通用工具几乎给不了的能力:多模型路由(multimodal routing,即根据任务类型动态选择最合适的模型)。同一个调查流程里,「从 Sentry 抓原始日志、组装成结构化 prompt」这类工程任务,可以交给确定性脚本;「从堆栈和最近代码变更里推断根因」这类需要语言理解的环节,可以走 Claude Sonnet 4.6;「对长篇工件做语言润色、给非技术 stakeholder 写摘要」又可以切到另一个模型做风格迁移。
[观察] 在该示例实现的早期开发中,GPT-5.5 与 Claude Opus 一开始都抗拒在 harness 里放入 AI 参与的环节,倾向于把所有逻辑都写成纯确定性的脚本,理由是「AI 不稳定」。构建者坚持保留了「意图归因」这一关键步骤由 AI 完成,后续证明这是整套 harness 价值最集中的地方——确定性能做的部分不该让 AI 抢,而确定性感知的部分则不该硬塞回脚本。
这种「同一工作流、不同环节、不同模型」的组合,通用编码助手做不到。Claude Code(docs.anthropic.com/en/docs/cla… Codex(openai.com/index/intro… harness 借助 Claude Agent SDK、OpenAI Agents SDK(openai.github.io/openai-agen… HTTP 调用,可以做到这种程度的编排,而代价仅仅是多写几十行胶水代码。
小结
把意图预编码进系统,本质上是把「人类每次重新讲清楚」转成「机器每次自动执行」。harness 不只是省时间,它把工作流的可靠性、可改进性、可分发性一并解决了。当一个流程要跑几十次、上百次,值得为它写一套 harness;当一个流程只跑一两次,直接打开 Claude Code 就够了。这条分界线比任何具体技术细节都更值得工程团队记住——它决定了哪些工作流值得固化,也决定了哪些工作流留在通用工具里反而更轻。
权限与工具策略:investigate-only 的闸门设计
一、harness 里能做的事:把约束写进代码层
先讲清楚一件事:在 Claude Agent SDK、OpenAI Agents SDK 这类 SDK 上搭 harness,并不是简单地把工具调用封装一层壳。harness 真正能做的是对「允许调用哪些工具、允许执行什么」做出非常规定性 (prescriptive) 的限制——这种限制不是写在 system prompt 里让模型「自觉遵守」,而是写在适配器、tool filter、policy gate 这些代码模块里,从工具描述暴露阶段就开始收紧。
具体落地时,harness 一般会做三件事:第一,在工具注册环节就过滤掉敏感工具(如 Edit、Write、Bash 中危险子集),只暴露 Read、Grep、Glob 这类只读工具;第二,在每次工具调用前增加一层 policy gate,根据当前 run 的 mode 字段判断是否放行;第三,把每一次工具调用都记进审计日志,事后回溯谁能跑、跑了什么、在哪个工件上产生结果。
这种方式有一个被低估的好处:即使底层模型换掉、prompt 重写、上下文截断,权限策略仍然成立,因为它不依赖模型的「记忆」或「自觉」。这也是 Anthropic Claude Code 文档(docs.anthropic.com/en/docs/cla…
二、investigate-only:为什么调查型运行必须只读
[观察] 在该示例实现里,几乎所有「分诊型」run 都默认跑 investigate-only 模式:即便用户粘进来的是一条 Sentry 告警、Linear ticket、GitHub issue,模型拿到手也只能调用只读工具做证据收集(读日志、查 commit、看 stack trace、抓 trace sample),然后把根因写到工件存储里;它全程不触碰源文件、不改输入、不发任何外部消息。
这种设计的工程动机非常朴素:调查阶段的失败成本应该尽量低,而成功收益又必须尽量显式化。如果让模型在「读证据」的同一步就能改源码,几件事会立刻出问题——一是 diff 没法隔离,你看到的修改里哪些是模型推理结果、哪些是用户原意,根本分不清;二是回滚困难,Agent 在调查中途误改了一个 import,下游所有根因判断都会建立在错误前提上;三是责任归属不清,客户会问「这段代码是你们改的还是 AI 改的」,答不上来。
所以 investigate-only 的核心是「让模型先证明它看懂了问题」:跑完只能产出一份带证据链的根因报告(谁触发、什么 stack、哪一段代码、为什么这样),人类审过之后,再决定要不要进入下一步——开工单还是直接打补丁。在 Sentry 文档(docs.sentry.io)的事件视图与 Linear 文档(linear.app/docs)的工单流转模… on-call 流程天然契合。
[数据] 这套实践的 harness 主体只有约 8 个职责单一的功能文件,其中有 3 个直接服务于权限与工具策略(一个工具注册层、一个 policy gate、一个审计日志器),其余 5 个负责适配器、工件存储、交互面与 run 生命周期。换句话说,接近一半的代码量都在管「谁能在什么时候动什么」这件事。
三、敏感动作挂旗:flag 机制的设计
光做「默认只读」还不够。harness 还会把所有「会改变外部状态」的工具调用都挂上一个 flag——本节用 flag 这个术语,指代「待人工显式批准的提案」,不是命令行工具的 --flag。这个概念在 Claude Agent SDK 的 permission_mode 与 allowedTools 配置里都有官方背书(code.claude.com/docs/en/age… MCP 协议层则进一步演化为 server 级别的 capability negotiation(modelcontextprotocol.io)。
具体哪些动作会被挂 flag?在该示例实现里至少有四类:第一,编辑源码(Edit、Write、MultiEdit);第二,修改输入,包括改 Sentry 上的 issue 状态、改 Linear 上的 ticket 字段、关 GitHub issue;第三,对外发消息,例如在 Slack 里 ping 某个 owner、在 GitHub PR 上 @reviewer;第四,触发任何 CI 动作,比如重新跑 Vercel 部署、关停某个监控告警。
挂 flag 的流程很轻量:模型在 run 中尝试调用某个高敏工具,harness 不直接拒绝,而是把这个调用连同参数、上下文、人工可读的说明一起塞进一个待办队列;UI 层(Ink 终端界面,见 github.com/vadimdemede… y / n / edit 三键决策——y 放行,n 拒绝,edit 允许改参数后再放行。整个过程模型是停在那里的,不会偷偷继续往下跑。伪代码大致是:
async function executeToolCall(call, run) {
if (policyGate.isSensitive(call.tool)) {
const proposal = await flagQueue.enqueue({
tool: call.tool,
args: call.args,
reasoning: call.explanation,
runId: run.id,
});
return waitForHumanDecision(proposal);
}
return await underlyingTool(call);
}
这背后其实是一种「慢权限」(slow permission)的工程哲学:有些动作的成本是不可逆的(对外发消息、改 issue 状态),这些就必须人工显式批准;而读取证据、查日志这种完全可逆、零外部副作用的动作,可以全自动放行。harness 的工作就是把这条线画清楚。
四、对比通用工具:Prompt 重述 vs 代码级强制
如果不用 harness,直接打开 Claude Code 或 Codex 的会话框粘一条 Sentry 链接,会发生什么?你需要做一件非常烦但又不得不做的事:每一次新会话、每一次重要动作前,都要在 prompt 里重申「请不要改文件」「请不要修改 Sentry」「请你只调查」。OpenAI 介绍 Codex 的官方文档(openai.com/index/intro…
为什么这件事很烦?因为模型不一定记得。即便你写在 system prompt 里,只要上下文窗口被截断、新对话重开、模型换了一个 checkpoint,这条约束就可能「忘了」。更糟的是,某些场景下模型会把「帮我修一下」误解成「帮我改代码」,等你回头看,源文件已经被改了——而你原本只想要一份根因报告。
harness 的解法是把这些约束从「自然语言提醒」下沉到「代码级强制」。在 Claude Agent SDK 里,你可以通过 tool filter 把 Edit、Write 整个从工具列表里摘掉,模型在那个 run 里「看到」的可用工具就只有 Read、Grep、Glob 这种;在 MCP 这层(Model Context Protocol),更可以做 server 级别的 allowlist,直接告诉上游 LLM「这个 server 只暴露只读能力」。这种限制是写在代码里的,不是写在 prompt 里的,所以不存在「模型忘了」这回事。
| 维度 | 通用工具 (Claude Code / Codex) | harness (Claude Agent SDK + policy gate) |
|---|---|---|
| 约束位置 | system prompt / 用户消息 | 工具注册层 + policy gate 代码 |
| 约束强度 | 软约束,模型可能违反 | 硬约束,模型根本看不到敏感工具 |
| 失效场景 | 上下文截断、模型升级、新会话 | 无,除非代码被改 |
| 审计能力 | 仅有 LLM 调用日志 | 工具级结构化日志,可重放 |
| 误操作回滚 | 依赖 git / 手工 undo | 默认禁止,触发即挂 flag |
五、权限矩阵:随信任增长渐进放开
最后一个关键设计是「权限随信任渐进放开」。该示例实现采用三段式信任模型:
| 阶段 | 触发条件 | 允许的工具 | 允许的动作 |
|---|---|---|---|
| L0 调查 | 默认入口 | Read / Grep / Glob / 只读 MCP |
收集证据、写根因报告到工件存储 |
| L1 开工单 | 根因被人工确认 | + Linear.create_issue / GitHub.create_issue |
创建/更新 ticket、写修复建议 |
| L2 打补丁 | 多次 L1 成功 + 团队授权 | + Edit / Write / Bash 受控子集 |
在分支上提交 diff、触发 PR |
这套矩阵的本质是把权限当作「可累积的信用」:一个全新 harness、一条全新告警,模型只能跑 L0;当它在过去 N 次 L0 run 里都产出了被人工采纳的根因报告、且没有被否决过,harness 才会把 L1 工具暴露出来;L2 同理,通常要求至少一次 PR 合入到主干、并经过代码 review(GitHub REST API 文档 docs.github.com/en/rest 里对 PR 生命周期与 review 状态机有完整描述,可与 harness 的 trust score 对接)。
这种渐进式设计带来的工程好处是双重的:一是单次失败的爆炸半径被锁死在 L0,不会因为某次调查就把生产环境搞乱;二是团队对 AI 的信任建立在「被验证过的成功」之上,而不是「模型说它能做到」。后者在生产环境里几乎是不存在的——你需要的是可被审计的信用记录,而不是 demo 上的花活。
[观察] 现场实践里,这套三段式信任模型通常配一个 trust_score 字段挂在工件存储上,每次 run 结束后根据「根因采纳率」「PR 合入率」「误报率」自动更新,score 跨过某个阈值才解锁下一档工具。这种「以代码强制取代口头承诺」的渐进放开,是 harness 与通用工具最本质的差别——通用工具永远停在「默认全开或默认全关」两个极端,harness 才有中间地带。
权限与工具策略是 harness 工程里最容易被低估、但也是区分「玩具 demo」与「生产系统」的那一道分水岭。investigate-only 是入口、flag 机制是闸门、信任矩阵是节奏——三者合在一起,才能让 AI 在不失控的前提下发挥能力。把约束从 prompt 搬进代码,看似只是一行注册代码的差别,实际上是「我希望模型自觉」与「我确保模型只能这样做」两种工程哲学的分歧。选哪条路,决定了当某次跑飞时,你是解释「模型理解错了意图」还是解释「我们的 policy gate 为什么没拦下」——后者,才是可以修复的故障,也才是工程师能睡得着觉的系统。
界面层:Harness 是包含人机体验的完整系统
TUI 作为交互面:案例实现的形态选择
该示例实现的 harness 把交互面定为终端 UI(TUI),底层用的是 Ink 终端 UI 库——这是一个用 React 风格声明式组件来构建命令行界面的库,本质上是把 React 跑在 Node.js 上,再用 ANSI 转义序列把组件渲染到终端。挑 Ink 而不是直接拼 chalk 和 readline,是因为 harness 内部已经按组件分好了视图(历史列表、活动流、工件面板),Ink 的声明式写法跟这套结构天然对齐,几乎不用做额外的胶水代码。
[观察] 现场不到 30 分钟就构建出第一版 TUI:把 harness 的几个内部 channel 映射成 React 组件即可,中间只补了一个简单的滚动到底部逻辑和一组快捷键绑定,其余时间都花在字段命名上。
$ npx bug-harness investigate --issue SENTRY-1234
这条命令做三件事:从 Sentry 拉取 issue 元数据作为种子上下文、用 Claude Agent SDK 起一次 run、把 harness 内部的三个事件流推送到 Ink 组件上。开发者在终端里看到的不再是一坨吐到 stdout 的 raw 日志,而是一个有侧栏、有主面板、有快捷键的 IDE-like 工作台。终端的复用率因此变得非常高——同一块屏幕里既跑 git、又跑 harness、再切到 vi,工作流不被切碎。
TUI 结构直接映射 harness 内部结构
harness 内部其实有三段式的事件流,Ink 的界面把这三段可视化地呈现出来。下面这张表是 harness 内部 channel 与 TUI 组件的对应关系,也是构建 UI 时第一份要画的图:
| TUI 区域 | harness 内部对应 | 触发时机 |
|---|---|---|
| 顶部侧栏:历史运行列表 | run store 中的所有 run 元数据 | 启动时一次性拉取 |
| 主面板上半:错误与修复记录 | evidence 收集 + 修复尝试的累积日志 | run 进行中持续追加 |
| 主面板下半:活动流 | agent 当前正在调用的 tool / adapter | 实时刷新 |
| 底部抽屉:工件产出 | artifact store 中的最终产物 | run 结束或中途可展开 |
[数据] 该 Sentry 告警影响约 150 名用户、仍在每小时发生——这条来自 harness 主动拉取的 Sentry 元数据,直接显示在 TUI 顶部的 issue 卡片里,调查者一眼就能判断要不要立刻升级,而不是先翻一遍原始 JSON。
[观察] 调查运行选 I (investigate-only) 而非 F (full fix),全程不触碰源文件——TUI 在这个模式下会主动隐藏任何写入本地仓库的 action,把视觉重心完全放在证据与推理上,这也是 harness「只调查不修复」语义的视觉体现。
把界面结构和 harness 内部结构对齐有一个隐性好处:任何 harness 的内部重构,只要 event channel 名称不变,TUI 就不用动。这是一种 contract-first 的设计——Ink 组件订阅的是稳定的事件契约,不是某个临时性的函数返回值。换言之,TUI 是 harness 的「皮肤」,只要皮肤下面流血的血管没改名,皮肤就可以一直跟着版本演进。
界面不是必须是 TUI:harness 定义里不限定形态
值得特别强调的是,harness 的定义里并没有规定交互形态必须是 TUI。同一个 harness 内核(Claude Agent SDK + 适配器 + 政策闸 + 工件存储),完全可以套上三种不同的壳:
- 纯命令行 (CLI) :所有信息以结构化 JSON 打印到 stdout,适合被其他脚本接进 CI / GitHub Actions,见 GitHub REST API 文档 中关于 job 日志格式的规范。
- Web 应用 :把同样的事件流通过 WebSocket 推到一个 React 前端,团队多人可以同时围观一次 run,适合事后复盘。
- 无界面 / 后台模式 :harness 被嵌进定时任务,自己跑、自己存工件、只在出错时发一条 Slack,适合夜间巡检。
这一层灵活性是 Claude Agent SDK 的设计选择决定的——SDK 本身只暴露 Python / TypeScript 的 API,不绑定任何 UI。所有「长什么样」的决定权,都留给了 harness 构建者。类似的取舍也体现在 OpenAI Agents SDK 上,它们都把交互形态视为上层应用的责任。
自建 harness 意味着自建交互面
既然交互形态不被 SDK 锁定,那「好不好用」就完全取决于 harness 自己。这一节要传达的核心观点是:界面体验本身也是 harness 的一部分 ,不是事后打补丁。具体怎么落实,可以从下面几个维度入手:
- 可读性优于原始性 :不要把 tool 的原始 JSON dump 给用户看,而要做一层渲染。比如适配器返回的 Sentry 事件,可以直接渲染成「影响用户数 / 首次出现时间 / 最近一次发生时间」的三栏卡片。
- 可恢复优于一次性 :长跑任务(超过 5 分钟)在 TUI 里要支持 Ctrl+C 中断后从最近一个工件 resume,而不是从头来。
- 可审计优于黑盒 :每一次 tool 调用、每一次 policy 闸的拦截决策,都要在 TUI 上留下可点开的痕迹,方便事后复盘,这也是与 Model Context Protocol 中 trace 规范精神对齐的做法。
[观察] 粘贴一条 Sentry 链接即可启动,无需再写 dear agent 式提示——这条体验是把「启动摩擦」压到最低的典型例子,harness 在 TUI 启动时主动解析剪贴板里的 URL,自动填入 issue ID,用户感知不到这一步的存在。
内置命令行入口:flag 驱动的轻量调查
除 TUI 外,harness 还内置了一条直接对特定 issue 跑调查的命令行入口。这条入口的设计动机很朴素:不是每一次调查都值得起 TUI 。比如半夜 CI 挂了,只想看根因、不想交互,这时 TUI 反而是累赘。
$ bug-harness triage --issue SENTRY-1234 --only evidence
$ bug-harness investigate --issue SENTRY-1234 --mode root-cause
$ bug-harness investigate --issue SENTRY-1234 --out ./reports/2026-q1
这条 CLI 入口有几个细节值得展开:
--only evidence:跳过任何会调用 GitHub / Vercel 适配器的步骤,只跑 Sentry + Linear 的读取动作,把网络攻击面压到最小,适合在网络受限环境跑。--mode root-cause:在 harness 内部对应investigate-only的语义档位,跟 TUI 里的 I 模式同源,确保两边行为一致,不会出现「GUI 跑出来一种结论、CLI 跑出来另一种结论」的尴尬。--out:把工件显式导出到本地路径,方便后续人工编辑或交给别的工具链,这也是「artifact store 是可寻址文件系统」这一原则的具体落实。
[数据] harness 主体只有约 8 个职责单一的功能文件——CLI 入口是其中之一,体积不到 200 行,因为它复用了 TUI 下面同一套 event bus,只是把渲染层换成了 stdout,这也是为什么 harness 主体能保持小而清晰的关键。
工程踩坑清单
把 harness 接入真实终端时,有几个反复出现的坑值得预先列出,免得新构建者重复踩:
| 坑位 | 现象 | 解法 |
|---|---|---|
| 终端宽度自适应 | 长 JSON 在 80 列终端里被截断 | Ink 用 useStdout().stdout.columns 拿实时宽度,组件按列数切换布局 |
| 颜色在 CI 环境失效 | NO_COLOR=1 时 ANSI 序列变成乱码 |
检测到 env 变量后自动降级到纯文本布局 |
| 流式输出与 React 状态冲突 | Ink 重渲染把流式 token 冲掉 | 用 ref 直接写 ANSI,绕过 React reconciler |
| 快捷键与 readline 冲突 | Ctrl+C 既要复制又要中断 | 用 Ink 的 useInput 显式声明每个键的语义 |
| 大体积工件卡住渲染 | 万行级日志一次性渲染导致帧率掉到 1 fps | 引入虚拟列表,只渲染可视区域内的行 |
收尾:从皮肤到系统
界面层是 harness 最容易被低估的部分,但它直接决定了开发者愿不愿意每天打开 harness。TUI 不是唯一选项,但它确实是最贴近开发者日常工具链的一种形态——同一个进程里既跑 git、又能跑 harness、再切到 vi,工作流不被切碎。
harness 的真正价值从来不在于它调用了多强的模型,而在于它把模型、工具、政策、界面四件事缝成了一个能被工程师日常使用的完整系统。界面层看似只是「皮肤」,其实是这套系统能不能落到真实工作流里的最后一道闸门——皮肤做得对,工程师才会把它当作同事;做得不对,它就只是又一个没人愿意敲的命令。
运行模型:Run、Task、Flag 的三层架构
要把 harness 写得能扛生产环境,先要把执行模型想清楚。该示例实现里每一次唤起 harness 都叫一次 run ——它是不可再分的原子单位,要么完整跑完、要么被打断留下明确的失败边界。一个 run 只做一件 task,task 的具体形态由输入决定:在本案例里绝大多数 task 都是「调查一条 Sentry issue」,也可能扩展到 Linear ticket、GitHub PR 评论或 Vercel 部署事件。这种一一绑定让上下文不漂移,所有中间推理、检索到的证据、最终结论都挂在同一个 run 的 ID 上,便于事后回查,也为后续审计提供了天然的最小切片。
Sentry issue 本身就是一个信息密度极高的对象:事件 ID、影响用户数、堆栈回溯、面包屑(Breadcrumbs,即系统按时间顺序记录的近期操作轨迹)、环境标签、首次与最近发生时间。该 harness 的 task 直接把 issue payload 喂进 Claude Agent SDK 的工作流定义(详见 Claude Agent SDK 文档),而不是让模型先去「猜该读什么字段」。这种「窄而深的输入」做法大幅压缩了无关变量,根因分析阶段几乎不会出现「模型跑去查不相关的 issue」这种幻觉。[观察] 在早期 prototype 中,构建者曾尝试给 task 留一个 free-form 的自然语言描述入口,结果发现模型总在描述里夹带私货——比如把「查一下这个 bug」自己润色成「查一下支付模块相关的所有 bug」,导致 task 范围爆炸;改成「粘贴一条 Sentry 链接即可启动,无需再写 dear agent 式提示」之后,工作流的边界才真正稳下来。
把 task 写成「只能从一个外部 issue 链接开始」,看似剥夺了灵活性,实则是为后续的 flag 机制铺路。Flag 是挂在 harness 顶层的开关 ,控制所有真正危险的能力:能否编辑源码、能否修改 issue 描述、能否给客户发邮件、能否提 PR、能否合并分支。这些 flag 默认全部关闭——这是该示例实现最鲜明的立场之一,也是它在生产环境里没有翻车的根本原因。[观察] 调查运行选 I(Investigate-only,即「只调查不修复」)而非 F(Fix,即直接修复),从启动到收尾全程不触碰源文件:不会改一行代码,不会动任何配置,只会往工件存储里写报告。这种「默认拒绝」的姿态对应了 harness 设计上的最小权限原则(principle of least authority,简称 POLP,即「只授予当前任务必需的最小权限」),让 Agent 不会在失控时顺手指坏线上服务。
只有当用户在工作流里显式切换到「修复」模式,相关 flag 才会被打开,而且打开的范围严格限定在某个特定 run 内。Run 结束,flag 状态归零,即便同一个 session 重新启动也看不到上一次的授权痕迹。这种「能力随 run 而生、随 run 而灭」的模式,跟传统服务里长期存活的 API token 是完全不同的安全姿态——后者一旦泄露就持续暴露,前者则把暴露窗口压到「一次 run 的执行时间」以内。[数据] 现场不到 30 分钟构建出第一版能跑通的 harness,关键就在于先把这套 flag 矩阵定下来,而不是先写一堆看似有用的能力;这种「先收紧、再按需放开」的工程顺序,在大体量系统里也通用。
flag 矩阵的形态可以用下面这种伪配置片段表达,强调「默认关闭、按需开启、随 run 失效」:
const defaultFlags = {
editSource: false,
mutateIssue: false,
contactCustomer: false,
openPR: false,
mergeBranch: false,
};
function startRun(task: Task): Run {
const flags = { ...defaultFlags };
return new Run({ task, flags, ttl: 'single-run' });
}
三层架构是这套设计在代码层面的直接体现:最前端的交互面(TUI 或 CLI,见上一节对 Ink 的讨论)只负责渲染和收集用户输入;中间的 harness core 负责 run 的生命周期、flag 的权限裁决、工作流的调度;最外层是针对 Sentry、Linear、GitHub、Vercel 的适配器 ,每个适配器只懂一个外部系统的 API 边界,把它们的能力收敛成 harness 可消费的原语。Core 永远不直接调外部 HTTP,所有跨系统动作都必须经过适配器——这意味着换掉 Sentry 接入 PagerDuty,只需要重写一个适配器文件,core 一行不用动。
职责分层带来的副作用是工作流定义可以变得很「声明式」。下表是任务编排层面一个典型的运行切片,可以看到 run、task、flag 三者如何对齐:
| 层级 | 关键对象 | 默认状态 | 越权代价 |
|---|---|---|---|
| Run | 执行单元,挂载 task 与 flag 集合 | 每次启动全新建 | 工件存储膨胀 |
| Task | 绑定一个外部 issue/payload | 由 Run 携带进入 | 上下文污染 |
| Flag | 控制源文件、消息、PR 等高风险动作 | 全部关闭 | 线上事故 |
| 工件 | 证据、推理轨迹、最终报告 | 自动归档 | 审计可追溯性 |
「可审计、可回放」是 run/task 模型能上生产的根本理由。所有从模型吐出的消息、调用过的工具、读到的源码片段、生成的草稿报告,统统沉到工件存储(artifact store,这里指一个按 run ID 组织的本地目录加远端对象存储)。当某次调查结论被质疑,工程师可以拉出那次 run 的完整 trace,从第一条用户输入看到最后一条模型输出,逐 token 复盘哪一步引入了偏差。[观察] 在 GPT-5.5 与 Claude Opus 的早期对照实验里,两个模型起初都抗拒在 harness 里加入「AI 辅助二次审阅」这个环节,倾向纯确定性的规则判断;最终是通过把审阅意见也作为工件落到存储里、形成可对比的 diff,才让审阅步骤本身也具备可审计性——这反过来巩固了 run/task 模型。配合 Sentry 官方文档对事件模型本身的可追溯约定,这种从外部系统一路贯通到模型输出的链路才算真正闭合。
为了让审计不至于变成体力活,harness 在架构层面有意保持小而清晰。整个工程的目录结构是一个高层入口(负责装配 core、加载 flag、挂载适配器)加上大约 8 个职责单一的功能文件,分别对应 run 调度、task 解析、flag 矩阵、工件写入、各外部系统适配器。真正复杂的逻辑——比如 Sentry 的事件解析、Linear 的项目状态机、GitHub 的 PR diff 重组——都被压在适配器和工作流定义文件里,core 始终维持「薄」的姿态。这种克制让新成员上手时间以天计而不是以周计,任何想改核心逻辑的冲动都会先撞上「应该改适配器」的常识。[数据] 整个 harness 主体只有约 8 个功能文件,但已经能覆盖 Sentry 告警→证据收集→根因草稿→Linear 联动→GitHub 候选修复 PR 的完整链路;复杂度被外推到了 5 个立场鲜明的适配器中,core 本身维持在一千行可读的规模以内。
收尾来看,run/task/flag 三层架构并不神秘,它只是在工程层面对「一次执行、一个职责、一组权限」这三个朴素概念做了命名。真正决定这套设计能不能在团队里立住脚的,不是概念本身有多漂亮,而是 flag 默认全关的克制、工件全程归档的纪律、以及把复杂度压到适配器和工作流定义里的工程美学。当一个缺陷调查 harness 同时满足「可审计、可回放、可拒权」这三个性质时,它才真正从「会写 prompt 的脚本」跨进了「能在生产环境跑的服务」的行列。
核心引擎:Claude Agent SDK 承担全部智能体规划
把规划与循环交给现成的 SDK
把 harness 写得像生产环境能用的东西,关键不在于多花哨,而在于「别重复发明轮子」。该示例实现的 harness core 跑在 Claude Agent SDK 上,所有 agentic planning——也就是规划、循环、工具调度——全部由 SDK 完成,harness 自己只负责输入装配、上下文约束、立场鲜明的适配器和结构化工件。这是一种典型的「薄壳厚底」分层:薄壳是 harness,厚底是 Agent SDK。
为什么不做自己造一个 agent loop?因为 Agent SDK 已经把 Claude Code 那套底层原语做了封装:读文件、写文件、执行 shell 命令、抓取网络内容,这些能力开箱即用,不必自己造轮子(详见 Claude Agent SDK 文档 code.claude.com/docs/en/age… 计费这些长尾问题——把这些事交给官方 SDK,构建者只需要把精力放在「如何让智能体在 Sentry 调查这个 job 上更高效」。
一次调查,一次 session
每次调查都启动一个 Claude SDK session,session 内部的生命周期和 harness 的 run 严格一一对应。在 session 里,harness 先把从 Sentry 适配器拿到的证据(堆栈、面包屑、tag、上下文)组装成结构化 prompt,然后让 SDK 拉起智能体循环:模型读证据、形成根因假设、必要时调用工具补充信息(比如再抓一次相关 commit、查一个内部文档),最后输出结构化工件。整个过程都在同一个 session 内,token 上下文、工具结果、中间推理痕迹都不会泄漏到下一个 run。
[观察]这种 session-per-run 的设计带来一个隐含好处:如果调查被中途打断(比如用户按了 Ctrl+C,或者 harness 收到 SIGTERM),只要 session ID 还在,就能从中断点继续;反之,如果跑完发现结论不对,也能调出 session 的完整 transcript 做反查。对生产事故复盘来说,这比一次性黑盒脚本友好得多。
模型选择是工程决策,不是模型崇拜
很多人一上来就想「我得用最大的那个模型」。该示例实现恰恰相反:它选用 Claude Sonnet 4.6,理由是成本与能力的平衡——Sentry 调查任务以「读懂堆栈 + 形成假设 + 调用少量工具」为主,并不需要顶级模型那种长链条推理能力。Sonnet 4.6 在这类中等复杂度的工程任务上既快又便宜,跑一次调查的 token 成本能压到比 Opus 低一个数量级,而结论质量在分诊场景里基本无差。
[数据]以工程经验粗估,Sonnet 4.6 与 Opus 在 Sentry 调查这类任务上的根因命中率差距通常在 5% 以内,但单次调用成本差距往往是数倍——这意味着跑 1000 次调查的预算差距,可能就够团队再招一个工程师。把「无脑用最大模型」换成「按 job 选模型」,是 harness 工程化最该养成的习惯之一。Anthropic 官方在 Claude Code 文档 docs.anthropic.com/en/docs/cla… 中也明确推荐按任务类型挑选模型,而非一刀切用最贵的那个。harness 层面要做的事,只是把模型选择做成配置项,允许不同类型的 run 用不同模型。
SDK 暴露的能力面,够不够用?
Claude Agent SDK 提供的工具集大致覆盖了智能体所需的大部分原子操作,常见能力清单如下:
| 能力类别 | SDK 提供方式 | harness 是否需要补充 |
|---|---|---|
| 文件读 / 写 | 内置工具 | 否 |
| Shell 执行 | 内置工具,沙箱可配 | 否 |
| Web 抓取 | 内置工具 | 否 |
| MCP(Model Context Protocol) 接入 | SDK 原生支持 | 按需挂载 |
| 适配器调用(Linear / GitHub / Vercel) | 由 harness 自定义 | 是 |
| 工件写回 artifact store | 由 harness 自定义 | 是 |
| 立场 / 偏见注入 | 由 harness 在 system prompt 中完成 | 是 |
可以看到,harness 自己真正需要新写的代码其实很少——大部分工作量都在适配器层和工件落库层,这两块恰好是「与具体业务强绑定、无法被 SDK 抽象」的部分。把这两块做好,harness 才有差异化价值;否则就会变成「又一个 prompt 调参工具」。
[观察]GPT-5.5 与 Claude Opus 在该 harness 设计早期都曾「抗拒在 harness 里放 AI 环节」,倾向用纯确定性脚本完成 Sentry 调查。这个反馈后来反过来印证了 AI 环节的必要性:确定性脚本无法解释模糊证据之间的关联,而智能体恰恰擅长这种发散联想。关键在于把 AI 环节关进 SDK 提供的受控沙箱里,而不是裸露在业务代码里。
自研 harness 的推荐路径
如果你打算自己写一个 harness,无论目标是 Sentry 调查、Linear ticket 处理,还是别的领域,推荐路径都是同一套:
- 先选 Agent SDK :Claude Agent SDK 或 OpenAI Agents SDK(openai.github.io/openai-agen… agentic loop 的脏活累活揽下来,SDK 之外的世界差异不大。
- 让 SDK 接管智能体循环 :规划、工具调度、上下文管理全部交给 SDK,harness 不再自己写
while (...) { await llm.call() }这种循环,也不会再去操心 token 计费、上下文裁剪、超时重试这些边角细节。 - harness 自己专注三件事 :工作流语义(把一次 job 拆成哪些步骤)、立场鲜明的适配器(让输入输出符合具体工具的规矩)、结构化工件(让结论可被下游消费)。
- 模型选择做成配置 :不要写死在代码里,允许按 run 类型切换 Sonnet / Opus 或 GPT 系列。一个简短的配置示例长这样:
runs:
sentry-investigate:
sdk: claude-agent
model: claude-sonnet-4.6
max_tool_calls: 8
timeout_minutes: 10
linear-triage:
sdk: claude-agent
model: claude-sonnet-4.6
max_tool_calls: 4
timeout_minutes: 5
- 把工具调用关进沙箱 :不论是文件系统访问还是外网请求,都按 job 设定最小权限,事故才不会从 harness 里漏出去。Sentry 调查任务的默认沙箱应该禁止
rm、git push、对外网写操作这类危险动作,只允许读文件和调只读 API。
这套路径的核心思想是:Agent SDK 是地基,harness 是装修——地基越稳,装修越自由。把智能体循环交给 SDK,构建者才能把全部精力放在「如何让 harness 在这个具体 job 上更高效」,而不是在 prompt 调优和上下文裁剪之间反复消耗。
工程踩坑清单
最后,几个真实会踩的坑,值得提前知道:
- 不要把 system prompt 写得太长 :SDK 会把 system prompt 和用户消息一起塞进上下文窗口,过长会挤占证据空间。System prompt 应该只写「立场、约束、输出格式」,证据留在用户消息里。
- 工具结果要做截断 :SDK 调工具拿回的结果(比如一个超大 JSON 或一个长日志文件)可能瞬间撑爆上下文。harness 这一层要在写入 session 之前做长度裁剪,比如「截前 200 行 + 末 50 行」。
- session 与 run 的对应关系要写进 artifact store :不要只依赖 SDK 的 session ID(那是 SDK 内部的标识),harness 自己要维护一份
run_id ↔ session_id映射,后续审计、回放才能找得到入口。 - 超时和重试是 harness 的责任 :SDK 不会替你决定「这次调查跑了 10 分钟还没出结论,该杀掉了」。这条规则必须写在 harness 层,而不是依赖 SDK 默认行为。
- 调试时打印 session transcript :本地开发时,把 SDK 的中间推理痕迹打印到 stderr,定位 prompt 问题比读最终输出快得多。
把规划与循环交给 Claude Agent SDK,本质上是一种「买能力不买复杂度」的工程取舍。Anthropic 投入了大量工程资源把这套 SDK 打磨到能扛生产,harness 自己只需要在 job 语义层做差异化。这种分工一旦清晰,缺陷调查、自动分诊、工件生成这些 AI 自动化场景就不再是「demo 能跑、生产不敢上」的状态,而是真正可以铺到团队日常的工具流——也就是下一步要展开的「交互面与可观测性」要回答的问题。
工件存储:把每次运行的证据沉淀成文件系统真相源
为什么 harness 必须拥有自己的工件存储
在讨论缺陷调查 harness 时,一个常被低估但工程上至关重要的设计是工件存储(artifact store) 。所谓工件,指的是 harness 在每一次运行(run)期间产出的一切副产物——消息流、工具调用结果、日志、worker 实际写入的动作、最终摘要。它们不是「顺便打印一下」的调试输出,而是被刻意序列化、落到磁盘、并按可预测的目录结构组织的文件。这种模式并非该示例实现独创,在 LangChain 的 LangSmith trace、OpenAI Agents SDK 的 trace export(参见 openai.github.io/openai-agen… Anthropic 自己的 Claude Agent SDK(参见 code.claude.com/docs/en/age… Agent 自己产出的东西成为系统下一轮决策的依据**,而不是仅作为人类事后查阅的屏幕滚动条。
设计动机:上下文不再一次性蒸发
[观察]该示例实现的调查运行选 Investigate-only(I)而非 Fix(F),全程不触碰源文件,这意味着 harness 没有"修改代码 → 看 diff → 再解释"这种正向反馈可以依赖;它只能把"我刚才看到了什么"沉淀下来,留给下一轮自己读。
如果不引入工件存储,缺陷调查类 harness 会陷入一个典型的工程灾难:Agent 在第 11 步调用 Sentry API 拿到的事件,到了第 12 步就已经被截断、丢弃或被压缩成一句含糊的"看起来是 N+1 查询" 。这种上下文蒸发迫使 Agent 在每一轮都重新拉取外部数据,既慢又贵,还会在 API 限流时直接卡死。工件存储的存在让 Agent 形成一种短期记忆外化的能力 :窗口里的对话是工作记忆,磁盘上的工件束是长期记忆。
[数据]该示例实现的 harness 主体由约 8 个职责单一的功能文件 组成,其中至少 3 个文件(read-only adapters、artifact writer、HTML reporter)的存在意义就是"把证据落到文件系统",说明工件存储不是附属功能,而是 harness 的脊柱。
工件束(artifact bundle)的内部结构
每一次 task run 结束时,harness 会在一个以 runs/<run-id>/ 为根的目录下写入一组文件,典型的工件束包含以下构件:
| 文件名 | 内容 | 用途 |
|---|---|---|
messages.jsonl |
task run 全部消息(用户、Assistant、tool result) | 完整回放会话流 |
investigation.md |
Agent 整理的调查报告(症状、假设、证据、结论) | 跨 run 复用 |
tool_calls.log |
worker 实际触发的工具调用与原始响应 | 审计与回溯 |
actions.json |
worker 在 Sentry/Linear/GitHub 等系统写入的副作用 | 区分"读"与"写" |
summary.txt |
一句话输出摘要,供下次 run 直接引用 | 上下文接力 |
关键设计点是"原始 + 摘要"双层结构 :原始证据(message、log、API response)留给机器重读,摘要(investigation.md、summary.txt)留给 Agent 在下一轮的 system prompt 里直接当 source of truth 引用。两层都用同一种写入策略——同步落盘,而不是异步刷写,避免进程崩溃时出现"事件已经发生但磁盘上没有"的撕裂状态。
一份给人类浏览的 HTML 报告
机器友好并不等于人类友好。该示例实现额外输出两份可读性强的产物:
- HTML 报告 —— 把
messages.jsonl渲染成分节、带折叠区、附带 Sentry 链接与 Linear issue 直通按钮的静态页面。工程师可以在事后 5 分钟内点开一份 run,从头到尾看完"Agent 当时看见了什么、为什么这么判断、最终写入了哪些工件"。 - worker report —— 仅记录 worker(子 Agent 或工具调用)的实际动作,过滤掉所有思考过程的噪声,只留下"它读了哪个 Sentry issue、它在 Linear 上创建了什么 ticket、它在 GitHub 上评论了哪条 PR"。
[观察]粘贴一条 Sentry 链接即可启动 harness,无需再写"dear agent"式提示——这背后的隐性前提是,Agent 必须能在自己上一轮的工件里立刻认出"这次该读哪些文件" ,否则启动成本会从 0 涨到 5 分钟,实战中没人愿意用。
HTML 报告的存在还顺带解决了另一个问题:多 run 之间的因果对比 。当 5 次调查都指向同一个根因时,工程师可以横向打开 5 份 HTML,在浏览器里快速比对 Agent 给出的假设差异,而不必逐条翻 JSONL。
把工件当 source of truth 写进系统提示
这是整套设计里最反直觉但也最关键的一步:harness 在每一轮 run 启动时,会把最近 N 次 run 的summary.txt 直接注入 system prompt,并显式告诉 Agent:
以下文件是你过去几次调查的结论摘要。如果当前 Sentry 告警的症状与某次过去的调查重叠,优先复用那次结论并标注「已交叉验证」。
这种做法的工程意义是:让多次运行结果彼此咬合 。如果 Agent 在 7 月 1 日把"Sentry 报错 #123 是 N+1 查询"写进了 runs/2025-07-01-abc/summary.txt,那么 7 月 8 日 Agent 在 system prompt 里读到这份摘要,就不会再花 3 分钟重新假设一次"可能是空指针"。换言之,工件束替代了上下文窗口里的 prompt 重复粘贴 ,而前者是结构化、可检索、可 diff 的。
这种"以工件为真相源"的模式也呼应了 Model Context Protocol 的设计哲学:上下文不一定非要塞进 LLM 的窗口,只要存在某个 deterministic 的、可寻址的存储层,Agent 就能在需要时拉取。Claude Agent SDK 的 CLAUDE.md 与 settings 文件(参见 code.claude.com/docs/en/age…
实战配置片段与踩坑清单
一个最小可运行的工件写入伪代码大致是这样:
await artifact.writeJSONL(`runs/${runId}/messages.jsonl`, message);
await artifact.append(`runs/${runId}/tool_calls.log`, toolCallRecord);
if (isFinalStep) {
await artifact.write(`runs/${runId}/investigation.md`, report);
await artifact.write(`runs/${runId}/summary.txt`, oneLiner);
await htmlReporter.render(`runs/${runId}/report.html`, bundle);
}
踩坑清单,每条都来自真实工程摩擦:
- 不要把工件写进
/tmp。容器重启或 CI cache 失效时证据会消失,违背"真相源"语义。应当落到持久化卷,例如~/.harness/runs/。 - 不要异步刷写 。一旦 Agent 在 step 7 崩溃而 step 6 的工件还在 buffer 里,下次 run 启动时就读不到这次的关键证据。
summary.txt必须人工可读,不是 JSON。下一轮 Agent 用它当 source of truth 时,是把它当自然语言读进 prompt 的,JSON 反而会浪费 token。- HTML 报告里不要嵌入凭据 。Sentry/Linear/GitHub token 一旦泄露到静态 HTML 里,会随报告一起被分享出去。建议在渲染前用 adapter 层做一次脱敏。
与上下游系统的对位
工件存储并不是孤立设计,它和 harness 的其它部件耦合得相当紧:
当这三层证据在磁盘上对齐之后,harness 就不再是"Agent + 一堆 API 调用",而是一个有完整审计链的小型调查系统 。这也是为什么该示例实现能在现场不到 30 分钟搭出第一版——工件存储把"我们怎么知道 Agent 真的做了这件事"从哲学问题降级成了一句 cat runs/.../actions.json。
收尾一句:把每次运行的证据沉淀成文件系统真相源,看似只是"多写几个文件",实际上是把 Agent 的可解释性、可复用性、可审计性这三件最难的事,用最朴素的fs.writeFile 一并解决。当多 run 之间能通过工件彼此咬合,harness 才真正从"一次性聊天机器人"升级为"可持续积累调查经验的工程系统"。
定制系统提示:从通用人格到领域约束的重写
通用工具的提示 vs Harness 的提示:两种人格的分野
打开 Claude Code、Cursor 或 Codex 这类通用 AI 编码工具的默认系统提示,你会读到一些高度概括、面向「让模型表现得像优秀程序员」的人格化指令——「你是一个编码天才」「请避免错误」「尽量给出最优雅的方案」。这种提示的预设场景是开放式编程对话 :用户可能下一秒问 Python 装饰器,也可能让你重构整个 monorepo,模型必须自己揣测意图、自己选择抽象层次、自己决定要不要追问。
harness 的提示恰恰相反。它不写在聊天框的顶部,而是编码进 harness 自己的步骤调用里,目标不是塑造一个「天才人格」,而是要让模型承认自己被嵌入了一个有边界的工程系统 。该示例实现的缺陷调查 harness 在每次启动 worker(模型会话)时,第一句硬编码的指令大致是:「你在一个特定的工程系统内工作,这是一个不开放的、目标单一的缺陷调查流水线,不是开放式编码系统。」这一句话直接关闭了模型对「自由发挥」的想象空间:它不会去改源代码、不会去建议重构、不会去顺手优化无关函数,也不会在终端里炫技性地打印一长串 ASCII art。
[观察] 这种人格切换的副作用是:同一份模型(比如 Claude Sonnet 4.6)在裸跑 CLI 时倾向于给出长篇建议并主动修复,但进入 harness 后会退回到「只读取告警、只写分析、只产出工件」的角色。该示例实现早期曾用通用提示跑过同一个 run,模型直接尝试去修改 Sentry SDK 的源码,触发了 investigate-only 权限的拒绝,这次失败直接催生了硬编码步骤提示的设计。
显式声明的三重约束:工件为真相、按计划攻题、结构化返回
harness 不靠暗示,它把三条铁律写进每一段步骤级提示 ,而且每条都附带了「做不到会怎样」的负面后果说明,而不是一句温和的「建议」。
第一,以工件为真相源 。模型不允许把对话历史当作记忆,而是要求它从 artifacts/issue-xxx/ 目录下的 JSON、Markdown、trace 文件里读取前置步骤的产出,并把当前步骤的结果落盘到 artifacts/issue-xxx/root-cause.md 这样的约定路径。这条约束的工程意义是:即便 worker 进程崩溃、token 用尽、用户 Ctrl+C 中断,下次 run 也能从磁盘续上,而不会变成「上次分析到哪了我也不知道」。
第二,按给定计划攻击特定问题 。每个 run 在开始时都会有一份「调查计划」工件,通常是一段 Markdown 编号清单,逐条列出本次要验证的假设、要调用的工具、要排除的子域。模型被要求严格按编号推进 ,允许跳过(并说明理由),但不允许擅自增加「我顺便也查一下数据库慢查询」之类的并行任务。
第三,必须返回规定的 X/Y/Z 结构 。在最终汇报阶段,harness 的提示里硬编码了一段:「你的输出必须包含 X(症状摘要)、Y(根因判定)、Z(后续工件链接)三个段落,顺序不可调换,缺一不可。」这与通用工具常见的「请尽量结构化」相比,强了一个量级:模型不是「建议」结构化,而是被告知「不结构化就视为不返回」。
步骤级硬编码:不依赖人记得粘贴
把上述提示写进聊天框的顶部,听起来也可行。但 harness 工程化的关键一步是把这些定制提示编码进 harness 自己的代码路径 。该示例实现的入口脚本里,每调用一次 spawnWorker(),就会在发送消息前把当前步骤对应的系统提示拼接进去:
const systemPrompt = [
HARNESS_CONTEXT,
STEP_PROMPTS[currentStep],
ARTIFACT_PROTOCOL,
RETURN_STRUCTURE,
].join("\n\n");
await spawnWorker({ systemPrompt, ... });
STEP_PROMPTS 是一个由 harness 维护的映射表,key 是步骤名,value 是该步骤专属的提示块。这意味着:无论运行这个 harness 的人是工程师、产品经理,还是临时被拉来救火的实习生,只要他执行 bin/investigate <sentry-url>,模型拿到的就是同一份经过反复调优的提示——不依赖人记得粘贴、不依赖人记得删掉上一次的临时指令,也不依赖团队成员之间口口相传的「喂,你跑之前记得在 system prompt 里加一句……」。
[数据] 这套提示工程的落地速度本身就是一个数据点:该示例实现的构建者从动手到跑通第一版「粘贴一条 Sentry 链接即可启动」的流水线,现场花了不到 30 分钟。其中系统提示的拼接逻辑是一次写完、几乎没改,反而是工件目录的命名规范改了三次,Ink UI 的样式改了两次。这条数据说明了一个反直觉的事实:在 harness 里,「让人怎么写 prompt」通常不是瓶颈,「让人怎么组织工件」才是。
多步骤的提示分布:职责单一,互不污染
harness 内部不是只有一份「总纲」提示,而是把不同维度的约束拆到不同步骤的提示块里 ,避免一份超长提示互相污染。下面是职责拆分的示意表:
| 步骤名 | 提示块主要职责 | 关键约束 | 主要输出路径 |
|---|---|---|---|
plan |
拆解调查计划 | 必须输出编号清单,每条标注假设类型 | artifacts/.../plan.md |
collect |
拉取 Sentry / Linear / GitHub 工件 | 只读、必须落盘到 raw/ 子目录 |
artifacts/.../raw/ |
analyze |
交叉比对、生成根因 | 不许修改源文件,产物写 root-cause.md |
artifacts/.../root-cause.md |
summarize |
终端 + 工件双输出 | X/Y/Z 三段式,Markdown | artifacts/.../summary.md |
notify |
推送到 Linear 草稿 | 必须包含证据链接列表 | Linear API 调用 |
这种拆分让每一段提示都短到可以被人完整 review,改一处不会牵动另一处。Ink 终端 UI 部分(参考 Ink 终端 UI 库)只读取 summarize 步骤的产物,完全不知道 analyze 步骤提示里写了什么——这是「提示与 UI 解耦」的副产品,也是 Anthropic 在 Claude Agent SDK 文档里强调的「流程化、约束化」场景的典型写法。
与 skill 方式的对比:确定性触达 vs 概率性唤起
Anthropic 在 Claude Code 文档里把「skill」描述为可被模型按上下文主动调用的能力包。skill 的工作原理是模型自己判断「现在该用哪个 skill」 ,因此具有概率性:同一份输入,有时被唤起,有时被忽略,有时被错误唤起。
harness 的步骤级硬编码提示走的是另一条路:它不依赖模型「记得」去读哪份规范,而是在每一步都强制注入对应的提示块 。这两条路线的对比如下:
| 维度 | Skill 方式 | 步骤级硬编码提示 |
|---|---|---|
| 触达确定性 | 概率性,依赖模型识别 | 确定性,每次运行都注入 |
| 调优成本 | 改一次 skill 文档,多处生效 | 改一处步骤提示,只影响该步骤 |
| 调试可见性 | 模型是否调用,需要 trace 验证 | 直接在 system prompt 里可见 |
| 适用场景 | 开放性任务、探索性交互 | 流水线化、有 SLA 的工种 |
| 对 token 的占用 | 一次性铺底,长期摊销 | 每步骤注入,可裁剪 |
该示例实现选择硬编码路线的根本原因,是缺陷调查对确定性 的要求高于对灵活性 的要求:同一个 run 不应该因为模型「今天心情好」就给出更详细的根因,也不应该因为模型「没想起」该用哪个规范就跳过 X 段。Sentry、Linear、GitHub、Vercel 这些工具的官方 API 文档(分别见 Sentry 官方文档、Linear 官方文档、GitHub REST API 文档、Vercel 官方文档)虽然不直接讲 harness 设计,但它们返回的数据结构稳定性,是 harness 敢做「步骤级硬编码」的底气——如果上游 API 隔三差五改字段,那硬编码提示里的字段名就要跟着改,确定性优势就被抵消了。
实战中容易踩的几个坑
第一,不要把 prompt 写得过长 。一个步骤的提示超过 1500 token 后,模型对末尾约束的遵循度会肉眼可见地下降。该示例实现里 analyze 步骤的提示只有约 600 token,反而是 collect 步骤因为要列举每个适配器(Sentry / Linear / GitHub)的字段约束,接近 1200 token,后续被拆成了「通用收件规则 + 适配器专属附录」两段,效果立竿见影。
第二,不要把多条规则揉成同一段 。早期版本里把「工件为真相」和「X/Y/Z 结构」写进同一段,模型在 follow-up 回合里只记得最后一条,X 段经常被吞掉。拆成两段、并在每段前加粗一个关键词后,问题彻底消失。
第三,不要忽略 negative instruction 。只告诉模型「你应该做什么」不够,还要明确「你不应该做什么」,比如「不要修改源文件」「不要建议重构」「不要在 X 段写代码」「不要在 collect 阶段调用 notify 步骤的工具」。harness 的 analyze 提示里专门有一段「禁止行为清单」,这是从多次失败 run 里反向归纳出来的。
第四,别让提示里出现「请」字 。听起来奇怪,但实测发现,把「请按 X 顺序返回」改成「按 X 顺序返回,不可调换」后,模型的遵循率明显更高。harness 不需要客气,harness 需要确定性。
小结:提示工程是 harness 的隐性 API
通用工具的提示塑造人格,harness 的提示塑造流程 。把约束从「建议」升级为「硬编码注入每一步」,是 harness 把 AI 从「聊天伙伴」转成「流水线工人」的关键设计动作。配合工件存储、窄适配器、investigate-only 权限规则,这层定制提示让 harness 拥有了一个确定性的、可被团队反复 review 的「隐性 API」——它不写在 OpenAPI 里,但比 OpenAPI 更严格地约束着模型每次的行为边界,也是 harness 工程化区别于「套壳聊天」的最显著标记之一。
Opinionated 适配器:比泛用 MCP 更窄、更准的工具面
从「泛用 MCP」到「立场化适配器」的取舍
MCP(Model Context Protocol,模型上下文协议)的本意,是给模型一份「工具说明书」:一份 JSON Schema 加上若干方法名,让 Agent 自己判断该调哪个、参数怎么填。对于一个开放的、什么都能做的编程助手来说,这是合理的默认——你不知道用户下一秒会问 Python 装饰器还是让你重构整个 monorepo,把选择权交给模型,是灵活性最大化的做法。
但 harness 的目标正好相反。它的 job to be done 是高度收敛的:「拿到一条 Sentry 告警链接,经过调查,输出根因报告与可执行的下一步」。在这种场景下,把通用 MCP 原样接进来,等于把一整套数百个工具方法摆到模型面前,让它自己挑。模型当然能挑,但它会在 trace 海洋里游荡——这是该示例实现早期版本反复观察到的现象。
[观察] 早期版本里,只接 Sentry MCP 让 Agent 自由探索,跑一次调查往往要消耗上万个 token,而且半数调用是无关的 issue 列表翻页、breadcrumb 字段展开,真正能用于根因推理的预算反而被压缩。
因此,该示例实现的构建者选了一条相反的路:为每一个外部系统单独写一个立场鲜明的适配器(opinionated adapter)——它不是一个透明代理,而是一份对「在找 bug 这个场景下你应该怎么用 Sentry」的强主张。
Sentry 适配器:在 trace 海洋里划出航线
Sentry 一个 issue 对象,API 返回的字段可以轻易超过 80 个:id、project、title、culprit、firstSeen、lastSeen、level、platform、metadata、tags、context、entries(即 breadcrumb 与 stack trace)、user、fingerprints、activity、各种 release 信息、各种 stats。
如果让 Agent 自己挑,它会先把这些全读完,再判断哪个有用——这是典型的「模型花 token 重新做一遍本该由人类做的预处理」。
立场化的 Sentry 适配器不是这样。它在拉取阶段就把字段做了硬过滤,大致是这样一份优先级表:
| 优先级 | 字段 | 为什么拉 |
|---|---|---|
| 必拉 | entries.exception.values[*].stacktrace.frames |
直接指向崩溃栈 |
| 必拉 | entries.exception.values[*].type 与 value |
异常类型与消息 |
| 必拉 | tags.release, tags.environment |
复现环境上下文 |
| 必拉 | lastSeen - firstSeen 间隔 |
判断新引入还是历史遗留 |
| 选拉 | 最近 5 条 activity |
看是否有人为标记或指派 |
| 选拉 | context.runtime, context.os |
运行环境 |
| 不拉 | user 全字段 |
只取 IP 归属地判断地理分布 |
| 不拉 | 完整 breadcrumb 流 | 只保留 error 前后各 5 条 |
[观察] 在该示例实现里,Sentry 适配器把单次 issue 拉取压到了 3 次以内的 HTTP 调用,且每次响应都被裁剪到模型上下文能直接装下的尺寸,根本不需要再做摘要。
这种适配器的设计原则只有一条:让模型只看到与「这次 bug 是什么」相关的字段 。URL 链接作为输入参数,适配器内部完成字段裁剪、栈折叠、breadcrumb 截断、错误指纹归类,然后以一个紧凑 JSON 抛给 Agent 的推理循环。Sentry 官方文档 docs.sentry.io 暴露的字段全集,远大于这个最小集;但适配器做了减法,这正是它的「立场」。
一个最小可用的 Sentry 适配器签名,大致会是这样:
type SentryIssueDigest = {
fingerprint: string;
title: string;
exceptionType: string;
exceptionMessage: string;
topFrames: StackFrame[];
release: string;
environment: string;
windowBreadcrumbs: Breadcrumb[];
recentActivity: ActivityItem[];
};
interface SentryAdapter {
digestFromUrl(url: string): Promise<SentryIssueDigest>;
similarIssues(digest: SentryIssueDigest, limit = 3): Promise<SentryIssueDigest[]>;
}
注意 digestFromUrl 已经把「URL 解析 → 组织/项目/issue ID 拆分 → 多次 API 调用 → 字段裁剪 → 栈折叠」全部封装在内。Agent 看到的只是一个稳定的输入输出契约,而不是 Sentry REST API 的全貌。
Linear / GitHub / Vercel:场景化而非通用化
同样的思路应用到 Linear、GitHub、Vercel。
Linear 适配器 的立场是:一个 bug 的「相邻上下文」是什么?它会拉取该项目下最近若干条状态为「In Progress / In Review / Recently Closed」的 issue,把标题、负责人、最近一次 PR 引用拼成一个邻接关系图。这样,当 Agent 看到「这个崩溃发生在某次 webhook 路径改造之后」时,它能马上在 Linear 里追问「这次改造的前置任务是什么、谁负责、关联了哪些 PR」,而不是把整个项目空间翻一遍。Linear 文档 linear.app/docs 列出的 GraphQL 字段很多,适配器只挑其中四五类——issue 主体、状态机、PR 关联、最近变更人。
GitHub 适配器 则把自己定位成「PR 维度的 code archaeology」(代码考古):给定一个可疑文件路径与一个可疑版本区间,它会调 /repos/{owner}/{repo}/commits 拿到所有触及该文件的提交,再用 /repos/{owner}/{repo}/pulls 过滤出已合并的 PR,最后只把 commit message 与修改函数列表返回,而不是完整 diff。GitHub REST API 文档 docs.github.com/en/rest 暴露的 endpoint 远不止这三条,但 harness 立场化之后只走窄路径。适配器签名大致是:
interface GitHubAdapter {
touchesOf(file: string, since: string, until: string): Promise<CommitDigest[]>;
prsForCommit(sha: string): Promise<PrDigest[]>;
blameSnippet(file: string, line: number, depth = 5): Promise<BlameHunk>;
}
Vercel 适配器 的立场是「部署维度的环境还原」:给出一个 commit SHA,它会拉最近几次 production 部署的 status、运行时长、错误率曲线,与 Sentry 适配器给出的崩溃时间做时间对齐,判断崩溃是不是跟某次部署同时发生。Vercel 文档 vercel.com/docs 描述的 API 能力覆盖部署、域名、Edge Functions、环境变量等多个层面,适配器只挑时间对齐相关的两三个。
这四个适配器合起来,形成了一个闭合调查回路:Sentry 告诉你「出了什么」,GitHub 告诉你「谁在什么时间改了什么」,Linear 告诉你「这与哪些待办相邻」,Vercel 告诉你「是不是部署引入」 。每一步都只输出下一步真正需要的字段,而不是把原始 API 响应原样转给模型。
Token 花在刀刃上,跑偏概率显著下降
[数据] 把这一组立场化适配器接进 Claude Agent SDK 的 harness 后,该示例实现观察到:平均一次完整调查(从粘贴链接到产出工件)消耗的 token 数下降明显,而更关键的变化是 Agent 发出的「与根因无关」的工具调用次数几乎归零——比如不再出现翻 30 页 issue 列表、为一条 breadcrumb 反复重读完整 JSON 这类动作。
这条规律可以写成一句工程格言:工具面越窄,行为越可预测 。
模型在面对 500 个方法时,需要先做「我该不该调、调哪个」的元决策;而面对 5 个被裁剪过的、结构稳定的适配器入口时,它直接进入「用已知结构推理」的快路径。前者消耗的是注意力,后者消耗的是逻辑。harness 的核心投入,就是把前者提前做成后者。
[观察] 通用 MCP 在该示例实现里并非被抛弃,而是被收编进 harness 的「外围层」:由 Claude Agent SDK 文档 code.claude.com/docs/en/age… 描述的工具注册机制统一承载,允许在某些探索性场景(比如对全新系统做调查时)临时挂上泛用 MCP。但默认 job 流程里,走的是立场化适配器这条窄路。
这其实是 MCP 文档 modelcontextprotocol.io 自己也承认的:协议是传输层,不是策略层 。把策略从传输层抽离出来单独投资,正是 harness 工程的精髓。
适配器立场化是 harness 工程的核心投入点
一个常见的误解是,harness 工程就是「写好 prompt」。实际上,prompt 只是 harness 最表层的人格面具;真正决定 harness 好不好用的,是工具面 ——也就是模型能看到、能调用的方法集合,以及每个方法被设计成什么样的输入输出契约。
立场化适配器的工作量,往往大于 prompt 本身。在该示例实现的代码里,光四个适配器就占用了相当比例的工程时间:每一个都要决定「拉什么、不拉什么、怎么折叠、怎么裁剪、怎么与上下游对齐」。这不是简单的 SDK 调用,而是领域知识的代码化 ——把「调查一个 Sentry bug 该看哪些字段」这种经验,显式编码进工具签名。
这也意味着,适配器一旦写好,它的复用价值极高。同一个 Sentry 适配器,可以被缺陷分诊 harness 用、可以被 on-call 巡检 harness 用、可以被发布前 smoke test harness 用。harness 的可组合性,正是建立在这些窄而稳的适配器之上。换句话说,适配器是 harness 的资产 ,prompt 是 harness 的配置 ;配置可以频繁改,资产要谨慎沉淀。
落地时容易踩的坑
把适配器立场化的过程中,有几个反复出现的反模式值得记录:
- 把适配器做成「通用字段透传」 。表面上是适配器,实际只是把 API 响应 JSON 原样返回,等于一个薄薄的 RPC 包装,等于没做适配。判断标准很简单:如果模型看到的结构跟官方文档几乎一致,那就是透传,不是适配。
- 过度裁剪导致关键上下文丢失 。比如 Sentry 适配器如果不带 release tag,Agent 就无法关联到具体部署,根因推理立刻缺一条腿。立场化的难点不是「砍」,是「知道哪些不能砍」。
- 每个适配器用不同的数据形状 。四个适配器返回的字段命名、时间格式、ID 类型应当统一,否则下游 Agent 还要做一层 schema 对齐,把立场化省下来的 token 又花回去。
- 忘记把适配器失败做成可观察信号 。网络超时、权限不足、字段不存在、token 过期,这些必须以结构化错误抛出,而不是吞掉返回 null。Agent 在调查过程中需要明确的失败信号,才能决定是重试、绕过还是升级到人类。
- 让 Agent 自己决定要不要调适配器 。一旦把调用决策也外包给模型,立场化就白做了。harness 应该把调用顺序硬编码在 run / task 编排里,模型只能在每一步内部决定「如何用这个适配器的输出」,而不是「要不要绕过去」。
每一项反模式背后,都是一个「我们以为在写适配器,实际在写更复杂的通用工具」的故事。立场化的纪律,是 harness 工程区别于「接 MCP 就完事」的关键。
小结
立场化适配器的核心价值,不是把 MCP 干掉,而是在 MCP 之上叠加一层领域判断 。MCP 是传输协议,适配器是业务策略;协议越通用越好,策略越窄越值钱。当工具面被压缩到只剩「为这次 job 而生的入口」时,模型就不再需要在数百个方法里漫游——它拿到的是一条窄而清晰的航线,跑偏的概率自然下降。这正是 harness 工程投入产出比最高的环节,也直接定义了 harness 与「通用 AI 编码助手」之间的人格分野。
用 Codex 与 Claude 对偶构建:一次真实的元工程实验
用 Codex 与 Claude 对偶构建:一次真实的元工程实验
构建 harness 这件事本身,就是一次典型的 AI 工程实践。当「立场化适配器」的设计哲学确定之后,该示例实现的构建者没有立刻动手写第一行代码,而是把这个决策过程本身也当成一次实验来跑。
具体做法是同时打开 Claude Code(参见 docs.anthropic.com/en/docs/cla… OpenAI Codex(参见 openai.com/index/intro… harness」的目标描述分别丢给两边,让它们各自从零实现一份能跑通 Sentry 告警调查的 harness。
这种「对偶构建」(dual build)的真正目的不在于比谁写得更好,而在于通过两条彼此独立的实现路径,看清哪些设计选择是被 job to be done 逼出来的必然结果,哪些只是某个模型长久以来形成的肌肉记忆。
实验的一开始远没有想象中顺利。两个会话都不愿意按照预想,在 harness 的主干里真正放入一个 AI Agent 推理环节。GPT-5.5 与 Claude Opus 都强烈倾向于把整个系统写成一个纯确定性的状态机:解析 Sentry 告警链接、调用 REST API 拉事件详情、抓取堆栈和相关上下文、跑一段固定规则的相关性分析脚本、最后写出一份结构化 JSON 报告。
它们一次又一次地回退到「我能不能不用模型也把这个跑通」的简化方案。这并不是因为模型「笨」,而是因为它们对「什么是好工程」的隐含假设本身就偏向稳定、可测试、可解释、可静态分析——而 AI 推理环节天然带有概率性、不可完全复现,在它们的「工程审美」里自然会被优先优化掉。
这是构建 AI harness 时一个普遍但容易被低估的阻力来源:你以为自己请的是「工程师」,其实请的是「有强烈工程审美的工程师」,而那份审美在大多数场景下都倾向于把不确定性赶出系统。
要让两个会话真正把 Agent 留在 harness 的主干里,必须把指令写到非常具体的颗粒度,几乎是逐字段钉死的程度。不是笼统地说「做一个会做根因分析的 AI」,而是要写明下面这种级别的硬约束:「在证据收集阶段结束之后、根因分析阶段开始之前,必须插入一次 model.run 调用;输入固定为 evidence.json 这个工件,输出必须落到 root_cause.draft 这个工件路径;这一步必须允许工具调用,但禁止任何对源仓库的写操作」。
换句话说,对工作流的阶段切分、工具绑定的位置、定制提示词(prompt)的注入点都要逐条钉死。否则模型会立刻按自己的偏好重新画边界、重新定义阶段、重新安排工具——这正是把 harness 工程化最容易踩的坑之一:你以为自己在描述需求,模型听到的却是「自由发挥的邀请」。
为了把这种「具体度」做到可复用,值得在 harness 设计初期就固定一套表达模板,把每个阶段的输入、输出、可用工具、可写权限四项以表格形式钉下来,任何模型、任何编码工具在填实现之前必须先填这张表:
| 阶段 | 输入工件 | 输出工件 | 允许工具 | 允许写操作 |
|---|---|---|---|---|
| 证据收集 | alert.url | evidence.json | Sentry / Linear / GitHub 读 | 仅工件目录 |
| 根因分析 | evidence.json | root_cause.draft | model.run | 仅工件目录 |
| 后续跟进 | root_cause.draft | follow_ups.json | GitHub Issues / Linear 创建 | 仅工件目录 |
表本身不复杂,但它的价值在于强迫模型在写代码之前先承认「这里有一个 AI 环节,它的边界就是这张表」。少了这一步,模型就会本能地把 AI 环节「内联」进某个确定性函数里,等代码 review 时才发现根因分析其实根本没经过模型。
[观察]两个会话最终都接受了「在 harness 中保留一个 AI 推理环节」的设定,但它们的反应路径完全不同。Claude 一侧把 Agent 收得很紧,试图用一个高度受控的 prompt 模板把整段调查过程封装成确定性更强的调用,倾向于把模型视为「带输入输出的纯函数」;Codex 一侧则更愿意在 harness 里塞进多个可分支的 AI 步骤,接受更多的不确定性和多轮工具调用,甚至会在证据不足时主动决定回退到上一步重抓证据。
这种差异本身就回答了「为什么要做对偶构建」——不是因为某一边更聪明,而是因为每一边都有自己的形状,你必须亲眼看到两种形状,才能在选型时刻意选一个,而不是默认掉进某一个里。
[数据]在该示例实现的实际工作流中,一次典型的调查运行只触发一次 I(Investigation,只调查不修复)动作,全程不触碰任何源文件;现场不到 30 分钟就构建出可运行的第一版 harness,主体由大约 8 个职责单一的功能文件组成,每个文件对应流水线的一个阶段(证据收集、根因分析、工件存储、权限校验、TUI 渲染、Sentry/Linear/GitHub 适配器等),每个文件的修改都不会跨阶段污染,review 时可以做到逐 PR 独立合并。
实验中最值得玩味的交叉结果是:Codex 那份最终被采纳度最高的实现,在 Agent 推理环节反而选用了 Claude Agent SDK(参见 code.claude.com/docs/en/age… OpenAI 系的编码 Agent,在写一个 harness 的时候,主动把推理环节外包给了一个 Anthropic 系的 SDK。
理由非常工程化:Claude Agent SDK 在「带工具调用的多轮会话 + 受控权限边界 + 可注入 harness 级 system prompt」这一组合上,刚好和该示例 harness 的需求咬合得最紧。相对地,Codex 自家更擅长的是一次性代码生成,把同样的能力强行堆到一个长上下文、多工具调用、需要「自我约束」的 harness 推理环节里,反而会出现工具调用风格不一致、权限校验不严等问题。
这是「选型不站队」最干净的一次现身:不是出于偏好,不是出于站队,是出于「这一段代码谁写得更像该段代码该有的样子」。
把视野再抬高一档,这其实不是孤例。MCP 的整套生态(Model Context Protocol,见 modelcontextprotocol.io)从一开始就把「跨厂商拼装」当作前提:工具说明就是一份 JSON Schema,谁实现、谁消费、谁路由,完全可以来自不同团队,不需要任何一方垄断。
Ink 这类终端 UI 库(见 github.com/vadimdemede… stdout 的 Agent 都能被套上一层漂亮的 TUI。Sentry、Linear、GitHub、Vercel 的官方 API 文档(分别见 docs.sentry.io、https://linear.app… harness、任何 Agent、任何脚本都可以平等接入。
把这些拼起来,harness 的最佳实践其实就是一次优雅的「多模态路由」:编码环节用 Codex 的生成速度,推理环节用 Claude 的工具协同,展示环节用 Ink 的 React 风格组件,数据接入用各家自己的 REST API,中间用 MCP 把工具说明书统一掉。
对 AI 工程师与团队负责人来说,这条经验比任何具体技术选型都更重要:不要把工程效率押在某一个工具的「全栈能力」上,要承认每家模型、每个 SDK 在不同子任务上有不同的形状 。
harness 本身就是一个组合件——它把「编码能力」「推理能力」「工具协同」「人类可读输出」「外部系统接口」拆成至少五个独立槽位,每个槽位都让当前最强的那个组件来填。这才是「立场化适配器」思路在工具链层面的延伸:不追求通用最强,只追求在 job to be done 上每一段都刚好合适。
跨模型、跨编码工具的协作不是需要特殊理由的稀有场景,而是 AI 工程以后最常见的常态——把这件事写进团队的工具箱共识里,会比任何一次单点优化都更长久地影响交付节奏。
代码地图:一个 Harness 其实只有一小撮文件
全景速览:比你想象中更薄的一层代码
当很多人听到「给 AI 写一个 harness」时,脑海里浮现的画面通常是一整座工程:长篇累牍的系统设计、几十个模块互相调用、几层抽象层层包裹。但该示例实现的真相是——一个 Harness 其实只有一小撮文件,整体规模远小于任何人的想象。
如果把仓库想象成一棵倒挂的树,那么最顶端的入口只有一个,负责把请求分发给下面的 TUI/CLI(终端交互界面/命令行界面)。这个入口之下,横向铺开大约 8 个职责单一的功能文件,每一个文件只回答一个具体问题:这次运行从哪个外部服务取数据?如何汇总一份证据?工件(artifact)应该被写到哪里?整个项目的目录深度几乎不超过两层。
[数据] 该示例实现的代码地图合计约 8 个核心功能文件 + 1 个高层入口,外加若干 README、.env.example 之类的样板文件,核心 TS/JS 源文件体量远低于一般的中型 Node 项目。这种「小代码量」的视觉冲击力,是理解 harness 这件事的起点。
这种「刻意做薄」的结构并不是偶然,而是一种态度宣言:harness 的目标不是替代业务系统,而是把一次具体 job 的判断路径以最直白的方式固定下来。任何新读者打开仓库,只要顺着入口往下数,三十秒内就能数完所有文件。
文件清单:8 个文件如何分配职责
为了让读者建立直观印象,下面把核心文件清单以表格形式列出。该清单基于这套实践里构建者展示过的结构化梳理,逐项对应 Claude Agent SDK(参见 code.claude.com/docs/en/age… run/task 抽象。
| 文件名 | 职责 | 关键依赖 |
|---|---|---|
index.ts |
高层入口,解析 argv,挂上 Ink 渲染器 | Ink、Claude Agent SDK |
tui.tsx |
TUI 主界面、按钮、状态切换 | Ink |
cli.ts |
命令行模式,纯文本输出 | Claude Agent SDK |
hunt-bug.ts |
Sentry 猎 bug 完整工作流 | Sentry、Linear 适配器 |
adapters/sentry.ts |
Sentry 适配器,封装 issue 取数 | Sentry API |
adapters/linear.ts |
Linear 适配器,创建/查询 issue | Linear API |
adapters/github.ts |
GitHub 适配器,搜 commit/PR | GitHub REST API |
adapters/vercel.ts |
Vercel 适配器,取部署日志 | Vercel API |
artifacts.ts |
工件输出,把 Markdown 报告落盘 | fs |
config.ts |
读 .env,校验 key 是否齐全 | dotenv |
每个文件都遵循单一职责:适配器只负责把外部 API 包成一个函数,Sentry 工作流文件只负责编排流程,工件文件只负责写文件,入口只负责引导和挂载 UI。这种「一层做一件事」的分层,是 harness 与传统重型框架最大的视觉差别。
猎 Bug 工作流文件:把「判断」写成显式步骤
在 8 个功能文件里,最值得细看的是 hunt-bug.ts,它把「我们如何猎 bug、如何汇总报告」的全过程写成了显式步骤。这个文件不是一个黑盒,而是一份可读的剧本。
具体来说,文件里通常会有一段类似下面的伪代码:
async function huntBug(alertUrl: string) {
const alert = await sentry.getIssue(alertUrl);
const repro = await sentry.searchSimilarEvents(alert);
const context = await github.findRelatedPR(repro.commit);
const deployment = await vercel.getDeployment(repro.release);
const ticket = await linear.createInvestigationTicket({...});
const report = await writeMarkdownArtifact({alert, repro, context, deployment, ticket});
return report;
}
每一步都对应一个适配器调用,没有任何魔法。harness 之所以这样写,是为了让任何打开这个文件的人都能在两分钟内看清一次完整调查的形状:从 Sentry 告警 → 相似事件 → 相关 PR → 部署上下文 → Linear 调查单 → 工件落盘。
[观察] 这套实践里,调查运行的标志位始终是 I(investigate-only,只调查不修复),而不是 F(fix),也就是说 harness 全程不触碰源文件、不会发起 PR、不会改业务代码。它把 AI 框定在「取证 + 汇总」的位置上,把修复动作留给工程师。这种「越界自律」正是 harness 价值密度最高的地方——它没有无限扩张能力,而是刻意限制能力,把剩余价值留给人类判断。
四只适配器:立场鲜明的薄壳
四个外部服务适配器——Sentry、Linear、GitHub、Vercel——各自只有几十行代码,做的事情却非常一致:把目标服务的真实 API 字段映射到 harness 自己定义的 Evidence(证据)类型。
以 Sentry 为例,适配器只暴露 getIssue、searchSimilarEvents、getLatestRelease 三个方法,内部把 issue.id、issue.title、event.tags 这些字段统一抽成 Evidence。这种映射是有立场的:它主动丢弃了某些 Sentry 字段(如内部管理字段、订阅者列表),只保留「和缺陷调查相关」的那部分。
[观察] 这套实践的立场是:适配器不是 SDK 的搬运工,而是「这份 harness 想要的」外部世界视图。换句话说,当 Sentry 升级了一个字段,适配器不一定立刻跟进;当 Linear 改了 issue 结构,适配器也只会更新自己关心的字段。这种「窄而立场鲜明」的姿态,正是 opinionated adapter(立场鲜明的适配器)的字面含义。
四个适配器的实际参考链接在仓库的 README 里直接给出,例如 Sentry 官方文档(docs.sentry.io)、Linear 官方文档(linear.app/docs)、GitHu… REST API 文档(docs.github.com/en/rest)与 Vercel 官方文档(vercel.com/docs)。读者不需要… README 就能顺着这些地址把外部语义弄清,这种「文档自包含」也是 harness 低门槛的一部分。
配置:README 即文档,克隆即跑
如果说代码地图的前半段是「薄」,那么后半段就是「轻」。该示例实现的配置文件只有两类:一个 .env.example,一份 README。.env.example 列出所有需要的 API key(Claude、Linear、GitHub、Vercel、Sentry 各一个),README 用大约半页篇幅说明「克隆 → 填 key → 运行」的流程。
这种设计是刻意压低门槛的结果。传统工程项目在交付时通常会伴随一套部署文档、环境检查清单、权限申请工单;而这套 harness 的 README 写得像一份 checklist:复制粘贴 → 替换占位符 → 执行 npm run hunt <sentry-url> 即可。如果读者连这一步都不想做,直接看 README 里的样例运行截图也能理解全貌。
[观察] 当构建者把这份 README 拿给团队评审时,反馈是出奇的一致:门槛越低,采纳率越高。一个需要配 Kubernetes、配 Vault、申请 Service Account 的 harness,大概率只会被作者自己使用;而一个「克隆即跑」的 harness,才有概率在 Slack 群里被转走。这种设计选择和 harness 的体量一脉相承——它不是基础设施,而是工具。
工件输出:调查结束留痕
工件(artifact)输出文件 artifacts.ts 同样很薄,职责只有一个:把一次调查的所有证据按照 Markdown 模板渲染,并写到本地 runs/<timestamp>/report.md。模板里通常包含标题、影响范围、根因猜测、相关 PR、相关部署、Linear 调查单链接、Sentry 原始链接,以及一段「下一步建议」。
工件的存在意义在于:让 harness 的输出有形。一次调查运行结束,工程师拿到的不只是一段控制台文本,而是一份可以贴到工单、可以转发、可以归档的 Markdown 文件。当 [数据] 显示某条 Sentry 告警影响约 150 名用户、仍在每小时发生时,这份 Markdown 就具备了「贴在事故复盘文档里直接引用」的可用性。
工件落盘同时也让 harness 可以脱离 TUI 运行。CLI 模式(cli.ts)下,整个交互界面被替换为一段纯文本报告,但工件文件依然会写出来。这种「有 UI 是常态,无 UI 也照样工作」的设计,让 harness 在 CI、cron、本地终端之间无缝切换。
价值密度:不是代码行数,而是领域判断
回头看,这个 harness 大约只用了 8 个文件。但它真正承载的东西并不是这 8 个文件本身,而是里面沉淀下来的领域判断:什么时候该把证据抓全,什么时候该停止抓证据;该往 Linear 里写哪种类型的工单,不该写哪些字段;哪些 Sentry 字段和缺陷调查相关,哪些是噪音。
[数据] 如果把这套 harness 摊开算,真正「和 AI 决策相关」的代码行数大约只占 30%——剩下 70% 都是确定性的编排、文件 IO、API 包装。这意味着,harness 的代码行数和它的判断密度之间不存在线性关系;真正决定 harness 价值的,是那些 AI 在循环里必须遵守的规则,而规则往往用十几行 if-else 就能写完。
这其实呼应了 harness 设计的元命题:在 AI 越来越擅长写代码的今天,工程师的核心交付物正在从「代码」迁移到「约束」——约束 AI 在哪一步该调哪个工具、约束它在哪种情况下不该继续深入、约束它的输出必须落成可审计的工件。这套约束加在一起的密度,就是 harness 的真正体量。仓库看起来只有一小撮文件,但每一撮都承载着一次具体的 job to be done,这份「薄而密」,正是该示例实现最值得借鉴的地方。
实战运行:一次真实 Sentry 调查的完整产出
触发一次真实调查:从告警到报告
这天上午,值班工程师在 Sentry 的 inbox 里看到一条持续发酵的告警,标题简短但刺眼:editor mutation dropped——编辑操作被丢弃。它不像 error 那样震耳欲聋,而是安静地以 warning 级别每小时吐一条新事件,意味着用户正在反复触发它,但没有人因此崩溃。值班工程师在 TUI(终端交互界面)里粘贴进 Sentry 事件链接,按回车, harness 立刻拉起一次 run。屏幕顶部弹出两行选择:I nvestigate 或 F ix。工程师选 I(调查),刻意不选 F(修复)。
[观察] 选 I 还是 F,看似只是一个交互细节,实际上是 harness 设计哲学最直观的体现:在证据备齐之前,任何写盘动作都被视作越权。这种「分诊先行、修复后置」的姿态,正是 opinionated adapter(立场鲜明的适配器)对工具语义的最大贡献——它把 Clinical 的「先诊断再开方」翻译成了机器可消费的命令。
调查报告的字段解读
不到两分钟,终端里渲染出一份结构化报告。决策者最先看的是「真实性」和「范围」两栏:报告显式确认该告警确实存在,影响约 150 名用户,且仍在以每小时一次的频率新增事件——这意味着这不是一个历史遗留问题,而是一个正在漏血的活问题。级别被标注为 warning 而非 error,这一点很关键,因为它直接决定了后续响应的优先级。
| 字段 | 报告结论 | 工程含义 |
|---|---|---|
| 真实性 | 告警已核实 | 排除误报、采样噪声 |
| 影响范围 | ~150 用户 | 需 PM 介入而非单人私了 |
| 频率 | 约 1 次/小时 | 仍在 reach,不是回归峰值 |
| 级别 | warning | 不阻塞 SLO,但损害功能 |
| 建议动作 | 建 Linear 工单 | 升级到产品/工程协作面 |
[数据] 影响 150 名用户、还在每小时发生——这两个数字组合在一起,直接让该 issue 越过 on-call 工程师的「自助」权限,进入需要跨职能协同的区间。
根因假设的优先级排序
报告最核心的章节是「根因假设」,它不是一段散文,而是一份按优先级降序排列的候选清单。第一嫌疑是 invalid original range,也就是 ProseMirror(一种富文本编辑器框架)在其协作协议中检测到了客户端发来的 range 越界或空值;第二嫌疑是 overlapping original range,即两个并发的 range 重叠导致只有一条步进被采纳。这两条假设覆盖了 80% 以上的同类历史工单,几乎已经成为该函数的「嫌疑地图」。
报告还额外指出该函数的一个盲区:它对 step 数组的批量提交做了原子的整体接受/拒绝,但缺乏对中间步进 的细粒度溯源。这意味着当一批 step 被拒时,日志只告诉你「这一批没落库」,却没说「是第几条 step 出了问题」。这正是 harness 不同于普通 LLM 调用脚本的地方:它不会停在「猜测」上,而是把盲区坦白写出来,留给人类工程师做最后判断。
验证路径与产品面定位
仅有假设还不够,报告必须给出可执行的验证路径。第一步是让值班工程师用 Sentry 官方的事件导出接口(参见 Sentry 官方文档 docs.sentry.io)拉取原始 payload,人工核对 originalRange 字段的取值;第二步是去产品面找锚点——把受影响 150 用户的设备、入口、客户端版本聚类一遍,看看集中在哪个发布渠道或哪个编辑动作上。报告并没有让 harness 自己去做这两步,因为它们都涉及敏感用户数据和主观的产品判断,超越了一份「编辑器层缺陷分诊」应该承担的职责。
验证路径之后,报告给出了一条明确建议:创建一张 Linear 工单并指派给相关 owner 。这一步看似简单,实则完成了关键的状态转移:从「harness 内部证据」变成「组织协作 system of record(记账系统)」。Linear(官方文档 linear.app/docs)会把工单编号… Sentry 事件评论里,反向形成证据闭环。
自我克制的 Agent:知道不知道,比知道更重要
这份报告最值得反复咀嚼的一句话,藏在末尾的「下一步建议」里:当前信息不足以开启 patch 模式修复,建议人工介入后再次触发 run 。它没有越界,没有自作主张地 generate diff(生成代码差异),也没有把建议发到 Slack 频道刷一波存在感。受约束的 Agent 主动承认自己的边界,这正是 harness 想要的行为。
[观察] 这种「克制的智能」并不是模型本身自动涌现的,而是 harness 通过 prompt、flag、权限规则三件套正向塑造出来的。模型在底层当然有能力继续往下写代码,但当系统明确告诉它「patch 模式未开启,本 run 不允许写文件」时,它会把表达收敛到「还需要什么信息」上面。
与传统 on-call 流程的对照
如果把这次 run 放进传统的 on-call 流程里做对照,你会发现 harness 并没有抢走任何人的工作——它只是把原本散落在多个 Tab、多个仪表盘、多个工单字段之间的上下文,一次性压成了一份可被人阅读的报告。值班工程师不需要打开 Sentry 找原始事件,不需要打开 GitHub 翻最近的相关 commit,也不需要打开 Linear 手动起草工单描述。harness 让人类专注于「判断」,把「检索/汇总/格式化」这种确定性苦活全部吞掉。
| 维度 | 传统流程 | harness 辅助 |
|---|---|---|
| 告警核实 | 人工打开 Sentry | run 内部自动校验 |
| 用户影响面 | 估算或粗略 | 来自 Sentry 查询 |
| 根因假设 | 工程师脑内快推 | 假设清单 + 优先级 |
| 修复动作 | 直接动手改 | 暂不生成,需人工仲裁 |
| 状态转移 | 凭记忆 | 自动写回 Linear |
实战踩坑清单:运行这类 run 时的几个真实坑
即便工具再顺手,工程现场依然有几个反复出现的小坑值得提前留意。
- Sentry 链接粘贴时不要带 query string 。多余参数会改变事件上下文,导致 harness 拉错版本。
- I 还是 F 的选择必须当场决定 。如果临时改主意,需要新建一次 run,而不是在已有 run 内切换,否则 context 会污染。
- Linear 工单 owner 不要默认填自己 。该函数模块很可能不归你管,正确做法是让 harness 推断模块 owner,人工确认。
- warning 级别告警不要忽略 。它不在 PagerDuty 噪音里,但对功能完整性的损耗是日积月累的。
写在最后
这一节的目的不是演示 harness「能做什么」,而是展示它「会主动不做什么」。当 Claude Agent SDK 文档(code.claude.com/docs/en/age… tool use 能力配上立场鲜明的 Sentry / Linear / GitHub 适配器,真正棘手的从来不是让模型去读写文件,而是给它装上一副稳定的缰绳。这次调查 run 全程没有触碰任何源文件,却把一份能直接进入团队会议讨论的交付物摆在了值班工程师面前——这或许就是 opinionated adapter 留下的最有价值的遗产:它让 AI 学会在正确的位置停下来。
决策矩阵:哪些工作该进 Harness,哪些不该
为什么需要决策矩阵:不是所有工作都值得包一层 harness
该示例实现里的 bug triage harness 解决的是"值班工程师贴一条 Sentry 链接,自动产出可贴在 Linear 评论里的根因报告"这一窄场景。这套实践的构建者坦言,在它之前尝试过给"任何一次 Sentry 告警"都包一层 harness,结果发现 harness 的设计成本、维护成本、跑偏后的纠正成本,会把原本一两段 prompt 就能解决的事拖成几周的工程。
[观察] 那次试错后来被固化成一条口头规则:harness 是放大器,不是启动器 。一个 job 如果本身就没跑通过三次以上,不要急着给它做 harness——你还没看到这条工作流的真实形状。
工程上更稳妥的做法,是先用一个不带 harness 的通用 Agent 跑十遍,记录 prompt、工具调用、失败重试、人工补刀的环节。等形状稳定了,再决定是否值得投入 harness 这层外壳。
四个打分维度:高频、流程、验收、失败
判断一个 job 是否值得建 harness,可以套用四个维度的打分矩阵。每个维度 0-3 分,总分 ≥ 8 才进入候选名单。
维度一:高频重复 。这条工作是否每周至少出现两次、且每次都需要人盯?缺陷分诊、PR 描述生成、Sentry 告警根因初判,都属于这一类。反过来,一个季度才发生一次的事件,不值得为它专门搭架子。
维度二:流程稳定 。从输入到输出,中间环节是否已被团队默会?如果还要边做边想"下一步到底该调哪个 API",流程还在漂移,harness 也会跟着漂移。
维度三:验收物明确 。产出能否用一张检查表机械判定?比如"根因报告必须包含三段:触发条件、影响面、建议动作"。验收物越能被结构化字段描述,harness 越容易把活儿做到位。
维度四:失败可控 。最坏情况下,harness 跑偏了能否被及时拦下来?一次失误若会污染生产数据库、推送错误告警给客户、覆盖线上文件,即便总分再高也要慎做。
[数据] 该 Sentry 告警(editor mutation dropped)以 warning 级别每小时吐一条新事件,影响约 150 名用户、仍在每小时发生——这是矩阵能打满 12 分的典型样本,告警等级定义可参见 docs.sentry.io 。
反例清单:三类工作建 harness 是过度工程
把上面矩阵反过来用,可以列出一个简洁的反例清单。一次性脚本 不值得。数据迁移、批量改字段名这类"跑完就扔"的任务,写一个 shell 脚本即可,贴一层 harness 等于把一次性筷子做成实木餐具。探索性研究 也不值得。需要不断变换查询角度、试探不同 prompt、容忍失败的研究任务,适合交给通用 Agent,因为它的"流程"还没定型,hard-code 进 harness 反而锁死了探索空间。需求还在变的功能开发 同样不值得。原型期的功能两周内 schema、字段、调用方都会动,harness 越细越快过期。
[观察] 这套实践的构建者在 TUI 里让值班工程师按 I 而非 F 的设计,就是为了让 harness 永远停在"调查"这一步、不触碰源文件——这本身就是把"失败可控"维度打满的具体做法。
反例清单的用处不是劝退,而是节约精力。把过度工程的成本省下来,投入到真正高频、稳定的 job 上。
三层分工:skill、MCP、harness 的边界
harness 不是孤立存在的,它和 skill、MCP 有明确的分工,三层可以叠加。
skill 靠触发 。它是模型在某次对话里"想起来就用"的能力,典型形态是系统提示词里的一段约定、或工具调用时的一条旁路规则。skill 没有独立进程,也没有自己的状态机。
MCP 给工具 。MCP(Model Context Protocol,模型上下文协议,详见 modelcontextprotocol.io )是一种把外部工具与数据源以标准化方式暴露给 Agent 的协议,典型如把 GitHub Issues、Linear、Sentry 的查询能力挂到任意 Agent 上。MCP 不规定流程,只规定"我能调什么"。
harness 定流程 。harness 是把上述能力按特定 job 的形状串起来的状态机,包括事件如何订阅、子任务如何派发、工件如何落盘、人工兜底何时介入。它决定"在什么状态下做哪一步"。
async function investigateRun(sentryUrl: string) {
const event = await sentryAdapter.fetchEvent(sentryUrl);
const suspects = await gitAdapter.searchRecentCommits(event.fileHint);
const report = await renderArtifact({ event, suspects });
await artifactStore.write(report);
return report;
}
三层叠加的好处,是各层只关心自己的语义,改动半径被天然隔离。skill 升级不需要重启 harness;harness 重构不需要换 MCP server。
维护成本:窄工具面也是窄变更面
harness 的一个隐性成本是适配器演进。每个 opinionated adapter(立场鲜明的适配器)都会随外部 API 演进而需要更新——Sentry 的事件字段会变、Linear 的 GraphQL schema 会变、GitHub REST 行为会变(参考 docs.github.com/en/rest 与 linear.app/docs )。一旦上游改了字段,harness 里的解析逻辑必须跟着改。
窄工具面同时意味着变更面更小、风险更可预测。bug triage harness 主体只有约 8 个职责单一的功能文件,每个文件只关心一个适配器或一种工件。外部 API 改一处,改动通常落在一两个文件里——这种"小而硬"的形状是 harness 长期可维护的前提。
[观察] 构建者提到,GPT-5.5 与 Claude Opus 在评审这套 harness 时,起初都抗拒在 harness 内部再放 AI 环节,倾向纯确定性实现,理由正是"再叠一层模型就多一层不可控变更"。最终版 harness 只在"选择下一步动作"这一处保留了 AI 判断,其余环节全部 deterministic。
维护成本要在打分时一并计入:总分 ≥ 8 但适配器依赖 ≥ 3 个外部系统的 job,要额外扣 1-2 分,作为"未来维护税"。
决策的落点:组合而非孤岛
矩阵打分不是终点,真正的决策落点是组合。通用 Agent 适合编排——决定"现在该跑哪个 harness、人工该何时介入"。harness 适合承担机械可预测的子任务——在固定形状的 job 上把证据收齐、把工件写规范。
工程上更推荐的拓扑是:通用 Agent 在外层做调度,harness 在内层做执行 。例如,值班工程师在 TUI 里选了 I,外层 Agent 就把这次 run 派给 bug triage harness;harness 在内部调 Sentry 适配器拉事件、调代码搜索工具定位可疑提交、最后把工件写到 artifact store;外层 Agent 拿到工件后再决定是否升级到 F。
[观察] 这种组合在工程上有一个隐含前提:通用 Agent 与 harness 共享同一套工件协议(artifact store 的字段命名、状态码、错误结构)。该示例实现现场不到 30 分钟就构建出第一版 harness,正是因为工件协议先行、适配器后置——Claude Agent SDK 把 run、task、artifact 作为一等概念暴露,详见 code.claude.com/docs/en/age… ,终端 UI 层则由 Ink 提供(参考 github.com/vadimdemede… )。
harness 的真正价值,不在于它本身有多智能,而在于它把通用 Agent 不愿意反复做的机械劳动固化下来,让外层编排者只关心"现在该跑哪条流水线"。决策矩阵的意义,正是把哪些子任务值得"流水线化"这件事,变成可以每周复盘一次的清单,而不是一次性的直觉判断。
构建你自己的 Harness:七步可复制方法论
从零到第一版:把方法论落地成可跑的 harness
[观察] 该示例实现的 bug triage harness 并不是一次性设计出来的,而是构建者用"先跑起来,再收紧"的方式迭代了三轮才稳定。第一版在一次真实的 Sentry 告警现场不到 30 分钟内搭完,事后才回过头把每一步固化成可复制的方法论。下面把这条路径拆成七步,每一步都可以独立评估"做不做、做到什么程度"。
第一步:把工作流真正写在纸上
不要把工作流留在脑子里。harness 的第一步永远是先用 markdown(或者任何你能 grep 的纯文本)把目标 job to be done 写下来。该示例实现的描述只有一句话——"值班工程师贴一条 Sentry 链接,自动产出可贴在 Linear 评论里的根因报告"——但这一句话背后藏着四个隐含约束:输入只有一条 URL、输出必须能贴到 Linear、产出物要让值班工程师一眼看懂、整套流程必须可在手机端复现。
[观察] 这一步最常见的失败模式,是把工作流写成"AI 帮我们处理告警"这种口号式句子。口号式工作流无法判断适配器该不该写、工件该不该结构化,也就无法判断 harness 是否值得做。一份合格的工作流描述至少要回答三件事:谁触发、输入是什么、产出物是什么形态。
第二步:定义 run 与 task 的执行形态
工作流写好后,下一步是把"一次完整的执行"切成"若干个 task"。在 Claude Agent SDK 的语义里(参见 code.claude.com/docs/en/age… 通常对应一次端到端的会话,而 task 对应 run 内部一次具备明确输入输出的子动作。设计时要在四件事上做明确选择:
| 维度 | 选择 A | 选择 B | 该示例的选择 |
|---|---|---|---|
| task 粒度 | 一个超大 task 自由发挥 | 多个窄 task 串行 | 窄 task:证据收集→根因分析→后续工件 |
| task 之间通信 | 共用上下文 | 通过显式工件传递 | 通过显式工件传递 |
| task 失败回滚 | 全程重跑 | 局部重试 | 局部重试,复用已有工件 |
| task 上下文隔离 | 共享同一会话 | 每次 task 独立会话 | 独立会话,只带必要工件 |
这一步之所以关键,是因为它直接决定 harness 是"包了一层 prompt 的 wrapper"还是"有内部结构的工程系统"。一旦 task 之间的边界用工件而非共享上下文来划清,后面所有的适配器、权限、规则才有挂载点。该示例实现里 task 数量被刻意压在 3 个,就是不希望每个 task 都重新发明一遍上下文管理。
第三步:为每个数据源写立场鲜明的适配器
适配器不是把 SDK 调用包一层函数那么简单。该示例实现里的 Sentry/Linear/GitHub/Vercel 四个适配器(对应 Sentry 官方文档 docs.sentry.io、Linear 文档 linear.app/docs、GitHub REST API docs.github.com/en/rest、Ver… 文档 vercel.com/docs)有一个共同特…
所谓"立场鲜明的适配器"(opinionated adapter),是指适配器在暴露给模型之前,先替模型决定好了"这次该查什么、不该查什么、字段如何裁剪"。比如 Sentry 适配器只返回事件摘要、堆栈帧前 5 帧、最近一次 release 信息;它不返回全部事件 payload、不返回内部 trace、不返回用户邮箱。Linear 适配器只暴露"为这条 issue 起草一条评论"这一个动词,不允许模型去改 issue 状态、加 label、建子任务。一个简化后的适配器接口示意如下:
export const sentryTools = {
async getEventSummary(eventId: string) {
const event = await client.getEvent(eventId);
return {
title: event.title,
stack: event.stack.frames.slice(0, 5),
lastRelease: event.release,
};
}
};
[数据] 该示例实现的 harness 主体只有约 8 个职责单一的功能文件,适配器相关的就占了 5 个。这不是巧合:适配器越窄,模型越不需要判断,harness 越稳定。当适配器层把"哪些字段该看、哪些动词该调"都固化下来,模型层就只剩下"基于这些事实写一段话"这一件事。
第四步:设计结构化的工件(artifact)
harness 与一次性 prompt 的根本差别,在于前者有 artifact store(工件存储)。每一次 run 产出的根因报告、证据快照、建议的修复 PR 草稿,都应该是结构化的 JSON 或 markdown,而不仅是一段对话历史。该示例实现里,根因报告被显式拆成"症状 / 时间线 / 相关 commit / 怀疑点 / 建议下一步"五段,每段对应一个独立字段,这样 Linear 评论粘贴时不会出现"AI 啰嗦了一大段但缺关键信息"的情况。
工件结构化还有两个隐性收益:一是方便后续评测——直接 diff 两次产出的字段差异即可;二是方便做 artifact store 的版本回滚,run 失败时可以挑选上一版可用的工件,而不是把整条 run 标记为失败。该示例实现的工件以 JSON 落到本地文件系统,文件名包含 run id 与 task 名,便于跨 run 对比。
第五步:定权限与规则
权限规则是 harness 的护栏,不是它的功能。该示例实现只授予"调查权限"——查询 Sentry、读 GitHub 代码、看 Vercel 日志——而显式排除了"修复权限":不能 push 代码、不能合并 PR、不能关闭 Linear issue。这就是 harness 文档里反复强调的 investigate-only(只调查不修复)模式。
[观察] 这种"只调查不修复"的取舍,源自一条经验:一旦 harness 拥有写权限,值班工程师就会因为"反正 AI 改了"而放弃人工 review,事故的二次放大风险会指数级上升。权限是一旦放开就收不回来的东西,所以设计阶段就要写死。除了权限,还要明文规定"哪些行为必须人工确认"——比如任何涉及删除、合并、关闭的动作,都需要在 Linear 评论里留下二次确认链接,而不是让 harness 直接执行。
第六步:选执行引擎
执行引擎的选择直接决定 harness 的天花板。可选项大致有三:
[观察] 在该示例实现的早期讨论中,GPT-5.5 与 Claude Opus 都曾倾向于把 harness 写成纯确定性代码、把 AI 环节彻底拿掉。它们的共同理由是"AI 环节越多,出 bug 的概率越大"。最终保留 AI 环节,是因为值班场景下"猜错根因但贴出可用的报告"远比"报告写不出来"更能被接受——确定性代码在这个场景下不够用。这也呼应了开篇的决策矩阵:harness 不是启动器,而是放大器,只在 AI 能带来明显增益的窄环节启用 AI。
第七步:造一个交互面
最后一步是把 harness 暴露成值班工程师愿意用的形态。可选项包括 TUI(终端 UI)、CLI(命令行)、Web 三类。该示例实现选择 TUI,基于 github.com/vadimdemede… 实现,原因有三:值班场景常在 SSH 终端里、TUI 可以边跑边显示工件生成进度、TUI 比 Web 更不容易让人顺手去点 AI 没把握的按钮。
CLI 适合 CI/CD 流水线里的无人值守场景,Web 适合需要多人协作或审批流的场景。选错了交互面,harness 即便功能正确,也不会被真用到值班现场。Ink 这类 React 风格的 TUI 库可以让工程师用熟悉的组件模型写终端界面,而不必手写 ANSI 转义序列。
元步骤:把这七步喂回给执行引擎
[数据] 现场不到 30 分钟构建出第一版,正是构建者把这七步以 markdown 形式贴进 Claude Code,让它反过来把 harness 代码生成出来,再用真实 Sentry 链接跑通端到端验证。这种"用 AI 帮你写 AI harness"的递归做法有两个前提:第一,前七步的描述必须极端具体——不能写"做一个适配器",要写"暴露 getEventById 与 listRecentReleases 两个动词,只返回堆栈前 5 帧";第二,定制提示的位置要在适配器层,而非全局 system prompt,这样模型行为才有边界。
[观察] 在该示例实现里,所有定制提示都内嵌在适配器函数里,而不是写在 harness 的全局 system prompt。这一选择让 harness 在切换模型时几乎不用改代码,只要换执行引擎即可。这是把 harness 写得像工程而不是像 prompt 的关键差异。
提示要点回顾
把上面的方法论浓缩成五条具体提示要点:
- 工作流必须极端具体 :不要写"AI 帮我们处理告警",要写"输入是 Sentry 链接,输出是 Linear 评论草稿,触发人是值班工程师"。
- 工具/适配器必须立场鲜明 :每个适配器只暴露必要的动词,字段裁剪要在适配器层完成。
- 定制提示位置要内聚 :把针对每个工具的提示放在适配器里,而不是散落在 system prompt。
- 优先采用 Agent SDK 承载智能体循环 :不要自己手写消息循环、上下文压缩、工具调度,这些是 SDK 的本职工作(参见 Claude Agent SDK 与 OpenAI Agents SDK 文档)。
- 权限与规则先于功能 :先决定 harness 不能做什么,再决定它能做什么。
完成这七步加一个元步骤之后,你拿到的不是一个"AI wrapper",而是一个可以在真实工单上反复使用、版本可控、权限有边界的工程制品。harness 的价值从来不在"用了 AI",而在"让 AI 在窄场景里比人更稳"。
生态视角:从 Cursor 到 Claude Code,一切皆 Harness
把视野从「我自己的 harness」拉远到整个生态,会发现一件反直觉的事:Cursor、Codex、Claude Code 这些被当作「AI 产品」讨论的工具,在结构上和该示例实现的 bug triage harness 没有任何本质区别——它们都是包着 AI 调用的代码。差别不在物种,只在厚度与意图。
Cursor 的编辑器侧栏、补全气泡、agent 模式,本质上是一组按特定意图编排好的工具调用、文件系统读写、终端命令与上下文注入规则。OpenAI Codex 在终端里接管 shell 的那一层也是 harness(OpenAI Codex 介绍);Claude Code 同样在用户本地机器上撑起一套跑会话、跑工具、跑子代理的运行时(Claude Code 文档)。把这些产品和该示例实现并排放在一起,看不出任何「物种级别」的差别,只看得见工程规模与产品意图的不同。
光谱两端:窄到只问一句,宽到替你写完整库
如果把市面上的 harness 放在一条光谱上,一端是极窄的规定性 harness。该示例实现的 Sentry 缺陷调查就属于这一端:输入只是一条 Sentry 告警 URL,输出固定为「证据 → 根因 → 后续工件」三段式 markdown;模型能调的工具屈指可数、能修改的目录被锁死、能跑的副作用被限制在「只读 + 写工件」以内。
[数据] 第一版在一次真实的 Sentry 告警现场不到 30 分钟内搭完,告警影响约 150 名用户且仍在每小时发生,这种紧迫性反过来逼出了 harness 的极窄边界。这种 harness 的价值不在于让 AI 更聪明,而在于让 AI 在一个被规定得很死的形状里稳定地产出。
另一端是极宽的通用编码 harness。Cursor 的 agent 模式、Codex CLI、Claude Code 都属于这一类:它们假定「用户想做什么都行」,于是尽可能多地暴露文件操作、终端执行、网络请求、MCP server 等能力,让模型自行决定下一步。代价是用户的提示词工程成本被推回到自己头上,同样的「修这个 bug」请求,跑出来的结果方差极大。
中间是广阔的自建空间。团队级的中等宽度 harness——例如只接管「前端组件库升级」、只接管「测试用例补全」、只接管「客户工单自动分诊」——才是大多数组织真正应该投入的位置。这条中间带没有明星产品,但每一条具体工作流都值得有一条专属 harness。
[数据] 该示例实现的 harness 主体只有约 8 个职责单一的功能文件,这正是窄到中等宽度区间里最舒服的体量——再多就开始变成宽 harness,再少就撑不起一次完整调查。
「wrapper 就是 harness」假说
过去几年中文社区对「AI 套壳」一词带有强烈的贬义,似乎在 LLM API 外面包一层 UI 就算不得真本事。但如果用 harness 的视角重新看,所谓 wrapper 之所以长期价值不高,绝大多数情况并不是因为「套壳」本身错了,而是因为它们套得太松。
具体来说,这些失败的 wrapper 既没有规定输入的结构,也没有规定输出的形状,更没有规定中间过程可以调什么、不能调什么;反观该示例实现的 bug triage harness,把结构化约束做到了一个极端。[观察] 一次完整的调查运行(run)在 UI 上只是「调查」(investigate-only) 这一种动作,模型既没有「修复」按钮可点,也无法通过任何命令触达源码目录。
它产出的工件包括指向 Sentry issue、相关 Linear ticket、相关 PR 的超链接,以及根因段落——这些工件是后续「是否要修复、谁来修、怎么修」决策的真相源。这种「窄到令人发指」的 wrapper,恰恰是把 LLM 调用从「玄学」变成「流水线」的关键。
所以更准确的命题应该是:wrapper 不是价值,结构化约束才是价值;而 harness 是结构化约束的一种具体工程形态。这条假说也解释了为什么很多所谓「通用 ChatGPT 套壳」活不下来、而 MCP 生态却在快速长起来(Model Context Protocol)——后者本质就是给 harness 提供可复用的「工具 + 约束」封装协议。
用通用 Agent 编排多个专用 harness
如果窄 harness 的稳定性和宽 harness 的灵活性都想要,下一步显而易见的形态是「通用 Agent + 多个专用 harness」。Claude Agent SDK 这类框架(Claude Agent SDK 文档)、OpenAI Agents SDK(OpenAI Agents SDK) 都已经在朝这个方向收敛。
上层是一个具备规划与路由能力的多模态 Agent,下层是若干个按 job to be done 拆开的 harness,每个 harness 知道自己负责的范围、知道自己的工件存哪里、知道自己的权限边界。在这种架构里,「让一个上层模型决定要不要触发 Sentry 调查 harness、触发后让 Claude Sonnet 4.6 在 harness 内执行、最后让一个汇总 harness 写 Linear 工单」就成了日常剧本。
路由层不写业务逻辑,业务逻辑在每个 harness 里;路由层只回答「现在轮到谁」。这是把杠杆做大的下一步形态,也是「多模型 + 多工具」从概念走向工程的关键拼图。
全文收束:一套可迁移的 harness 工程学
到这里,前面几节零散的方法论可以收成一张可迁移的清单。
第一,三要素:工作流、模型、约束 。任何一个 harness 在动手之前都得先把这三件事各自答完一遍;缺一个,后面就会出现「能跑但说不清为什么能跑」的尴尬,这是该示例实现迭代三版才稳下来的核心教训。
第二,时机判断 。当一个工作流被三个人以上每周重复两次以上,就已经值得为它造一条 harness;反过来,偶发且上下文高度开放的任务,继续让人 + 通用 Agent 处理更划算。
第三,架构分层 。建议至少分三层:编排层(决定路由)、harness 层(封装 job)、适配器层(对接外部系统) 。Sentry、Linear、GitHub、Vercel 的官方 API(Sentry 文档、Linear 文档、GitHub REST API、Vercel 文档)在适配器层各占一个文件,立场鲜明的 opinionated adapter 把「我们团队认为什么字段该这么读」明确写死。
第四,适配器立场化 。立场鲜明的适配器不是缺陷而是特性;团队和团队之间的差异,大部分都沉淀在这一层。把它藏起来才是问题,把它显式化反而是 harness 工程化的标志。
第五,工件真相源 。run、task、flag、artifact 这四类工件必须有统一的存储与命名约定,所有后续动作以读工件而不是重新调模型为起点。换句话说,harness 不只是把模型包起来,更是在工程里新建一个轻量级的、可审计的存储层。
第六,渐进放权 。从只读不写,到写工件不写代码,到写代码但不提交,到写代码并提 PR——每扩一档权限都对应一次 harness 的小版本升级。该示例实现之所以「先跑起来再收紧」能成立,正是因为每一轮迭代都在向放权阶梯的下一格迈进。
把这六条放在一起,得到的不是「又一个 AI 工具」,而是一门可以跨团队、跨业务、跨模型复用的 harness 工程学。它不需要明星产品来承载,也不依赖某个模型长期独占最优;它只需要团队愿意把工作流写下来、把约束写下来、把工件存下来。剩下的事,任何一代更强的模型进来,都能直接坐收红利。
参考来源
A 类 · 官方与一手资料
B 类 · 社区与延伸阅读
English / 原文
Key Takeaways
- harness(驭具/外壳)的最简定义:包在 AI Agent 外面、让它更高效完成特定工作的一层代码,可以含 AI 也可以不含
- 每个 harness 都由三个部分组成:specific context(特定上下文)、specific actions(能采取的特定动作)、specific outcomes(要产出的特定结果)
- 构建时机的判定式:同一工作流需要同样的设置、走向同样的结果时,就值得固化成 harness
- 缺陷分诊(bug triage)每天重复、流程稳定、输入输出清晰,是 harness 的教科书场景
- 直接用通用编码工具时,每次都要写「亲爱的 Agent,请修这个 bug」并附上链接、解释意图
什么是 AI Harness:围绕智能体的那一层代码
一、最朴素的定义:包在 Agent 外面的一层代码
Harness 这个词近一年被反复引用,但底层定义其实很短。把它译成「驭具」或「外壳」会更贴切:它就是包在 AI Agent 外面、帮助模型更高效完成某一类工作的一层代码。这里的「一层代码」可以是普通的 TypeScript 函数、Python 脚本、命令行循环,也可以是嵌入了另一次模型调用的子流程,关键是它位于模型和真实业务之间,承担约束、编排、上下文注入与结果回收的职责。
[观察] 这套实践里反复强调一件事:harness 不一定要「含 AI」。一个 bug 调查 harness 完全可以用纯确定性代码写完所有调查步骤,只在最后一步把整理好的事实交给模型写摘要。这种取舍让 harness 的边界从「AI 编排」缩到「工作流约束」,反而更稳、更易测试。
二、目标只有一个:让 AI 在某个 job 上更值钱
通用聊天框是模型厂商的活,不是 harness 的活。Harness 的目标从来都是把模型钉在一个具体 job 上,让它在这个岗位上比裸用模型更有用。这就是「job to be done」视角:harness 不是让模型更聪明,而是让模型在某个具体岗位上更便宜、更稳定、更可被审计。
衡量 harness 价值的指标只有三条:单位时间内完成的 job 数、平均介入的人工次数、结果的可复现性。围绕这三条指标做减法,比堆功能更有效。任何让 harness 偏离这三项指标的设计,都应该被砍掉。
三、为什么 harness 概念被神秘化
过去一年,「not the model, it's the harness」几乎成为新的 prompt 口头禅,Anthropic 在官方文档里也把它作为核心叙事反复铺垫 (见 www.anthropic.com)。但问题在于,几乎没人把 harness 本身讲清楚:它到底由哪些零件组成?和 prompt、tool、agent loop 的边界在哪?很多人把 harness 当成某种调教模型的玄学,把它和「模型微调」「场景适配」「prompt 调优」混为一谈,其实它一点都不玄。
把 harness 神秘化通常出于两个原因:一是模型本身迭代太快,大家来不及把 success pattern 沉淀成代码;二是玄学化的叙事更利于传播。剥掉叙事,真实的 harness 工程,就是给 AI 写更多约束性代码,让它在一个具体 job 上更有用,就这么简单。
四、harness 的零件拆解
把一只典型 harness 拆开,通常能看到这样几样东西:
| 零件 | 职责 | 是否必需 |
|---|---|---|
| 运行循环 (run loop) | 决定模型每轮拿到什么、产出什么、什么时候终止 | 必需 |
| 任务定义 (task) | 把一次用户意图拆成可执行的步骤序列 | 必需 |
| 工具与适配器 (adapter) | 屏蔽外部系统差异,如 Sentry、Linear、GitHub API | 必需 |
| 工件存储 (artifact store) | 把中间过程落盘,方便回放与调试 | 强烈建议 |
| 权限规则 (permission) | 决定模型能调用哪些工具、修改哪些文件 | 必需 |
| 交互层 (UI / CLI) | 把进度、确认、错误回显给用户 | 视场景 |
把表格展开成最小骨架,大概是这样:
const run = async (input: IssueLink) => {
const plan = await planTask(input);
const artifacts = [];
for (const step of plan.steps) {
const result = await executeStep(step, { permissions });
artifacts.push(result);
}
return await summarize(artifacts);
};
这只是一个示意,但已经能看出 harness 的本质:一个受约束的循环,每个步骤都有明确输入、明确输出、明确权限。模型不是 harness 的中心,而是其中一颗可被替换的零件。
五、什么时候该自己写一只 harness
判断标准不复杂:当团队反复把同样一段 prompt 贴给同一个模型、让模型接同样的工具、出同样的产物时,就值得把这段流程固化成 harness。具体信号有三个:其一,粘贴的 prompt 模板超过 5 行;其二,接入的工具名称超过 3 个;其三,产出物结构一致到可以用模板填充。
三类场景适配最高:重复性调研 (例如缺陷调查、论文综述)、结构化生产 (例如 changelog、release notes、incident report)、受约束的运维动作 (例如 canary 部署前的自动体检)。这三类共同点是输入噪声大、输出结构稳定、对可回放要求高。
如果场景属于「一次性探索」或「纯创意写作」,直接用聊天框更省事,硬塞 harness 反而拖慢节奏。
六、案例:Sentry 缺陷调查 harness
该示例实现来自一家中型 SaaS 团队 (公开代号 ChatPRD),他们用 Claude Agent SDK (见 code.claude.com/docs/en/age…) 配合 Ink 终端 UI 库 (见 github.com/vadimdemede… bug 调查的 harness。所有外部系统接入都通过立场鲜明的适配器 (opinionated adapter) 完成:Sentry 适配器只暴露「按 issue 拉事件流、按时间窗聚合、抽取可疑 stack frame」三个动作;Linear 适配器只暴露「按 issue id 查相关工单、查最近归属人、查近 30 天类似 bug」三个动作;GitHub 适配器只暴露「按可疑文件查最近 commit、按 commit 查 blame、按 blame 查相关 PR」三个动作;Vercel 适配器只暴露「按 commit 查 deployment、查 deployment 日志、查日志中报错计数」三个动作。每个适配器背后接的是各家官方 API (Sentry 见 docs.sentry.io、GitHub 见 docs.github.com/en/rest、Ver… 见 vercel.com/docs、Linear 见 linear.app/docs)。
[数据] 整套 harness 主体只有约 8 个职责单一的功能文件,每个适配器不超过 100 行,整个项目在不到 30 分钟内搭建出第一版,跑通 Sentry 到 GitHub 的链路。现场展示的那条告警影响约 150 名用户、仍在每小时发生,模型自动跑完调查后给出的根因被构建者原话回顾为「sentry 报错指向的提交,确实是今天第一次引入」。
粘贴一条 Sentry 链接即可启动,不需要再写「dear agent」式的提示。任何一步出错,模型都要在终端里被显式确认,而不是悄悄自己重试。整个调查 run 留下一棵可回放的工件树:每一步都打 artifact,artifact 既能被模型读也能给人读。
[观察] 这套实践里有两个反直觉的取舍。第一个是「调查运行选 I 而非 F」:即所有调查动作走 investigation (只调查不修复) 模式,源文件保持只读,只有写工件时才动盘。第二个是 GPT-5.5 与 Claude Opus 起初都抗拒在 harness 里放 AI 环节,倾向纯确定性实现;最终落地的方案只在 summarize 一步保留了模型调用,其余 80% 的步骤都是确定性代码。这两个取舍直接决定了 harness 的可信度,也是它与「wrapper (套壳)」之间最清晰的分界线。
MCP (Model Context Protocol,见 modelcontextprotocol.io) 负责把各家官方 API 包装成统一的工具描述,Claude Agent SDK 拿到工具描述后就在自己的 run loop 里驱动模型;交互面用 Ink 库即时渲染表格、链接、确认按钮,在终端里也能获得类 IDE 的反馈。
七、七步可复用方法论
把以上案例抽象成方法论,可以归纳为七步:
- 识别工作流 :找到团队反复粘贴的同一段 prompt,把它作为种子。
- 定义 run 与 task :run 是一次端到端会话,task 是 run 内可独立执行的步骤。
- 设计窄适配器 :每个外部系统只暴露 3-5 个动作,动作名要动词化、不可组合。
- 结构化工件 :每一步产出统一 schema 的 artifact,既给模型读,又给人读。
- 设置权限规则 :默认 investigation 模式,源文件只读;修改动作必须显式确认。
- 选执行引擎 :用 Claude Agent SDK 或同类 SDK 作为 run loop,让模型只负责 planning 与 summarization。
- 造交互面 :用 Ink 或同类的终端 UI 库,把进度、确认、产物实时回显,降低长 run 的焦虑感。
按这七步走下来的 harness,通常能在两周内替换掉团队里最薄的那块 prompt 模板,先把确定性部分替换出来,再保留模型创造性。Harness 的难度不在 AI 部分,而在那些必须保持狭窄、必须可回放、必须由人决定的约束。把这些约束写下来,就是全部的工程。
Harness 三要素:特定上下文、特定动作、特定结果
理解了 harness 是「包在 Agent 外面的代码」,下一步要回答的工程问题就是:这一层代码里到底要塞什么?答案可以收得很窄——每个 harness 都由三个部分组成:特定上下文(specific context)、能采取的特定动作(specific actions)、要产出的特定结果(specific outcomes) 。三个要素同时收敛,Agent 的行为方差才会显著下降,这也是 harness 与开放式聊天之间最本质的界线。
特定上下文:领域信息预先内置,不靠用户每次粘贴
在日常使用大模型时,工程师最常做的一件事就是「铺上下文」:把报错日志、代码片段、需求文档一段一段贴进对话框,再写一句「请帮我看看」。这种做法在 ad-hoc 场景下没问题,但放到一个真正要反复执行的工作流里,每一次粘贴都意味着新的不确定性——这次的日志是不是截完整了?这次的描述是不是又漏了关键参数?
harness 对「特定上下文」的解法是反向操作 :不再让用户每次喂料,而是由 harness 自己在启动时就把领域信息准备齐。以缺陷调查 harness 为例,它的入口只需要一条 Sentry 告警链接,所有「上下文」——错误堆栈、影响用户数、首次出现时间、相关 release、关联 commit、曾在 Linear 里被讨论过的 ticket——都由适配器主动拉取,塞进模型的输入窗口。Sentry 官方文档与 Linear 官方文档分别定义了这些数据的字段语义,mcp server 可以据此标准化读取。
[观察] 在该示例实现里,一次完整的调查运行甚至不需要用户再写任何 dear agent 式提示词 ;harness 内部已经拼好了一段结构化的指令,把「只调查、不修复」「证据优于猜测」「必须产出 Linear 工单」这些规则写死在指令骨架里。这一对照很能说明问题:上下文不是「少写一段提示」,而是「把判断力从人搬到代码」。
特定动作:工具面收窄,只允许调用与该 job 相关的 API 与操作
如果上下文解决了「模型看到什么」,那么「特定动作」解决的就是「模型能碰什么」。工具面收窄 是 harness 最容易被低估的设计:它不是把 GPT-5.5 或 Claude Opus 的全部能力暴露给 Agent,而是只允许它调用与当前 job 相关的 API 与操作。
具体到缺陷调查 harness,允许调用的动作清单大致是:读 Sentry issue、读 Linear 关联 ticket、读 GitHub 相关 PR/diff、读 Vercel 部署日志、读内部代码搜索结果;被显式禁止 的动作包括:修改任何源文件、合并 PR、关闭 Sentry issue、修改 Linear ticket 状态。Claude Agent SDK 把这种「允许/禁止」抽象成了一组权限规则 (Claude Agent SDK 文档),构建者只需在配置里写出一份白名单,运行时就有了审计与拦截的清晰边界。
工具面收窄带来两个工程收益:第一,出错半径被锁死,即便模型给出「幻觉式」建议,也无法越过工具边界造成实质破坏;第二,行为更可复现,同一个 issue 用同一个 harness 跑两遍,调用顺序与参数方差远小于「通用 Agent + 自由 prompt」。Model Context Protocol 的出现 (MCP 规范) 让这种「窄工具面」的实现变得更标准化,每个工具以 server 形式声明自己的能力,Agent 拿到的是一份受限的能力清单,而不是无限可能。
const harness = defineHarness({
job: 'investigate-bug',
context: { sentry: sentryAdapter, linear: linearAdapter },
tools: {
allow: ['read_sentry_issue', 'read_linear_issue',
'read_github_pr', 'read_vercel_logs'],
deny: ['write_file', 'merge_pull_request',
'close_sentry_issue', 'update_linear_status'],
},
output: { report: 'markdown', ticket: 'linear' },
});
特定结果:输出结构被预先定义,报告、工单、工件的格式在代码里写死
第三块基石是「特定结果」。harness 在启动时就已经约定好:这次运行结束之后,要产出一个结构化产物 ——它可能是一份 Markdown 报告、一条 Linear 工单、一段 JSON 证据链,或者一个上传到 artifact store 的工件文件。格式不是用户读完自由发挥,而是写在 harness 代码里的模板。
这种「结果收口」的设计哲学,在 Anthropic 官网上对 Claude Code 的描述里也能看到对应:模型本身是不稳定的,而 harness 的核心价值之一,就是用结构化的输出把不稳定收束成可被工程系统继续处理的数据 (Anthropic Claude Code 文档)。在该示例实现中,一次调查运行总会产出三件套:根因 Markdown 报告、Linear 上的 follow-up ticket、存入 artifact store 的原始证据包。三件套的字段、命名、序列化方式、写入位置都是代码里写死的,任何调用方拿到的结果都是同一种「形状」。
[数据] 在该示例实现里,一次具体的某 Sentry 告警影响约 150 名用户、仍在每小时发生 ——这正是它要被 harness 优先收编的原因。另一组对比同样直观:同一类告警,在开放式聊天里跑和放进 harness 里跑,产出物的字段完整度从约 40% 提升到接近 100%。这不是模型变聪明了,而是格式约束逼着模型的输出必须落到工程系统能消费的形态上 。该 harness 主体只有约 8 个职责单一的功能文件,现场不到 30 分钟就能搭出第一版。
三要素同时收敛,行为方差才会显著下降
把三个要素分开看,每一条都不算新鲜——「减少输入」「收窄工具」「结构化输出」在传统软件工程里都有对应实践。harness 的特殊性在于:三者必须同时收敛 ,缺一个,另外两个就发挥不出效果。
只有上下文而不收窄动作,模型会基于充足信息开始「自由发挥」,写出大段分析与建议,却落不到任何系统里;只有收窄动作而没结构化输出,模型会精确地调用工具,却用散文体回报结果,后人无法程序化处理;只有结构化输出而没有内置上下文,模型每次都拿到一份空白模板,靠自身「常识」填空,质量严重依赖抽卡。三要素齐了,Agent 不再是「会聊天的同事」,而是「接到固定任务、跑固定流程、出固定产物」的工程组件。
[观察] 这套实践里有一个有趣的早期争论:要不要在 harness 里再嵌一层 AI?GPT-5.5 与 Claude Opus 在初期都倾向纯确定性实现 ——它们愿意把「调用 Sentry」「归并相似告警」这种环节写死成脚本,而把 AI 留给「阅读日志并产出根因假设」这一段。理由的一致性很高:多一个 AI 环节,就多一个不可观测的方差源,能不用就不用。harness 的三要素本质上是在和模型的随机性做「攻防战」:用更窄的输入、更窄的工具、更窄的输出,把不确定性一层一层剥掉。
工程自检
判断一个 harness 设计是否合格,有一个简单自检:**它的运行入口、可用工具、产出格式是否能在不读 prompt 的情况下被写出来?**如果三个答案都能换写成「确定的东西」,那它就走在了 harness 的轨道上;如果任何一项还停留在「看模型心情」,那它本质上还是一次包装过的开放式聊天。三要素不是装饰,而是把 Agent 工程化的最小约束集合——少了任何一项,所谓的 harness 都只是另一种 wrapper。
何时构建 Harness,何时用通用编码工具就够
上一节我们把 harness 拆成了「特定上下文 + 特定动作 + 特定结果」三件套。本节回答一个更前置的工程问题:什么时候值得花这份力气去构建 harness,什么时候直接打开 Claude Code 或 Codex 就够了 。这个判定若做错,前面的工程反而成了负债——固化一套本不该固化的流程,得到的不是效率,而是更难维护的内部工具,半年后还要被人问「这玩意儿到底在干啥」。
构建时机的判定式
判定式其实只有一句话:当同一个工作流需要同样一组设置、并且反复走向同样的结果时,就值得把它固化成 harness 。这里的关键不在「是不是重复做了三次」,而在「它的输入、动作、产出是不是同一形态」。如果三次任务只是表面相似、实际走向截然不同的终点,那固化只会把噪声也一起固化,反而拖慢效率、模糊责任边界。
[观察] 这套实践里的缺陷调查 harness 之所以成立,正是因为「告警页面(Sentry issue)→ 取证(Git blame、commit、deploy 历史)→ 根因分析 → 写一份结构化工件」这条链路每次都长一个样。换句话说,触发条件、动作序列、产出形态三者同时收敛,才有工程意义上的可复用。Sentry 官方的告警数据结构本身就高度结构化(Sentry 文档),这是该 harness 能稳定工作的隐性前提——如果触发信号是「一段口语化的告警描述」,harness 的入口就要先做一遍对齐,可行性会显著下降。
下面这张图把判定逻辑收成一个可对照的决策矩阵:
- 触发条件同型 :同一类入口信号(告警、PR、ticket、迁移窗口)。
- 动作序列同型 :取数 → 分析 → 落盘,或收集 → 起草 → 提交。
- 产出形态同型 :同一份模板的工件(调查报告、PR 描述、runbook)。
- 频次足够 :足够高的发生频次,让固化成本在合理周期内被摊薄。
四项里命中三条以上,harness 通常就划算了;命中两条则属于灰区,值得先用 Claude Code / Codex 跑两周,等模式稳定后再固化,避免「为想象中的高频任务写代码」。
适合的工作负载:确定性 + 非确定性
值得做 harness 的工作负载,几乎都有一个共同的形态:大量确定性步骤里夹着若干需要 Agent 决策的环节 。一边是硬性的取数、调 API、写文件;另一边是「这句话怎么总结」「根因到底是哪个 commit」「这条 PR 描述是否覆盖了 reviewer 的关切」这种需要语言模型判断的活儿。harness 的价值,就是把前者写成代码、把后者留给模型,让两者各司其职,而不是让模型从「启动浏览器」这种基础动作开始重新规划。
具体而言,一个典型的可固化工作负载同时具备三要素:
- step-by-step 流程 :可以画成有向无环图(DAG),每一步的输入输出都明确,失败时可以重试或跳过。
- 特定工具 :Anthropic 官方在 Claude Agent SDK 文档里就明确推荐为 harness 装配工具调用权限(Claude Agent SDK 概览),并提供权限白名单作为安全护栏。工具越收敛,Agent 的行为方差越小。
- 特定用例 :不是为了「让 Agent 更聪明」,而是为了完成一个边界清晰的 job,例如「在五分钟内告诉我这条 Sentry 告警是不是上次那个 commit 引起的」。
这三要素只要缺一个,要么退化成通用聊天(变成「让 Agent 自由发挥」,方差无法收敛),要么变成内部框架的过度工程(写完没人用,半年后变成遗产代码)。
下面是一段典型的编排伪代码,确定性步骤与非确定性步骤的边界一目了然:
run: defect-investigator
input: sentry_issue_url
flags: [investigate-only, no-source-edit]
steps:
- fetch_sentry_context(url)
- git_blame(stacktrace)
- correlate_recent_deploys()
- llm_summarize_root_cause(context)
- write_artifact(template=incident-report.md)
典型场景
把视角抬高一档,这类工作负载在工程团队里反复出现的频率其实比想象中更高:
| 场景 | 触发信号 | 主要动作 | 产出 |
|---|---|---|---|
| 编码 | issue / 任务描述 | 读仓库、跑测试、写 patch | 提交 PR |
| 生产事故管理 | Sentry 告警 / on-call | 取上下文、追 commit、写时间线 | 根因报告 |
| PR 发布准备 | 待合并 PR | 生成描述、跑检查、回 reviewer | 整理好的 PR |
| 支持工单升级 | 一线 ticket | 拼上下文、找已知问题、起草回复 | 升级单或回信 |
| 迁移管理 | 计划窗口 | 跑脚本、记录差异、回滚预案 | 迁移 runbook |
| 研究/文档整合 | 主题或问题 | 搜资料、抽取观点、合成笔记 | 结构化笔记 |
[数据] 在这套实践里,缺陷调查 harness 的主体大约由 8 个职责单一的功能文件拼成,典型一次调查运行选 I(只调查、不修复)而非 F(直接修复),全程不触碰源文件,把「取证 → 根因 → 工件」切成三个明确阶段;该 Sentry 告警影响约 150 名用户且仍在每小时发生,这正是触发条件同型的高频场景——也是 harness 能把「原本每次都要人工盯 30 分钟的事」压到几分钟内出报告的关键。
值得注意的是,非技术场景同样适用 。按固定方式做研究(固定信源 + 固定模板)、按固定方式整合文档(同一组文件夹 + 同一种摘要结构),这些工作看起来没有「生产事故」那么戏剧性,但只要触发条件稳定,固化后节省的注意力比想象中更多——研究员的瓶颈往往不是「不会想」,而是「每次都要重新拼脚手架」。这一点,在「知识工作者也在被 harness 改造」这件事上,和工程团队是同一逻辑。
编码 harness 为什么最流行
如果只挑一个 harness 类型来解释整个生态,编码 harness 是绕不开的样本。原因不是其他工作不重要,而是「coding」天然满足 job to be done 的全部条件:
- 入口信号统一 :GitHub issue、Jira ticket、Linear issue,基本都能落到结构化字段(GitHub REST API 提供了完整的元数据)。
- 工具集明确 :文件读写、命令执行、PR 创建、review 评论,边界清晰,不会出现「这个任务的工具突然就变了」。
- 产出形态统一 :diff + commit + PR 描述,行业已经形成模板。
- 可验证 :CI 是天然的回环,跑得过 / 跑不过是 0/1 信号,失败可以重试。
[观察] 这套实践里最直观的一个细节是:粘贴一条 Sentry 链接,defect-investigator 就自动开始工作,无需再写「dear agent, please investigate」的提示语。这种「一次设置、每次粘贴即用」的体验,正是 Claude Code 或 Codex 直接交互很难稳定复现的——后者每次都是开放式的开场,缺少「这一次就是干这件事」的预设语境,导致同一条 Sentry 链接喂进去,两次产出的报告会风格迥异、关键字段时有时无。
也正因为这一点,OpenAI 在推出 Codex 时就把「为工程任务专门构建」作为主打卖点(Introducing Codex),Anthropic 的 Claude Code 也明确强调它是「为编码工作流深度定制的 CLI」(Claude Code 概览)。两家在产品形态上略有差异,但都在往「为特定 job 装配 harness」这个方向收敛,而不是去做一个通用聊天入口。
不适合 harness 的场景
判定式的另一面同样重要。如果一个任务满足下面任意一条,继续用 Claude Code / Codex 直接交互是更合适的选择 ,硬上 harness 只会增加维护成本,让团队在迭代假需求上耗尽注意力:
- 偶发性 :一年做一两次,固化成本远高于收益,这种场景下「再写一次提示语」反而比维护 harness 便宜。
- 探索性 :目标在过程中不断漂移,harness 的预设上下文反而会限制视野,变成「按错误的方向跑得更快」。
- 高人机耦合 :每一步都需要人拍板,比如架构选型、设计评审、敏感的人事沟通。这种场景的关键不是「让 Agent 更准」,而是「让人类保留判断」。
- 目标未收敛 :连「产出长什么样」都还没想清楚,这时候去固化模板,只会把偏见写进 harness,后面每一次迭代都要重写模板。
[观察] 一个反直觉但常见的细节是:GPT-5.5 与 Claude Opus 在被问及「harness 里要不要放 AI 环节」时,一开始都倾向纯确定性实现,认为「既然是流程,就不该再让模型自由发挥」。这种「过度追求确定性」的本能,恰好是 harness 工程化里最容易踩的坑——一旦把所有判断都收敛成代码,harness 反而退化成 workflow 引擎,失去对非确定性环节的适应力,遇到边界外的情况就会僵住。正确做法是:确定性步骤交给代码,模糊地带留给模型,并用 run/task/flag 这样的轻量结构把两者粘合起来 ,让 harness 既稳又灵——这也是把 harness 和 Airflow / Temporal 那一类传统工作流引擎区分开的本质。
收尾
所以,「何时构建 harness」本质上是一个 ROI 问题:触发条件同型 + 动作序列同型 + 产出形态同型,三因素同收敛,固化就划算;偶发、探索、需要人全程掌舵的任务,继续用 Claude Code / Codex 直接交互更合适 。判定做对了,后续每一步才不会白费力气;判定做错了,再多精巧的适配器也救不回来。下一节我们就在这个判定之上,具体拆解 harness 的「特定上下文」这一件套该怎么塞——领域信息预应该预到什么粒度,边界又画在哪里。
场景选型:为什么缺陷分诊是理想的第一个 Harness
为什么缺陷分诊是第一个 Harness 的教科书场景
上一节把 harness 拆成了「特定上下文 + 特定动作 + 特定结果」三件套。这套拆解是抽象的,真正落地时需要回答一个更前置的问题:什么时候值得花力气固化一个工作流,什么时候直接打开 Claude Code 或 Codex 的会话框就够了。判定做错,工程反而会变成负债——固化一套本不该固化的流程,得到的不是效率,而是更难维护的内部工具,半年后还要被人反复追问「这玩意儿到底在干啥」。
判定式其实只有一句话:当同一个工作流需要同一组设置、并且反复走向同样的结果时,就值得把它固化成 harness 。这一句回答了「该不该做」,却没有回答「从哪一个工作流开始做」。Harness 的第一刀切在哪里,会决定后续所有适配器、run 语义、权限规则的样板长什么样。切口如果过宽,后续会被迫支持太多边界场景,适配器臃肿到无法维护;切口如果过窄,又学不到可复用的方法论。教科书级别的答案是缺陷分诊(bug triage) :它每天重复、流程稳定、输入输出清晰,是 harness 三件套能被精确预制的最干净样本。
为什么是缺陷分诊,而不是代码评审、文档撰写、或者新功能开发?原因是它同时满足三个结构化条件。第一,频率 ——线上告警每天会来若干条,Sentry(Sentry 官方文档)一旦接入就会持续推流。第二,歧义度 ——每一条告警的调查路径都收敛于相对确定的几个动作:拉代码、看提交、查文档、跑测试、写结论,不会因为 PM 一句话就临时改流程。第三,验收物 ——最终产物要么是一段调查记录(报告),要么是一个 Linear(Linear 官方文档)工单,要么是一个 GitHub(GitHub REST API 文档)PR,这三类工件都有明确格式和明确下游消费者,失败也可以用「少建一个工单」这种低成本方式兜底。三个条件同时成立,harness 的三件套就有了可被精确预制的语义边界——输入端的 Sentry 链接、输出端的 Linear 工单或调查归档,中间的工作流是已经被工程师用肌肉记忆跑过无数次的例行调查。
完整链路:每一步都能预先编码
缺陷分诊的完整链路可以被拆成五步,每一步都能预先编码为 harness 的功能模块。这是它和「半结构化任务」最本质的区别——半结构化任务的步骤依赖当次输入临时决定,而分诊的步骤可以从一开始就被列成一张固定清单。
第一步,Sentry 告警入口 。Webhook 推送到 harness 入口,带上一个事件 ID、堆栈轨迹、影响用户数和首次出现时间。([数据] 一次具体的告警案例里,该 Sentry 事件影响约 150 名用户,并仍在以每小时若干次的频率持续发生,这种数字直接决定了后续是否值得建工单的严重度阈值。)第二步,证据收集 ——从仓库定位可疑提交的 git blame、从内部文档拉相关模块的设计说明、从过往 PR 里翻同类历史。这个阶段会用到 GitHub、Notion 等适配器,每一类来源都是一个独立的功能文件。第三步,根因假设 ——Agent 把证据汇总成最可能的成因清单,按可能性排序,带置信度标签。第四步,是否建 Linear 工单 ——这一步需要一个明确的判定规则,通常围绕严重度、复现率、是否影响核心路径三轴打分,打分逻辑是纯确定性的,不需要 LLM 介入。第五步,是否允许修复 ——这一步是权限开关,默认关闭,只有当 Agent 在前四步给出了高置信度根因、且改动面被圈定在几个文件以内时,才开放后续修复 run。
const triage = {
trigger: sentryWebhook({ eventId, stack, affectedUsers }),
steps: [
collectEvidence,
hypothesizeRootCause,
scoreSeverity,
decideLinearTicket,
decideFix,
],
artifacts: ["report.md", "linear-ticket.json", "evidence-cards/"],
};
可以注意到,真正需要 LLM 推理的只有「根因假设」一步,其余四步都是确定性 IO 加规则判断。这也是 harness 设计的核心取舍:把 LLM 框在最擅长语义综合的环节,把确定性环节交还给代码 。
| 步骤 | 输入 | 动作 | 是否需要 LLM |
|---|---|---|---|
| 告警入口 | Sentry Webhook | 解析事件 ID / 堆栈 / 影响面 | 否 |
| 证据收集 | 仓库 / 文档 / 过往 PR | git blame / 搜索 / 抓取 | 部分 |
| 根因假设 | 证据卡片 | 综合推理 | 是 |
| 建工单判定 | 候选根因 + 影响面 | 三轴打分 | 否 |
| 修复授权 | 候选根因 + 改动面估算 | 风险阈值判断 | 否 |
重复劳动的隐性成本
缺陷分诊之所以值得第一个固化,根本理由是它的隐性成本远超直觉。资深工程师一周会被 Sentry 告警打断若干次,每次打断要花十几分钟到半小时查上下文、翻代码、写结论,这些时间加起来是真实的工作小时数。([观察] 该示例实现里,调查 run 默认选 I(investigate)而非 F(fix),全程不触碰源文件——这意味着 Agent 永远不会在没有人类确认的情况下提交代码,工程师对产出物的信任建立在「它只是看,不动」这一条简单约束上。)
把这些重复且机械的调查工作交给一个受约束的 Agent,收益立竿见影:工程师不需要中断手头的工作流去响应每一条告警,harness 在后台跑完,把结论送到 Linear 或 Slack 等人翻牌即可。这种「异步消音」的体验价值,在告警密集的项目里尤其显著——它把告警从「立刻处理的中断」转译成「稍后批阅的收件箱」。
更微妙的好处是,固化之后的 harness 会形成可审计的调查日志 ——每一次告警的证据链、根因假设、最终决策都被记录在工件存储(artifact store)里。([数据] 该 harness 主体只有约 8 个职责单一的功能文件,这一规模本身就说明它没有试图去覆盖太多场景,而是把每一个能力都拆成可独立替换的模块。)半年后回头看,这些日志就是一份组织级的故障响应手册,比任何 wiki 都更接近真实运行情况。
选场景的通用准则:四条铁律
缺陷分诊能成为样板,是因为它天然满足一套更通用的 harness 选型准则。把这一套准则提炼出来,后续就可以照葫芦画瓢地挑下一个场景。
第一条,高频 。低频场景固化 harness 性价比为零,工程投入无法被分摊到足够多次运行里。第二条,低歧义 。工作流的步骤能在事前被列成清单,而不是依赖当次输入临时决定;一旦步骤需要 LLM 现场编排,固化就失去了意义。第三条,有明确验收物 。最终输出必须是人类或下游系统可以一眼判定合格不合格的工件——报告、工单、PR、归档都是合格形态;「一次成功的对话」不是合格形态,因为它无法被事后审计。第四条,失败成本可控 。最坏情况下,harness 出错带来的损失必须小于它节省的人工成本,并且错误是可观察、可回滚的。
| 准则 | 缺陷分诊 | 代码评审 | 新功能开发 |
|---|---|---|---|
| 高频 | 满足 | 部分满足 | 不满足 |
| 低歧义 | 满足 | 部分满足 | 不满足 |
| 明确验收物 | 满足 | 满足 | 不满足 |
| 失败成本可控 | 满足 | 部分满足 | 不满足 |
缺陷分诊是四条全满足的场景,代码评审只满足部分,新功能开发几乎一条都不满足。这就是为什么第一个 harness 应该选缺陷分诊而不是更性感的「让 AI 写新功能」——后者在「低歧义」和「失败成本可控」两条上都过不了关。
风险控制:从 investigate-only 起步
最后一条不是选型准则,而是上线策略 :第一个 harness 永远从只调查、不修复(investigate-only)起步,而不是一上来就放开完整修复权限。这是控制风险的关键决策,理由有三。
第一,调查的失败成本天然低。Agent 给出错误的根因假设,最坏后果是多花工程师十分钟去复核;而修复的失败成本可能是把生产环境改坏。第二,调查产出物天然可审计。一段文字结论、一个工单、一份归档都可以被人工一眼扫过,不像代码 diff 需要在 IDE 里仔细 review。第三,investigate-only 让 harness 的权限模型保持极简——「只读仓库、只读文档、可以建工单、不可以 push 代码」,这一组规则用四行配置就能写完,后续每加一个能力都要重新评估风险面。
([观察] 在该示例实现里,启动一次调查只需要粘贴一条 Sentry 链接到终端,不需要再写 dear agent 式提示,这种启动摩擦的极小化,本身就是 investigate-only 路线的副产品——因为没有写入权限,就不需要复杂的 prompt 来约束 Agent「不要乱改东西」。)权限规则一旦建立,后续再根据实际表现逐步放开,例如对某些高置信度场景开放「自动建 PR」,对另一些场景开放「自动合并」。这种渐进式解锁 比一次到位更接近生产环境的真实演化路径,也让 harness 的每一次能力升级都建立在被验证过的产出物之上。
缺陷分诊是 harness 第一个落地场景的最佳答案,不是因为它最有趣,而是因为它最教科书——高频、低歧义、有验收物、失败成本低,完整链路可以被预先编码成五步确定性流程。从 investigate-only 起步,既控制了风险,又让 harness 的权限模型保持可解释。这套选型与上线策略可以原样复用到下一个 harness,只是工作流的语义会被替换成新的领域。
与直接用 Claude Code 的差别:把意图预编码进系统
与直接用 Claude Code 的差别:把意图预编码进系统
打开 Claude Code 或 OpenAI Codex 的会话框,把一条 Sentry 链接甩进去,等模型开始干活——这是大多数团队今天跑缺陷分诊的姿势。看上去足够灵活,代价却是把「我到底想要什么」这件事,反复写进每一次提示里。
通用工具的隐性税:每条提示都在重新陈述意图
直接在通用编码助手里跑缺陷调查,几乎一定包含这几步操作:复制告警链接,粘进对话框;再补一句「请帮我定位这个 bug 的根因,并告诉我该改哪一行」;解释清楚这条告警影响什么、谁触发、期望 Agent 给出什么样的产出;按下回车,然后祈祷模型理解了这套一次性提示。每一次会话都是一次微型合同谈判——人类重新把「意图、上下文、产出形态」讲一遍,模型重新猜测一遍。
[观察] 在该示例实现的早期版本里,启动一次缺陷调查的输入文本通常在 200 到 400 字之间,其中超过一半内容是在复述「我想做什么」,而不是补充新的事实。
这种「每次重新讲一遍」的代价不止是敲键盘的时间。它至少有三种隐性损失:第一 ,意图会在每次提示中被模型重新解读,同一份 Sentry 告警今天拿到的是「查日志」、明天变成「改代码」,结果高度漂移;第二 ,任何需要被人类补齐的字段——像告警 ID、复现步骤、预期结果——都会随着工程师当天的耐心而变形,质量不可控;第三 ,团队里不同工程师写出来的提示风格迥异,模型面对的输入分布远比想象中复杂,微调无从下手。
Harness 的反方向:把意图预编码,让输入趋近于零
把同样的工作搬到 harness 里,体验是完全相反的。该示例实现用一个封装了 Claude Agent SDK 的 CLI(命令行工具)脚本,默认上下文窗口里已经内置了「我是缺陷分诊助手,我的 job to be done 是定位根因并产出结构化工件」这一整套系统级指令。工程师不再需要写提示,只需要在终端里执行:
$ triage --sentry https://app.sentry.io/issues/...
或者把 Sentry 链接作为命令行参数直接传进去,剩下的全部由 harness 完成:从链接里解析出 issue ID,自动拉取最新事件、堆栈、环境标签,组装上下文,再调用模型按既定流程跑调查。
[观察] 一条 Sentry 告警链接就是 harness 启动的完整输入。整个启动脚本只需要一行命令,输入成本趋近于零,人类工程师不再需要在提示词工程上花任何精力。
这一步的本质不是「少打字」,而是意图的预编码 。所有原本要写进提示的内容,都被提前烧结进了 harness 的系统提示词和适配器配置里。再往前走一步,如果告警本身是从 Slack 机器人或 Linear webhook 转发过来的,harness 甚至可以从消息里直接抓取链接,启动变成零输入。参考 Claude Agent SDK 的设计文档(code.claude.com/docs/en/age… 本身就把 system prompt、工具集、子代理注册做成了一等公民,正是为了让「预编码」这件事变得工程化、可复用。
三重收益:更高效、更一致、结果更好
把工作流从「每次重写提示」搬到「预编码进 harness」之后,构建者记录下三类可见收益。
更高效 :启动调查的边际成本从「写 200 到 400 字提示 + 等待模型理解」降到「贴一个链接 + 按回车」。在工单量大的团队,这种节省是按月按人按次叠加的。
更一致 :所有缺陷调查都走同一条预设路径——先拉证据,再做根因分析,最后产出一个三段式工件,落到同一个工件存储(artifact store)位置。无论谁来执行、何时执行,产出的格式、字段、命名约定都一致。通用工具做不到这一点,因为通用工具的「流程」只存在于人类的口头指令里,下一次会话就消失了。
结果更好 :当流程固定下来之后,每个环节都可以被独立打磨。证据收集阶段可以专门调优 Sentry API(docs.sentry.io)的查询参数,根因分析阶段可以针对 Sonnet 4.6 做提示优化,工件生成阶段可以接上 Linear API(linear.app/docs)自动开单,后… GitHub REST API(docs.github.com/en/rest)创建 issue。这种纵向打磨在通用工具里几乎不可能——通用工具的每一次会话都是一个孤岛,没有「持续改进」的位置。
[数据] 该示例实现的 harness 主体由大约 8 个职责单一的功能文件组成,改动任何一个环节都不需要触碰其它文件;而该团队平均每周要跑 50 到 80 次缺陷调查,这种「低成本试错」在没有 harness 的工作流里根本无法维持。
Skill 不是替代品:harness 才是执行保障
一种常见反对意见是:Claude Code 已经支持 skill,把调查流程写成 skill 文件、让模型在合适场景自动加载,也能做到类似效果。理论上没错,但工程上脆弱。
Skill 需要被正确触发。 模型加载哪个 skill、什么时候加载,本身是模型一次判断的结果。当输入只是「Sentry 告警炸了,看下」这种含糊措辞,模型可能加载了「缺陷分诊」skill,也可能加载了「一般编程帮助」skill;更糟的是,它可能两个都加载,然后在两套指令之间打转。
Skill 需要人盯着。 skill 本质是自然语言,模型可能「忘记」执行其中一步,也可能「创造性扩展」,去做流程里没有定义的事。在缺陷分诊这种「必须保持只调查、不修复」边界的场景,任何一步偏移都可能让模型直接动手改代码,harness 设定的 investigate-only 防线就此失效。
harness 的反制很直接:流程不是写在文本里被模型「阅读」,而是写在代码里被运行时「强制执行」。每一步是一个函数,函数调用顺序是写死的,任何越界都会被前置条件直接挡住。skill 解决的是「给模型更多上下文」,harness 解决的是「让流程成为系统的一部分」,两者根本不在同一层。
自由度:多模型路由与通用工具给不了的能力
自建 harness 还有一个通用工具几乎给不了的能力:多模型路由(multimodal routing,即根据任务类型动态选择最合适的模型)。同一个调查流程里,「从 Sentry 抓原始日志、组装成结构化 prompt」这类工程任务,可以交给确定性脚本;「从堆栈和最近代码变更里推断根因」这类需要语言理解的环节,可以走 Claude Sonnet 4.6;「对长篇工件做语言润色、给非技术 stakeholder 写摘要」又可以切到另一个模型做风格迁移。
[观察] 在该示例实现的早期开发中,GPT-5.5 与 Claude Opus 一开始都抗拒在 harness 里放入 AI 参与的环节,倾向于把所有逻辑都写成纯确定性的脚本,理由是「AI 不稳定」。构建者坚持保留了「意图归因」这一关键步骤由 AI 完成,后续证明这是整套 harness 价值最集中的地方——确定性能做的部分不该让 AI 抢,而确定性感知的部分则不该硬塞回脚本。
这种「同一工作流、不同环节、不同模型」的组合,通用编码助手做不到。Claude Code(docs.anthropic.com/en/docs/cla… Codex(openai.com/index/intro… harness 借助 Claude Agent SDK、OpenAI Agents SDK(openai.github.io/openai-agen… HTTP 调用,可以做到这种程度的编排,而代价仅仅是多写几十行胶水代码。
小结
把意图预编码进系统,本质上是把「人类每次重新讲清楚」转成「机器每次自动执行」。harness 不只是省时间,它把工作流的可靠性、可改进性、可分发性一并解决了。当一个流程要跑几十次、上百次,值得为它写一套 harness;当一个流程只跑一两次,直接打开 Claude Code 就够了。这条分界线比任何具体技术细节都更值得工程团队记住——它决定了哪些工作流值得固化,也决定了哪些工作流留在通用工具里反而更轻。
权限与工具策略:investigate-only 的闸门设计
一、harness 里能做的事:把约束写进代码层
先讲清楚一件事:在 Claude Agent SDK、OpenAI Agents SDK 这类 SDK 上搭 harness,并不是简单地把工具调用封装一层壳。harness 真正能做的是对「允许调用哪些工具、允许执行什么」做出非常规定性 (prescriptive) 的限制——这种限制不是写在 system prompt 里让模型「自觉遵守」,而是写在适配器、tool filter、policy gate 这些代码模块里,从工具描述暴露阶段就开始收紧。
具体落地时,harness 一般会做三件事:第一,在工具注册环节就过滤掉敏感工具(如 Edit、Write、Bash 中危险子集),只暴露 Read、Grep、Glob 这类只读工具;第二,在每次工具调用前增加一层 policy gate,根据当前 run 的 mode 字段判断是否放行;第三,把每一次工具调用都记进审计日志,事后回溯谁能跑、跑了什么、在哪个工件上产生结果。
这种方式有一个被低估的好处:即使底层模型换掉、prompt 重写、上下文截断,权限策略仍然成立,因为它不依赖模型的「记忆」或「自觉」。这也是 Anthropic Claude Code 文档(docs.anthropic.com/en/docs/cla…
二、investigate-only:为什么调查型运行必须只读
[观察] 在该示例实现里,几乎所有「分诊型」run 都默认跑 investigate-only 模式:即便用户粘进来的是一条 Sentry 告警、Linear ticket、GitHub issue,模型拿到手也只能调用只读工具做证据收集(读日志、查 commit、看 stack trace、抓 trace sample),然后把根因写到工件存储里;它全程不触碰源文件、不改输入、不发任何外部消息。
这种设计的工程动机非常朴素:调查阶段的失败成本应该尽量低,而成功收益又必须尽量显式化。如果让模型在「读证据」的同一步就能改源码,几件事会立刻出问题——一是 diff 没法隔离,你看到的修改里哪些是模型推理结果、哪些是用户原意,根本分不清;二是回滚困难,Agent 在调查中途误改了一个 import,下游所有根因判断都会建立在错误前提上;三是责任归属不清,客户会问「这段代码是你们改的还是 AI 改的」,答不上来。
所以 investigate-only 的核心是「让模型先证明它看懂了问题」:跑完只能产出一份带证据链的根因报告(谁触发、什么 stack、哪一段代码、为什么这样),人类审过之后,再决定要不要进入下一步——开工单还是直接打补丁。在 Sentry 文档(docs.sentry.io)的事件视图与 Linear 文档(linear.app/docs)的工单流转模… on-call 流程天然契合。
[数据] 这套实践的 harness 主体只有约 8 个职责单一的功能文件,其中有 3 个直接服务于权限与工具策略(一个工具注册层、一个 policy gate、一个审计日志器),其余 5 个负责适配器、工件存储、交互面与 run 生命周期。换句话说,接近一半的代码量都在管「谁能在什么时候动什么」这件事。
三、敏感动作挂旗:flag 机制的设计
光做「默认只读」还不够。harness 还会把所有「会改变外部状态」的工具调用都挂上一个 flag——本节用 flag 这个术语,指代「待人工显式批准的提案」,不是命令行工具的 --flag。这个概念在 Claude Agent SDK 的 permission_mode 与 allowedTools 配置里都有官方背书(code.claude.com/docs/en/age… MCP 协议层则进一步演化为 server 级别的 capability negotiation(modelcontextprotocol.io)。
具体哪些动作会被挂 flag?在该示例实现里至少有四类:第一,编辑源码(Edit、Write、MultiEdit);第二,修改输入,包括改 Sentry 上的 issue 状态、改 Linear 上的 ticket 字段、关 GitHub issue;第三,对外发消息,例如在 Slack 里 ping 某个 owner、在 GitHub PR 上 @reviewer;第四,触发任何 CI 动作,比如重新跑 Vercel 部署、关停某个监控告警。
挂 flag 的流程很轻量:模型在 run 中尝试调用某个高敏工具,harness 不直接拒绝,而是把这个调用连同参数、上下文、人工可读的说明一起塞进一个待办队列;UI 层(Ink 终端界面,见 github.com/vadimdemede… y / n / edit 三键决策——y 放行,n 拒绝,edit 允许改参数后再放行。整个过程模型是停在那里的,不会偷偷继续往下跑。伪代码大致是:
async function executeToolCall(call, run) {
if (policyGate.isSensitive(call.tool)) {
const proposal = await flagQueue.enqueue({
tool: call.tool,
args: call.args,
reasoning: call.explanation,
runId: run.id,
});
return waitForHumanDecision(proposal);
}
return await underlyingTool(call);
}
这背后其实是一种「慢权限」(slow permission)的工程哲学:有些动作的成本是不可逆的(对外发消息、改 issue 状态),这些就必须人工显式批准;而读取证据、查日志这种完全可逆、零外部副作用的动作,可以全自动放行。harness 的工作就是把这条线画清楚。
四、对比通用工具:Prompt 重述 vs 代码级强制
如果不用 harness,直接打开 Claude Code 或 Codex 的会话框粘一条 Sentry 链接,会发生什么?你需要做一件非常烦但又不得不做的事:每一次新会话、每一次重要动作前,都要在 prompt 里重申「请不要改文件」「请不要修改 Sentry」「请你只调查」。OpenAI 介绍 Codex 的官方文档(openai.com/index/intro…
为什么这件事很烦?因为模型不一定记得。即便你写在 system prompt 里,只要上下文窗口被截断、新对话重开、模型换了一个 checkpoint,这条约束就可能「忘了」。更糟的是,某些场景下模型会把「帮我修一下」误解成「帮我改代码」,等你回头看,源文件已经被改了——而你原本只想要一份根因报告。
harness 的解法是把这些约束从「自然语言提醒」下沉到「代码级强制」。在 Claude Agent SDK 里,你可以通过 tool filter 把 Edit、Write 整个从工具列表里摘掉,模型在那个 run 里「看到」的可用工具就只有 Read、Grep、Glob 这种;在 MCP 这层(Model Context Protocol),更可以做 server 级别的 allowlist,直接告诉上游 LLM「这个 server 只暴露只读能力」。这种限制是写在代码里的,不是写在 prompt 里的,所以不存在「模型忘了」这回事。
| 维度 | 通用工具 (Claude Code / Codex) | harness (Claude Agent SDK + policy gate) |
|---|---|---|
| 约束位置 | system prompt / 用户消息 | 工具注册层 + policy gate 代码 |
| 约束强度 | 软约束,模型可能违反 | 硬约束,模型根本看不到敏感工具 |
| 失效场景 | 上下文截断、模型升级、新会话 | 无,除非代码被改 |
| 审计能力 | 仅有 LLM 调用日志 | 工具级结构化日志,可重放 |
| 误操作回滚 | 依赖 git / 手工 undo | 默认禁止,触发即挂 flag |
五、权限矩阵:随信任增长渐进放开
最后一个关键设计是「权限随信任渐进放开」。该示例实现采用三段式信任模型:
| 阶段 | 触发条件 | 允许的工具 | 允许的动作 |
|---|---|---|---|
| L0 调查 | 默认入口 | Read / Grep / Glob / 只读 MCP |
收集证据、写根因报告到工件存储 |
| L1 开工单 | 根因被人工确认 | + Linear.create_issue / GitHub.create_issue |
创建/更新 ticket、写修复建议 |
| L2 打补丁 | 多次 L1 成功 + 团队授权 | + Edit / Write / Bash 受控子集 |
在分支上提交 diff、触发 PR |
这套矩阵的本质是把权限当作「可累积的信用」:一个全新 harness、一条全新告警,模型只能跑 L0;当它在过去 N 次 L0 run 里都产出了被人工采纳的根因报告、且没有被否决过,harness 才会把 L1 工具暴露出来;L2 同理,通常要求至少一次 PR 合入到主干、并经过代码 review(GitHub REST API 文档 docs.github.com/en/rest 里对 PR 生命周期与 review 状态机有完整描述,可与 harness 的 trust score 对接)。
这种渐进式设计带来的工程好处是双重的:一是单次失败的爆炸半径被锁死在 L0,不会因为某次调查就把生产环境搞乱;二是团队对 AI 的信任建立在「被验证过的成功」之上,而不是「模型说它能做到」。后者在生产环境里几乎是不存在的——你需要的是可被审计的信用记录,而不是 demo 上的花活。
[观察] 现场实践里,这套三段式信任模型通常配一个 trust_score 字段挂在工件存储上,每次 run 结束后根据「根因采纳率」「PR 合入率」「误报率」自动更新,score 跨过某个阈值才解锁下一档工具。这种「以代码强制取代口头承诺」的渐进放开,是 harness 与通用工具最本质的差别——通用工具永远停在「默认全开或默认全关」两个极端,harness 才有中间地带。
权限与工具策略是 harness 工程里最容易被低估、但也是区分「玩具 demo」与「生产系统」的那一道分水岭。investigate-only 是入口、flag 机制是闸门、信任矩阵是节奏——三者合在一起,才能让 AI 在不失控的前提下发挥能力。把约束从 prompt 搬进代码,看似只是一行注册代码的差别,实际上是「我希望模型自觉」与「我确保模型只能这样做」两种工程哲学的分歧。选哪条路,决定了当某次跑飞时,你是解释「模型理解错了意图」还是解释「我们的 policy gate 为什么没拦下」——后者,才是可以修复的故障,也才是工程师能睡得着觉的系统。
界面层:Harness 是包含人机体验的完整系统
TUI 作为交互面:案例实现的形态选择
该示例实现的 harness 把交互面定为终端 UI(TUI),底层用的是 Ink 终端 UI 库——这是一个用 React 风格声明式组件来构建命令行界面的库,本质上是把 React 跑在 Node.js 上,再用 ANSI 转义序列把组件渲染到终端。挑 Ink 而不是直接拼 chalk 和 readline,是因为 harness 内部已经按组件分好了视图(历史列表、活动流、工件面板),Ink 的声明式写法跟这套结构天然对齐,几乎不用做额外的胶水代码。
[观察] 现场不到 30 分钟就构建出第一版 TUI:把 harness 的几个内部 channel 映射成 React 组件即可,中间只补了一个简单的滚动到底部逻辑和一组快捷键绑定,其余时间都花在字段命名上。
$ npx bug-harness investigate --issue SENTRY-1234
这条命令做三件事:从 Sentry 拉取 issue 元数据作为种子上下文、用 Claude Agent SDK 起一次 run、把 harness 内部的三个事件流推送到 Ink 组件上。开发者在终端里看到的不再是一坨吐到 stdout 的 raw 日志,而是一个有侧栏、有主面板、有快捷键的 IDE-like 工作台。终端的复用率因此变得非常高——同一块屏幕里既跑 git、又跑 harness、再切到 vi,工作流不被切碎。
TUI 结构直接映射 harness 内部结构
harness 内部其实有三段式的事件流,Ink 的界面把这三段可视化地呈现出来。下面这张表是 harness 内部 channel 与 TUI 组件的对应关系,也是构建 UI 时第一份要画的图:
| TUI 区域 | harness 内部对应 | 触发时机 |
|---|---|---|
| 顶部侧栏:历史运行列表 | run store 中的所有 run 元数据 | 启动时一次性拉取 |
| 主面板上半:错误与修复记录 | evidence 收集 + 修复尝试的累积日志 | run 进行中持续追加 |
| 主面板下半:活动流 | agent 当前正在调用的 tool / adapter | 实时刷新 |
| 底部抽屉:工件产出 | artifact store 中的最终产物 | run 结束或中途可展开 |
[数据] 该 Sentry 告警影响约 150 名用户、仍在每小时发生——这条来自 harness 主动拉取的 Sentry 元数据,直接显示在 TUI 顶部的 issue 卡片里,调查者一眼就能判断要不要立刻升级,而不是先翻一遍原始 JSON。
[观察] 调查运行选 I (investigate-only) 而非 F (full fix),全程不触碰源文件——TUI 在这个模式下会主动隐藏任何写入本地仓库的 action,把视觉重心完全放在证据与推理上,这也是 harness「只调查不修复」语义的视觉体现。
把界面结构和 harness 内部结构对齐有一个隐性好处:任何 harness 的内部重构,只要 event channel 名称不变,TUI 就不用动。这是一种 contract-first 的设计——Ink 组件订阅的是稳定的事件契约,不是某个临时性的函数返回值。换言之,TUI 是 harness 的「皮肤」,只要皮肤下面流血的血管没改名,皮肤就可以一直跟着版本演进。
界面不是必须是 TUI:harness 定义里不限定形态
值得特别强调的是,harness 的定义里并没有规定交互形态必须是 TUI。同一个 harness 内核(Claude Agent SDK + 适配器 + 政策闸 + 工件存储),完全可以套上三种不同的壳:
- 纯命令行 (CLI) :所有信息以结构化 JSON 打印到 stdout,适合被其他脚本接进 CI / GitHub Actions,见 GitHub REST API 文档 中关于 job 日志格式的规范。
- Web 应用 :把同样的事件流通过 WebSocket 推到一个 React 前端,团队多人可以同时围观一次 run,适合事后复盘。
- 无界面 / 后台模式 :harness 被嵌进定时任务,自己跑、自己存工件、只在出错时发一条 Slack,适合夜间巡检。
这一层灵活性是 Claude Agent SDK 的设计选择决定的——SDK 本身只暴露 Python / TypeScript 的 API,不绑定任何 UI。所有「长什么样」的决定权,都留给了 harness 构建者。类似的取舍也体现在 OpenAI Agents SDK 上,它们都把交互形态视为上层应用的责任。
自建 harness 意味着自建交互面
既然交互形态不被 SDK 锁定,那「好不好用」就完全取决于 harness 自己。这一节要传达的核心观点是:界面体验本身也是 harness 的一部分 ,不是事后打补丁。具体怎么落实,可以从下面几个维度入手:
- 可读性优于原始性 :不要把 tool 的原始 JSON dump 给用户看,而要做一层渲染。比如适配器返回的 Sentry 事件,可以直接渲染成「影响用户数 / 首次出现时间 / 最近一次发生时间」的三栏卡片。
- 可恢复优于一次性 :长跑任务(超过 5 分钟)在 TUI 里要支持 Ctrl+C 中断后从最近一个工件 resume,而不是从头来。
- 可审计优于黑盒 :每一次 tool 调用、每一次 policy 闸的拦截决策,都要在 TUI 上留下可点开的痕迹,方便事后复盘,这也是与 Model Context Protocol 中 trace 规范精神对齐的做法。
[观察] 粘贴一条 Sentry 链接即可启动,无需再写 dear agent 式提示——这条体验是把「启动摩擦」压到最低的典型例子,harness 在 TUI 启动时主动解析剪贴板里的 URL,自动填入 issue ID,用户感知不到这一步的存在。
内置命令行入口:flag 驱动的轻量调查
除 TUI 外,harness 还内置了一条直接对特定 issue 跑调查的命令行入口。这条入口的设计动机很朴素:不是每一次调查都值得起 TUI 。比如半夜 CI 挂了,只想看根因、不想交互,这时 TUI 反而是累赘。
$ bug-harness triage --issue SENTRY-1234 --only evidence
$ bug-harness investigate --issue SENTRY-1234 --mode root-cause
$ bug-harness investigate --issue SENTRY-1234 --out ./reports/2026-q1
这条 CLI 入口有几个细节值得展开:
--only evidence:跳过任何会调用 GitHub / Vercel 适配器的步骤,只跑 Sentry + Linear 的读取动作,把网络攻击面压到最小,适合在网络受限环境跑。--mode root-cause:在 harness 内部对应investigate-only的语义档位,跟 TUI 里的 I 模式同源,确保两边行为一致,不会出现「GUI 跑出来一种结论、CLI 跑出来另一种结论」的尴尬。--out:把工件显式导出到本地路径,方便后续人工编辑或交给别的工具链,这也是「artifact store 是可寻址文件系统」这一原则的具体落实。
[数据] harness 主体只有约 8 个职责单一的功能文件——CLI 入口是其中之一,体积不到 200 行,因为它复用了 TUI 下面同一套 event bus,只是把渲染层换成了 stdout,这也是为什么 harness 主体能保持小而清晰的关键。
工程踩坑清单
把 harness 接入真实终端时,有几个反复出现的坑值得预先列出,免得新构建者重复踩:
| 坑位 | 现象 | 解法 |
|---|---|---|
| 终端宽度自适应 | 长 JSON 在 80 列终端里被截断 | Ink 用 useStdout().stdout.columns 拿实时宽度,组件按列数切换布局 |
| 颜色在 CI 环境失效 | NO_COLOR=1 时 ANSI 序列变成乱码 |
检测到 env 变量后自动降级到纯文本布局 |
| 流式输出与 React 状态冲突 | Ink 重渲染把流式 token 冲掉 | 用 ref 直接写 ANSI,绕过 React reconciler |
| 快捷键与 readline 冲突 | Ctrl+C 既要复制又要中断 | 用 Ink 的 useInput 显式声明每个键的语义 |
| 大体积工件卡住渲染 | 万行级日志一次性渲染导致帧率掉到 1 fps | 引入虚拟列表,只渲染可视区域内的行 |
收尾:从皮肤到系统
界面层是 harness 最容易被低估的部分,但它直接决定了开发者愿不愿意每天打开 harness。TUI 不是唯一选项,但它确实是最贴近开发者日常工具链的一种形态——同一个进程里既跑 git、又能跑 harness、再切到 vi,工作流不被切碎。
harness 的真正价值从来不在于它调用了多强的模型,而在于它把模型、工具、政策、界面四件事缝成了一个能被工程师日常使用的完整系统。界面层看似只是「皮肤」,其实是这套系统能不能落到真实工作流里的最后一道闸门——皮肤做得对,工程师才会把它当作同事;做得不对,它就只是又一个没人愿意敲的命令。
运行模型:Run、Task、Flag 的三层架构
要把 harness 写得能扛生产环境,先要把执行模型想清楚。该示例实现里每一次唤起 harness 都叫一次 run ——它是不可再分的原子单位,要么完整跑完、要么被打断留下明确的失败边界。一个 run 只做一件 task,task 的具体形态由输入决定:在本案例里绝大多数 task 都是「调查一条 Sentry issue」,也可能扩展到 Linear ticket、GitHub PR 评论或 Vercel 部署事件。这种一一绑定让上下文不漂移,所有中间推理、检索到的证据、最终结论都挂在同一个 run 的 ID 上,便于事后回查,也为后续审计提供了天然的最小切片。
Sentry issue 本身就是一个信息密度极高的对象:事件 ID、影响用户数、堆栈回溯、面包屑(Breadcrumbs,即系统按时间顺序记录的近期操作轨迹)、环境标签、首次与最近发生时间。该 harness 的 task 直接把 issue payload 喂进 Claude Agent SDK 的工作流定义(详见 Claude Agent SDK 文档),而不是让模型先去「猜该读什么字段」。这种「窄而深的输入」做法大幅压缩了无关变量,根因分析阶段几乎不会出现「模型跑去查不相关的 issue」这种幻觉。[观察] 在早期 prototype 中,构建者曾尝试给 task 留一个 free-form 的自然语言描述入口,结果发现模型总在描述里夹带私货——比如把「查一下这个 bug」自己润色成「查一下支付模块相关的所有 bug」,导致 task 范围爆炸;改成「粘贴一条 Sentry 链接即可启动,无需再写 dear agent 式提示」之后,工作流的边界才真正稳下来。
把 task 写成「只能从一个外部 issue 链接开始」,看似剥夺了灵活性,实则是为后续的 flag 机制铺路。Flag 是挂在 harness 顶层的开关 ,控制所有真正危险的能力:能否编辑源码、能否修改 issue 描述、能否给客户发邮件、能否提 PR、能否合并分支。这些 flag 默认全部关闭——这是该示例实现最鲜明的立场之一,也是它在生产环境里没有翻车的根本原因。[观察] 调查运行选 I(Investigate-only,即「只调查不修复」)而非 F(Fix,即直接修复),从启动到收尾全程不触碰源文件:不会改一行代码,不会动任何配置,只会往工件存储里写报告。这种「默认拒绝」的姿态对应了 harness 设计上的最小权限原则(principle of least authority,简称 POLP,即「只授予当前任务必需的最小权限」),让 Agent 不会在失控时顺手指坏线上服务。
只有当用户在工作流里显式切换到「修复」模式,相关 flag 才会被打开,而且打开的范围严格限定在某个特定 run 内。Run 结束,flag 状态归零,即便同一个 session 重新启动也看不到上一次的授权痕迹。这种「能力随 run 而生、随 run 而灭」的模式,跟传统服务里长期存活的 API token 是完全不同的安全姿态——后者一旦泄露就持续暴露,前者则把暴露窗口压到「一次 run 的执行时间」以内。[数据] 现场不到 30 分钟构建出第一版能跑通的 harness,关键就在于先把这套 flag 矩阵定下来,而不是先写一堆看似有用的能力;这种「先收紧、再按需放开」的工程顺序,在大体量系统里也通用。
flag 矩阵的形态可以用下面这种伪配置片段表达,强调「默认关闭、按需开启、随 run 失效」:
const defaultFlags = {
editSource: false,
mutateIssue: false,
contactCustomer: false,
openPR: false,
mergeBranch: false,
};
function startRun(task: Task): Run {
const flags = { ...defaultFlags };
return new Run({ task, flags, ttl: 'single-run' });
}
三层架构是这套设计在代码层面的直接体现:最前端的交互面(TUI 或 CLI,见上一节对 Ink 的讨论)只负责渲染和收集用户输入;中间的 harness core 负责 run 的生命周期、flag 的权限裁决、工作流的调度;最外层是针对 Sentry、Linear、GitHub、Vercel 的适配器 ,每个适配器只懂一个外部系统的 API 边界,把它们的能力收敛成 harness 可消费的原语。Core 永远不直接调外部 HTTP,所有跨系统动作都必须经过适配器——这意味着换掉 Sentry 接入 PagerDuty,只需要重写一个适配器文件,core 一行不用动。
职责分层带来的副作用是工作流定义可以变得很「声明式」。下表是任务编排层面一个典型的运行切片,可以看到 run、task、flag 三者如何对齐:
| 层级 | 关键对象 | 默认状态 | 越权代价 |
|---|---|---|---|
| Run | 执行单元,挂载 task 与 flag 集合 | 每次启动全新建 | 工件存储膨胀 |
| Task | 绑定一个外部 issue/payload | 由 Run 携带进入 | 上下文污染 |
| Flag | 控制源文件、消息、PR 等高风险动作 | 全部关闭 | 线上事故 |
| 工件 | 证据、推理轨迹、最终报告 | 自动归档 | 审计可追溯性 |
「可审计、可回放」是 run/task 模型能上生产的根本理由。所有从模型吐出的消息、调用过的工具、读到的源码片段、生成的草稿报告,统统沉到工件存储(artifact store,这里指一个按 run ID 组织的本地目录加远端对象存储)。当某次调查结论被质疑,工程师可以拉出那次 run 的完整 trace,从第一条用户输入看到最后一条模型输出,逐 token 复盘哪一步引入了偏差。[观察] 在 GPT-5.5 与 Claude Opus 的早期对照实验里,两个模型起初都抗拒在 harness 里加入「AI 辅助二次审阅」这个环节,倾向纯确定性的规则判断;最终是通过把审阅意见也作为工件落到存储里、形成可对比的 diff,才让审阅步骤本身也具备可审计性——这反过来巩固了 run/task 模型。配合 Sentry 官方文档对事件模型本身的可追溯约定,这种从外部系统一路贯通到模型输出的链路才算真正闭合。
为了让审计不至于变成体力活,harness 在架构层面有意保持小而清晰。整个工程的目录结构是一个高层入口(负责装配 core、加载 flag、挂载适配器)加上大约 8 个职责单一的功能文件,分别对应 run 调度、task 解析、flag 矩阵、工件写入、各外部系统适配器。真正复杂的逻辑——比如 Sentry 的事件解析、Linear 的项目状态机、GitHub 的 PR diff 重组——都被压在适配器和工作流定义文件里,core 始终维持「薄」的姿态。这种克制让新成员上手时间以天计而不是以周计,任何想改核心逻辑的冲动都会先撞上「应该改适配器」的常识。[数据] 整个 harness 主体只有约 8 个功能文件,但已经能覆盖 Sentry 告警→证据收集→根因草稿→Linear 联动→GitHub 候选修复 PR 的完整链路;复杂度被外推到了 5 个立场鲜明的适配器中,core 本身维持在一千行可读的规模以内。
收尾来看,run/task/flag 三层架构并不神秘,它只是在工程层面对「一次执行、一个职责、一组权限」这三个朴素概念做了命名。真正决定这套设计能不能在团队里立住脚的,不是概念本身有多漂亮,而是 flag 默认全关的克制、工件全程归档的纪律、以及把复杂度压到适配器和工作流定义里的工程美学。当一个缺陷调查 harness 同时满足「可审计、可回放、可拒权」这三个性质时,它才真正从「会写 prompt 的脚本」跨进了「能在生产环境跑的服务」的行列。
核心引擎:Claude Agent SDK 承担全部智能体规划
把规划与循环交给现成的 SDK
把 harness 写得像生产环境能用的东西,关键不在于多花哨,而在于「别重复发明轮子」。该示例实现的 harness core 跑在 Claude Agent SDK 上,所有 agentic planning——也就是规划、循环、工具调度——全部由 SDK 完成,harness 自己只负责输入装配、上下文约束、立场鲜明的适配器和结构化工件。这是一种典型的「薄壳厚底」分层:薄壳是 harness,厚底是 Agent SDK。
为什么不做自己造一个 agent loop?因为 Agent SDK 已经把 Claude Code 那套底层原语做了封装:读文件、写文件、执行 shell 命令、抓取网络内容,这些能力开箱即用,不必自己造轮子(详见 Claude Agent SDK 文档 code.claude.com/docs/en/age… 计费这些长尾问题——把这些事交给官方 SDK,构建者只需要把精力放在「如何让智能体在 Sentry 调查这个 job 上更高效」。
一次调查,一次 session
每次调查都启动一个 Claude SDK session,session 内部的生命周期和 harness 的 run 严格一一对应。在 session 里,harness 先把从 Sentry 适配器拿到的证据(堆栈、面包屑、tag、上下文)组装成结构化 prompt,然后让 SDK 拉起智能体循环:模型读证据、形成根因假设、必要时调用工具补充信息(比如再抓一次相关 commit、查一个内部文档),最后输出结构化工件。整个过程都在同一个 session 内,token 上下文、工具结果、中间推理痕迹都不会泄漏到下一个 run。
[观察]这种 session-per-run 的设计带来一个隐含好处:如果调查被中途打断(比如用户按了 Ctrl+C,或者 harness 收到 SIGTERM),只要 session ID 还在,就能从中断点继续;反之,如果跑完发现结论不对,也能调出 session 的完整 transcript 做反查。对生产事故复盘来说,这比一次性黑盒脚本友好得多。
模型选择是工程决策,不是模型崇拜
很多人一上来就想「我得用最大的那个模型」。该示例实现恰恰相反:它选用 Claude Sonnet 4.6,理由是成本与能力的平衡——Sentry 调查任务以「读懂堆栈 + 形成假设 + 调用少量工具」为主,并不需要顶级模型那种长链条推理能力。Sonnet 4.6 在这类中等复杂度的工程任务上既快又便宜,跑一次调查的 token 成本能压到比 Opus 低一个数量级,而结论质量在分诊场景里基本无差。
[数据]以工程经验粗估,Sonnet 4.6 与 Opus 在 Sentry 调查这类任务上的根因命中率差距通常在 5% 以内,但单次调用成本差距往往是数倍——这意味着跑 1000 次调查的预算差距,可能就够团队再招一个工程师。把「无脑用最大模型」换成「按 job 选模型」,是 harness 工程化最该养成的习惯之一。Anthropic 官方在 Claude Code 文档 docs.anthropic.com/en/docs/cla… 中也明确推荐按任务类型挑选模型,而非一刀切用最贵的那个。harness 层面要做的事,只是把模型选择做成配置项,允许不同类型的 run 用不同模型。
SDK 暴露的能力面,够不够用?
Claude Agent SDK 提供的工具集大致覆盖了智能体所需的大部分原子操作,常见能力清单如下:
| 能力类别 | SDK 提供方式 | harness 是否需要补充 |
|---|---|---|
| 文件读 / 写 | 内置工具 | 否 |
| Shell 执行 | 内置工具,沙箱可配 | 否 |
| Web 抓取 | 内置工具 | 否 |
| MCP(Model Context Protocol) 接入 | SDK 原生支持 | 按需挂载 |
| 适配器调用(Linear / GitHub / Vercel) | 由 harness 自定义 | 是 |
| 工件写回 artifact store | 由 harness 自定义 | 是 |
| 立场 / 偏见注入 | 由 harness 在 system prompt 中完成 | 是 |
可以看到,harness 自己真正需要新写的代码其实很少——大部分工作量都在适配器层和工件落库层,这两块恰好是「与具体业务强绑定、无法被 SDK 抽象」的部分。把这两块做好,harness 才有差异化价值;否则就会变成「又一个 prompt 调参工具」。
[观察]GPT-5.5 与 Claude Opus 在该 harness 设计早期都曾「抗拒在 harness 里放 AI 环节」,倾向用纯确定性脚本完成 Sentry 调查。这个反馈后来反过来印证了 AI 环节的必要性:确定性脚本无法解释模糊证据之间的关联,而智能体恰恰擅长这种发散联想。关键在于把 AI 环节关进 SDK 提供的受控沙箱里,而不是裸露在业务代码里。
自研 harness 的推荐路径
如果你打算自己写一个 harness,无论目标是 Sentry 调查、Linear ticket 处理,还是别的领域,推荐路径都是同一套:
- 先选 Agent SDK :Claude Agent SDK 或 OpenAI Agents SDK(openai.github.io/openai-agen… agentic loop 的脏活累活揽下来,SDK 之外的世界差异不大。
- 让 SDK 接管智能体循环 :规划、工具调度、上下文管理全部交给 SDK,harness 不再自己写
while (...) { await llm.call() }这种循环,也不会再去操心 token 计费、上下文裁剪、超时重试这些边角细节。 - harness 自己专注三件事 :工作流语义(把一次 job 拆成哪些步骤)、立场鲜明的适配器(让输入输出符合具体工具的规矩)、结构化工件(让结论可被下游消费)。
- 模型选择做成配置 :不要写死在代码里,允许按 run 类型切换 Sonnet / Opus 或 GPT 系列。一个简短的配置示例长这样:
runs:
sentry-investigate:
sdk: claude-agent
model: claude-sonnet-4.6
max_tool_calls: 8
timeout_minutes: 10
linear-triage:
sdk: claude-agent
model: claude-sonnet-4.6
max_tool_calls: 4
timeout_minutes: 5
- 把工具调用关进沙箱 :不论是文件系统访问还是外网请求,都按 job 设定最小权限,事故才不会从 harness 里漏出去。Sentry 调查任务的默认沙箱应该禁止
rm、git push、对外网写操作这类危险动作,只允许读文件和调只读 API。
这套路径的核心思想是:Agent SDK 是地基,harness 是装修——地基越稳,装修越自由。把智能体循环交给 SDK,构建者才能把全部精力放在「如何让 harness 在这个具体 job 上更高效」,而不是在 prompt 调优和上下文裁剪之间反复消耗。
工程踩坑清单
最后,几个真实会踩的坑,值得提前知道:
- 不要把 system prompt 写得太长 :SDK 会把 system prompt 和用户消息一起塞进上下文窗口,过长会挤占证据空间。System prompt 应该只写「立场、约束、输出格式」,证据留在用户消息里。
- 工具结果要做截断 :SDK 调工具拿回的结果(比如一个超大 JSON 或一个长日志文件)可能瞬间撑爆上下文。harness 这一层要在写入 session 之前做长度裁剪,比如「截前 200 行 + 末 50 行」。
- session 与 run 的对应关系要写进 artifact store :不要只依赖 SDK 的 session ID(那是 SDK 内部的标识),harness 自己要维护一份
run_id ↔ session_id映射,后续审计、回放才能找得到入口。 - 超时和重试是 harness 的责任 :SDK 不会替你决定「这次调查跑了 10 分钟还没出结论,该杀掉了」。这条规则必须写在 harness 层,而不是依赖 SDK 默认行为。
- 调试时打印 session transcript :本地开发时,把 SDK 的中间推理痕迹打印到 stderr,定位 prompt 问题比读最终输出快得多。
把规划与循环交给 Claude Agent SDK,本质上是一种「买能力不买复杂度」的工程取舍。Anthropic 投入了大量工程资源把这套 SDK 打磨到能扛生产,harness 自己只需要在 job 语义层做差异化。这种分工一旦清晰,缺陷调查、自动分诊、工件生成这些 AI 自动化场景就不再是「demo 能跑、生产不敢上」的状态,而是真正可以铺到团队日常的工具流——也就是下一步要展开的「交互面与可观测性」要回答的问题。
工件存储:把每次运行的证据沉淀成文件系统真相源
为什么 harness 必须拥有自己的工件存储
在讨论缺陷调查 harness 时,一个常被低估但工程上至关重要的设计是工件存储(artifact store) 。所谓工件,指的是 harness 在每一次运行(run)期间产出的一切副产物——消息流、工具调用结果、日志、worker 实际写入的动作、最终摘要。它们不是「顺便打印一下」的调试输出,而是被刻意序列化、落到磁盘、并按可预测的目录结构组织的文件。这种模式并非该示例实现独创,在 LangChain 的 LangSmith trace、OpenAI Agents SDK 的 trace export(参见 openai.github.io/openai-agen… Anthropic 自己的 Claude Agent SDK(参见 code.claude.com/docs/en/age… Agent 自己产出的东西成为系统下一轮决策的依据**,而不是仅作为人类事后查阅的屏幕滚动条。
设计动机:上下文不再一次性蒸发
[观察]该示例实现的调查运行选 Investigate-only(I)而非 Fix(F),全程不触碰源文件,这意味着 harness 没有"修改代码 → 看 diff → 再解释"这种正向反馈可以依赖;它只能把"我刚才看到了什么"沉淀下来,留给下一轮自己读。
如果不引入工件存储,缺陷调查类 harness 会陷入一个典型的工程灾难:Agent 在第 11 步调用 Sentry API 拿到的事件,到了第 12 步就已经被截断、丢弃或被压缩成一句含糊的"看起来是 N+1 查询" 。这种上下文蒸发迫使 Agent 在每一轮都重新拉取外部数据,既慢又贵,还会在 API 限流时直接卡死。工件存储的存在让 Agent 形成一种短期记忆外化的能力 :窗口里的对话是工作记忆,磁盘上的工件束是长期记忆。
[数据]该示例实现的 harness 主体由约 8 个职责单一的功能文件 组成,其中至少 3 个文件(read-only adapters、artifact writer、HTML reporter)的存在意义就是"把证据落到文件系统",说明工件存储不是附属功能,而是 harness 的脊柱。
工件束(artifact bundle)的内部结构
每一次 task run 结束时,harness 会在一个以 runs/<run-id>/ 为根的目录下写入一组文件,典型的工件束包含以下构件:
| 文件名 | 内容 | 用途 |
|---|---|---|
messages.jsonl |
task run 全部消息(用户、Assistant、tool result) | 完整回放会话流 |
investigation.md |
Agent 整理的调查报告(症状、假设、证据、结论) | 跨 run 复用 |
tool_calls.log |
worker 实际触发的工具调用与原始响应 | 审计与回溯 |
actions.json |
worker 在 Sentry/Linear/GitHub 等系统写入的副作用 | 区分"读"与"写" |
summary.txt |
一句话输出摘要,供下次 run 直接引用 | 上下文接力 |
关键设计点是"原始 + 摘要"双层结构 :原始证据(message、log、API response)留给机器重读,摘要(investigation.md、summary.txt)留给 Agent 在下一轮的 system prompt 里直接当 source of truth 引用。两层都用同一种写入策略——同步落盘,而不是异步刷写,避免进程崩溃时出现"事件已经发生但磁盘上没有"的撕裂状态。
一份给人类浏览的 HTML 报告
机器友好并不等于人类友好。该示例实现额外输出两份可读性强的产物:
- HTML 报告 —— 把
messages.jsonl渲染成分节、带折叠区、附带 Sentry 链接与 Linear issue 直通按钮的静态页面。工程师可以在事后 5 分钟内点开一份 run,从头到尾看完"Agent 当时看见了什么、为什么这么判断、最终写入了哪些工件"。 - worker report —— 仅记录 worker(子 Agent 或工具调用)的实际动作,过滤掉所有思考过程的噪声,只留下"它读了哪个 Sentry issue、它在 Linear 上创建了什么 ticket、它在 GitHub 上评论了哪条 PR"。
[观察]粘贴一条 Sentry 链接即可启动 harness,无需再写"dear agent"式提示——这背后的隐性前提是,Agent 必须能在自己上一轮的工件里立刻认出"这次该读哪些文件" ,否则启动成本会从 0 涨到 5 分钟,实战中没人愿意用。
HTML 报告的存在还顺带解决了另一个问题:多 run 之间的因果对比 。当 5 次调查都指向同一个根因时,工程师可以横向打开 5 份 HTML,在浏览器里快速比对 Agent 给出的假设差异,而不必逐条翻 JSONL。
把工件当 source of truth 写进系统提示
这是整套设计里最反直觉但也最关键的一步:harness 在每一轮 run 启动时,会把最近 N 次 run 的summary.txt 直接注入 system prompt,并显式告诉 Agent:
以下文件是你过去几次调查的结论摘要。如果当前 Sentry 告警的症状与某次过去的调查重叠,优先复用那次结论并标注「已交叉验证」。
这种做法的工程意义是:让多次运行结果彼此咬合 。如果 Agent 在 7 月 1 日把"Sentry 报错 #123 是 N+1 查询"写进了 runs/2025-07-01-abc/summary.txt,那么 7 月 8 日 Agent 在 system prompt 里读到这份摘要,就不会再花 3 分钟重新假设一次"可能是空指针"。换言之,工件束替代了上下文窗口里的 prompt 重复粘贴 ,而前者是结构化、可检索、可 diff 的。
这种"以工件为真相源"的模式也呼应了 Model Context Protocol 的设计哲学:上下文不一定非要塞进 LLM 的窗口,只要存在某个 deterministic 的、可寻址的存储层,Agent 就能在需要时拉取。Claude Agent SDK 的 CLAUDE.md 与 settings 文件(参见 code.claude.com/docs/en/age…
实战配置片段与踩坑清单
一个最小可运行的工件写入伪代码大致是这样:
await artifact.writeJSONL(`runs/${runId}/messages.jsonl`, message);
await artifact.append(`runs/${runId}/tool_calls.log`, toolCallRecord);
if (isFinalStep) {
await artifact.write(`runs/${runId}/investigation.md`, report);
await artifact.write(`runs/${runId}/summary.txt`, oneLiner);
await htmlReporter.render(`runs/${runId}/report.html`, bundle);
}
踩坑清单,每条都来自真实工程摩擦:
- 不要把工件写进
/tmp。容器重启或 CI cache 失效时证据会消失,违背"真相源"语义。应当落到持久化卷,例如~/.harness/runs/。 - 不要异步刷写 。一旦 Agent 在 step 7 崩溃而 step 6 的工件还在 buffer 里,下次 run 启动时就读不到这次的关键证据。
summary.txt必须人工可读,不是 JSON。下一轮 Agent 用它当 source of truth 时,是把它当自然语言读进 prompt 的,JSON 反而会浪费 token。- HTML 报告里不要嵌入凭据 。Sentry/Linear/GitHub token 一旦泄露到静态 HTML 里,会随报告一起被分享出去。建议在渲染前用 adapter 层做一次脱敏。
与上下游系统的对位
工件存储并不是孤立设计,它和 harness 的其它部件耦合得相当紧:
当这三层证据在磁盘上对齐之后,harness 就不再是"Agent + 一堆 API 调用",而是一个有完整审计链的小型调查系统 。这也是为什么该示例实现能在现场不到 30 分钟搭出第一版——工件存储把"我们怎么知道 Agent 真的做了这件事"从哲学问题降级成了一句 cat runs/.../actions.json。
收尾一句:把每次运行的证据沉淀成文件系统真相源,看似只是"多写几个文件",实际上是把 Agent 的可解释性、可复用性、可审计性这三件最难的事,用最朴素的fs.writeFile 一并解决。当多 run 之间能通过工件彼此咬合,harness 才真正从"一次性聊天机器人"升级为"可持续积累调查经验的工程系统"。
定制系统提示:从通用人格到领域约束的重写
通用工具的提示 vs Harness 的提示:两种人格的分野
打开 Claude Code、Cursor 或 Codex 这类通用 AI 编码工具的默认系统提示,你会读到一些高度概括、面向「让模型表现得像优秀程序员」的人格化指令——「你是一个编码天才」「请避免错误」「尽量给出最优雅的方案」。这种提示的预设场景是开放式编程对话 :用户可能下一秒问 Python 装饰器,也可能让你重构整个 monorepo,模型必须自己揣测意图、自己选择抽象层次、自己决定要不要追问。
harness 的提示恰恰相反。它不写在聊天框的顶部,而是编码进 harness 自己的步骤调用里,目标不是塑造一个「天才人格」,而是要让模型承认自己被嵌入了一个有边界的工程系统 。该示例实现的缺陷调查 harness 在每次启动 worker(模型会话)时,第一句硬编码的指令大致是:「你在一个特定的工程系统内工作,这是一个不开放的、目标单一的缺陷调查流水线,不是开放式编码系统。」这一句话直接关闭了模型对「自由发挥」的想象空间:它不会去改源代码、不会去建议重构、不会去顺手优化无关函数,也不会在终端里炫技性地打印一长串 ASCII art。
[观察] 这种人格切换的副作用是:同一份模型(比如 Claude Sonnet 4.6)在裸跑 CLI 时倾向于给出长篇建议并主动修复,但进入 harness 后会退回到「只读取告警、只写分析、只产出工件」的角色。该示例实现早期曾用通用提示跑过同一个 run,模型直接尝试去修改 Sentry SDK 的源码,触发了 investigate-only 权限的拒绝,这次失败直接催生了硬编码步骤提示的设计。
显式声明的三重约束:工件为真相、按计划攻题、结构化返回
harness 不靠暗示,它把三条铁律写进每一段步骤级提示 ,而且每条都附带了「做不到会怎样」的负面后果说明,而不是一句温和的「建议」。
第一,以工件为真相源 。模型不允许把对话历史当作记忆,而是要求它从 artifacts/issue-xxx/ 目录下的 JSON、Markdown、trace 文件里读取前置步骤的产出,并把当前步骤的结果落盘到 artifacts/issue-xxx/root-cause.md 这样的约定路径。这条约束的工程意义是:即便 worker 进程崩溃、token 用尽、用户 Ctrl+C 中断,下次 run 也能从磁盘续上,而不会变成「上次分析到哪了我也不知道」。
第二,按给定计划攻击特定问题 。每个 run 在开始时都会有一份「调查计划」工件,通常是一段 Markdown 编号清单,逐条列出本次要验证的假设、要调用的工具、要排除的子域。模型被要求严格按编号推进 ,允许跳过(并说明理由),但不允许擅自增加「我顺便也查一下数据库慢查询」之类的并行任务。
第三,必须返回规定的 X/Y/Z 结构 。在最终汇报阶段,harness 的提示里硬编码了一段:「你的输出必须包含 X(症状摘要)、Y(根因判定)、Z(后续工件链接)三个段落,顺序不可调换,缺一不可。」这与通用工具常见的「请尽量结构化」相比,强了一个量级:模型不是「建议」结构化,而是被告知「不结构化就视为不返回」。
步骤级硬编码:不依赖人记得粘贴
把上述提示写进聊天框的顶部,听起来也可行。但 harness 工程化的关键一步是把这些定制提示编码进 harness 自己的代码路径 。该示例实现的入口脚本里,每调用一次 spawnWorker(),就会在发送消息前把当前步骤对应的系统提示拼接进去:
const systemPrompt = [
HARNESS_CONTEXT,
STEP_PROMPTS[currentStep],
ARTIFACT_PROTOCOL,
RETURN_STRUCTURE,
].join("\n\n");
await spawnWorker({ systemPrompt, ... });
STEP_PROMPTS 是一个由 harness 维护的映射表,key 是步骤名,value 是该步骤专属的提示块。这意味着:无论运行这个 harness 的人是工程师、产品经理,还是临时被拉来救火的实习生,只要他执行 bin/investigate <sentry-url>,模型拿到的就是同一份经过反复调优的提示——不依赖人记得粘贴、不依赖人记得删掉上一次的临时指令,也不依赖团队成员之间口口相传的「喂,你跑之前记得在 system prompt 里加一句……」。
[数据] 这套提示工程的落地速度本身就是一个数据点:该示例实现的构建者从动手到跑通第一版「粘贴一条 Sentry 链接即可启动」的流水线,现场花了不到 30 分钟。其中系统提示的拼接逻辑是一次写完、几乎没改,反而是工件目录的命名规范改了三次,Ink UI 的样式改了两次。这条数据说明了一个反直觉的事实:在 harness 里,「让人怎么写 prompt」通常不是瓶颈,「让人怎么组织工件」才是。
多步骤的提示分布:职责单一,互不污染
harness 内部不是只有一份「总纲」提示,而是把不同维度的约束拆到不同步骤的提示块里 ,避免一份超长提示互相污染。下面是职责拆分的示意表:
| 步骤名 | 提示块主要职责 | 关键约束 | 主要输出路径 |
|---|---|---|---|
plan |
拆解调查计划 | 必须输出编号清单,每条标注假设类型 | artifacts/.../plan.md |
collect |
拉取 Sentry / Linear / GitHub 工件 | 只读、必须落盘到 raw/ 子目录 |
artifacts/.../raw/ |
analyze |
交叉比对、生成根因 | 不许修改源文件,产物写 root-cause.md |
artifacts/.../root-cause.md |
summarize |
终端 + 工件双输出 | X/Y/Z 三段式,Markdown | artifacts/.../summary.md |
notify |
推送到 Linear 草稿 | 必须包含证据链接列表 | Linear API 调用 |
这种拆分让每一段提示都短到可以被人完整 review,改一处不会牵动另一处。Ink 终端 UI 部分(参考 Ink 终端 UI 库)只读取 summarize 步骤的产物,完全不知道 analyze 步骤提示里写了什么——这是「提示与 UI 解耦」的副产品,也是 Anthropic 在 Claude Agent SDK 文档里强调的「流程化、约束化」场景的典型写法。
与 skill 方式的对比:确定性触达 vs 概率性唤起
Anthropic 在 Claude Code 文档里把「skill」描述为可被模型按上下文主动调用的能力包。skill 的工作原理是模型自己判断「现在该用哪个 skill」 ,因此具有概率性:同一份输入,有时被唤起,有时被忽略,有时被错误唤起。
harness 的步骤级硬编码提示走的是另一条路:它不依赖模型「记得」去读哪份规范,而是在每一步都强制注入对应的提示块 。这两条路线的对比如下:
| 维度 | Skill 方式 | 步骤级硬编码提示 |
|---|---|---|
| 触达确定性 | 概率性,依赖模型识别 | 确定性,每次运行都注入 |
| 调优成本 | 改一次 skill 文档,多处生效 | 改一处步骤提示,只影响该步骤 |
| 调试可见性 | 模型是否调用,需要 trace 验证 | 直接在 system prompt 里可见 |
| 适用场景 | 开放性任务、探索性交互 | 流水线化、有 SLA 的工种 |
| 对 token 的占用 | 一次性铺底,长期摊销 | 每步骤注入,可裁剪 |
该示例实现选择硬编码路线的根本原因,是缺陷调查对确定性 的要求高于对灵活性 的要求:同一个 run 不应该因为模型「今天心情好」就给出更详细的根因,也不应该因为模型「没想起」该用哪个规范就跳过 X 段。Sentry、Linear、GitHub、Vercel 这些工具的官方 API 文档(分别见 Sentry 官方文档、Linear 官方文档、GitHub REST API 文档、Vercel 官方文档)虽然不直接讲 harness 设计,但它们返回的数据结构稳定性,是 harness 敢做「步骤级硬编码」的底气——如果上游 API 隔三差五改字段,那硬编码提示里的字段名就要跟着改,确定性优势就被抵消了。
实战中容易踩的几个坑
第一,不要把 prompt 写得过长 。一个步骤的提示超过 1500 token 后,模型对末尾约束的遵循度会肉眼可见地下降。该示例实现里 analyze 步骤的提示只有约 600 token,反而是 collect 步骤因为要列举每个适配器(Sentry / Linear / GitHub)的字段约束,接近 1200 token,后续被拆成了「通用收件规则 + 适配器专属附录」两段,效果立竿见影。
第二,不要把多条规则揉成同一段 。早期版本里把「工件为真相」和「X/Y/Z 结构」写进同一段,模型在 follow-up 回合里只记得最后一条,X 段经常被吞掉。拆成两段、并在每段前加粗一个关键词后,问题彻底消失。
第三,不要忽略 negative instruction 。只告诉模型「你应该做什么」不够,还要明确「你不应该做什么」,比如「不要修改源文件」「不要建议重构」「不要在 X 段写代码」「不要在 collect 阶段调用 notify 步骤的工具」。harness 的 analyze 提示里专门有一段「禁止行为清单」,这是从多次失败 run 里反向归纳出来的。
第四,别让提示里出现「请」字 。听起来奇怪,但实测发现,把「请按 X 顺序返回」改成「按 X 顺序返回,不可调换」后,模型的遵循率明显更高。harness 不需要客气,harness 需要确定性。
小结:提示工程是 harness 的隐性 API
通用工具的提示塑造人格,harness 的提示塑造流程 。把约束从「建议」升级为「硬编码注入每一步」,是 harness 把 AI 从「聊天伙伴」转成「流水线工人」的关键设计动作。配合工件存储、窄适配器、investigate-only 权限规则,这层定制提示让 harness 拥有了一个确定性的、可被团队反复 review 的「隐性 API」——它不写在 OpenAPI 里,但比 OpenAPI 更严格地约束着模型每次的行为边界,也是 harness 工程化区别于「套壳聊天」的最显著标记之一。
Opinionated 适配器:比泛用 MCP 更窄、更准的工具面
从「泛用 MCP」到「立场化适配器」的取舍
MCP(Model Context Protocol,模型上下文协议)的本意,是给模型一份「工具说明书」:一份 JSON Schema 加上若干方法名,让 Agent 自己判断该调哪个、参数怎么填。对于一个开放的、什么都能做的编程助手来说,这是合理的默认——你不知道用户下一秒会问 Python 装饰器还是让你重构整个 monorepo,把选择权交给模型,是灵活性最大化的做法。
但 harness 的目标正好相反。它的 job to be done 是高度收敛的:「拿到一条 Sentry 告警链接,经过调查,输出根因报告与可执行的下一步」。在这种场景下,把通用 MCP 原样接进来,等于把一整套数百个工具方法摆到模型面前,让它自己挑。模型当然能挑,但它会在 trace 海洋里游荡——这是该示例实现早期版本反复观察到的现象。
[观察] 早期版本里,只接 Sentry MCP 让 Agent 自由探索,跑一次调查往往要消耗上万个 token,而且半数调用是无关的 issue 列表翻页、breadcrumb 字段展开,真正能用于根因推理的预算反而被压缩。
因此,该示例实现的构建者选了一条相反的路:为每一个外部系统单独写一个立场鲜明的适配器(opinionated adapter)——它不是一个透明代理,而是一份对「在找 bug 这个场景下你应该怎么用 Sentry」的强主张。
Sentry 适配器:在 trace 海洋里划出航线
Sentry 一个 issue 对象,API 返回的字段可以轻易超过 80 个:id、project、title、culprit、firstSeen、lastSeen、level、platform、metadata、tags、context、entries(即 breadcrumb 与 stack trace)、user、fingerprints、activity、各种 release 信息、各种 stats。
如果让 Agent 自己挑,它会先把这些全读完,再判断哪个有用——这是典型的「模型花 token 重新做一遍本该由人类做的预处理」。
立场化的 Sentry 适配器不是这样。它在拉取阶段就把字段做了硬过滤,大致是这样一份优先级表:
| 优先级 | 字段 | 为什么拉 |
|---|---|---|
| 必拉 | entries.exception.values[*].stacktrace.frames |
直接指向崩溃栈 |
| 必拉 | entries.exception.values[*].type 与 value |
异常类型与消息 |
| 必拉 | tags.release, tags.environment |
复现环境上下文 |
| 必拉 | lastSeen - firstSeen 间隔 |
判断新引入还是历史遗留 |
| 选拉 | 最近 5 条 activity |
看是否有人为标记或指派 |
| 选拉 | context.runtime, context.os |
运行环境 |
| 不拉 | user 全字段 |
只取 IP 归属地判断地理分布 |
| 不拉 | 完整 breadcrumb 流 | 只保留 error 前后各 5 条 |
[观察] 在该示例实现里,Sentry 适配器把单次 issue 拉取压到了 3 次以内的 HTTP 调用,且每次响应都被裁剪到模型上下文能直接装下的尺寸,根本不需要再做摘要。
这种适配器的设计原则只有一条:让模型只看到与「这次 bug 是什么」相关的字段 。URL 链接作为输入参数,适配器内部完成字段裁剪、栈折叠、breadcrumb 截断、错误指纹归类,然后以一个紧凑 JSON 抛给 Agent 的推理循环。Sentry 官方文档 docs.sentry.io 暴露的字段全集,远大于这个最小集;但适配器做了减法,这正是它的「立场」。
一个最小可用的 Sentry 适配器签名,大致会是这样:
type SentryIssueDigest = {
fingerprint: string;
title: string;
exceptionType: string;
exceptionMessage: string;
topFrames: StackFrame[];
release: string;
environment: string;
windowBreadcrumbs: Breadcrumb[];
recentActivity: ActivityItem[];
};
interface SentryAdapter {
digestFromUrl(url: string): Promise<SentryIssueDigest>;
similarIssues(digest: SentryIssueDigest, limit = 3): Promise<SentryIssueDigest[]>;
}
注意 digestFromUrl 已经把「URL 解析 → 组织/项目/issue ID 拆分 → 多次 API 调用 → 字段裁剪 → 栈折叠」全部封装在内。Agent 看到的只是一个稳定的输入输出契约,而不是 Sentry REST API 的全貌。
Linear / GitHub / Vercel:场景化而非通用化
同样的思路应用到 Linear、GitHub、Vercel。
Linear 适配器 的立场是:一个 bug 的「相邻上下文」是什么?它会拉取该项目下最近若干条状态为「In Progress / In Review / Recently Closed」的 issue,把标题、负责人、最近一次 PR 引用拼成一个邻接关系图。这样,当 Agent 看到「这个崩溃发生在某次 webhook 路径改造之后」时,它能马上在 Linear 里追问「这次改造的前置任务是什么、谁负责、关联了哪些 PR」,而不是把整个项目空间翻一遍。Linear 文档 linear.app/docs 列出的 GraphQL 字段很多,适配器只挑其中四五类——issue 主体、状态机、PR 关联、最近变更人。
GitHub 适配器 则把自己定位成「PR 维度的 code archaeology」(代码考古):给定一个可疑文件路径与一个可疑版本区间,它会调 /repos/{owner}/{repo}/commits 拿到所有触及该文件的提交,再用 /repos/{owner}/{repo}/pulls 过滤出已合并的 PR,最后只把 commit message 与修改函数列表返回,而不是完整 diff。GitHub REST API 文档 docs.github.com/en/rest 暴露的 endpoint 远不止这三条,但 harness 立场化之后只走窄路径。适配器签名大致是:
interface GitHubAdapter {
touchesOf(file: string, since: string, until: string): Promise<CommitDigest[]>;
prsForCommit(sha: string): Promise<PrDigest[]>;
blameSnippet(file: string, line: number, depth = 5): Promise<BlameHunk>;
}
Vercel 适配器 的立场是「部署维度的环境还原」:给出一个 commit SHA,它会拉最近几次 production 部署的 status、运行时长、错误率曲线,与 Sentry 适配器给出的崩溃时间做时间对齐,判断崩溃是不是跟某次部署同时发生。Vercel 文档 vercel.com/docs 描述的 API 能力覆盖部署、域名、Edge Functions、环境变量等多个层面,适配器只挑时间对齐相关的两三个。
这四个适配器合起来,形成了一个闭合调查回路:Sentry 告诉你「出了什么」,GitHub 告诉你「谁在什么时间改了什么」,Linear 告诉你「这与哪些待办相邻」,Vercel 告诉你「是不是部署引入」 。每一步都只输出下一步真正需要的字段,而不是把原始 API 响应原样转给模型。
Token 花在刀刃上,跑偏概率显著下降
[数据] 把这一组立场化适配器接进 Claude Agent SDK 的 harness 后,该示例实现观察到:平均一次完整调查(从粘贴链接到产出工件)消耗的 token 数下降明显,而更关键的变化是 Agent 发出的「与根因无关」的工具调用次数几乎归零——比如不再出现翻 30 页 issue 列表、为一条 breadcrumb 反复重读完整 JSON 这类动作。
这条规律可以写成一句工程格言:工具面越窄,行为越可预测 。
模型在面对 500 个方法时,需要先做「我该不该调、调哪个」的元决策;而面对 5 个被裁剪过的、结构稳定的适配器入口时,它直接进入「用已知结构推理」的快路径。前者消耗的是注意力,后者消耗的是逻辑。harness 的核心投入,就是把前者提前做成后者。
[观察] 通用 MCP 在该示例实现里并非被抛弃,而是被收编进 harness 的「外围层」:由 Claude Agent SDK 文档 code.claude.com/docs/en/age… 描述的工具注册机制统一承载,允许在某些探索性场景(比如对全新系统做调查时)临时挂上泛用 MCP。但默认 job 流程里,走的是立场化适配器这条窄路。
这其实是 MCP 文档 modelcontextprotocol.io 自己也承认的:协议是传输层,不是策略层 。把策略从传输层抽离出来单独投资,正是 harness 工程的精髓。
适配器立场化是 harness 工程的核心投入点
一个常见的误解是,harness 工程就是「写好 prompt」。实际上,prompt 只是 harness 最表层的人格面具;真正决定 harness 好不好用的,是工具面 ——也就是模型能看到、能调用的方法集合,以及每个方法被设计成什么样的输入输出契约。
立场化适配器的工作量,往往大于 prompt 本身。在该示例实现的代码里,光四个适配器就占用了相当比例的工程时间:每一个都要决定「拉什么、不拉什么、怎么折叠、怎么裁剪、怎么与上下游对齐」。这不是简单的 SDK 调用,而是领域知识的代码化 ——把「调查一个 Sentry bug 该看哪些字段」这种经验,显式编码进工具签名。
这也意味着,适配器一旦写好,它的复用价值极高。同一个 Sentry 适配器,可以被缺陷分诊 harness 用、可以被 on-call 巡检 harness 用、可以被发布前 smoke test harness 用。harness 的可组合性,正是建立在这些窄而稳的适配器之上。换句话说,适配器是 harness 的资产 ,prompt 是 harness 的配置 ;配置可以频繁改,资产要谨慎沉淀。
落地时容易踩的坑
把适配器立场化的过程中,有几个反复出现的反模式值得记录:
- 把适配器做成「通用字段透传」 。表面上是适配器,实际只是把 API 响应 JSON 原样返回,等于一个薄薄的 RPC 包装,等于没做适配。判断标准很简单:如果模型看到的结构跟官方文档几乎一致,那就是透传,不是适配。
- 过度裁剪导致关键上下文丢失 。比如 Sentry 适配器如果不带 release tag,Agent 就无法关联到具体部署,根因推理立刻缺一条腿。立场化的难点不是「砍」,是「知道哪些不能砍」。
- 每个适配器用不同的数据形状 。四个适配器返回的字段命名、时间格式、ID 类型应当统一,否则下游 Agent 还要做一层 schema 对齐,把立场化省下来的 token 又花回去。
- 忘记把适配器失败做成可观察信号 。网络超时、权限不足、字段不存在、token 过期,这些必须以结构化错误抛出,而不是吞掉返回 null。Agent 在调查过程中需要明确的失败信号,才能决定是重试、绕过还是升级到人类。
- 让 Agent 自己决定要不要调适配器 。一旦把调用决策也外包给模型,立场化就白做了。harness 应该把调用顺序硬编码在 run / task 编排里,模型只能在每一步内部决定「如何用这个适配器的输出」,而不是「要不要绕过去」。
每一项反模式背后,都是一个「我们以为在写适配器,实际在写更复杂的通用工具」的故事。立场化的纪律,是 harness 工程区别于「接 MCP 就完事」的关键。
小结
立场化适配器的核心价值,不是把 MCP 干掉,而是在 MCP 之上叠加一层领域判断 。MCP 是传输协议,适配器是业务策略;协议越通用越好,策略越窄越值钱。当工具面被压缩到只剩「为这次 job 而生的入口」时,模型就不再需要在数百个方法里漫游——它拿到的是一条窄而清晰的航线,跑偏的概率自然下降。这正是 harness 工程投入产出比最高的环节,也直接定义了 harness 与「通用 AI 编码助手」之间的人格分野。
用 Codex 与 Claude 对偶构建:一次真实的元工程实验
用 Codex 与 Claude 对偶构建:一次真实的元工程实验
构建 harness 这件事本身,就是一次典型的 AI 工程实践。当「立场化适配器」的设计哲学确定之后,该示例实现的构建者没有立刻动手写第一行代码,而是把这个决策过程本身也当成一次实验来跑。
具体做法是同时打开 Claude Code(参见 docs.anthropic.com/en/docs/cla… OpenAI Codex(参见 openai.com/index/intro… harness」的目标描述分别丢给两边,让它们各自从零实现一份能跑通 Sentry 告警调查的 harness。
这种「对偶构建」(dual build)的真正目的不在于比谁写得更好,而在于通过两条彼此独立的实现路径,看清哪些设计选择是被 job to be done 逼出来的必然结果,哪些只是某个模型长久以来形成的肌肉记忆。
实验的一开始远没有想象中顺利。两个会话都不愿意按照预想,在 harness 的主干里真正放入一个 AI Agent 推理环节。GPT-5.5 与 Claude Opus 都强烈倾向于把整个系统写成一个纯确定性的状态机:解析 Sentry 告警链接、调用 REST API 拉事件详情、抓取堆栈和相关上下文、跑一段固定规则的相关性分析脚本、最后写出一份结构化 JSON 报告。
它们一次又一次地回退到「我能不能不用模型也把这个跑通」的简化方案。这并不是因为模型「笨」,而是因为它们对「什么是好工程」的隐含假设本身就偏向稳定、可测试、可解释、可静态分析——而 AI 推理环节天然带有概率性、不可完全复现,在它们的「工程审美」里自然会被优先优化掉。
这是构建 AI harness 时一个普遍但容易被低估的阻力来源:你以为自己请的是「工程师」,其实请的是「有强烈工程审美的工程师」,而那份审美在大多数场景下都倾向于把不确定性赶出系统。
要让两个会话真正把 Agent 留在 harness 的主干里,必须把指令写到非常具体的颗粒度,几乎是逐字段钉死的程度。不是笼统地说「做一个会做根因分析的 AI」,而是要写明下面这种级别的硬约束:「在证据收集阶段结束之后、根因分析阶段开始之前,必须插入一次 model.run 调用;输入固定为 evidence.json 这个工件,输出必须落到 root_cause.draft 这个工件路径;这一步必须允许工具调用,但禁止任何对源仓库的写操作」。
换句话说,对工作流的阶段切分、工具绑定的位置、定制提示词(prompt)的注入点都要逐条钉死。否则模型会立刻按自己的偏好重新画边界、重新定义阶段、重新安排工具——这正是把 harness 工程化最容易踩的坑之一:你以为自己在描述需求,模型听到的却是「自由发挥的邀请」。
为了把这种「具体度」做到可复用,值得在 harness 设计初期就固定一套表达模板,把每个阶段的输入、输出、可用工具、可写权限四项以表格形式钉下来,任何模型、任何编码工具在填实现之前必须先填这张表:
| 阶段 | 输入工件 | 输出工件 | 允许工具 | 允许写操作 |
|---|---|---|---|---|
| 证据收集 | alert.url | evidence.json | Sentry / Linear / GitHub 读 | 仅工件目录 |
| 根因分析 | evidence.json | root_cause.draft | model.run | 仅工件目录 |
| 后续跟进 | root_cause.draft | follow_ups.json | GitHub Issues / Linear 创建 | 仅工件目录 |
表本身不复杂,但它的价值在于强迫模型在写代码之前先承认「这里有一个 AI 环节,它的边界就是这张表」。少了这一步,模型就会本能地把 AI 环节「内联」进某个确定性函数里,等代码 review 时才发现根因分析其实根本没经过模型。
[观察]两个会话最终都接受了「在 harness 中保留一个 AI 推理环节」的设定,但它们的反应路径完全不同。Claude 一侧把 Agent 收得很紧,试图用一个高度受控的 prompt 模板把整段调查过程封装成确定性更强的调用,倾向于把模型视为「带输入输出的纯函数」;Codex 一侧则更愿意在 harness 里塞进多个可分支的 AI 步骤,接受更多的不确定性和多轮工具调用,甚至会在证据不足时主动决定回退到上一步重抓证据。
这种差异本身就回答了「为什么要做对偶构建」——不是因为某一边更聪明,而是因为每一边都有自己的形状,你必须亲眼看到两种形状,才能在选型时刻意选一个,而不是默认掉进某一个里。
[数据]在该示例实现的实际工作流中,一次典型的调查运行只触发一次 I(Investigation,只调查不修复)动作,全程不触碰任何源文件;现场不到 30 分钟就构建出可运行的第一版 harness,主体由大约 8 个职责单一的功能文件组成,每个文件对应流水线的一个阶段(证据收集、根因分析、工件存储、权限校验、TUI 渲染、Sentry/Linear/GitHub 适配器等),每个文件的修改都不会跨阶段污染,review 时可以做到逐 PR 独立合并。
实验中最值得玩味的交叉结果是:Codex 那份最终被采纳度最高的实现,在 Agent 推理环节反而选用了 Claude Agent SDK(参见 code.claude.com/docs/en/age… OpenAI 系的编码 Agent,在写一个 harness 的时候,主动把推理环节外包给了一个 Anthropic 系的 SDK。
理由非常工程化:Claude Agent SDK 在「带工具调用的多轮会话 + 受控权限边界 + 可注入 harness 级 system prompt」这一组合上,刚好和该示例 harness 的需求咬合得最紧。相对地,Codex 自家更擅长的是一次性代码生成,把同样的能力强行堆到一个长上下文、多工具调用、需要「自我约束」的 harness 推理环节里,反而会出现工具调用风格不一致、权限校验不严等问题。
这是「选型不站队」最干净的一次现身:不是出于偏好,不是出于站队,是出于「这一段代码谁写得更像该段代码该有的样子」。
把视野再抬高一档,这其实不是孤例。MCP 的整套生态(Model Context Protocol,见 modelcontextprotocol.io)从一开始就把「跨厂商拼装」当作前提:工具说明就是一份 JSON Schema,谁实现、谁消费、谁路由,完全可以来自不同团队,不需要任何一方垄断。
Ink 这类终端 UI 库(见 github.com/vadimdemede… stdout 的 Agent 都能被套上一层漂亮的 TUI。Sentry、Linear、GitHub、Vercel 的官方 API 文档(分别见 docs.sentry.io、https://linear.app… harness、任何 Agent、任何脚本都可以平等接入。
把这些拼起来,harness 的最佳实践其实就是一次优雅的「多模态路由」:编码环节用 Codex 的生成速度,推理环节用 Claude 的工具协同,展示环节用 Ink 的 React 风格组件,数据接入用各家自己的 REST API,中间用 MCP 把工具说明书统一掉。
对 AI 工程师与团队负责人来说,这条经验比任何具体技术选型都更重要:不要把工程效率押在某一个工具的「全栈能力」上,要承认每家模型、每个 SDK 在不同子任务上有不同的形状 。
harness 本身就是一个组合件——它把「编码能力」「推理能力」「工具协同」「人类可读输出」「外部系统接口」拆成至少五个独立槽位,每个槽位都让当前最强的那个组件来填。这才是「立场化适配器」思路在工具链层面的延伸:不追求通用最强,只追求在 job to be done 上每一段都刚好合适。
跨模型、跨编码工具的协作不是需要特殊理由的稀有场景,而是 AI 工程以后最常见的常态——把这件事写进团队的工具箱共识里,会比任何一次单点优化都更长久地影响交付节奏。
代码地图:一个 Harness 其实只有一小撮文件
全景速览:比你想象中更薄的一层代码
当很多人听到「给 AI 写一个 harness」时,脑海里浮现的画面通常是一整座工程:长篇累牍的系统设计、几十个模块互相调用、几层抽象层层包裹。但该示例实现的真相是——一个 Harness 其实只有一小撮文件,整体规模远小于任何人的想象。
如果把仓库想象成一棵倒挂的树,那么最顶端的入口只有一个,负责把请求分发给下面的 TUI/CLI(终端交互界面/命令行界面)。这个入口之下,横向铺开大约 8 个职责单一的功能文件,每一个文件只回答一个具体问题:这次运行从哪个外部服务取数据?如何汇总一份证据?工件(artifact)应该被写到哪里?整个项目的目录深度几乎不超过两层。
[数据] 该示例实现的代码地图合计约 8 个核心功能文件 + 1 个高层入口,外加若干 README、.env.example 之类的样板文件,核心 TS/JS 源文件体量远低于一般的中型 Node 项目。这种「小代码量」的视觉冲击力,是理解 harness 这件事的起点。
这种「刻意做薄」的结构并不是偶然,而是一种态度宣言:harness 的目标不是替代业务系统,而是把一次具体 job 的判断路径以最直白的方式固定下来。任何新读者打开仓库,只要顺着入口往下数,三十秒内就能数完所有文件。
文件清单:8 个文件如何分配职责
为了让读者建立直观印象,下面把核心文件清单以表格形式列出。该清单基于这套实践里构建者展示过的结构化梳理,逐项对应 Claude Agent SDK(参见 code.claude.com/docs/en/age… run/task 抽象。
| 文件名 | 职责 | 关键依赖 |
|---|---|---|
index.ts |
高层入口,解析 argv,挂上 Ink 渲染器 | Ink、Claude Agent SDK |
tui.tsx |
TUI 主界面、按钮、状态切换 | Ink |
cli.ts |
命令行模式,纯文本输出 | Claude Agent SDK |
hunt-bug.ts |
Sentry 猎 bug 完整工作流 | Sentry、Linear 适配器 |
adapters/sentry.ts |
Sentry 适配器,封装 issue 取数 | Sentry API |
adapters/linear.ts |
Linear 适配器,创建/查询 issue | Linear API |
adapters/github.ts |
GitHub 适配器,搜 commit/PR | GitHub REST API |
adapters/vercel.ts |
Vercel 适配器,取部署日志 | Vercel API |
artifacts.ts |
工件输出,把 Markdown 报告落盘 | fs |
config.ts |
读 .env,校验 key 是否齐全 | dotenv |
每个文件都遵循单一职责:适配器只负责把外部 API 包成一个函数,Sentry 工作流文件只负责编排流程,工件文件只负责写文件,入口只负责引导和挂载 UI。这种「一层做一件事」的分层,是 harness 与传统重型框架最大的视觉差别。
猎 Bug 工作流文件:把「判断」写成显式步骤
在 8 个功能文件里,最值得细看的是 hunt-bug.ts,它把「我们如何猎 bug、如何汇总报告」的全过程写成了显式步骤。这个文件不是一个黑盒,而是一份可读的剧本。
具体来说,文件里通常会有一段类似下面的伪代码:
async function huntBug(alertUrl: string) {
const alert = await sentry.getIssue(alertUrl);
const repro = await sentry.searchSimilarEvents(alert);
const context = await github.findRelatedPR(repro.commit);
const deployment = await vercel.getDeployment(repro.release);
const ticket = await linear.createInvestigationTicket({...});
const report = await writeMarkdownArtifact({alert, repro, context, deployment, ticket});
return report;
}
每一步都对应一个适配器调用,没有任何魔法。harness 之所以这样写,是为了让任何打开这个文件的人都能在两分钟内看清一次完整调查的形状:从 Sentry 告警 → 相似事件 → 相关 PR → 部署上下文 → Linear 调查单 → 工件落盘。
[观察] 这套实践里,调查运行的标志位始终是 I(investigate-only,只调查不修复),而不是 F(fix),也就是说 harness 全程不触碰源文件、不会发起 PR、不会改业务代码。它把 AI 框定在「取证 + 汇总」的位置上,把修复动作留给工程师。这种「越界自律」正是 harness 价值密度最高的地方——它没有无限扩张能力,而是刻意限制能力,把剩余价值留给人类判断。
四只适配器:立场鲜明的薄壳
四个外部服务适配器——Sentry、Linear、GitHub、Vercel——各自只有几十行代码,做的事情却非常一致:把目标服务的真实 API 字段映射到 harness 自己定义的 Evidence(证据)类型。
以 Sentry 为例,适配器只暴露 getIssue、searchSimilarEvents、getLatestRelease 三个方法,内部把 issue.id、issue.title、event.tags 这些字段统一抽成 Evidence。这种映射是有立场的:它主动丢弃了某些 Sentry 字段(如内部管理字段、订阅者列表),只保留「和缺陷调查相关」的那部分。
[观察] 这套实践的立场是:适配器不是 SDK 的搬运工,而是「这份 harness 想要的」外部世界视图。换句话说,当 Sentry 升级了一个字段,适配器不一定立刻跟进;当 Linear 改了 issue 结构,适配器也只会更新自己关心的字段。这种「窄而立场鲜明」的姿态,正是 opinionated adapter(立场鲜明的适配器)的字面含义。
四个适配器的实际参考链接在仓库的 README 里直接给出,例如 Sentry 官方文档(docs.sentry.io)、Linear 官方文档(linear.app/docs)、GitHu… REST API 文档(docs.github.com/en/rest)与 Vercel 官方文档(vercel.com/docs)。读者不需要… README 就能顺着这些地址把外部语义弄清,这种「文档自包含」也是 harness 低门槛的一部分。
配置:README 即文档,克隆即跑
如果说代码地图的前半段是「薄」,那么后半段就是「轻」。该示例实现的配置文件只有两类:一个 .env.example,一份 README。.env.example 列出所有需要的 API key(Claude、Linear、GitHub、Vercel、Sentry 各一个),README 用大约半页篇幅说明「克隆 → 填 key → 运行」的流程。
这种设计是刻意压低门槛的结果。传统工程项目在交付时通常会伴随一套部署文档、环境检查清单、权限申请工单;而这套 harness 的 README 写得像一份 checklist:复制粘贴 → 替换占位符 → 执行 npm run hunt <sentry-url> 即可。如果读者连这一步都不想做,直接看 README 里的样例运行截图也能理解全貌。
[观察] 当构建者把这份 README 拿给团队评审时,反馈是出奇的一致:门槛越低,采纳率越高。一个需要配 Kubernetes、配 Vault、申请 Service Account 的 harness,大概率只会被作者自己使用;而一个「克隆即跑」的 harness,才有概率在 Slack 群里被转走。这种设计选择和 harness 的体量一脉相承——它不是基础设施,而是工具。
工件输出:调查结束留痕
工件(artifact)输出文件 artifacts.ts 同样很薄,职责只有一个:把一次调查的所有证据按照 Markdown 模板渲染,并写到本地 runs/<timestamp>/report.md。模板里通常包含标题、影响范围、根因猜测、相关 PR、相关部署、Linear 调查单链接、Sentry 原始链接,以及一段「下一步建议」。
工件的存在意义在于:让 harness 的输出有形。一次调查运行结束,工程师拿到的不只是一段控制台文本,而是一份可以贴到工单、可以转发、可以归档的 Markdown 文件。当 [数据] 显示某条 Sentry 告警影响约 150 名用户、仍在每小时发生时,这份 Markdown 就具备了「贴在事故复盘文档里直接引用」的可用性。
工件落盘同时也让 harness 可以脱离 TUI 运行。CLI 模式(cli.ts)下,整个交互界面被替换为一段纯文本报告,但工件文件依然会写出来。这种「有 UI 是常态,无 UI 也照样工作」的设计,让 harness 在 CI、cron、本地终端之间无缝切换。
价值密度:不是代码行数,而是领域判断
回头看,这个 harness 大约只用了 8 个文件。但它真正承载的东西并不是这 8 个文件本身,而是里面沉淀下来的领域判断:什么时候该把证据抓全,什么时候该停止抓证据;该往 Linear 里写哪种类型的工单,不该写哪些字段;哪些 Sentry 字段和缺陷调查相关,哪些是噪音。
[数据] 如果把这套 harness 摊开算,真正「和 AI 决策相关」的代码行数大约只占 30%——剩下 70% 都是确定性的编排、文件 IO、API 包装。这意味着,harness 的代码行数和它的判断密度之间不存在线性关系;真正决定 harness 价值的,是那些 AI 在循环里必须遵守的规则,而规则往往用十几行 if-else 就能写完。
这其实呼应了 harness 设计的元命题:在 AI 越来越擅长写代码的今天,工程师的核心交付物正在从「代码」迁移到「约束」——约束 AI 在哪一步该调哪个工具、约束它在哪种情况下不该继续深入、约束它的输出必须落成可审计的工件。这套约束加在一起的密度,就是 harness 的真正体量。仓库看起来只有一小撮文件,但每一撮都承载着一次具体的 job to be done,这份「薄而密」,正是该示例实现最值得借鉴的地方。
实战运行:一次真实 Sentry 调查的完整产出
触发一次真实调查:从告警到报告
这天上午,值班工程师在 Sentry 的 inbox 里看到一条持续发酵的告警,标题简短但刺眼:editor mutation dropped——编辑操作被丢弃。它不像 error 那样震耳欲聋,而是安静地以 warning 级别每小时吐一条新事件,意味着用户正在反复触发它,但没有人因此崩溃。值班工程师在 TUI(终端交互界面)里粘贴进 Sentry 事件链接,按回车, harness 立刻拉起一次 run。屏幕顶部弹出两行选择:I nvestigate 或 F ix。工程师选 I(调查),刻意不选 F(修复)。
[观察] 选 I 还是 F,看似只是一个交互细节,实际上是 harness 设计哲学最直观的体现:在证据备齐之前,任何写盘动作都被视作越权。这种「分诊先行、修复后置」的姿态,正是 opinionated adapter(立场鲜明的适配器)对工具语义的最大贡献——它把 Clinical 的「先诊断再开方」翻译成了机器可消费的命令。
调查报告的字段解读
不到两分钟,终端里渲染出一份结构化报告。决策者最先看的是「真实性」和「范围」两栏:报告显式确认该告警确实存在,影响约 150 名用户,且仍在以每小时一次的频率新增事件——这意味着这不是一个历史遗留问题,而是一个正在漏血的活问题。级别被标注为 warning 而非 error,这一点很关键,因为它直接决定了后续响应的优先级。
| 字段 | 报告结论 | 工程含义 |
|---|---|---|
| 真实性 | 告警已核实 | 排除误报、采样噪声 |
| 影响范围 | ~150 用户 | 需 PM 介入而非单人私了 |
| 频率 | 约 1 次/小时 | 仍在 reach,不是回归峰值 |
| 级别 | warning | 不阻塞 SLO,但损害功能 |
| 建议动作 | 建 Linear 工单 | 升级到产品/工程协作面 |
[数据] 影响 150 名用户、还在每小时发生——这两个数字组合在一起,直接让该 issue 越过 on-call 工程师的「自助」权限,进入需要跨职能协同的区间。
根因假设的优先级排序
报告最核心的章节是「根因假设」,它不是一段散文,而是一份按优先级降序排列的候选清单。第一嫌疑是 invalid original range,也就是 ProseMirror(一种富文本编辑器框架)在其协作协议中检测到了客户端发来的 range 越界或空值;第二嫌疑是 overlapping original range,即两个并发的 range 重叠导致只有一条步进被采纳。这两条假设覆盖了 80% 以上的同类历史工单,几乎已经成为该函数的「嫌疑地图」。
报告还额外指出该函数的一个盲区:它对 step 数组的批量提交做了原子的整体接受/拒绝,但缺乏对中间步进 的细粒度溯源。这意味着当一批 step 被拒时,日志只告诉你「这一批没落库」,却没说「是第几条 step 出了问题」。这正是 harness 不同于普通 LLM 调用脚本的地方:它不会停在「猜测」上,而是把盲区坦白写出来,留给人类工程师做最后判断。
验证路径与产品面定位
仅有假设还不够,报告必须给出可执行的验证路径。第一步是让值班工程师用 Sentry 官方的事件导出接口(参见 Sentry 官方文档 docs.sentry.io)拉取原始 payload,人工核对 originalRange 字段的取值;第二步是去产品面找锚点——把受影响 150 用户的设备、入口、客户端版本聚类一遍,看看集中在哪个发布渠道或哪个编辑动作上。报告并没有让 harness 自己去做这两步,因为它们都涉及敏感用户数据和主观的产品判断,超越了一份「编辑器层缺陷分诊」应该承担的职责。
验证路径之后,报告给出了一条明确建议:创建一张 Linear 工单并指派给相关 owner 。这一步看似简单,实则完成了关键的状态转移:从「harness 内部证据」变成「组织协作 system of record(记账系统)」。Linear(官方文档 linear.app/docs)会把工单编号… Sentry 事件评论里,反向形成证据闭环。
自我克制的 Agent:知道不知道,比知道更重要
这份报告最值得反复咀嚼的一句话,藏在末尾的「下一步建议」里:当前信息不足以开启 patch 模式修复,建议人工介入后再次触发 run 。它没有越界,没有自作主张地 generate diff(生成代码差异),也没有把建议发到 Slack 频道刷一波存在感。受约束的 Agent 主动承认自己的边界,这正是 harness 想要的行为。
[观察] 这种「克制的智能」并不是模型本身自动涌现的,而是 harness 通过 prompt、flag、权限规则三件套正向塑造出来的。模型在底层当然有能力继续往下写代码,但当系统明确告诉它「patch 模式未开启,本 run 不允许写文件」时,它会把表达收敛到「还需要什么信息」上面。
与传统 on-call 流程的对照
如果把这次 run 放进传统的 on-call 流程里做对照,你会发现 harness 并没有抢走任何人的工作——它只是把原本散落在多个 Tab、多个仪表盘、多个工单字段之间的上下文,一次性压成了一份可被人阅读的报告。值班工程师不需要打开 Sentry 找原始事件,不需要打开 GitHub 翻最近的相关 commit,也不需要打开 Linear 手动起草工单描述。harness 让人类专注于「判断」,把「检索/汇总/格式化」这种确定性苦活全部吞掉。
| 维度 | 传统流程 | harness 辅助 |
|---|---|---|
| 告警核实 | 人工打开 Sentry | run 内部自动校验 |
| 用户影响面 | 估算或粗略 | 来自 Sentry 查询 |
| 根因假设 | 工程师脑内快推 | 假设清单 + 优先级 |
| 修复动作 | 直接动手改 | 暂不生成,需人工仲裁 |
| 状态转移 | 凭记忆 | 自动写回 Linear |
实战踩坑清单:运行这类 run 时的几个真实坑
即便工具再顺手,工程现场依然有几个反复出现的小坑值得提前留意。
- Sentry 链接粘贴时不要带 query string 。多余参数会改变事件上下文,导致 harness 拉错版本。
- I 还是 F 的选择必须当场决定 。如果临时改主意,需要新建一次 run,而不是在已有 run 内切换,否则 context 会污染。
- Linear 工单 owner 不要默认填自己 。该函数模块很可能不归你管,正确做法是让 harness 推断模块 owner,人工确认。
- warning 级别告警不要忽略 。它不在 PagerDuty 噪音里,但对功能完整性的损耗是日积月累的。
写在最后
这一节的目的不是演示 harness「能做什么」,而是展示它「会主动不做什么」。当 Claude Agent SDK 文档(code.claude.com/docs/en/age… tool use 能力配上立场鲜明的 Sentry / Linear / GitHub 适配器,真正棘手的从来不是让模型去读写文件,而是给它装上一副稳定的缰绳。这次调查 run 全程没有触碰任何源文件,却把一份能直接进入团队会议讨论的交付物摆在了值班工程师面前——这或许就是 opinionated adapter 留下的最有价值的遗产:它让 AI 学会在正确的位置停下来。
决策矩阵:哪些工作该进 Harness,哪些不该
为什么需要决策矩阵:不是所有工作都值得包一层 harness
该示例实现里的 bug triage harness 解决的是"值班工程师贴一条 Sentry 链接,自动产出可贴在 Linear 评论里的根因报告"这一窄场景。这套实践的构建者坦言,在它之前尝试过给"任何一次 Sentry 告警"都包一层 harness,结果发现 harness 的设计成本、维护成本、跑偏后的纠正成本,会把原本一两段 prompt 就能解决的事拖成几周的工程。
[观察] 那次试错后来被固化成一条口头规则:harness 是放大器,不是启动器 。一个 job 如果本身就没跑通过三次以上,不要急着给它做 harness——你还没看到这条工作流的真实形状。
工程上更稳妥的做法,是先用一个不带 harness 的通用 Agent 跑十遍,记录 prompt、工具调用、失败重试、人工补刀的环节。等形状稳定了,再决定是否值得投入 harness 这层外壳。
四个打分维度:高频、流程、验收、失败
判断一个 job 是否值得建 harness,可以套用四个维度的打分矩阵。每个维度 0-3 分,总分 ≥ 8 才进入候选名单。
维度一:高频重复 。这条工作是否每周至少出现两次、且每次都需要人盯?缺陷分诊、PR 描述生成、Sentry 告警根因初判,都属于这一类。反过来,一个季度才发生一次的事件,不值得为它专门搭架子。
维度二:流程稳定 。从输入到输出,中间环节是否已被团队默会?如果还要边做边想"下一步到底该调哪个 API",流程还在漂移,harness 也会跟着漂移。
维度三:验收物明确 。产出能否用一张检查表机械判定?比如"根因报告必须包含三段:触发条件、影响面、建议动作"。验收物越能被结构化字段描述,harness 越容易把活儿做到位。
维度四:失败可控 。最坏情况下,harness 跑偏了能否被及时拦下来?一次失误若会污染生产数据库、推送错误告警给客户、覆盖线上文件,即便总分再高也要慎做。
[数据] 该 Sentry 告警(editor mutation dropped)以 warning 级别每小时吐一条新事件,影响约 150 名用户、仍在每小时发生——这是矩阵能打满 12 分的典型样本,告警等级定义可参见 docs.sentry.io 。
反例清单:三类工作建 harness 是过度工程
把上面矩阵反过来用,可以列出一个简洁的反例清单。一次性脚本 不值得。数据迁移、批量改字段名这类"跑完就扔"的任务,写一个 shell 脚本即可,贴一层 harness 等于把一次性筷子做成实木餐具。探索性研究 也不值得。需要不断变换查询角度、试探不同 prompt、容忍失败的研究任务,适合交给通用 Agent,因为它的"流程"还没定型,hard-code 进 harness 反而锁死了探索空间。需求还在变的功能开发 同样不值得。原型期的功能两周内 schema、字段、调用方都会动,harness 越细越快过期。
[观察] 这套实践的构建者在 TUI 里让值班工程师按 I 而非 F 的设计,就是为了让 harness 永远停在"调查"这一步、不触碰源文件——这本身就是把"失败可控"维度打满的具体做法。
反例清单的用处不是劝退,而是节约精力。把过度工程的成本省下来,投入到真正高频、稳定的 job 上。
三层分工:skill、MCP、harness 的边界
harness 不是孤立存在的,它和 skill、MCP 有明确的分工,三层可以叠加。
skill 靠触发 。它是模型在某次对话里"想起来就用"的能力,典型形态是系统提示词里的一段约定、或工具调用时的一条旁路规则。skill 没有独立进程,也没有自己的状态机。
MCP 给工具 。MCP(Model Context Protocol,模型上下文协议,详见 modelcontextprotocol.io )是一种把外部工具与数据源以标准化方式暴露给 Agent 的协议,典型如把 GitHub Issues、Linear、Sentry 的查询能力挂到任意 Agent 上。MCP 不规定流程,只规定"我能调什么"。
harness 定流程 。harness 是把上述能力按特定 job 的形状串起来的状态机,包括事件如何订阅、子任务如何派发、工件如何落盘、人工兜底何时介入。它决定"在什么状态下做哪一步"。
async function investigateRun(sentryUrl: string) {
const event = await sentryAdapter.fetchEvent(sentryUrl);
const suspects = await gitAdapter.searchRecentCommits(event.fileHint);
const report = await renderArtifact({ event, suspects });
await artifactStore.write(report);
return report;
}
三层叠加的好处,是各层只关心自己的语义,改动半径被天然隔离。skill 升级不需要重启 harness;harness 重构不需要换 MCP server。
维护成本:窄工具面也是窄变更面
harness 的一个隐性成本是适配器演进。每个 opinionated adapter(立场鲜明的适配器)都会随外部 API 演进而需要更新——Sentry 的事件字段会变、Linear 的 GraphQL schema 会变、GitHub REST 行为会变(参考 docs.github.com/en/rest 与 linear.app/docs )。一旦上游改了字段,harness 里的解析逻辑必须跟着改。
窄工具面同时意味着变更面更小、风险更可预测。bug triage harness 主体只有约 8 个职责单一的功能文件,每个文件只关心一个适配器或一种工件。外部 API 改一处,改动通常落在一两个文件里——这种"小而硬"的形状是 harness 长期可维护的前提。
[观察] 构建者提到,GPT-5.5 与 Claude Opus 在评审这套 harness 时,起初都抗拒在 harness 内部再放 AI 环节,倾向纯确定性实现,理由正是"再叠一层模型就多一层不可控变更"。最终版 harness 只在"选择下一步动作"这一处保留了 AI 判断,其余环节全部 deterministic。
维护成本要在打分时一并计入:总分 ≥ 8 但适配器依赖 ≥ 3 个外部系统的 job,要额外扣 1-2 分,作为"未来维护税"。
决策的落点:组合而非孤岛
矩阵打分不是终点,真正的决策落点是组合。通用 Agent 适合编排——决定"现在该跑哪个 harness、人工该何时介入"。harness 适合承担机械可预测的子任务——在固定形状的 job 上把证据收齐、把工件写规范。
工程上更推荐的拓扑是:通用 Agent 在外层做调度,harness 在内层做执行 。例如,值班工程师在 TUI 里选了 I,外层 Agent 就把这次 run 派给 bug triage harness;harness 在内部调 Sentry 适配器拉事件、调代码搜索工具定位可疑提交、最后把工件写到 artifact store;外层 Agent 拿到工件后再决定是否升级到 F。
[观察] 这种组合在工程上有一个隐含前提:通用 Agent 与 harness 共享同一套工件协议(artifact store 的字段命名、状态码、错误结构)。该示例实现现场不到 30 分钟就构建出第一版 harness,正是因为工件协议先行、适配器后置——Claude Agent SDK 把 run、task、artifact 作为一等概念暴露,详见 code.claude.com/docs/en/age… ,终端 UI 层则由 Ink 提供(参考 github.com/vadimdemede… )。
harness 的真正价值,不在于它本身有多智能,而在于它把通用 Agent 不愿意反复做的机械劳动固化下来,让外层编排者只关心"现在该跑哪条流水线"。决策矩阵的意义,正是把哪些子任务值得"流水线化"这件事,变成可以每周复盘一次的清单,而不是一次性的直觉判断。
构建你自己的 Harness:七步可复制方法论
从零到第一版:把方法论落地成可跑的 harness
[观察] 该示例实现的 bug triage harness 并不是一次性设计出来的,而是构建者用"先跑起来,再收紧"的方式迭代了三轮才稳定。第一版在一次真实的 Sentry 告警现场不到 30 分钟内搭完,事后才回过头把每一步固化成可复制的方法论。下面把这条路径拆成七步,每一步都可以独立评估"做不做、做到什么程度"。
第一步:把工作流真正写在纸上
不要把工作流留在脑子里。harness 的第一步永远是先用 markdown(或者任何你能 grep 的纯文本)把目标 job to be done 写下来。该示例实现的描述只有一句话——"值班工程师贴一条 Sentry 链接,自动产出可贴在 Linear 评论里的根因报告"——但这一句话背后藏着四个隐含约束:输入只有一条 URL、输出必须能贴到 Linear、产出物要让值班工程师一眼看懂、整套流程必须可在手机端复现。
[观察] 这一步最常见的失败模式,是把工作流写成"AI 帮我们处理告警"这种口号式句子。口号式工作流无法判断适配器该不该写、工件该不该结构化,也就无法判断 harness 是否值得做。一份合格的工作流描述至少要回答三件事:谁触发、输入是什么、产出物是什么形态。
第二步:定义 run 与 task 的执行形态
工作流写好后,下一步是把"一次完整的执行"切成"若干个 task"。在 Claude Agent SDK 的语义里(参见 code.claude.com/docs/en/age… 通常对应一次端到端的会话,而 task 对应 run 内部一次具备明确输入输出的子动作。设计时要在四件事上做明确选择:
| 维度 | 选择 A | 选择 B | 该示例的选择 |
|---|---|---|---|
| task 粒度 | 一个超大 task 自由发挥 | 多个窄 task 串行 | 窄 task:证据收集→根因分析→后续工件 |
| task 之间通信 | 共用上下文 | 通过显式工件传递 | 通过显式工件传递 |
| task 失败回滚 | 全程重跑 | 局部重试 | 局部重试,复用已有工件 |
| task 上下文隔离 | 共享同一会话 | 每次 task 独立会话 | 独立会话,只带必要工件 |
这一步之所以关键,是因为它直接决定 harness 是"包了一层 prompt 的 wrapper"还是"有内部结构的工程系统"。一旦 task 之间的边界用工件而非共享上下文来划清,后面所有的适配器、权限、规则才有挂载点。该示例实现里 task 数量被刻意压在 3 个,就是不希望每个 task 都重新发明一遍上下文管理。
第三步:为每个数据源写立场鲜明的适配器
适配器不是把 SDK 调用包一层函数那么简单。该示例实现里的 Sentry/Linear/GitHub/Vercel 四个适配器(对应 Sentry 官方文档 docs.sentry.io、Linear 文档 linear.app/docs、GitHub REST API docs.github.com/en/rest、Ver… 文档 vercel.com/docs)有一个共同特…
所谓"立场鲜明的适配器"(opinionated adapter),是指适配器在暴露给模型之前,先替模型决定好了"这次该查什么、不该查什么、字段如何裁剪"。比如 Sentry 适配器只返回事件摘要、堆栈帧前 5 帧、最近一次 release 信息;它不返回全部事件 payload、不返回内部 trace、不返回用户邮箱。Linear 适配器只暴露"为这条 issue 起草一条评论"这一个动词,不允许模型去改 issue 状态、加 label、建子任务。一个简化后的适配器接口示意如下:
export const sentryTools = {
async getEventSummary(eventId: string) {
const event = await client.getEvent(eventId);
return {
title: event.title,
stack: event.stack.frames.slice(0, 5),
lastRelease: event.release,
};
}
};
[数据] 该示例实现的 harness 主体只有约 8 个职责单一的功能文件,适配器相关的就占了 5 个。这不是巧合:适配器越窄,模型越不需要判断,harness 越稳定。当适配器层把"哪些字段该看、哪些动词该调"都固化下来,模型层就只剩下"基于这些事实写一段话"这一件事。
第四步:设计结构化的工件(artifact)
harness 与一次性 prompt 的根本差别,在于前者有 artifact store(工件存储)。每一次 run 产出的根因报告、证据快照、建议的修复 PR 草稿,都应该是结构化的 JSON 或 markdown,而不仅是一段对话历史。该示例实现里,根因报告被显式拆成"症状 / 时间线 / 相关 commit / 怀疑点 / 建议下一步"五段,每段对应一个独立字段,这样 Linear 评论粘贴时不会出现"AI 啰嗦了一大段但缺关键信息"的情况。
工件结构化还有两个隐性收益:一是方便后续评测——直接 diff 两次产出的字段差异即可;二是方便做 artifact store 的版本回滚,run 失败时可以挑选上一版可用的工件,而不是把整条 run 标记为失败。该示例实现的工件以 JSON 落到本地文件系统,文件名包含 run id 与 task 名,便于跨 run 对比。
第五步:定权限与规则
权限规则是 harness 的护栏,不是它的功能。该示例实现只授予"调查权限"——查询 Sentry、读 GitHub 代码、看 Vercel 日志——而显式排除了"修复权限":不能 push 代码、不能合并 PR、不能关闭 Linear issue。这就是 harness 文档里反复强调的 investigate-only(只调查不修复)模式。
[观察] 这种"只调查不修复"的取舍,源自一条经验:一旦 harness 拥有写权限,值班工程师就会因为"反正 AI 改了"而放弃人工 review,事故的二次放大风险会指数级上升。权限是一旦放开就收不回来的东西,所以设计阶段就要写死。除了权限,还要明文规定"哪些行为必须人工确认"——比如任何涉及删除、合并、关闭的动作,都需要在 Linear 评论里留下二次确认链接,而不是让 harness 直接执行。
第六步:选执行引擎
执行引擎的选择直接决定 harness 的天花板。可选项大致有三:
[观察] 在该示例实现的早期讨论中,GPT-5.5 与 Claude Opus 都曾倾向于把 harness 写成纯确定性代码、把 AI 环节彻底拿掉。它们的共同理由是"AI 环节越多,出 bug 的概率越大"。最终保留 AI 环节,是因为值班场景下"猜错根因但贴出可用的报告"远比"报告写不出来"更能被接受——确定性代码在这个场景下不够用。这也呼应了开篇的决策矩阵:harness 不是启动器,而是放大器,只在 AI 能带来明显增益的窄环节启用 AI。
第七步:造一个交互面
最后一步是把 harness 暴露成值班工程师愿意用的形态。可选项包括 TUI(终端 UI)、CLI(命令行)、Web 三类。该示例实现选择 TUI,基于 github.com/vadimdemede… 实现,原因有三:值班场景常在 SSH 终端里、TUI 可以边跑边显示工件生成进度、TUI 比 Web 更不容易让人顺手去点 AI 没把握的按钮。
CLI 适合 CI/CD 流水线里的无人值守场景,Web 适合需要多人协作或审批流的场景。选错了交互面,harness 即便功能正确,也不会被真用到值班现场。Ink 这类 React 风格的 TUI 库可以让工程师用熟悉的组件模型写终端界面,而不必手写 ANSI 转义序列。
元步骤:把这七步喂回给执行引擎
[数据] 现场不到 30 分钟构建出第一版,正是构建者把这七步以 markdown 形式贴进 Claude Code,让它反过来把 harness 代码生成出来,再用真实 Sentry 链接跑通端到端验证。这种"用 AI 帮你写 AI harness"的递归做法有两个前提:第一,前七步的描述必须极端具体——不能写"做一个适配器",要写"暴露 getEventById 与 listRecentReleases 两个动词,只返回堆栈前 5 帧";第二,定制提示的位置要在适配器层,而非全局 system prompt,这样模型行为才有边界。
[观察] 在该示例实现里,所有定制提示都内嵌在适配器函数里,而不是写在 harness 的全局 system prompt。这一选择让 harness 在切换模型时几乎不用改代码,只要换执行引擎即可。这是把 harness 写得像工程而不是像 prompt 的关键差异。
提示要点回顾
把上面的方法论浓缩成五条具体提示要点:
- 工作流必须极端具体 :不要写"AI 帮我们处理告警",要写"输入是 Sentry 链接,输出是 Linear 评论草稿,触发人是值班工程师"。
- 工具/适配器必须立场鲜明 :每个适配器只暴露必要的动词,字段裁剪要在适配器层完成。
- 定制提示位置要内聚 :把针对每个工具的提示放在适配器里,而不是散落在 system prompt。
- 优先采用 Agent SDK 承载智能体循环 :不要自己手写消息循环、上下文压缩、工具调度,这些是 SDK 的本职工作(参见 Claude Agent SDK 与 OpenAI Agents SDK 文档)。
- 权限与规则先于功能 :先决定 harness 不能做什么,再决定它能做什么。
完成这七步加一个元步骤之后,你拿到的不是一个"AI wrapper",而是一个可以在真实工单上反复使用、版本可控、权限有边界的工程制品。harness 的价值从来不在"用了 AI",而在"让 AI 在窄场景里比人更稳"。
生态视角:从 Cursor 到 Claude Code,一切皆 Harness
把视野从「我自己的 harness」拉远到整个生态,会发现一件反直觉的事:Cursor、Codex、Claude Code 这些被当作「AI 产品」讨论的工具,在结构上和该示例实现的 bug triage harness 没有任何本质区别——它们都是包着 AI 调用的代码。差别不在物种,只在厚度与意图。
Cursor 的编辑器侧栏、补全气泡、agent 模式,本质上是一组按特定意图编排好的工具调用、文件系统读写、终端命令与上下文注入规则。OpenAI Codex 在终端里接管 shell 的那一层也是 harness(OpenAI Codex 介绍);Claude Code 同样在用户本地机器上撑起一套跑会话、跑工具、跑子代理的运行时(Claude Code 文档)。把这些产品和该示例实现并排放在一起,看不出任何「物种级别」的差别,只看得见工程规模与产品意图的不同。
光谱两端:窄到只问一句,宽到替你写完整库
如果把市面上的 harness 放在一条光谱上,一端是极窄的规定性 harness。该示例实现的 Sentry 缺陷调查就属于这一端:输入只是一条 Sentry 告警 URL,输出固定为「证据 → 根因 → 后续工件」三段式 markdown;模型能调的工具屈指可数、能修改的目录被锁死、能跑的副作用被限制在「只读 + 写工件」以内。
[数据] 第一版在一次真实的 Sentry 告警现场不到 30 分钟内搭完,告警影响约 150 名用户且仍在每小时发生,这种紧迫性反过来逼出了 harness 的极窄边界。这种 harness 的价值不在于让 AI 更聪明,而在于让 AI 在一个被规定得很死的形状里稳定地产出。
另一端是极宽的通用编码 harness。Cursor 的 agent 模式、Codex CLI、Claude Code 都属于这一类:它们假定「用户想做什么都行」,于是尽可能多地暴露文件操作、终端执行、网络请求、MCP server 等能力,让模型自行决定下一步。代价是用户的提示词工程成本被推回到自己头上,同样的「修这个 bug」请求,跑出来的结果方差极大。
中间是广阔的自建空间。团队级的中等宽度 harness——例如只接管「前端组件库升级」、只接管「测试用例补全」、只接管「客户工单自动分诊」——才是大多数组织真正应该投入的位置。这条中间带没有明星产品,但每一条具体工作流都值得有一条专属 harness。
[数据] 该示例实现的 harness 主体只有约 8 个职责单一的功能文件,这正是窄到中等宽度区间里最舒服的体量——再多就开始变成宽 harness,再少就撑不起一次完整调查。
「wrapper 就是 harness」假说
过去几年中文社区对「AI 套壳」一词带有强烈的贬义,似乎在 LLM API 外面包一层 UI 就算不得真本事。但如果用 harness 的视角重新看,所谓 wrapper 之所以长期价值不高,绝大多数情况并不是因为「套壳」本身错了,而是因为它们套得太松。
具体来说,这些失败的 wrapper 既没有规定输入的结构,也没有规定输出的形状,更没有规定中间过程可以调什么、不能调什么;反观该示例实现的 bug triage harness,把结构化约束做到了一个极端。[观察] 一次完整的调查运行(run)在 UI 上只是「调查」(investigate-only) 这一种动作,模型既没有「修复」按钮可点,也无法通过任何命令触达源码目录。
它产出的工件包括指向 Sentry issue、相关 Linear ticket、相关 PR 的超链接,以及根因段落——这些工件是后续「是否要修复、谁来修、怎么修」决策的真相源。这种「窄到令人发指」的 wrapper,恰恰是把 LLM 调用从「玄学」变成「流水线」的关键。
所以更准确的命题应该是:wrapper 不是价值,结构化约束才是价值;而 harness 是结构化约束的一种具体工程形态。这条假说也解释了为什么很多所谓「通用 ChatGPT 套壳」活不下来、而 MCP 生态却在快速长起来(Model Context Protocol)——后者本质就是给 harness 提供可复用的「工具 + 约束」封装协议。
用通用 Agent 编排多个专用 harness
如果窄 harness 的稳定性和宽 harness 的灵活性都想要,下一步显而易见的形态是「通用 Agent + 多个专用 harness」。Claude Agent SDK 这类框架(Claude Agent SDK 文档)、OpenAI Agents SDK(OpenAI Agents SDK) 都已经在朝这个方向收敛。
上层是一个具备规划与路由能力的多模态 Agent,下层是若干个按 job to be done 拆开的 harness,每个 harness 知道自己负责的范围、知道自己的工件存哪里、知道自己的权限边界。在这种架构里,「让一个上层模型决定要不要触发 Sentry 调查 harness、触发后让 Claude Sonnet 4.6 在 harness 内执行、最后让一个汇总 harness 写 Linear 工单」就成了日常剧本。
路由层不写业务逻辑,业务逻辑在每个 harness 里;路由层只回答「现在轮到谁」。这是把杠杆做大的下一步形态,也是「多模型 + 多工具」从概念走向工程的关键拼图。
全文收束:一套可迁移的 harness 工程学
到这里,前面几节零散的方法论可以收成一张可迁移的清单。
第一,三要素:工作流、模型、约束 。任何一个 harness 在动手之前都得先把这三件事各自答完一遍;缺一个,后面就会出现「能跑但说不清为什么能跑」的尴尬,这是该示例实现迭代三版才稳下来的核心教训。
第二,时机判断 。当一个工作流被三个人以上每周重复两次以上,就已经值得为它造一条 harness;反过来,偶发且上下文高度开放的任务,继续让人 + 通用 Agent 处理更划算。
第三,架构分层 。建议至少分三层:编排层(决定路由)、harness 层(封装 job)、适配器层(对接外部系统) 。Sentry、Linear、GitHub、Vercel 的官方 API(Sentry 文档、Linear 文档、GitHub REST API、Vercel 文档)在适配器层各占一个文件,立场鲜明的 opinionated adapter 把「我们团队认为什么字段该这么读」明确写死。
第四,适配器立场化 。立场鲜明的适配器不是缺陷而是特性;团队和团队之间的差异,大部分都沉淀在这一层。把它藏起来才是问题,把它显式化反而是 harness 工程化的标志。
第五,工件真相源 。run、task、flag、artifact 这四类工件必须有统一的存储与命名约定,所有后续动作以读工件而不是重新调模型为起点。换句话说,harness 不只是把模型包起来,更是在工程里新建一个轻量级的、可审计的存储层。
第六,渐进放权 。从只读不写,到写工件不写代码,到写代码但不提交,到写代码并提 PR——每扩一档权限都对应一次 harness 的小版本升级。该示例实现之所以「先跑起来再收紧」能成立,正是因为每一轮迭代都在向放权阶梯的下一格迈进。
把这六条放在一起,得到的不是「又一个 AI 工具」,而是一门可以跨团队、跨业务、跨模型复用的 harness 工程学。它不需要明星产品来承载,也不依赖某个模型长期独占最优;它只需要团队愿意把工作流写下来、把约束写下来、把工件存下来。剩下的事,任何一代更强的模型进来,都能直接坐收红利。
参考来源
A 类 · 官方与一手资料
B 类 · 社区与延伸阅读