Agent 编排 Agent:DeepSeek Harness 的子代理与工作流系统有多强

0 阅读10分钟

第14章:子代理与工作流编排——agent 如何编排 agent

系列:DeepSeek Harness 源码实战 | 进度 14/16
原文仓库:github.com/deepseek-ai…

一个 agent 能写代码、能跑命令。但如果任务太大——「重构这个模块」「同时修 10 个文件」——单个 agent 的上下文窗口会爆。解法是让 agent 编排 agent:父 agent 把子任务委派给子 agent,子 agent 独立完成后汇报结果。

dsh 用两个 Seam 实现这个能力:subagent(子代理)让 agent 委派工作,workflow(工作流)让 agent 运行编排脚本。两者都是可选能力,不属于 agent loop(智能体循环)主干。

我是怕浪猫,第 14 章。我们从 subagent 开始。14.png

14.1 subagent Seam:与 bash 不同的多提供方

subagent 与其他能力 Seam 有一个关键区别。来自子系统文档(docs/subsystems/subagent.zh.md):

与 bash 一样,它是一项可选能力。但它不同于其他能力 seam,因为同一上下文中可共存多个提供方实现,并按名称注册(ctx.subagents),而 bash 只允许一个执行器。

对比两种注册模式:

维度bash Seamsubagent Seam
注册方式单服务(ctx.shell)命名注册表(ctx.subagents)
Provider 数量一个上下文一个,重复注册报错可多个共存
注册表模式Cordis 标准 duplicate-service类似 LLM adapter 注册表
选择方式cordis.yml 选一个按名称选择

这意味着你可以同时挂载 in-process(进程内)、ACP(Agent Client Protocol,代理客户端协议)、Codex、Claude Code 等多个 subagent provider,按名称选择。

六个 Provider 包(来自文档):

Provider 包说明典型用途
dsh-subagent-spawn-in-process独立进程 spawn隔离执行
dsh-subagent-fork-in-process进程内 fork共享内存快速派生
dsh-subagent-acpACP 协议远程跨进程互操作
dsh-subagent-codexOpenAI Codex 后端用 Codex 做子 agent
dsh-subagent-claude-codeClaude Code 后端用 Claude Code 做子 agent
dsh-subagent-dsh-sdkdsh SDK 后端外部驱动

三个面向模型的 Consumer(消费方):

Consumer 包作用作用域
dsh-tool-subagent按提供方委派(主工具)全局
dsh-tool-subagent-control全局控制工具全局
dsh-tool-subagent-reportreport 返回通道child 作用域

14.2 能力声明与 fail-loud 原则

Provider 通过静态描述符公布其启动时能力。来自源码(packages/subagent/subagent/src/types.ts):

interface SubagentCapabilities {
  readonly outputSchema: boolean    // 是否支持输出 schema
  readonly depthLimit: boolean      // 是否支持深度限制
  readonly toolFilter: boolean      // 是否支持工具过滤
  readonly persona: boolean         // 是否支持人设
}

服务在 start 之前针对指定 Provider 进行校验。如果请求依赖 Provider 不具备的能力,会被明确拒绝(SubagentError('UNSUPPORTED_CAPABILITY')),绝不会被接受后静默忽略。

文档原文:

A request that needs a capability the chosen provider lacks is rejected with a typed error rather than accepted-then-ignored.

校验流程:

模型请求:启动子 agent,需要 outputSchema + depthLimit
  |
  v
检查 Provider 的 SubagentCapabilities
  |-- outputSchema: true, depthLimit: true -> 继续启动
  |-- outputSchema: true, depthLimit: false -> 抛 SubagentError('UNSUPPORTED_CAPABILITY')
  |-- outputSchema: false, depthLimit: true -> 抛 SubagentError('UNSUPPORTED_CAPABILITY')

金句:能力声明不是文档注释,是运行时契约。Provider 说不支持,服务就拒绝——不尝试、不降级、不静默吞掉。

14.3 单次启动请求

单次启动(one-shot)是 subagent 的基本操作模式。来自文档的请求定义:

interface SubagentStartRequest {
  readonly label?: string              // 显示标签
  readonly prompt: ContentBlock[]      // 子 agent 的用户消息
  readonly parent: Agent               // 父 agent(提供 cwd、谱系、深度)
  readonly outputSchema?: ObjectJsonSchema  // 输出 schema(需能力匹配)
  readonly maxDepth?: number           // 深度限制(需能力匹配)
  readonly toolFilter?: ToolRestriction  // 工具过滤(需能力匹配)
  readonly persona?: string            // 人设(需能力匹配)
  readonly signal?: AbortSignal        // 取消信号
}

每个可选字段对应一个能力 flag,文档说明了各字段的行为:

toolFilter:进程内后端将其作为 scoped(作用域化)的 tools.restrict() 应用在子 agent 创建窗口——被命名的工具从子 agent 的 prompt 中消失且拒绝执行(双向可见性),unknown-name(未知名称)会 loud validate。

persona:进程内后端将其注册为 scoped deployment:persona section,shadowing(遮蔽)部署级 persona——与部署 persona 相同的模板语义({{...}} 插值)。

