从Nanobot源码看Agent架构:OpenClaw核心链路剖析
我不打算先铺垫那些“读源码之前你需要知道的知识点”直接说结论OpenClaw 这类 AI Agent 项目最容易让人劝退的地方不是模型能力不够而是架构太绕。你以为打开仓库就能看懂结果扑面而来的是事件总线、插件系统、多通道适配器、会话状态机……每一层都写得挺讲究但合在一起就成了迷宫。我自己读 OpenClaw 源码的时候读了两天还在入口函数里打转。后来换了个思路先拿 Nanobot 这个相对轻量、边界清晰的 Agent 框架当解剖样本把整个调用链摸透再回头看 OpenClaw 就顺畅多了。这篇文章就是记录我“先学 Nanobot 源码、再反推 OpenClaw 架构”的整个过程标题里的“总体刎”你可以理解成“总体把握”——我们要做的不是逐行背书而是把骨架先立起来。1. 为什么拿 Nanobot 当解剖样本1.1 先搞清楚 OpenClaw 和 Nanobot 的角色差异OpenClaw 是一个集成度非常高的智能体网关项目它把聊天机器人、工具调用、多平台接入、消息路由、记忆存储这些东西全部揉在了一起。好处是开箱即用坏处是当你试图搞清楚它内部原理的时候代码规模和政治边界会让你很头疼。Nanobot 则更像一个“骨架型” Agent 框架它只做几件最基本的事接收消息、调用大模型、执行工具、返回结果。没有复杂的插件生态没有花哨的可视化界面代码规模控制得很小。但麻雀虽小五脏俱全Agent 架构里最核心的那几条链路它都有消息标准化、会话管理、工具注册与调用、模型无关抽象。我实际对比过两个项目的源码体量OpenClaw 的核心逻辑散落在几十个模块里而 Nanobot 的核心路径只需要跟着几条 TypeScript 文件就能走完。对于想学架构的人来说Nanobot 是“教学版”OpenClaw 是“工业版”。1.2 架构学习比 API 学习重要得多很多人学开源项目第一反应是跑 demo跑通了就觉得“我会了”。但跑 demo 只解决了“怎么用”没解决“为什么能跑”。比如 OpenClaw 里一个看似简单的“发消息给 botbot 调工具查天气然后回复你”的流程背后涉及消息适配层、意图路由、工具参数提取、模型上下文组装、工具结果回填、会话持久化这一整套机制。你在 demo 里看到的是结果成品你学的应该是这套机制。而这套机制在 Nanobot 里被拆得非常干净每条链路都是独立模块模块之间靠清晰的接口通信。你只要读懂 Nanobot再看 OpenClaw 那些模块命名和依赖关系几乎不用费劲就能猜出每一块是干什么的。2. 源码总览先画整体鸟瞰图2.1 仓库结构拆解Nanobot 的仓库结构大概是这样的不同版本会略有出入但总体分层思路稳定src/ channels/ # 消息渠道适配 adapter.ts # 适配器抽象 cli.ts web.ts core/ # 核心逻辑 agent.ts # Agent 主循环 context.ts # 上下文对象 message.ts # 消息模型 tool.ts # 工具抽象 providers/ # 模型提供方适配 openai.ts anthropic.ts ... storage/ # 会话存储 memory.ts index.ts # 入口这四层就是 Agent 项目的标准分层适配层、核心层、模型层、存储层。2.2 启动入口一进到底Nanobot 的入口做得非常干净。我最初以为会看到一个复杂的依赖注入容器结果看到的是一个顺序执行的引导流程初始化存储引擎注册内置工具配置模型提供方启动各渠道适配器进入消息等待循环整个启动过程说白了就是你手写一个聊天机器人时也会做的步骤准备好数据准备好大脑准备好吃进去的消息然后开跑。我之前在 OpenClaw 里找了半天的“入口在哪”绕了很多弯路因为它的启动过程要考虑容器环境、插件加载、平台鉴权等多层逻辑。但你要是先熟悉了 Nanobot 的启动顺序再看 OpenClaw 的入口会发现它本质上也是这套顺序只是每一层都叠加了更多扩展点。2.3 核心对象模型先看懂三个读任何源码之前先看它的数据模型百试百灵。Nanobot 有三个核心类型理解了它们后面的调用链就顺了。消息模型NormalizedMessage所有渠道的消息进来之后都会被统一成同一个结构——带role、content、toolCalls、toolCallId这些字段。这就是消息标准化你自己写机器人对接多个平台时如果每个平台一套数据结构后面做公共逻辑会非常痛苦标准化的价值在渠道一多的时候立刻体现出来。上下文对象Context每次对话都会生成一个上下文用来承载当前会话的状态、用户信息、历史记录、调用链路上的临时数据。我在读代码的时候想过一个问题为什么不直接传消息对象而是要包一层 Context后来动手改代码才发现随着 Agent 功能变复杂你需要传递的东西越来越多——当前会话 id、当前渠道类型、自定义业务字段、中断标记、工具执行结果缓存。如果只传消息对象每次加东西都要改函数签名而用 Context 就灵活得多。工具抽象ToolNanobot 把工具定义成一个结构化对象名称、描述、参数 Schema、执行函数。这个设计很不起眼但它几乎是整个 Agent 架构里最关键的一个决定因为大模型能不能正确调用工具完全取决于你给模型的“工具说明书”写得好不好。你可以在逻辑层任意扩展工具能力而在模型层看到的永远只是一份 JSON 描述。3. 关键链路拆解一次对话的完整旅程3.1 消息接入适配器如何把外部输入变成统一格式从一段原始消息到一个标准的NormalizedMessage中间经历了几次转换。以 CLI 渠道为例终端输入的文字被接进来之后适配器会根据当前上下文构造一条用户消息再交给 Agent 处理。这段转换逻辑的核心思想是“适配器只负责翻译不负责决策”。CLI 适配器不需要知道大模型是什么不需要知道工具怎么执行它做的事情就是把你敲的字包装成一个消息对象然后丢给下一层。同理Web 渠道的 HTTP 请求进来后Web 适配器也只负责解析参数、包装消息绝不越权处理业务逻辑。这个设计直接保证了渠道的可插拔性以后想加一个钉钉渠道只需要新增一个适配器把钉钉消息格式转成NormalizedMessage就完事了核心链路一行不用改。3.2 调度编排Agent 循环不是简单的“输入-输出”很多人以为 Agent 就是“用户说一句模型回一句”读完 Nanobot 核心循环你就知道理想化太多了。真实的核心循环长这样接收用户消息 循环 把当前对话历史 工具描述 组装成请求 请求模型生成回复 如果回复里包含工具调用请求 解析并执行对应工具 把工具结果追加到对话上下文 回到循环开头再次请求模型 否则 输出最终回复结束循环这就是 Agent 与普通聊天机器人最本质的区别模型可以在一次对话中多次被调用每次调用之间可能穿插工具执行。Nanobot 的循环实现得非常直白但我第一次代入真实场景时才意识到这个循环的威力。比如说用户问“北京和上海今天哪个适合户外跑步”模型先调用天气工具查询两个城市数据然后拿到结果再综合回答这中间发生了两次模型推理而用户只发了一条消息。整个过程是在循环里自动完成的用户感受不到中间经过了多轮“思考”。3.3 工具调用参数提取和 HITL 确认机制工具调用的实现是整个 Nanobot 架构里含金量最高的部分。你说“帮我定个明天早上八点的闹钟”模型在生成回复的时候会同时输出一个结构化的函数调用请求Nanobot 拿到这份请求之后要做什么第一步是校验把模型输出的参数 JSON 对照工具定义的 Schema 加载一遍缺什么补什么类型不对的尝试转换。第二步是匹配工具找到对应名称的执行函数。第三步是跑一个我特别想提的机制——HITLHuman In The Loop也就是人在回路确认。某些敏感操作在执行前会先弹确认问用户真的要执行吗我当时第一次看这段代码的时候有点吃惊因为很多人做 Agent 时会忽略这个环节结果就是 AI 乱调用工具、乱发消息、乱删数据。Nanobot 把这个确认机制放在了工具执行链路里说明作者是真的在生产环境里遇到过灾难才会设计得这么务实。我自己的经验是凡是会对外产生副作用、不可逆、涉及成本的操作都应该做确认。比如发邮件、下单、删除文件、转账这些操作一旦执行就回不了头没有确认就是给自己埋雷。这件事无论你是从零写 Agent还是基于框架二次开发都应该当成铁律。3.4 会话记忆上下文永恒增长的隐患Nanobot 的会话管理用的是比较朴素的内存存储方案每个 session 维护一个消息列表新消息进来就追加请求模型的时候把列表整体作为上下文传过去。这个方案写起来最省事但有个非常典型的隐患——上下文窗口有限。聊得越久历史消息越多模型能接收的内容就越来越少最终直接顶爆 token 上限。我当时用 Nanobot 做了个压力测试连续对话 50 轮之后模型开始“失忆”明显感觉到前面的对话内容被截断了后面回复质量直线下降。这个问题的根源不在 Nanobot而是所有基于有限上下文窗口的 Agent 都会面临的问题。正确解法通常是分层记忆短期记忆存近期对话中期记忆做摘要压缩长期记忆用向量库检索。OpenClaw 里就是做了这种多级记忆的而 Nanobot 把最简单的版本给你看懂了后面的优化方向你需要自己动手。4. 从 Nanobot 反推 OpenClaw架构中的“变”与“不变”4.1 共同的架构骨架事件流贯穿始终读完 Nanobot 再回头看 OpenClaw 的架构你会发现它们的骨架惊人的相似消息从渠道进入系统Agent 自己变成循环运转工具执行穿插在中间任务流中。核心区别在于 OpenClaw 把这条主线包装成了事件驱动架构在每一步操作上增加了一个事件总线。事件驱动的好处是大规模扩展时非常灵活比如我加一个消息日志功能不需要修改任何核心代码只要监听消息事件再写自己的处理逻辑就行。坏处是调试困难你很难直观地知道一条请求到底走了哪条分支因为每个节点都可以被任意订阅者截获。我的建议是如果你想快速掌握 OpenClaw不要从事件总线入手先找一条本地到端最简链路从入口文件走到最终执行理出主干再慢慢看哪些环节发了什么事件、哪些模块订阅了这些事件。4.2 模块化边界插件系统的两面性OpenClaw 里插件系统是一个非常核心的设计它允许社区贡献各种 skill、通道适配器、模型提供商上方生态都是靠这个支起来的。但它也是一把双刃剑——插件越多模块间隐含的耦合越重新人上手难度越大。Nanobot 则几乎没有真正意义上的插件系统它的扩展靠“改代码”实现加一个工具就是写一个 tool 文件加一个渠道就是写一个 adapter 文件然后手动注册。这种方式看起来原始但对于理解源码来说反而是优点依赖都是显式的不会出现“这个能力是哪来的”的困惑。如果你想基于 OpenClaw 二次开发我强烈建议先做一件事把项目里所有插件和内置模块的依赖关系列出来画一张模块依赖图。这一步做完你对整个项目的理解会有一个质的飞跃。4.3 状态持久化从内存到数据库的必经之路Nanobot 默认把会话放在内存里进程重启记忆清空。这在学习阶段没问题但到了真实部署阶段就是灾难。OpenClaw 默认的持久化方案就能对接多种存储因为真实场景必然要跨进程、跨机器。我自己踩过一个非常愚蠢的坑第一版 Agent 服务用内存存储消息历史上线后一切正常结果某天深夜部署平台自动重启了容器第二天所有用户进来都是“全新会话”半天工单轰炸才反应过来是存储没落地。如果你用 Nanobot 学完架构之后自己动手做生产项目第一件事就是把内存存储换成持久化存储别等踩了坑再改。5. 跟着源码亲手改一个功能从读到写的跳跃5.1 给 Nanobot 增加一个自定义工具读源码不等于学会亲手改才能把“看懂的”变成“会用的”。我的建议是给 Nanobot 加一个最简单的自定义工具获取服务器当前时间。在src/tools/目录下新建一个文件导出符合Tool接口的对象export const currentTimeTool { name: get_current_time, description: 获取服务器当前时间, parameters: { type: object, properties: {}, }, async execute() { return { time: new Date().toISOString(), }; }, };然后在入口文件里注册这个工具。重启项目后你可以直接问模型“现在几点”模型会“聪明地”调用这个工具而不是凭空编一个时间。就这么一个简单的功能你实际动手会碰到几个之前读代码时完全注意不到的问题工具描述怎么写模型才愿意调用它参数 Schema 里的字段名和描述对模型有多重要工具返回结果的格式是不是越简单越好这些问题只看源码是想不出来的必须亲手试一遍才会有真实的体感。我从这里开始意识到Ag ent 项目架构设计里工具描述文本和参数 Schema 的重要程度不亚于核心调度逻辑。模型像一个实习生只会照着你的“操作手册”干活手册写得烂再好的骨架也白搭。5.2 改造消息循环加入人工审核节点第二个建议改造的点是消息循环。在 Nanobot 核心循环里模型返回结果到正式输出之间加一个“审核”节点。你可以在 Agent 的循环里把最终回复先存到上下文、标记为“待确认”然后走一个回调函数确认通过后再真正输出到渠道。这样做的目的不是推翻原本设计而是让你理解哪些位置适合插入横切逻辑。这个改造做完之后你会重新理解“Agent 可控制性”这个名词完全自主地让 AI 干活一旦开始跑偏返工成本极高。而通过架构层面的时机设计你可以低成本地控制 AI 的每个行为。5.3 聊天记录落地到 SQLite第三个建议把 Nanobot 的内存会话存储改成 SQLite 存储。这一改你就能理解什么是存储抽象、什么是序列化、什么是会话并发锁。原项目的 memory storage 是个很简单的 Map 结构你只需要把get/set/append这几个操作换成 SQLite 语句就行。改完后你会发现多进程部署终于不会互相覆盖上下文了但也带来了新的问题并发读写冲突、序列化效率低、历史消息无限增长。这些问题出现之后你才会理解为什么生产级 Agent 需要精心设计会话存储策略。6. 常见问题与踩坑实录6.1 模型就是不调用工具怎么办这是所有人都会遇到的问题工具定义好了按文档注册了模型就是不调用。我这里列一个排查顺序排查点操作方法工具描述是否清晰描述里写明“什么情况下用”“用来做什么”少用抽象词汇参数是否必要工具参数越少越好模型更倾向于调用简单的工具模型是否支持部分模型对函数调用支持不完善优先选用原生支持 tool calling 的模型上下文是否够新模型用的是你给它看的工具描述不是说你改了代码就生效需要确认请求体里确实带上了最新工具定义我判断这一项排在第一位的原因是大部分所谓“模型不听话”都是工具描述不够具体导致。模型根本不知道这个工具有什么用当然不会主动使用。6.2 工具执行结果太长上下文爆炸这个问题在真实 Agent 场景里极其常见。工具返回 10 万字符模型上下文只有 8k还没等到结果读完就已经截断了。我的处理原则是能返回摘要就不返回全文能只返回状态就不返回内容。比如查询数据库真正需要给模型看的往往只是“有多少条数据、大概什么类型、有没有异常”而不是把每一条记录都塞进去。工具返回内容的精简程度直接影响 Agent 的稳定性。6.3 多会话并发时的数据串线Nanobot 的设计里每个会话有独立的上下文。但如果你自己实现的时候把上下文数据存到了全局变量里就会出现 A 用户的问题被 B 用户看到这种灾难。我建议所有上下文数据都放在 Context 对象里传递而不是依赖全局变量。全局变量这套方案在单测阶段看着没事一旦上了生产环境用户一多立刻爆雷。7. 从源码学习到实际落地的最后一步说实话读完 Nanobot 源码之后我并没有停下来而是基于这套架构认识重新梳理了 OpenClaw 的代码发现很多模块都能对应上。这时再看那些以往的困惑基本都能自己给出答案了。如果你也想走这条路我的建议是从小处入手先跑通 Nanobot再模仿它的架构写一个极简版 Agent然后把 OpenClaw 里对应的模块一个接一个地对照着看。这个过程不会太轻松但每多搞懂一个模块你手里就多一个可以复用的架构武器。另外一个小心得读源码的时候不要追求读完全部文件。二八法则在这里同样适用吃透 20% 的核心主链路剩下的 80% 大概率只是在不同维度上做业务扩展。你先能画出这个项目的主干图就已经赢过一大半学习者了。我个人在实际操作中的最大体会是Agent 项目的门槛从来不是大模型本身而是工程化能力。模型负责聪明代码负责可控。Nanobot 给出了一个足够清晰的可控性样板你把它吃透再往后看任何 Agent 架构都会有一种“三招制敌”的轻松感。