> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# NanoBot 架构拆解
- URL: https://www.hubwiz.com/blog/nanobot-architecture-teardown/
- Published: 2026-02-24T10:10:04.000Z
- Updated: 2026-02-24T10:10:04.000Z
- Description: 这是一篇纯粹的架构拆解。没有"2 分钟部署"指南。没有"开箱即用"演示。我们要检查的是 NanoBot 如何使用最小骨架来运行 Agent——并使其可读、可修改和可控。
- Author: admin
- Tags: AGENT, NANOBOT, #Import 2026-10-14 14:41

当你将 430,000 行代码与 4,000 行代码放在一起比较时，一个自然的问题出现了：什么被删除了？什么被保留了？

这是一篇纯粹的架构拆解。没有"2 分钟部署"指南。没有"开箱即用"演示。我们要检查的是 NanoBot 如何使用最小骨架来运行 Agent——并使其可读、可修改和可控。

让我们从仓库开始：[https://github.com/HKUDS/nanobot](https://github.com/HKUDS/nanobot?ref=hubwiz.com)

## ​1、为什么这很重要：对能力的控制

许多人想要"可用的 Agents"，但害怕它们变成黑盒子——不知道何时调用了工具、哪些文件被修改了，或者为什么执行突然偏离了方向。

NanoBot 采用直接的方法：首先将 Agent 构建为最小可行运行时。消息到达 → 上下文组装 → LLM 决策 → 工具执行 → 结果回填 → 响应返回。

它追求三件事：

- 您可以端到端读取整个管道（小代码库）
- 您可以准确定位问题发生的位置（清晰的边界）
- 您可以安全地逐步添加能力（可交换的组件）

## 2、我的结论

NanoBot 的工作以工程为中心：将 Agent 分解为管道，使用 MessageBus 进行解耦，使用 AgentLoop 驱动核心循环，并通过 Cron/Heartbeat 实现主动性。

最值得学习的不是"它连接到 Telegram/WhatsApp/Slack"——而是这个最小骨架：

- 入口：Channels 将不同的 IM 消息统一为 InboundMessage
- 中心：MessageBus 解耦"接收消息"和"生成响应"
- 核心：AgentLoop 驱动 LLM ↔ Tools 循环
- 上下文：ContextBuilder 将规则/性格/工具描述/技能/记忆组装到系统提示中
- 可扩展性：ToolRegistry + JSON Schema 验证将工具变成可注册、可验证的插件
- 主动性：Cron 处理计划任务，Heartbeat 处理周期性唤醒

如果你想构建一个可控的 Agent，这个结构让你以最小的成本关闭循环，然后逐步添加多模型路由、更强的记忆检索、更严格的权限边界和更可靠的审计重放。

## 3、OpenClaw 给你产品，NanoBot 给你骨架

如果你研究过 OpenClaw，你会感觉到定位差异。这是一个清晰的细分：

一句话总结：OpenClaw 感觉更像"生产就绪产品"，NanoBot 感觉更像"学习和复制的最小运行时"。

#### TL;DR

- NanoBot 将 Agent 减少到最基本的循环：消息 → 上下文 → LLM → 工具 → 回填 → 响应
- 它使用 MessageBus 进行解耦：Channels 只处理发送/接收，AgentLoop 只处理"思考和执行"
- AgentLoop 是标准的"工具调用循环"：LLM 给出工具调用，执行每个调用，将结果作为工具消息回填，LLM 继续
- ContextBuilder 的关键是基于文件的：AGENTS/SOUL/USER/TOOLS/IDENTITY + memory 目录都是版本控制的真相源
- 工具系统不是分散的 if-else：ToolRegistry 统一注册，工具参数使用 JSON Schema 验证，错误收敛为可读字符串
- exec 工具具有粗粒度的安全护栏（危险命令正则表达式阻止 + 可选的工作空间限制），但仍然需要允许列表和审计用于生产
- Session 以 JSONL 格式保存对话历史（易于阅读，易于重放），但默认为 `~/.nanobot/sessions`，而不是项目工作空间
- Cron/Heartbeat 解决"Agent 在没有人说话时做事情"，本质上是向 AgentLoop 提供定时触发器
- README 显示"\~4000 行"，实际的 nanobot/ 目录总计约 5k 行；差异来自 bridges、channels、cron、tools 和 peripherals 的计数

#### 01 | 正确分类：NanoBot 实际上是什么

将 NanoBot 称为"聊天机器人"低估了它。

更准确的工程描述：一个在你的机器上运行的 Agent Runtime。

它将"对话"升级为"可执行的工作流入口"：

- 你从 Telegram/WhatsApp/Slack 发送消息
- LLM 首先决定"是否调用工具、哪些工具、什么参数"
- 工具在本地/受控环境中执行（读/写文件、运行命令、获取网页等）
- 结果回填到 LLM
- 最终答案返回到原始聊天窗口

一旦你稳定了这个管道，其他一切都是附加的。

#### 02 | 架构概述：一张图显示数据流

关键不是漂亮的图片——而是每个框都映射到实际的仓库文件：

- `nanobot/channels/*`：通道适配器和启动
- `nanobot/bus/*`：消息事件和队列
- `nanobot/agent/loop.py`：核心循环
- `nanobot/agent/context.py`：系统提示组装
- `nanobot/agent/tools/*`：工具系统
- `nanobot/cron/* + nanobot/heartbeat/*`：调度和心跳

#### 03 | 核心循环：LLM 不"做"——它决定"做什么"

AgentLoop 逻辑是教科书式的：

1. 从入站队列拉取一条消息
2. 检索/创建会话，从历史记录中获取最近的 N 条消息
3. 使用 ContextBuilder 组装系统提示，然后将历史记录 + 当前消息打包成消息
4. 调用 LLM（将工具定义作为函数模式传递给模型）
5. 如果模型返回工具调用：执行每个调用，将工具结果作为工具角色消息回填，继续下一轮
6. 如果模型停止调用工具：获取最终内容，写入会话，发送到出站队列

它的价值："思考"和"做"被明确分离。

- LLM 只输出结构化的工具调用
- 工具处理执行
- 结果回填
- LLM 基于"真实结果"继续推理

这比"模型在脑海中想象执行结果"好得多。

关键消息格式：

```
messages = [system_prompt] + history + [user_message]
assistant -> tool_calls[]  # 决定做什么
tool_results -> messages   # 将真实执行结果反馈回来

```

在这个循环中，最重要的不是"更多工具"——而是"工具调用证据链"：

- 你看到模型如何决定（消息中的工具调用）
- 你看到工具返回了什么（消息中的工具结果）
- 你看到最终响应看起来是这样的原因（它基于这个证据）

这是可控性的基础：所有关键动作都可以在上下文中重放。

两个实现细节影响你的稳定性：

- 上限：max\_iterations 默认为 20，防止模型进入工具调用死循环
- 历史存储：Session 使用 JSONL 格式写入 `~/.nanobot/sessions`，第一行是元数据，后续行是单独的消息；对调试和重放非常友好

#### 03.1 | MessageBus：为什么添加队列层

许多 Agent 构建者将"接收消息"与"处理消息"混合在一起。问题：

- 不同的通道有不同的消息格式（Telegram 有语音，WhatsApp 有媒体，Slack 有 @mention）
- 处理逻辑需要串行控制（否则对话状态会损坏）
- 当你想要可观察性（日志记录、审计）时，没有地方插入它

NanoBot 的 MessageBus 有两个队列：

```
self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()

```

Channels 将各种格式的消息统一为 InboundMessage，并将它们扔入入站队列。AgentLoop 从队列中拉取，处理，并将 OutboundMessage 扔入出站队列。ChannelManager 订阅出站并按通道分发回来。

这种设计的好处：

- Channels 不需要知道 Agent 如何工作
- AgentLoop 不需要知道消息来自哪里
- 想要添加审计、速率限制和优先级吗？在队列层添加

#### 03.2 | 子 Agent 生成：将复杂任务与主对话分离

NanoBot 有一个实用但容易被忽视的能力：spawn。

它不是"多 Agent"宏大叙事；方法很直接：

- 主 Agent 接收任务，发现它太长或太复杂
- 使用 spawn 为特定的隔离工作拉起一个子 agent（比如"先读取这批文件，做一个结构化摘要"）
- 子 agent 完成，通过 MessageBus 作为系统消息将结果返回给主 Agent
- 主 agent 更自然地为用户总结结果

SubagentManager 的关键限制：

```
# 子 agent 没有消息工具，没有 spawn 工具
tools.register(ReadFileTool())
tools.register(WriteFileTool())
tools.register(ListDirTool())
tools.register(ExecTool(...))
tools.register(WebSearchTool(...))
tools.register(WebFetchTool())

```

这是设计价值：子 agent 有更少的工具，更小的权限，更窄的目标——不会成为第二个全能的黑盒子。

#### 03.3 | Gateway：它同时启动 AgentLoop、Channels、Cron、Heartbeat

当你运行 `nanobot gateway` 时，它不仅仅是"启动一个机器人"。

它在一个进程中拉起四个部分：

- AgentLoop：消费入站，生成出站
- ChannelManager：按配置启动 Telegram/WhatsApp/Slack，处理出站分发
- CronService：从 `~/.nanobot/cron/jobs.json` 读取/写入计划任务，触发 agent 按计划运行
- HeartbeatService：默认每 30 分钟唤醒一次，让 Agent 读取 HEARTBEAT.md 以检查待处理任务

这就是我称之为最小运行时的原因：它将"持续运行"视为架构的一部分。

#### 04 | ContextBuilder：为什么它也像 OpenClaw 一样使用一堆 Markdown 文件

你会在工作空间中看到一个熟悉的文件组：

- `AGENTS.md`：工作规范（如何做事情）
- `SOUL.md`：性格/价值观/边界（如何说话，如何选择）
- `USER.md`：用户偏好（你是谁，你在乎什么）
- `TOOLS.md`：工具描述（存在什么能力，如何调用）
- `IDENTITY.md`：身份（名称、风格）
- `memory/`：日常笔记 + 长期信息

ContextBuilder 的工作：将这些"真相源"组装到系统提示中：

```
BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"]

def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
    parts = []
    parts.append(self._get_identity())
    bootstrap = self._load_bootstrap_files()
    if bootstrap:
        parts.append(bootstrap)
    memory = self.memory.get_memory_context()
    if memory:
        parts.append(f"# Memory\n\n{memory}")
    # ...
    return "\n\n---\n\n".join(parts)

```

这种基于文件的方法的好处：

- 规则是版本控制的：Git 跟踪"何时添加了这个边界"
- 上下文是可控制的：想要 Agent 更克制吗？编辑 SOUL。想要工具更稳定吗？编辑 TOOLS。想要它更好地理解你吗？编辑 USER
- 连续性不依赖对话：丢失的对话不是致命的，文件仍然保留

我更喜欢用一句话理解它：文本 > 大脑。

#### 04.1 | SkillsLoader：渐进式技能加载

NanoBot 的技能系统有一个值得提及的设计点：并非所有技能都立即塞入系统提示中。

```
def build_skills_summary(self) -> str:
    # 只返回技能名称/描述/位置
    # Agent 需要时使用 read_file 读取完整内容

```

这解决了一个真实问题：太多技能会爆炸系统提示。

它的方法：

1. 带有 `always=true` 的技能：直接进入系统提示
2. 其他技能：只向 Agent 显示摘要（名称 + 描述 + 路径）
3. 当 Agent 需要时：使用 read\_file 读取完整内容本身

这是"渐进式加载"：让 Agent 按需获取，而不是提前给出所有东西。

#### 04.2 | MemoryStore：记事本级别的记忆

MemoryStore 做两件事：

- 日常笔记：`memory/YYYY-MM-DD.md`，每天一个文件
- 长期记忆：`memory/MEMORY.md`，手动维护的持久信息

```
def get_memory_context(self) -> str:
    parts = []
    long_term = self.read_long_term()
    if long_term:
        parts.append("## Long-term Memory\n" + long_term)
    today = self.read_today()
    if today:
        parts.append("## Today's Notes\n" + today)
    return "\n\n".join(parts) if parts else ""

```

它满足了"让 Agent 记住你告诉它的东西"的基本需求。

但距离"可检索的长期记忆"还有差距。README 路线图也将长期记忆列为待处理工作。

#### 05 | 工具系统：为什么"可控 Agent"不能没有 ToolRegistry + 参数验证

许多 Agent 构建者最终得到越来越混乱的工具，最终变成：

- 一堆分散的函数
- 一堆"如果模型说某些关键词则执行"
- 一堆基于约定的参数格式

NanoBot 的工具系统做对了两件事：

1. 工具是"注册表"，而不是"松散脚本"

ToolRegistry 统一注册，将所有工具定义打包成模型的函数模式。

```
def _register_default_tools(self) -> None:
    self.tools.register(ReadFileTool())
    self.tools.register(WriteFileTool())
    self.tools.register(EditFileTool())
    self.tools.register(ListDirTool())
    self.tools.register(ExecTool(...))
    self.tools.register(WebSearchTool(...))
    self.tools.register(WebFetchTool())
    self.tools.register(MessageTool(...))
    self.tools.register(SpawnTool(...))

```

AgentLoop 不需要知道"存在哪些工具"，只知道"所有工具看起来都一样"：名称/描述/参数/执行。

1. 参数使用 JSON Schema 验证，并且失败也是可读的

工具基类有 `validate_params()`，根据模式检查类型、必填字段、枚举、最小/最大值等。

这对稳定性至关重要：

- 当模型输出错误的参数时，它不会用异常炸毁整个循环
- 将错误收敛为可读文本，反馈给模型进行更正
- 你可以在日志中看到"哪个字段错了"

#### 05.1 | web\_fetch 返回 JSON，而不是"文章正文"

为了避免你认为它是"随机抓取器"，我将指出两个细节：

1. web\_search 使用 Brave Search API，需要 `tools.web.search.apiKey`（或环境变量 `BRAVE_API_KEY`）
2. web\_fetch 返回值是一个 JSON 字符串，包含 finalUrl、status、extractor、truncated、text 等字段

这对 Agent 至关重要。因为模型需要的不仅仅是"正文"——它需要知道"这次获取实际上得到了什么，它被截断了吗，它重定向到另一个域了吗"。

这是我喜欢的方式：工具输出尽可能结构化，让模型读取"可验证的证据"而不是随机文本。

#### 05.2 | LiteLLM Provider：简单的多模型路由实现

NanoBot 使用 LiteLLM 进行多模型路由：

```
class LiteLLMProvider(LLMProvider):
    def __init__(self, api_key, api_base, default_model="anthropic/claude-opus-4-5"):
        # 根据 api_key 前缀确定提供商
        self.is_openrouter = api_key and api_key.startswith("sk-or-")
        self.is_vllm = bool(api_base) and not self.is_openrouter

```

支持的提供商：

- OpenRouter（推荐，访问所有模型）
- Anthropic / OpenAI / DeepSeek / Gemini / Groq（直接连接）
- vLLM（本地模型）

配置很简单：

```
{
  "providers": {
    "openrouter": { "apiKey": "sk-or-v1-xxx" }
  },
  "agents": {
    "defaults": { "model": "anthropic/claude-opus-4-5" }
  }
}

```

对于本地模型：

```
{
  "providers": {
    "vllm": {
      "apiKey": "dummy",
      "apiBase": "http://localhost:8000/v1"
    }
  },
  "agents": {
    "defaults": { "model": "meta-llama/Llama-3.1-8B-Instruct" }
  }
}

```

#### 06 | 主动性：Cron 和 Heartbeat 是两种不同的"唤醒方式"

许多 Agents 在某一点体验很差：它们只等你发送消息。

NanoBot 使用两种机制：

#### Cron：明确的计划任务

CronService 管理任务，当时机到来时向 Agent 发送"类似用户输入"的消息（agent 轮次），然后可选地将结果传递到指定通道。

你可以把它想象为：将"提醒/例行检查"产品化。

```
# 添加任务
nanobot cron add --name "daily" --message "Good morning!" --cron "0 9 * * *"

# 列出任务
nanobot cron list

# 删除任务
nanobot cron remove <job_id>

```

#### Heartbeat：轻量级周期性唤醒

HeartbeatService 默认每 30 分钟触发一次，但不强制任务——让 Agent 读取 HEARTBEAT.md：

- 如果文件没有任何内容，什么都不做
- 如果它有待处理的项目，按检查清单执行

我真的很喜欢这个设计：将主动性变成"非常便宜的接口"，不需要重新发明复杂的工作流 DSL。

#### 06.1 | Channels：更多入口，更严格的边界

从架构角度来看，Channels 的意义不是"连接几个聊天应用"——而是将"入口不确定性"拒之门外。

典型的不确定性：

- Telegram 可能有语音（加上转录）
- WhatsApp 需要桥接（Node 桥）
- Slack 使用长连接（WebSocket）
- 不同的通道有不一致的 sender\_id、chat\_id 语义

NanoBot 的处理：不要与入口纠缠，统一为 InboundMessage / OutboundMessage。

你需要注意的是配置层的 `allowFrom` 字段。更多入口会让"你没有计划响应的人也得到了响应"的意外变得更容易。

如果 `allowFrom` 为空，BaseChannel 逻辑默认为开放。

如果你真的为团队使用部署，建议将"谁被允许触发"视为硬阈值，而不是随意的配置。

#### 06.2 | 三个通道的工程细节

Telegram

- 长轮询，不需要公共 webhook
- 下载的图像/语音/文件进入 `~/.nanobot/media`
- 语音转录是可选的：配置 Groq API 密钥后，它使用 Whisper 转换为文本，然后喂给 Agent
- 发送消息时，将 Markdown 转换为 Telegram 安全的 HTML，失败时回退到纯文本

WhatsApp

- Python 端不直接实现协议，通过 WebSocket 连接到 Node.js 桥
- 桥使用 @whiskeysockets/baileys（WhatsApp Web 协议——现实世界的麻烦在这里）
- 语音消息目前不支持从桥端直接下载和转录

Slack

- 使用 WebSocket 长连接，更像企业系统集成
- 有 message\_id 去重缓存以避免重新处理（超过阈值时修剪）
- 接收消息后给出反应（如"已读/已处理"信号），然后转发到 AgentLoop

这三个部分的细节解释了一个现实：通道不是"适配完成"——它们强迫你赶上可靠性、权限边界和可观察性。

#### 07 | 什么最值得复制，什么需要谨慎

值得复制：4 件事

1. 消息总线解耦：Channels 和 AgentLoop 之间只有 Inbound/OutboundMessage
2. 清晰的工具循环：工具调用生命周期（调用 → 执行 → 结果 → 继续）在一个文件中可读
3. 基于文件的上下文：工作空间 Markdown 携带稳定的规则和偏好
4. 轻量级主动性：Cron/Heartbeat 都向 AgentLoop 发送"下一个输入"

需要谨慎/建议补充：4 件事

1. 更强的记忆检索：当前的 MemoryStore 更像"记事本"，仍然距离"可检索的长期记忆"很远
2. 更严格的权限边界：exec 的正则表达式护栏是必要的但不足；对于生产，最好引入允许列表、双重确认、审计重放
3. Session 和可移植性：Session 默认为 `~/.nanobot/sessions`，对于"将工作区移动到另一台机器"不是很友好
4. 异步并发策略：现在是"逐一执行工具调用"，足够简单，但对于复杂的编排（并行工具、车道队列、重试策略）需要更多工作

还有一个小陷阱：`process_direct()` 目前忽略传入的 session\_key，导致 Cron/Heartbeat 等"系统触发"的输入可能与你的 CLI 会话混合。

这类问题不难修复，但它提醒你：Agent 稳定性往往不关于模型——而是关于"状态隔离"。

## 4、为什么我建议阅读一次 NanoBot 代码

许多 Agent 框架让你阅读后更焦虑：更多概念、更多抽象、实际执行管道变得更模糊。

NanoBot 的好处：它将"可用的 Agent"压缩到一个你可以完成阅读的规模。

你不需要将其视为最终解决方案。最好将其视为"最小可行骨架"：先关闭循环，然后逐步添加护栏、添加记忆、添加工作流。

这是构建可控 Agents 的正确节奏。

如果你准备好开始阅读，建议这个顺序——基本上不会迷路：

1. `nanobot/agent/loop.py`：首先理解主管道
2. `nanobot/agent/context.py`：然后看如何组装上下文
3. `nanobot/agent/tools/*`：最后，看工具系统和安全护栏
4. `nanobot/cron/* + nanobot/heartbeat/*`：理解"主动性"如何连接
5. `nanobot/channels/*`：当你需要连接新入口时查看

比较图清楚地显示了执行流程——这就是使 NanoBot 成为一个值得学习的最小骨架而不是另一个框架的原因。

关键技术指标（2026 年 2 月）：

- 实际代码库：约 3,510 行（核心 agent 代码，通过 `core_agent_lines.sh` 验证）
- 启动时间：0.8 秒（vs OpenClaw 的 8-12 秒）
- 内存占用：45MB 基本操作（vs OpenClaw 的 200-400MB）
- GitHub 指标：15.9K 星，2.2K fork，32+ 贡献者
- 最新版本：v0.1.3.post6（2026 年 2 月 10 日）
- 支持的通道：Telegram、Discord、WhatsApp、Mochat、钉钉、Slack、Email、QQ
- 支持的 LLM 提供商：13+，包括 OpenRouter、Anthropic、OpenAI、DeepSeek、Gemini、Groq、vLLM

该架构表明，有效的 AI agent 不需要庞大的框架。通过将 agent 设计减少到基础知识，NanoBot 实现了更快的实验、更容易的调试、更低的资源消耗和更大的执行控制——证明了 99% 的代码减少可以在架构正确时保持完整功能。

---

原文链接: [NanoBot Architecture Teardown: 4,000 Lines Achieving OpenClaw Capability](https://jinlow.medium.com/nanobot-architecture-teardown-4-000-lines-achieving-openclaw-capability-3f242113ccbc?ref=hubwiz.com)

汇智网翻译整理，转载请标明出处