Agent开发一年后,我删掉了90%的抽象层

去年八月,我在一个空仓库里敲下第一个 commit。那时我刚读完一堆关于 Agent 架构的文章,脑子里装满了规划器、执行器、记忆模块、工具注册表、事件总线。我给自己定了一个宏大的目标:不写一个 Agent,而是写一个「能长出任何 Agent 的东西」。一年之后的今天,2026 年 9 月,我的仓库里只剩下四个文件,加起来不到六百行。删掉的那些,按行数算大约有九成。

这不是一个极简主义胜利的鸡汤故事。恰恰相反:那些抽象层在被写下的当下,几乎每一个都有充分的理由,其中一部分确实解决过真实的问题。问题在于,它们解决的问题在三个月后就消失了,而它们带来的成本一直留到了今天。

我想把这一年里删掉的东西,以及为什么删,一条条写下来。不是为了劝人少写代码,而是因为我发现 Agent 开发这个领域有一个非常特殊的性质:它奖励的不是工程上的完备,而是对模型能力边界的准确判断。而抽象层,恰恰是最容易让人看不清边界的东西。

一、那股「我要造框架」的冲动从哪里来

开始的处境是混乱的。prompt 散落在七八个文件里,工具调用各写各的,模型调用到处硬编码。我的第一反应非常自然:抽象它。于是我写了 BaseAgent、ToolRegistry、PromptTemplate、MemoryStore、Orchestrator、EventBus。每一个类都遵循单一定责,每一个模块都有清晰的边界,PR 里的架构图看起来像是可以直接拿去投会议的样子。

我至今记得去年秋天那次代码评审。我在描述里画了一张四层架构图:最底层是 Provider 层,负责屏蔽不同模型的差异;往上是 Tool 层,用装饰器注册工具并自动生成 JSON Schema;再往上是 Memory 层,负责短期对话、长期记忆和实体抽取;最上面是 Orchestrator,用 Planner-Executor-Critic 三角色循环驱动多智能体协作。图很漂亮,评审通过得也很顺利,同事夸了一句「这个设计很干净」。

问题在于,我当时并不知道自己在为什么样的未来写代码。我在为一个想象中的、符合教科书描述的「Agent 平台」写代码,而不是为下个月要交付的那个具体功能写代码。这两种代码看起来很像,但它们对变化的反应完全不同:前者把每一个新需求都当作对架构的挑战,后者把每一个新需求都当作一段待写的逻辑。

一年后回头看,那张图上的每一个方框,要么被删了,要么被压扁成了一行代码。下面我按顺序说。

二、抽象层的清单,以及它们各自的死因

  1. LLM Provider 抽象层

我最初写它的理由非常正当:不想被任何一家模型厂商锁定,还想着有一天能一键切换模型做 A/B 测试。于是我设计了一个统一的 Message 结构、统一的 ToolCall 结构、统一的流式事件结构,再给每家的 SDK 写一个适配器。

结果是我花了两周时间,做了一个「把 A 家的格式翻译成 B 家的格式,再从 B 家翻译回来」的东西。更糟的是,每当有一家模型推出新能力——比如某种新的推理控制参数、某种新的多模态输入方式——我的统一结构就变成了天花板:我得先扩我的 schema,再改三个适配器,最后才能用上这个能力。而如果我直接用原生 SDK,那天下午就能跑起来。

我最终删掉了 Provider 层,改成一个薄薄的函数:给我一组消息和一个工具列表,返回模型的回复。不同厂商的差异,用三个各自独立的函数处理,允许它们长得不一样。代码从八百多行变成了不到一百行,而我再也没有因为「统一」而错过任何新特性。回头看,真正的防锁定从来不是抽象层,而是你的业务逻辑和 prompt 不依赖某家特有的怪癖。

  1. 工具注册中心

装饰器加自动 Schema 生成,这是几乎所有 Agent 教程都会教的东西,我也照做了。写一个 @tool 装饰器,读函数签名,读类型注解,自动生成 JSON Schema,自动注册进全局 Registry。

它在前两周确实好用。第三周开始出问题:有几个工具需要动态 schema——比如一个让模型自己写查询的工具,它的参数结构取决于当前的表结构;还有几个工具的参数需要互相依赖校验,类型注解根本表达不了。于是我开始在装饰器里加参数,加逃生通道,加「如果这个工具需要自定义 schema 就走另一条路」的分支。到第四周,装饰器已经比它包装的那个函数还长。

删掉之后,我直接把工具定义写成 JSON Schema 字面量,放在一个列表里。丑,重复,但是:搜索、跳转、diff 全都正常工作,而且任何人打开文件都能一眼看懂模型到底看到了什么。

