Harness Engineering这个说法,这两周几乎是以刷屏的方式出现在我关注的好几个技术社群里。有人把它翻译成“控制框架”,有人叫它“工程束”,但不管叫什么,大家讨论的核心其实非常一致:大模型的能力边界已经摆在那了,决定Agent(智能体)到底好用还是难用的,越来越多地取决于模型外面那层“脚手架”搭得好不好。我自己的项目在Deep Agents(深度智能体)上断断续续折腾了大半年,对这一点的体会尤其深。
所谓Deep Agents,简单说就是那些要自主完成多步复杂任务的智能体:比如“帮我排查这个仓库里的内存泄漏,定位后直接把补丁提交上来,再跑一遍测试”,或者“去调研这三家竞品的最新动态,整理成一份带结论的分析报告”。这类任务没法靠一次对话搞定,Agent必须自己拆解目标、规划步骤、多次调用工具,还要在中间根据结果调整方向。而Harness Engineering,就是负责把这些能力“武装”到一个普通模型身上的工程体系——它不改变模型本身,但决定了模型能把本事发挥出几分。
这篇文章我不打算讲什么宏大理论,就老老实实把我这段时间做Deep Agents时关于Harness的思考、拆解、落地代码和踩坑记录整理出来,给同样在折腾Agent的朋友一些可参考的素材。如果你发现自己的Agent总是“看起来聪明、用起来拉胯”,那问题大概率不在模型,而在harness。
1. 为什么Harness Engineering突然被推到了台前
1.1 先理解Deep Agents真正难在哪里
很多人刚开始做Agent时会有一个错觉:只要底层模型够强,Agent就能自动处理好一切。但这个想法在真实任务里很快会被打击得体无完肤。同一个模型,用裸的对话接口让它去调用工具,它可能连参数都填不对;但换上精心设计的工具定义和调用流程之后,它能稳定完成非常复杂的多步操作。差别在哪?就差别在那层让模型“更容易做对事”的工程设施上。
拿我做过的一个数据分析Agent来说,初始版本给它接了五六个工具,让它自己决定怎么查数据库、怎么做统计、怎么出报告。听起来很灵活对吧?实际跑起来却是另一番景象:它经常在没必要的时候调用工具,把简单问题复杂化;有时候查一次就够了,它非要查五六次;更让人抓狂的是,它偶尔会忘记之前查到的结果,反复执行同一个查询。这些问题没有一个是模型“变笨了”,而是我根本没有给它搭好一套行为的约束和引导框架。
Deep Agents之所以难,恰恰因为它的能力来自“模型推理”和“外部工具”的组合。组合带来的自由度很高,但自由度本身就是双刃剑。没有规则,模型就会在庞大的可能性空间里东冲西撞。而Harness Engineering要做的,就是把这个自由度有意识地收敛成一套可预期、可控制、可复用的执行模式。它不是限制模型的发挥,而是把模型的能力往正确的方向引导。
1.2 Harness的定位:不是模型,而是模型身边的“整套装备”
你可以把基础模型想象成一名刚毕业的高材生——底子很好,脑子转得快,但没经验,不知道职场里的具体规则和打法。Harness就是这位高材生入职后得到的整套装备:工位上的操作手册(系统提示词)、内部系统的使用指南(工具说明)、每天的待办清单(任务拆解机制)、还有遇到问题时的上报流程(错误处理)。
这套装备的质量,往往比员工本身的聪明程度更容易决定最后产出好不好。一个脑子很灵但没有流程的人,做起事来依然可能一团乱;一个能力中等但手里有一套清晰SOP和趁手工具的人,反而能稳定交付。同样的道理,Harness Engineering 的核心任务就是用一套系统的方法,把“模型该看什么、能碰什么、怎么碰、碰错了怎么办”全部明确下来。
这个视角一转变,很多问题就豁然开朗了。以前Agent表现不好,我的第一反应是“要不要换个更大的模型”;现在我的第一反应是“是不是harness的哪里没有设计好”。后者的改动成本低得多,而且往往效果立竿见影。Anthropic那篇流传很广的文章里把workflow和agent做了区分,其实讲的也是这个东西:你要明确agent的外围机制是什么,而不是让模型在每次调用时临时抱佛脚去猜。
2. 一个合格Harness的模块拆解
2.1 工具层:工具描述比工具本身更重要
工具层是Harness里最基础也最容易被忽视的一部分。很多人的做法是把函数签名丢给模型就算完事,但实际效果差得远。这里有个关键认知:模型对工具的理解,完全建立在工具描述的文本之上。它看不到你的函数内部是怎么实现的,也猜不到某个参数的隐含规则——你写在描述里的内容,就是它能知道的一切。
我在做SQL Agent时,第一版工具描述只写了“执行SQL查询,返回结果集”。结果模型经常把UPDATE语句也发过来了,虽然我的接口在底层做了拦截,但一次好好的数据分析任务就卡住了。后来我把描述改成了“在只读副本上执行SQL查询,仅允许SELECT;查询超时30秒;返回最多100行;结果按表格形式返回”,并且把参数说明补充完整。从那以后,模型误用工具的几率大幅下降。
工具描述里值得注意的几个点:第一,明确边界,能做什么、不能做什么,要写清楚,模型不是人,不会“察言观色”;第二,把参数的含义、格式、单位、可选项都写明白,模型对参数的填写准确度会肉眼可见地提升;第三,如果有依赖关系或调用顺序,最好直接写在描述里,比如“该工具应该先于analyze_trend调用”。这些信息看着琐碎,但每一条都可能在关键时刻避免一次无效调用。
2.2 上下文层:模型“看到的信息环境”需要精心编排
Deep Agents在运行过程中,上下文(context)是动态累积的。每调用一次工具,返回结果都会塞进对话历史;每走一步,模型都会基于前几步继续推理。如果这个累积过程不加以管理,上下文很容易变成一个臃肿、杂乱、充满噪声的信息池。模型就像在一间堆满杂物的房间里找钥匙,不是找不到,而是找得很吃力。
上下文层要解决的核心问题有三个:保留什么、压缩什么、丢掉什么。系统提示词和最初的用户任务属于“保留”范畴,它们是模型理解目标的锚点;工具返回的原始数据往往是“压缩”的主要对象——尤其是日志、列表、长文内容,全量塞进去既浪费token又稀释注意力;而那些已经被消化过的中间推理过程,则可以考虑折叠成更简洁的摘要。
我常用的做法是给历史消息做分层管理。最近几轮的用户消息、助手消息、工具结果保持完整。更早的轮次,尤其是那些已经完成使命的中间步骤,则替换为一段模型生成的摘要——这个摘要可以专门由一次“压缩调用”产生,也可以在上一步结束时顺手让模型输出。这种分层策略能让模型始终聚焦在“当前最重要”的信息上,而不是被历史细节淹没。
2.3 控制层:循环、终止条件与行动约束
控制层是Harness里最像一个“程序”的部分。它负责回答几个关键问题:Agent在执行过程中如何循环?什么条件下认为任务已经完成?如果一直得不到结果怎么办?模型是否允许连续调用多个工具,还是只能一步步来?
我见过不少新手做Agent时,连一个最基础的最大调用轮次都没有设置,结果模型在一个任务上反复横跳,把API账单跑出天价才被发现。这其实是Harness设计里最低级、但也最常见的失误。控制层至少要做到:限制最大轮次、设定任务完成的判断条件、在超出限制时给出可读的错误现场。
另外,行动约束也很重要。有些任务允许多工具并行调用(比如同时拉取多个接口),有些则必须串行(比如依赖前面工具的输出)。Harness应该根据任务类型施加相应的约束,而不是放任模型自由发挥。自由是Agent的天性,但约束才是工程化的前提。
3. 实操:如何从零构建一套可落地的Harness
3.1 一个最小的Deep Agent主循环
下面这段代码是我在项目里实际使用的一个极简Harness骨架。它把上面说的几个模块都串起来了:一个最大轮次限制、一个工具注册表、一个简单的记忆容器。用来跑通流程、做原型验证绰绰有余。
from typing import Callable, Dict, List, Any class ToolSpec: def __init__(self, name: str, description: str, parameters: Dict[str, Any], func: Callable): self.name = name self.description = description self.parameters = parameters self.func = func def to_openai_schema(self) -> Dict[str, Any]: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, }, } class Harness: def __init__(self, model: Callable, tools: List[ToolSpec], max_turns: int = 10, system_prompt: str = ""): self.model = model self.tool_map: Dict[str, ToolSpec] = {t.name: t for t in tools} self.tool_schemas = [t.to_openai_schema() for t in tools] self.max_turns = max_turns self.messages: List[Dict[str, Any]] = [] if system_prompt: self.messages.append({"role": "system", "content": system_prompt}) def run(self, task: str) -> Any: self.messages.append({"role": "user", "content": task}) for turn in range(self.max_turns): response = self.model( messages=self.messages, tools=self.tool_schemas, tool_choice="auto", ) self.messages.append({ "role": "assistant", "content": response["content"], "tool_calls": response.get("tool_calls", []), }) tool_calls = response.get("tool_calls", []) if not tool_calls: # 模型没有要求调用工具,视为任务完成 return response["content"] for call in tool_calls: result = self._execute_tool(call["function"]["name"], call["function"]["arguments"]) self.messages.append({ "role": "tool", "tool_call_id": call["id"], "content": result, }) # 超过最大轮次,返回现场信息而不是空白 raise RuntimeError(f"Agent exceeded max_turns={self.max_turns}") def _execute_tool(self, name: str, args_json: str) -> str: tool = self.tool_map.get(name) if not tool: return f"Error: unknown tool '{name}'" try: args = json.loads(args_json) result = tool.func(**args) return json.dumps(result, ensure_ascii=False, default=str) except Exception as e: return f"Error executing {name}: {type(e).__name__}: {e}"这段代码虽然短,但已经覆盖了控制层和工具层的基本职责。几个关键设计点:超轮次之后不是直接return一个空值,而是抛出异常并保留现场信息,这样方便排查问题;工具执行出错时,把异常信息作为工具返回值返回给模型,而不是让整个流程崩溃——模型有时候能根据错误信息自我修正,这是个廉价但有效的容错手段。
3.2 系统提示词设计:给Agent立规矩
系统提示词是Harness里成本最低、影响面却非常广的一个杠杆。一个好的系统提示词,应该让模型在没有额外思考负担的情况下就知道:自己的角色是什么、做事的基本顺序是什么、遇到什么情况应该停下来、最终交付物长什么样。
我给数据分析Agent写的系统提示词大致分四段。第一段定义角色:“你是一名资深数据分析师,通过工具分析数据并回答用户问题”。第二段规定流程:“接到任务后,先判断需要哪些数据,再调用工具获取数据,最后基于数据给出结论;如果数据不足,可以多次查询”。第三段说明输出要求:“最终回答必须包含核心结论、关键数据、可能的局限性”。第四段设置自我检查:“在给出最终结论之前,检查你是否已经掌握了足够的证据,如果没有,先补充查询”。
这里要特别注意克制——提示词不是写得越多越好。过于冗长的提示词会稀释模型的注意力,反而让重要的约束起不到作用。我踩过一次坑:给一个Agent写了长达两千字的系统提示词,结果它在简单任务上的表现反而变差了,因为注意力被太多规则分散。后来精简到五六百字,效果立刻回升。提示词工程和Harness设计一样,核心不是“多给信息”,而是“给到关键信息”。
3.3 工具结果的后处理与压缩策略
工具调用返回的结果不一定适合直接塞给模型,尤其是那些本身就很长的内容。比如爬虫返回的完整网页HTML、数据库返回的几千行查询结果、日志检索工具返回的原始日志片段。如果原封不动推进上下文,不仅浪费token,还会让模型在后续推理时被噪声干扰。
我的经验是给每个工具设计一个独立的后处理器。拿SQL查询来说,返回结果如果超过一定行数,就截断成前N行,并在末尾附上一句“结果共1234行,以上为前20行”。拿网页抓取来说,先用简单的文本提取把HTML标签剥掉,再截断到目标长度。这些后处理逻辑看似琐碎,却是维持整个Agent上下文健康的关键措施。
除了逐工具处理,还可以在Harness层面做一层全局的“记忆压缩”。当一个任务执行了很久、历史消息非常多时,我会把早期轮次压缩成摘要,替换掉原文。这个摘要可以由模型自己生成(调用一次带引导的文本总结),也可以简单地用“最终结论+关键步骤”这种手工模板。实测下来,模型生成的摘要更自然,但成本略高;模板方式更省,适合对成本敏感的场景。
3.4 结构化输出:让模型的中间决策可以被程序消费
Deep Agents不只关心最终答案,更关注模型在中间过程中做出的决策。工具调用是否合理?下一步打算做什么?有没有偏离任务目标?这些信息如果只是埋在自由文本里,程序没法有效地检查和干预。所以我会要求模型在某些关键节点输出结构化的中间结果,最常见的就是JSON格式的“下一步行动计划”。
还是那个SQL Agent,我会让它在每次工具调用前,先输出一个JSON块:包含“当前理解”(用一句话说明当前进度)、“下一步动作”(调用哪个工具、参数是什么)、“为什么这样做的理由”。这个JSON块可以被Harness解析,用于记录审计日志、做异常检测(比如发现模型重复同一个动作三次)、甚至在必要时中断流程。相比完全靠模型在工具调用的参数里自解释,这种结构化输出让整个系统的可控性强了很多。
实现方式其实不复杂,只要在系统提示词里给出明确的JSON格式模板,并在解析时允许一定的容错(比如用正则提取JSON片段),就能稳定工作。少数情况下模型输出不合法JSON,我会把解析错误作为一条工具消息返回,提示模型修正格式——这和工具执行出错时的处理策略是一致的。
4. 常见问题与排查技巧实录
4.1 模型反复调用同一个工具,陷入原地打转
这是Deep Agents运行中最容易遇到的现象:模型好像卡住了一样,一遍又一遍地查同一个数据、执行同一个动作,只是参数略微变化,甚至参数完全一样。归根到底,是因为模型在上下文里缺少一个“这个动作已经做过了”的信号。
我在Harness里加了两道防线。第一道是在工具结果返回时,加入一个简短的历史提示:“你在第3轮执行过相同查询,当时的结果是XXX。请不要重复相同查询,除非你有新的筛选条件。”这话虽然简单,但对模型的影响非常直接。第二道防线是程序层面的检测——在控制循环里检查工具调用签名是否重复,如果发现连续N次同样的调用,就强制终止并抛出“Detected repetitive behavior”错误。两道防线结合起来,能把死循环的发生率降到极低。
4.2 模型编造工具结果,直接给出看似合理的答案
这个问题比较隐蔽,也非常危险。当模型认为“自己知道答案”时,它可能不调用工具,直接在文本里把结果编出来,而且语气非常肯定。这种情况在模型面对不熟悉的领域时尤其常见——它会把训练数据里的记忆当成事实,一本正经地输出。
Harness层面的应对思路是:在系统提示词里强调“所有数据必须来自最新工具调用的返回结果,禁止根据常识猜测数据”,并且在控制循环里不轻易接受“无工具调用”的回复。对高风险场景,我甚至会强制要求模型在给出最终答案前至少调用过一个工具。这种方式虽然有点“暴力”,但对防止幻觉非常有效。
4.3 上下文窗口溢出,怎么及时止损
上下文窗口溢出是另一个高频事故,尤其在任务步骤多、工具返回长文本的时候。等到报错才处理,往往已经晚了。我的做法是提前干预:在每次工具返回后检查当前消息总token数,超过阈值时先触发压缩逻辑,把历史摘要化之后再继续。
压缩策略要灵活。如果最占空间的是某个大型工具结果,而且它已经被后续步骤用过了,那可以直接删除,用一个简短的文字摘要替代。如果占用的是模型的中间推理过程,可以让模型自己总结成三句话。这个提前干预机制,几乎杜绝了我项目里的上下文溢出问题。注意阈值不要设到模型窗口的100%,留个20%的缓冲给后续步骤是更稳妥的选择,不然压缩动作本身就是一次额外的token消耗。
4.4 模型调用工具时反复填错参数
填错参数这个问题的根源通常是工具描述不够具体。比如一个接收日期字符串的参数,你没写清楚格式,模型就可能一会儿传“2024-01-01”,一会儿传“2024/01/01 00:00:00”,导致工具端解析失败。
解决问题的路径有两条。第一是在描述里把格式精确到示例级别:“日期格式必须为YYYY-MM-DD,示例:2024-01-01”;第二是在工具执行端做宽容处理,比如把日期解析封装得更鲁棒,能自动识别多种格式。但两条路各有优劣:前者的成功率更高,因为模型能直接遵循;后者更省事,但对格式的混乱可能无解。最合理的做法是先用描述解决问题,再在工具端兜底。
5. 后续还能往哪些方向延伸
5.1 让Harness拥有“自我反思”能力
这是目前我用下来性价比最高的进阶优化之一。具体做法是:在Agent执行完一轮完整任务后,增加一个额外的反思调(reflection turn),让模型回顾整个过程,指出自己哪些步骤是多余的、哪些信息是缺失的、如果重新来过会怎么调整。然后把反思结论作为输入,让模型重新执行或修正结果。这个机制对提升最终答案质量很有帮助,代价是额外的token消耗和一倍的运行时间,是否启用要看具体场景的容错要求。
5.2 为不同任务类型定制不同的Harness模板
Deep Agents的一大误区是把所有任务都塞进同一个通用Harness里。实际工程中,数据分析、代码开发、网页操作、客服对话,它们对工具、上下文、控制逻辑的需求差异非常大。比如代码开发Agent需要把文件读写和命令行执行作为一等公民,而数据分析Agent更看重查询结果的表格化呈现。与其找一个万能模板,不如为每个高频场景做一套带默认工具的Harness配置,跑起来更顺手,调试方向也明确很多。
5.3 观察性建设:没有日志,就没有调试的抓手
最后这一点虽然不性感,但可能是我最想强调的。Deep Agents天然是非确定性的,如果Harness里没有日志系统,一次失败可能根本找不到原因。我在Harness里记录了每一次模型决策、工具调用的输入输出、耗时、token消耗、以及最终结果。遇到问题回放日志时,往往一眼就能看出是模型跑偏了、工具报错了,还是上下文被污染了。观察性不是收尾工作,而是Harness工程里面向长期迭代的基础设施。
我在实际项目中最大的体会是,Deep Agents的优化空间远比想象中大,但它不属于模型侧,而属于外围的Harness侧。与其反复纠结要不要换更强的模型,不如先把手里的Harness打磨到位。工具描述、上下文管理、控制循环、错误恢复——每一项打磨下去,都能看到Agent稳定性的肉眼可见提升。做Agent确实有运气的成分,但把Harness工程做到位,等于把运气变成了稳定发挥。