ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent落地成败一半在Harness:拆解执行循环、工具与上下文工程

Agent落地成败一半在Harness:拆解执行循环、工具与上下文工程

上个月有个做代码助手的同行问我一个问题:他们把底层模型换成了当时最新的一版,任务成功率只涨了三个点;后来花两周把外面那层执行循环重写了一遍,模型一个字没换,成功率涨了二十多个点。他问这正常吗。我说太正常了,做过智能体落地的人基本都有类似体感——决定一个 Agent 能不能真正干活的,模型权重只占一半,另一半在你套在模型外面的那层壳上。这层壳,现在圈内习惯叫它Harness。

Harness 这个词原本是马具、挽具的意思,套在马身上,把马的力气导到车上。放到 AI 语境里非常贴切:模型是那匹马,力气很大但方向不定;Harness 是挽具、缰绳、车辕和刹车,负责把模型的输出导成一步步可执行、可验证、可回滚的动作。它管的不是"模型怎么想",而是"模型想完之后,谁去执行、执行结果怎么喂回去、什么时候该停、出错了怎么办"。这篇文章就是把这层壳彻底拆开:它由哪几个部件组成、为什么各家做法最后都长得差不多、怎么从零搭一个能跑的最小版本、以及上生产前那些不踩一遍就学不会的坑。适合已经调用过模型接口、想把 demo 变成能交付的东西的读者,也适合只写过 prompt、还没接触过执行框架的同学——文中会补足必要的基础概念。

1. 把模型从聊天框里拽出来:Harness 补的到底是哪块空缺

1.1 一个反直觉的事实:换外壳的收益常常大于换模型

大部分人评估一个 AI 产品的能力时,第一反应是看模型排行榜。但排行榜上的分数,测的从来不是"裸模型",而是"某个团队写的 Harness + 那个模型"。同一份模型权重,配一个只会把用户输入原样转发、把回复原样返回的极简外壳,和配一个带工具调用、带结果校验、带失败重试的完整外壳,在同一个任务集上的完成率能差出二十到四十个百分点。这个差距不是玄学,来源非常具体。

举个具体的例子。让模型修一个代码仓库里的 bug。极简外壳下,模型只能输出一段"我认为应该这样改"的文字,改没改对没人知道,用户得自己复制粘贴、自己跑测试。带 Harness 的版本,流程是这样的:Harness 先把仓库结构和相关文件内容装配进上下文,模型输出一个"读取某文件"的动作,Harness 真的去读,把内容回灌;模型输出"改写某段",Harness 落盘;接着 Harness 主动跑一遍测试命令,把测试输出(包括失败堆栈)回灌给模型;模型看到自己改错了,再改一次;直到测试通过或者步数用尽。前者是"给建议",后者是"交结果"。这中间的差别,全部由外壳提供,跟模型有多聪明关系不大。

一个判断标准:如果你的系统在模型输出之后就没有任何代码逻辑了,那你做的不是智能体,是一个带界面的文本框。Harness 的存在感,正比于模型输出之后你还有多少工程代码在跑。

这也解释了一个常见困惑:为什么有些团队把模型换得更强,效果反而没提升甚至下降。因为旧 Harness 是围绕老模型的脾气调出来的——老模型喜欢啰嗦,所以裁剪得狠;老模型不擅长并行工具调用,所以一次只给一个工具。新模型风格变了,旧外壳的假设全部失效,能力被外壳卡住了。壳和模型是一对,得一起调。

1.2 Harness、Prompt、通用框架、Agent:四个容易搅在一起的概念

这四个词经常被混用,但它们指的东西不一样,分清楚了后面看架构才不会乱。

  • Prompt是最小单元,一段文字指令。它通常是 Harness 的一部分——Harness 负责在每一步动态拼装 system prompt(系统规则、可用工具清单、当前状态摘要),而不是让用户手写一段固定的。
  • 通用框架是脚手架原料,提供工具调用的抽象、消息结构、模型适配层。它解决的是"怎么写起来省事",不解决"这个任务该怎么收敛"。用同一个框架,两个人能搭出天差地别的 Harness。
  • Harness是为特定任务定制的执行体,包含主循环、工具集、上下文策略、终止条件、验证逻辑。它是"我这个任务专属的作业流程"。
  • Agent是对外呈现的产品形态,用户感知到的是一个会干活的助手。Harness 是它内部的骨架。

