通过 Nanobot 源码学习架构 ---(4)SubAgent 与 TaoToken 统一 Key 通道的协作拆解
1. 从一次“卡死”的对话说起Nanobot SubAgent 到底解决了什么问题如果你正在读 Nanobot 的源码大概率已经翻过agent/loop.py和agent/subagent.py这两个文件。我第一次跑 Nanobot 的时候让它“分析一下当前目录的代码结构顺便把 README 里的接口整理成表格”结果主对话直接卡了将近两分钟期间输入框完全没反应。后来翻源码才明白主 Agent 的AgentLoop是同步阻塞处理消息的一旦某个任务里塞了十几个工具调用用户就只能干等。Nanobot 是香港大学数据科学实验室HKUDS开源的超轻量级个人 AI 助手框架定位是“Ultra-Lightweight OpenClaw”整个仓库代码量不大非常适合拿来学 Agent 架构。它的 SubAgent 机制本质上就是把“耗时任务”从主对话里剥离出去丢到一个独立上下文里跑跑完再通过消息总线把结果塞回来。主 Agent 的messages列表始终保持干净只保留用户对话和最终摘要子任务中间那一大堆工具调用记录全部在子 Agent 自己的上下文里结束后直接丢弃。这套设计解决的核心痛点有三个。第一是响应性主 Agent 不会被耗时任务阻塞用户可以继续对话。第二是上下文隔离子 Agent 有独立的 System Prompt、独立的工具集、独立的消息历史不会污染主对话的上下文窗口。第三是资源管控子 Agent 的迭代次数被限制在 15 次主 Agent 是 40 次工具集也被裁剪不能发消息、不能派生子代理、不能创建定时任务避免递归爆炸。我实测下来把“整理接口文档”这类任务交给 SubAgent 后主对话的响应时间从原来的 90 多秒降到 2 秒以内子任务在后台跑完后再通知主 Agent 汇总。这个体验差异非常明显。那么问题来了SubAgent 要调用 LLM就得有 API Key主 Agent 也要调用 LLM也得有 Key。如果每个 Agent 都配一套 Key管理起来会很乱。这就引出了本篇要重点拆解的第二个话题——TaoToken 统一 Key 通道。TaoToken 提供统一的 API 入口主 Agent 和 SubAgent 共享同一个 Base URL 和 Key只是在 Model ID 和参数上做区分。下面我会把 SubAgent 的注册、分发、执行链路以及它怎么和统一 Key 通道协作一步步拆开讲。2. TaoToken 统一 Key 通道多 Agent 场景下的鉴权前置在讲 SubAgent 怎么接入之前得先把 TaoToken 这条通道说清楚。Nanobot 的LLMProvider是一个抽象层主 Agent 和 SubAgent 都通过它来发请求。如果你用的是 OpenAI 兼容接口Provider 内部就是拼一个POST /v1/chat/completions。TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 的请求格式所以可以直接作为 Provider 的base_url。为什么多 Agent 场景下要强调“统一 Key 通道”因为 SubAgent 是动态生成的每次spawn()都会创建一个新的执行实例。如果每个实例都去读一份独立的配置文件、拿一个独立的 Key那配置管理会变成灾难。更合理的做法是主 Agent 初始化时创建一个LLMProvider实例SubAgentManager 持有这个实例的引用所有 SubAgent 共享它。这样 Key 只需要配一次Base URL 只需要配一次Model ID 可以在 spawn 时按任务类型覆盖。我试过在 Nanobot 的config里这样组织# config.toml [llm] base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 default_model claude-sonnet-4-20250514 [agents.defaults] model claude-sonnet-4-20250514 temperature 0.7 max_tokens 4096 [agents.subagent] model claude-haiku-3-5-20241022 temperature 0.3 max_tokens 2048 max_iterations 15这里主 Agent 用 Sonnet 保证对话质量SubAgent 用 Haiku 降低成本、提高速度。两者共享同一个base_url和api_key只是 Model ID 不同。Nanobot 的SubagentManager.__init__接收provider、model、temperature、max_tokens这几个参数正好对应上面的配置。如果你还没拿到 Key可以去 TaoToken 的 API Keys 页面创建一个https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentsubagent_keyutm_campaignrewrite。创建后复制sk-开头的字符串填到上面的api_key字段即可。注意不要把 Key 硬编码到源码里Nanobot 支持从环境变量读取你可以用TAOTOKEN_API_KEY这个变量名。统一通道还有一个好处计费和限流集中管理。主 Agent 和 SubAgent 的请求都走同一个 KeyTaoToken 后台能看到所有调用记录方便排查是哪个 Agent 在消耗额度。如果发现 SubAgent 调用异常频繁可以单独调低它的max_iterations或换更便宜的 Model ID。这里要提醒一句SubAgent 的 System Prompt 里明确写了“不能直接给用户发消息”所有结果都通过MessageBus回传给主 Agent。所以统一 Key 通道不只是鉴权层面的统一也是调用路径的统一——所有 LLM 请求都从同一个 Provider 出去只是消息的流向不同。3. 可复制配置SubAgent 注册与分发的完整片段这一节给出可以直接抄的配置和代码片段。Nanobot 的 SubAgent 注册分两步第一步是在SubagentManager初始化时传入共享的 Provider 和参数第二步是在spawn()时指定任务描述和会话键。先看SubagentManager的初始化。在 Nanobot 的gateway()函数里主 Agent 创建完之后会紧接着创建 SubagentManager# gateway.py 片段 from nanobot.agent.subagent import SubagentManager from nanobot.providers import LLMProvider # 创建共享的 Provider主 Agent 和 SubAgent 共用 provider LLMProvider( base_urlconfig.llm.base_url, # https://taotoken.net/api api_keyconfig.llm.api_key, # sk-xxx default_modelconfig.llm.default_model, ) # 创建 SubagentManager传入共享 Provider subagent_manager SubagentManager( providerprovider, # 共享同一个 Provider 实例 workspaceconfig.workspace_path, # 共享工作空间 busbus, # 共享消息总线 modelconfig.agents.subagent.model, # SubAgent 专用 Model ID temperatureconfig.agents.subagent.temperature, max_tokensconfig.agents.subagent.max_tokens, brave_api_keyconfig.tools.brave_api_key, exec_configconfig.tools.exec, restrict_to_workspaceconfig.tools.restrict_to_workspace, )这段代码的关键点是providerprovider这一行。SubagentManager 不自己创建 Provider而是接收主 Agent 已经创建好的实例。这样 Base URL 和 Key 只配一次所有 SubAgent 自动继承。接下来是spawn()的调用。主 Agent 在AgentLoop里通过SpawnTool触发子代理创建实际调用的是subagent_manager.spawn()# 主 Agent 内部触发 spawn 的片段 task_id await subagent_manager.spawn( task分析当前目录下所有 Python 文件的类结构输出 Markdown 表格, label代码结构分析, origin_channelcli, origin_chat_iddirect, session_keysession-abc123, ) # 返回: Subagent [代码结构分析] started (id: a1b2c3d4). Ill notify you when it completes.spawn()内部会做几件事生成 8 位 UUID 作为task_id记录origin来源渠道和聊天 ID用于后续把结果通知回正确的用户用asyncio.create_task()启动_run_subagent()把task_id注册到_running_tasks和_session_tasks两个字典里。_session_tasks这个映射很关键。它把session_key映射到一个task_id集合这样当用户输入/stop时cancel_by_session()能一次性取消该会话下所有正在运行的 SubAgent。我实测过同时 spawn 三个子代理然后/stop三个任务全部被取消返回取消数量 3。如果你用的是 Cline 或 Claude Code 这类工具配置逻辑类似核心就是三件套Base URL Key Model ID。以 Cline 的 MCP 配置为例{ mcpServers: { nanobot-subagent: { command: python, args: [-m, nanobot.gateway], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的密钥, TAOTOKEN_MODEL_ID: claude-haiku-3-5-20241022 } } } }Codex 的auth.json也是同样的三件套结构{ base_url: https://taotoken.net/api, api_key: sk-你的密钥, model: claude-sonnet-4-20250514 }注意 Model ID 要和 TaoToken 支持的模型列表对齐。你可以在模型对话页面先验证一下 Model ID 是否可用https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentsubagent_modelutm_campaignrewrite。选一个模型发一条测试消息确认返回正常再填到配置里。4. 验证请求从 SubAgent 到统一通道的完整链路配置写完之后得验证一次完整请求。我建议用一个最小任务来跑让 SubAgent 读一个文件并返回摘要。这样既能验证 SubAgent 的 spawn 链路又能验证 TaoToken 通道的鉴权是否正常。第一步启动 Nanobot 的 gatewaypython -m nanobot.gateway --config config.toml启动日志里应该能看到 Provider 初始化信息包括base_url和model。如果 Key 配错了这里不会报错要等到第一次请求才会暴露。第二步在主对话里输入触发 SubAgent 的指令帮我 spawn 一个子代理读取 workspace/README.md返回 3 句话的摘要主 Agent 会调用SpawnTool返回类似Subagent [README摘要] started (id: e5f6g7h8). Ill notify you when it completes.第三步观察后台日志。SubAgent 的_run_subagent()会依次执行构建工具集ReadFileTool、WriteFileTool、ExecTool 等 7 个工具构建 System Prompt发起provider.chat()请求。这条请求会打到https://taotoken.net/api/v1/chat/completions带上Authorization: Bearer sk-xxx头。如果一切正常日志里会看到INFO Subagent [e5f6g7h8] completed successfully然后主 Agent 收到_announce_result()通过MessageBus发来的通知内容格式是[Subagent README摘要 completed successfully] Task: 读取 workspace/README.md返回 3 句话的摘要 Result: README 介绍了 Nanobot 的安装方式、核心模块和配置项... Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like subagent or task IDs.主 Agent 会把这段内容自然化后输出给用户比如“README 主要讲了安装、模块和配置三块内容”。第四步验证统一通道的调用记录。去 TaoToken 控制台看 API 调用日志应该能看到两条记录一条是主 Agent 的对话请求一条是 SubAgent 的摘要请求。两条记录的 Key 相同但 Model ID 可能不同取决于你的配置。这就证明统一 Key 通道生效了。如果你想更直观地验证可以在_run_subagent()里加一行日志打印实际请求的base_url和modellogger.info(Subagent [{}] calling provider: base_url{}, model{}, task_id, self.provider.base_url, self.model)跑一次就能确认 SubAgent 确实走的是共享 Provider而不是自己新建了一个。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节列几个我踩过的坑都是真实报错对照着排查能省不少时间。报错一401 UnauthorizedError: 401 Client Error: Unauthorized for url: https://taotoken.net/api/v1/chat/completions原因通常是 Key 没配、配错或者环境变量没生效。排查步骤先确认config.toml里的api_key是sk-开头再确认没有多余空格或换行然后检查环境变量TAOTOKEN_API_KEY是否覆盖了配置文件。Nanobot 的 Provider 初始化时会打印实际使用的 Key 前缀前 8 位对照一下就知道用的是哪个。报错二local proxy failedError: local proxy failed: connection refused这个报错说明请求根本没发出去卡在本地网络层。常见原因是base_url写错了比如写成了https://taotoken.net/api/多了个斜杠或者写成了http://而不是https://。正确的 Base URL 是https://taotoken.net/api不带尾部斜杠。另外检查一下本机是否有其他进程占用了端口或者防火墙拦截了出站请求。报错三reading choices 相关错误KeyError: choices 或 TypeError: NoneType object is not subscriptable (reading choices)这个报错说明请求发出去了但返回的 JSON 结构不对。常见原因是 Model ID 写错了TaoToken 返回了一个错误对象而不是正常的choices数组。排查方法把 Model ID 拿到模型对话页面单独测一下确认模型存在且可用。另外检查max_tokens是否超过了模型上限有些模型上限是 4096你配了 8192 就会报错。报错四OAuth 相关错误Error: OAuth token expired or invalid如果你用的是 Claude Code 或 Codex 这类带 OAuth 的工具可能会遇到这个。原因是工具内部缓存了旧的 OAuth token和 TaoToken 的 Key 冲突了。解决办法是清掉本地缓存的凭证文件重新用 API Key 认证。Claude Code 的凭证一般在~/.claude/目录下Codex 的在~/.codex/auth.json。删掉后重新配置三件套Base URL、Key、Model ID。报错五SubAgent 结果不返回如果 SubAgent 跑完了但主对话没收到通知检查_announce_result()里的chat_id拼接。代码里是chat_idf{origin[channel]}:{origin[chat_id]}如果origin字典的 key 写错了消息就发不到正确的会话。另外确认MessageBus的publish_inbound()被正确调用主 Agent 的AgentLoop在监听system渠道的消息。6. 把 SubAgent 用起来长期编码与 Agent 场景的接入建议SubAgent 这套机制最适合的场景是“主对话保持轻量重任务后台跑”。如果你经常用 Nanobot 做代码分析、文档整理、批量文件处理建议把 SubAgent 的 Model ID 配成更便宜的型号主 Agent 保持高质量模型。这样成本能降下来响应速度也更快。对于长期编码和 Agent 场景TaoToken 的 Coding Plan 提供了更稳定的调用通道适合高频次、长时间的 Agent 运行https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentsubagent_codingutm_campaignrewrite。如果你的 SubAgent 需要频繁调用工具、跑多轮迭代Coding Plan 的额度模型会比按次计费更划算。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentsubagent_docutm_campaignrewrite里面有完整的 API 参数说明和错误码对照。遇到不确定的报错先查文档里的错误码表大部分问题都能定位。最后给一个实用技巧在_build_subagent_prompt()里把当前时间和工作空间路径动态注入 System Prompt。Nanobot 源码里已经这么做了但你可以再加一行“当前任务类型”让 SubAgent 知道自己是在做代码分析还是文档整理这样它的输出格式会更贴合预期。我试过加这一行后SubAgent 返回的摘要结构化程度明显提升主 Agent 汇总时也更省 token。