
1. 从校园智能体的调度难题说起主 Agent 调用子 Agent 架构设计到底解决什么如果你正在做多 Agent 协作大概率会遇到一个很具体的困境用户问“今天中午吃啥”你的系统里明明有食堂推荐、选课助手、校园通知三个独立服务但主入口的 LLM 根本不知道该把请求转给谁更不知道转过去之后怎么把结果拿回来。这就是主 Agent 调用子 Agent 架构设计要解决的核心问题——让一个统一入口的 Agent 具备“派发任务”的能力把垂直领域的活交给专门的子 Agent 去干。我试过几种做法早期用 if-else 硬编码意图路由关键词一多就崩后来用 LLM 输出 JSON 再手动解析结果模型偶尔多写一句话就解析失败。直到用上 OpenAI Agents SDK 的 function_tool 机制才找到一个相对干净的方案把每个子 Agent 注册成一个可调用的工具主 Agent 的 LLM 根据工具描述自动决定调不调、调哪个。子 Agent 的模型请求统一走 TaoToken 的 OpenAI 兼容端点主 Agent 和子 Agent 用同一套鉴权和追踪体系排错时一个 request_id 就能串起全链路。这套架构适合谁适合已经有一个主入口 Agent、又想把垂直能力拆成独立服务的团队适合用 Python 写后端、愿意接受 OpenAI Agents SDK 这套抽象的实现者也适合那些被“日志对不上、token 传丢了、子服务挂了主流程跟着崩”折磨过的同学。下面我会把主/子 Agent 的 SDK 配置片段、function_tool 的 schema 定义、子 Agent 独立 system prompt 与工具集拆分方式全部给出来并附一次“主 Agent 派发任务→子 Agent 返回结构化结果”的可复现验证步骤与日志检查点。核心检索词先明确主 Agent 负责意图理解与任务派发子 Agent 负责垂直领域执行function_tool 是两者之间的桥OpenAI Agents SDK 是这套桥的施工图纸TaoToken 统一通道则让所有模型请求走同一个 OpenAI 兼容端点省掉多套 Key 和多套 base_url 的维护成本。2. TaoToken 统一通道前置准备base_url、API Key 与模型 ID 三件套在写任何 Agent 代码之前先把模型通道这件事定下来。多 Agent 架构最怕的就是主 Agent 用一个模型服务、子 Agent 各用各的最后 Key 散落在五六个 .env 文件里谁过期了都不知道。TaoToken 的 OpenAI 兼容端点在这里的价值就是主 Agent 和所有子 Agent 共用同一个 base_url 和同一个 API Key模型 ID 按需切换配置面收敛到一处。你需要准备的三件套是Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-开头的一串Model ID比如gpt-4o-mini、claude-3-5-sonnet这类按你实际开通的模型填获取 Key 的入口在控制台的 API Keys 页面创建后只显示一次记得立刻存进.env。如果你还没决定用哪个模型可以先去模型对话页面手动试几条 prompt确认响应风格和延迟符合预期再写进配置。这里有个容易踩的坑OpenAI Agents SDK 默认会往 OpenAI 官方发 tracing 数据多 Agent 场景下这些 trace 里可能带上你的业务 prompt。建议在代码最开头调用set_tracing_disabled(True)关掉避免不必要的数据外发。另外TaoToken 的端点路径是/api拼接完整地址时不要重复加/v1SDK 内部会自己补/chat/completions。配置写进.envTAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api MAIN_AGENT_MODELgpt-4o-mini SUB_AGENT_MODELgpt-4o-mini然后在app/core/config.py里用 pydantic-settings 读进来from pydantic_settings import BaseSettings class Settings(BaseSettings): taotoken_api_key: str taotoken_base_url: str https://taotoken.net/api main_agent_model: str gpt-4o-mini sub_agent_model: str gpt-4o-mini sub_agents_config_path: str config/sub-agents.yaml class Config: env_file .env settings Settings()这样主 Agent 和子 Agent 都从同一个settings对象取 Key 和 base_url换模型只改一个环境变量。如果你打算长期跑编码类 Agent可以了解下 Coding Plan 的额度方案只是验证模型连通性的话模型对话页面更直接。3. 可复制配置function_tool schema 定义与主/子 Agent SDK 配置片段这一节是整篇的核心所有片段都可以直接抄。先看子 Agent 的清单配置用 YAML 管理新增一个子 Agent 只加一段不改 Python 代码。config/sub-agents.yamlagents: - name: food-agent description: - 食堂菜品推荐。当用户询问食堂、菜品、吃什么、美食推荐、口味偏好、 餐厅价格、素食、辣度、套餐搭配等相关话题时调用此工具。 base_url: http://food-agent-svc.default.svc.cluster.local:8000 domain: food timeout_sec: 30 - name: course-agent description: - 选课助手。当用户询问课程、学分、选课系统、教师评价、培养方案、 冲突检测、课程表查询等相关话题时调用此工具。 base_url: http://course-agent-svc.default.svc.cluster.local:8000 domain: course timeout_sec: 30description是 LLM 决策的唯一依据必须包含触发关键词别写“我可以帮你……”这种营销话术模型不需要。关键词要覆盖同义词“食堂”“吃什么”“美食推荐”是三个独立触发点。接下来是主 Agent 的 SDK 配置。先定义运行期上下文campus_token、request_id、conversation_id 这些绝对不能走 prompt必须走RunContextWrapperfrom dataclasses import dataclass dataclass class AgentContext: campus_token: str request_id: str - conversation_id: str 然后是 LLM 客户端单例指向 TaoToken 的 OpenAI 兼容端点from openai import AsyncOpenAI from agents import set_tracing_disabled set_tracing_disabled(True) _llm_client: AsyncOpenAI | None None def _get_llm_client() - AsyncOpenAI: global _llm_client if _llm_client is None: if not settings.taotoken_api_key: raise RuntimeError(TAOTOKEN_API_KEY 未配置) _llm_client AsyncOpenAI( api_keysettings.taotoken_api_key, base_urlsettings.taotoken_base_url, ) return _llm_clientfunction_tool 工厂是主 Agent 把子 Agent 注册成工具的地方。注意name_override里横杠转下划线LLM 看到的工具名是call_food_agentfrom agents import function_tool, RunContextWrapper from app.agents.tools_loader import invoke_sub_agent from app.core.context import request_id_ctx def _create_tool_fn(tool_config: dict): function_tool( name_overridefcall_{tool_config[name].replace(-, _)}, description_overridetool_config[description], ) async def tool_fn( ctx: RunContextWrapper[AgentContext], message: str, ) - str: agent_ctx ctx.context token request_id_ctx.set(agent_ctx.request_id) try: result await invoke_sub_agent( tool_configtool_config, messagemessage, campus_tokenagent_ctx.campus_token, request_idagent_ctx.request_id, conversation_idagent_ctx.conversation_id, ) return result finally: request_id_ctx.reset(token) return tool_fn这里request_id_ctx.set那行是必须的。OpenAI Agents SDK 的 tool executor 可能不继承外层 ContextVar不在 tool 入口重新 settool 内部 httpx、sqlalchemy 的日志就会丢 request_id排错时你会看到一堆request_id-。主 Agent 创建与单例from agents import Agent, OpenAIChatCompletionsModel, ModelSettings SYSTEM_PROMPT 你是校园数据智能体的主调度 Agent名叫校园小智。 你的职责 1. 理解用户意图选择合适的子 Agent 并调用对应工具 2. 工具返回内容原样整理后回复用户不要编造工具未返回的信息 3. 工具调用失败或返回错误时直接告诉用户该领域暂时查询失败请稍后重试。 可用工具见 tools 列表每个工具的 description 说明何时调用。 未命中任何工具时用通用中文礼貌回答。 def create_main_agent() - Agent: tool_configs build_sub_agent_tools() tools [_create_tool_fn(cfg) for cfg in tool_configs] agent Agent( namecampus-main-agent, instructionsSYSTEM_PROMPT, toolstools, modelOpenAIChatCompletionsModel( modelsettings.main_agent_model, openai_client_get_llm_client(), ), model_settingsModelSettings(temperature0.7), ) return agent子 Agent 侧是独立 FastAPI 服务暴露统一契约POST /api/v1/chat接收{message: ...}返回 SSE 流。子 Agent 有自己的 system prompt 和工具集比如食堂子 Agent 的 prompt 只关心菜品推荐工具集只挂菜品查询函数不挂选课相关的。这样拆分的好处是每个子 Agent 的上下文窗口干净prompt 不用塞一堆无关领域说明。子 Agent 的模型请求同样走 TaoTokensub_client AsyncOpenAI( api_keysettings.taotoken_api_key, base_urlsettings.taotoken_base_url, )4. 验证请求与成功结果一次主 Agent 派发任务的完整复现配置写完跑一次端到端验证。启动主 Agent 服务假设跑在 8000 端口然后发一条请求curl -N -X POST http://localhost:8000/api/v1/chat \ -H Authorization: Bearer 你的JWT \ -H Content-Type: application/json \ -H X-Request-Id: 9bfee368691a4dc3 \ -d {message: 今天中午吃啥}预期你会看到一串 SSE 事件按顺序大致是data: {type:session,conversation_id:...,request_id:9bfee368691a4dc3} data: {type:tool_call,tool:call_food_agent,args:} data: {type:tool_result,ok:true} data: {type:chunk,content:今天} data: {type:chunk,content:食堂} data: {type:chunk,content:有...} data: {type:done,full_text:今天食堂有...}关键检查点有三个。第一tool_call事件的tool字段应该是call_food_agent说明主 Agent 的 LLM 正确选中了食堂子 Agent。第二tool_result的ok为 true说明子 Agent 返回了正常结果。第三done事件的full_text是完整回复不是半截。再看日志。主 Agent 侧应该能看到这样一串全部带同一个 request_id2026-04-17 10:22:14.331 | INFO | request_id9bfee368691a4dc3 | app.core.middleware - → POST /api/v1/chat 2026-04-17 10:22:14.345 | INFO | request_id9bfee368691a4dc3 | app.api.v1.chat - chat request conversation_id... message今天中午吃啥 2026-04-17 10:22:14.890 | INFO | request_id9bfee368691a4dc3 | app.sub_agents_client.http_client - call sub-agent urlhttp://food-agent-svc...:8000/api/v1/chat request_id9bfee368691a4dc3 2026-04-17 10:22:15.120 | INFO | request_id9bfee368691a4dc3 | httpx._client - HTTP Request: POST http://food-agent-svc...:8000/api/v1/chat HTTP/1.1 200 OK 2026-04-17 10:22:18.045 | INFO | request_id9bfee368691a4dc3 | app.api.v1.chat - chat done conversation_id... cost_ms3714 reply_len128 2026-04-17 10:22:18.046 | INFO | request_id9bfee368691a4dc3 | app.core.middleware - ← POST /api/v1/chat status200 cost_ms3715子 Agent 侧也会看到同一个 request_id因为主 Agent 通过X-Request-Id头透传过去了。这就是全链路追踪的基准从网关到主 Agent 到 LLM 到 function_tool 到 HTTP 到子 Agent 到子 Agent 的 LLM一个 id 串起来。如果你想让子 Agent 返回结构化结果比如带图表数据可以在子 Agent 的done事件里加chart字段主 Agent 的 stream_handler 解析后透传给前端。本版主 Agent 不提取 chart先留 null后续扩展。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照多 Agent 架构跑起来之后报错集中在几个地方。下面按真实报错对照排查。401 Unauthorized。最常见的是 TaoToken API Key 没配或配错。检查.env里TAOTOKEN_API_KEY是否以sk-开头是否有多余空格。另一个可能是子 Agent 侧的 campus_token 过期子 Agent 应该返回 SSE error 事件{code: 40102, message: campus_token_expired}主 Agent 降级提示用户重新登录。注意区分两种 401主 Agent 自己的 JWT 鉴权失败是 HTTP 401子 Agent 的 campus_token 失败是 SSE 流里的 error 事件。local proxy failed。这个报错通常出现在 httpx 连接子 Agent 时说明 base_url 写错了或者子 Agent 服务没起来。检查sub-agents.yaml里的base_url是否可达K8s 集群内用 svc 域名本地开发用http://localhost:8001这类。如果子 Agent 在另一个容器确认网络策略允许主 Agent 访问。reading choices 相关报错。如果你在解析 LLM 响应时看到类似reading choices的报错多半是模型返回了非预期结构或者 base_url 拼错了导致请求打到了错误端点。确认TAOTOKEN_BASE_URL是https://taotoken.net/api不要手动加/v1。另外检查模型 ID 是否在你开通的列表里不存在的模型会返回错误结构。OAuth 相关报错。如果你用 Claude Code 或类似工具接入可能会遇到 OAuth 流程问题。这类工具通常需要配置 Base URL、API Key、Model ID 三件套。以 Claude Code 为例在 settings 里配置{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key, ANTHROPIC_MODEL: claude-3-5-sonnet } }如果你用 CC Switch 或 Cline MCP 这类工具同样要写全三件套Base URL 填https://taotoken.net/apiKey 填控制台创建的Model ID 填实际开通的。Codex 的auth.json也是类似结构确认字段名和路径对得上。request_id 丢失。日志里出现request_id-两个原因一是 middleware 用了BaseHTTPMiddleware子类anyio task group 破坏了 ContextVar 传递必须改成函数式app.middleware(http)二是 function_tool 入口没重新 set ContextVar按第 3 节的 try/finally 模式补上。子 Agent 超时。默认 30 秒如果子 Agent 内部还要调 LLM建议子 Agent 自己设 25 秒超时给 SSE 收尾留余量。主 Agent 侧捕获httpx.TimeoutException后返回字符串给 LLM让 LLM 决定怎么回复用户不要自动重试——重试 SSE 流意味着用户可能看到两段半吊子回复。6. 语义一致 CTA把统一通道接进你的多 Agent 工程这套架构落地之后你会发现最省心的地方是模型通道收敛到了一处。主 Agent 和所有子 Agent 共用 TaoToken 的 OpenAI 兼容端点换模型、加额度、查用量都在一个控制台里完成不用在五六个服务之间同步 Key。如果你正在排障或接入阶段先去 API Keys 页面确认 Key 有效再对照接入文档检查 base_url 和模型 ID 的写法。如果你只是想验证某个模型在子 Agent 场景下的表现模型对话页面可以快速试 prompt不用写代码。如果你打算长期跑编码类或 Agent 类任务Coding Plan 的额度方案比按量付费更适合高频调用。最后留一个实用技巧在sub-agents.yaml里给每个子 Agent 的description加上领域关键词的同义词LLM 的路由准确率会明显提升。我实测下来把“食堂”扩展成“食堂、餐厅、吃什么、美食推荐”之后误路由到通用回答的比例从三成降到了一成以内。新增子 Agent 时只改 YAML 不改代码重启主 Agent 即可生效这是这套架构最舒服的地方。