Agent Harness 工程:Subagent 与上下文隔离

📅 2026/7/17 9:52:24
Agent Harness 工程:Subagent 与上下文隔离
上一篇我们给 Agent 加了 TodoWrite让它在复杂任务开始前先列清单并在执行过程中持续更新进度。这已经让 Agent 从“想到哪做到哪”进化到了“按计划推进任务”。但 TodoWrite 只能解决一部分问题。当任务继续变复杂时Agent 仍然会遇到一个更底层的限制上下文窗口是有限的注意力也是有限的。比如让 Agent 做一个完整功能先读项目结构再读相关组件再找状态管理逻辑再修改代码再跑测试测试失败后继续排查最后总结修改。即使有 TodoWritemessages里仍然会混进大量过程信息文件内容、工具调用参数、测试输出、错误栈、模型中间判断、权限提示、审计结果。这些内容不是完全没用但它们不一定都应该一直占着主 Agent 的上下文。所以这一篇继续往前走给 Harness 增加Subagent。为什么 TodoWrite 之后还需要 SubagentTodoWrite 维护的是任务状态它能告诉 Agent当前计划是什么哪一步正在进行哪些步骤已经完成是否还有遗漏。但 TodoWrite 不负责压缩过程信息。如果主 Agent 亲自完成所有细节它还是要把所有工具结果都塞进自己的messages。任务越长上下文越吵。可以做一个类比。假设你是一个汽车工厂的负责人。如果发动机、底盘、外观、仪表、测试、采购每个环节都要你亲手做你很快就会忙到顾不过来。更合理的方式是你负责拆任务、定标准、协调进度每个环节交给对应的人去完成最后你只接收关键结果。软件开发里也是一样。项目经理不应该亲自改每一行代码主进程不应该亲自执行每一个 worker 的细节主 Agent 也不应该把所有局部探索都塞进自己的上下文。Subagent 的核心价值就是主 Agent 负责规划、分派和整合子 Agent 在干净上下文里完成局部任务并把压缩后的结果返回给主 Agent。Subagent 把局部探索和局部执行隔离出去主 Agent 只保留任务计划、关键结论和最终决策。Subagent 不是更聪明的模型很多人一提到 Subagent就容易想成“多找几个模型一起想答案就更聪明”。这不是这篇要讲的重点。在 Harness 工程里Subagent 首先是一种上下文管理手段。它解决的不是“模型智商不够”而是“主上下文太乱、太长、太容易分散注意力”。一个子 Agent 通常应该具备几个特点特点说明上下文干净只接收完成当前子任务需要的信息目标单一一次只完成一个明确任务工具受限默认只给它必要工具不要把所有能力都开放结果压缩返回结论、证据和建议而不是把完整过程倒回主上下文生命周期短子任务完成就结束不长期持有状态这里最关键的是“干净上下文”。如果你把主 Agent 的完整messages原样传给子 Agent那其实没有隔离。子 Agent 只是换了一个循环继续读同一堆噪声问题并没有解决。更好的方式是给子 Agent 一个任务包要完成什么任务已知背景是什么可以查看哪些文件或信息不能做什么输出格式是什么最多执行多少步。子 Agent 完成后只把整理过的结果返回给主 Agent。子代理也是一个工具从 Harness 的角度看Subagent 不需要被神化。它其实也是一个工具。主 Agent 调用run_subagentHarness 创建一个新的 Agent Loop把主 Agent 给出的子任务交进去。子 Agent 在自己的消息历史里执行工具、收集信息、生成结果。最后Harness 把子 Agent 的最终回答作为run_subagent的工具结果写回主 Agent 的messages。也就是说主 Agent 眼里的 Subagent 调用还是熟悉的工具协议模型请求调用run_subagentHarness 解析参数权限和 Hook 生效Harness 启动一个隔离的子循环子循环返回压缩结果主 Agent 继续推进 Todo。这样设计的好处是前几篇搭出来的TOOLS、TOOL_HANDLERS、权限策略、Hook、TodoWrite都还能继续复用。先定义 Subagent 工具先把run_subagent暴露给主 Agent。这个工具的参数不要太复杂先保留三个核心字段task子 Agent 要完成的任务context主 Agent 已经整理好的必要背景expected_output希望子 Agent 按什么格式返回结果。SUBAGENT_TOOL { type: function, function: { name: run_subagent, description: ( Run an isolated subagent for a focused subtask. Use it when a local investigation or implementation detail would pollute the main agent context. ), parameters: { type: object, properties: { task: { type: string, description: The focused subtask for the subagent., }, context: { type: string, description: Relevant background selected by the main agent., }, expected_output: { type: string, description: The desired structure of the subagent result., }, }, required: [task], }, }, }然后把它加到主 Agent 的工具列表里TOOLS [ BASH_TOOL, LIST_DIR_TOOL, READ_FILE_TOOL, WRITE_FILE_TOOL, EDIT_FILE_TOOL, TODO_WRITE_TOOL, SUBAGENT_TOOL, ]注意这里是把SUBAGENT_TOOL加给主 Agent。子 Agent 自己不一定拥有这个工具。这个区别非常重要后面会讲。子 Agent 的工具应该受限如果子 Agent 一上来就拥有主 Agent 的全部工具包括write_file、edit_file、run_subagent问题会很快变复杂。它可能修改主 Agent 不知道的文件可能自己再创建子 Agent可能把一个局部任务扩张成更大的任务也可能让权限审批链条变得很难追踪。教学版里建议先从只读子 Agent 开始。比如只给它目录和文件读取能力SUBAGENT_TOOLS [ LIST_DIR_TOOL, READ_FILE_TOOL, ] SUBAGENT_TOOL_HANDLERS: dict[str, Callable[..., str]] { list_dir: list_dir, read_file: read_file, }这意味着子 Agent 可以帮主 Agent 调查问题、阅读代码、整理结论但不能直接改文件。如果后面确实需要让子 Agent 修改文件也应该先做更严格的边界限制它只能修改某个目录限制它只能使用某些工具限制它最多执行几轮要求它返回修改摘要让主 Agent 或用户审批关键动作。更重要的是不要默认让子 Agent 调用run_subagent。否则很容易出现递归派生主 Agent 创建子 Agent子 Agent 再创建孙 Agent孙 Agent 继续创建新的 Agent。即使每一层都“看起来合理”整体也会变得难以控制。一个简单规则是子 Agent 默认不再派生自己的子 Agent。实现 run_subagent现在实现run_subagent。它和主 Agent Loop 很像只是有自己的系统提示词、自己的消息历史、自己的工具列表和自己的最大步数。SUBAGENT_SYSTEM_PROMPT 你是一个子代理只负责完成主 Agent 分配给你的局部任务。 要求 1. 只处理当前任务不要扩展目标。 2. 优先使用给定 context不要假设没有提供的信息。 3. 如果需要读取文件只读取和任务直接相关的文件。 4. 不要修改文件不要执行破坏性操作。 5. 最终回答要简洁包含结论、关键证据和建议下一步。 def build_subagent_user_message( task: str, context: str , expected_output: str , ) - str: return f 任务 {task} 已知背景 {context or 没有额外背景。} 期望输出 {expected_output or 请返回结论、关键证据和建议下一步。} .strip()然后写一个子 Agent 专用的工具执行函数。它只从SUBAGENT_TOOL_HANDLERS里取工具所以即使主 Agent 拥有更多工具子 Agent 也调用不到。def run_subagent_tool_call(tool_call: Any) - str: name tool_call.function.name handler SUBAGENT_TOOL_HANDLERS.get(name) if handler is None: return fError: tool {name} is not available to subagent. try: arguments json.loads(tool_call.function.arguments or {}) except json.JSONDecodeError as exc: return fError: invalid JSON arguments for {name} - {exc} if not isinstance(arguments, dict): return fError: arguments for {name} must be a JSON object. try: return handler(**arguments) except TypeError as exc: return fError: invalid arguments for {name} - {exc} except Exception as exc: return fError: subagent tool {name} failed - {exc}最后是子循环本身def run_subagent( task: str, context: str , expected_output: str , max_steps: int 6, ) - str: messages: list[dict[str, Any]] [ { role: user, content: build_subagent_user_message( tasktask, contextcontext, expected_outputexpected_output, ), } ] max_steps min(max_steps, 8) for _ in range(max_steps): response client.chat.completions.create( modelMODEL, messages[{role: system, content: SUBAGENT_SYSTEM_PROMPT}] messages, toolsSUBAGENT_TOOLS, ) assistant_message response.choices[0].message if not assistant_message.tool_calls: return (assistant_message.content or ).strip()[:12000] messages.append( { role: assistant, content: assistant_message.content or , tool_calls: [ { id: tool_call.id, type: function, function: { name: tool_call.function.name, arguments: tool_call.function.arguments, }, } for tool_call in assistant_message.tool_calls ], } ) for tool_call in assistant_message.tool_calls: tool_result run_subagent_tool_call(tool_call) messages.append( { role: tool, tool_call_id: tool_call.id, content: tool_result, } ) return Error: subagent reached max_steps before producing a final result.这段代码里有几个刻意的限制。第一子 Agent 的messages是新建的。它不会继承主 Agent 的完整历史只接收主 Agent 整理出来的task、context和expected_output。第二子 Agent 使用SUBAGENT_TOOLS不是主 Agent 的TOOLS。这保证了工具边界。第三max_steps被限制住了。子 Agent 是短生命周期 worker不应该无限运行。第四最终结果被截断到 12000 字符。真实项目里你可以用更精细的结果压缩策略但教学版先避免子 Agent 把大段原始内容倒回主上下文。主 Agent 不把完整历史塞给子 Agent只传必要任务包子 Agent 也只返回压缩后的结果。注册到 TOOL_HANDLERS有了run_subagent()再把它注册到主 Agent 的处理器里TOOL_HANDLERS: dict[str, Callable[..., str]] { bash: bash, list_dir: list_dir, read_file: read_file, write_file: write_file, edit_file: edit_file, run_todo_write: run_todo_write, run_subagent: run_subagent, }主 Agent Loop 不需要大改。对它来说run_subagent和read_file、edit_file一样都是一次工具调用。区别只是read_file直接读取文件而run_subagent会在 Harness 内部启动一个短生命周期的子循环。这也是前几篇不断抽象工具系统的回报只要工具协议稳定后面就可以把很复杂的能力包装成一个工具。权限策略怎么处理run_subagent不直接修改文件但它会消耗模型调用次数也可能读取项目内容。所以权限策略要看你给子 Agent 开放了什么工具。如果子 Agent 是只读的可以默认放行def check_permission(tool_name: str, arguments: dict[str, Any]) - PermissionResult: if tool_name run_subagent: return PermissionResult( allow, subagent uses isolated read-only tools, ) # 其他规则沿用前几篇如果子 Agent 具备写文件、执行命令、访问网络的能力就不应该简单放行。可以把权限策略升级成创建写入型子 Agent 前需要用户确认子 Agent 内部每次高风险工具调用仍然走权限 Hook子 Agent 的工具调用记录进审计日志子 Agent 的最终结果必须说明它执行了哪些关键操作。也就是说Subagent 不是绕过权限系统的通道。它只是一个新的工具仍然应该被 Harness 管住。和 TodoWrite 怎么配合TodoWrite 和 Subagent 的关系很自然。TodoWrite 负责主 Agent 的任务计划Subagent 负责完成其中某个局部步骤。比如用户让 Agent “排查登录失败并修复问题”。主 Agent 可以先写 Todo{ todos: [ { id: inspect-auth-flow, content: 调查登录流程和失败位置, status: in_progress }, { id: fix-login-bug, content: 根据调查结果修复登录问题, status: pending }, { id: verify-login, content: 运行测试或手动验证登录流程, status: pending } ] }然后把第一个步骤交给子 Agent{ task: 调查登录流程找出登录失败最可能发生的位置。, context: 项目是一个 Next.js 应用。请重点查看 app、components、lib 目录中和 auth、login、session 相关的代码。, expected_output: 返回1. 关键文件2. 登录流程概述3. 最可疑的问题点4. 建议主 Agent 下一步修改哪里。 }子 Agent 读取文件、整理结果后返回类似结论登录失败最可能发生在 lib/auth/session.ts 的 cookie 解析逻辑。 关键证据 - app/login/page.tsx 提交后调用 loginAction。 - loginAction 成功后写入 session cookie。 - lib/auth/session.ts 读取 cookie 时使用了旧字段 user_id。 建议下一步 - 主 Agent 修改 session 解析字段为 userId。 - 修改后运行 auth 相关测试。主 Agent 拿到这个结果后再更新 Todoinspect-auth-flow标为completedfix-login-bug标为in_progress然后自己执行edit_file修改代码。这样主 Agent 不需要保留子 Agent 读取过的所有文件全文只需要保留结论和证据。子代理什么时候有用Subagent 适合用在局部但信息量大的任务上。比如在大项目里调查某个功能的实现路径阅读多个文件后总结模块边界根据错误栈定位可能的失败原因让一个只读 worker 先收集证据让测试 worker 专门分析失败输出让文档 worker 总结某个目录的设计。它不适合用在很小的任务上。如果只是读一个文件、改一行配置、解释一个函数主 Agent 自己做就好。强行创建子 Agent 只会增加延迟、成本和复杂度。一个简单判断是如果某个子任务会产生大量中间信息但主 Agent 最后只需要少量结论就适合交给 Subagent。常见坑Subagent 很有用但也容易做乱。第一不要把完整上下文传给子 Agent。子 Agent 的价值就在于上下文隔离。如果把主上下文原样传过去它只是在另一个窗口里继续承受同样的噪声。第二不要让子 Agent 默认拥有全部工具。工具越多行为越不可控。先从只读能力开始确认边界后再逐步开放写入和命令执行。第三不要让子 Agent 无限递归。默认不把run_subagent暴露给子 Agent。如果确实要支持多层 Agent需要单独设计深度限制、预算限制和审计。第四不要让子 Agent 返回流水账。主 Agent 需要的是结论、证据和建议不是另一份完整messages。第五不要把责任丢给子 Agent。主 Agent 仍然负责最终决策。子 Agent 的结果是输入不是自动执行的真理。小结这一篇在 TodoWrite 的基础上为 Agent Harness 增加了 Subagent用run_subagent把子代理包装成主 Agent 可以调用的工具子 Agent 使用独立messages避免污染主上下文子 Agent 使用受限工具集默认不具备写文件和继续派生子代理的能力用max_steps和结果截断控制子 Agent 的生命周期和输出大小让 TodoWrite 负责主线计划Subagent 负责局部探索和压缩结果继续让权限策略和 Hook 管住 Subagent而不是绕过 Harness。到这里Agent Harness 已经有了一个更完整的工作形态主 Agent 维护目标和 Todo工具系统提供真实动作权限和 Hook 管控工具调用Subagent 承担局部任务减少主上下文污染。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】