agentic-revisit-decision-plan.md 18 KB

AGENTIC 模式 Planner 重访决策增强计划

文档目的:增强 AGENTIC 模式下 Planner 的重访决策能力,让"用户在节点输出中表达新需求""编译/校验失败需要重做""业务需要重新执行节点"等场景能被 Planner 正确识别并主动跳回前序节点,而不是机械地走向 output 终结。

改造范围

  • 后端:PlannerService prompt 重构 + AgenticExecutor 死循环防护策略调整
  • 不动 clarify 通道、不动 WorkflowPause 表、不动节点执行器

状态:待用户确认

关联文档docs/plans/agentic-workflow-plan.md(AGENTIC 模式原始实施计划,已实施完成)


1. 用户需求复述

1.1 触发场景(用户原话)

我执行了"自由跳转测试"工作流。我在智能操作节点的 clarify 工具调用时,表达我还有新需求。我本意是想让 planner 在节点运行结束后再次执行节点,处理我新增的需求。但 planner 直接选择了结束。

1.2 现象

  • 用户在智能操作节点(HermesSmartActionExecutor / Hermes Agent 内部)通过 clarify 工具表达"还有新需求"
  • Hermes Agent 接收 answer,继续内部 LLM loop,最终 final_response 中包含"用户有新的需求:XXX"字样
  • 节点 output 进入 currentVars
  • Planner 决策时看不到 clarify Q&A 过程,但能看到节点 output
  • 结果:Planner 选择 output 节点 → 运行结束,新需求未被处理

1.3 用户反馈(修订要点)

  1. Planner 不需要看 clarify 过程,节点 final_response 已包含"用户有新的需求:XXX"等信号,让 Planner 看节点 output 即可
  2. 重走 ≠ 重试:Planner 主动决策的重走(如新需求、编译失败回退)是合法的运行,不应让"重试次数"增加,也不应触发死循环兜底
  3. 弱化规则 3:"避免在同一节点上反复横跳(已访问次数多的节点优先级降低)"过于压制合理重访

2. 现状梳理(根因 + 链路)

2.1 clarify 真实链路(说明为何不需关注)

clarify 是 Hermes Agent (Python Bridge) 内部 LLM loop 的事件

  1. Hermes Agent 内部 LLM 决定调 clarify 工具
  2. Python Bridge 推送 clarify_requestHermesBridgeClient.java:442-464 转发前端 + ClarifyHooks.java:40-84 落库
  3. 用户提交答案 → HermesBridgeClient.java:599 submitClarifyAnswer → Bridge → 唤醒 Hermes Agent loop
  4. Hermes Agent 继续 loop,最终产出 final_response
  5. final_response 作为节点 envelope.data → 进入 currentVars(带 {nodeId}__ 前缀)

关键事实:用户在 clarify 中表达的新需求,最终会出现在节点的 final_response 中。Hermes Agent 本身的 system prompt 会让它把"用户表达的新需求"作为 output 的一部分回传(如"用户有新的需求:XXX")。

结论:无需新增 clarify → Planner 通道。Planner 只需正确解读节点 output 即可。

2.2 Planner 当前感知的上下文

来自 PlannerService.java:120-187 buildPrompt

  • 当前节点 id + 已访问次数
  • failureContext(仅 lastFailed=true 时附加)
  • currentVars 预览:Top-30 条,每条值截断到 MAX_VALUE_LEN=200 字符PlannerService.java:31
  • 可访问节点清单(id / type / label / 已访问次数)

2.3 当前阻碍重访的三道墙

阻碍 位置 表现
决策规则 3 PlannerService.java:177-179 "避免在同一节点上反复横跳(已访问次数多的节点优先级降低)" — 直接在 prompt 层压制重访
死循环兜底 AgenticExecutor.java:54-56 + 122-130 MAX_SAME_NODE_REVISITS=5:同节点累计访问 6 次直接 abort,不区分业务重走 vs 无意义横跳
节点 output 截断 PlannerService.java:31 MAX_VALUE_LEN=200:节点 final_response 中"用户新需求"详细描述可能被截断

2.4 根因总结

  1. Planner prompt 规则 3 在语义上压制重访,Planner 即便看到"用户新需求"也不敢重走
  2. MAX_SAME_NODE_REVISITS=5 把"业务重走"和"无意义横跳"一锅端,开发者担心死循环就把阈值设得太严
  3. 节点 output 截断到 200 字符可能丢关键信号

3. 跳回前序节点的典型场景(用户确认已基本覆盖)

按"触发主体"分类:

