悔不当初、悔不当初啊!
——这一切都要从 2026 年年初,那个罪恶的时间段说起。
随着 OpenClaw 突然爆火,什么牛鬼蛇神都来安装运行 OpenClaw,美其名曰「AI赋能」,实际上很大可能是 token 贩子的阴谋。
不论如何,虽然我尚未从学畜升级为社畜,好奇心还是让我想下个 agent 框架玩玩。龙虾框架当时内容臃肿、内存爆炸的问题我早有耳闻,于是没有去下载原版,而是下了一个用 Rust 重写的 ZeroClaw。而且因为不想给别人送钱,~~又能偷学校的电,~~我将运行在自己电脑上的小模型接入了框架。
在试用的过程中,我遇到了各种各样奇葩的问题。因为是新项目,文档完成度不高,我需要跑到代码里看 config 能填的选项是什么。这改改那改改终于能跑起来了,却因为 prompt 太长,过不了两轮小模型就注意力涣散,或者干脆超出 context 长度了。
经过几十次试错,我的 agent 终于能正常运行了。看着它在聊天窗口发送「你好✨」,然后变成一个除了聊天,别的任务都做不好的、白费电的废物。我不禁想:这一切真的值得吗?*这框架有好多我用不上的东西,有我不能自己 DIY 的部分,还有我觉得可以做得更好的部分……我是不是能写出一个更适合自己、更*针对小模型优化的 agent 框架来使用呢?
好,那就这么做吧!
如果能穿越时空,我一定会去扇当时的自己一巴掌。因为这突如其来的念想,我痛并快乐的 agent 框架设计生涯开始了……
贾宝玉初拔垂杨柳
我对龙虾系主要有两点不满:
- 笨重。原版的体积和内存占用庞大是一方面,所以这里要讨论的不仅仅是框架臃肿的问题——prompt 的组成本身也很笨重,缺乏上下文工程。它一股脑把所有工具和信息扔给 agent,盲目信任 AI 的超强智力。虽然压根不维护项目,git 工具还是要扔进去;即使整个工作区找不到一份 pdf,阅读 pdf 的工具还是要在列表里。一长串的 JSON 工具列表,再来一块长长的 prompt,加上一坨原汁原味的历史消息,token 数直接爆炸。对于云端那些几百 B,甚至上 T 的模型,这个做法虽然粗暴,但绝对可行;问题是,小模型经不起这么折腾。上到审阅代码,下到重命名个文件,全都让一个模型来完成,「你把大家叫出来就为了这点事?」,既不灵活也不实惠。
- 流程不可控。不得不说,这个确实不是龙虾系的问题,因为龙虾从一开始主打的就是相信、相信、再相信,任务交给了 agent,怎么完成的你别问,结果对就行~~,token消耗你最好也别看~~——这就是它的哲学。在完全放空大脑的情况下,每次派发任务主打的就是一个
赌博概率学;若有心,也可以让 agent 通过 skill 或者 script 进行任务流程化,但由于框架本身对可复现性的支持有限,折腾起来费时又费力。如果框架本身就对流程化支持很好,用户就能消耗更少的精力去维护,进而提升 agent 能力和 token 效率,大赢特赢啊!
抱着这两点不满,我对框架进行了初步构思:
没必要每个问题都调用最昂贵的模型。可以用一个小模型作为路由,在每轮对话开始时评估任务并指定模型,顺便挑选会用到的 tool 和 skill。评估任务时,分为三个复杂度:简单、中等、复杂。简单任务只需要聊天,路由直接回复即可;中等任务涉及到工具调用;复杂任务则涉及长线规划和推理。之后循环进行任务,直到 agent 调用 finish_task 工具。
配置文件方面,我选择了 toml 格式~~(因为我在zeroclaw配置了很久toml文件,已经习惯了)~~。此外,在工作区里,由一个 SOUL.md 规定 agent 的角色设定;再由一个 AGENT.md 规定 agent 在任务中的表现,与前者解耦。对于长期记忆,我没有使用 markdown 或者数据库,而是用了 JSONL 格式。markdown 很好,但难以检索内容,只能一次全吞;数据库的结构和查询效率很好,但是不方便用户修改查阅。考虑到不能指望小模型主动记忆并记对任何内容,还是用一个结构化又易修改的格式吧……
安全性上,我懒得设计太多我选择相信用户。Agent 包含自动通过请求的 auto 和需要手动同意请求的 supervised 两个模式;严格禁止在工作区和允许目录以外的地方读取和修改文件;屏蔽了 rm -rf / 等经典妙妙小命令;并为每轮工具调用和回合数设置了上限。
Context 保卫战
相信读完上面两小节,我想你已经发现我很抠门了。*是的,事情就是这样。*我不仅抠输出的钱,还抠 context。正如之前所说,我主要是为了在本地跑小模型才写的这个框架。我的显存有限且宝贵,对于 9B 尺寸的模型,8k 上下文就已经很勉强了。为了让任务顺利完成,我必须能省则省。
最初添加的设计是在路由环节挑选工具,从而减小工具 JSON 的体积。工具数量不多的时候,这个设计并不会有太显著的效果(不过我相信减少工具可以一定程度上增强小模型的注意力,避免搞错任务),但如果有一天工具疯狂膨胀——希望不会有这样一天——效果就十分显著了。以防万一,我还给工具加了些小逻辑:比如,强制注入 finish_task,防止路由犯病忘掉;如果记忆为空,就不将检索记忆的 tool 添加到可选列表中。
有人可能会问,**万一路由犯病把工具挑错了怎么办?**我确实思考过这个问题,于是给
finish_task加了一个字段,可以标记本轮失败,相当于「这活我真接不了,你回去吧」。礼貌婉拒而不是埋头硬干,你好我好大家都好。
一般来说,JSON 因为符号多,token 占用通常比自然语言要大。因此我加入了“将 n 条前的消息中的 JSON 工具调用转换为自然语言”这个设计。为什么不是立即转换,而是等过了 n 条消息后呢?哈哈,因为我踩坑了!!一开始我确实是立即转换,没想到小模型居然对工具调用格式产生了幻觉,真是活爹。我最近意识到这个设计可能会减少缓存命中率,后续可能会在 config 里增加一个关闭选项。
必不可少的 context summarizer 就不多说了,我没有像 Claude Code 那样对总结进行详细的分级,只是一段 prompt 草草了事。个人觉得没必要过度设计,100k 以内 AI 的总结能力还是不错的!
此外,我在使用中常常遇到上下文污染这个问题,小模型的注意力很容易被大段落的阅读带偏,忘记自己到底在做什么。我希望框架保持单 agent 架构,但还是加入了两个有些打擦边球的 tool:delegate 和 escalate。前者意为委托,仅将一个特定的任务和必要的背景信息传递给一个轻量的 subagent,任务完成后清除中间过程,只给主 agent 返回结果,从而保持 context 干净;escalate 则是升级,逻辑与前者类似,但会将任务转交给一个强力模型,以便在主 agent 思维陷入死局时,委托给一个没有那么多历史包袱的专家重新寻思寻思。
Subagent 工具用着用着,某天我突然拍桌站起,惊呼:「~~尤里卡!~~既然 subagent 拿到的都是非常详细的小任务,干嘛还要它每次读完消息后再『搞半天自己去调用』?我直接在派发任务的时候让主 agent 提前调用不就好了?!」于是,这两个工具的 pre_tool 字段堂堂诞生!让 subagent 读网页或者文件之类的操作,直接把调用命令写在 JSON 里,就会一并传给 subagent 并执行。
数字代理会梦到进电子厂打工吗?
基本的交互能力具备后,有纪律地完成多步骤任务的能力就成了我的主要优化方向。实现方法也不难,当然是采用当时最潮最火热的大家都在蒸馏自己亲朋好友的 Skill 啦!我很快就实现了大家都在用的 Markdown 格式的 skill,一如既往地让路由快乐零元购。虽然它方便、不需要编程知识、容错率高,但我不禁觉得,给小模型用也太「奢侈」了。动辄几千字的文本量,实在太占脑容量!基于这种想法,我开始思考是否有办法让部分重复的内容(如特定指令和工具的运行)实现自动执行。然后就想到了:流水线 (pipeline) 啊!
一开始,我设计了一个和 Github Action 长得差不多的 YAML 流水线,每个节点附带一堆设置。但是问题接踵而至:1、对人类来说,编写起来太累了,需要疯狂查文档;2、AI 不擅长 DSL(特定领域语言),很容易出现幻觉写成 action 语法。于是我简化了配置的复杂度,它现在大体长这样:
name: research-and-summarize
description: "Fetch a URL and summarize it." # 不写简介的话,人和 AI 都不知道这是啥玩意!
vars:
url: "" # 变量声明
pipeline:
- id: fetch
type: auto # 自动节点!有一说一,这是一个很不好的例子,因为用户的输入不一定只有链接……
tool: web_fetch
inputs:
url: $vars.input # 赋值
outputs:
raw_content: result
- id: summarize # agent接管的节点!它的type叫agent
prompt: |
Summarize the following web page content in 3–5 bullet points:
{{$vars.raw_content}}
outputs:
answer: result
此外,我对工具进行了更改,增加了 skill-only tool 的概念。顾名思义,这些工具不会出现在平时的可选列表中,只有被包含在一个 skill 的 required_tools 字段中时,才会变为可用状态。这么设计是为了防止在大多数情况下无用的工具占用空间或被错误选择。比如 browser 这个 tool,如果我们每次使用它都需要配合 browsing 这个 skill,那让它单飞好像意义也不大。
流水线:崛起
没过多久,OpenClaw 的热度降下去了;然而,新框架 Hermes 又火了!看大家都说 Hermes 行,自我迭代老带派了。我寻思,这么带派的功能我应该抄袭借鉴一下,也就是说,我们要让 agent 知道自己主动去写 skill/pipeline!
——结果,我失败了。
我为复杂路由加入了 planner 和 evaluator 两个额外的节点。当遇到复杂任务时,planner 会进行分析并写成 pipeline,Go 引擎确认语法无误后执行。不管 pipeline 执行成功与否,结束后都会将历史消息、pipeline 文件以及相关文档交给 evaluator 进行评估、优化和修改。如果连续三次成功,评估环节就不会再被触发。这个设计的本意是增加任务执行的稳定性,不再和数学模型掷骰子。
结果在测试时,我发现这一套小流程压根毫无稳定性可言!读不进文档瞎写、evaluator 被用户的 prompt 带偏、变量定义混乱,几乎没有成功运行过一次。后来好不容易勉强能跑了,受制于 AI 的固有局限,planner 编写出来的文件毫无泛用性,evaluator 更是帮倒忙,居然根据当前回合的输入将 prompt 乱改成针对特定任务的版本!我真是眼前一黑啊!
我很快安静下来进行了自我反思。别人有的我也要有,这种想法未免有些太网红产品思维了。
模型的参数差距摆在那里,指望两位数尺寸的小模型突然“开智”是不现实的。况且,即便是人类,也很难在不进行任何实践和调研的情况下,拿到任务就写出一份完美的计划表。我的想法实在是太幼稚了!不过,让 agent 写流水线的大方向是对的,毕竟我本来就是为了这个才简化流水线语法的;而且,流程固化也确实能增加稳定性。深思熟虑后,我想出了一个折中方案:加入一个 /build 指令,让用户自己在执行前输入 pipeline 的具体任务内容(比如,把用户提供的网页按 50 行切片交给 subagent 阅读)。有了清晰的任务期望后,agent 的表现确实有所提升。只是不得不承认,我的 planner 和 evaluator 使用的都是云端几百 B 的模型,本地小模型能否担当此任,我也不清楚。
顺带一提,在增加了原有复杂任务的处理流程后,之前路由设定的“简单、中等、复杂”分级似乎不再那么符合语义了;此外,这种主观的评价(尽管我在 prompt 中提供了评定标准)本身就很模糊。于是我将分级重新规划为:
- simple → direct,直接回复语句。
- medium → atomic,恰好只调用一个工具。
- complex → workflow,按照排除法来说,就是任何其他情况。
在模型调用上,我取消了路由分级与模型分配的强绑定。路由现在自带一个 checklist,如果任务出现某种情况就打勾,最后由 Go 引擎根据 checklist 自行分配模型。实测下来这个改动非常有效,路由的发癫率大幅下降……
零星其他
除了与 agent 流程有关的设计外,我还做了些其他增强用户体验的周边功能,这里就顺口一提:
-
多线程:此多线程非彼多线程,指的是利用 Matrix、Discord 平台 Thread 功能的特性,在一个对话里开启多个 session,且互不干扰。
虽然常常因为用户回复错串而把 session 搞混乱。 -
凭证储存:尽管大家都说这很不安全、千万别这么做,但不得不承认,很多时候我们确实需要让 agent 使用凭证来执行任务。比起在 skill 里直接写明文,框架原生支持“凭证的储存与调用,并在运行时使用占位符替换”听起来稍微安全一点。放心吧,我们也不是明文储存的,而是做了加密处理,这听起来是不是算得上一丝聊胜于无的慰藉?!如果真有黑客打进来,那就只能自求多福了!
-
撤回和重做:只修改历史记录,不退回文件编辑。善用 git,人人有责。
结语
折腾好累啊,但是下次还要折腾。
我的项目地址是:https://codeberg.org/CarbonC/AgeAge。太累了,所以暂时不会加新功能和新设计。
不管怎么样,希望这个饱含折腾之作不要烂尾吧!

评论 (0)
▾点击左下角右下角的看板娘登录后即可评论。
加载中...