一句话概括:框架给你螺丝刀,Harness 是你用螺丝刀装出来的那台机器,Agent 是用户看到的那个能用的产品。

理解这个分层很重要,因为它决定了你优化时的着力点。效果不好时,新手的第一反应是改 prompt,第二反应是换模型;有经验的人会先看 trace,判断问题出在上下文装配、工具设计、还是终止条件上——这三样都在 Harness 层。

1.3 什么任务值得上 Harness,什么任务纯属给自己找麻烦

不是所有 AI 需求都值得套一层 Harness。判断依据是"任务是否具备可验证的中间状态"。

值得上的典型特征:任务需要多步、需要跟外部系统交互(读写文件、查数据库、调接口)、有客观的成功判据(测试通过、数据对得上、格式校验通过)、单次尝试成功率明显不够(比如只有五成)。这类任务上 Harness,收益是复利式的。

不值得上的典型特征:一次性生成、结果好坏靠人主观判断(写文案、起标题)、步骤极短。这类任务套 Harness 只是把一次调用变成三次调用,成本和延迟翻倍,成功率提升有限。我见过有团队给"生成商品短描述"套了七步循环加自我批判,结果是把一句三十字的话磨了四十秒,还经常越改越差。Harness 不是万能增益,它是用额外的 token 和延迟,换可控性和成功率;当任务本身不需要可控性时,这笔交易就是亏的。

2. 拆开外壳看内脏:一个 Harness 的六个必备部件

2.1 主循环:那个 while 比你想象的难写

所有 Harness 的心脏都是一个循环:装配上下文 → 调模型 → 解析出动作 → 执行动作 → 把结果追加回上下文 → 判断是否结束 → 回到开头。逻辑上五行代码,工程上有五处容易出事。

第一处是动作解析。模型返回的可能是结构化工具调用,也可能是一段自然语言里夹着动作描述,还可能是格式错误的 JSON。解析器必须能容错:格式错了不要直接抛异常终止,而是把"你的输出格式不对,正确格式是这样的"回灌给模型,让它自己修。经验上,格式纠错重试一次能救回八成的解析失败。

第二处是终止条件。至少要有四重保险:模型主动声明完成、达到最大步数、连续 N 步没有产生新的有效动作、总 token 预算耗尽。少了任何一个,你都会在某个深夜收到一张离谱的账单。我建议把最大步数设成"正常任务所需步数的 2.5 倍",既留了纠错余地,又不会无限放飞。

第三处是并行与顺序。模型一次可能吐出多个互不依赖的动作。并行执行能省时间,但如果两个动作有副作用(比如都写同一个文件),并行就是事故。稳妥做法是:只对明确标记为只读的工具开并行,写操作一律串行。

第四处是中断恢复。用户可能中途插话,进程可能被重启。循环的每一步状态都要能落盘,恢复时从最后一个检查点继续,而不是从头再来一遍——从头再来既贵又可能重复执行副作用。

第五处是流式输出与工具调用的冲突。用户想看到模型"正在思考"的实时文字,但工具调用需要完整解析。常见解法是把思考过程和工具参数分开流式:文字边出边显示,工具参数攒齐了再执行。

2.2 工具层:JSON Schema 只是入场券,描述才是胜负手

新手以为工具就是"写个函数 + 生成一份参数 schema"。真正决定工具好不好用的,是描述文本和错误信息。

工具描述要回答三个问题:这个工具干什么、什么时候该用它、什么时候不该用它。第三条最容易被忽略,也最有用。比如一个"读取文件"的工具,描述里写明"如果只是想确认文件是否存在,请用 stat 工具,不要用本工具读取全文",能省掉大量无谓的上下文占用。工具名也要讲究,动词开头、语义具体,"search_docs" 比 "query" 好,"run_tests" 比 "execute" 好。

工具的数量需要控制。经验值是同时暴露给模型的工具别超过十几个,超过之后模型的选错率明显上升。工具多了就分组:核心工具常驻,其余工具通过一个"列出更多工具"的元工具按需加载。

错误信息的设计同样关键。工具失败时不要返回一个裸的异常堆栈,也不要返回空。要返回三样东西:发生了什么(用自然语言)、可能的原因、建议的下一步。对比一下:

