news 2026/10/14 19:51:46

mini-coding-agent源码解析(六):有界子Agent委派,只读受限的delegate工具设计思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mini-coding-agent源码解析(六):有界子Agent委派,只读受限的delegate工具设计思路

【免费下载链接】mini-coding-agent

Minimal and readable coding agent harness implementation in Python to explain the core components of coding agents.

项目地址:https://gitcode.com/gh_mirrors/mi/mini-coding-agent
点击查看免费下载

在mini-coding-agent的六大核心组件中,第六个组件是"委派与有界子Agent"(Delegation And Bounded Subagents)。它通过一个名为delegate的工具,让主 Agent 把耗时的调查类子任务交给一个只读、有步数上限的子 Agent 去执行,既扩展了主 Agent 的探索能力,又严格限制了子 Agent 的权限边界。本文带你读懂 mini_coding_agent.py 中tool_delegate的完整设计思路。

为什么 Coding Agent 需要"委派"能力?

想象你让 Agent 排查一个跨文件的问题:"为什么这个函数在某个输入下返回异常?"这类任务可能需要:

  • 读 5~10 个文件
  • 多轮搜索关键字
  • 逐步缩小排查范围

如果全部由主 Agent 完成,这些工具调用和输出会全部挤进主会话的历史,很快把上下文撑爆。委派(delegation)的思路是:把这些"跑腿式"的调查工作外包给一个临时子 Agent,子 Agent 独立跑完只返回一份浓缩的结论,主 Agent 的上下文只多出一条delegate_result。

这就是"有界"(bounded)的含义:子 Agent 不是全能的,它被锁在深度、权限、步数三道栅栏之内。

delegate 工具如何被"条件注册"

delegate工具并非无条件存在。在 build_tools 的末尾有这样一段判断:

if self.depth < self.max_depth: tools["delegate"] = { "schema": {"task": "str", "max_steps": "int=3"}, "risky": False, "description": "Ask a bounded read-only child agent to investigate.", "run": self.tool_delegate, }

mini_coding_agent.py 的这段逻辑很巧妙:只有当当前 Agent 的depth小于max_depth时,才把 delegate 注入工具表。也就是说:

  • 顶层 Agent 构造时默认 depth=0, max_depth=1,因此它拥有 delegate 工具;
  • 子 Agent 构造时depth=self.depth + 1恰好等于max_depth,它根本看不到 delegate 这个工具,从源头上杜绝了"子 Agent 再套娃"的无限递归委派。

工具本身只有两个参数:task(子任务描述,必填)和max_steps(子 Agent 最多可用的步数,默认 3)。risky标记为False,因为委派本身不直接修改任何东西——真正的安全约束写在下一节的栅栏里。

tool_delegate 的三道安全栅栏

核心实现位于 tool_delegate:

def tool_delegate(self, args): if self.depth >= self.max_depth: raise ValueError("delegate depth exceeded") task = str(args.get("task", "")).strip() if not task: raise ValueError("task must not be empty") child = MiniAgent( model_client=self.model_client, workspace=self.workspace, session_store=self.session_store, approval_policy="never", max_steps=int(args.get("max_steps", 3)), max_new_tokens=self.max_new_tokens, depth=self.depth + 1, max_depth=self.max_depth, read_only=True, ) child.session["memory"]["task"] = task child.session["memory"]["notes"] = [clip(self.history_text(), 300)] return "delegate_result:\n" + child.ask(task)

虽然代码不长,但信息密度极高。逐条拆解:

栅栏一:深度检查(depth / max_depth)

入口处的if self.depth >= self.max_depth是最后一道保险。虽然条件注册已经保证只有depth < max_depth的 Agent 才拥有该工具,但 validate_tool 中对delegate的校验也重复了同一检查,并在越界时抛出"delegate depth exceeded"。注册层 + 校验层双重检查,任何路径都无法突破深度上限。

栅栏二:read_only=True(只读锁)

子 Agent 被显式标记为read_only=True。它的效果体现在 approve 方法的第一行:

def approve(self, name, args): if self.read_only: return False

read_only会让所有 risky 工具的审批直接返回 False。由于 run_tool 在高风险工具执行前强制走approve关卡,子 Agent 即便"想"调用write_file、patch_file或run_shell,也会在审批环节被拒绝。于是子 Agent 在物理上只能使用list_files、read_file、search这类安全的只读工具——这正是标题中"只读受限"四个字的由来。

栅栏三:approval_policy="never"(审批策略兜底)

read_only已经拒绝了 risky 工具,approval_policy="never"是双保险:即使未来某次修改遗漏了read_only分支,never策略也会让 approve 对所有审批返回False。更值得注意的是,子 Agent 的approval_policy被硬编码为"never",不会继承父 Agent 的策略——即使你在命令行用--approval auto启动了主 Agent(自动批准所有危险操作),委派出去的子 Agent 依然是只读的。父级信任模型,不代表信任模型派生出的子代理,这一细节体现了权限设计上"最小授权"的原则。

步数预算:max_steps 控制子 Agent 的"体力"

子 Agent 的max_steps直接取自调用参数(默认 3),并沿depth=self.depth + 1向下传递。回顾 ask 的主循环,while tool_steps < self.max_steps意味着子 Agent 最多只能发起 3 次工具调用就必须给出最终答案,超时会返回Stopped after reaching the step limit without a final answer.。

这一步数预算的作用:

  • 限制 token 消耗:子 Agent 每次模型调用都独立计费,3 步是"够用但不浪费"的默认值;
  • 防止跑偏:调查任务如果 3 步还没结果,说明子任务定义可能有问题,早停比死磕更划算;
  • 父级可调:主 Agent 委派时可以根据任务复杂度主动传max_steps: 2或更大的值,把预算分配权交给最需要判断的主体。

