☰
Agent能力扩展的陷阱:Harness复杂度如何导致旧能力遗忘
2026/10/27 17:42:48 网站建设 项目流程

1. 从"堆料式增强"到"能力遗忘":一个被忽视的Agent困局

过去一年多,我参与过好几个Agent项目的搭建和调优,从最早的简单工具调用,到后面接入十几个Skill、挂载各种外部工具,一路走来最大的感受就是:大家默认"加得越多,能力越强"。团队里讨论方案时,最常见的思路就是"这个能力缺,那就再加一个工具""这个场景覆盖不到,那就再挂一个Skill"。仿佛Agent是一个可以无限扩容的容器,往里塞的东西越多,它就越聪明。

但实际跑下来,情况往往不是这样。我印象特别深的一次,是一个原本在纯文本推理任务上表现很稳的Agent,在接入了五六个外部工具之后,简单的事实问答反而开始出错——它会莫名其妙地去调用一个根本不需要的工具,或者在多轮对话里"忘记"了前面已经确认过的信息。当时我们第一反应是"模型不行",换了更大的模型,结果问题依旧。后来才慢慢意识到,问题可能不在模型本身,而在于我们给Agent套上的那层"外壳"——也就是Harness。

所谓Harness,直白点说,就是包裹在模型外面的那套工程框架:它负责组织提示词、管理工具调用、拼接上下文、处理多轮状态、做输出解析。你可以把它理解成模型的"驾驶舱"和"操作系统"。模型是发动机,Harness是方向盘、仪表盘和传动系统。发动机再强,如果传动系统设计得别扭,车照样跑不快,甚至会把发动机本身的性能给拖累掉。

最近有个新基准的研究结论,恰好戳中了这个痛点:当Harness变得越来越复杂、接入的工具和Skill越来越多时,Agent原有的、不依赖这些工具的基础能力,反而可能先被"忘掉"。这个结论听起来有点反直觉,但如果你亲手搭过Agent,就会觉得"太真实了"。它解释了我们之前遇到的那个怪现象——不是模型变笨了,而是Harness的复杂度把模型的注意力"挤占"了。

这篇文章我想聊的,就是围绕这个现象展开的一整套思考:Harness到底在Agent架构里扮演什么角色,它和Agent本身有什么区别,为什么"越加越强"是个危险的假设,以及在实际工程里,我们该怎么设计Harness才能既扩展能力、又不丢掉旧本事。适合正在做Agent开发、工具集成、Skill编排的同行参考,也适合刚接触Agent架构、想搞清楚"工具、Skill、Agent、Harness"这几个词到底啥关系的朋友。

2. Harness和Agent到底差在哪:先把概念的地基打牢

2.1 一个容易被混淆的边界

很多人第一次听到Harness这个词,会下意识觉得"这不就是Agent吗"。其实两者是包含关系,不是等同关系。Agent是一个完整的功能实体,它对外表现出"能感知、能决策、能行动"的整体行为;而Harness是支撑这个实体运转的工程层,它不直接产生智能,但决定了智能怎么被调用、怎么被约束、怎么被传递。

打个比方。Agent像是一家餐厅,顾客点菜、后厨出餐、服务员上菜,整套流程跑通才叫"这家餐厅能营业"。Harness则是这家餐厅的管理系统:菜单怎么排版、订单怎么流转、后厨和前厅怎么沟通、库存怎么盘点。系统设计得好,餐厅运转顺畅;系统设计得烂,哪怕厨师手艺再好,顾客也可能等半小时吃不到菜。

在实际代码里,这个边界体现得很清楚。一个典型的Agent调用链大致是这样的:

# 简化示意,非真实生产代码 def agent_step(user_input, history, tools, harness): # Harness负责的部分 prompt = harness.build_prompt(user_input, history, tools) context = harness.assemble_context(prompt, history) # 模型负责的部分 raw_output = model.generate(context) # Harness再次介入 parsed = harness.parse_output(raw_output) if parsed.is_tool_call: result = harness.execute_tool(parsed.tool_name, parsed.args) return harness.format_tool_result(result) return parsed.final_answer