# 差的错误信息 return {"error": "KeyError: 'user_id'"} # 好的错误信息 return { "error": "查询订单失败:请求参数里缺少 user_id 字段。", "hint": "订单查询需要 user_id 或 order_no 其中之一。如果你只有订单号,请改用 query_by_order_no 工具。" }

后一种写法,模型大概率下一步就能自己纠正;前一种,模型会开始瞎猜参数,浪费三到五步。这一条改动成本极低,收益极高。

还有一点:工具必须幂等或者可判别。写文件、发消息、扣库存这类有副作用的操作,Harness 层要记录"这个动作是否已执行过",避免模型重试时造成重复副作用。

2.3 上下文装配与裁剪:真正决定成败的地方

如果只能优化 Harness 的一个部件,我会选上下文装配。因为模型每一步看到的世界,完全由这里决定。

上下文通常分四层,重要性从高到低:系统规则层(身份、硬约束、输出格式)、任务层(当前目标、进度、已完成事项)、工具结果层(最近若干步的观察)、历史对话层(更早的交互)。裁剪的原则不是"从旧到新切",而是按预算分层分配:系统规则永远完整保留,任务层动态更新摘要,工具结果层做单条截断,历史层做滚动摘要。

单条工具结果的截断尤其重要。一次文件读取可能返回几万字,一次命令执行可能刷屏几千行日志。直接塞进去,两三轮就把上下文撑爆。常见做法是设一个单条上限(比如两千 token),超了就保留头尾、中间用"...省略 N 行..."代替,同时把完整内容存到外部,给模型一个"用 offset 继续读"的工具。这样既不丢信息,又不炸上下文。

工具结果的格式也值得抠。JSON 比自由文本好解析,但对模型来说,冗余的括号和引号非常费 token。常见折中是:结构化数据用紧凑格式,人读的说明用自然语言,两者拼接时用清晰的分隔符。

一个容易被忽略的细节:上下文里的顺序会影响模型注意力。把最关键的约束放在上下文的最后(紧贴模型开始生成的位置),比放在最前面有效得多。这叫"近因偏好",几乎所有模型都有这个倾向。

还有一个高阶技巧叫约束重注入。长任务跑到十几步之后,模型会逐渐"忘记"系统规则里的硬约束(比如"不要修改配置文件")。解决办法不是加大 prompt 权重,而是每隔若干步把核心约束重新插一遍。成本很低,效果立竿见影。

2.4 状态、检查点与恢复:别把记忆托付给模型的上下文

新手最常见的架构错误,是把"任务进度"存在对话历史里,指望模型自己记住做到哪一步了。这在五步以内的任务上勉强能用,十步以上必然崩。

正确做法是状态外置:Harness 自己维护一份结构化状态——当前任务、已完成子目标、待办列表、已产生的文件变更、已调用过的工具及结果摘要。每一步把这份状态序列化落到本地存储,同时把其中一小段渲染成文字塞进上下文。模型看到的只是状态的一个视图,真相在 Harness 手里。

这么做有三个直接好处。一是可恢复:进程挂了,从最后一个检查点读回来即可。二是可审计:出问题时能精确回放"第几步、看到了什么、做了什么决定"。三是可压缩:状态是结构化的,压缩成摘要比压缩自由文本对话容易得多。

检查点的粒度也要想清楚。每一步都存全量快照,存储会爆炸;存得太粗,恢复时丢的步数太多。折中方案是增量存:每步存"这一步新增的消息 + 状态变更",定期做一次全量快照。

2.5 权限、沙箱与副作用隔离:出事之后才想起这块就晚了

这块在 demo 阶段完全感觉不到存在,在上生产的第一周就会给你上课。

沙箱要解决四件事:文件系统边界(只能读写指定目录)、网络出口(默认禁止,白名单放行)、资源上限(CPU 时间、内存、单次执行超时)、危险动作确认(删除、覆盖、对外发送这类操作,走人工确认或双阶段执行)。

我倾向的设计是"默认拒绝 + 显式授权":工具默认没有任何权限,注册时逐项声明它需要什么。这样即使模型被诱导去执行危险动作,也会在权限层被拦下来,返回一个明确的"该操作未获授权"错误,模型会转而寻找其他路径。

