为什么重要:能力之上,可控先行
有好些人渴望着具有可用性的智能程序,然而心里又担忧会演变成难以捉摸,无法知晓相关工具究竟于何时被启用,哪些文档遭到了更改,同时亦不清楚其执行过程为何会突然偏离正轨。
NanoBot运用了简洁明了的办法,首先将Agent打造成为最小可行运行时,消息抵达后,进行上下文组装,接着由LLM做出决策,随后执行工具,把结果回填,最后返回响应。
追求三个目标:
结论
NanoBot 的工作围绕工程展开,把 Agent 拆解成流水线,借助 MessageBus 进行解耦,利用 AgentLoop 驱动核心循环,经由 Cron/Heartbeat 达成主动性。
最为值得去学习的并非是“连接了Telegram”,也不是“连接了WhatsApp”,更不是“连接了Slack”,而是这个最小的骨架:
要是想构建能被控制的 Agent,这样的结构会使你以最低成本达成闭合循环,接着逐步增添多模型路由,以及更强有力的记忆检索,还有更严谨的权限边界,以及更可靠的审计回放。
对比:OpenClaw 给你成品,NanoBot 给你骨架
要是你探究过 OpenClaw,那便能够察觉到定位方面的差异,此处存在着一个明晰的对比。
OpenClaw 更像是那种具备生产准备就绪状态的产品,NanoBot 更像是那种专门用于辅助学习,以及进行复刻操作的最小化运行时段状态的东西。
TL;DR01 | 正确分类:NanoBot 究竟是什么
把 NanoBot 叫做"聊天机器人"低估了它。
更加精准的工程描述是,运行于本地机器之上的 Agent 运行之时。
它将"对话"升级为"可执行工作流入口":
一旦稳定了这条流水线,其他一切都是增量添加的。
02 | 架构概览:一张图展示数据流
重点不在于漂亮的图片,而在于每个框都对应仓库中真实的文件:
03 | 核心循环:LLM并非是那种从事实际事务操作的运行形式 仅只是对将要开展的事项进行判定式的决定。
AgentLoop 的逻辑是教科书级的:
从入队队列那儿来拉取这么一条消息,以此来获取到/创建会话,自历史当中得到最近的 N 条消息,运用 ContextBuilder 去组装这系统提示词,接着呢,把历史加上此当前消息给捆绑成消息列表,进而调用 LLM,也就是会把工具定义当作函数 schema 传递给模型,要是模型返回了工具调用指令,那就逐个去执行,把工具的结果当作工具角色消息来回填,而后继续下一轮,若是模型停止调用工具,那就去获取最终的内容,写入会话里,再发送到那出队队列。
其价值在于:"思考"和"执行"明确分离。
这比"让模型通过想象执行结果"强太多了。
关键消息格式:
messages = [system_prompt] + history + [user_message]
assistant -> tool_calls[] # 决定要做什么
tool_results -> messages # 将真实执行结果反馈回去
于这个循环里面,最为关键的并非着"更多工具",而是那"工具调用证据链":
这就是可控性的基础:所有关键动作都可以在上下文中回放。
两个影响稳定性的实现细节:
03.1 | MessageBus:为什么要加一层队列
众多 Agent 开发者将“接收消息这一行为”与“处理消息这一行为”进行了混淆,问题存在于:
NanoBot 的 MessageBus 有两个队列:
self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()
Channels对各种格式消息予以统一变成InboundMessage,而后抛入进队队列,AgentLoop从该队列进行拉取处理,接着把OutboundMessage抛进出队队列,ChannelManager订阅出队队列,依照通道进行分发回返。
这个设计的好处:
03.2 | 子代理生成:从主会话中分离复杂任务
NanoBot存在着一项具备实用性然而极易被忽视掉的能力,那便是spawn(此为生成子代理之意)。
没有"多智能体"那么复杂;方法非常直接:
SubagentManager 的关键限制:
# 子代理没有消息工具,没有生成工具
tools.register(ReadFileTool())
tools.register(WriteFileTool())
tools.register(ListDirTool())
tools.register(ExecTool(...))
tools.register(WebSearchTool(...))
tools.register(WebFetchTool())
这便是设计价值所在,子代理工具呈现出数量更少的状况,其权限也更为有限,目标变得更加集中,不会演化为第二个全能黑盒。
03.3 | 网关:与此同时启动代理循环,启动通道,启动定时任务,启动心跳。
当运转nanobot gateway之际,并非光只是“让一个机器人开始运作”了事,而是有着别样的情况。
它在同一个进程中拉起四个组件:
之所以我把它称作最小运行时,原因在于此,它把“持续运行”当作架构的其中一部分。
04 | ContextBuilder,它为何如同 OpenClaw 那样,去运用一堆 Markdown 文件呢?
你会在工作区看到一组熟悉的文件:
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)
这种基于文件的方法的好处是:
我喜欢用一句话来理解:文本胜过大脑。
04.1 | SkillsLoader:渐进式技能加载
NanoBot的技能系统,存在一个设计要点,它是值得被提及的,那就是并非所有技能,都会被一次性地,全部塞进系统提示词之内。
def build_skills_summary(self) -> str:
# 只返回技能名称/描述/位置
# Agent 需要时用 read_file 读取完整内容
这解决了一个真实问题:技能太多会炸掉系统提示词。
它的方案:
具有 always=true 特性的技能,其具备使直接进入系统提示词的功能,另外的其他技能,呈现的是仅仅给 Agent 展示包含名称、描述以及路径的摘要,当 Agent 产生需求的时候,会自行运用 read_file 去读取完整内容。
这即是 “渐进式加载”所呈现的情形,它使得 Agent 能够依据需求去获取,并非在一开始就将所有的内容都交付出去。
04.2 | MemoryStore:记事本级别的记忆
MemoryStore 做两件事:
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 记住你所告知它的事物。
然而,和“能够被检索的长期记忆”相比,存在着距离,并且,路线图把长期记忆当作有待完成的工作。
05批次,工具系统,为何“可控智能体”与“工具注册库”以及“参数验证”紧密关联,不可分割?
很多 Agent 开发者最后工具越来越乱,最终变成:
NanoBot 的工具系统有两点做对了:
工具是"注册式",不是"松散脚本"。
对ToolRegistry进行统一注册,将所有工具定义进行打包,使其成为供给模型的函数schema。
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用不着晓得“有啥工具”,仅仅得晓得“一切工具模样皆一致”:名称、描述、参数、执行。
参数使用 JSON Schema 验证,失败也可读
工具基类存在validate_params(),此函数依据schema去查看检查类型,去查证必填字段,去核对枚举,去查验各种最大最小值不等。
这对稳定性至关重要:
05.1,web_fetch返回的是JSON,并非“文章正文”。
为了避免你以为它是"随便写的爬虫",这里有两个细节:
web_search运用Brave Search API时,需借助tools.web.search.apiKey(或者环境变量BRAVE_API_KEY),web_fetch的返回值为JSON字符串,其涵盖finalUrl、status、extractor、truncated、text等字段。
这于Agent而言相当关键,原因在于模型所需的并非仅仅是“正文”,它还得清楚“此次抓取实际所获取到的内容是什么,是否出现了被截断的情况,是否被重定向至另外一个域名”。
这便是我所倾心的风格,工具输出要尽可能具备结构化,以使模型读取的是“可验证的证据”,而非随机文本。
05.2,LiteLLM提供商,实现了简单的多模型路由,是这样的情况。
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-v1")
self.is_vllm = bool(api_base) and not self.is_openrouter
支持的提供商:
配置很简单:
{
"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-Instant" }
}
}
06 | 主动性:Cron 是一种"唤醒手段",Heartbeat 亦是一种"唤醒方式",二者并不相同。
很多 Agent 在一点上体验很差:它们只等着你发消息。
NanoBot 使用两种机制:
Cron:显式定时任务
CronService对任务进行管理,当时间来到设定之时,会给Agent发送一条呈现“类似用户输入”情形的消息,此消息发送之后会开启一轮Agent流程,之后能够选择把结果投递给指定得到路径通道。
你可以把它理解为:产品化的"提醒/例行检查"。
# 添加任务
nanobot cron add --name "daily" --message "Good morning!" --cron "0 9 * * *"
# 列出任务
nanobot cron list
# 删除任务
nanobot cron remove
Heartbeat:轻量级周期性唤醒
HeartbeatService触发的频率是每30分钟一次,不过它并不强制任务,而是由Agent自行去读取HEARTBEAT.md:
我极其喜爱这个设计,它能使主动性转变为堪称“极为低廉的接口”这般的样式,无需再去重新创制繁杂的工作流 DSL。
06.1 | Channels:更多入口,更严边界
在架构层面去看,可以这么说,Channels 的意义并非是“连接几个聊天应用” ,而是要将“入口不确定性”阻挡在外面。
典型的不确定性:
纳米机器人的处理方法是,不围着入口处执着纠缠,而是悉数统一成为入站消息或者出站消息。
很关键的一点是,要留意配置层那儿的 allowFrom 字段。当入口呈现出更多数量的时候,就越发容易出现那种“本没计划给予响应的人最终居然获得了响应”这种意想不到的状况。
要是 allowFrom 呈现为空的状态,那么 BaseChannel 的逻辑情况则是进行默认的开放操作。
要是真的安排给团队去运用,提议将“谁允许触发”设定成硬性的门槛,而并非随意地进行配置。
06.2 | 三个通道的工程细节
Telegram
Slack
存在这样一个事实,这三块的细节所表明的,并非通道是那种“适配完便可了事”的情况,而是会迫使你去补全可靠性,还有权限边界以及可观测性。
07 | 最值得借鉴的是什么,需要注意什么
值得借鉴:4 点
消息总线进行解耦,Channels与AgentLoop之间,仅有Inbound/OutboundMessage,存在清晰的工具循环,工具调用的生命周期,即调用、执行、结果、继续,在一个文件里是可读的,基于文件的上下文,工作区Markdown承载着稳定的规则和偏好,具备轻量级主动性,Cron/Heartbeat都仅仅是给AgentLoop发送“下一个输入”。
需要注意/建议补充:4 点
具备更强的记忆检索功能:就目前而言,MemoryStore 更类似于“记事本”,然而距离“能够进行检索的长期记忆”依旧存在差距。有着更严格的权限边界:exec 的正则护栏虽属必要但并不充分;在生产环境中,最好引入允许列表、二次确认、审计回放会话。存在会话和可移植性方面的情况:会话默认被存储于~/.nanobot/sessions,这对于“把工作区迁移至另一台机器”而言不太友好。还有异步并发策略:当下是“逐个执行工具调用”,此做法足够简单,不过针对复杂编排(并行工具、通道队列、重试策略)而言,仍需要开展更多工作。
存在这么一个小坑,process_direct() 当下会忽略传进来的 session_key,这就致使 Cron/Heartbeat 这种“系统触发”的输入,有可能与你的 CLI 会话产生混淆。
这类问题并非难以修复,然而,它却对你进行提醒,这个提醒是,Agent 的稳定性常常并非由模型所决定,而是由“状态隔离”来决定。
结语:为什么建议读一遍 NanoBot 代码
许多 Agent 框架读完后,会使你变得愈发焦虑,存在更多的概念,有着更多的抽象,然而实际执行流水线反倒更为模糊。
NanoBot的益处在于,将“具备工作能力的Agent”,压缩至一个你能够阅读完的大小范围之内。
并非要将其当作最终解决办法,而是更适宜把它当作“最小可行骨架”,首先闭合循环,接着逐步增添护栏、增添记忆、增添工作流。
这就是构建可控 Agent 的正确节奏。
如果准备开始阅读,建议这个顺序,基本不会迷路:
nanobot/agent/loop.py:首先去领会主流水线,nanobot/agent/context.py;接着瞧瞧上下文是怎样组合的,nanobot/agent/tools/*;最后瞅瞅工具系统以及安全护栏,nanobot/cron/* + nanobot/heartbeat/*;弄明白“主动性”是怎样接入的,nanobot/channels/*:当需要连接新入口的时候再去看。
这便是为何,NanoBot属于一个值得予以学习的最小骨架,并非仅仅只是另外的一个框架呀。
关键技术指标(2026 年 2 月):
具备这样架构的情况表明,能够产生效用的人工智能智能体并不需要规模巨大的框架。把智能体的设计简化至基础要素的过程里,纳米机器人达成了速度更为快捷的实验,调试变得更加容易,耗费的资源更少,执行控制能力更强。这证实了架构无误时,减掉百分之九十九的代码依然能保有完善功能。
本文由mdnice多平台发布
相关标签: # AIAgent # NanoBot架构 # 可控性 # 消息总线 # 最小运行时