你看,模型真正"思考"的只有model.generate那一行,其余全是Harness的活儿。Harness的复杂度,直接决定了模型能拿到什么样的输入、它的输出会被怎么解读。这就是为什么Harness一变复杂,模型的行为就可能跟着变——因为模型看到的"世界"变了。

2.2 工具、Skill、Agent三层能力的关系

热词里反复出现"工具""Skill""Agent""Harness"这几个词,它们其实是一条能力递进链上的不同层级,我习惯用下面这张表来梳理:

层级本质典型形态依赖关系
工具(Tool)单一原子能力一个函数、一个API、一个命令行被Skill或Agent调用
Skill一组工具+流程编排一个可复用的任务模块依赖若干工具,被Agent调度
Agent完整决策实体能自主规划、调用Skill和工具依赖Harness运行
Harness工程支撑层提示词管理、上下文拼接、工具路由包裹Agent,不产生智能

这个分层很关键。工具是"手",Skill是"一套动作",Agent是"会做决定的人",Harness是"这个人所处的办公环境"。环境越复杂,人越容易分心——这就是新基准发现的那个现象的本质。

我见过不少团队,一上来就猛加工具,觉得工具越多Agent越万能。但他们往往忽略了:每多一个工具,Harness就要多维护一份工具描述、多一套参数校验、多一条路由分支。这些额外的信息全都要塞进模型的上下文里,模型的注意力是有限的,工具描述占得越多,留给真正推理的空间就越少。

2.3 为什么"越加越强"是个危险的默认假设

"越加越强"这个假设之所以危险,是因为它把Agent当成了一个加法系统,而实际上Agent更接近一个资源竞争系统。上下文窗口、注意力权重、推理步数,这些都是有限资源。你加一个工具,不是简单地"多了一个能力",而是"从原有能力里分走了一部分资源"。

新基准的结论之所以值得重视,就是因为它用可量化的方式证明了这种资源竞争的存在:在Harness复杂度提升后,Agent在那些"不需要新工具"的旧任务上,表现出现了下降。换句话说,新能力还没完全用起来,旧能力先被挤掉了。这就像给一个本来跑得挺稳的程序不断加功能模块,结果核心功能反而开始出bug——典型的"功能蔓延"(feature creep)在Agent领域的翻版。

3. 新基准揭示的"能力遗忘":机制、表现与量化

3.1 基准是怎么测出"遗忘"的

要理解这个结论,得先搞清楚基准是怎么设计的。根据公开的研究思路,这类基准通常会做一件事:固定模型,只改变Harness的复杂度,然后对比同一批任务在不同Harness配置下的表现。

具体来说,它会构造几组对照:

  • 极简Harness:只给模型最基础的提示词,不挂任何工具,纯靠模型自身能力完成任务。
  • 中等Harness:挂载少量核心工具,提示词里加入工具描述和调用规范。
  • 复杂Harness:挂载大量工具和Skill,提示词里塞满各种能力说明、路由规则、格式约束。

然后让同一批任务(包括"需要工具"的任务和"不需要工具"的任务)在这三种配置下各跑一遍。结果发现:在"不需要工具"的那批任务上,复杂Harness的表现反而低于极简Harness。这就是"能力遗忘"的量化证据。

这个设计很聪明,因为它把变量控制住了——模型没变,任务没变,唯一变的就是Harness。所以表现下降只能归因于Harness本身。

3.2 遗忘的三种典型表现

结合我自己踩过的坑,这种"能力遗忘"在实际中主要有三种表现,每一种我都遇到过:

第一种:工具误触发。模型本来可以直接回答的问题,却非要去调用一个工具。比如问"今天星期几",它不去直接推理,反而去调用一个日历工具,结果工具返回格式不对,整个回答就崩了。这在复杂Harness里特别常见,因为提示词里工具描述太多,模型形成了"遇事不决先调工具"的惯性。

第二种:上下文稀释。多轮对话里,前面确认过的关键信息,到后面被工具返回的大段内容"淹没"了。模型的注意力被工具输出占据,反而记不住用户最初的需求。我遇到过最夸张的一次,Agent在第三轮突然问用户"你刚才说的需求是什么来着"——而那个需求在第一轮就说得清清楚楚。

