第三篇:Agent 的大脑 —— 核心循环与消息处理机制

深入 agent/loop.py,看 nanobot 如何实现"思考→行动→观察"的智能闭环

1. 引言

在上一篇文章中,我们俯瞰了 nanobot 的整体架构和启动流程,知道了消息最终会通过消息总线(MessageBus)派发到 AgentLoop。那么,AgentLoop 究竟是如何"思考"并决定下一步行动的? 这就是本篇要解答的核心问题。

如果把 nanobot 比作一个人,那么:

  • channels/ 是五官,负责接收外界信息
  • bus/ 是神经网络,传递信号
  • AgentLoop 就是大脑皮层------它整合信息、做出决策、指挥行动

今天我们就打开这个"大脑",一探究竟。


2. AgentLoop 的核心职责

agent/loop.py 中的 AgentLoop 类承担了以下关键职责:

  1. 接收消息:从消息总线订阅并处理传入的用户消息
  2. 构建上下文:将用户消息、对话历史、长期记忆等组装成发给 LLM 的提示
  3. 调用 LLM:向配置的模型提供商发送请求,获取推理结果
  4. 解析工具调用:如果 LLM 返回工具调用请求,则提取并执行
  5. 管理迭代:控制"推理→行动→观察"的循环次数,避免无限循环
  6. 返回响应:将最终答案或错误信息通过总线返回给用户

下面我们逐项深入分析。


3. 核心循环的源码实现

3.1 类定义与初始化

python 复制代码
# agent/loop.py (简化版)
class AgentLoop:
    def __init__(self, llm_provider, tool_registry, config, message_bus):
        self.llm = llm_provider
        self.tools = tool_registry
        self.config = config
        self.bus = message_bus
        self.context_builder = ContextBuilder(config)
        
        # 订阅消息总线,处理用户消息
        self.bus.subscribe("user_message", self.process_message)

初始化时,AgentLoop 订阅了消息总线上类型为 "user_message" 的事件,这样每当有用户消息到达,process_message 就会被调用。

3.2 核心方法:process_message

这是 AgentLoop 的心脏,完整实现了 ReAct 循环。下面我附上简化后的代码,并逐段解释:

python 复制代码
async def process_message(self, event: MessageEvent):
    """处理单条用户消息的主入口"""
    message = event.message
    session_id = message.session_id
    max_iterations = self.config.max_iterations or 20
    current_iter = 0

    # 1. 构建初始上下文
    context = await self.context_builder.build(message)

    while current_iter < max_iterations:
        # 2. 调用 LLM 进行推理
        response = await self.llm.complete(
            messages=context.messages,
            tools=self.tools.get_tool_schemas(),  # 将工具描述传给 LLM
            tool_choice="auto"
        )

        # 3. 检查是否有工具调用请求
        if response.tool_calls:
            # 3.1 执行工具调用
            tool_results = await self._execute_tool_calls(response.tool_calls)
            
            # 3.2 将工具结果作为观察添加到上下文
            context.add_observation(tool_results)
            
            # 3.3 迭代计数+1,继续循环
            current_iter += 1
            continue
        else:
            # 4. 没有工具调用,直接返回最终回答
            final_answer = response.content
            break
    else:
        # 5. 达到最大迭代次数,返回超时提示
        final_answer = "我思考太久了,请稍后再试。"

    # 6. 将响应发回消息总线
    await self.bus.publish("bot_response", ResponseEvent(
        session_id=session_id,
        content=final_answer
    ))

这段代码虽然简短,却完整地体现了 ReAct 模式的精髓:

  • 推理(Reason):调用 LLM 获取下一步行动指令
  • 行动(Act):如果 LLM 要求调用工具,则执行工具
  • 观察(Observe):将工具执行结果加入上下文,继续推理
  • 重复,直到 LLM 直接给出最终答案

3.3 工具调用的执行:_execute_tool_calls

当 LLM 返回工具调用请求时,AgentLoop 会调用 _execute_tool_calls 来实际执行工具:

