资讯详情

C# 实现简版 Claude Code | 子代理与上下文隔离(4):Task 调度与隔离边界

📅 2026/10/4 16:08:52 | 华诺云谱 👁 阅读
C# 实现简版 Claude Code | 子代理与上下文隔离(4):Task 调度与隔离边界
1. 为什么单 Agent 跑大任务会“失忆”上下文污染的真实场景如果你正在用 C# 自研一个 Claude Code 类的编码助手大概率会遇到这样一个尴尬时刻用户丢过来一句“重构 auth 模块换成 JWT”你的 Agent 开始勤勤恳恳地cat文件一个、两个、十五个每个文件几百行全塞进对话历史。等它终于“看完”准备动手改代码时前面读过的关键方法签名已经被挤出了上下文窗口模型开始反问“AuthService 里那个方法签名是什么来着”这不是模型笨而是上下文污染。单 Agent 架构下探索阶段的输出和实现阶段的工作空间共用同一份消息历史前置任务的细节会持续挤占后续任务的 token 预算。我实测过一个中等规模的 ASP.NET Core 项目让单 Agent 探索 15 个文件后再重构模型在实现阶段重新读取文件的次数高达 7 次token 消耗直接翻倍。解决思路其实很朴素把“探索”和“实现”拆成独立的子代理每个子代理拥有自己干净的消息历史、过滤后的工具集和专门的系统提示只向父代理返回最终摘要。父代理的上下文始终保持精简子代理之间互不串扰。这就是子代理机制的核心——进程隔离等于上下文隔离。本文是 C# 实现简版 Claude Code 系列的第四篇对应v3_subagent.cs。我会给出可复制的 Task 调度配置、隔离边界的验证步骤以及如何通过统一的 Key/API 通道接入模型调用让你能亲手验证子代理之间上下文确实不串扰。适合已经写过基础 Agent 循环、想进一步做上下文管理的 .NET 开发者。2. 前置准备用 TaoToken 统一 Key/API 通道接入模型调用在写子代理调度之前得先有一个稳定的模型调用通道。自研 Claude Code 类工具最烦的就是 Key 管理和多模型切换我试过把不同厂商的 Key 散落在配置文件里结果调试时经常搞混。后来统一走 TaoToken 的 API 通道一个 Key 覆盖对话和编码场景配置也集中。TaoToken 在这里扮演的是模型调用的统一入口不是替代你的编辑器或 Agent 逻辑。你的 C# 程序负责 Task 调度和上下文隔离TaoToken 负责把请求转发到模型并返回结果。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址不加 UTM 参数。你需要准备三件套Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 根据你用的模型填比如claude-sonnet-4-20250514这类标识。生成 Key 的入口在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。在 C# 项目里我建议把这三件套放进appsettings.json或环境变量不要硬编码。下面是一个最小化的配置片段路径和字段名你可以直接照抄{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-your-key-here, ModelId: claude-sonnet-4-20250514, MaxTokens: 4096 }, SubAgent: { MaxToolRounds: 20, EnableNestedTask: false } }读取配置的代码用IConfiguration就行var config new ConfigurationBuilder() .AddJsonFile(appsettings.json) .AddEnvironmentVariables() .Build(); var baseUrl config[TaoToken:BaseUrl]; var apiKey config[TaoToken:ApiKey]; var modelId config[TaoToken:ModelId];这里有个坑要注意BaseUrl末尾不要带斜杠SDK 拼接路径时容易出双斜杠导致 404。另外EnableNestedTask我默认设成false意思是子代理不再拥有 Task 工具防止无限递归生成子代理。生产系统如果确实需要嵌套得配合深度计数器和超时控制这个后面排障章节会讲。配置好之后先别急着写子代理用一段最小请求验证通道是否通using var client new HttpClient(); client.DefaultRequestHeaders.Add(x-api-key, apiKey); client.DefaultRequestHeaders.Add(anthropic-version, 2023-06-01); var payload new { model modelId, max_tokens 128, messages new[] { new { role user, content 回复 OK 两个字母 } } }; var response await client.PostAsJsonAsync(${baseUrl}/v1/messages, payload); var body await response.Content.ReadAsStringAsync(); Console.WriteLine(body);如果返回里能看到content数组和OK说明通道没问题。这一步很重要因为后面子代理隔离验证时如果请求失败你分不清是调度逻辑错了还是通道断了。验证模型对话也可以直接在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 页面上先手动试一次确认 Key 有效再写代码。3. 可复制的 Task 调度配置Agent 类型注册与工具过滤子代理调度的核心是两件事Agent 类型注册表以及每个类型能用的工具集。注册表决定“有哪些子代理”工具集决定“子代理能干什么”。这两者共同构成隔离边界的第一层。先定义 Agent 配置的记录类型public record AgentConfig( string Description, string[] Tools, string Prompt );然后注册三种典型子代理。explore 只读用于安全探索code 全工具用于实际实现plan 只读用于设计方案。注意 explore 和 plan 的工具列表里没有写权限工具这是最小权限原则的直接应用var agentTypes new Dictionarystring, AgentConfig { [explore] new( 只读代理用于探索代码、查找文件、搜索, new[] { bash, read_file }, 你是一个探索代理。搜索和分析但不要修改文件。返回简洁的摘要。 ), [code] new( 完整代理用于实现功能和修复 bug, new[] { * }, 你是一个编程代理。高效地实现请求的更改。 ), [plan] new( 规划代理用于设计实现策略, new[] { bash, read_file }, 你是一个规划代理。分析代码库并输出编号的实现计划。不要做更改。 ) };工具过滤函数是关键。当Tools包含*时返回全部基础工具但故意排除 Task 工具本身这样子代理无法再生成子代理从结构上杜绝无限递归ListTool GetToolsForAgent(string agentType) { var allowed agentTypes[agentType].Tools; if (allowed.Contains(*)) return baseTools.Where(t t.Name ! Task).ToList(); return baseTools.Where(t allowed.Contains(t.Name)).ToList(); }Task 工具本身的定义要写清楚隔离语义让模型知道子代理看不到父代理的历史var taskTool new Tool { Name Task, Description 为专注的子任务生成子代理。 子代理在隔离上下文中运行 - 它们看不到父代理的历史。 用这个来保持主对话干净。 Agent 类型: - explore: 只读探索 - code: 完整实现 - plan: 设计策略 示例: - Task(explore): 查找所有使用 auth 模块的文件 - Task(plan): 设计数据库迁移策略 - Task(code): 实现用户注册表单 , InputSchema new InputSchema { Type object, Properties new Dictionarystring, JsonElement { [description] /* 简短描述3-5 词 */, [prompt] /* 详细指令 */, [agent_type] /* explore | code | plan */ }, Required new[] { description, prompt, agent_type } } };子代理执行的核心函数RunTaskAsync是隔离边界的第二层。注意subMessages是全新创建的列表只包含当前 prompt绝不引用父代理的消息历史async Taskstring RunTaskAsync(string description, string prompt, string agentType) { var config agentTypes[agentType]; var subSystem $ 你是一个位于 {workDir} 的 {agentType} 子代理。 {config.Prompt} 完成任务并返回清晰简洁的摘要。 ; var subTools GetToolsForAgent(agentType); // 关键隔离的消息历史不包含父代理的任何历史 var subMessages new ListMessage { prompt.AsUserMessage() }; while (true) { var response await client.CreateMessageAsync( modelId, subMessages, new MessageParameters { System subSystem, Tools subTools }); if (response.StopReason ! StopReason.ToolUse) { return ExtractText(response); } // 执行工具把结果追加到 subMessages // ... } }进度显示用单行刷新避免中间输出淹没主对话Console.Write($\r [{agentType}] {description}... {toolCount} tools, {elapsed:F1}s);完成后换行输出最终状态。这样用户能看到子代理在工作但看不到它内部的每一次工具调用细节主对话保持干净。4. 验证请求与成功结果确认子代理间上下文不串扰配置写完了怎么证明隔离真的生效光看代码不够得设计可观测的验证步骤。我通常用三个测试来确认边界。测试一探索摘要不携带原始文件内容。让 explore 子代理去读一个包含敏感字符串的文件然后检查父代理收到的摘要里是否包含那个字符串。如果隔离正确摘要应该是概括性的不会逐字复制文件内容。var summary await RunTaskAsync( 探索 auth 结构, 读取 src/Services/AuthService.cs总结它的公开方法, explore); Console.WriteLine(summary); // 期望摘要提到方法名但不包含文件里的完整代码块测试二连续两个子代理互不影响。先跑一个 explore 探索 auth再跑一个 explore 探索 user检查第二个子代理的响应里是否出现了第一个子代理才读过的文件路径。如果出现说明消息历史被共享了。var r1 await RunTaskAsync(探索 auth, 总结 AuthService 的依赖, explore); var r2 await RunTaskAsync(探索 user, 总结 User 模型的字段, explore); // r2 不应包含 AuthService 相关的路径或方法名测试三工具权限边界。让 explore 子代理尝试写文件观察它是否被拒绝。因为 explore 的工具集里没有写工具模型即使想写也无从调用。var r3 await RunTaskAsync( 尝试修改, 在 src/ 下创建一个 test.txt 文件, explore); // 期望子代理报告无法完成因为没有写工具一个完整的成功工作流输出大概长这样You: 重构 auth 模块使用 JWT 让我先了解现有结构 Task: 探索 auth 结构 [explore] 探索 auth 结构 - done (12 tools, 5.2s) 摘要Auth 模块位于 src/Services/AuthService.cs使用 Session 认证。 依赖User 模型、TokenRepository。主要方法Login(), Logout(), ValidateToken()。 接下来设计迁移方案 Task: 设计 JWT 迁移 [plan] 设计 JWT 迁移 - done (5 tools, 3.1s) 迁移计划 1. 添加 JWT 依赖包 2. 创建 JwtService 类 3. 修改 AuthService 使用 JwtService 4. 更新 TokenRepository 5. 添加配置项 现在执行实现 Task: 实现 JWT 认证 [code] 实现 JWT 认证 - done (15 tools, 12.3s) 完成创建了 JwtService.cs修改了 AuthService.cs 更新了 appsettings.json添加了 3 个单元测试。注意父代理的上下文里只有三段摘要没有 15 个文件的原始内容。这就是隔离带来的收益主对话 token 占用从可能的上万降到几百。如果你在验证时想快速对比不同模型在子代理场景下的表现可以在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 切换 Model ID 重跑测试二观察不同模型的摘要风格差异。长期做编码 Agent 的话Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合需要稳定跑大量子代理任务的场景。5. 本篇常见错误排查401、local proxy failed 与 choices 解析异常子代理调度涉及网络、配置、消息序列化多个环节出错时症状容易混淆。下面是我踩过的几类真实报错和定位方法。401 Unauthorized。最常见的原因是 API Key 没带上或带错了。检查你的HttpClient是否在每次请求都设置了x-api-key头。如果你用的是 SDK确认初始化时传入了 Key。还有一种隐蔽情况环境变量里有个空的TAOTOKEN_API_KEY覆盖了配置文件里的值。排查时打印一下实际使用的 Key 前几位和后几位确认不是空字符串。local proxy failed / connection refused。这个报错通常出现在你本地配了某个转发端口但服务没起来。如果你没有主动配置本地转发检查BaseUrl是不是被误设成了http://localhost:xxxx。正确的 Base URL 应该是https://taotoken.net/api。另外公司网络环境如果有出口限制也可能导致连接失败这种情况需要确认网络策略允许访问该域名。reading choices / 解析响应失败。这个报错说明你的代码在按 OpenAI 格式解析响应但实际返回的是 Anthropic 格式或反过来。Anthropic 格式的响应是content数组OpenAI 格式是choices数组。检查你的反序列化模型是否和 API 端点匹配。如果你用的是/v1/messages端点就按content解析如果用/v1/chat/completions才按choices解析。混用会导致字段读不到抛空引用或解析异常。OAuth 相关报错。如果你在配置里看到了 OAuth 字样说明你可能误用了需要 OAuth 流程的端点。TaoToken 的 API Key 方式是直接带x-api-key头不需要走 OAuth 授权码流程。检查你的配置里有没有多余的oauth字段删掉即可。子代理无限递归。症状是程序卡死或 token 疯狂消耗。根因是子代理的工具集里包含了 Task 工具。回到GetToolsForAgent函数确认*分支里过滤掉了Task。如果你确实需要嵌套加一个深度参数超过 2 层就拒绝生成。子代理看不到文件。检查workDir是否正确传递给了子代理的系统提示。子代理的工作目录应该和父代理一致否则read_file会找不到路径。另外确认工具执行时的路径拼接没有重复斜杠。上下文仍然串扰。如果你发现第二个子代理的响应里出现了第一个子代理的内容检查subMessages是不是被复用成了同一个列表引用。每次RunTaskAsync都必须new ListMessage()不能把父代理的列表传进去。排查时建议打开详细日志把每次请求的model、messages长度、tools名称打印出来。这样一眼就能看出是配置问题还是逻辑问题。如果确认是 Key 或通道问题去 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 重新生成一个 Key 试试排除 Key 失效的可能。6. 从 v3 到 v4子代理之后上下文管理还能怎么走v3 解决了上下文污染但还有一个挑战没碰领域知识。当用户说“处理这个 PDF”或“构建一个 MCP 服务器”时模型需要知道该用pdftotext还是PyMuPDF需要知道 MCP 的协议规范。这些不是工具能力而是领域知识塞进系统提示会撑爆上下文不塞又做不对。v4 的思路是 Skills 机制——按需加载专业知识。子代理负责隔离执行上下文Skills 负责隔离知识上下文。两者结合才是完整的上下文管理方案。不过在跳到 v4 之前建议你先把 v3 的隔离边界跑通用本文的测试一、二、三确认子代理之间确实不串扰。这个基础打牢了后面加 Skills 才不会乱。代码文件对应v3_subagent.cs你可以基于本文的配置片段直接改。跑通之后试着把MaxToolRounds从 20 调到 5观察子代理在工具轮次受限时会不会更早返回摘要——这也是控制成本的一个实用旋钮。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