第三种:格式僵化。复杂Harness往往对输出格式有严格要求(比如必须返回JSON、必须带特定字段)。模型为了满足格式要求,会把大量算力花在"怎么把话说得符合格式"上,反而牺牲了内容质量。表现出来就是回答变得又臭又长、套话连篇,实质信息却很少。

3.3 为什么旧能力会"先"被忘掉

这里有个很值得琢磨的细节:为什么是旧能力先被忘掉,而不是新能力学不会?

我的理解是,这跟模型的工作机制有关。模型在生成时,本质是在做"下一个token的概率预测"。当上下文里塞满了工具描述、Skill说明、格式约束这些"新信息"时,这些新信息在上下文里的权重天然更高(因为它们更靠后、更具体、更显眼),模型会优先照顾它们。而那些"旧能力"——比如基础的事实推理、常识判断、多轮记忆——它们依赖的是模型预训练阶段就内化的能力,在上下文里没有显式的"提示",自然就容易被边缘化。

用一个类比:你让一个会做饭的人去学开飞机,如果他每天脑子里想的都是飞行手册,那做饭时可能就会手忙脚乱——不是他不会做饭了,而是他的注意力被新东西占满了。旧能力不是被"删除"了,而是被"挤到后台"了。

这也解释了为什么换更大的模型往往解决不了问题。因为问题不在模型的"容量",而在Harness的"信息组织方式"。更大的模型上下文窗口更大,但如果你往里塞的东西也同比增加,稀释效应依然存在。

4. 拆解Harness复杂度的四个来源

要解决问题,得先知道问题从哪来。我把Harness复杂度的来源拆成四块,每一块都会独立地"吃掉"模型的能力。

4.1 工具描述膨胀

这是最直观的一块。每接入一个工具,你就要在提示词里写一段工具说明:它叫什么、干什么用、参数是什么、什么时候该调、返回什么格式。一个工具的描述少则几十字,多则几百字。挂十个工具,光工具描述就可能上千字。

这上千字不是白占的。它们会持续占据模型的注意力,让模型在每次生成时都要"考虑"这些工具。哪怕当前任务根本用不到工具,模型也得先"扫一眼"这些描述,才能决定不用。这个"扫一眼"的成本,就是旧能力被稀释的代价。

我实测过一个对比:同一个模型,在提示词里挂3个工具和挂15个工具,做纯文本摘要任务,后者的摘要质量明显下降,而且更容易跑题。原因就是那15个工具的描述把摘要任务本身的指令"压"下去了。

4.2 路由逻辑复杂化

工具多了,就得有路由逻辑:什么情况下调哪个工具、多个工具怎么选、调用失败怎么降级。这些逻辑通常也写在提示词里,或者写在Harness的代码里。

写在提示词里的路由规则,会进一步挤占上下文。写在代码里的路由规则,虽然不占上下文,但会让Harness的行为变得难以预测——因为模型看到的输入和最终执行的动作之间,隔了一层代码逻辑,调试起来非常痛苦。

我踩过的一个坑是:Harness里写了一个"如果问题涉及时间,优先调用时间工具"的规则。结果模型遇到"历史上某个时间点发生了什么"这种问题,也去调时间工具,而时间工具只能返回当前时间,完全答非所问。路由逻辑越复杂,越容易和模型的真实意图错位。

4.3 上下文拼接策略

多轮对话里,Harness要决定"把哪些历史信息放进上下文"。放少了,模型记不住;放多了,上下文被稀释。这个平衡点非常难找。

更麻烦的是,工具调用会产生大量中间结果。这些结果要不要放进上下文?放多少?放多久?每一个决策都会影响模型的表现。我见过最极端的Harness,把每一次工具调用的完整输入输出都塞进上下文,结果几轮下来上下文就爆了,模型开始"胡言乱语"。

4.4 输出格式约束

为了让Agent的输出可被程序解析,Harness通常会对输出格式做约束,比如要求返回结构化数据。这些约束本身是必要的,但约束越严,模型的"表达自由度"就越低。