parent 是必填字段。文档原文:

In-process providers derive workspace, lineage, and delegation depth from its durable session state.

signal 是就绪前后唯一的取消通道。来自文档:

signal is the canonical cancellation channel both before and after startup.

工具层构建请求后,服务在 start 之前解析分离的一次性描述符(SubagentDescriptorData),再将 ResolvedSubagentStartRequest 传给 Provider。

14.4 可继续子代理与 Activation

可继续(continuable)子代理是 subagent 的高级模式——一份持久化的子 agent 会话(Session),可跨轮次、跨进程恢复。

来自文档的定义:

可继续后台 subagent 是一份持久化子 agent 会话,至多关联一个进程内的 Activation(激活),即被重建的子 Agent 处于驻留状态的时段。

Activation 的三种状态:

状态含义followup 行为
runningAgent 有活跃轮次或正在唤醒在同一 Activation 中入队
waiting已停稳但拥有未完成 dispose 的子 Activation唤醒同一 Activation
settled完全停稳且所有子级已 disposedispose AgentHandle,移除 Activation

无 Activation 时 followup 执行冷恢复——从持久化日志重建 Agent。

文档的生命周期图:

persisted Session
  -> optional live Activation
       -> one retained AgentHandle
       -> Agent inbox as the only turn FIFO
       -> zero or more owned child Activations

关键设计:Activation 不是请求、结果、取消或 Task。它可以执行多个 FIFO(先进先出)轮次,并在其创建的后代仍在运行期间保持驻留。继续执行管理器负责 activation 准入、直接父级鉴权、实时所有权图、冷恢复与子级优先释放。

SubagentRuntime.startContinuable() 的流程:

准入之前的任何失败都会 reject 并 dispose 所有已创建的 handle,回滚 Activation 与父级所有权——不留半成品。

14.5 中断与所有权

SubagentRuntime.interrupt(targetSessionId, authority) 是唯一的公开停止操作。来自文档:

type SubagentInterruptAuthority =
  | { readonly kind: 'user'; readonly parentSessionId: SessionId }
  | { readonly kind: 'ancestor'; readonly agent: Agent }

两种鉴权方式:

  • user:携带持久化的直接父级会话 id,人类客户端呈现
  • ancestor:携带确切的在线 Agent 对象,其谱系链必须包含调用方

interrupt 的行为:

keepInbox: true 意味着未领取的待处理 inbox 工作不受影响。已被领取进入中断轮次的工作不会重新入队——中断只影响当前轮次,不丢弃排队消息。

不存在的目标(未知、一次性或已结算)以及未绑定管理器的组合是被接受的 no-op(无操作)。错误地址或不在在线祖先链中的调用方以 UNAUTHORIZED 拒绝。

金句:subagent 的安全模型不是能力令牌,是所有权图。父级拥有子级,祖先可中断后代——权限沿谱系树流动,不沿网络流动。

14.6 生命周期事件

subagent 通过 emit 事件向外部观察者公布生命周期:

事件载荷作用
subagent/startSubagentRunInfo一次接受的 run 开始
subagent/endSubagentRunEndInfo一次 run 终结
subagent/provider-addedProvider 名Provider 注册
subagent/provider-removedProvider 名Provider 注销

来自源码(packages/subagent/subagent/src/types.ts):

export interface SubagentRunInfo {
  readonly runId: SubagentRunId   // 唯一身份
  readonly provider: string       // Provider 名称
  readonly id: SessionId          // 子 agent 的会话 id
  readonly local: boolean         // 是否有本地 agent 实例
}

两个事件通过 runId 配对。文档补充了一个细节:Provider 名可能在 start 时存在但在 end 时不存在——因为可继续子代理的冷恢复不依赖 Provider 持续注册。

14.7 workflow Seam:模型编写编排脚本

workflow 让 agent 运行由模型编写的 JavaScript 编排脚本,脚本可以启动 subagent。来自子系统文档(docs/subsystems/workflow.zh.md):

工作流 seam 允许 agent 运行由模型编写、会启动 subagent 的编排脚本。

workflow 与 subagent 的关系:

模型调用 workflow 工具
  |
  v
ctx.workflowEngine.start(script, meta, args)
  |
  v
引擎在 worker thread 中执行脚本
  |
  v
脚本调用 agent("子任务描述")
  |
  v
ctx.subagents.start(...)  -- 委派给子 agent
  |
  v
子 agent 完成,返回结果
  |
  v
脚本继续,可能启动更多子 agent
  |
  v
脚本 return 最终结果

启动请求(来自文档):

interface WorkflowStartRequest {
  script: string               // 脚本正文(top-level await 允许)
  meta: WorkflowMeta           // 身份块
  args?: unknown              // 输入参数(作为 args 全局变量)
  subagentProvider?: string    // 子 agent provider 覆盖
  maxTotalAgents?: number      // 子 agent 总数上限
  parent: Agent                // 父 agent
  signal?: AbortSignal         // 取消信号
}