这里有一条我后来反复验证的经验:在 Agent 系统里,「模型看到的那个 schema」应该是源代码里肉眼可见的一等公民,而不是某个元编程过程在运行时算出来的副产品。因为当模型行为异常时,你第一个要问的问题就是「它到底看到了什么」,而这个问题不该需要你运行一段代码才能回答。

  1. Prompt 模板 DSL

这是我删得最晚、也最心疼的一层。我写了一个小小的模板引擎:支持变量插值、条件块、循环、片段引入,还配了单元测试。我想象中,产品经理可以改模板,工程师不用动代码。

现实是,一年里只有我一个人改过这些模板。而且随着我越来越依赖模型本身的指令遵循能力,prompt 的写法从「精确拼接」变成了「用自然语言描述情境」——也就是说,模板本身越来越少地需要逻辑,越来越多地需要上下文。最后我把所有模板变成了普通的 f-string,塞进一个 prompts.py,配上几段注释说明每段是干什么的。改起来更快,评审起来更清楚,模型的表现没有任何变化。

那个 DSL 唯一的真实用户是我,而它给我带来的唯一真实收益,是让我在改一个逗号的时候,需要先读一遍引擎的实现。

  1. 多 Agent 编排

Planner 拆任务,Executor 干活,Critic 挑错,循环直到通过。这个架构在演示视频里非常好看,在执行长任务时也确实有它的价值。

但我在真实场景里发现,Critic 这个角色的边际收益衰减得极快。第一轮 critique 确实能发现明显问题,从第三轮开始,它就开始制造工作:为了让 Critic 有东西可说,它会提出一些无关痛痒的修改建议,Executor 照做,结果有时候反而改坏了。整个系统开始空转,token 消耗上去,用户等待时间变长,而最终质量并没有提升。

我现在用的是:一个主循环,一个可选的「最后检查」步骤,而且在检查之前先问一句——这个任务真的值得再花一次完整的上下文吗?多数时候不值得。

顺带说一句,那个把所有 Agent 连起来的事件总线,也在同一次清理里被删了。多 Agent 之间其实只有两种通信:要么是函数调用返回,要么是写进共享的上下文。事件总线让我在调试的时候需要在脑子里同时维护三个时间线,而收益只是「解耦」——一个我在这个规模上根本不需要的耦合度。

  1. 工作流状态机

我一度把每次 Agent 运行建模成状态机:Init、Plan、Act、Observe、Reflect,每个状态可以序列化、可以断点续跑、可以从任意状态重放。动机来自传统后端:长任务需要持久化,需要可恢复。

但在 Agent 场景里,我发现真正需要恢复的不是「第 17 步的状态对象」,而是「到目前为止发生的对话和工具调用记录」。前者是我自己发明的、脆弱的中间表示;后者是模型本来就理解、我也本来就需要的原始轨迹。后者天然可序列化,天然可重放,而且不需要我维护任何状态迁移规则。

删掉状态机之后,我的「断点续跑」变成了一句话:把消息历史存下来,下次把它塞回去继续跑。

  1. 记忆层

向量库、摘要压缩、实体抽取、记忆重要性打分、遗忘曲线。我大概写了六七百行,涵盖了这些概念里最流行的几个。

这里我要诚实地承认一件事:这一层是我唯一删了之后明确感受到能力下降的。摘要确实有用,检索确实有用。但我最终并没有把它们恢复成「一层」,而是恢复成了两个具体的、写在主循环里的动作:当上下文超过某个阈值时,把最早的一段消息压成摘要;当模型主动说「我需要回忆一下」时,调一次检索工具。没有存储抽象,没有记忆管理器,没有重要性打分。就是两个动作。

我逐渐意识到,我对「记忆层」的执念,来自一个隐含的类比:把 Agent 想象成一个人,因此需要一个人格化的记忆系统。但 Agent 更接近一个每次都被重新唤醒的执行者,它需要的不是记忆系统,而是恰当的上下文。

  1. 自研评测框架

有一阵子我坚信,Agent 的评测必须有自己的框架:数据集抽象、评分器抽象、多轮对比、回归看板。我搭了一个,跑通了,然后发现自己一周只看一次它的输出,而且每次都要手动去翻原始会话才能理解分数为什么变。

后来我把它换成了三十条真实的、来自线上用户的任务,存成一个纯文本文件,加一个脚本,跑完把完整轨迹打印出来,我自己一条条看。慢,土,但有效。因为关于 Agent 的多数信息不在那个数字里,而在轨迹本身。分数告诉你「变差了」,轨迹才告诉你「为什么」。

  1. 配置中心与插件系统

