☰
从零实现自己的agent第五期:用TaoToken统一Key打通子代理dispatch_subagent
2026/9/29 9:08:45 网站建设 项目流程

1. 为什么主 Agent 越跑越慢:上下文污染的真实场景

做自研 Agent 到第五期,很多人会卡在同一个坎上:任务规划已经能跑通,主 Agent 知道先做什么后做什么,但执行几轮之后响应明显变慢,回答质量也开始飘。我试过抓三个网页做观点对比,主 history 里塞进了上万字的网页正文、grep 输出、报错堆栈,最后模型在总结时反而抓不住重点。

这不是模型能力问题,而是上下文结构问题。主 Agent 的 history 应该只保留「决策」和「结论」,而不是「过程」。网页正文、命令输出、文件搜索结果、报错日志,这些都属于局部探索的中间材料,对最终回答的价值密度很低,却会持续稀释注意力。

dispatch_subagent要解决的就是这件事:把脏活放进独立上下文执行,主 history 只接收一条高密度的 tool_result。子代理不是和主 Agent 平级的另一个角色,而是主 Agent 可以调用的一个特殊工具,和run_command、read_file同级,区别在于它内部会启动一套独立的 AgentRunner。

这一期要交付的是可复制的配置骨架:settings.json与config.toml示例、子代理注册流程、上下文隔离的验证动作,以及用 TaoToken 统一 Key 作为子代理调用外部模型的接入层。适合已经在写自研 Agent、准备引入多子代理架构的开发者。

2. TaoToken 前置:统一 Key 作为子代理的模型接入层

多子代理架构里有一个容易被忽略的工程问题:每个子代理都需要调用外部模型,如果每个子代理各自维护一套 Key 和 base_url,配置会迅速失控。更麻烦的是,子代理的上下文是隔离的,但模型接入层不应该隔离——它应该是共享的、统一的、可审计的。

TaoToken 在这里扮演的角色就是统一接入层。你只需要在环境变量里配置一次 Key 和 API 地址,主 Agent 和所有子代理都通过同一个通道调用模型。子代理的隔离发生在 history、system prompt、工具白名单层面,而不是发生在网络接入层面。

先拿到 Key。访问控制台创建 API Key:

https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite

创建完成后,Key 只在创建时完整显示一次,复制保存到本地环境变量。接入文档在这里,包含各语言 SDK 的调用示例:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

API 基础地址统一为:

https://taotoken.net/api

注意这个地址不加 UTM 参数,它是纯粹的接口端点。环境变量建议这样设置:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

这样主 Agent 和子代理在初始化模型客户端时,都读取同一组环境变量。子代理的 runner 工厂在创建时不需要额外传 Key,直接复用父级的模型客户端配置即可。这是统一 Key 的核心价值:接入层收敛到一处,隔离层专注在上下文和权限。

3. 可复制配置:settings.json 与 config.toml 骨架

配置分两层:一层是模型接入配置,一层是子代理注册配置。先看模型接入层的settings.json:

{ "model": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "default_model": "claude-sonnet-4-20250514", "max_tokens": 8192, "timeout_seconds": 120 }, "agent": { "max_turns": 30, "enable_subagent": true, "subagent_max_turns": 12, "concurrency_safe_tools": ["dispatch_subagent", "read_file", "grep", "glob"] } }

关键字段说明:api_key_env指向环境变量名而不是硬编码 Key,避免密钥进仓库;subagent_max_turns限制子代理的最大回合数,防止子代理陷入死循环消耗 token;concurrency_safe_tools列出可以并行执行的工具,dispatch_subagent必须在其中,否则并发派遣不会生效。

再看子代理注册层的config.toml:

[subagents.readonly_explorer] display_name = "只读探索者" description = "适合阅读代码、搜索项目结构、整理提纲,不可写文件" tools = ["load_skill", "read_file", "glob", "grep"] max_turns = 8 system_prompt_file = "templates/subagents/readonly_explorer.md" [subagents.web_researcher] display_name = "网页研究员" description = "适合网页抓取、资料查访、探索性搜索" tools = ["web_fetch", "load_skill", "read_file", "grep"] max_turns = 10 system_prompt_file = "templates/subagents/web_researcher.md" [subagents.builder] display_name = "构建者" description = "可读写可执行,适合真正动手改文件、跑命令" tools = ["run_command", "web_fetch", "load_skill", "read_file", "write_file", "edit_file", "glob", "grep"] max_turns = 15 system_prompt_file = "templates/subagents/builder.md"