一个典型的副作用是:模型为了满足格式,会把原本一句话能说清的事,硬生生拆成好几个字段,填一堆冗余内容。表现出来就是回答变得啰嗦、模板化。这其实就是"格式僵化"的根源。

下面这张表总结了四个来源和它们的典型症状:

复杂度来源典型症状对旧能力的影响
工具描述膨胀工具误触发、答非所问挤占推理注意力
路由逻辑复杂化行为难预测、调试困难意图与动作错位
上下文拼接策略记忆丢失、上下文爆炸多轮能力下降
输出格式约束回答啰嗦、模板化表达质量下降

5. 实操:给Harness做"减法"的五个具体手法

知道了问题来源,接下来就是怎么改。我把自己实践下来有效的几个手法整理出来,都是可以直接落地的。

5.1 工具描述按需加载,而不是全量常驻

最有效的一招,是不要把全部工具描述都塞进系统提示词。改成"按需加载":Harness先根据用户输入判断可能用到哪类工具,只把相关工具的描述注入上下文。

具体做法可以分两步。第一步,给工具打标签分类,比如"时间类""文件类""网络类"。第二步,Harness先用一个轻量级的分类器(甚至可以用关键词匹配)判断当前任务属于哪类,只加载对应类别的工具描述。

# 按需加载工具描述的简化示意 TOOL_REGISTRY = { "time": [time_tool, timezone_tool], "file": [read_file, write_file, list_dir], "network": [http_get, http_post], } def select_tools(user_input): # 简单关键词匹配,生产环境可换成小模型分类 if any(kw in user_input for kw in ["时间", "几点", "日期"]): return TOOL_REGISTRY["time"] if any(kw in user_input for kw in ["文件", "读取", "保存"]): return TOOL_REGISTRY["file"] return [] # 默认不加载任何工具

这样做的直接好处是:在不需要工具的任务上,上下文里几乎没有工具描述,旧能力不会被稀释。实测下来,纯文本任务的质量能恢复到接近极简Harness的水平。

5.2 把路由逻辑从提示词挪到代码里

路由规则写在提示词里,既占上下文又容易被模型误解。更好的做法是把路由逻辑下沉到Harness代码里,提示词里只保留最必要的工具说明。

比如"什么时候调用工具"这个判断,与其在提示词里写一堆规则,不如在代码里做一个前置判断:如果用户输入明显是闲聊或常识问答,直接走纯模型路径,根本不进入工具调用流程。

def should_use_tools(user_input, history): # 前置判断:明显不需要工具的场景直接跳过 chitchat_patterns = ["你好", "谢谢", "你是谁", "讲个笑话"] if any(p in user_input for p in chitchat_patterns): return False # 其他情况再交给模型判断 return True

这个前置判断虽然简单,但能挡掉大量"工具误触发"。把确定性的逻辑交给代码,把不确定性的判断留给模型,这是我总结的一条重要原则。

5.3 上下文做"滚动摘要"而非"全量堆叠"

多轮对话的上下文管理,我的经验是不要全量堆叠,要做滚动摘要。具体来说,保留最近N轮的完整对话,更早的对话压缩成一段摘要。

def build_context(history, max_recent=5): if len(history) <= max_recent: return history recent = history[-max_recent:] older = history[:-max_recent] summary = summarize(older) # 用模型或规则压缩 return [{"role": "system", "content": f"早前对话摘要:{summary}"}] + recent

关键是工具调用的中间结果不要全量保留。工具返回的大段内容,只保留结论性的部分,原始数据该丢就丢。我见过太多Harness把工具返回的原始JSON整个塞进上下文,几轮下来上下文就爆了。

5.4 输出格式约束分级,别一刀切

不是所有输出都需要严格格式。我的做法是分级约束:需要程序解析的输出(比如工具调用参数)用严格格式;面向用户的最终回答用宽松格式,允许自然语言。

这样模型在生成最终回答时,不用背着格式的包袱,表达质量会明显提升。格式约束应该服务于可解析性,而不是反过来绑架表达。

5.5 定期做"能力回归测试"

