
Dynamic Workflow
编写于 2026-08-16
Dynamic Workflow 是 Peri 的多 Agent 编排机制,一段脚本描述执行计划,脚本由 LLM 在任务现场生成,执行交给独立进程并保持确定性。本文讲四件事,workflow 是什么、为什么说它是动态的、它与子代理和普通回合的边界、journal 与恢复怎么工作,最后是适用边界。
编排层的位置
workflow 是用脚本定义多 Agent 执行计划。脚本用 JavaScript 写,meta 声明名称与描述,正文调用少量原语完成编排。下面是一个最小脚本(演示示例,不是真实会话),
export const meta = { name: 'parallel-review', description: '从三个视角并行审查一段代码',}
const [security, perf, bugs] = await parallel([ () => agent('审查这段代码的安全问题:注入、鉴权、数据暴露。', { label: 'security', allowedTools: ['Read', 'Grep'], }), () => agent('审查这段代码的性能问题:N+1 查询、热点路径。', { label: 'perf', allowedTools: ['Read', 'Grep'], }), () => agent('审查这段代码的边界问题:错误处理、空值分支。', { label: 'bugs', allowedTools: ['Read', 'Grep'], }),])
return { security, perf, bugs }使用者不需要写这段脚本。对 Peri 说用 ultracode 并行审查我改过的代码,或直接输入 /ultracode,ultracode skill 载入上下文,由 LLM 把任务拆成脚本并通过 Workflow 工具提交执行。需要固定流程(比如每次合并前跑同一套审计)时,才把脚本落盘到 .claude/workflows/ 下,用 scriptPath 引用。
计划生成与确定执行
现场生成计划
脚本本身是 LLM 生成的工具参数。拆几路并行、每路 agent() 的 prompt、模型档位、allowedTools、阶段划分,都是在任务当下由 LLM 决定并写进脚本的。这里没有工程师预先写死的 pipeline,也没有工作流作者这个角色——相对 CI 里人维护的静态流水线,动作(actions)来自生成而非手写。
运行期注入状态
- 外部数据经
args在启动时注入(审查目录、commit 范围、时间戳)。 phase()在运行中切换阶段,后续agent()自动带上当前阶段名。- 每个
agent()是一次独立的 LLM 推理,各跑各的上下文,不共享状态。
固定脚本执行路径
动态指脚本内容,不指执行路径。一旦脚本定型,执行就是可重放的状态机,沙箱拦截 Date.now()、new Date()(无参数调用)与 Math.random() 这类非确定性来源,import 也被禁止。真正不可预期的只有 LLM 对每个 agent() 的回答,而它们被逐条记进 journal,成为恢复的依据。
复用子代理执行体
三种执行形态放在一起看,
| 维度 | 普通回合 | 子代理 | Dynamic Workflow |
|---|---|---|---|
| 决策位置 | 主会话上下文 | 主会话上下文(一次派发) | 脚本(LLM 生成,随后独立执行) |
| 执行体 | 主 ReAct 循环 | 独立 ReAct 循环 | 独立进程内的多个 ReAct 循环 |
| 并发 | 回合内工具并行 | 同步每轮 1 个;后台并发上限 3 | 脚本内 parallel,默认并发 3 |
| 可重放 | 否 | 否 | journal 前缀可复用 |
| 结果去向 | 会话历史 | 回传主 Agent 上下文 | 脚本变量,跨阶段显式传递 |
- 普通回合,主会话的单 ReAct 循环,动作随手在上下文里产生,无法保存、重跑、比对。适合需要连续对话的任务——确认边界、逐轮追问、反复试错。
- 子代理,主 Agent 在回合里用 Agent 工具派发独立 ReAct 循环,输出回传上下文。派发决策仍在 LLM 手里——同一任务跑两次,拆法可能不同、结果不可比。机制细节见 Multi Agent 架构。
- workflow,决策权从上下文移到脚本。脚本生成后脱离主会话的推理路径独立执行,主会话只收完成通知;流程的版本、日志、结果都在脚本和磁盘上留痕。
值得注意的复用点,workflow 的每个 agent() 最终都落到统一的 SubAgent 执行体——复用 Agent Loop、工具过滤和 token 统计,并继承主会话在 session/new 时形成的 owner snapshot。执行体是子代理基础设施,变的是决策者:普通子代理由模型在回合里派发,workflow 由脚本批量声明、runner 统一运行。
边界判据一句话,这个流程还会重跑吗? 会,就交给脚本;一次性探索,直接对话加子代理。两者互补,不互相取代。
脚本进入执行进程
脚本由 meta 字面量与正文组成。引擎抽出 meta(必须是纯字面量,不能是变量或插值),正文以函数体运行,六个原语连同 args、budget 以注入参数的方式到达脚本,
agent(prompt, opts) 调度一个子代理执行,返回输出parallel(thunks) 并发执行一组工厂函数,结果聚合成数组pipeline(items, stages) 数据项逐个顺序经过多个阶段phase(title) 切换阶段标记,供面板展示log(message) 写入面板日志workflow(ref, args) 运行子工作流(仅一层嵌套)- 不复制控制流原语。 条件分支、收敛循环这类结构是 JavaScript 自身的
if/while/for,直接在脚本里写。ultracode 文档里的七种模式(编排模式图解)多数依赖脚本语言表达力,而不是原语数量。 - 脱离 ESM。
import在脚本里会直接报错;需要外部依赖时经args由调用方注入。这既是沙箱的一部分,也让脚本与宿主进程解耦。 phase()只影响展示。 它组织的是可观测性边界,不改变执行顺序;面板按阶段归组,agent()的归属在phase()与显式opts.phase之间取运行时当前值。- 失败语义。
agent()返回 dead 或抛错时整体重试一次;用户取消(killed)与中断(interrupted)直接记为失败,不做重试。parallel/pipeline单项失败返回null,不中断整轮;是否接受单项失败,由脚本作者在结构里决定。
执行架构三段式,
- 进程,runner 拉起独立 Node.js 进程跑引擎,与 Peri 主进程通过新行分隔 JSON-RPC 通信(stdout 走协议,stderr 留给日志)。脚本在启动时写盘到
.claude/workflow-runs/<run_id>/script.js。 - 回调,脚本里每次
agent()经 RPC 回到 Peri,WorkflowAgentExecutor 解析 agentType 定义、模型档位、工具白名单,装配完整中间件链,跑一次 ReAct 循环,返回结构化结果(输出、token、模型、阶段、耗时),同时经进度通道实时上报。 - 投递,Workflow 工具是 fire-and-forget——调用立即返回
run_id,主会话不阻塞;完成时结果经会话消息流通知,快照写state.json。
日志支撑恢复
一次 run 的全部证据落在 .claude/workflow-runs/<run_id>/,
| 文件 | 内容 |
|---|---|
script.js | 本次执行的脚本副本 |
journal.jsonl | 追加式 agent 调用记录 |
state.json | 结束快照(状态、返回值、错误),原子写入 |
outputs/ | 超长输出的落盘 |
journal 每条是 { key, seq, result },key 是 prompt 与参数(剔除 label、phase 后)的 SHA256,result 是该次 agent() 的完整结果。恢复时用 resumeFromRunId 把旧 run 的 journal 载入引擎,对 agent 调用序列做逐条比较,
- 命中,
key一致,旧结果直接返回,不重新执行; - 首个未命中,视为新脚本与旧脚本在此处分歧,该点之后的 journal 失效并从该点重新执行,新结果续写。
label 与 phase 被剔除出 key,意味着改标签、改阶段名不影响命中;恢复宽容的是展示性参数,不宽容 prompt 本身。
确定性沙箱在这里才有价值,脚本能取系统时间或随机数时,同前缀重放就不可预期。需要时间戳的场景由调用方经 args 传入。
两点边界要讲清,
- resume 依赖的是**同一套 agent 调用序列(prompt 与参数一致)**的前缀复用,不是通用断点续跑。固定脚本每次展开的调用序列稳定,前缀命中率高;临时生成的脚本每次重写,拆解与 prompt 随之变化,
key会对不上——恢复对固定scriptPath的脚本才可靠,对随机生成的脚本帮助有限。 - 恢复后的执行开销与命中率没有实测数据。
磁盘按 mtime 保留最近 50 个 run,超出自动清理。
限制与边界
适合交给 workflow,
- 可拆解的独立并行子任务,扇出收益大于单次请求开销
- 同构批量,同一套规则过 N 个文件、N 个 commit,输出可比对
- 阶段质量门禁,plan → code → review → fix 各有验收
- 需要跨进程证据的流程,脚本副本、日志、快照都留在磁盘
不适合(ultracode skill 的原话语义),
- 单个 agent 就能做的小任务——直接做,不写脚本
- 需要紧密顺序对话的任务——用普通回合
- 内联做比写脚本更快的任务——脚本化是为固定与批量,不为形式完整
当前实现的硬上限(均可回溯代码),
maxConcurrency默认 3- 单 run 总
agent()调用上限 1000;parallel/pipeline单次 items 上限 4096 - 单个
agent()的 ReAct 循环默认 200 轮(agentType 定义可覆盖) - 子工作流嵌套仅一层
- token 预算按输出 token 计费,预算耗尽即停;工具参数面未设置
budgetTotal时视为不限
细节,
- 模型分层是脚本字段,审计类用 haiku、核心改写用 sonnet,写在哪就那么执行。成本策略随脚本可见、可审查,不藏在 LLM 的临时判断里。
- 监控走
/workflows面板,阶段与 agent 的实时状态、token、工具调用数,数据经 2 秒轮询拉取(拉取间隔为当前实现值,非承诺)。 - 编排模板与委托写法见 ultracode 使用建议。
相关实现
- Multi Agent 架构,workflow 的执行体从哪来,子代理的派发与并发限制
- RCRA 中断恢复逻辑,回合级中断恢复与 workflow 的 journal 恢复是两个层次
- System Prompt 设计,workflow agent 复用的冻结系统提示词机制