这两个是一起删的。坦白说,它们主要是出于惯性——因为我以前做后端,总觉得「迟早要支持多租户、迟早要支持插件」。

事实上,直到今天,我的 Agent 只有一个部署、一种配置(环境变量)、零个第三方插件。那个插件系统是我写过的最精致也最孤独的代码:接口清晰、文档齐全、生命周期管理完善,而它的插件列表从头到尾是空的。这大概是所有过度设计里最典型的一种:为一个从未出现的需求,写了一段永远不会被执行的测试。

三、抽象层为什么会腐烂

列完清单,我想说点更一般的。

第一,抽象的前提是稳定,而 Agent 领域最不稳定的东西恰恰是底层能力本身。

传统的抽象之所以值得,是因为其下层的语义是稳定的:文件系统就是文件系统,HTTP 就是 HTTP。你可以放心地写一层薄的封装,并且预期它在三年后依然成立。但模型不是这样。从去年到今年,模型在工具调用的可靠性、长上下文的一致性、指令遵循的精细度上都发生了肉眼可见的变化。一年前需要我用一套状态机和重试策略才能勉强稳住的事情,今天往往只是一句更清楚的指令。当底层以季度为单位移动时,任何低于这个频率的抽象都会迅速变成对旧能力的补丁——你封装的不是能力,而是某一次能力不足时的临时应对。

第二,抽象会隐藏最需要被看见的东西:原始上下文。

Agent 调试和传统调试最大的区别在于,bug 往往不在代码里,而在模型看到的那段文本里。一个多余的换行、一个被截断的工具返回、一段被模板引擎顺序打乱的示例,都可能让行为完全改变。而我写的每一层抽象,都在代码和那段真实文本之间加了一道翻译。当我用抽象的时候,我看到的永远是抽象层的输出,而不是原始输入。这让我在排查问题时多花的时间,远超抽象省下的时间。

后来我给自己立了一条规矩:任何时候,我都要能用一行代码打印出模型这一次看到的完整、原封不动的输入。做不到的系统,我就认为它过度抽象了。

第三,抽象层有隐性的 token 与延迟成本。

这一点在早期很容易被忽略。为了让抽象层「优雅」,我倾向于在 prompt 里加入结构化的说明、工具的元信息、角色的定义、格式的约束。这些东西每一轮都要重新发一遍。当对话轮次上来以后,它们的成本是每轮线性累加,而轮数往往在十几轮以上。删掉一部分抽象之后,我一个典型任务的输入 token 少了将近四成。四成,这直接变成了成本和响应速度。

第四,抽象层会阻断你对模型行为的直觉。

这一点最难量化,但可能最重要。当系统只有一层时,你会慢慢形成一种手感:什么样的措辞容易让模型偷懒,什么样的工具描述容易让它选错,什么样的上下文长度会让它开始丢细节。这种手感是从成千上万次改动中长出来的,而它是无法被文档化的。每一层抽象都在你和模型之间加了一层。改的是抽象层的配置,看到的是抽象层的结果,你就很难建立那种「我大概知道它会怎么反应」的直觉。

第五,也是最难承认的一点:抽象层会给人一种「我正在做正经工程」的安慰。

写 Agent 的时候,最让人不安的时刻是:你盯着一段 prompt,改了一个词,行为就变了;再改回来,行为没有变回去。这种不确定感非常折磨人。而抽象层的出现,恰好能缓解这种不安——它把问题从「这个模型为什么这样」转换成了「我的架构哪里不对」,后者至少在直觉上是可解的。我们造框架,有时候不是因为框架有用,而是因为造框架让我们感觉良好。

第六,抽象层让协作变得更难而不是更容易。

这一条我是在带人之后才意识到的。当我给一个新同学看那套四层架构时,他的第一个问题永远是「我该在哪一层加东西」;而当系统只有一层时,他的第一个问题是「这个工具的描述为什么这么写」。前者是关于我自己发明的规则的问题,后者是关于真实问题的问题。抽象层把学习成本从「理解问题」转移到了「理解我的设计」,这在一个人写代码时是纯亏损,在多人协作时是双重亏损。

四、删到只剩什么

现在,我的 Agent 大概长这样:

一个 while 循环,最多跑 N 轮。循环里,把当前的消息历史和一个工具列表发给模型。如果模型要求调用工具,就执行,把结果追加到历史里。如果模型给出最终答案,就退出。历史太长的时候,把最早的一段压成摘要。仅此而已。

除此之外就是一些非常具体、非常不优雅、但非常必要的代码:

工具定义是手写的 JSON Schema,一个萝卜一个坑。

