Nanobot 从 Channel 消息处理看python协程的使用

背景

Nanobot 从 AgentLoop 启动看怎么驱动大模型运行中提到协程和线程的区别,

通过Nanobot(channels.start_all)这里了解一下协程一些基本操作.

shell 复制代码
协程是用户态的轻量级"微线程",切换由用户程序控制,没有内核态切换,开销极小;线程是内核态的资源单元,切换由操作系统调度,开销较大.
线程切换需要涉及用户态到内核态的转换,上下文包括内核栈、硬件寄存器等,保存和恢复资源较多

channels.start_all

python 复制代码
    async def start_all(self) -> None:
        """Start all channels and the outbound dispatcher."""
        if not self.channels:
            logger.warning("No channels enabled")
            return

        # Start outbound dispatcher
        self._dispatch_task = asyncio.create_task(self._dispatch_outbound())

        # Start channels
        tasks = []
        for name, channel in self.channels.items():
            logger.info("Starting {} channel...", name)
            tasks.append(asyncio.create_task(self._start_channel(name, channel)))

        # Wait for all to complete (they should run forever)
        await asyncio.gather(*tasks, return_exceptions=True)

总体上的channel的启动代码如上。

在至少有一个已注册频道时,先起 _dispatch_outbound 消费总线出站消息,再为每个频道 create_task(start),

最后用 gather(..., return_exceptions=True) 一直等到这些长任务结束(正常即进程生命周期);无任何启用频道则直接返回。

总体上来说:

shell 复制代码
1. 使用 async def 定义协程
2. 使用 asyncio.run 运行协程
  • asyncio.create_task(self._dispatch_outbound())
    它将协程(coroutine)封装为任务(Task)对象,并立即调度该任务在事件循环中并在后台执行,从而允许在等待其完成的同时执行其他代码
    dispatch_outbound是一个 async 死循环,在 start_all() 里被 asyncio.create_task 单独跑,从 MessageBus 的出站队列取 OutboundMessage,
    按 msg.channel 找到对应 BaseChannel,再真正发到 Telegram/Discord 等

    python 复制代码
    async def _dispatch_outbound(self) -> None:
          """Dispatch outbound messages to the appropriate channel."""
          logger.info("Outbound dispatcher started")
    
          while True:
              try:
                  msg = await asyncio.wait_for(
                      self.bus.consume_outbound(),
                      timeout=1.0
                  )
    
                  if msg.metadata.get("_progress"):
                      if msg.metadata.get("_tool_hint") and not self.config.channels.send_tool_hints:
                          continue
                      if not msg.metadata.get("_tool_hint") and not self.config.channels.send_progress:
                          continue
    
                  channel = self.channels.get(msg.channel)
                  if channel:
                      await self._send_with_retry(channel, msg)
                  else:
                      logger.warning("Unknown channel: {}", msg.channel)
    
              except asyncio.TimeoutError:
                  continue
              except asyncio.CancelledError:
                  break
    • asyncio.wait_for(self.bus.consume_outbound(), timeout=1.0)
      这个是 asyncio 库中用于给异步操作(协程或任务)设置超时限制的函数。如果 aw 在 timeout 秒内未完成,则会触发 asyncio.TimeoutError 异常,并取消该任务
      用来消费出站的消息,并发动到对应的Channel中,如feishu
      这里的 _send_with_retry 最终会调用 channel.send,这里我们以feishu的处理逻辑为例:

      复制代码
         loop = asyncio.get_running_loop()
         ...
         if ext in self._IMAGE_EXTS:
            key = await loop.run_in_executor(None, self._upload_image_sync, file_path)
            if key:
                await loop.run_in_executor(
                    None, _do_send,
                    "image", json.dumps({"image_key": key}, ensure_ascii=False),
                )
      • asyncio.get_running_loop()
        在异步函数(async def)或回调函数内部,获取当前线程的事件循环对象,以便绑定定时器、socket 或执行其他循环级操作
        这是为了后续获取线程池的方便
      • loop.run_in_executor(None,...)
        通过协程将任务委托给线程池 (ThreadPoolExecutor) 执行,从而避免阻塞核心的异步事件循环,因为协程是在同一个线程中运行的
        这里的第一个参数为None,则使用事件循环默认的线程池,而且这里用了 await,所以进行同步等待
  • asyncio.create_task(self._start_channel(name, channel))
    这里启动对应的 Channel,以feishu为例:

    python 复制代码
      async def start(self) -> None:
          """Start the Feishu bot with WebSocket long connection."""
          self._loop = asyncio.get_running_loop()
          ...
          builder = lark.EventDispatcherHandler.builder(
              self.config.encrypt_key or "",
              self.config.verification_token or "",
          ).register_p2_im_message_receive_v1(
              self._on_message_sync
          )
          ...
          def run_ws():
              import time
              import lark_oapi.ws.client as _lark_ws_client
              ws_loop = asyncio.new_event_loop()
              asyncio.set_event_loop(ws_loop)
              # Patch the module-level loop used by lark's ws Client.start()
              _lark_ws_client.loop = ws_loop
              try:
                  while self._running:
                      try:
                          self._ws_client.start()
                      except Exception as e:
                          logger.warning("Feishu WebSocket error: {}", e)
                      if self._running:
                          time.sleep(5)
              finally:
                  ws_loop.close()
    
          self._ws_thread = threading.Thread(target=run_ws, daemon=True)
          self._ws_thread.start()
    
          logger.info("Feishu bot started with WebSocket long connection")
          logger.info("No public IP required - using WebSocket to receive events")
    
          # Keep running until stopped
          while self._running:
              await asyncio.sleep(1)
    • self._loop = asyncio.get_running_loop()
      这里也是获得当前线程的事件循环对象,和上面send的时候获取的是一个。
    • _loop.is_running 和 asyncio.run_coroutine_threadsafe(self._on_message(data), self._loop)
      这个主要出现在 _on_message_sync方法中,
      is_running() 用于检查当前线程中正在运行的事件循环是否处于运行状态(而非已停止或关闭)
      asyncio.run_coroutine_threadsafe(coro, loop) 用于在主线程或其他工作线程中安全地提交协程到异步事件循环(loop)中执行的函数
    • threading.Thread(target=run_ws, daemon=True)
      启动线程,这里的 run_ws方法是在另一个线程中跑,这里会单独启动协程,这是为了在独立守护线程里跑,而收消息同步回调再通过 self._loop 把异步处理 _on_message 投到主事件循环
      • ws_loop = asyncio.new_event_loop()
        用于显式创建并返回一个新的事件循环对象的函数。它不会自动将新循环设置为当前线程的默认循环,主要用于需要手动管理循环、在不同线程中创建独立循环,或避免默认循环策略限制的场景
      • asyncio.set_event_loop(ws_loop)
        用于将指定的事件循环实例(loop)设置为当前OS线程的默认事件循环
      • ws_loop.close()
        asyncio.new_event_loop().close 释放其底层资源(如 socket、文件描述符),防止资源泄露
  • asyncio.gather
    用于并发运行多个可等待对象(如协程或任务)并收集结果的实用函数。它按参数顺序返回所有结果列表,await 时暂停直到所有任务完成
    其中 return_exceptions = True 表示异常会被视为正常结果放入结果列表