另一个常被忽略的是副作用隔离。凡是会改动外部世界的操作,都应该先在一个可回滚的环境里试跑,确认无误再落到真实环境。代码类任务好办——在临时副本里改、跑测试、通过了再合并。业务类任务麻烦些,常见做法是"预演模式":工具返回"如果执行会发生什么"的模拟结果,模型确认后再真正执行。

2.6 观测与回放:没有 trace,你的优化全是猜的

一个没有 trace 的 Harness,调优方式是"改一改,跑一跑,感觉好像好点了"。这种方式在小任务上还能凑合,任务一复杂就完全失效——你根本不知道成功率是从哪一步丢掉的。

要记录的东西按重要性排序:每一步的完整输入上下文(可以只存哈希 + 增量)、模型原始输出的完整文本、解析出的动作、工具的真实输入输出、耗时、token 消耗、以及这一步的判定结果(成功/失败/重试)。有了这些,你才能回答"这次失败是因为上下文没给到、还是工具选错了、还是工具执行本身失败了"。

trace 的另一个用途是构造回归集。把线上真实的失败案例脱敏后存下来,每次改 Harness 都跑一遍。这套东西建立起来之后,迭代速度会快一个量级——从"改完心里没底"变成"改完跑一遍,涨了跌了一眼看到"。

3. 为什么各家最后都长成一个样:架构收敛背后的四股力量

3.1 模型的能力边界,直接决定了循环必须存在

如果模型能一次性把复杂任务做对,那就不需要循环。现实是它做不到,而且失败模式很规律:多步任务里,步数越多,累积错误率越高。假设单步正确率 95%,十步串起来就只剩六成。循环的价值不在于让模型更聪明,而在于给每一步一个纠错机会——做完一步立刻验证,错了马上回退重来,把误差控制在一步之内,而不是让它滚雪球。

这也是为什么几乎所有 Harness 都有"验证"环节,而且验证往往是外部的:跑测试、跑编译、跑 schema 校验、跑对账。让模型自己检查自己,效果远不如让一个确定性的程序去检查。

3.2 成本与延迟,把"多智能体"按回了地面

前两年"多智能体协作"很热,一个任务拆给五六个角色互相讨论。真跑起来问题很明显:token 消耗是单体的三到五倍,延迟翻几倍,而且角色之间的"讨论"经常退化成互相附和,质量并没有提升。

现在主流的做法务实多了:默认单循环,只在明确需要隔离的场景下拆。什么场景需要拆?上下文差异极大的(一个角色只看代码,一个只看日志)、需要对抗性检查的(一个写一个审)、需要并行探索的(同时试三条路,选最好的)。除此之外,单循环加好工具,性价比高得多。

3.3 三种典型形态,对应三类任务

不同团队的做法看起来相似,细看其实分三派,各有适用场景。

形态结构优点代价适合
自由循环一个循环,模型自主决定下一步灵活,能处理没预设过的路径不可预测,容易跑偏,成本浮动大探索类、调试类任务
编排图预设节点和边,模型只在节点内决策可控、可测、成本稳定僵硬,遇到图外情况就卡住业务流程固定、合规要求高
多角色协作多个带独立上下文的执行体上下文隔离干净,可并行通信开销大,协调逻辑复杂需要多视角审查、超长任务

我个人的倾向是"编排图打底 + 局部自由循环":主干流程用图固定死,每个节点内部给模型一定自由度。这样既保证了关键路径可控,又不至于遇到意外就抓瞎。

3.4 收敛的深层原因:失败模式是客观的,解法自然趋同

各家做法趋同,不是因为互相抄,而是因为面对的是同一批客观失败模式。模型会幻觉出不存在的工具、会编造参数、会重复同一个无效动作、会在长上下文里忘记约束、会自信地宣布完成但实际没做完。这五类问题,不管哪家做,都得用同一套办法治:工具白名单、参数校验、动作去重、约束重注入、外部验证。工程约束摆在那里,最优解的空间本来就不大。

4. 从零撸一个能跑的最小 Harness

4.1 骨架:一百多行能跑通的最小闭环

先给一个能跑的最小版本,不依赖任何框架。核心是消息列表、工具注册表、和一个带熔断的循环。