上下文交接:子 Agent 带着什么信息出发?

子 Agent 是全新构造的MiniAgent实例,历史是空的。如果什么都不给,它就像蒙着眼睛进仓库。tool_delegate用两行代码完成"交接":

child.session["memory"]["task"] = task child.session["memory"]["notes"] = [clip(self.history_text(), 300)]
  • memory["task"] = task:把委派任务写入子 Agent 的工作记忆,后续 memory_text 会将其渲染进提示词,让子 Agent 始终知道"我为什么在这里";
  • memory["notes"] = [clip(self.history_text(), 300)]:把父 Agent **当前历史的压缩摘要(裁剪到 300 字符)**塞进子 Agent 的 notes。这样子的子 Agent 虽然看不到父级的完整对话,但能大致了解排查背景,避免从零开始乱撞。

注意这里刻意只传 300 字符:委派的本意就是隔离上下文,如果父级的完整历史灌进去,委派就失去了节省上下文的意义。这是"足够信息"与"上下文隔离"之间的精妙折中。

另外子 Agent 复用了父级的model_client、workspace和session_store,意味着它操作的是同一个工作区快照、调用同一个 Ollama 模型,并且它的会话 JSON 也会照常落盘到 .mini-coding-agent/sessions/,事后可以完整追溯子 Agent 干过什么。

结果回传:一条 delegate_result 就够了

子 Agent 跑完后,tool_delegate只返回一行结构化结果:

delegate_result: <子 Agent 的最终答案>

这个返回值作为普通工具输出记录进父 Agent 的历史,父 Agent 在下一轮模型调用中读取它并整合进最终答案。由于子 Agent 的输出本身被 ask 约束为简洁结论、工具输出又被clip截断(默认 MAX_TOOL_OUTPUT = 4000),父级的上下文增量被控制得很小。

官方测试 test_delegate_uses_child_agent 完整验证了这条链路:父 Agent 先发出 delegate 工具调用,子 Agent(由FakeModelClient喂给的答案)返回结果,父 Agent 将结果写进delegate_result工具记录,最终输出整合后的答案。

一张表看懂 delegate 的约束全景

约束维度机制代码位置
递归深度depth < max_depth才注册工具;构造时depth+1L321-L327、L860
越界校验双重检查,越界抛delegate depth exceededL594-L600、L848-L849
只读权限read_only=True使approve恒返回 FalseL862、L602-L604
审批兜底approval_policy="never"硬编码,不继承父级L857、L607-L608
步数预算max_steps默认 3,超步即停L858、L455
上下文隔离仅传 300 字符历史摘要 + 任务文本L864-L865

小结:有界委派的三个设计要点

读完tool_delegate,可以提炼出 mini-coding-agent 委派设计的三条通用原则,对你自己设计多 Agent 系统同样有参考价值:

  1. 能力从结构上剥夺,而不是从提示词上请求。子 Agent 不是"被要求不要写文件",而是通过read_only+never策略在机制上无法写文件。依赖模型自觉的安全设计都是脆弱的。
  2. 递归必须有界。depth/max_depth的组合让"能否再委派"成为构造期就确定的事实,子 Agent 的工具表里压根没有 delegate,比运行时拒绝更干净。
  3. 上下文按需交接。只传任务文本 + 300 字符摘要,既给了子 Agent 足够的背景,又保住了委派最核心的价值——把冗长的调查过程关在子上下文里,只让结论上浮。

至此,mini-coding-agent 六大核心组件已全部解析完毕:实时仓库上下文、提示词结构与缓存复用、结构化工具与权限、上下文缩减与输出管理、转录记忆与断点续传、有界子 Agent 委派。整个项目仅一个 mini_coding_agent.py(约千行),想动手复现一个最小 coding agent 的同学,可以从 EXAMPLE.md 的完整交互示例开始上手。

【免费下载链接】mini-coding-agent

Minimal and readable coding agent harness implementation in Python to explain the core components of coding agents.

项目地址:https://gitcode.com/gh_mirrors/mi/mini-coding-agent
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 19:50:31

后端接口从能跑到扛得住:缓存、幂等、限流与索引优化实战

说实话&#xff0c;前两期学习记录里我写的那些接口&#xff0c;放到真实环境里大概率撑不过10分钟。第2期把某个练习商城的表结构和基础接口搭完&#xff0c;自测返回数据一切正常&#xff0c;我一度觉得自己已经懂后端了。直到某导师丢给我一句话&#xff1a;“你去看看&…

作者头像 李华
网站建设 2026/10/14 19:50:30

Qt QWidget自绘实现滑动页签下划线动画实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 19:50:16

TypeScript重制仙剑奇侠传:2D RPG引擎与可视化编辑器全解

说一个我折腾了快一年的个人项目&#xff1a;用 TypeScript 从零重写《仙剑奇侠传》&#xff0c;做成一个在浏览器里能从头打到通关的版本&#xff0c;顺便还配了一个可视化编辑器&#xff0c;用来给游戏生产内容。两个东西加起来&#xff0c;才算是把这款经典 RPG 完整复刻了一…

作者头像 李华
网站建设 2026/10/14 19:49:31

Go反射实战:从核心原理到性能优化与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 19:49:28

STM32F407寄存器实现ADC-DMA单通道采集实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 19:49:12

触发器原理与工程实践:从RS到D触发器、时序参数与FPGA应用

触发器算是数字电路里一道真正的分水岭。前面学的与门、或门、译码器再多&#xff0c;本质上都是在做“组合逻辑”——输入一变&#xff0c;输出立刻跟着变&#xff0c;电路没有记忆。可一旦进入触发器&#xff0c;事情就完全不一样了&#xff1a;同一个输入&#xff0c;电路可…

作者头像 李华