子智能体究竟继承了什么。
如今每个框架都给了你一个开关,用来决定子智能体继承多少对话,却没有一个给你开关去决定它继承多少权限——于是你调的那个是花 token 的那个,而你看不见的那个,才决定一次被攻破的运行会烂到什么程度。「委派」这个直觉借自组织架构图:把活往下交,会收窄接手者能做的事。在代码里它恰恰相反:派生五个子智能体,通常产出的是五个并发的行动者,各自握着父智能体全套工具、它的凭据和它的环境可达范围,而且没有任何记录说明是谁的指令启动了它们。把这件事做对,意味着在框架只给你一个选项的地方,写下五个各自独立的继承决定。
可以被继承的有五样。你的配置文件覆盖两样。
当父智能体派生一个子智能体时,下面这些东西会跨过边界——或者跨不过去——而每一样都是一个独立的决定,多数技术栈却把它们捆在一起或者干脆忽略。
- 对话记录。到目前为止的对话:用户最初的请求、中间的工具结果、父智能体的推理。这一项几乎处处可配。
- 指令。系统提示词、各项策略、风格规则、拒答边界。通常被和「对话记录」那个决定捆在一起,而这是个错误——「未经批准绝不给客户发邮件」这条约束应当属于子智能体,无论对话记录是否属于它。
- 权限。工具集、这些工具背后的凭据,以及那些压根不需要显式凭据、从进程里就够得着的一切。这一项几乎从不可配;也几乎总是全额继承。
- 预算与截止时间。剩余 token、剩余墙上时钟、剩余步数、剩余花费。通常不继承,也就意味着不执行。
- 出处。那些产生了子智能体任务的材料所带的信任标签。基本上从不被带下去,而它的缺席以一种我们将在 STEP 4 讲到的方式起着承重作用。
把这份清单对照你框架提供的子智能体模式来读,那种不对称会格外刺眼:最便宜搞错的两样被配得花里胡哨,而三样昂贵的则是无人选定的默认值。
往下走之前先换个说法:子智能体不是一名员工,它是一条挂着语言模型的线程。线程默认会继承进程的文件描述符、环境变量与网络访问,没人觉得这有什么奇怪。让这件事在此处显得奇怪的正是那张组织架构图,而它完全值得彻底扔掉——唯一的问题是:你有意往下传了哪些能力。
对话记录的继承:确实存在的那个开关,以及它背后真正的取舍。
LangChain 的 deepagents 把这个选择摆得很明,是个不错的推理起点。子智能体默认是 mode: "isolated",只看得见交给它的那份任务描述,不带任何导致这次委派的对话记忆。切成 mode: "fork",它就继承父智能体完整的对话历史和一字不差的系统提示词。技能又是另一码事:自定义子智能体默认不继承技能,而在技能存在时,技能状态是双向隔离的——父的对子不可见,子的也不回传给父。LangGraph 的子图在结构上表达了同一根轴:共享状态 schema 意味着子图读得到父图的键,而私有状态则把它的工作记忆挡在父图之外。
两种设置都会失败,而且以相反且可预期的方式失败。
- fork 败在成本与注意力上。你把一个很大的上下文窗口复制了 N 份、付了 N 次钱,并交给每个子智能体一个窗口——在那个窗口里,真正相关的那条指令埋在成千上万条无关 token 之间。这是上下文预算最不留情的一面,因为这种复制是乘法而不是加法。
- isolated 败在「第三轮说过的那条约束」上。用户八条消息之前说过「控制在两页以内」;父智能体的任务描述没有重复它;子智能体无从知晓。每一次隔离式交接,都把父智能体那份简报变成了全部规格说明,而在 token 压力下由模型写出的简报,会丢掉限定词。
务实的解法两个开关都不是:是一份带类型的交接。给每一种子智能体角色一个小小的结构化 schema——任务、累积下来的约束、完成的定义、它可以读的制品——并由父智能体来填。这就把「要给多少历史」变成了一个有可检验答案的问题,因为缺一个字段在轨迹里看得见,而一段话里缺一句话看不见。它同时让监督者的任务分解变得可评审,而这类失败大多其实就住在那儿。
权限的继承:并不存在的那个开关。
真正要紧的不对称在这里。工具访问权是绑在进程上的,不是绑在对话上的。在同一个运行时里派生出来的子智能体,共享同一批 MCP 客户端连接、同一批环境变量、磁盘上同一批 OAuth 令牌、同一个网络命名空间、同一个数据库会话。把 mode: "isolated" 打开,隔离的是对话记录,这些一样都没改。
所以,在设计评审上该念出来的那句话是:上下文隔离是可配的,权限隔离是架构性的。一个 isolated 的子智能体不是一个被沙箱化的子智能体。它是一个可达范围完全相同的全新上下文窗口;如果它被那份「请你摘要一下」的文档说服了,它能做父智能体能做的一切——通过父智能体打开的连接,没有审批环节,而且通常也没有任何监督者去检视它单独的工具调用。
接着,扇出会在拓扑图不会画出来的方向上把暴露面成倍放大:
- 并发,权限不变。五个并行工作者是五个都能写的行动者,而不是五个五分之一的写者。任何按单个智能体调用模式调好的限流、审批队列或人工检查点,现在面对的是五个。
- 审计轨迹丢掉了主语。如果子智能体们是用父智能体的凭据去调工具,那日志里每一个动作都归到同一个身份名下,事后你说不出是哪个工作者发了那封邮件。这就是智能体身份里「归因」那一半,而它丢失的时刻是派生时,不是记日志时。
- 递归通常没有护栏。一个能派生子智能体的子智能体,若没有被继承的深度计数器,就是一颗以 token 计价的 fork 炸弹。框架很少默认给它设界。
真正有用的做法并不花哨,而且大半早于智能体存在:给每一种子智能体角色自己声明的工具白名单,按那份白名单铸造属于该子智能体的凭据而不是共享父智能体的,并把有写权限的角色放进单独的进程或沙箱,让「隔离」在操作系统层面也有意义。读者角色拿只读工具、不拿任何值得偷的凭据;唯一会写的那个角色拿一份窄而短命的授权,外加一个检查点。这就是把受限凭据往下多用了一层——多数团队没用到这一层——而它是本文里唯一一项改变最坏情况而不是改变平均情况的控制。
出处:隔离是一条洗白通道。
这是那个反直觉的后果,它把「isolated 更安全」这一直觉整个翻了过来。
把一次交接倒着追一遍。父智能体取了一个网页。页面里有一句写给智能体看的话。父智能体照着做,写下一份任务描述,派生了一个子智能体。子智能体在它的指令位收到这份描述——一份干净、简短、看起来很权威、不带任何历史的简报——然后执行了它。那个不可信的来源是真实存在的、在它发生的那一刻是可知的,并被这次委派销毁了。一个在父智能体处执行污点规则的派发器本会拒绝那次写;同一个派发器在子智能体处看到的是一条来自自家编排者的特权指令,于是放行。
污点追踪文献里那种「切开上下文」的构造之所以有效,恰恰因为它走的是反方向:不可信内容进入被隔离的子智能体,而它不持有任何凭据,只有一个窄的结构化值传出来。洗白出处的那个模式是它的镜像——不可信内容塑造了简报,而有能力的那个子智能体才是据此行动的那个。两者都叫「用子智能体做隔离」,而只有一个真的隔离了什么。
两条规则能让交接变诚实,而且都不需要改框架:
- 污点向下流。交接负载带上所有促成它的东西中最高的信任等级。一份源自被取回页面的简报是带污点的,子智能体继承这个标签,并由子智能体的派发器施加与父智能体本会施加的同一套参数级检查。没有这一条,每一次委派都是一次信任等级重置。
- 返回值向上流时是不可信的。子智能体的返回值是一个工具结果,不是同事的一份汇报。它是由一个读过任意内容的模型产出的,父智能体必须像对待任何其他不可信字符串那样对待它——尤其意味着,子智能体不能授权父智能体去做某件事,不管它把那条建议说得多有把握。
有必要直说,因为它和宣传口径相左:子智能体买到的是上下文的隔离,这对把一份长文档挡在父智能体窗口之外确实很有价值。它买到权限的隔离,只有在你单独把那件事建起来的情况下才成立。当一家厂商说子智能体提升了安全性,先问他们指的是两者中的哪一个,再问凭据放在哪儿。
预算和终止条件,谁都没继承。
剩下两条继承通道更便宜修,却产生了大部分运维上的痛。
- 父智能体的步数上限管不住一棵树。在编排者身上写「最多 30 步」,指的是它自己的三十轮,而每一轮都可能派生一个跑四十步的子智能体。你需要的界是整棵树的界,由一个往下传、逐次扣减的计数器来执行,而不是由一个按智能体设定的常量。
- 花费必须是一份共享的、可扣减的预算。给这次运行一份 token 与成本额度,往下传一个句柄,让每个子智能体去扣——于是一次意外的扇出退化成「子智能体们把预算用光了」,而不是退化成一张发票。按子智能体设上限是会乘起来的;一本共享的账不会,这正是循环内成本控制所依赖的那个区别。
- 截止时间必须是绝对的,不能是相对的。往下传一个墙上时钟的时刻,绝不要传一段时长,否则每一层都会重置时钟,一棵三层的树会跑到你设定超时的三倍。这和截止时间预算在 RPC 链路上描述的是同一个缺陷,而智能体树比 RPC 链路更深。
- 取消必须能传播。当用户中止或熔断开关触发时,在飞的子智能体必须停下来。如果它们握着凭据,一个被遗弃的子智能体就是一个仍在做写操作的无人监督行动者——前面两条的最坏组合。
- 失败语义要按角色声明。一个死掉的子智能体是让整次运行失败,还是返回一个父智能体必须自己去识别的部分结果?静默的部分结果,正是错误传播带着十足自信进入最终答案的路径。
把委派契约写下来,一次,每个角色一份。
解法不是一个更好的框架设置,而是每一种子智能体角色一份简短声明,像 API 那样被评审。六个字段就够了,而填写它们的过程,正是设计错误浮出水面的地方。
# one of these per sub-agent ROLE, reviewed like an API role: document_reader context: task_schema # not "fork", not "none" — a typed brief instructions: [policy.core] # inherited explicitly, listed tools: [fetch, extract] # allowlist; no writer in this role credentials: none # nothing worth stealing budget: shared_ledger # debits the run's allowance trust_out: untrusted # parent must treat returns as tool results
- 把每一个写者放进它自己的角色,配它自己的凭据。多数系统恰好只需要一个,而发现这一点通常是你能拿到的、对影响半径最大的一次单项削减。
- 把契约和派生事件一起记下来。角色、剩余预算、传入的信任标签、工具白名单。没有这些,功劳分配无从下手,因为你事后重建不出一个子智能体当时被允许做什么。
- 要测契约,不只测行为。「读者角色够不着任何写工具」这样一条断言,是一个会随着提示词变化继续通过的单元测试;而「盼着它够不着」不是。
具体地,按这个顺序来:给每一种子智能体角色声明一份工具白名单并用测试断言它,让权限不再被意外继承;用一份带约束与信任标签的类型化简报替换散文式交接;再给这次运行一本共享预算和一个绝对截止时间,由每个子智能体去扣减。之后——也只有在之后——再去调上下文继承,因为对话记录那个设置出现在你的账单上,而权限那个设置出现在你的事故报告里。该带着离开设计评审的那个问题,就是整篇文章的浓缩版:当这个子智能体被它正在读的那份文档说服时,它究竟够得着什么?如果答案是「和父智能体一样的东西」,那你什么也没委派出去——你只是又多了一个自己的副本。