1. 从"堆模块"到"砍模块":一次反直觉的架构瘦身
做Agent这一块,我前后折腾了差不多一年半。最开始那几个月,我和大多数人一样,觉得能力越强越好,于是往系统里塞了一堆东西:规划器、反思模块、记忆检索、工具路由、多轮校验、结果重排……每个模块单拎出来看都挺有道理,论文里也都能找到出处。但真正跑起来之后,效果并没有想象中那么好,反而经常出现一种很尴尬的情况——简单任务被搞复杂,复杂任务又因为模块之间互相打架而卡死。
后来我做了一次彻底的重构,核心动作只有一个:把架构删了。不是删代码,而是删掉那些"为了显得智能而存在"的中间层。这次改动之后,任务成功率、响应速度、调试体验三个维度同时变好,也是我这一年里效果提升最明显的一次。
这篇文章我想把这次"删架构"的完整思路拆开讲清楚:为什么模块越多反而越差、删的时候依据什么判断、删完之后靠什么兜底、以及哪些东西是绝对不能删的。如果你也在做Agent相关的东西,或者正准备给自己的系统加新模块,这篇应该能帮你少走一些弯路。
提示:本文讲的"删架构"不是让你把工程规范也删了,而是删掉那些职责重叠、收益为负的智能层。工程上的日志、监控、重试这些该留还得留。
2. 模块堆叠为什么会让Agent变笨:三个真实的失效场景
2.1 规划器和反思模块互相"甩锅"
我最早的系统里有一个任务规划器,负责把用户请求拆成子步骤;后面又加了一个反思模块,负责在每步执行后判断"这一步做得对不对"。听起来很合理,但实际跑起来是这样的:规划器拆出五步,反思模块执行到第三步觉得方向不对,要求回退重规划,规划器重新拆出七步,反思模块又觉得第二步有问题……一个本来三步能搞定的任务,在两者之间来回拉扯了十几轮,最后超时。
问题的本质在于:两个模块都在做"判断下一步该干什么"这件事,只是换了个名字。规划器判断的是全局路径,反思模块判断的是局部修正,但它们的输入信息高度重叠,输出又互相覆盖。这种职责重叠在单模块时看不出来,一旦叠加就会产生振荡。
2.2 记忆检索把无关信息塞进上下文
第二个坑是记忆模块。我当时的想法很朴素:Agent应该像人一样有记忆,所以给它接了一个向量检索,每次执行前把"相关历史"捞出来塞进提示词。结果发现,检索出来的东西经常是"看起来相关但实际没用"的片段。比如用户问"帮我整理一下上周的会议纪要",检索模块捞出来的是三个月前一次完全无关的会议记录,只因为里面也出现了"会议"这个词。
这些噪声进入上下文之后,模型反而被带偏了,开始模仿那些无关内容的格式和语气。记忆模块的问题不在于它没用,而在于它的召回精度达不到"可以直接信任"的程度,而它又占据了宝贵的上下文空间。当上下文被噪声占满,真正重要的指令反而被稀释了。
2.3 工具路由在多工具场景下频繁误判
第三个坑是工具路由。当Agent可以调用十几个工具时,我加了一个路由模块,先用一个小模型判断"这个请求该用哪个工具",再交给主模型执行。想法是省token、提速度。但实际结果是:路由模块的准确率大概只有七成,剩下三成误判会导致主模型拿到错误的工具描述,然后要么报错,要么用一个不合适的工具硬做。
更麻烦的是,这种误判很难排查。因为从日志上看,每一步都"成功"了,只是最后结果不对。你得像侦探一样一层层往回翻,才能发现是路由那一步选错了工具。
这三个场景指向同一个结论:每个中间模块都在引入自己的误差,而这些误差会沿着链路累积。当模块数量超过某个阈值,累积误差就会超过模块本身带来的收益,系统整体表现开始下降。
3. 判断哪些模块该删:一套可操作的收益评估方法
3.1 用"单模块消融"定位真正的贡献者
删模块不能凭感觉,我用的是一个很土但很有效的办法:单模块消融测试。具体做法是,保持其他所有条件不变,每次只关掉一个模块,跑同一批测试任务,记录三个指标——任务成功率、平均耗时、平均token消耗。
| 模块 | 关掉后成功率变化 | 关掉后耗时变化 | 关掉后token变化 | 结论 |
|---|---|---|---|---|
| 规划器 | -2% | -35% | -40% | 收益低,可删 |
| 反思模块 | -1% | -28% | -33% | 收益低,可删 |
| 记忆检索 | +3% | -20% | -25% | 负收益,必删 |
| 工具路由 | -8% | -15% | -18% | 有收益,保留 |
| 结果重排 | 0% | -12% | -10% | 无收益,可删 |
这张表是我当时跑出来的真实结果(任务集是两百条混合任务)。可以看到,规划器和反思模块关掉之后,成功率只掉了不到两个点,但耗时和token都大幅下降。记忆检索更夸张,关掉之后成功率反而涨了三个点——说明它引入的噪声比它提供的信息更多。
注意:消融测试一定要用同一批任务、同样的随机种子、同样的模型版本。否则你测出来的差异可能只是随机波动,不是模块的真实贡献。
3.2 区分"能力模块"和"协调模块"
跑完消融之后,我把所有模块分成两类:
- 能力模块:直接扩展Agent能做的事情边界,比如工具调用、代码执行、文件读写。这类模块删了,Agent就真的做不了某些事。
- 协调模块:不扩展能力边界,只负责在已有能力之间做调度、判断、修正。比如规划器、反思模块、路由模块。
我的经验是:协调模块的收益普遍低于预期,而能力模块的收益普遍高于预期。原因是协调模块做的事情,主模型本身也能做,只是可能做得没那么"结构化"。但主模型做的时候,它拥有完整的上下文,不会出现模块之间信息割裂的问题。
所以删减的优先级很明确:先删协调模块,保留能力模块。协调模块里,只有那些"主模型明确做不好且消融测试证明有正收益"的才留下。
3.3 一个简单的决策清单
如果你不想跑完整的消融测试,可以用下面这个清单快速判断一个模块该不该留:
- 关掉它之后,Agent是否还能完成同类任务?如果能,进入下一步。
- 它做的事情,主模型在完整上下文下是否也能做?如果能,倾向于删。
- 它的输出是否会被下游模块直接信任而不加校验?如果是,风险很高,倾向于删或改。
- 它的存在是否让调试变难(比如错误被掩盖、链路变长)?如果是,倾向于删。
- 消融测试中它的正收益是否稳定超过5%?如果不到,倾向于删。
这五条里只要有三条指向"删",我基本就会动手。实测下来,这个清单帮我砍掉了将近一半的中间层。
4. 删完之后靠什么兜底:把复杂度还给主模型
4.1 用更长的上下文替代记忆检索
删掉记忆检索之后,我并没有让Agent变成"没有记忆"的状态,而是换了一种方式:把需要的历史信息直接、显式地放进上下文,而不是靠检索去猜。
具体做法是,在任务开始时,由调用方明确指定"这次任务需要参考哪些历史内容",而不是让Agent自己去检索。比如整理会议纪要的任务,调用方直接把上周的会议记录传进来,Agent不需要去向量库里捞。
这样做的好处是:信息的准确性从"检索精度"变成了"调用方责任",而调用方通常比检索模块更清楚哪些信息是相关的。代价是调用方要多做一点工作,但换来的是Agent行为的可预测性大幅提升。
如果确实需要自动检索,我的建议是:检索结果不要直接进上下文,而是先经过一次显式的"相关性确认"——让主模型自己判断"这些内容是否和当前任务相关",不相关的直接丢弃。这一步看起来多了一次调用,但比让噪声污染上下文要划算得多。
4.2 用主模型的推理替代规划器和反思模块
删掉规划器和反思模块之后,我把"拆步骤"和"检查"这两件事都交给了主模型,通过提示词来引导,而不是用独立的模块来强制。
提示词的核心结构大概是这样的:
你是一个任务执行助手。在开始执行前,请先在心里拆解任务步骤, 但不要输出拆解过程。执行过程中,如果发现当前方向可能有问题, 请停下来重新评估,而不是继续硬做。评估时请参考以下标准: 1. 当前步骤是否在推进任务目标? 2. 是否有更直接的方式达成目标? 3. 已执行步骤的结果是否可信?这段提示词的关键在于:它给了模型"可以停下来重新评估"的许可,但没有强制它每步都评估。这比独立的反思模块灵活得多——简单任务直接做完,复杂任务才触发重新评估。
实测下来,这种方式在简单任务上比原来的规划器+反思模块快40%以上,在复杂任务上成功率基本持平甚至略好。原因是主模型在完整上下文下做的判断,比两个割裂模块各自判断再合并要准确。
4.3 工具路由的替代方案:让主模型直接选
工具路由我保留了,但改了一种方式:不再用独立的小模型做路由,而是把所有工具的描述直接放进主模型的上下文,让主模型自己选。
这样做的前提是工具数量不能太多。我的经验是,工具描述控制在15个以内、总token不超过2000时,主模型的选择准确率明显高于独立路由模块。如果工具超过15个,可以按场景分组,每次只放当前场景相关的工具描述。
如果工具实在太多,必须做路由,那我的建议是:路由结果不要直接信任,而是让主模型在拿到路由结果后再确认一次。也就是路由模块只负责"缩小候选范围",最终选择权还在主模型手里。这样即使路由错了,主模型也有机会纠正。
5. 删减过程中踩过的坑和验证方法
5.1 删过头导致能力缺失
我第一次删的时候有点上头,把工具路由也删了,结果Agent在十几个工具面前彻底懵了,经常选错工具或者干脆不选。后来才明白:协调模块里也有"必须保留"的,判断标准是主模型在完整上下文下是否真的做不好。工具选择这件事,当工具数量超过一定阈值,主模型确实做不好,所以路由得留。
这个坑的教训是:删减要一个一个来,每删一个就跑一轮测试,不要一次性删一堆然后一起测。否则你根本不知道是哪个模块删错了。
5.2 提示词补偿过度导致上下文爆炸
删掉模块之后,我试图用提示词来补偿,结果提示词越写越长,最后光系统提示词就占了三千多token。这又回到了老问题:上下文被占满,真正重要的信息被稀释。
后来我定了一个规矩:系统提示词不超过800token,超过就说明有东西该拆出去或者该删掉。这个数字不是绝对的,但作为一个约束,它逼着我把提示词写得足够精炼。
5.3 验证删减效果的正确姿势
验证删减效果,我用的是A/B对比加分层测试:
- A/B对比:同一批任务,一半走旧架构,一半走新架构,对比成功率和耗时。
- 分层测试:把任务按复杂度分成简单、中等、复杂三层,分别看删减对每层的影响。通常简单任务受益最大,复杂任务需要特别关注是否退化。
| 任务层级 | 旧架构成功率 | 新架构成功率 | 旧架构平均耗时 | 新架构平均耗时 |
|---|---|---|---|---|
| 简单任务 | 82% | 94% | 12s | 5s |
| 中等任务 | 71% | 76% | 28s | 18s |
| 复杂任务 | 58% | 61% | 65s | 52s |
这张表是我当时跑出来的结果。可以看到,简单任务提升最明显,复杂任务也有小幅提升。如果复杂任务出现明显退化,那就说明删过头了,需要把某些模块加回来。
提示:分层测试的任务集要提前准备好,每层至少50条,否则统计意义不够。任务集一旦定下来,整个删减过程中不要改,否则前后对比没有意义。
6. 删减之后,哪些东西反而要加回来
6.1 可观测性不能省
删掉智能模块之后,系统的黑盒程度其实降低了,因为链路变短了。但这不意味着不需要可观测性。相反,链路越短,每一步的输入输出越要记录清楚,因为出问题时你没有那么多中间层可以排查。
我的做法是:每次模型调用都记录完整的输入提示词、输出内容、耗时、token数。这些日志看起来笨重,但出问题时能救命。有一次一个任务反复失败,我翻了日志才发现是某个工具的描述里有个错别字,导致模型理解偏了。如果没有完整日志,这种问题根本找不到。
6.2 失败重试要有,但不能无脑重试
删掉反思模块之后,失败处理变成了一个需要显式设计的事情。我的方案是:区分可重试失败和不可重试失败。工具调用超时、网络错误这类可以重试;模型输出格式错误、任务理解偏差这类重试也没用,应该直接报错让调用方处理。
重试次数我一般设两次,超过两次就放弃。因为实测下来,同一个任务重试两次还失败,基本就不是偶然问题了,继续重试只是浪费资源。
6.3 人工兜底通道要留好
不管架构怎么删,有一条通道不能删:当Agent连续失败时,能够把任务转交给人工处理。这个通道不需要很复杂,一个简单的"转人工"接口就行。它的价值不在于经常用,而在于给整个系统一个安全底线。
我在实际使用中发现,有了这条通道之后,我反而更敢删模块了。因为我知道即使删错了,最坏情况也只是转人工,不会造成不可挽回的后果。
7. 关于"删架构"这件事,我的一些个人体会
做Agent这一年多,我最大的体会是:智能不是堆出来的,是留出来的。每加一个模块,你都在给系统增加一个可能出错的环节。当模块数量超过某个点,系统的整体可靠性不是由最强的模块决定,而是由最弱的模块决定。
删架构这件事,技术上并不难,难的是心理上要过一道坎:你得承认自己之前加的那些模块,大部分是没用的。这个承认的过程挺痛苦的,尤其是当你为那些模块写了不少代码、调了不少参数的时候。
但删完之后,你会发现系统变得"诚实"了。它不再假装自己很聪明,而是老老实实做它能做的事。这种诚实反而让它的表现更稳定、更可预测。
如果你也在做类似的东西,我的建议是:每隔一段时间就做一次消融测试,看看哪些模块其实可以删。不要等到系统复杂到无法维护才动手。删减不是倒退,是把复杂度放回它该在的地方。
最后分享一个我常用的小技巧:当你犹豫一个模块该不该删时,先把它注释掉,跑一天看看。如果一天下来你都没想起它,那它大概率就是可以删的。这个方法很土,但比任何评估框架都直接。