1. 从 Codex auth.json 说起:多智能体协作的真实痛点
如果你正在用 Microsoft Agent Framework 做 Multi-Agent 编排,大概率会遇到一个很具体的问题:SubAgent 各自跑得好好的,一旦串成工作流,调用链就断在某个节点上,日志里只留下一句401 Unauthorized或者local proxy failed。我最近在把一个 Codex 风格的 CLI 工具接进 MAF 的 SubAgent 体系时,就反复卡在auth.json这个环节。
Microsoft Agent Framework(下称 MAF)本身是一套用于编排多个 Agent 或处理组件执行流程的 Workflow 框架。它的核心思路是把每个处理单元抽象成 Executor,用 Edge 把它们连起来,再用 SuperStep 做批量调度。这套模型非常适合做 SubAgent 分工:一个负责意图识别,一个负责代码生成,一个负责结果校验,最后由一个汇总节点输出。但问题在于,每个 SubAgent 背后往往要调用外部模型服务,而模型服务的鉴权配置如果散落在各个 Agent 里,维护起来就是灾难。
Codex 的auth.json正好是这类配置的典型载体。它通常放在用户目录下的.codex/文件夹里,记录 Base URL、API Key、默认模型 ID 这些信息。很多开发者第一次接触 MAF 的 SubAgent 时,会直接把 Key 硬编码进 Executor,结果就是:本地能跑,换台机器就 401;或者某个 SubAgent 用了旧 Key,整个工作流在第三个 SuperStep 才报错,排查成本极高。
这篇内容要解决的,就是把这个配置收敛到auth.json一处,让 MAF 的 SubAgent 和 Multi-Agent 协作链路都能稳定拿到鉴权信息。适合谁看?如果你已经写过一两个 Executor,想让多个 Agent 按工作流协作,并且希望调用链可验证、可排查,那接下来的步骤可以直接跟做。核心检索词就三个:Microsoft Agent Framework、SubAgent、Multi-Agent,全文围绕它们展开。
我会先讲清楚 MAF 里 SubAgent 的注册与路由机制,再给出可复制的auth.json配置片段,然后跑一个最小任务集验证协作是否按预期返回。过程中遇到的 401、local proxy failed、reading choices 这些报错,也会在排障章节逐一对照。
2. TaoToken 前置:把模型接入收敛到一处配置
在 MAF 里做 Multi-Agent,最怕的就是每个 SubAgent 各自维护一套模型接入参数。我的做法是:所有 SubAgent 共享同一份auth.json,里面只保留一个 Base URL、一个 Key、一个默认 Model ID。这样无论工作流里有多少个 Executor,鉴权入口只有一个,排查时也只需要看一个文件。
这里用到的模型服务接入地址是 TaoToken。它的 API 入口是https://taotoken.net/api,控制台和密钥管理在官网https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。你需要先在控制台创建一个 API Key,然后把它写进auth.json。注意,Key 只创建一次,后续所有 SubAgent 都复用这一个,不要每个 Agent 建一个,否则 Multi-Agent 的调用链会变得难以追踪。
为什么强调“前置”?因为 MAF 的 Workflow 是声明式构建的,Executor 在Build()之前就已经绑定了路由和依赖。如果你等到运行时报错才去补 Key,工作流已经进入 SuperStep 调度,改配置需要重新构建整个 Workflow 实例。所以正确顺序是:先准备好auth.json,再写 Executor,最后Build()。
具体来说,auth.json放在~/.codex/auth.json(Windows 是%USERPROFILE%\.codex\auth.json)。这个路径是 Codex 风格工具的默认读取位置,MAF 的 SubAgent 在初始化时也会去这里找。文件内容是一个 JSON 对象,包含base_url、api_key、model三个核心字段。下面这段可以直接复制,把sk-开头的部分换成你在控制台生成的 Key:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的实际密钥", "model": "claude-sonnet-4-20250514", "provider": "taotoken" }这里model字段填你实际要用的模型 ID。如果你不确定该填哪个,可以先去模型对话页面试一下,确认模型能正常返回再写进配置。provider字段是给 MAF 的 SubAgent 做路由判断用的,保持taotoken即可。
有一点要提醒:auth.json里不要写多余字段,比如proxy、endpoint之类的,MAF 的配置解析器对未知字段比较敏感,多余字段可能导致local proxy failed这类看起来和网络有关的报错,实际是配置解析失败。我踩过的坑就是加了一个自定义字段,结果排查了半天才发现是 JSON 结构问题。
配置准备好之后,先别急着写完整工作流。用一个最小的 Executor 验证auth.json能被正确读取,再往下做 Multi-Agent 编排。这样能把“配置问题”和“编排问题”分开,排障效率高很多。
3. 可复制配置:SubAgent 注册与 auth.json 读取片段
这一节给出可以直接粘贴运行的配置和代码。目标是在 MAF 里注册两个 SubAgent,一个负责生成,一个负责校验,它们共享同一份auth.json。先看目录结构,建议这样组织:
project/ config/ auth.json agents/ generator_agent.cs validator_agent.cs workflow/ program.csauth.json就放在config/下,内容沿用上一节的三个字段。接下来是读取配置的辅助类,它负责把 JSON 反序列化成强类型对象,供各个 SubAgent 使用:
using System.Text.Json; public sealed class AuthConfig { public string BaseUrl { get; init; } = ""; public string ApiKey { get; init; } = ""; public string Model { get; init; } = ""; public string Provider { get; init; } = ""; public static AuthConfig Load(string path) { var json = File.ReadAllText(path); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; return JsonSerializer.Deserialize<AuthConfig>(json, options) ?? throw new InvalidOperationException("auth.json 解析失败"); } }注意PropertyNameCaseInsensitive = true,这样 JSON 里的base_url能映射到BaseUrl。如果你不加这一行,反序列化会得到空字符串,然后请求时就会 401,但报错信息不会直接告诉你字段没映射上。
然后是 SubAgent 的注册。MAF 里每个 Executor 需要实现ConfigureRoutes,声明自己能处理哪些消息类型。下面这个GeneratorExecutor负责调用模型生成内容:
using Microsoft.AgentFramework; public sealed class GeneratorExecutor : Executor { private readonly AuthConfig _auth; public GeneratorExecutor(AuthConfig auth) { _auth = auth; } protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) { return routeBuilder .AddHandler<GenerationRequest, GenerationResult>(HandleAsync); } private async ValueTask<GenerationResult> HandleAsync( GenerationRequest request, IWorkflowContext context, CancellationToken ct) { using var client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_auth.ApiKey}"); var payload = new { model = _auth.Model, messages = new[] { new { role = "user", content = request.Prompt } } }; var response = await client.PostAsJsonAsync( $"{_auth.BaseUrl}/v1/chat/completions", payload, ct); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadFromJsonAsync<JsonElement>(ct); var text = body.GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString() ?? ""; await context.SendMessageAsync( new GenerationResult { Text = text }, "validator", ct); return new GenerationResult { Text = text }; } }这里有几个关键点。第一,Authorization头用的是Bearer加_auth.ApiKey,这是标准写法。第二,请求路径是{BaseUrl}/v1/chat/completions,BaseUrl来自auth.json,不要在这里再拼一次域名。第三,生成完成后通过context.SendMessageAsync把结果发给validator节点,这就是 Multi-Agent 协作的消息传递。
ValidatorExecutor结构类似,它接收GenerationResult,做一次校验后再输出:
public sealed class ValidatorExecutor : Executor { protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) { return routeBuilder .AddHandler<GenerationResult, ValidationResult>(HandleAsync); } private async ValueTask<ValidationResult> HandleAsync( GenerationResult input, IWorkflowContext context, CancellationToken ct) { var passed = !string.IsNullOrWhiteSpace(input.Text) && input.Text.Length > 10; var result = new ValidationResult { Passed = passed, Reason = passed ? "内容长度与格式校验通过" : "内容为空或过短" }; await context.YieldOutputAsync(result, ct); return result; } }最后是工作流的组装。用WorkflowBuilder把两个 Executor 连起来,指定起点和输出节点:
var auth = AuthConfig.Load("config/auth.json"); var generator = new GeneratorExecutor(auth); var validator = new ValidatorExecutor(); var workflow = new WorkflowBuilder(generator) .WithName("SubAgent 生成校验工作流") .AddEdge(generator, validator) .WithOutputFrom(validator) .Build();这段配置里,AddEdge(generator, validator)建立了一条 Direct Edge,消息从生成节点流向校验节点。WithOutputFrom(validator)指定校验节点的输出作为整个工作流的最终结果。到这里,SubAgent 的注册和路由就完成了,两个 Agent 共享同一份auth.json,调用链是generator -> validator -> output。
4. 验证请求:用最小任务集检查 Multi-Agent 是否按预期返回
配置写完之后,不要直接上复杂任务。先用一个最小任务集验证协作链路是否按预期工作。所谓最小任务集,就是输入一个简单 Prompt,观察工作流是否依次经过 generator 和 validator,并且最终输出符合预期。
启动工作流的代码大致如下:
var run = await workflow.RunAsync(new GenerationRequest { Prompt = "用一句话说明什么是 SubAgent" }); await foreach (var evt in run.OutgoingEvents) { Console.WriteLine($"[事件] {evt.GetType().Name}"); } var output = await run.GetOutputAsync<ValidationResult>(); Console.WriteLine($"校验结果: {output.Passed}, 原因: {output.Reason}");运行后,你期望看到的事件顺序是:WorkflowStartedEvent、ExecutorInvokedEvent(generator)、ExecutorCompletedEvent(generator)、ExecutorInvokedEvent(validator)、ExecutorCompletedEvent(validator)、WorkflowOutputEvent。如果事件顺序不对,比如 validator 没有被调用,说明 Edge 没连上,或者消息类型不匹配。
验证成功的标志有三个。第一,控制台没有 401 或 403,说明auth.json里的 Key 和 Base URL 生效了。第二,choices字段能被正确解析,说明模型返回结构符合预期。第三,ValidationResult.Passed为true,说明校验节点确实收到了生成节点的输出。
如果模型返回的内容为空,先检查auth.json里的model字段是否拼写正确。模型 ID 写错时,服务端通常返回 404 或 400,而不是 401,这一点容易混淆。另外,choices数组为空也会导致GetProperty("choices")[0]抛异常,报错信息里会出现reading choices字样,这是模型没有正常返回的典型信号。
再给一个检查清单,按顺序排查:
| 检查项 | 预期结果 | 异常表现 |
|---|---|---|
| auth.json 路径 | 文件存在且可读 | FileNotFoundException |
| api_key 字段 | 非空且以 sk- 开头 | 401 Unauthorized |
| base_url 字段 | 指向 https://taotoken.net/api | local proxy failed |
| model 字段 | 与控制台可用模型一致 | 404 或 reading choices |
| Edge 连接 | generator 到 validator 有边 | validator 未被调用 |
| 消息类型 | GenerationResult 匹配 | 路由无匹配 handler |
这张表建议在第一次跑通前逐项过一遍。尤其是base_url,如果误写成带/v1的地址,再拼一次/v1/chat/completions就会变成/v1/v1/chat/completions,服务端返回 404,但报错信息可能被包装成local proxy failed,看起来像网络问题,实际是路径重复。
验证通过之后,你可以把 Prompt 换成更复杂的任务,比如让 generator 生成一段代码,validator 检查代码里是否包含特定关键字。只要最小任务集跑通,后续扩展就是加 Executor 和 Edge 的事。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节把实际遇到的报错和原因对照清楚。MAF 的 SubAgent 在 Multi-Agent 协作时,报错往往不会直接指向根因,需要结合auth.json和调用链一起看。
401 Unauthorized:最常见。原因通常是api_key为空、过期,或者Authorization头没带上。检查auth.json里的api_key是否以sk-开头,以及代码里是否真的把_auth.ApiKey拼进了请求头。如果 Key 正确但仍然 401,确认base_url没有多余空格。JSON 里字符串前后的空格不会被自动去除," https://taotoken.net/api "会导致请求发到错误地址。
local proxy failed:这个报错名字容易误导,它不一定是网络代理问题。在 MAF 场景下,多数是auth.json解析失败或字段缺失导致的。比如base_url写成了baseUrl,而反序列化没有开启大小写不敏感,结果BaseUrl为空,请求发到空地址。解决办法是确认PropertyNameCaseInsensitive = true,并且 JSON 字段名和类属性名能对应上。另外,auth.json里如果混入了注释,标准 JSON 解析器会直接失败,也会报这个错。
reading choices:这个报错出现在解析模型响应时,通常是choices数组为空或结构不符。原因可能是model字段填了一个不存在的模型 ID,服务端返回了错误结构;也可能是请求体里messages格式不对。检查model是否和控制台里可用的模型 ID 完全一致,注意大小写和日期后缀。请求体里messages必须是数组,每个元素有role和content。
OAuth 相关报错:如果你在auth.json里保留了oauth或token字段,MAF 的某些版本会优先走 OAuth 流程,导致和 API Key 冲突。解决办法是删掉auth.json里除base_url、api_key、model、provider之外的字段。OAuth 和 API Key 不要混用,选一种即可。
还有一个容易忽略的点:如果你用了 CC Switch 或 Cline MCP 这类工具,它们可能会改写auth.json。确保这些工具没有在后台覆盖你的配置。检查方法是运行前先cat ~/.codex/auth.json确认内容,运行后再看一次,如果被改了,说明有工具在写这个文件。
排查顺序建议是:先看auth.json内容,再看请求头,再看请求路径,最后看响应结构。大部分问题在前两步就能定位。如果 401 和 local proxy failed 同时出现,优先解决 401,因为鉴权失败时请求可能根本没发出去,后面的报错是连锁反应。
6. 语义一致 CTA:把 SubAgent 协作链路跑稳之后
当你的最小任务集能稳定返回,事件顺序符合预期,ValidationResult.Passed为true,说明 Microsoft Agent Framework 下的 SubAgent 注册、auth.json 读取、Multi-Agent 消息传递这条链路已经通了。接下来可以做的扩展包括:增加第三个 Executor 做结果格式化,或者用 FanOut Edge 把同一个请求分发给多个 SubAgent 并行处理,再用 FanIn 汇聚结果。
如果你在配置auth.json或排查 401 时需要对照密钥和接入参数,可以去 API Keys 页面管理密钥,接入文档里有完整的 Base URL 和请求示例。想先确认模型 ID 和返回结构,用模型对话页面发一条测试消息最快。长期做编码类 Agent 或需要多轮协作的场景,Coding Plan 更适合,它把调用配额和模型路由都收敛好了,不用每个 SubAgent 单独配。
回到 MAF 本身,SubAgent 和 Multi-Agent 的价值在于把复杂任务拆成可验证的小步骤。每个 Executor 只做一件事,Edge 决定消息流向,SuperStep 负责批量调度。只要auth.json这一层配置稳定,上层的工作流编排就可以放心迭代。我自己的习惯是:每加一个 SubAgent,就先单独跑一次它的 Handler,确认能拿到模型返回,再连进 Workflow。这样出问题时,能立刻判断是 Agent 本身的问题,还是 Edge 或消息类型的问题。