这是最容易被忽略但最重要的一招。每次给Harness加新工具或新Skill,都要跑一遍旧任务的回归测试,确认旧能力没有退化。

我现在的习惯是维护一个小型测试集,包含几十条"不需要工具"的基础任务(事实问答、摘要、多轮记忆等)。每次Harness改动后跑一遍,一旦发现某项指标下降,就回头检查是不是新加的东西挤占了资源。

提示:回归测试集不用很大,但要覆盖你关心的核心旧能力。宁可少而精,不要大而全。

6. 那些年我在Harness上踩过的坑

6.1 坑一:以为工具越多越"专业"

早期我特别迷信"工具数量",觉得一个Agent挂20个工具才显得专业。结果上线后用户反馈最多的就是"答非所问"。排查了很久才发现,模型经常在简单问题上乱调工具。

后来我把工具砍到5个核心的,其余改成按需加载,问题立刻缓解。工具的价值不在于数量,而在于"在正确的时机被正确调用"。一个从不被误触发的工具,比十个天天乱触发的工具强得多。

6.2 坑二:提示词里写太多"注意事项"

有段时间我特别喜欢在系统提示词里写各种"注意事项":注意不要这样、注意要那样、遇到X情况要Y。写了几十条,结果模型反而更不听话了。

原因很简单:注意事项太多,模型分不清主次。每条注意事项都在争夺注意力,最后哪条都没被真正遵守。后来我把注意事项精简到3条最关键的,其余全部下沉到代码逻辑里,效果反而更好。

6.3 坑三:忽略"空Harness"基线

这是最隐蔽的一个坑。我们做优化时,总是和"上一版Harness"对比,却从来没和"空Harness"(不挂任何工具、只有基础提示词)对比过。

直到有一次做基准测试,才发现我们的复杂Harness在基础任务上,居然还不如空Harness。没有基线,你根本不知道自己的优化是在进步还是在退步。现在我每次评估Harness,都会把空Harness作为一个必测的对照。

6.4 坑四:把Skill当成"越大越好"的模块

Skill的设计也有类似的陷阱。一个Skill如果包揽太多功能,它的描述就会很长,加载它就会挤占大量上下文。我现在的做法是Skill尽量做小、做单一,一个Skill只解决一类问题,需要组合时由Agent来编排,而不是把什么都塞进一个Skill里。

7. 从"堆料"到"编排":Agent能力扩展的正确姿势

聊到这里,其实核心观点已经很清楚了:Agent的能力扩展,不该是简单的加法,而应该是精细的编排。

所谓编排,就是承认资源有限,然后在这个约束下做最优分配。具体来说有三层含义。

第一层是时机编排:不是所有能力都要时刻在线,而是"需要时才加载"。这对应前面说的按需加载工具描述。

第二层是优先级编排:当多个能力冲突时,要有明确的优先级。比如"基础推理"永远优先于"工具调用",除非任务明确需要工具。这对应前置判断和路由下沉。

第三层是资源编排:上下文、注意力、推理步数都是资源,要像管理内存一样管理它们。该压缩的压缩,该丢弃的丢弃,该保留的保留。

我现在的Agent项目,Harness的复杂度是被严格控制的。每加一个新能力,我都会问三个问题:它真的必要吗?它会不会挤占旧能力?有没有更轻量的实现方式?这三个问题挡掉了很多"看起来很美"的加法。

回到标题那个问题——"工具、Skill、Agent越加越强吗?"我的答案是:不一定,而且很可能相反。加得越多,Harness越复杂,旧能力越容易被挤掉。真正强的Agent,不是能力最多的那个,而是在有限资源下把能力编排得最合理的那个。

这个道理其实不新鲜,软件工程里早就有"KISS原则""关注点分离"这些老话。只是在Agent这个新领域,大家容易被"能力扩展"的兴奋感冲昏头,忘了这些基本功。新基准的价值,就是用数据把这个老道理重新讲了一遍。

最后分享一个我自己的小习惯:每次给Agent加新东西之前,先花十分钟想想"如果不加,能不能用现有能力解决"。十次里有六七次,答案是可以的。省下来的那六七次加法,就是旧能力得以保留的空间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询