
zcode源码解析 Day4Subagent 和 Actor 的区别——临时工与合同演员本文是 zcode 源码学习系列第 4 篇。Day 1 拆动态工作流时子代理和actor两个词混着用Day 3 讲了父子代理怎么通信——这篇把最后一块拼图补上这两个词到底差在哪。答案一句话subagent 是父代理临时叫来的帮手actor 是工作流剧本里提前写好的角色。底层是同一套机器合同完全不同。一、先说人话结论临时工 vs 合同演员想象你是个包工头主 agent手下有两种用人方式临时工subagent你干着活突然觉得搜代码这活儿太杂随手叫来一个帮手去把所有用到数据库的地方找出来。他跑完把结果递给你走人。什么时候叫人、叫来干什么全是你现场临时决定的。合同演员actor你先写好一个剧本workflow 脚本剧本里写着有三个角色写初稿的、审稿的、改错别字的。戏开演时系统照剧本把演员请上台一场一场给他们派活。谁上场、干什么、什么顺序剧本提前写死不是现场抓人。三条核心区别临时抓 vs 提前定——subagent 由父代理临场决定派生actor 由脚本声明、引擎调度对人汇报 vs 对剧本汇报——subagent 的结果给父代理给模型看actor 的结果交回脚本由剧本继续往下走干完就走 vs 可以重演——subagent 用完即弃actor 有名字、有档案journal剧本改了重拍时没改到的角色直接复用上次的片段。有个很有意思的细节可以佐证actor 本质上也是 subagentactor 的身份提示词第一句就是——You are a subagent inside a dynamic workflow run. A script created you and hands you work one ask at a time; the script — not a person — consumes what you return.你是一场动态工作流里的子代理。一个脚本创建了你一次一个 ask 地给你派活消费你返回内容的是脚本——不是人。二、Subagent临时工是怎么雇的subagent 的通用机制全部在core/src/subagent/目录。雇佣流程第一步看岗位手册profile。内置两个岗位general-purpose和explore用户还能用 markdown 自定义。profile.ts里内置岗位的定义值得看一眼exportfunctioncreateBuiltInGeneralPurposeAgentProfile():AgentProfile{return{name:DEFAULT_SUBAGENT_TYPE,description:General-purpose agent for researching complex questions...,color:blue,injectAgentsMd:true,systemPrompt:buildGeneralPurposeSystemPrompt(),tools:[*],// ← 全量工具source:built-in,};}注意tools: [*]——普通 subagent 拿的是白名单表达式岗位定义里写了能用哪些工具默认全开。工具是岗位描述的一部分跟着 profile 走。第二步父代理现场决定叫人。没有任何编排就是父代理在对话回合里调 Task 工具参数里写清楚岗位类型和任务描述。什么时候叫人完全取决于父代理模型的临场判断。第三步跑腿交差。runner.ts负责 launch → run → 把最终回复作为工具返回值交回父代理前台阻塞等待或后台跑完发通知Day 3 讲过的通知机制。任务结束这个 subagent 的历史使命就完成了——没有档案没有复跑。三、Actor剧本里的角色是怎么来的actor 只存在于动态工作流体系里诞生路径完全不同。第一步剧本里写角色。Day 1 讲过 facade APIagent()就是在剧本里签约演员constrevieweragent(reviewer,{system:你是代码评审只挑问题不修代码});constverdictawaitreviewer.ask(评审这段改动);第二步编译期就认识你。脚本提交前要过 TypeScript 编译分析analyzeScriptanalysis/sites.ts会把每个agent()调用登记为一个站点site——也就是说戏还没开演系统已经在剧本里数好了有几个角色、各叫什么。这是 subagent 完全没有的待遇临时工入职前没人知道他会来。第三步开演时造 runtime。引擎调度到某个角色有活干时driver 侧的工厂script-workflow-child-runtime.ts给这个 actor 造一个货真价实的子AgentRuntime——和 subagent 用的同一套机器。但配置的展开方式是合同制的工具面来自workflowActorToolPolicy()的减法表模型面来自workflow-actor-model.ts的三级优先级。这两份合同条款下面细说。第四步活干完记档案。每个 actor 的执行结果写进 journalSQLite 日志。这一步是 actor 和 subagent 最本质的分野actor 的名字是身份键。actor-names.ts的注释把理由说得非常直白An actor name must be unique within a run: it is the identity key an amended re-run matches its imported cache against.改了剧本重跑AmendWorkflow时系统按名字匹配导入上次的缓存——没改到的角色上次拍好的戏份直接复用不花 token 重拍。所以重名是 run 级失败DuplicateActorName因为重名会让缓存匹配产生歧义匿名 actor 合法只是永远享受不到复用。四、同一台机器不同的合同actor 的子 runtime 和 subagent 的子 runtime 是同一套AgentRuntime区别全在配置怎么定。逐条对比4.1 谁创建临场判断 vs 声明式编排subagent 由父代理调 Task 工具临时派生actor 由脚本里的agent()站点声明由engine/scheduler.ts调度上岗。前者是模型自由裁量后者是代码写死的编排。4.2 汇报对象父代理 vs 脚本subagent 的最终回复作为工具返回值交给父代理是给模型看的素材actor 的每个 ask 的返回值交回脚本是类型化的数据ask里的T剧本拿它继续往下走。这就是身份提示词里no user in this conversation的含义——actor 的对话里没有人类也不需要讨好任何读者。4.3 工具面白名单 vs 减法表这是两种完全不同的自由度。subagent 的 profile 用白名单表达tools: [*]或列出具体工具actor 用减法表——从全集里减掉会出事的那几个。workflow-actor-tools.ts的减法清单constACTOR_DISALLOWED_TOOLS:readonlystring[][ASK_USER_QUESTION_TOOL_NAME,ENTER_PLAN_MODE_TOOL_NAME,EXIT_PLAN_MODE_TOOL_NAME,CreateWorkflow,AmendWorkflow,READ_SESSION_CONTEXT_TOOL_NAME,RESOLVE_WORKFLOW_QUESTION_TOOL_NAME,];为什么减这七个源码注释给了三类根因悬挂AskUserQuestion / EnterPlanMode / ExitPlanMode 会阻塞在一个不存在的人类上——actor 是 headless 的没有人能回答turn 永远不结束递归与越权CreateWorkflow / AmendWorkflow 会让 actor 再提交一条工作流套娃编排ReadSessionContext 能越界读到父会话身份ResolveWorkflowQuestion 是替创建者回答升级问题——升级的整个意义是把判断权交还给创建工作流的那一方让另一个 actor 顺手作答等于把升级退化成演员之间互相说服。除此之外Bash / Edit / Write / 搜索 / web 全部保留——actor 就是要干活的。至于裁判不许改文件这种软约束靠 ask 的文本说清楚不做工具层面的硬限制Explore 岗位的 subagent 也保留 Bash只读同样靠提示约束——两种代理在这点上理念一致。4.4 模型面岗位配置 vs 三级优先级subagent 用什么模型写在 profile 里内置岗位有modelSelection自定义岗位写在 markdown frontmatter。actor 的模型解析是独立的一套规则workflow-actor-model.ts优先级从高到低本 run 的subagentModel用户对这一次运行的显式表态 journal 里的 resume pin这个 actor 上次实际跑的模型 父会话当前模型。展开成决策表run 选择resume pin解析结果有任意覆盖成 run 选择含 reasoning 档位无无不覆盖继承父会话当前模型无恰好等于父会话当前模型不覆盖钉的就是现在的主模型无不等于父会话当前模型覆盖成 pin 解析出的模型无畸形缺 provider 段抛WorkflowActorPinnedModelError这条规则的哲学是省略即继承显式值即替换。pin 的存在尤其精妙一个没写subagentModel的 run其子代理的缺省不是父会话此刻的模型而是这个子代理上次实际跑的模型——续拍的时候老角色继续用他熟悉的机器。4.5 生命周期一次性 vs 跨任务存续subagent 一个任务一个实例跑完即弃actor 是持久对话上下文Day 1 讲过同一个 actor 的多个 ask 排队串行上下文跨任务累积加上 journal 存档断点续跑、修订重跑都找得到他。五、总结一张表说清维度SubagentActor出生方式父代理调 Task 工具临场决定脚本agent()站点声明编译期登记驱动者父代理的模型工作流引擎scheduler汇报对象父代理工具返回值脚本类型化的 ask 返回值对话里有人吗没有但对父代理负责没有身份提示词明说 “no user”工具面profile 白名单默认[*]全集减去 7 个禁用工具的减法表模型面profile 里的 modelSelectionrun 选择 resume pin 父会话模型身份agentId用完即弃run 内唯一名字是 amend-resume 的缓存键生命周期单任务跨 ask 持久上下文 journal 存档能复跑吗不能能没改到的角色按名字复用缓存一句话收尾subagent 是父代理的雇佣兵actor 是工作流剧本里的角色。前者靠模型的临场判断后者靠脚本的声明式编排——正因为它要对剧本负责、要能重演才需要名字、档案和一份更严格的工具合同。Day 1 埋的子代理的名字是缓存键这颗种子到这里才算真正发芽。