适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
crates/cheng-nodes/src/nodes/builtin/tools/sub_flow.rs、crates/cheng-nodes/src/nodes/builtin/tools/batch_sub_flow.rs、crates/cheng-core/src/domain/loop_container.rs、crates/cheng-core/src/domain/container.rs
在图里跑图有两种不同的方式,它们解决的是不同的问题:
| 子流程节点 | 循环容器 | |
|---|---|---|
| 跑什么 | 另一个工作流,按 id 或路径 | 画在本工作流内部的子图 |
| 复用 | 可跨工作流共享 | 仅限本工作流 |
| 节点 | tools/subflow、tools/batch_subflow |
画在画布上的容器 |
| 何时用 | 被调方本身是一个独立单元 | 只是要对每个 item 重复本地的几步 |
被调方有自己的身份时用子流程;只是需要对每个 item 重跑同样那几个节点时用循环。
子流程节点
tools/subflow 通过与 API 相同的 Executor 端口去运行另一个工作流——它不重新实现执行逻辑——并返回结果。
| 输入 | 含义 |
|---|---|
workflow_path |
工作流 UUID,或 JSON 工作流文件的路径 |
input_data |
交给子工作流的数据 |
async_execution |
true 表示子流程一启动就返回;false(默认)等待完成 |
timeout_seconds |
默认 300,范围 1–3600 |
max_parallelism |
子工作流内部的节点并行度,1–100 |
response_path |
点路径(如 result.answer),用于从子工作流输出中提取 response |
context_path |
点路径,用于提取子上下文 |
return_child_context |
返回子工作流更新后的上下文(默认开) |
return_node_outputs |
在 result.outputs 中包含子节点输出(默认开) |
child_context_max_bytes |
内联上限,默认 100000;超限的上下文改为在诊断信息里给出摘要 |
输出为 status、agent_context、elapsed_ms、summary 和 error。在画布上折叠时,节点只显示 status 和 summary。
response_path 和 context_path 值得设置。不设时,节点会用启发式方法在子工作流的各节点输出里找「答案」;设了之后,提取就是精确的。
上下文优先级
有三个输入都能提供子工作流的 context,优先级最高的生效:
1. child_context ← 上一次子流程调用返回的权威后续状态
2. agent_context ← 上游路由(通常从条件路由节点连过来)
3. input_data.context ← 手动透传
正是这个次序让多轮委派能够成立:父流程把子流程返回的上下文在下一次调用时传回去,而这份状态压过路由层提供的任何东西。
conversation_id 由父执行作用域环境注入,因此子工作流与父流程共享同一个会话,用于回合生命周期、审批以及依赖历史的工具。
递归防护
每次调用都携带一个调用栈。两道防线,都是硬失败而非警告:
- 深度上限 10。 超出即中止,并在错误信息里打印调用栈。
- 成环检测。 若
workflow_path已在栈中,调用被拒绝——「Recursive workflow call detected」——并打印调用栈。
推荐的默认嵌套深度是 5;10 是绝对上限。
批量子流程
tools/batch_subflow 并发地运行多次子流程调用。每个 item 都可以指定不同的工作流和不同的输入——除非你自己让它们相同,否则这并不是「同一个工作流跑 N 次」。
| 限制 | 取值 |
|---|---|
| item 上限 | 256 |
max_concurrency(批层) |
1–64,默认 4 |
max_parallelism(每个子工作流内部) |
1–100 |
timeout_seconds(每个 item) |
1–3600 |
max_concurrency 和 max_parallelism 是两个不同的旋钮。 前者是同时在飞的子工作流数量,后者是每个子工作流内部并行运行的节点数。二者相乘才是你真实的并发量,容量评估时该看的就是这个乘积。这些限制在运行时也会强校验,而不只是写在 schema 里,因此非法取值会在节点处失败,而不是深入执行之后才炸。
失败语义是 collect-all。 所有 item 都跑到底,然后再汇总;没有 fail-fast 选项。fail-fast 需要可取消的执行句柄,留到后续版本。容器的 status 报告为 completed、partial_failed 或 failed,另附 total / succeeded / failed 计数和逐项结果。
结果按输入顺序返回,而不是完成顺序,因此上游数据与下游结果无需额外的对齐步骤就能一一对应。
有两条实现特性会体现在行为上:批量节点内部持有一个常驻的子流程节点,因此它的工作流缓存在多次批量运行之间共享,而不是每次重新加载;以及各次迭代是在当前异步调用栈上轮询的,而不是 spawn 出去,因为工具上下文和执行上下文是借用引用。
循环容器
循环容器是一个工作流结构,不是节点。它包住一个子图,每次迭代运行一遍,并把结果汇总成容器级输出。
在外层 DAG 里,容器作为一个整体被调度,由一个合成 NodeId 引用,该 id 从容器 id 确定性地推导而来(固定命名空间的 UUID v5)。这份确定性很重要:持久化的工作流和执行轨迹都依赖同一个容器在跨进程、跨部署时始终映射到同一个节点 id。
虚拟端口
loop/input 边界向内部节点暴露一组固定的虚拟端口。它们不是真实节点,也从不进入 DAG——引擎在每次迭代时投影出它们:
| 端口 | 含义 |
|---|---|
items |
源数组(边界输入) |
item |
当前项 |
item_text |
当前项的文本投影 |
item_id |
当前项的稳定 id(存在时) |
item_path |
当前项的路径,用于文件引用 |
index |
从 0 开始的下标 |
iteration |
从 1 开始的迭代序号(= index + 1) |
previous |
while 循环用:上一轮循环体的合并输出,首轮为 Null |
condition |
条件来源节点必须产出的布尔输出 |
模式
| 模式 | 迭代方式 |
|---|---|
forEach(默认) |
遍历绑定到边界输入的数组 |
count |
恰好 N 次,N 从边界输入读取 |
while |
条件为真则继续,在每次迭代之前检查 |
until |
直到条件为真才停,在每次迭代之后检查 |
while 的条件来源节点必须是内部图的成员并输出布尔 condition 端口。until 的条件来源节点必须位于循环体下游——而且触发停止的那一轮会被计入汇总。
并发与错误
| 设置 | 取值 |
|---|---|
| 并发 | sequential(默认),或 parallel 且 maxParallelism 在 2–32 |
| 错误策略 | failFast(默认)、continue、collectErrors |
maxParallelism: 1 会被拒绝而不是接受——它等价于串行,所以校验器要求你显式写 sequential。
并行容器若内部含有需审批的节点,必须设 allowApprovals: true;否则校验器会直接拒绝该配置,而不是在运行到一半时给你惊喜。而且不论完成顺序如何,汇总总是按迭代下标顺序定稿——并行循环产出的输出数组与串行完全一致。
嵌套有硬上限:最大深度 3,且整棵嵌套循环树在飞的迭代总数不超过 64,无论各层声明了多大的并行度。
collectErrors 是 continue 的并行版本:每次迭代都跑到自己的终态,只要有任何一次失败容器就失败,但要等就绪队列排空之后才判定,且汇总只包含成功的迭代。
目前只有 forEach + sequential + 数组汇总是完整实现的。其他组合作为向前兼容的槽位存在,执行器会带着明确的错误拒绝不支持的组合,而不是悄悄降级——因此一个配置只要被接受,它就是真的被支持。
可观测性
容器会发出 CONTAINER_STARTED / CONTAINER_COMPLETED 和 LOOP_ITERATION_* 事件,于是 100 项的批处理是逐次迭代可观测的,而不是一个不透明的节点。见轨迹、日志与回放。

暂无评论内容