相关推荐
一切皆是因缘际会19 小时前
从概率拟合到内生心智:2026 下一代 AI 架构演进与落地实践
人工智能·深度学习·算法·架构
科研前沿19 小时前
镜像视界 CameraGraph™+多智能体:构建自感知自决策的全域空间认知网络技术方案
大数据·运维·人工智能·数码相机·计算机视觉
爱学习的张大19 小时前
具身智能论文问答(2):Diffusion Policy
人工智能
AI科技星19 小时前
全域数学·72分册·射影原本 无穷维射影几何卷细化子目录【乖乖数学】
人工智能·线性代数·算法·机器学习·数学建模·数据挖掘·量子计算
Chef_Chen19 小时前
论文解读:MemOS首次把记忆变成大模型的一等公民资源,Scaling Law迎来第三条曲线
人工智能·agent·memory
风落无尘20 小时前
《智能重生:从垃圾堆到AI工程师》——第四章 变化的艺术
人工智能·线性代数·算法
发哥来了20 小时前
AI视频生成模型选型指南:五大核心维度对比评测
大数据·人工智能·机器学习·ai·aigc
wenha20 小时前
大模型基础(六):从聊天机器人到智能体-大模型的下一站
ai
发哥来了20 小时前
AI驱动生产线的实际落地:一个东莞厂商的技术选型实录
大数据·人工智能·机器学习·ai·aigc
AC赳赳老秦20 小时前
知识产权辅助:用 OpenClaw 批量生成专利交底书 / 软著申请材料,自动校验格式与内容合规性
java·人工智能·python·算法·elasticsearch·deepseek·openclaw