import json, time, hashlib class MiniHarness: def __init__(self, llm, tools, max_steps=20, max_tokens=200000): self.llm = llm self.tools = {t.name: t for t in tools} self.max_steps = max_steps self.max_tokens = max_tokens self.messages = [] # 对话与工具结果 self.state = {"done": False, "notes": []} self.seen_actions = {} # 动作指纹 -> 次数,用于去重 self.tokens_used = 0 def build_context(self): """分层装配:系统规则 + 状态摘要 + 最近消息(带预算裁剪)""" sys_prompt = self.render_system_prompt() state_text = self.render_state() recent = self.trim_messages(self.messages, budget=60000) return [ {"role": "system", "content": sys_prompt}, {"role": "system", "content": "当前进度:\n" + state_text}, *recent, ] def step(self): ctx = self.build_context() resp = self.llm.chat(ctx, tools=[t.schema for t in self.tools.values()]) self.tokens_used += resp.usage.total_tokens self.messages.append({"role": "assistant", "content": resp.text, "tool_calls": resp.tool_calls}) if not resp.tool_calls: return self.judge_finish(resp.text) for call in resp.tool_calls: fp = hashlib.md5(f"{call.name}:{json.dumps(call.args, sort_keys=True)}".encode()).hexdigest() self.seen_actions[fp] = self.seen_actions.get(fp, 0) + 1 if self.seen_actions[fp] > 2: result = {"error": "这个动作你已经重复执行过,结果不会变。请换一种思路或换一个工具。"} else: result = self.run_tool(call) self.messages.append({ "role": "tool", "tool_call_id": call.id, "content": self.truncate(json.dumps(result, ensure_ascii=False), 4000) }) return False def run_tool(self, call): tool = self.tools.get(call.name) if tool is None: avail = ", ".join(self.tools.keys()) return {"error": f"工具 {call.name} 不存在。可用工具:{avail}"} try: return tool(**call.args) except TypeError as e: return {"error": f"参数不匹配:{e}", "hint": f"该工具的参数定义是 {tool.schema}"} except Exception as e: return {"error": f"执行失败:{type(e).__name__}: {e}"} def run(self, task): self.messages.append({"role": "user", "content": task}) for i in range(self.max_steps): if self.tokens_used > self.max_tokens: return {"status": "budget_exhausted", "steps": i} if self.step(): return {"status": "done", "steps": i + 1} return {"status": "max_steps", "steps": self.max_steps}

这段代码省略了render_system_prompt、trim_messages、judge_finish、truncate的实现,但主干逻辑都在了。注意我在里面埋了三个保护:重复动作熔断(同一指纹超过两次直接返回提示,不真的执行)、工具不存在时返回可用清单(引导模型自我纠正,而不是抛异常)、参数错误时回传 schema。这三处加起来不到十行,能挡掉实际运行中相当大比例的死循环和无效调用。

4.2 工具怎么写才不容易被误用

工具的注册信息不是越详细越好,而是越有区分度越好。三个同类工具的描述如果都是"查询数据",模型必然乱选。每一份描述都要能把"什么时候不该用它"说清楚。

@tool( name="read_file", description="读取指定文件的完整内容。只用于需要理解文件内容的场景。" "如果只想确认文件是否存在或行数,请用 file_stat,不要用本工具。" "单次最多返回 2000 行,超出部分请用 start_line 参数分页读取。", params={"path": "相对仓库根目录的路径", "start_line": "可选,起始行号,从 1 开始"} ) def read_file(path: str, start_line: int = 1): ...

分页这件事值得单独说。一个不受限的读取工具,是上下文爆炸的头号元凶。强制分页 + 明确告知"还有多少行没读",能让模型养成按需读取的习惯,而不是一上来就把整个目录树读一遍。

4.3 上下文预算的分配公式

预算不能拍脑袋,得有分配。我的经验分配是这个比例:系统规则与工具定义占 15%,任务状态摘要占 10%,最近若干轮工具结果占 60%,更早历史的压缩摘要占 15%。这个比例不是死的,任务越偏"探索",最近结果那部分可以再放大;任务越偏"流程化",状态摘要的占比可以提高。