类别 场景 是否本次范围
A. 用户主动表达 A1:节点 output 中含"用户新需求"(本次核心 ✅ 本次
A2:userInput 节点多轮交互(AGENTIC 已天然支持) 已支持
B. 业务回退 B1:编译/校验失败,需要回到前序节点重新生成 ✅ 本次(不区分对待)
B2:依赖缺失,回前序节点补全 ✅ 本次(不区分对待)
C. 节点自声明信号 C1:节点声明 requestRevisit=true 后续扩展(本次不做)
D. Planner 自主判断 D1:Planner 看 currentVars 空字段主动回 userInput ✅ 本次(顺带受益)
E. 外部信号 E1:定时器、人工干预 ❌ 超出范围

本次设计原则:所有"业务驱动的重走"统一视为合法行为,与"网络重试(maxRetries)"严格分离。


4. 最终方案

4.1 改动一:弱化 Planner prompt 规则 3

位置PlannerService.java:177-179(buildPrompt 中的规则段)

当前规则

3. 避免在同一节点上反复横跳(已访问次数多的节点优先级降低)

改为

3. 重访已执行过的节点是允许的,但必须基于具体理由。典型合理理由:
   - 上一节点 output 中出现"用户新需求""需要修正""请重做"等明确信号
   - 编译/校验失败,需要重新生成
   - 依赖数据缺失,需要补全
   无理由的反复横跳(A→B→A→B→A...)才被压制。

配套:在 prompt 中增加"上一节点 output 摘要"段(独立于 currentVars 预览),让 Planner 直接看到当前节点产出,无需从 currentVars 反推。

4.2 改动二:放宽 MAX_VALUE_LEN

位置PlannerService.java:31

当前private static final int MAX_VALUE_LEN = 200;

改为private static final int MAX_VALUE_LEN = 500;

理由:节点 final_response 中"用户新需求"详细描述常超过 200 字符,500 字符能覆盖大部分场景,对 Planner token 占用也可接受(30 * 500 = 15k 上限,远低于现代 LLM 上下文窗口)。

4.3 改动三:语义重构 —— 区分 retry vs revisit,移除 MAX_SAME_NODE_REVISITS 硬 abort

4.3.1 语义模型

AGENTIC 模式下"节点再次执行"有两类完全不同的语义:

类型 触发主体 触发原因 计数策略 abort 行为
retry(失败重试) 节点执行器内部 attempt 内抛异常 / 业务返回 FAILED 计入 attempt(已有,单 visit 内 maxRetries 上限) attempt 用尽后整个 visit 标记 FAILED,交 Planner 决策
revisit(计划重走) Planner LLM 节点成功完成后,Planner 主动决策"再来一次" 不计入任何 abort 计数器;属于"计划的一部分" 不触发 abort

关键洞察(用户原话)

如果是重试,增加重试次数;如果是重走,不仅不应该增加重试次数,反而应该清零,因为这是计划的一部分。

典型合法 revisit 场景

  • ReAct 模式:根据上一节点 output 调整实现,可能需要 20+ 次重走,每次都是"计划的一部分"
  • 用户新需求:clarify 中用户追加需求,节点 output 标记"用户有新的需求",Planner 决策重走
  • 编译/校验失败回退:节点 A 产出不合格,回到 A 重新生成
  • 依赖补全:B 节点发现 A 的产出缺失字段,回 A 补全

这些场景下,visit 链中同节点出现 N 次是正常的,不应该被 abort。

4.3.2 死循环防护策略

放弃"同节点访问次数"作为死循环指标,改用多维防护:

防护层 触发条件 行为 备注
L1 总量兜底 sortOrder >= MAX_VISITS=50 abort 绝对兜底,任何场景都生效
L2 单 visit 重试 attempt > maxRetries(默认 3) visit 标记 FAILED,交 Planner 不变,沿用现有逻辑
L3 无进展检测(可选增强) 同节点连续 3 次访问,且 currentVars hash 完全相同 abort 本次不做,留作后续扩展

为什么 L1 单独够用

  • 真正的死循环(A↔B 反复横跳无变化)会快速耗满 50 次总量
  • 合法的 ReAct 模式虽然多次重走单节点,但总量仍在 50 内
  • 50 次的上限对单 run 的 token 消耗是可接受的硬约束

为什么不做 L3

  • 检测"无进展"需要维护 visit signature 历史,增加复杂度
  • "无进展"的语义边界模糊(部分场景下相同 input + 不同 output 也是合法的)
  • 用户原意是"重走即合法",L3 与原意存在张力
  • 留作后续扩展点(基于实际运行数据再决定是否需要)

4.3.3 具体改动

位置AgenticExecutor.java:55-56 + 122-130

删除

// 第 55-56 行
/** 同节点连续访问 5 次仍找不到 Output,强制终止 */
private static final int MAX_SAME_NODE_REVISITS = 5;

// 第 122-130 行
int revisits = nodeVisitCounter.merge(currentNodeId, 1, Integer::sum);
if (revisits > MAX_SAME_NODE_REVISITS) {
    String msg = "节点 " + currentNodeId + " 连续访问超过 " + MAX_SAME_NODE_REVISITS
            + " 次,疑似死循环";
    log.warn("[Agentic] runId={} {}", runId, msg);
    return abort(context, emitter, runId, msg, sortOrder, currentNodeId,
            fromVisitUuid, fromNodeId, currentVars);
}

保留

  • nodeVisitCounter 仍维护,但仅用于:
    • iterSeq 字段(同节点第几次访问,写入 Visit 表用于前端展示)
    • Planner prompt 中"已访问 N 次"信息展示(让 Planner 自主判断)
  • 不再作为 abort 依据

注释更新

// nodeVisitCounter 仅用于 iterSeq 计算与 Planner prompt 信息展示
// 不再作为死循环 abort 依据 —— 业务重走(Planner 主动决策)是合法行为
// 死循环防护改由 MAX_VISITS=50 总量兜底
// 详见 docs/plans/agentic-revisit-decision-plan.md §4.3

4.4 改动四:Planner prompt 增加"上一节点 output 摘要"段

位置PlannerService.java:120-187 buildPrompt

新增段落(在 currentVars 预览之前):

上一节点({currentNodeId})的输出:
{effectiveResult.output 截断到 800 字符}

工作区变量(部分,按访问顺序累积,同名变量最近覆盖):
{currentVars 预览}

实现AgenticExecutor.java:228(Planner 调用前)把 effectiveResult.getOutput() 传给 PlannerService。

理由

  • Planner 通过 currentVars 反推"上一节点产出"需要先 strip {nodeId}__ 前缀,认知负担重
  • 独立段落直接展示原 output,更易识别"用户新需求"等信号
  • 800 字符足以覆盖大部分 final_response(截断阈值与 MAX_VALUE_LEN 解耦,因为 output 是核心信号)

4.5 不改动的部分(明确列出)

  • ❌ WorkflowPause 表结构不动(不加 answer_text 字段)
  • ❌ WorkflowController.resume 不动
  • ❌ HermesBridgeClient.submitClarifyAnswer 不动
  • ❌ ClarifyHooks 不动
  • ❌ 节点执行器不动(SmartAction / HermesAgent 等)
  • ❌ 前端不动

5. 实施步骤

步骤 文件 行号 内容
1 PlannerService.java 31 MAX_VALUE_LEN = 200500
2 PlannerService.java 57 decideNext 签名加 Map<String,Object> lastNodeOutput 参数
3 PlannerService.java 120-187 buildPrompt 加"上一节点 output 摘要"段(截断到 800 字符)
4 PlannerService.java 177-179 规则 3 改写为"基于理由判断"版本
5 AgenticExecutor.java 55-56 删除 MAX_SAME_NODE_REVISITS 常量
6 AgenticExecutor.java 122-130 删除 同节点访问次数 abort 兜底逻辑;保留 nodeVisitCounter 用于 iterSeq 与 Planner prompt
7 AgenticExecutor.java 228-237 Planner 调用前传入 effectiveResult.getOutput()
8 PlannerServiceTest.java - 加测试:lastNodeOutput 包含"用户新需求"时 prompt 含该文本
9 AgenticExecutorTest.java - 更新原"死循环兜底"用例:移除 MAX_SAME_NODE_REVISITS 相关断言;保留 MAX_VISITS=50 兜底用例
10 AgenticExecutorTest.java - 加测试:同节点连续 revisit 10+ 次仍正常运行(验证业务重走合法)

6. 风险评估

风险 等级 缓解
Planner LLM 仍选择 output(不理解"新需求"信号) prompt 中给出明确示例;规则 3 已改写不压制重访;MAX_VISITS=50 终极兜底
移除 MAX_SAME_NODE_REVISITS 后真正死循环耗满 50 次浪费 token L1 总量兜底仍生效;实际运行中真死循环很少见;可基于运行数据后续加 L3 无进展检测
ReAct 模式下 token 消耗显著上升 中-高 设计预期内的成本,由用户主动选择;可后续加 run 级 token 上限
Planner 理解错信号,无意义重访拉爆 token 低-中 规则 3 改写后仍保留"无理由横跳压制"语义;reason 中要求给出理由
800 字符 output 摘要仍不够 实际场景中"用户新需求"通常在 output 开头,800 字符足够
lastNodeOutput 为 null(节点失败时) PlannerService 端 null 检查,段落降级为"(节点无输出)"
历史已运行的 Visit 不受影响 本次只改 Planner 决策与上限阈值,Visit 数据结构不变
移除常量后老测试用例失败 实施时同步更新 AgenticExecutorTest 中 MAX_SAME_NODE_REVISITS 相关用例(步骤 9)

7. 测试用例(验证目标)

7.1 单元测试

文件 用例 期望
PlannerServiceTest lastNodeOutput 含"用户新需求:补充 XXX" 时 prompt 文本含该字符串;规则 3 文本已更新
PlannerServiceTest lastNodeOutput 为 null 时 prompt 段落降级为"(节点无输出)",不抛异常
PlannerServiceTest MAX_VALUE_LEN=500 截断验证 超过 500 字符的 var value 被截断到 500 + "..."
AgenticExecutorTest 同节点连续 revisit 10+ 次(每次 Planner 都返回同节点) 运行正常,不 abort(验证业务重走合法)
AgenticExecutorTest 总 visit 数达到 50 次 abort,errorMsg 含"超过最大访问次数"(MAX_VISITS 兜底仍生效)
AgenticExecutorTest 单 visit 内 attempt 用尽 visit 标记 FAILED,Planner 接管(retry 逻辑不变)

7.2 手工验证(用户操作)

执行"自由跳转测试"工作流:

  1. 在智能操作节点的 clarify 中输入"还有新需求:补充 YYY"
  2. Hermes Agent 完成节点,final_response 应含"用户有新的需求:补充 YYY"
  3. 观察后端日志:Planner prompt 应含上一节点 output 摘要 + 规则 3 新版本
  4. 期望 Planner 决策为重访该智能操作节点
  5. 第二次执行该节点处理"补充 YYY"
  6. 最终到 output 节点结束

7.3 边界验证(可选)

构造一个 ReAct 风格工作流,强制 Planner 在同节点重走 15+ 次:

  • 期望:运行正常完成,不触发任何 abort
  • 验证:nodeVisitCounter 不再作为 abort 依据

8. 与原计划的差异(修订点对比)

维度 初版方案(已撤销) 二版方案(已撤销) 最终方案
信号通道 新增 clarify Q&A 注入 Planner 同左 删除。Planner 看节点 output 即可
WorkflowPause 表 answer_text 字段 同左 不动
WorkflowController.resume 写入 answer_text 同左 不动
Planner prompt 规则 3 改为"用户意图豁免" 改为"基于理由判断" 同二版
MAX_VALUE_LEN 200 不变 200 → 500 同二版
MAX_SAME_NODE_REVISITS 5 不变 5 → 10(放宽) 完全移除该常量与 abort 逻辑
retry vs revisit 语义区分 新增:retry 计 attempt 内;revisit 不计入任何 abort 指标
新增"上一节点 output 摘要"段 新增(800 字符) 同二版
死循环防护层 L1 + L2 + MAX_SAME_NODE_REVISITS 同左 L1(MAX_VISITS=50)+ L2(maxRetries);L3 无进展检测留作扩展
改动文件数 6 个 3 个 3 个(PlannerService + AgenticExecutor + 对应测试)

9. 后续扩展点(不在本次范围)

  1. 节点声明 control 信号:envelope.data 增加可选字段 __control__,节点 LLM 主动声明 requestRevisit=true,Planner 直接消费
  2. Planner reason 监控:累计统计每次 Planner 决策的 reason,识别"业务重走"模式用于优化 prompt
  3. MAX_SAME_NODE_REVISITS 自适应:按节点类型差异化(如 llm 节点容忍度高、skill 节点容忍度低)
  4. 重走原因可视化:前端 Visit 卡片展示 Planner 决策原因(已持久化在 decisionJson,前端可读)

10. 待用户确认

  • Q1:MAX_VALUE_LEN 从 200 放宽到 500,是否同意?
  • Q2完全移除 MAX_SAME_NODE_REVISITS 常量与 abort 逻辑(不再用同节点访问次数作为死循环指标),死循环仅靠 MAX_VISITS=50 总量兜底,是否同意?
  • Q3:新增"上一节点 output 摘要"段(截断 800 字符),是否同意?
  • Q4:规则 3 改写为"基于理由判断"版本,是否同意?
  • Q5:本次完全不动 clarify 通道 / WorkflowPause 表 / 节点执行器,是否同意?
  • Q6:L3 无进展检测(同节点连续 3 次访问且 currentVars hash 相同 → abort)作为后续扩展,本次不做,是否同意?

等待用户确认是否按本最终方案推进实施。