从Nanobot源码读懂OpenClaw架构:Agent循环与技能机制解析

发布时间:2026/9/8 6:05:02
从Nanobot源码读懂OpenClaw架构:Agent循环与技能机制解析 1. 先搞清楚这件事的来龙去脉最近在折腾 OpenClaw这个项目在 GitHub 上的热度一直不低官方定位是“你的个人 AI 助理框架”玩法相当野——既能接 Telegram、Discord 这些消息渠道又能操作本地文件、执行命令还能自己装技能Skill。说白了它就是个带手带脚的 LLM Agent 壳子开发者真正的差异点在于你给它配了哪些能力、组织了哪些工作流。但问题也来了OpenClaw 的功能模块好几块配置项也多新手一上来很容易被各种概念绕晕。我在啃了一段时间官方文档之后发现想要真正吃透这套架构最狠的办法就是把源码拉下来硬读。尤其它内部还藏了个轻量实现 Nanobot整个代码量非常克制非常适合当“教学切片”用。这就像你想搞明白汽车原理没必要一上来拆整车先找个减速箱或者转向节的剖面图琢磨清楚再回头看整车就顺了。这篇文章就围绕通过 Nanobot 的源码去理解 OpenClaw 的总体架构这条线展开。我会结合自己的阅读路径按“入口文件 - 配置加载 - 会话循环 - 工具调用 - 记忆管理”的顺序拆解最后还会补一块实操部分教你如何在本地把 Nanobot 跑起来边跑边对照源码。适合谁看想深入理解 LLM Agent 项目源码结构的朋友正准备给 OpenClaw 做二次开发或写扩展的人以及那种“文档看了记不住非得读代码才心安”的选手。别指望这篇文章逐行注释每个文件那不现实我尽量做到的是帮你建立一张地图让你拿到源码后知道先看哪、为什么看它、它解决了什么问题。2. Nanobot 在架构学习里到底扮演什么角色2.1 学架构为什么要选一个“小”项目OpenClaw 本体功能相对完整目录会更多模块之间的依赖关系也更复杂。如果第一眼就直接扎进 OpenClaw 全量代码里很容易出现一种典型困境claw/agent引用了claw/memorymemory又依赖extension你顺着依赖链往下查半天之后发现自己还在第一层文件里转圈完全不知道整体是怎么运转的。Nanobot 则不一样。它是同一套架构理念下的最小实现去掉了各种渠道适配的复杂性保留了一条最干净的主链路接收消息 - 调用模型 - 执行动作 - 返回结果。项目规模小到你可以在一个下午读完核心部分但又没有小到失去代表性。我读完之后的直接感受是它是 OpenClaw 的一个“可运行架构图”。从学习价值的维度去看Nanobot 帮我们过滤掉了三类噪音渠道适配层Telegram、Discord 等的代码被简化不用处理大量平台 API 细节配置项被收敛到能跑通的最小集合避免在没搞懂核心机制前先被 YAML 淹死扩展机制被保留但更直观你想看“Skill 是怎么被加载的”直接找目录就完了。2.2 读源码之前需要补的一些基础概念直接看代码不是不行但如果对 Agent 类项目的基本概念有概念性认知效率会高非常多。这里快速过一遍 Nanobot 代码里一定会碰到的核心概念后面的章节会展开分析Agent Loop智能体循环这是整个系统的“心脏”。程序启动后它会不断重复“拿消息 - 构造上下文 - 调用大模型 - 解析模型返回的动作指令 - 执行动作 - 把结果再喂回给模型 - 继续”这个循环直到模型认为任务已经结束。你可以把它理解为工厂流水线上游传下来一个订单每个工位处理完再传给下一个最后包装出货。Skill技能一段可以被模型调用的功能模块。在 Nanobot 里一个 Skill 通常就是一个文件夹或一个文件里面声明了技能的描述、参数结构以及处理函数。模型不是凭空学会调用技能的它靠的是系统提示词里对这些技能的“说明书”描述。Tool Call工具调用大模型在对话过程中决定“我需要去访问某个外部能力”时会输出一个结构化的 JSON包含工具名称和参数。Agent 框架的核心工作之一就是把这种 JSON 转化成真正的函数执行再把执行结果交回给模型。这套机制现在在各大模型 API 里都有原生支持Nanobot 做的是把这些协议适配起来。Memory记忆模型本身没有记忆所以 Agent 框架需要自己维护历史消息、摘要或者向量索引。Nanobot 的 memory 部分相对直观主要解决“在一次会话里模型能记住之前聊了什么”以及“跨会话的持久化”这两件事。Extension扩展比 Skill 更底层的一种能力注入方式可能往系统提示词里加内容也可能往请求管道的某个环节加钩子。Nanobot 对这块的处理方式很直接读起来不费劲。有了这两个版本在脑子里我们再去看源码思路就不一样了——你不是在“看代码”你是在“验证自己脑内的模型对不对”。3. 源码总览先把骨架摸出来再谈细节3.1 目录结构与每个目录的真实用途这一步不需要任何技巧把仓库拉下来之后直接展开目录树就完事。以我当时读到的版本为例顶层大致长这样nanobot/ ├── src/ │ ├── agent.ts # Agent 主循环与上下文生成 │ ├── config.ts # 配置加载与校验 │ ├── memory.ts # 普通对话记忆管理 │ ├── skills/ # 内置技能模块 │ ├── extensions/ # 扩展机制的示例与实现 │ └── index.ts # 程序入口读取配置、启动 agent ├── skills/ # 用户可以自定义技能的地方 ├── data/ # 运行时数据会话持久化等 ├── config.json # 示例配置文件 ├── package.json └── README.md说实话我第一次看到这个结构的时候第一反应是“这也能跑”——它看起来实在是太精简了。但恰恰是这种精简让你可以毫无心理负担地进入源码阅读模式。对照 OpenClaw 本体的目录结构你会发现大方向是一致的入口启动 - 配置加载 - 初始化 memory - 启动 agent 循环 - 根据消息触发 skill。只不过 OpenClaw 包了更厚的壳。读目录的过程中有一个经验可以分享不要急着点进每一个文件。先只看文件名自己尝试回答“这个文件大概负责什么”然后翻一个文件验证你的猜想。这不是浪费时间这是在训练你对项目结构的直觉。等你以后读大型项目这种直觉能帮你快速定位代码位置。3.2 程序入口一切从 index.ts 开始读源码一定要找入口而且入口找起来很简单——打开package.json看main字段。Nanobot 的入口指向src/index.ts。这个文件做的事非常纯粹读取配置文件config.json或环境变量指定路径初始化 memory 实例创建 agent 实例启动一个事件循环或者消息监听器然后把收到的消息丢给 agent 处理。我摘一段核心逻辑的思路不贴完整代码因为版本不同代码会有些许差异重点看“它做了什么”// 思路伪代码基于我在源码里看到的实际流程整理 const config loadConfig(process.env.CONFIG_PATH ?? config.json); const memory new Memory(); const agent new Agent(config, memory); // 假设这里用标准输入模拟消息入口 const rl readline.createInterface({ input: process.stdin }); rl.on(line, async (line) { const response await agent.handleMessage(line); console.log(response); });这里最值得关注的是Agent这个类的构造函数——它接收了配置和记忆两个依赖。这就是一种很典型的依赖注入写法后续你想替换模型来源、改记忆后端只需要替换传入的实例就行。这个小细节在你理解 OpenClaw 的“扩展机制”时会有大用因为 OpenClaw 的很多扩展本质上就是“往 Agent 构造过程里塞自定义实例”。3.3 配置加载一切皆有默认值但一切皆可覆盖Nanobot 的配置加载逻辑在config.ts里。它做的事情说白了就是读文件 - 和默认配置深度合并 - 校验必填项 - 导出最终配置对象。我特别想提一下默认配置的设计思路。比如模型提供商、模型名称、温度参数这些代码里会先写好一串默认值然后用户配置的优先级更高会把默认值覆盖掉。这种设计的巧妙之处在于用户第一次跑起来的时候甚至可以不写任何配置程序用默认值也能跑通。等你理解了这个逻辑再去看 OpenClaw 那套复杂配置就会明白底层方法完全一样只是配置项数量增多分组更细而已。另一个值得留意的点是配置里的systemPrompt或者类似字段。这个字段直接决定了模型的行为模式。Nanobot 的默认提示词写得很有水平因为它把“你是一个 AI 助理”“你有这些技能可用”“工具调用完了之后要继续分析”这些指令全部塞进了系统提示词里。与其说 Agent 框架在控制模型不如说它在精心编写提示词来引导模型——这个认知一旦建立你以后设计任何 Agent 类产品都会受用。配置这块还有一个细节调试模式。我当时在config.ts里翻到一个开关打开后会在控制台打印出完整请求体。这功能特别适合用来排查“模型为什么没按照预期调用工具”的问题因为你能直接看到发给模型的消息到底长什么样。建议你自己看源码时也留意一下有没有类似的 log 开关没有的话也可以自己加一个对学习大有帮助。4. 核心机制拆解Agent 循环与工具调用4.1 Agent 主循环系统运转的发动机Agent 循环是整个源码里含金量最高的一段。Nanobot 把它封装在agent.ts的某个方法里核心逻辑可以用一段极简伪代码表示while (true) { 接收输入消息 把消息加入消息历史 调用 LLM API传入系统提示词 消息历史 可用工具描述 如果返回结果里有 toolCalls 逐个执行工具调用 把工具结果作为新消息放回历史 再次调用 LLM API让模型基于工具结果继续推理 否则 把模型生成的文本回复返回给用户 跳出循环 }注意循环里的一个关键点只要模型还在返回 toolCalls循环就不会结束。这就是为什么你让 Agent“帮我写个文件然后总结一下内容”的时候它先会调一次写文件的工具看到执行成功的结果然后才继续生成最终总结。这个“工具结果回填再调用”的机制在行业里有个说法叫 ReAct 模式Reason Act当年是从论文里来的现在已经成为 Agent 框架的地基。从工程实现的角度看这个循环里最容易被忽略、但也最容易出 bug 的地方是消息历史怎么随着工具调用结果增长。如果每次工具执行完你没有把工具结果追加进历史模型在下一次调用时就会“失忆”根本无法知道刚才写文件到底成功了没有。Nanobot 在这个部分的处理思路是直接维护一个消息数组结构上兼容 OpenAI 的多角色消息格式system、user、assistant、tool代码清晰很容易读懂。4.2 从“模型想调工具”到“工具真的跑了”之间的距离理解了循环之后你肯定会好奇模型输出一段带工具调用的 JSON代码是怎么知道要执行哪个函数的这一段的实现细节堪称整个项目里最值得抄作业的地方。完整的链路分为四步支撑第一步把技能转成模型的工具描述。每个 Skill 文件里都有name、description、parameters之类的元信息Agent 启动时把这些元信息整理成 LLM API 要求的 JSON Schema 格式放进请求里。这一步直接决定了模型能不能“看到”这些工具。第二步解析模型返回。当模型决定调用工具返回内容里会出现结构化的toolCalls。Nanobot 的代码里把这部分解析出来拿到工具名和参数对象。第三步从注册表里找到对应函数。Nanobot 维护了一个字符串到函数的映射表。这就像字典查询你给一个代号它返回一个真实函数引用。这里唯一的坑是参数校验——如果模型返回的参数缺失或者类型不对直接调用会炸所以好的实现都会带一层校验或容错。第四步执行并返回。执行结果会以标准格式返回成功就返回函数返回值失败就返回错误信息。关键点是错误信息也会被喂回给模型。这听起来有点反直觉但其实是设计亮点——模型看到错误之后可以自我纠错比如发现自己少传了一个参数下一轮自己就修正了。这里强烈建议你在源码里找到handleToolCall相关的方法自己手写一遍它的调用流程。写完之后你会明白所谓“Agent 控制工具”本质上就是“模型输出结构化文本 - 框架解析 - 函数调用”一点黑魔法都没有。4.3 技能Skill机制给 Agent 安上手和脚在 Nanobot 里技能有两种存在方式一部分内置在src/skills/里另一部分放在运行时的skills/目录。无论是哪种核心结构一致一个类或者一个对象对外暴露name、description和execute方法。我给你一个非常直观的“最小技能”示例就假设这个技能用来获取当前时间// 伪代码风格表现核心结构不同版本实现略有差异 export default class GetTimeSkill { name get_time; description 获取当前系统的日期和时间适合用户询问‘现在几点’时调用。; parameters { type: object, properties: {}, }; async execute(args: any) { return new Date().toISOString(); } }模型是怎么知道什么时候该调它的答案在系统提示词和工具描述里。当用户问“现在几点”如果系统提示词里没有任何关于时间的说明模型大概率会直接胡诌一个时间。但只要模型能看到get_time这个工具的存在它就会优先考虑调用它而不是自己回答。这就是工具描述写得越仔细模型调用工具越准确的原因。从源码阅读的方法论角度我想额外说一句看技能加载的代码时重点关注两个时间点——一个是启动时加载内置技能另一个是在运行时扫描外部技能目录。后者通常是用fs.readdir加动态导入实现的这也是实现“热更新技能”的基础。你能在 OpenClaw 本体里看到类似但更复杂的机制核心套路都是一样的。4.4 记忆模块让 Agent 不会“聊完就忘”记忆系统通常分成两块短期记忆和长期记忆。Nanobot 里面更侧重短期记忆——它维护当前会话的消息历史并想办法让它能在模型上下文限制内滚动。具体实现上常见的手段有两种直接截断最旧的历史消息或者对历史做摘要。Nanobot 的代码里用到的方案比较直接通常就是截断但代码结构预留了替换的空间。这里有个细节很值得思考什么时候该截断谁来决定截断如果不管三七二十一每条消息都原样塞进上下文那很快会把上下文窗口塞满后面的对话质量会急剧下降。Nanobot 的做法通常是设定一个最大历史条数超过就丢掉最旧的。这个方法简单粗暴但在会话不长的情况下完全够用。理解短期记忆之后再做扩展思路就清晰了。比如你想让 Agent 能跨天记住用户偏好那你就需要长期记忆组件典型做法是引入向量数据库把历史对话做 embedding然后在每次新对话前把最相似的几条历史记录取出来塞进上下文。OpenClaw 本体的记忆模块就是这么演进的。所以你读 Nanobot 的 memory 时不要觉得它“太简陋”它只是在向你演示“最核心的那块应该长什么样”剩下的都是可插拔的强化。5. 实操本地把 Nanobot 跑起来边跑边验证架构5.1 环境准备与最小配置说这么多不如实际跑一遍。先准备环境你需要 Node.js 20 以上版本和 npm/yarn/pnpm 任意一个包管理器。然后git clone nanobot仓库地址 cd nanobot npm install安装完依赖之后找到根目录下的config.example.json或者config.json。打开看一眼你会发现它需要配置模型供应商的 API Key。这里拿 OpenAI 兼容接口为例手动创建一个config.local.json{ modelProvider: openai, model: gpt-4o-mini, apiKey: 你的密钥, baseUrl: https://api.openai.com/v1, systemPrompt: 你是一个善于使用工具解决问题的助理。, maxHistory: 20 }然后启动npm run dev正常的话程序会在终端里打印出 Agent 已就绪的日志等待你输入内容。你输入一句“现在几点了”如果配置正确模型会走一遍 Agent 循环解析用户意图 - 决定调用get_time技能 - 执行技能 - 基于工具结果生成最终回复。全程日志里能看到模型返回的 toolCalls JSON。5.2 通过日志逆向拆解运行流程跑起来之后仔细观察控制台输出最好把日志里每一次 LLM 请求的信息都截下来。你会看到几条关键日志请求消息体大小和轮次模型返回的 toolCalls 内容工具执行结果最终回复。这些日志直接对应前面流程图里的每一步。这个习惯非常重要——日志不是用来排查问题的是拿来读懂系统的。当你用“日志 源码”互相印证的方式读熟了 Nanobot再去跑 OpenClaw 本体你会发现同样的日志结构出现了只是内容更多、渠道更多。也因为这个原因我特别建议你在阅读agent.ts的时候顺手在关键分支上打几个console.log打印出“当前消息数”“这次算不算工具调用”“历史截断到多少条”之类的信息。这种“主动打日志”的方式比干读代码快得多也能帮你建立对系统运行的直觉。5.3 加一个自定义技能感受扩展机制只跑通还不够强烈建议你亲手写一个技能试试。假设要加一个“计算两个日期相差多少天”的技能// 放在 skills/ 目录下 export default { name: days_between, description: 计算两个日期字符串YYYY-MM-DD格式之间相差的天数, parameters: { type: object, properties: { start: { type: string, description: 起始日期 }, end: { type: string, description: 结束日期 }, }, required: [start, end], }, async execute(args: any) { const start new Date(args.start).getTime(); const end new Date(args.end).getTime(); return String(Math.round((end - start) / 86400000)); }, };重启程序输入“帮我看看 2024-01-01 和 2024-12-31 差多少天”。看看模型会不会调用新技能以及调用的参数结构是否和你定义的一致。这里有个非常经典的调试现象如果你在description里写的是“计算日期”模型可能在你问“一个礼拜有多少天”时也去调它因为描述太模糊。把描述改成“计算两个具体日期字符串之间的天数差适合用户给出两个明确日期时调用”模型的选择就会更精准。模型不是你的代码它只会理解你写的说明。这一个例子比你看十篇文章都能理解“为什么技能描述这么重要”。5.4 用调试器和测试代码逐行追关键路径最后再推荐一个硬核玩法直接在agent.ts里设置断点用 VSCode 的调试器单步执行。你在handleMessage方法上打一个断点然后触发一次简单对话观察变量变化messageHistory数组是怎么增长的模型返回的原始响应对象长什么样toolCalls数组在哪一步被解析成了函数实参。这个过程会让你对数据流的理解直冲天花板。如果项目里有测试文件顺手跑一下npm test测试用例本身就是“最小可运行示例合集”比单纯看代码更直观。我读这么多开源项目最大的心得就是源码 断点 日志三者配合才是读代码的最优解只看不跑等于没看。6. 常见问题与排查心得这块内容是重点中的重点。我在学习 OpenClaw 和 Nanobot 的过程中踩过不少坑下面分成四类每条都是血泪换来的经验。6.1 模型 API 相关请求报错与上下文超限症状 1启动时提示模型请求失败。首选检查三个地方API Key 是否设置正确、baseUrl是否正确自建网关和官方接口不一样、网络环境能不能连上模型服务商。这几个都属于最基础的检查项但几乎 80% 的失败都出在这。症状 2对话进行到一半报“context length exceeded”。这就是前面提到的上下文窗口上限问题。Nanobot 的maxHistory参数就是干这个用的把它调小一点比如从 20 调到 10效果立竿见影。另一种思路是改用上下文更大的模型但成本会更高。从架构角度看这个报错恰好提醒你Agent 框架的记忆管理模块不是可选项是必需的。6.2 技能加载失败模块路径与导出格式症状自定义技能没被加载日志里没有任何相关信息。排查步骤确认技能文件放在正确的目录通常是skills/注意大小写确认文件默认导出对象或者类的方式和项目要求一致重启程序并观察启动日志里是否有“加载到 N 个技能”这行输出。很多人第一次自定义技能失败不是逻辑写错是导出格式不对。比如写成了具名导出export function但框架用的是默认导出export default那自然加载不到。这个知识点你在读源码的时候就会发现所以再次印证先读源码再写代码能少踩无数坑。6.3 权限和文件系统相关exec-approvals.json 的作用如果你用的是 OpenClaw 本体跑某些技能或者工具时可能会看到exec-approvals.json这个文件。它其实是“命令执行审批”的存档文件OpenClaw 会把一些敏感操作比如执行 shell 命令标记为需要审批只有你手动批准过的命令才会被写进这个文件。这样做是为了防止模型在循环里偏离目标做出一些不可控的操作。从这个细节你能感受到框架设计的一个重要原则能力越强越要加约束。读代码的时候看到这层逻辑别觉得是“麻烦”它其实帮你守住最后一道安全线。Nanobot 相对简化了这块但你在阅读时也可以想想如果这个技能会导致破坏性操作框架该如何约束带着这个问题去看 OpenClaw 的权限模型理解会更深。6.4 Workspace 目录运行时的文件沙箱在 OpenClaw 里有个概念叫 workspace默认路径类似~/.openclaw/workspaceAgent 操作文件一般都在这个目录内进行。它的意义在于把 Agent 的工作范围限制在一个沙箱里避免它读写系统关键目录。Nanobot 中也会有类似约定只是没那么强调。在实际使用中我建议你把 workspace 指向一个专门的目录不要把整个用户目录暴露给 Agent。不是说不信任它而是模型在长对话里可能会产生不可预见的操作一个隔离的工作区能把风险降到最低。7. 读完 Nanobot 之后再回头看 OpenClaw最后一部分我想聊点更高层面的东西。Nanobot 是一张地图但它的目标是让你能看懂 OpenClaw 这座“大城”。7.1 从 Nanobot 到 OpenClaw 需要补什么两者代码结构相似但 OpenClaw 增加了几个大模块渠道适配层、多用户会话管理、更完善的安全审批机制、技能市场式的动态安装流程。你通过 Nanobot 学会了“Agent 循环”和“技能加载”再去看 OpenClaw 这些新增模块时会发现它们都是围绕核心循环做的外围强化没有改变本质。7.2 如何快速上手 OpenClaw 的源码顺手分享一下我的阅读顺序先看package.json的依赖列表了解它依赖了哪些大框架找入口文件理清启动流程和配置加载顺序看 Agent 主类和 Nanobot 的 Agent 类做对比找出新增字段和方法重点看渠道适配层比如如何接 Telegram、Discord最后看权限审批和 workspace 的实现体会生产级框架的安全设计。这样读下来你至少能对 OpenClaw 建立起“整体不乱、局部复杂但套路一致”的认知。等你真到了需要改代码的时候你已经知道该从哪个目录下手了。7.3 为什么我建议你用“源码学习”代替“文档学习”我不反对读文档文档能帮你快速了解功能特性。但架构感这种东西光靠文档是养不出来的。文档告诉你“能做”源码告诉你“为什么能做到”后者才是你迁移到新项目时真正能带走的能力。比如你读完 Nanobot 以后就算让你用 Python 重写一套简化版 Agent你也能熟练地画出模块边界写出循环逻辑。这个能力比记住某个框架的 API 值钱得多。现在如果你手头还有其他 Agent 类项目想读我的建议是照这个方法再来一遍先找入口、再找循环、再看扩展机制、最后补安全边界。框架千变万化套路就那几样。我自己的体会是源码阅读是一项极其划算的投资。花一个周末读完 Nanobot省下的是后面读 OpenClaw、读其他 Agent 项目时无数个挠头的深夜。你现在打开仓库跑一遍比收藏这篇文章有用一百倍。