【免费下载链接】rp-engine
YAML-native agent workflow execution engine, written in Rust
🔍 在rp-engine(Rust 编写的 YAML 原生 Agent 工作流执行引擎)中,循环工作流最大的隐患是:节点每跑一轮,上一轮的结果就被覆盖,历史彻底丢失。rp-engine 用一套append-only(只追加)执行状态 + short ID 引用的设计化解了这个问题——每次执行都留一条带短 ID 的记录,配合max_iterations循环保护和可序列化的 CheckpointState,让循环既安全又可追溯。本文将带你完整读懂这套机制。
一、循环工作流为什么需要特殊的状态设计?
先想象一个典型的"思考-自我修正"循环:
think节点每轮产出thought、needs_more;- 条件边
think → think在needs_more == true时回跳,直到得出最终答案。
如果状态是"节点 → 一份输出"的字典,那么:
| 问题 | 后果 |
|---|---|
| 覆盖历史 | 第 2 轮的输出会顶掉第 1 轮,模板里想引用"之前所有思考"就无从谈起 |
| 无法定位某一轮 | 调试时无法回答"第 1 轮到底输出了什么" |
| 死循环风险 | 没有边级别的计数与上限,条件永远为 true 就会烧穿 token |
rp-engine 的 tests/fixtures/loop.yaml 就是一个标准案例:think节点的variables里同时用了
history: "think[*].thought" # 所有轮次(数组) task: "start.task" # 工作流输入而输出里all_thoughts: "think[*].thought"要把每一轮的思考原样带回。这要求状态必须记住每一轮,这正是 append-only 的由来。
二、ExecutionState:一张"只增不改"的执行台账
核心实现在 src/state/execution_state.rs。可以把ExecutionState理解为一本台账,关键字段各司其职:
node_executions:节点ID → Vec<ExecutionRecord>,每个节点的执行历史按时间顺序追加;latest_execution:节点ID → 最新 short_id,"取最新值"的快捷索引;edge_counts:边(from, to) → 已走次数,循环保护的计数器;dead_nodes/dead_edges:被判定为不可达的节点与条件为 false 的边,用于 dead path 检测;short_id_counter:全局递增的短 ID 发号器。
每一条ExecutionRecord(L18-L42)记录了 4 样东西:
short_id:本次执行的短 ID(如a、b、z);execution_index:该节点的第几次执行(0、1、2…);outputs:本次执行的全部输出字段;started_at/timestamp:开始与结束墙钟时间戳(毫秒),二者相减就是该次执行的权威耗时,直接供前端时间线/甘特图渲染。
写入统一走write/write_with_start(L203-L249):生成短 ID → 构造记录 →push 进历史数组→ 更新latest_execution。注意它只有 push,没有任何"修改已有记录"的接口——这就是 append-only 语义的落点。
🛡️ 还有一个防内存失控的细节:MAX_RECORDS_PER_NODE = 500,当单节点记录数超过上限时,会drain掉最旧的部分。长循环下状态体积被硬性封顶,不会 OOM。
三、short ID:base36 发号 + 跨断点不重号
短 ID 由全局计数器按base36生成(L626-L642):
0 →
a,1 →b,… 25 →z,26 →0,… 36 →ba
三个设计意图:
- 短:路径里写
counter[b]比一串 UUID 友好得多; - 全局唯一:计数器是引擎级的,不同节点、不同轮次共享同一序列,天然不会撞号;
- 断点续跑不重号:
CheckpointState里专门保存了short_id_counter(L53-L71),from_checkpoint恢复时沿用旧计数器(L156-L180)。即使工作流暂停后数小时才恢复,新记录也不会和历史 short ID 混淆。
四、路径表达式:五句话就能读完全部历史
parse_path(L573-L611)把字符串路径解析成段序列,支持的引用格式与 docs/FLOW_SPEC.md 的 "Path Expressions" 一节完全一致:
| 路径写法 | 含义 |
|---|---|
node.field | 该节点最新一次执行的field(最常用) |
node[a].field | 指定short_id的那次执行 |
node[0].field | 第一次执行 |
node[-1].field | 最后一次执行(等价于不带括号) |
node[*].field | 所有执行,返回数组 |
input.field | 工作流原始输入(特殊命名空间) |
对应的解析逻辑在resolve_segments(L401-L458)与resolve_all_executions(L496-L518)。源码内的单元测试test_multiple_executions(L683-L710)展示了完整行为:
// counter 执行了两次(count=1, count=2) state.read("counter.count") // → 2 (默认取最新) state.read("counter[a].count") // → 1 (按 short_id) state.read("counter[0].count") // → 1 (按序号) state.read("counter[-1].count") // → 2 (负数序号) state.read("counter[*].count") // → [1, 2] (全部)这套语法让 YAML 作者在不写任何脚本的情况下,就能在循环里精确选择"最新一轮""第一轮""全部轮次"的数据。
五、edge_counts 与 max_iterations:循环安全的最后一道闸
append-only 解决的是"数据不丢",循环安全还需要"次数可控"。机制在 src/engine/executor.rs:
- 每次过边先查计数:
evaluate_edges在决定是否走某条边前,先state.get_edge_count(from, to)与max_iterations比较(L1471-L1481),超限即跳过; - 过边时计数 +1:
state.increment_edge_count(...)(L1516-L1518); - 三级上限:边级
max_iterations(YAML 中配置)→ 引擎级默认(ExecutionConfig.max_iterations,默认 100,见 L264),schema 里也允许按边单独声明(src/schema/edge.rs)。
回到loop.yaml的例子:think → think的边声明了max_iterations: 3,即使模型一直回答needs_more: true,引擎最多也只放它循环 3 次,随后走is_default: true的兜底边退出——循环永远有出口。
此外,条件边求值为 false 时会被mark_edge_dead记入dead_edges,并经过propagate_dead_paths(L1546-L1588)固定点传播:所有入边都不活跃的节点被标记 dead,从而既避免无谓等待,也让引用 dead 节点且没有default的变量快速报错(详见 docs/FLOW_SPEC.md 的 "Dead Path Detection" 一节)。
六、CheckpointState:把整本台账序列化,暂停/恢复零损耗
长流程(尤其含 LLM 调用、等待人工确认的checkpoint节点)需要"存盘续跑"。to_checkpoint(L134-L153)把整份状态快照成可序列化的CheckpointState:
input:原始工作流输入;executions:全部节点的执行记录(append-only 的台账本体);edge_counts:每条边的已走次数;short_id_counter:短 ID 发号器当前值;dead_nodes/dead_edges:已判定的死路径。
恢复时from_checkpoint会用记录尾部自动重建latest_execution索引(rebuild_latest,L183-L190),并把新输入合并进input。执行侧在遇到 checkpoint 节点时会调用to_checkpoint并返回ExecutionStatus::Paused(src/engine/executor.rs),checkpoint节点执行器见 src/nodes/checkpoint.rs。
由于edge_counts、short_id_counter、历史记录三件套都随快照走,恢复后的执行与"从未暂停"在数据层面完全一致——第 N+1 轮循环引用的node[*].field依然包含暂停前的全部轮次。
七、执行结果长什么样?
run_yaml返回的ExecutionResult中,executions字段就是台账的 JSON 投影(docs/FLOW_SPEC.md "Result Schema"):
{ "status": "Success", "executions": { "think": [ { "short_id": "a", "execution_index": 0, "outputs": { "...": "..." }, "timestamp": 1776308363051 } // ……循环节点会有一条记录对应一次执行 ] } }也就是说,前端只要拿到这一个 JSON 就能渲染完整的执行时间线——这正是ExecutionRecord里保留started_at/timestamp的用武之地。
八、新手落地清单:写循环工作流时的 5 个要点
- 默认引用取最新:
node.field永远指向最新一轮,多数场景无需写选择器; - 要"历史上下文"用
[*]:如history: "think[*].thought",让模型看到完整轨迹; - 给回跳边加
max_iterations:不信任任何"一定会收敛"的条件; - 兜底边用
is_default: true:确保计数用尽后仍能离开循环; - 调试时按 short_id 回溯:
node[a].field可精确钉住某一次执行。
写在最后
rp-engine 的ExecutionState用三个极简决策撑起了循环数据流的全部安全:append-only 让历史永不丢失,base36 短 ID 让任意一轮可寻址,边级计数 +max_iterations让循环必然终止——再加上可完整序列化的 CheckpointState,暂停/恢复只是台账的一次"落盘"。读懂 src/state/ 这一个模块,你就掌握了这套引擎状态语义的钥匙。
【免费下载链接】rp-engine
YAML-native agent workflow execution engine, written in Rust
相关推荐
Requests 库 Cookie Jar 深度解析:RequestsCookieJar 如何让 Session 记住登录状态
Requests 库 Cookie Jar 深度解析:RequestsCookieJar 如何让 Session 记住登录状态 本篇技术指南以 Requests
人工智能AI 应用AI AgentRuView Evidence Engine 深度解析:为物理感知而生的 Append-Only 精度账本(ADR-304 与 `ruview-evidence` 实现)
RuView Evidence Engine 深度解析:为物理感知而生的 Append Only 精度账本(ADR 304 与 ruview evidence
人工智能计算机视觉物联网智能家居后端嵌入式OpenEMS ESS 循环控制器状态机(Controller Ess Cycle)深度解析:充放电循环与容量测试的完整状态流转
OpenEMS ESS 循环控制器状态机(Controller Ess Cycle)深度解析:充放电循环与容量测试的完整状态流转 导读 本文以 OpenEMS
后端前端物联网边缘计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考