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 表示异常会被视为正常结果放入结果列表

相关推荐
zhang133830890752 小时前
守护水工安全:CG-85D振弦式渗压计在大坝与堤防监测中的核心作用
运维·服务器·网络·人工智能·自动化
岁月标记2 小时前
MoE 混合专家模型
人工智能
才兄说2 小时前
机器人二次开发动作定制?数周内交付
人工智能·机器人
石榴树下的七彩鱼2 小时前
Python OCR 文字识别 API 接入完整教程
开发语言·人工智能·后端·python·ocr·api·图片识别
芯盾时代2 小时前
RSAC 2026观察 智能体治理崛起
网络·人工智能·网络安全·智能体
十铭忘2 小时前
Scaling Latent Reasoning via Looped Language Models:通过循环语言模型扩展潜在推理
人工智能
斯文by累2 小时前
CHATERM AI:开启云资源氛围管理新篇章!
人工智能
OpenAnolis小助手2 小时前
直播预告: 异构场景下的大模型优化技术 | 龙蜥大讲堂
人工智能·龙蜥大讲堂
輕華2 小时前
Word2Vec与CBOW算法实战:从词向量到上下文感知
人工智能·算法·word2vec