乐于分享
好东西不私藏

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

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

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

nanobot源码分析1--架构设计

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,完成一次交互。

完整的一次消息处理流程如下:

_process_message
系统通道?
处理系统消息并返回
获取/创建会话
命令 /new?
清空会话后台归档记忆
返回'新会话已开启'
命令 /help?
返回帮助信息
消息数超阈值?
后台归档记忆
设置工具上下文构建上下文消息
运行 Agent 循环得到最终回复 + 用到的工具
保存用户消息保存助手消息
保存会话
返回回复消息

在目前的实现中,系统消息是 subagent 生成的结果,它的处理流程如下:

_process_system_message
解析来源从 chat_id 拆出 origin_channel / origin_chat_id
按 origin 拼接 session_key获取/创建会话
设置工具上下文
构建完成提示词
运行 Agent 循环得到最终回复
回复为空?
使用默认回复'Background task completed.'
保存系统消息保存助手消息
保存会话
返回回复消息路由回 origin

调用大模型 API 处理信息是在 _run_agent_loop 函数中完成的,它的流程如下:

遍历结束

_run_agent_loop
初始化 iteration / final_content / tools_used
iteration < max_iterations?
返回 final_content, tools_used
iteration += 1调用 LLM provider.chat
模型请求工具调用?
推送中间进度给 on_progress
把助手消息+工具调用加入 messages
遍历每个 tool_call
记录工具名执行 tools.execute
把工具结果回灌进 messages
final_content = 去除 think 后的回复
break 跳出循环

在调用大模型 API 的过程中,可能产生和模型的多次交互:如果在模型给出的响应中,出现了工具的调用,就需要本地执行工具后,将工具执行的结果加入到会话历史中,再和模型交互,直到模型产出没有工具调用的文本输出,才将最终的结果返回。

大模型的根本能力是文字接龙,要能真正干活,还得倚仗工具的调用。

Build Prompt

System Prompt

Agent Loop 读取到输入的消息,在调用大模型 API 前,会加入 System Prompt,生成一份完成的 Prompt。

nanobot 的 System Prompt 由几部分构成:

  1. 1. Identity: nanobot的身份信息、能力、可使用的工具、工作路径,这部分是代码中固化的;
  2. 2. AGENTS.md, SOUL.md, USER.md, TOOLS.md:这部分类似于 claude code 的 CLAUDE.md,由用户自定义;
  3. 3. Memory:从历史对话中固化下来的记忆,主要是一些关键信息,包括用户的地址、身份、偏好等信息;
  4. 4. Skills: nanobot 内置了一些常用的 skill,并把这些 skill 的全文都加入了提示词;
  5. 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. 1. 配置好机器人,启动本地的机器人连接服务器;
  2. 2. 从服务器拉取消息;
  3. 3. 将消息转换成 InboundMessage,写入 MessageBus;
  4. 4. 轮询 MessageBus,读取 OutboundMessage;
  5. 5. 转成成对应平台的消息格式,发出去;
  6. 6. 循环第2步;

MessageBus

MessageBus 由一个 InboundQueue 和一个 OutboundQueye 组成,它作用是将核心的 AgentLoop 和通信的平台解耦,在架构图中一目了然。

Provider

nanobot 把和大模型交互的接口抽象成 Provider,不同的大模型基于这个接口去对应的 API。

核心接口就一个 chat,输入提示词,返回大模型的响应。

nanobot源码分析1--架构设计

点击关注,共同进步