python 复制代码
async def _execute_tool_calls(self, tool_calls):
    """并发执行多个工具调用"""
    tasks = []
    for tc in tool_calls:
        tool = self.tools.get(tc.name)
        if not tool:
            results.append({"error": f"Tool {tc.name} not found"})
            continue
        # 准备参数
        kwargs = tc.arguments
        # 执行工具(支持异步)
        tasks.append(tool.execute(**kwargs))
    
    # 并发执行所有工具
    raw_results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # 格式化结果
    formatted = []
    for i, result in enumerate(raw_results):
        if isinstance(result, Exception):
            formatted.append({"error": str(result)})
        else:
            formatted.append({"result": result})
    
    return formatted

这里有两个值得注意的设计:

  • 并发执行:如果 LLM 一次性请求多个工具,nanobot 会并发执行它们,提高效率
  • 异常处理:工具执行可能失败,nanobot 会将异常信息包装成正常结果返回给 LLM,让 LLM 决定如何处理(例如重试或告知用户)

4. 消息处理的全流程时序图

为了更直观地展示整个消息处理流程,下面是一张详细的时序图:
工具系统 LLM提供商 AgentLoop 消息总线 渠道(Telegram) 用户 工具系统 LLM提供商 AgentLoop 消息总线 渠道(Telegram) 用户 alt [返回工具调用] [返回文本] loop [最多 max_iterations 次] 发送消息 发布 user_message 事件 调用 process_message 构建上下文(ContextBuilder) 调用 complete (包含工具描述) 返回 tool_calls 并发执行工具 返回执行结果 将结果加入上下文 返回 content 跳出循环 发布 bot_response 事件 路由响应 发送消息

这张图清晰地展示了为什么 AgentLoop 被称为"大脑"------它处于整个信息流的中心,协调着各个模块的协作。


5. 迭代控制与安全机制

为了防止 Agent 陷入无限循环(例如反复调用同一个工具而不给出答案),nanobot 设置了多重安全机制:

5.1 最大迭代次数

config.json 中可以配置 max_iterations,默认为 20。当循环次数达到这个上限时,Agent 会强制终止并返回一条友好的错误信息。

5.2 工具调用频率限制

某些工具可能不适合频繁调用(例如发送邮件)。nanobot 在工具注册时可以设置调用限制:

python 复制代码
@tool(max_calls_per_minute=5)
def send_email(recipient, subject, body):
    """发送邮件"""
    # ...

ToolRegistry 会在执行前检查调用频率,超出限制时会返回错误,避免滥用。

5.3 超时控制

LLM 调用和工具执行都设置了超时时间,防止某个操作卡死整个循环。超时设置同样来自配置:

python 复制代码
response = await asyncio.wait_for(
    self.llm.complete(...),
    timeout=self.config.llm_timeout
)

6. 上下文构建器:如何组装 Prompt?

ContextBuilder 是 AgentLoop 的重要辅助组件,负责将各类信息组装成发给 LLM 的 messages 数组。它的核心逻辑如下:

python 复制代码
class ContextBuilder:
    def __init__(self, config):
        self.config = config
        self.memory_store = MemoryStore(config.memory_path)
    
    async def build(self, message: Message) -> Context:
        messages = []
        
        # 1. 系统提示:人格设定 + 工具描述
        system_prompt = self._build_system_prompt()
        messages.append({"role": "system", "content": system_prompt})
        
        # 2. 长期记忆(从 MEMORY.md 读取)
        memory = await self.memory_store.get_long_term()
        if memory:
            messages.append({"role": "system", "content": f"长期记忆:{memory}"})
        
        # 3. 对话历史(短期记忆)
        history = await self.session_manager.get_history(message.session_id)
        messages.extend(history)
        
        # 4. 当前用户消息
        messages.append({"role": "user", "content": message.content})
        
        return Context(messages=messages)
    
    def _build_system_prompt(self):
        """构建系统提示,包含工具描述"""
        prompt = self.config.system_prompt_template
        tool_descriptions = self.tool_registry.get_descriptions()
        return prompt.replace("{{tools}}", tool_descriptions)

