现在的 Agent 框架在比谁大:预装的能力越来越多,记忆越来越长,上下文窗口从 8k 涨到 128k 再到 1M,好像强弱完全取决于能塞下多少东西。但塞得满不等于用得对。上下文一大,注意力就被摊薄,模型在几十个工具、几百条历史里找该做的事时,找准的成本比做事本身还高;塞进来的每多一条,出了错就多一分定位难度。
所以这套架构的设计目标只有一句:不通过变大来变强,而通过变准来变强。变准落实到实现上是三件事:分层路由决定谁思考,图记忆决定记什么,稀疏激活决定想多少。
1. 分层路由:把 Agent 拆成器官
单体 agent 把所有事情交给一个模型:读全部记忆、看全部工具、决定调什么、动手执行、再组织语言回复。把这么多职责放进同一个上下文里,第一个代价是注意力:工具说明、历史消息、记忆、用户指令挤在一起,模型每一步都要先在一堆不相干的内容里翻找。第二个代价是定位:返回的结果不对时,分不清是检索漏了、决策错了,还是表达出了偏差,三种问题的修法完全不同,混在一起就只能整段重写。
于是这条链被拆成五个器官,每个器官只保留一类职责。
意图提取在最前面,任务是把用户的自然语言压缩成几个概念锚点。比如用户说“帮我改一下 neovim 的配置”,能提取出来的锚点就是“neovim”“配置”。这一层刻意做得很轻:正则、启发式规则或者一个本地小模型都够用,不调用远端模型,也不做任何决策。它的产出只是给下一步检索一个起点,错了也不要紧,后面还有机会修正。
上下文分诊接住锚点,进记忆图检索,把散落的材料组装成一份上下文包:相关的那几条记忆、这次任务用得上的工具说明、必要的会话状态。组装不是越多越好,所以这一层手里有一个硬预算,比如普通对话 1000 token、工作模式 4000 token。预算是物理天花板,装到上限就截断,宁可少给,不可超支。后面每一层的质量,都建立在“输入没有被塞爆”这个前提上。
路由拿着上下文包做决策。它的输出被锁死在一个结构里:动作类型只能是调用工具、询问用户或结束;选了调用工具,就必须同时给出工具名和参数。为了让这个结构稳定,生成端用模型的结构化输出,边界上再用反序列化校验拦一道脏数据。工具名只允许物理清洗,比如去空格、转小写,清洗之后仍然对不上任何已注册的工具,就作为错误信号交回路由重新判断,不猜测、不硬修、更不编造一个不存在的工具。路由也不写面向用户的自然语言,决策层一开口表演,结构和速度就都毁了。
执行是聋、哑、盲的纯函数,只接收运行编号、工具名、参数和一份权限快照。它不知道工具库的全貌,不读记忆,不参与决策,和路由之间没有任何回聊的通道。执行失败了就返回一条错误信号,除此之外什么都不做。大段输出在这里也不做语义摘要,只做物理处理:掐头去尾各留五百字,完整内容写到外部文件,结果里只留一个引用。做得这么绝,是因为执行层是全链路里唯一可测、可审计的环节,它越纯粹,测试和追责就越容易。
合成是唯一的自然语言出口。它读执行结果和错误摘要,把冷冰冰的结构化数据翻译成给用户看的回复。人格、语气、语言偏好只在这一层注入。路由保持冷、短、准,执行保持可测、可审计,会说话的只有它。
五个器官各司其职,原理一句话就能说完:每层只看到它该看到的东西。路由器看到全部工具会分心,执行器看到全部记忆会越权,合成器看到内部状态会说漏。分层带来的也不只是干净:出了错能定位到层,改造时能只替换一层,而不用动整条链。
2. 控制流:错误信号与熔断
执行失败是常态,不是异常,所以错误处理的路径必须事先设计好,而不是靠散落在各处的 if/else 临场补救。
这里的做法是:执行层失败时,生成一条格式固定的错误信号,字段包括原始意图、当前状态、缺失的能力、失败的工具、错误摘要、证据和风险提示。信号很刻意地保持极简,不携带大段日志和文件正文。大块内容先写到外部文件,信号里只放引用,原因是这个信号要进入路由的上下文重新决策,塞满垃圾的信号会把决策层也污染掉。
信号回到路由之后,循环开始:重新选工具、修正参数、降低目标、请求确认,或者干脆拒绝任务。循环有上限,最多三次。三次之内解决不了,说明问题不在参数层面,继续重试只是原地打转,于是熔断,把几次摘要交给合成层,向用户如实说明卡在了哪里。
提问也有专门的设计,分主动和被动两条路。主动提问发生在执行之前:路由发现指令缺关键条件,比如用户说“删掉它”但上下文里找不到“它”指什么,就直接转向用户,在第一秒把废话拦住。被动熔断发生在执行之后:连续三次失败,循环强制停止。
两条路径各配了熔断条件。非交互环境下,比如脚本调用、管道流,禁止提问,路由如果在此时输出询问,直接转为致命错误写入 stderr,进程以非零退出码结束,宁可失败,也不等一个永远不会到来的输入。连续两次提问且中间没有任何成功执行,同样强制结束,防止用户被拖进无限澄清的乒乓。
最后是一条 Unix 式的边界:重试、反思、熔断这些状态都往 stderr 输出一行短提示,最终结果走 stdout。这样 agent ask "..." > result.txt 拿到的文件里,不会有内部状态混进来。
3. 图记忆:节点寻址,边承血肉
记忆系统的第一个决定是不用文件夹,也不设预设分类。“架构”“事实”“任务”这样的目录看起来井井有条,但分类的那个瞬间,就已经决定了以后只能按这个维度召回。一条记忆既是架构笔记又是任务状态时,放在哪个目录都是错的。
所以底层是扁平的图:节点和边。分工很明确,节点负责寻址,边负责承载。
节点极度干瘪:名字、类型、向量,外加检索用的辅助字段,没有别的。名字上有唯一索引,重复的概念不会新建节点,提取回来的“NeoVim 配置”和“neovim 配置”会被更新合并成一个节点,权重和时间随之增加。节点越用越准,而不是越用越多。
血肉全部挂在边上:长文本、推理过程、文件摘要、任务状态、用户偏好,都作为数据附在边上。边表达的是概念之间怎么跳跃,比如“用户—偏好—>中文文档”“项目—当前任务—>路由层”。这里有一个容易被忽略的前提:边的数据必须有独立的索引。直觉上索引节点就够了,实际上记忆的正文都在边上,只索引节点的话,这些内容永远搜不到,记忆会整个隐身。
检索从锚点出发,在图上做受限遍历:最多走两跳,最多取几十条边,到预算就停。遍历本身下沉到 SQL 的递归查询里完成,不把整张图搬进应用内存再慢慢筛。粗定位按优先级走四步:向量相似度、关键词匹配、节点唯一索引、最后由模型把用户措辞做语义归一,比如把“改 vim 配置”归到“neovim 配置”。
召回之后的排序用混合打分,四项相加:长期权重、时间衰减、向量相似度、当前措辞的文本相关度。只按权重排会形成茧房,高频记忆越来越高,冷门但关键的记忆永远沉底,混合打分的意义就是让用户当前的措辞有能力击穿历史权重。向量计算放在常驻的后台进程里做,命令行入口不加载模型,否则每敲一条短命令都要重新冷启动几十 MB 的模型。关键词索引只作辅助:中文分词的不可靠性,使它撑不起唯一召回路径的重任。
4. 工具:一切皆节点,按思维路径浮现
工具在这个架构里也是图上的节点,于是上一节的全部检索能力,对工具同样生效。
一个工具包含四部分:说明卡片、详细指南、实现、最小测试。说明卡片常驻检索索引,指南只在被选中后才进入上下文,实现只在执行时运行,测试负责把不合格的工具挡在门外。四部分分开的好处体现在上下文的账本上:路由做决策时,只需要几十张说明卡片。如果每个工具的全部文档都常驻提示词,这一项开销就能吃掉整个预算。
浮现的方式是拓扑注入。用户说“修改 Neovim 配置”,意图层给出锚点,检索命中“Neovim 配置”“配置文件”这些节点,遍历顺藤摸瓜,把挂在附近的工具节点拉出来:读文件、写文件、应用补丁。路由运行时看到的不是工具库的全貌,而是当前思维拓扑自然浮出的几张卡片。
这套机制顺带抹掉了几个常见的区分。技能和基础工具没有差别,技能只是一个更大的工具节点,把多个原语操作复合成稳定流程,但对路由来说,分配谁都是读说明、填参数,它不需要知道技能是特殊概念。外部协议也不需要专门的中间件:工具说明和调用结构向主流协议的格式看齐,外部工具通过轻量适配就能挂进用户目录,协议是接口形状,不是架构中心。
工具有了生命周期。现有工具完不成任务时,可以生成一个临时工具,先在沙盒里测试,失败就收集错误修正,通过后进入候选区;经过多次真实复用,才晋升为正式工具;长期不用,就废弃删除。生成的目的是把重复出现的需求沉淀成能力,沉淀的反义词是增生,什么都留下,最后什么都找不到。
5. 两个平面与几条纪律
最后是两条边界,它们保证这套架构可以被长期使用,而不是越用越乱。
第一条是骨肉分离。源码仓库是代码平面:受版本管理,可重装、可删除、可重新拉取。用户的记忆、工具、会话是用户平面:放在用户目录的标准路径下,随使用生长。用户平面永远不被更新覆盖,源码仓库也永远不保存用户数据。有了这条边界,agent 可以随时重装换新,而用户积累的一切原样还在。
第二条是几条短纪律。核心只保留路由、分诊、执行、权限这些通用机制,具体能力一律放用户平面。检索不引入外部向量数据库和重型推理框架,单个数据库、本地小模型、逻辑分区就够用。图遍历留在 SQL 层。反思循环必须有上限。
6. 还没解决的问题
粒度困境:节点和边应该切多细。“Neovim 配置”是一个节点,还是“Neovim”“配置文件”“Lua”三个节点?切得太粗,检索命中不了具体场景;切得太细,图会膨胀,遍历噪声随之变大。目前靠手感,缺一条原则性的划分标准。
信息茧房:混合打分缓解了它,但没有解决。权重和时间衰减天然偏爱高频与近期,冷门但关键的记忆可能永远召不回;完全按相关度排,检索又被当前语境锁死,长程联想全丢。平衡点目前靠参数硬调。
两个问题都没有原则性的解法,先记录在这里。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时