十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Nanobot反推OpenClaw:Agent框架源码核心骨架解析

用Nanobot反推OpenClaw:Agent框架源码核心骨架解析 最近在啃 OpenClaw 的源码。坦白说OpenClaw 这类项目什么都好就是对第一次看源码的人不太友好。模块多、入口多、各种连接器、部署脚本、Skill 仓库混在一起光是把整个仓库结构过一遍就容易劝退。后来我换了个路子先精读 Nanobot——一个体量小得多的 Agent 框架——把 Agent 最核心的运行骨架吃透再回头对照 OpenClaw 的模块划分。这套路径走下来效果比直接硬啃好得多。这篇文章就是这套学习路径的记录标题里“总体刎”三个字盲猜是想打“总体剖析”。要是你也被某个大型 Agent 项目的源码体积吓到过或者想搞清楚 Agent 框架到底由哪几个核心模块组成这篇应该能帮你省下不少弯路。我会从“为什么用 Nanobot 反推 OpenClaw”讲起再拆 Nanobot 的源码骨架最后落到 OpenClaw 的架构扩展和实战搭建上。1. 为什么用 Nanobot 来反推 OpenClaw 的架构1.1 OpenClaw 和 Nanobot 的定位差异OpenClaw 是典型的“全家桶”Agent 项目你能想到的形态它基本都有安装脚本、Windows 离线整合包、多种 IM 接入插件、Termux 环境部署、甚至还有往 ESP32 上移植的玩法。功能多意味着代码多而且这些功能往往围绕同一个核心循环展开外围代码会把你淹没。Nanobot 的定位则非常克制。它基本上只做一件事给你一个最小可用的 Agent 内核。没有复杂的插件体系没有一堆连接器核心逻辑就是“读取输入、调模型、执行工具、返回结果”。维度OpenClawNanobot代码量大模块多小结构清晰运行形态多端接入、可部署到不同环境偏向单进程、单会话核心能力Agent Skill 连接器Agent 内核适合场景生产级、多端、可扩展学习原理、快速验证上手门槛高低如果你一开始就冲 OpenClaw 源码去大概率会陷入“每个目录都打开过但每个目录都没看完”的状态。先看 Nanobot等于先把骨架抽出来再回来看 OpenClaw 时你会很清楚哪些代码是核心、哪些是外围适配。1.2 小项目学架构的优势小项目学架构最大的好处是“没有地方可躲”。代码只要少每一行都有它的位置你不会因为层层封装而找不到真正的逻辑。我在读 Nanobot 源码时最大的感受是它把 Agent 最本质的三件事摆在了明面上——调模型、管消息、执行工具。这三件事对应到任何 Agent 项目里都是逃不开的主干。大项目会把这三件事包装成各种 Service、Manager、Executor但底层的运行逻辑不会变。用个不恰当但很贴切的类比学写字先临摹大字帖而不是直接去抄一篇密密麻麻的公文。Nanobot 就是那张大字帖。OpenClaw 里的 Skill 机制、连接器抽象、部署脚本都是在 Nanobot 这一层骨架上长出来的肌肉和皮肤。骨架没搞清楚之前看肌肉只会觉得“这里鼓一块那里鼓一块”不知道为什么要这么长。1.3 我看源码时先锁定的 4 个关键模块读源码最忌讳从头翻到尾应该按运行主线来。我在 Nanobot 里锁定了 4 个关键模块顺序如下入口文件。看启动时初始化了什么外部依赖有哪些。消息循环。看 Agent 在拿到一条输入后到底经历了哪些步骤才给出回复。工具注册机制。看用户能力是怎么挂载到 Agent 身上的。上下文管理。看多轮对话时历史消息如何保留、如何截断。按这个顺序读下来你会发现整个项目其实是一条直线入口初始化好一切循环反复执行工具负责真正干活上下文保证 Agent 不会失忆。后续再看 OpenClaw 时我也是用同样的四步去找对应模块只不过 OpenClaw 里每一步都变得更厚实。2. Nanobot 源码里的 Agent 核心骨架2.1 入口文件与全局初始化Nanobot 的入口逻辑非常直白简化后大概是这样的过程先读取配置再创建 LLM 客户端然后把内置工具注册进一个注册表最后启动 Agent 的消息循环。# 简化的入口逻辑展示 Nanobot 启动时做了什么 def main(): config load_config(nanobot.yaml) llm create_llm(config.llm) registry ToolRegistry() registry.register(TimeTool()) registry.register(WeatherTool()) agent Agent(llmllm, toolsregistry) agent.run()这段代码几乎不需要注释就能看懂。但值得琢磨的是“为什么入口要做这三件事”。配置文件先行意味着项目的所有可调参数都被集中管理而不是散落在代码各处LLM 客户端单独创建说明模型服务被当成外部依赖隔离任何需要调用模型的地方都通过这个 client 走工具注册则定义了这个 Agent 能干什么。我自己看源码的习惯是先把入口函数里所有初始化调用列个清单再逐个去看对应类。这样你心里会有一张“外部依赖清单”哪些是要连网络的、哪些是纯本地的、哪些是可替换的一目了然。OpenClaw 的入口虽然复杂很多但本质上也是先做配置、再做连接、最后启动主循环只是多了环境探测和连接器初始化。2.2 对话循环与事件分发Agent 的核心不是某一类复杂算法而是那个反复执行的循环。Nanobot 的 agent loop 拆开看本质上是一个带工具调用的 while 循环收集新的用户输入追加到 messages 列表。把完整 messages 发给模型。检查模型返回内容里有没有 tool_calls。如果有逐个执行工具把工具结果追加到 messages回到第 2 步。如果没有 tool_calls说明模型已经给出最终回答把这段文本返回给用户。这个循环最关键的终止条件有两个一是模型不再返回 tool_calls说明任务结束了二是触发最大迭代次数防止死循环。实际项目中第二个条件极其重要。模型在复杂任务里经常会出现“调用工具 - 拿到结果 - 继续调用工具”的循环如果工具结果一直不满足模型预设条件它可以一直调下去。所以几乎所有 Agent 框架都会有一个 max_iterations 参数。我在 OpenClaw 的源码里也看到类似的防御逻辑只是它的叫法更花哨但底层的思路完全一致。# 极简 agent loop 伪代码 for _ in range(max_iterations): response llm.chat(messages) if not response.tool_calls: return response.content for call in response.tool_calls: messages.append(tool_result(call)) return 迭代次数超限这段伪代码基本就是 Nanobot 消息循环的骨架。理解了它你就理解了所有 Agent 框架的引擎部分。2.3 工具注册机制与 function calling工具注册是 Agent 框架里最容易出彩也最容易踩坑的模块。Nanobot 的做法是把每个工具描述成一个 JSON Schema让模型在对话时决定是否调用它。# 一个工具注册后模型能看到的 schema 长这样 tool_schema { type: function, function: { name: get_weather, description: 查询城市天气, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } }这段 schema 的作用不是给程序读的而是给模型读的。模型根据 description 和 parameters 判断“我现在需不需要调用这个工具、传入什么参数”。它不像传统代码那样“调用一个函数”而是“生成一个 JSON描述我想调用哪个函数、传什么参数”框架再去解析并分发。分发逻辑也非常直接本质就是一个字典查表async def dispatch(name: str, arguments: str): tool TOOLS.get(name) if not tool: return {error: ftool not found: {name}} args json.loads(arguments) return await tool[fn](**args)不要小看这个字典查表。OpenClaw 的 Skill 系统、插件系统、甚至多 Agent 调度底层都是这个模式的变种。你理解了“工具注册 维护一张名字到函数的映射表”再看任何 Agent 框架的能力扩展机制都不会慌。有一点需要特别注意工具函数的入参必须严格匹配 schema 里的 properties否则模型生成的参数在解包时很可能报错。实践里我给工具写参数时都会故意写成“尽量简单、平铺、不要嵌套对象”因为嵌套对象会让模型生成参数的出错率明显上升。2.4 记忆与上下文管理多轮对话的关键是上下文管理。Nanobot 的做法很实在用一个列表维护所有消息在发送给模型前做一次裁剪只保留最近 N 轮同时确保 system prompt 不被裁掉。def cut_messages(messages, max_tokens8000): # 估算每条消息的 token 数从最旧的消息开始丢弃 # 直到总长度低于 max_tokens while estimate_tokens(messages) max_tokens: if len(messages) 1: break messages.pop(1) # 保留 messages[0]system丢弃后面的旧消息 return messages有人会问为什么不能把所有历史全塞给模型原因有两个成本和窗口上限。模型上下文有长度限制token 超了就报错另一方面即使不超历史越长每次请求的耗时和费用都线性上涨。滑动窗口是性价比最高的方案。Nanobot 这种“无脑截断”的方式在简单场景够用但它有个明显缺点如果被截断的消息里包含关键信息Agent 就会“失忆”。OpenClaw 在记忆模块上明显走得更远它会做摘要、向量检索、长期存储但本质上解决的问题和 Nanobot 一样在有限上下文里保留最重要的信息。从源码学习的角度看我建议你先吃透滑动窗口再看摘要压缩最后看向量检索这条路最顺。3. 从 Nanobot 到 OpenClaw架构扩展的推演3.1 单 Agent 到多 Agent / Skill 体系Nanobot 是一个循环、一个模型、一组工具。这个模型在简单任务上表现不错但现实世界中一个能干的 Agent 往往需要挂载大量能力。OpenClaw 的解法是引入 Skill 体系每个 Skill 包含一组工具定义、触发规则、可能还有独立的提示词按需加载用完可卸载。对比一下就很清晰Nanobot 像一把瑞士军刀工具是固定在刀身上的OpenClaw 是可换模块的工具箱你出门前决定带哪几个模块。这个设计带来的直接好处是 prompt 不会被所有工具的定义撑爆。我见过有人把 50 个工具全塞给模型结果模型选择工具的错误率高得离谱因为 description 互相干扰。OpenClaw 用 Skill 把能力分组后每次只暴露当前任务需要的那一组工具模型的选择准确率会高很多。你在看 OpenClaw 的 skill 推荐列表时会发现很多 Skill 本质上只是“一组工具 一段使用说明”比如联网搜索、网页解析、代码执行。它们都遵循同样的注册和分发机制只不过被组织成了可插拔的单元。3.2 单进程到多端接入Nanobot 的输入来源很简单命令行敲一句话就完事。但 OpenClaw 需要同时服务多个不同的端终端、IM 插件、物联网设备、甚至跑在 ESP32 这类小硬件上。如果消息循环直接依赖特定输入端项目就没法扩展了。架构上的解法是抽象出 Connector 层也就是把“收到一条消息”和“来自哪个渠道”解耦开class Connector: def receive(self) - Message: ... def send(self, msg: str): ...每个端实现自己的 ConnectorAgent 核心循环只面对统一的 Message 对象。新增一个端就是新增一个 Connector 实现类核心循环完全不用改。这个设计在 OpenClaw 里被大量使用你看到的各种 IM 接入插件本质上都是 Connector 的具现化。这给源码学习提供了一个重要启发看一个 Agent 项目扩展能力有多强先看它的消息入口是写死的还是抽象过的。写死的入口加一个渠道就要大改抽象过的入口加渠道永远是新写一个类不动主干。3.3 部署形态的差异部署方式往往能反映项目架构的成熟度。Nanobot 的部署非常简单装依赖跑起来就行。OpenClaw 则面对更复杂的场景用户可能用安装脚本指定 git 分支安装、用 Windows 离线整合包、在安卓 Termux 里跑、甚至在更小的设备上跑。这里最值得学的不是安装脚本本身而是它背后的“环境校验”设计。OpenClaw 启动时会做各种环境探测比如确认当前是不是 WSL2 环境、有没有对应架构的二进制、依赖版本对不对。如果校验失败会明确告诉你“could not safely verify the WSL2 environment”而不是直接崩溃。这个设计在跨平台项目里非常重要。我在自己项目里也养成一个习惯所有部署脚本都分两层第一层是纯环境探测脚本用最简单的命令判断系统类型、架构、依赖是否齐全第二层才根据探测结果决定安装策略。不要一边安装一边校验否则用户看到的错误五花八门很难定位。3.4 我眼中的 OpenClaw 核心分层把 Nanobot 的骨架外推我在 OpenClaw 里看到的主要是这样几层分层职责对应 Nanobot 里的原型配置层读取 yaml、环境变量、CLI 参数入口处的 load_config接入层处理来自不同渠道的消息agent.run() 前的输入收集编排层Agent 循环、多 Agent 调度、任务分解while 循环本身能力层Skill、工具注册、工具分发ToolRegistry dispatch存储层会话状态、长期记忆、文件缓存messages 列表的持久化版本这个分层把 OpenClaw 的复杂表象简化成五件事。你读它的源码时心里带着这张表每看到一个文件就先问一句“它属于哪一层”。如果哪一层都归不进去大概率是辅助代码可以暂时跳过。这套分类法我后来也用在了其他项目上效率提升很明显。4. 实操基于 Nanobot 风格搭建一个最小 Agent 骨架4.1 准备环境与依赖理论讲再多不如实际跑一个最小骨架。我在本地测过的环境配置如下Python 3.10 以上安装 openai 库就行。模型服务可以用 OpenAI 兼容接口只要对方提供了 base_url 和 api_key代码可以通用。python -m venv venv source venv/bin/activate pip install openai这里有个小提醒如果你用的是云端模型接口注意网络连通性如果是在本机跑本地模型需要确保模型服务监听在可访问的端口上。两种方式用同一个 OpenAI 兼容客户端都能接只是 base_url 不同。4.2 核心代码一套可直接运行的 Agent 骨架下面这段代码是 Nanobot 思路的极简复刻版我把工具注册、消息循环、工具分发都压缩在一个文件里方便你理解整体运行逻辑。import json import os from openai import OpenAI client OpenAI( base_urlos.getenv(LLM_BASE_URL, https://api.openai.com/v1), api_keyos.getenv(LLM_API_KEY, YOUR_API_KEY), ) TOOLS {} TOOL_SCHEMAS [] def register(name, description, parameters): def decorator(fn): TOOLS[name] fn TOOL_SCHEMAS.append({ type: function, function: { name: name, description: description, parameters: parameters, }, }) return fn return decorator register(get_weather, 查询城市天气入参city为城市名, { type: object, properties: {city: {type: string}}, required: [city], }) def get_weather(city: str): result {city: city, temperature: 26, condition: 晴} return json.dumps(result, ensure_asciiFalse) def dispatch(name: str, args: dict): fn TOOLS.get(name) if not fn: return json.dumps({error: f未注册的工具: {name}}, ensure_asciiFalse) return fn(**args) def run_agent(messages): for _ in range(3): resp client.chat.completions.create( modelos.getenv(LLM_MODEL, qwen-plus), messagesmessages, toolsTOOL_SCHEMAS, ) msg resp.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg) for tc in msg.tool_calls: result dispatch(tc.function.name, json.loads(tc.function.arguments)) messages.append({ role: tool, tool_call_id: tc.id, content: result, }) return 迭代次数超限请简化任务或检查工具结果。 if __name__ __main__: messages [{role: system, content: 你是助手请用中文回答。}] while True: user_input input( ) if user_input.strip().lower() in (exit, quit): break messages.append({role: user, content: user_input}) answer run_agent(messages) print(Agent:, answer)这段代码的运行逻辑就是前面讲的那条直线注册工具、进入循环、模型决定是否调用工具、执行工具、继续循环。你可以直接把 api_key 和 base_url 换成自己的然后在终端里跑一句“北京今天天气怎么样”它会经历一次完整的工具调用流程。4.3 接一个真实工具时的关键细节以 get_weather 为例这个工具函数非常简单返回一个 JSON 字符串。但有一点必须强调工具函数的返回值必须是字符串不能是 dict也不能是对象。因为 OpenAI 兼容接口的 tool 消息要求 content 是字符串类型如果你返回 dict很多服务端会直接报错。另外schema 里的 description 写得越清楚模型越不容易乱传参数。像 get_weather 这种只有一个参数的函数还好如果是多参数的函数我的习惯是把每个参数的单位、可选值、默认值都写进 description这样能明显降低模型生成错误 JSON 的概率。4.4 参数选择与实测心得我自己连续测了几个 Agent 骨架项目后有几个参数经验值得记录。第一个是迭代次数上限。一般工具调用类的任务两步之内就能结束设置成 3 是一个比较稳的折中。设太大模型在工具结果不理想时会反复尝试成本飙升设太小遇到稍微复杂一点的连锁工具调用就被截断。第二个是上下文长度的预分配。发请求时要给工具结果预留 token 空间否则工具返回一大段内容时总长度会直接撑爆上下文窗口。我在骨架里没有显式处理但真实使用时会在发送前检查 messages 总长度预留出至少 2000 token 给工具结果。第三个是 temperature。普通闲聊用 0.7 没问题但一旦涉及工具调用我强烈建议把温度调到 0.3 以下。温度越高模型生成的参数 JSON 就越容易出现格式错误比如多一个引号、少一个逗号。工具调用的场景里稳定比创意重要得多。5. 源码阅读与部署中常见的坑5.1 源码拿下来先跑不起来的常见原因我在复现 Nanobot 和 OpenClaw 时遇到过几类导致跑不起来的问题按出现频率排个序Python 版本不对。项目要求 3.10但你用的是 3.8某些语法直接报错。依赖安装不完整。项目读的是 requirements.txt但你只装了核心依赖缺了几个隐式依赖。环境变量没配齐。api_key、base_url 之类的配置缺失启动时不报错调用模型时才挂。模型接口不兼容。项目默认模型要求支持某些参数你的模型服务不支持返回 400 错误。遇到这些问题不要慌排查思路是先看启动日志日志定位到第一处报错然后确认 Python 版本和依赖列表再看环境变量。最忌讳的是上来就改代码先确认环境再动手。环境问题导致的报错改代码是没用的。5.2 工具调用失败时的日志定位方法工具调用失败时的现象通常是模型返回了一句话“我暂时无法查询天气”而不是真正去调用工具。这时候要分几步排查。第一步检查工具 schema 是否成功传入模型。你可以在发请求前打印 TOOL_SCHEMAS确认里面有内容。如果列表为空模型当然不知道有这个工具可用。第二步检查模型返回的原始响应。很多项目只取 msg.content把 tool_calls 丢弃了导致你根本看不到模型其实尝试调用了工具。我排查时都会先打印原始响应结构确认模型给的是 content 还是 tool_calls。这一步能直接区分“模型没想调用工具”和“代码把工具调用结果吞了”。第三步检查工具函数的返回值。日志里如果报了“expected string, got dict”之类的错那就是返回值类型不匹配。改成 json.dumps 包装再返回就行。5.3 环境校验与跨平台部署的取舍OpenClaw 部署时经常出现环境校验失败的问题尤其是 WSL2 相关提示。我第一次看到“could not safely verify the WSL2 environment”时还以为是程序出 bug 了后来才意识到这是环境探测模块的正常反馈它没检测到预期的 WSL2 特征就给出警告而不是硬着头皮跑。这种设计值得借鉴。跨平台项目最怕的就是“用户环境千奇百怪程序假设只有一个环境”。好的做法是先把环境打上标签再根据标签决定路径。比如系统是 Linux 还是 Windows、架构是 x86 还是 ARM、有没有 GPU、网络通不通这些探测结果应该集中在一个地方管理。我自己在类似场景里踩过的坑是在 Termux 环境里部署时以为只要装了依赖就能跑结果项目依赖某个系统工具而 Termux 默认没有。所以现在我会先把“系统类型、架构、缺什么命令”一次性输出再决定下一步。OpenClaw 里的环境校验模块本质上就是干这件事的区别只在它把细节封装得更完整。5.4 会话残留与重复回复的排查思路在长连接类 IM 插件场景里有一个很经典的坑用户发了消息Agent 回复了但下次发消息时Agent 像失忆一样又重复处理了上一次的内容。这类问题通常不是模型问题而是会话状态没清理干净。排查时我一般分三步。第一步检查 messages 列表是不是在每次会话开始时被重置。如果重置逻辑只创建了新列表而没有清空旧引用就会出现残留。第二步检查 system prompt 是不是被重复注入。有些代码会在每轮循环里重新 append system 消息时间一长列表里堆了好几条 system模型行为会变得混乱。第三步确认长连接断开重连后会话 ID 是否仍然保持同一个如果是那历史消息可能会继续累积。这个场景下最直接的方案就是每次都生成独立的会话 ID超时自动清理。我在实际项目中还遇到过一类“类似会话残留”的情况模型服务端的上下文没清理导致代码层面已经重置了但服务端依然带着之前的记忆。这种问题比较隐蔽排查办法是换一个新会话 ID 再发一条消息看回复风格是否明显变化。如果变化很大说明问题出在会话 ID 复用上而不是消息列表本身。最后再分享一点个人体会。我读 OpenClaw 源码时原本觉得先看 Nanobot 是绕路真正读完之后才发现这是最省时间的路径。OpenClaw 里大量模块都能从 Nanobot 骨架上找到对应物工具注册变成 Skill 清单命令行输入变成多端 Connector上下文截断变成完整的记忆存储。如果你也在啃一个复杂项目不妨先在它的生态里找最小的那个参考实现读透了再回头看大的你会发现大型项目的复杂主要是“多”而不是“深”。另一个很实用的学习技巧是不要从头到尾读源码先给工具注册函数加个断点跑一个会触发工具的任务看整个调用过程的栈帧。这一趟走完你对架构的理解可能比读十篇文档都管用。
返回列表