压缩策略上,我推荐两级:第一级是单条截断(超过阈值的工具结果保留头尾),第二级是滚动摘要(把超过 N 轮的旧消息交给模型总结成三五条要点,替换原文)。摘要这一步要小心,别把关键的具体值(文件路径、错误码、ID)总结掉了,最好在摘要指令里明确要求"保留所有具体路径、编号、报错原文"。

4.4 什么时候该认输:失败分类比重试次数更重要

重试不是万能药。要先分清失败类型,再决定重试还是不重试。

失败类型典型表现处理方式
格式错误输出的 JSON 解析不了回传格式要求,重试一次
工具选错调了不该调的工具,参数明显不对回传可用工具清单和各自用途,重试
参数缺漏缺必填字段回传 schema 和缺失字段名,重试
执行失败工具真跑了但报错原样回传错误信息,让模型决策
方向性错误连续三步都在做无效的事不再重试,注入提醒或直接终止

最后一行是关键。有些失败是"再试一次就好",有些失败是"再试一百次也一样"。判别方法是看最近几步是否在同一个状态空间里打转——动作指纹重复、或者工具结果的相似度极高,就说明卡住了。卡住的时候,正确的做法不是重试,而是把当前状态和"你已经连续三次做了类似的事但没进展"这个事实告诉模型,逼它换策略。如果换了策略还是不行,那说明这个任务超出了当前能力,该体面地失败,把中间结果和卡点交给人工。

5. 上生产前必须踩过的那些坑

5.1 工具描述含糊导致的"假调用"

最典型的一幕:模型自信地调用了一个工具,参数看起来像模像样,但字段名是它自己编的。原因是工具描述里参数说明写得太抽象,比如params={"filter": "过滤条件"}。模型不知道这个 filter 是 SQL 语法、是 JSON、还是自然语言,于是自己发明了一种。

修法很直接:所有参数都要给类型和示例。"filter": "SQL WHERE 子句片段,例如 status = 'active' AND created_at > '2024-01-01',不要包含 WHERE 关键字"。加上示例之后,参数错误率通常能掉一大半。

还有一种假调用是"工具存在但当前不该用"。比如任务已经完成了,模型还在反复调用验证工具。这是终止条件设计的问题——judge_finish不能只看模型说什么,还要看客观状态(测试是否真的通过了、目标文件是否真的存在了)。

5.2 上下文腐烂:跑到第十步,模型开始不听指令

这个现象很普遍:任务前期一切正常,跑到七八步之后,模型开始违反最开始定下的硬约束,比如明明说了"不要改动依赖文件",它还是改了。原因不是模型变笨了,是那些约束在上下文里被几十条工具结果挤到了最边缘的位置,注意力权重天然就低。

解法就是前面提过的约束重注入:在每一步装配上下文时,把三到五条最硬的约束单独拎出来,放在紧贴生成位置的地方。我在几个任务上都试过这个改动,违反率能从两三成降到个位数。成本是每步多几百 token,非常划算。

5.3 死循环的三种变体

一种是完全重复:同样的工具、同样的参数,反复调用。熔断指纹能挡住。

一种是微小变化:参数每次都稍有不同(比如分页的 offset 每次加一,但读的是空内容),指纹挡不住。要在工具层拦——工具自己发现"这次返回的和上次一样或为空",直接返回"没有更多数据了,请停止翻页"。

还有一种是震荡:A 改文件,B 改回来,来回拉锯。这种最难自动检测,因为它每一步都是合法的、有效的。解法是记录文件的变更历史,发现同一个文件在短时间内被反复改回原状时,注入提示"你在两个方案之间反复切换,请选定一个并说明理由"。

5.4 过度并行带来的资源踩踏

模型学会了并行调用之后,会在一步里发五六个工具请求。如果这些请求里有三个是写操作,后果是灾难性的——同时写同一个文件、同时发起同一笔支付、同时扣同一份库存。

防御必须做在 Harness 层,不能指望模型自觉。我的做法是:工具注册时标注readonly=True/False,并发调度器只对只读工具开并发,写操作全部排队串行。另外再加一个全局的并发上限,防止模型一次发二十个请求把下游打挂。

5.5 成本失控的两个隐蔽来源

显性成本好控制(看账单)。隐蔽的有两个。