三个子代理身份对应三种权限边界。readonly_explorer只有只读工具,web_researcher多了网页抓取,builder才有写文件和执行命令的权限。注意所有子代理的工具白名单里都不包含dispatch_subagent和update_todos——前者防止无限递归派遣,后者避免子代理污染主 Agent 的任务计划。

注册逻辑在agent/subagents/registry.py里读取这份 TOML,构建子代理注册表:

import tomllib from pathlib import Path class SubagentRegistry: def __init__(self, config_path: str = "config.toml"): with open(config_path, "rb") as f: config = tomllib.load(f) self._specs = {} for name, spec in config.get("subagents", {}).items(): self._specs[name] = SubagentSpec( name=name, display_name=spec["display_name"], description=spec["description"], tool_names=tuple(spec["tools"]), max_turns=spec.get("max_turns", 10), system_prompt_file=spec["system_prompt_file"], ) def names(self, include_aliases: bool = False) -> list[str]: return list(self._specs.keys()) def get(self, name: str) -> SubagentSpec: return self._specs[name]

dispatch_subagent工具的参数 schema 保持精简:

def tool_parameters_schema(self): return { "agent_type": { "type": "string", "description": "子代理类型,必须是可用类型之一", "enum": self._subagent_registry.names(include_aliases=True), }, "task": { "type": "string", "description": "交代给子代理的任务,写清目标、范围、输出格式、边界", }, "purpose": { "type": "string", "description": "一句话用途标签,仅用于终端打印", }, }

agent_type决定用哪种子代理身份,task是真正交给子代理的工单,purpose只是日志标签。这里最容易被忽视的是task——因为子代理有独立上下文,它看不到主 Agent 脑内的隐含信息,所以 task 必须写成独立工单。

4. 验证请求:跑通一次完整的子代理调度链路

配置就绪后,用一次真实请求验证整条链路。先启动主 Agent,输入一个适合子代理的任务:

分别阅读 docs/architecture.md、docs/tools.md、docs/subagent.md 三个文档, 提炼每个文档的核心观点,最后只回传一个对比表,不要把原文全文带回主线。

预期行为是:主 Agent 在同一轮里发出三个dispatch_subagent调用,每个调用指定agent_type=readonly_explorer,task 分别指向一个文档。运行时并发等待三个子代理完成,主 history 只收到三条总结。

验证成功的三个信号:

第一,主线打印了派遣日志,能看到agent_type和purpose:

[dispatch] agent_type=readonly_explorer purpose=阅读架构文档 [dispatch] agent_type=readonly_explorer purpose=阅读工具文档 [dispatch] agent_type=readonly_explorer purpose=阅读子代理文档

第二,子上下文里出现了自己的工具调用日志,比如read_file读取了对应文档,这些日志不会进入主 history:

[subagent:readonly_explorer] tool_call read_file path=docs/architecture.md [subagent:readonly_explorer] tool_call read_file path=docs/tools.md

第三,主线只收到最终回禀,而不是所有中间输出:

[subagent] 子代理仅向主 history 追加 312 字 [subagent] 子代理仅向主 history 追加 287 字 [subagent] 子代理仅向主 history 追加 356 字

如果看不到这些信号,说明模型可能选择了普通工具路径,直接用read_file读了三个文档。这不一定错,可能只是普通工具更直接。想稳定触发子代理,task 描述里要明确说「分别派三个子代理,各自处理一个文档,再汇总结果」。

再验证一次上下文隔离。在子代理执行前后分别打印主 history 的长度:

before = len(json.dumps(main_history)) result = dispatch_subagent(agent_type="readonly_explorer", task="阅读 docs/architecture.md 并总结") after = len(json.dumps(main_history)) print(f"主 history 增长: {after - before} 字符")

