
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三层能力的关系热词里反复出现工具SkillAgentHarness这几个词它们其实是一条能力递进链上的不同层级我习惯用下面这张表来梳理层级本质典型形态依赖关系工具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_recent5): 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加新东西之前先花十分钟想想如果不加能不能用现有能力解决。十次里有六七次答案是可以的。省下来的那六七次加法就是旧能力得以保留的空间。