这种分层构建的方式保证了 prompt 的清晰性和可维护性。特别值得一提的是,工具描述是通过 JSON Schema 自动生成的,这为 LLM 理解工具调用提供了标准化的格式。


7. 错误处理与健壮性

在生产环境中,各种意外都可能发生。nanobot 在核心循环中加入了多层错误处理:

7.1 LLM 调用失败

如果 LLM 提供商返回错误(如 API 超时、认证失败),AgentLoop 会捕获异常并返回一条错误消息给用户,同时记录日志以便排查。

7.2 工具执行异常

工具代码可能抛出任何异常,_execute_tool_calls 中通过 return_exceptions=True 捕获所有异常,并将异常信息作为正常结果返回给 LLM。这样 LLM 可以"看到"工具执行失败,并可能采取替代方案(例如告诉用户"我暂时无法执行这个操作")。

7.3 消息总线异常

如果发布响应时消息总线出现问题,AgentLoop 会记录错误,但不会影响后续消息的处理(因为每个消息都是独立的任务)。


8. 小结:AgentLoop 的设计智慧

回顾整个 AgentLoop 的实现,我们可以总结出几个关键的设计智慧:

设计要点 作用 源码体现
事件驱动 解耦消息来源与处理逻辑 订阅消息总线事件
ReAct 循环 实现复杂的多步推理与行动 while 循环 + LLM 调用
工具 Schema 自动生成 降低 LLM 调用工具的难度 get_tool_schemas() 方法
并发工具执行 提升效率,支持多工具并行 asyncio.gather
多层安全控制 防止死循环和资源滥用 最大迭代、超时、频率限制
上下文分层构建 确保 prompt 结构清晰 ContextBuilder

正是这些精心设计的细节,使得 nanobot 能够在 4000 行代码内实现一个功能完备的 AI Agent。


下篇预告

在下一篇文章中,我们将探讨 nanobot 的插件系统(工具系统)------这是它灵活性的核心。你将看到:

  • 工具如何注册和发现
  • @tool 装饰器的实现原理
  • MCP 协议的集成方式
  • 如何编写自己的工具

敬请期待:《万能接口 ------ 插件系统设计与实现》


本文基于 nanobot v0.1.3 版本撰写,实际代码可能随项目迭代有所变化,建议结合最新源码阅读。

相关推荐
王解1 天前
Agent Team设计模式与思维:从单体智能到群体智慧
设计模式·ai agent
带娃的IT创业者3 天前
解密OpenClaw系列09-OpenClaw核心功能特性
macos·objective-c·ai agent·ai智能体·openclaw
带娃的IT创业者4 天前
系列5/5-WinClaw工程实战:从“写博客“灾难到TaskTrace追踪系统的涅槃重生
软件工程·ai agent·智能体开发·openclaw·编程文档·组件设计·winclaw
叶庭云4 天前
AI 知识库与 Agent 能力构建工具全景调研报告
rag·企业级·ai agent·上下文工程·ai 知识库问答·agent 工作流编排·智能知识库管理
在未来等你4 天前
AI Agent Skill Day 4:Skill Router技能路由:智能分发与技能选择机制
ai agent· langchain· spring ai· mcp协议· skill router· 智能路由· 技能系统
在未来等你5 天前
AI Agent Skill Day 3:Tool Use技能:工具使用能力的封装与集成
ai agent· langchain· tool use· 大模型工具调用· 技能开发· function calling· mcp协议
小小工匠5 天前
大模型开发 - 用纯Java手写一个多功能AI Agent:01 从零实现类Manus智能体
ai agent·manus
带娃的IT创业者5 天前
解密OpenClaw系列04-OpenClaw设计模式应用
设计模式·软件工程·软件架构·ai agent·ai智能体开发·openclaw
TGITCIC6 天前
AI Agent中的 ReAct 和 Ralph Loop对比说明
人工智能·ai大模型·ai agent·ai智能体·agent开发·大模型ai·agent设计模式