如果隔离生效,增长量应该只有子代理总结的长度,而不是文档全文的长度。这是判断子代理是否真正隔离上下文的最直接指标。

5. 本篇常见错排查

5.1 子代理没有触发,主 Agent 直接调用了普通工具

最常见的原因不是配置错误,而是 task 描述不够明确。模型会根据任务成本选择路径,如果「读三个文档」用一条grep或三次read_file就能完成,它不会主动派子代理。解决办法是在 task 里显式要求派遣,比如「分别派三个子代理,各自阅读一个文档,最后汇总对比表」。

另一个原因是enable_subagent没有设为true,或者dispatch_subagent没有注册进主 Agent 的工具表。检查settings.json里的agent.enable_subagent字段,以及主 ToolRegistry 是否注册了DispatchSubagentTool。

5.2 子代理报错「agent_type not found」

agent_type的枚举值来自SubagentRegistry.names(),如果config.toml里的子代理名称和调用时传的不一致,就会报这个错。注意 TOML 里的 section 名就是agent_type的值,比如[subagents.readonly_explorer]对应的agent_type是readonly_explorer,不是display_name。

排查方法是在启动时打印注册表:

registry = SubagentRegistry("config.toml") print("可用子代理:", registry.names())

5.3 子代理能读文件但无法写文件

这是权限白名单在起作用,不是 bug。readonly_explorer的工具白名单里没有write_file和edit_file,所以它无法写文件。如果任务确实需要写操作,把agent_type换成builder。这也是安全设计的核心:能用代码约束的地方就用代码约束,不要只靠 prompt 承诺。

5.4 并发派遣没有生效,三个子代理串行执行

检查dispatch_subagent是否在concurrency_safe_tools列表里。AgentRunner 只会对同一轮模型返回的、且标记为 concurrency_safe 的 tool blocks 做并行执行。如果dispatch_subagent不在列表里,即使模型在同一轮发出三个调用,也会串行执行。

另外,并发触发权在模型路由手里。如果模型把三个派遣分散在三轮里发出,运行时也无法并行。想稳定演示并发,task 里要明确说「在同一轮里分别派三个子代理」。

5.5 子代理返回内容过长,主 history 仍然被撑爆

这说明子代理的 system prompt 没有约束回禀格式。子代理不应该把所有中间过程原样倒回主线,那样只是把污染换了一个入口。在子代理的 system prompt 文件里明确要求回禀包含四类信息:结论、证据、不确定性、建议动作。并在dispatch_subagent的 task 里指定输出格式,比如「回传三条关键结论,每条不超过 50 字」。

5.6 子代理调用模型时报 401 或鉴权失败

检查环境变量TAOTOKEN_API_KEY是否在当前 shell 会话里生效。子代理的 runner 工厂在创建时读取的是同一组环境变量,如果主 Agent 能调用模型但子代理不能,通常是子代理 runner 初始化时没有继承父级的模型客户端配置。确保runner_factory在创建子代理 runner 时复用父级的 model client,而不是重新读取一份可能不存在的配置。

6. 继续把子代理链路跑稳

子代理的核心价值是把局部执行细节放进独立上下文,主 Agent 负责目标、计划和最终回答,子代理负责局部探索、试错和整理。TaoToken 统一 Key 在这里的作用是让接入层收敛到一处,主 Agent 和所有子代理共享同一个 API 通道,隔离发生在 history、system prompt 和工具白名单层面。

如果你在排障过程中需要确认 Key 和接入配置是否正确,可以直接在模型对话里发一条测试请求验证通道:

https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite

如果准备把子代理调度接入到长期编码或 Agent 工作流里,Coding Plan 提供了更适合持续调用的配额方案:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

接入文档里有各语言 SDK 的完整调用示例,排障时对照检查 base_url 和鉴权头:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

下一步可以做的验证:把子代理的max_turns调小到 3,观察子代理在回合耗尽时如何回禀;或者给builder子代理加一个写文件任务,验证权限白名单是否按预期放行。这两个动作能把子代理的边界行为摸清楚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询