文档强调:metaargs 是普通 JSON 数据。引擎用 schema 校验 meta,并在任何工作开始前明确报错并拒绝无效数据。引擎绝不会通过对脚本文本求值来获取它们——这防止了脚本注入。

WorkflowMeta(工作流元数据):

interface WorkflowMeta {
  name: string                 // kebab-case 名称
  description: string          // 一行描述
  whenToUse?: string           // 何时使用
  phases?: WorkflowPhase[]     // 阶段声明
}

文档明确:phases 仅用于进度展示。phase() 调用与标题匹配供观察者使用,但不暗示任何执行结构——引擎不强制按 phase 执行。

金句:workflow 脚本是模型写的 JavaScript,不是 YAML 配置。这意味着编排逻辑是图灵完备的——但执行环境是受限的 worker thread,不是主进程。

14.8 工作流引擎:worker thread 隔离

dsh-workflow-worker-thread 是唯一的工作流引擎实现。来自文档:

一个 node:worker_threads 引擎——每个 run 一个 worker,脚本的 vm 上下文位于其中。

每个 run 创建一个新的 worker thread,脚本在 worker 的 V8 虚拟机上下文中执行。这提供了隔离:

维度主进程worker thread
事件循环完整 dsh独立
ctx 服务完整仅 agent() API
失败影响可能崩溃 dsh仅该 run 失败
资源共享run 结束即释放

终态结果(来自文档):

interface WorkflowResult {
  value: unknown               // 脚本返回值(纯 JSON 数据,null = 无返回)
  stopReason: WorkflowStopReason  // completed / cancelled / error
  error?: string               // 失败信息(非 completed 时)
}

stopReason 不是 completed 时,消费方将其映射为 isError 工具结果——不把部分输出当作成功上报。来自文档:

A non-completed reason carries the failure in error; the consumer maps it to an isError tool result rather than reporting partial output.

14.9 工作流工具:模型接口

dsh-tool-workflow 是面向模型的 Consumer。来自源码(packages/workflow/tool-workflow/src/index.ts):

export const name = 'tool-workflow'
export const inject = ['tools', 'workflowEngine', 'systemPrompt']

export interface Config {
  toolName?: string       // 模型面向的工具名(默认 'workflow')
  maxResultChars?: number  // 结果截断上限(默认 50000)
}

源码注释揭示了关键设计:

Execution awaits run.result and always disposes the run; non-completed reasons become tool errors, and background collection remains deferred.

always disposes the run 意味着即使脚本失败,worker thread 也会被清理——不留资源泄漏。background collection remains deferred 意味着后台收集的结果不阻塞工具返回。

14.10 协作模式与资源控制

三种典型协作模式:

模式 1:并行扇出

const files = ['auth.ts', 'router.ts', 'store.ts']
const results = await Promise.all(
  files.map(f => agent(`审查 ${f} 的类型安全问题并修复`))
)
return { reviewed: files, issues: results.map(r => r.value) }

模式 2:流水线

const spec = await agent("分析需求,输出测试用例列表")
const impl = await agent(`根据以下测试用例实现代码:${spec.value}`)
const review = await agent(`审查实现是否通过测试用例:${impl.value}`)
return { tests: spec.value, code: impl.value, review: review.value }

模式 3:条件分支

const analysis = await agent("分析这个 bug 的根因")
const rootCause = JSON.parse(analysis.value).rootCause

if (rootCause === 'race-condition') {
  await agent("添加互斥锁修复竞态条件")
} else if (rootCause === 'null-reference') {
  await agent("添加空值检查修复空引用")
} else {
  await agent("添加输入验证和防御性编程")
}

资源控制:

限制维度作用需要能力
maxDepth深度防止无限递归depthLimit
maxTotalAgents广度防止资源爆炸引擎级
toolFilter工具限制子 agent 可用工具toolFilter

需要对应能力 flag 匹配——Provider 不支持 depthLimit 时,请求携带 maxDepth 会被 UNSUPPORTED_CAPABILITY 拒绝。

本章小结

要点说明
subagent 多提供方同一上下文可共存多个 Provider,按名称注册
六个 Providerspawn / fork / acp / codex / claude-code / dsh-sdk
能力声明 fail-loud不支持的能力明确拒绝,不静默降级
两类子代理单次启动(Provider 组合)/ 可继续(管理器组合)
Activation 三状态running / waiting / settled
所有权图鉴权父级拥有子级,祖先可中断后代
workflow 脚本编排模型写 JS,引擎在 worker thread 执行
worker thread 隔离每个 run 一个 worker,失败不影响主进程
资源控制maxDepth 防递归,maxTotalAgents 防爆炸
三种协作模式并行扇出、流水线、条件分支

我是怕浪猫,第 14 章写完。subagent 和 workflow 是 dsh 处理大规模任务的方案——让 agent 编排 agent,而不是让一个 agent 扛所有事。

有问题评论区聊,有纠错欢迎指出。如果这篇对你有帮助,收藏起来——设计 agent 编排系统时这张模式表最好用。

下一章拆解 compaction(上下文压缩)和 goal(目标管理)——会话太长怎么办、agent 怎么记住目标。

系列进度:14/16 | 未完待续