nanobot源码分析2--模块解析

Agent Loop
Agent Loop 是 nanobot 的核心。它的入口是一个死循环函数:
async def run(self) -> None: """Run the agent loop, processing messages from the bus.""" self._running = True await self._connect_mcp() logger.info("Agent loop started") while self._running: try: msg = await asyncio.wait_for( self.bus.consume_inbound(), timeout=1.0 ) try: response = await self._process_message(msg) if response: await self.bus.publish_outbound(response) except Exception as e: logger.error(f"Error processing message: {e}") await self.bus.publish_outbound(OutboundMessage( channel=msg.channel, chat_id=msg.chat_id, content=f"Sorry, I encountered an error: {str(e)}" )) except asyncio.TimeoutError: continue从消息总线 MessageBus 上读取输入消息 InboundMessage,实际的处理由 _process_message 函数执行,处理的结果 OutboundMessage 再交给 MessageBus,完成一次交互。
完整的一次消息处理流程如下:
在目前的实现中,系统消息是 subagent 生成的结果,它的处理流程如下:
调用大模型 API 处理信息是在 _run_agent_loop 函数中完成的,它的流程如下:
在调用大模型 API 的过程中,可能产生和模型的多次交互:如果在模型给出的响应中,出现了工具的调用,就需要本地执行工具后,将工具执行的结果加入到会话历史中,再和模型交互,直到模型产出没有工具调用的文本输出,才将最终的结果返回。
大模型的根本能力是文字接龙,要能真正干活,还得倚仗工具的调用。
Build Prompt
System Prompt
Agent Loop 读取到输入的消息,在调用大模型 API 前,会加入 System Prompt,生成一份完成的 Prompt。
nanobot 的 System Prompt 由几部分构成:
1. Identity: nanobot的身份信息、能力、可使用的工具、工作路径,这部分是代码中固化的; 2. AGENTS.md, SOUL.md, USER.md, TOOLS.md:这部分类似于 claude code 的 CLAUDE.md,由用户自定义; 3. Memory:从历史对话中固化下来的记忆,主要是一些关键信息,包括用户的地址、身份、偏好等信息; 4. Skills: nanobot 内置了一些常用的 skill,并把这些 skill 的全文都加入了提示词; 5. SkillsSummary:除了常驻提示词中的 skill 外,其他的 skill 的摘要信息,包括了用户自定义的 skill,。
每次调用大模型 API,System Prompt 都会被加入到提示词的头部。
Complete Prompt
除了 System Prompt,对话的历史也会加入到 Prompt 中,一次完整的提示词可能是这样的:
[ { "role": "system", "content": "# nanobot 🐈\n\nYou are nanobot, a helpful AI assistant. ..." }, { "role": "user", "content": "你好呀" }, { "role": "assistant", "content": "你好小明!今天有什么我可以帮你的吗?😊" }, { "role": "user", "content": "伦敦现在天气怎么样?" }, { "role": "assistant", "content": "我去查一下伦敦现在的天气。", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "web_search", "arguments": "{\"query\": \"London weather now\"}" } } ] }, { "role": "tool", "tool_call_id": "call_abc123", "name": "web_search", "content": "1. London weather - BBC Weather\n URL: https://www.bbc.co.uk/weather/2643743\n Snippet: Current conditions in London: 16°C, light rain, humidity 78%...\n2. wttr.in - London\n URL: https://wttr.in/London\n Snippet: London: ⛅️ +16°C ..." }]每次和 API 交互后产生的结果都会加入到提示词末尾,作为新一轮的输入交给模型 API,这也是为什么我们在使用大模型时,随着对话的历史越来越长,消耗的 token 会越来越多,模型的回答出错的概率会越来越大的原因。
Memory and History
nanobot 支持长期记忆,用户在对话中输入的关键信息都会被记录在 Memory.md 里面,每次和大模型的交互都会带上记忆,这也是日常使用的 AI 对话产品能记住我们的信息的原因。
nanobot 的记忆系统分成两部分:
• Memory: 对话历史中的关键信息,比如个人的位置、偏好、能力特点等信息 • History: 压缩的历史对话,被总结成结构化的文本存储在机器磁盘上
每一轮和大模型 API 的对话,nanobot 都会尝试从当前的会话历史中总结记忆,这部分也是通过大模型实现的,让调用大模型 API 总结生成新的记忆。
当前的记忆会加入到系统提示词中。而 History 则是留给大模型通过 grep 工具去搜索的,不会主动加入到对话中。
SubAgent
执行耗时较长,或者可以并行的任务时,nanobot 会被触发生成 subagent,将这类任务移交到后台运行,不占用主 agent 的资源。
在 nanobot 的实现中,没有显式指定什么时候会触发 subagent,而是注册了一个 Spawn 工具给大模型 API,模型自己决定是否要调用这个工具生成一个 subagent。
SubAgent 独立于主 agent,拥有自己独立的上下文,System Prompt 也更加精简,能调用的工具也受限,不能在产生 subagent。
subagent 产生的结果以 InboundMessage 的形式注入消息总线,并标记来源是 "System",关于 "System" 消息的处理在前文中已经提及。
Channels
nanobot 把对外通信的接口抽象成 Channel,可以基于这个接口自由的实现对接各种通信平台。
各种不同平台的处理模式大致是这样的:
1. 配置好机器人,启动本地的机器人连接服务器; 2. 从服务器拉取消息; 3. 将消息转换成 InboundMessage,写入 MessageBus; 4. 轮询 MessageBus,读取 OutboundMessage; 5. 转成成对应平台的消息格式,发出去; 6. 循环第2步;
MessageBus
MessageBus 由一个 InboundQueue 和一个 OutboundQueye 组成,它作用是将核心的 AgentLoop 和通信的平台解耦,在架构图中一目了然。
Provider
nanobot 把和大模型交互的接口抽象成 Provider,不同的大模型基于这个接口去对应的 API。
核心接口就一个 chat,输入提示词,返回大模型的响应。

夜雨聆风