错误处理是一堆 if,针对我实际遇到过的每一种错误:超时怎么办、限流怎么办、返回格式不对怎么办、工具抛异常怎么办。每一条都有明确的、我为它单独想过的处理方式。

上下文裁剪是一段带魔法数字的逻辑,注释里写着「这个数是我试出来的」,旁边注明了试出来的日期。

提示词是几个字符串常量,改的时候会有 diff,评审的时候能一眼看到。

这些东西没有任何一个值得写进博客的架构图,但它们具备三个我认为在 Agent 系统里最重要的性质:可读、可打印、可预测。

而且,删掉抽象层之后,我发现自己迭代产品的速度快了不止一倍。因为每一次改动,我都能立刻知道它到底改了模型看到的什么。改动和结果之间的因果链变短了,而在这个领域,因果链的长度直接等于学习速度。

还有一个我没预料到的收益:我重新开始享受写这个东西了。之前的每一天都在和架构搏斗,现在的每一天都在和问题搏斗。前者是自找的,后者才是这份工作本来的样子。

五、什么时候抽象层仍然值得

我不想把这篇写成一味的反抽象宣言。这一年的经验并没有让我认为抽象是错的,它只是让我把抽象的门槛提高了。

我现在认为,Agent 系统里的抽象层至少要在下面几个条件之一成立时才值得。

其一,你有多个真实的、正在运行的调用方。不是一个想象中的调用方,而是今天就在跑的、需求确实不同的两个东西。如果你只有一个 Agent,那么「统一接口」的唯一收益就是让你自己多写一层。注意「真实的」这三个字:我有过三次「马上就要有第二个调用方了」的预期,其中两次到今天都没有发生。

其二,被抽象的那部分确实已经稳定了。如果一个能力在过去半年里没有本质变化,且你判断它在未来一年也不会有,那抽象它通常是安全的。反过来,如果某个能力每季度都在变,就不要抽象它,让差异明明白白地暴露在代码里。宁可重复,不要过早统一。

其三,抽象带来的是可测试性、可审计性或合规能力,而不只是「整洁」。比如在金融、医疗这类场景里,你需要能完整重放一次决策过程并给出证据。这时候一层结构化的、有明确边界的运行时是非常有价值的。但请注意,这类需求往往需要的是「记录」,而不是「封装」——你需要的是一份完整的、不可篡改的轨迹,而不是一个漂亮的接口。

其四,抽象能显著降低 token 成本或延迟。如果某个抽象层的存在本身就是为了减少传输和计算——比如缓存、批处理、增量更新——那它值得单独评估,因为它的收益是可测量的。

除此之外,我倾向于先写最直白的那版代码。重复三次之后再考虑抽取。因为在这个领域,你几乎一定会重复三次——但每次重复的具体形状都不一样,而只有三次之后你才有资格判断,究竟哪一部分是真正共通的。

六、结论

写到这里,我想回到开头那个问题:为什么我们在 Agent 开发里会如此自然地想要造抽象层?

我觉得原因有两层。表面上,是因为我们带着过去十年的工程经验进入这个领域,那些经验里「好的架构」确实等于分层、解耦、可替换,而且这套经验在传统后端里被反复验证过。更深一层,是因为我们面对的是一个不太听话的东西。模型的行为有相当的随机性,我们很难像调试普通程序那样调试它。于是我们本能地想要加一层壳,把这层随机性包起来,让它看起来像一个确定性的系统。

但这一年的经验告诉我,那层壳并不真的让系统变得确定,它只是让不确定性变得更加难以观察。当你把随机性藏在五层抽象后面,你失去的不是随机性,而是你理解和驾驭随机性的机会。而驾驭随机性——知道它在什么时候出现、以什么形式出现、边界在哪里——恰恰是这个领域里唯一真正的技能。

所以我删掉了那九成。不是因为抽象不好,而是因为在这个阶段,抽象的成本远远大于它的收益:它拖慢了我的学习速度,而学习速度是这个领域里唯一真正的护城河。

如果一定要用一句话总结这一年,大概是:在模型能力还在快速移动的年代,最好的架构不是最优雅的那个,而是最接近真相的那个。而真相,往往就是那段你原封不动发给模型的文本。

当然,我也不认为现在这六百行就是终点。也许明年,当模型的某些能力真正稳定下来,当我的产品真的有了三个需求各异的调用方,我会重新把一些抽象加回来。到那时候,我希望自己是带着明确的理由加回去的,而不是因为不安。

最后留一个问题给同样在做 Agent 的你:如果你现在删掉代码库里最「聪明」的那一层,你的 Agent 会变差,还是只是变得不一样?如果答案是后者,那它可能早就不该在那里了。