第一个是上下文被重复计费。每一步都把完整上下文重新发一遍,如果上下文有一万字,跑二十步就是二十万字的输入。这是 Harness 结构的固有开销,没法完全消除,但可以通过缓存(相同前缀的上下文命中缓存)、精简工具定义、及时裁剪来压。前缀缓存对多步任务的效果尤其明显,因为系统规则和工具定义在每一轮都是完全相同的。

第二个是验证环节的重复执行。有些 Harness 每步都跑一遍完整测试套件,一个两百个测试用例的仓库,二十步就是四千次测试执行。正确做法是分层验证:每步只跑相关的子集,阶段性节点才跑全量。

5.6 一个反直觉的经验:工具越少,任务成功率越高

这条我踩过。早期做代码助手时,为了让模型"什么都能干",一口气装了二十多个工具:读文件、写文件、搜代码、跑测试、查依赖、看日志、格式化……结果模型经常在"该用搜代码还是该用读文件"上纠结,或者用一个笨工具去完成本该用另一个工具干的事。

后来砍到九个工具,把相似功能合并(比如搜索和读取合成一个带mode参数的工具),成功率反而上去了。原因很简单:工具选择本身就是一个消耗注意力的决策。选项越少,决策越准。如果确实需要很多能力,用"工具分组 + 按需加载"的方案,而不是把所有选项摊在模型面前。

6. 怎么判断你的 Harness 是好是坏:评估与迭代

6.1 别只看成功率,要看四类指标

只盯成功率会误导人。一个成功率 40% 但每步都很便宜的系统,和一个成功率 60% 但成本翻三倍的系统,哪个更好取决于你的业务场景。我一般同时看四个数。

指标含义关注点
任务完成率客观判据通过的比例核心指标,但要按任务类型分桶看
平均步数完成任务消耗的循环次数突然变多往往意味着某处退化
单位任务成本token 消耗折算与完成率一起看才是效率
人工干预率需要人接手或确认的比例决定能不能真正无人值守

除了这四个,还有一个我很看重的隐性指标:失败模式分布。把失败案例归类,看是"模型能力不够"占大头,还是"工具设计有问题"、"上下文没给到"、"终止条件太早"占大头。如果后者占多数,说明你的 Harness 还有很大优化空间,而且这些优化的性价比远高于换模型。

6.2 回归集怎么攒:从线上失败里长出来

评估集不要凭空造。最好的来源是线上真实失败案例。做法是:每次线上失败,把当时的完整 trace 脱敏后存一份,标注失败原因的分类。攒到几十条,你就有了一个针对性极强的回归集——它测的都是你的系统真正会犯的错。

跑回归的时候注意两点。一是一次只改一个变量。同时改了上下文裁剪和工具描述,涨了也不知道是谁的功劳,跌了也不知道该回滚哪个。二是关注分布而不是均值。均值涨了 2%,可能是某个短板任务涨了 20%,也可能是所有任务都微涨,这两种情况对应的下一步动作完全不同。

6.3 迭代的优先级排序

改 Harness 的收益不是均匀分布的。按我自己的经验,性价比从高到低大致是这样:

  1. 修工具的错误信息。半天工作量,通常能带来最明显的成功率提升。
  2. 加重复动作熔断。几十行代码,直接砍掉最多的一类无效消耗。
  3. 做约束重注入。改上下文装配的一小段,解决长任务的指令漂移。
  4. 重新设计上下文预算分配。需要一点实验,但天花板很高。
  5. 换主循环结构(比如从自由循环改成编排图)。工作量大,收益取决于任务形态。
  6. 换模型。放最后,因为前面的都没做完就换模型,大概率白换。

这个顺序不是绝对的,但它反映了一个判断:大部分 Agent 的问题出在工程层,不是模型层。先把工程层的洼地填平,再考虑模型的能力上限,顺序反了就是白烧钱。

最后再分享一个我自己踩过的小坑。早期我很迷信"让模型输出思考过程",觉得想得越多做得越对。实际跑下来,长思考在简单任务上纯属浪费,在复杂任务上又容易陷入自我怀疑式的循环。后来的做法是分级:简单任务关掉详细思考,复杂任务才开,而且在循环里加一个"思考长度上限"的软约束。这个改动让平均延迟降了三成,成功率没掉。很多 Harness 的调优就是这样,不是加东西,是知道什么时候该少加。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进