读完这篇,你能做什么
能实现检索、草稿、校验工作流,并通过测试确认非法跳转和超过修订预算的操作会被拒绝。
学习理由、准备要求与知识点
为什么学它
先画清任务允许怎样前进、失败后怎样处理,再决定哪几步可以让模型临场选择,流程会更容易检查。
先读这些内容
知识点清单
- 工作流与 Agent 控制权
- 固定节点与动态决策
- 显式状态数据
- 枚举路由白名单
- 节点前置条件和产出
- 合法状态转换
- 循环修订上限
- 假模型驱动流程测试
运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。
01先弄懂它是什么
有些步骤可以提前写死,有些步骤需要根据情况选择。工作流由开发者预先安排主要步骤和走向,Agent 则把部分下一步选择交给模型。两者可以一起用:先固定校验输入,再让模型在有限工具里选择,最后按固定规则验证和提交。设计时要说清每个决定由谁负责,以及程序必须强制限制哪些动作。
任务进度最好单独保存成数据,避免只能从聊天文字里猜。例如 question、evidence、draft、attempts 和 status 分别记录问题、证据、草稿、尝试次数和状态。每个节点先检查能否执行,再更新这些数据;路由只能从允许的下一节点中选择。使用枚举和白名单,就能在执行前拦住不存在或当前不允许的路径。
状态图存在通往终态的路径,并不意味着实际运行一定终止。模型可能总是选择修订,工具可能不断失败;应用还需要循环次数、截止时间和取消规则。本篇图算法检查“哪些可达状态根本没有任何终态出路”,这是结构诊断的一部分。
文中这几个词,先认一认
- 状态
- 能核对任务当前进度和事实的数据,例如已找到的证据、尝试次数和是否完成。
- 节点与路由
- 节点负责做一步处理,路由负责按规则选择下一步可以去哪里。
- 终态
- 任务已经完成、失败或取消等状态,到达后不再继续普通处理流程。
02看看它怎么工作
建立正向图表示允许转移,从起点搜索得到实际可能到达的状态。再反转每条边,从所有终态搜索,得到存在某条路径可以到达终态的状态。两集合之差,就是从起点可达但结构上没有终态出路的区域。
例如START到RETRIEVE,RETRIEVE和RETRY互相循环且没有其他边,这三个状态都没有终态出路;若RETRIEVE另有到DONE的边,则结构检查不再报无出口,但运行仍可能永远选择RETRY。区分“存在路径”和“所有策略都终止”,避免给新手过强保证。
每个节点还需要明确“拿到什么才能开始、完成后交出什么”。例如 GENERATE 至少要有一条授权证据,VERIFY 要有完整草稿,COMMIT 要有已确认的操作版本。流程图允许走到这个节点,不代表所需数据已经齐全,因此测试要同时检查路线是否合法,以及节点必需条件是否满足。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
S["START"] --> R["检索证据"]
R --> G["生成草稿"]
G --> V["确定性验证"]
V -->|"通过"| D["DONE"]
V -->|"可修复且有预算"| R
V -->|"终止条件"| F["FAILED 或 CANCELLED"]按顺序看,每一步做了什么
- 定义状态字段、节点输入产出和有限路由枚举。
- 画出正常、失败、取消及修订路径。
- 从起点做可达性检查,识别无法到达的配置节点。
- 反向从终态搜索,发现可达但无出口区域。
- 使用假模型与固定事件测试数据条件和循环预算。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · ArrayDeque.addLast / removeFirst
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。
Deque<Integer> queue = new ArrayDeque<>();
queue.addLast(start); visited[start] = true; // 入队时标记,避免循环重复入队。
while (!queue.isEmpty()) {
int node = queue.removeFirst();
for (int next : graph.get(node)) if (!visited[next]) {
visited[next] = true; queue.addLast(next); // 队列只保存尚待展开的可达节点。
}
}ArrayDeque提供先进先出的双端操作,用于广度优先搜索的待处理队列。容器不理解状态图,也不会替应用检测合法节点或终态。
入队时标记visited保证每个节点最多入队一次,循环边因此不会造成无限搜索。若出队后才标记,同一节点可能被多个前驱反复入队。
正向和反向搜索使用相同队列机制,但回答不同问题:从起点能去哪,与哪些节点能到终态。图的方向不能在构建时混淆。
Java模型只做静态结构检查,不调用模型与工具。运行时条件、网络故障和模型选择策略不在图搜索证明中,需通过状态机与预算另行约束。
代码先调用谁,再调用谁
- 构造正向与反向邻接表
- 从起点入队搜索
- 从所有终态在反图搜索
- 比较两组可达标记
- 报告结构无出口状态
04动手写一个例子
工作流可达无出口状态检测
给定有限有向图、起点和终态集合,返回从起点可达但不存在任何终态路径的节点。它不证明实际策略必然终止。Java17 Main.java内置图结构自测。
- 输入 1
0到1,1到1,终态2- 输出 1
[0,1]
- 输入 2
0到1,1到1且1到2,终态2- 输出 2
[]
正向搜索找出起点可达集合R,无关孤立节点不应被报告为当前任务堵点。所有节点和边索引先验证,防止坏配置造成数组异常。
在反向图从终态集合多源搜索得到集合T。节点能在反图被终态到达,当且仅当原图中它存在通往终态的路径。
输出R减T即可。算法检测不存在出口的强制失败区域;有出口但策略总绕圈的情况需要额外终止预算或策略证明,不能用空诊断列表代替。
为什么这样写是对的正向visited恰好为起点可达节点,反向visited恰好为能到任一终态的节点;输出每项同时满足前者且不满足后者。
两份邻接表保存每条边,两个BFS各访问节点和边至多一次。输出和访问数组O(V),不包含运行具体节点的成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
import java.util.*;
public class Main {
static boolean[] reach(List<List<Integer>> graph,Collection<Integer> starts){
boolean[] seen=new boolean[graph.size()];Deque<Integer> q=new ArrayDeque<>();
for(int s:starts)if(!seen[s]){seen[s]=true;q.addLast(s);}
while(!q.isEmpty())for(int v:graph.get(q.removeFirst()))if(!seen[v]){
seen[v]=true;q.addLast(v); // 入队时标记,每个节点最多展开一次。
}
return seen;
}
static List<Integer> trapped(int n,int[][] edges,int start,Set<Integer> terminal){
if(n<=0||start<0||start>=n)throw new IllegalArgumentException("start");
List<List<Integer>> g=new ArrayList<>(),r=new ArrayList<>();for(int i=0;i<n;i++){g.add(new ArrayList<>());r.add(new ArrayList<>());}
for(int t:terminal)if(t<0||t>=n)throw new IllegalArgumentException("terminal");
for(int[] e:edges){if(e.length!=2||e[0]<0||e[0]>=n||e[1]<0||e[1]>=n)throw new IllegalArgumentException("edge");g.get(e[0]).add(e[1]);r.get(e[1]).add(e[0]);}
boolean[] fromStart=reach(g,List.of(start)),toEnd=reach(r,terminal); // 反向搜索表示存在终态路径。
List<Integer> out=new ArrayList<>();for(int i=0;i<n;i++)if(fromStart[i]&&!toEnd[i])out.add(i);
return out; // 空结果只排除结构无出口,不保证策略一定终止。
}
static void eq(List<Integer> e,List<Integer> a){if(!e.equals(a))throw new AssertionError(a);}
public static void main(String[] args){
eq(List.of(0,1),trapped(3,new int[][]{{0,1},{1,1}},0,Set.of(2)));
eq(List.of(),trapped(3,new int[][]{{0,1},{1,1},{1,2}},0,Set.of(2)));
eq(List.of(),trapped(3,new int[][]{{0,1}},0,Set.of(1)));// 孤立2不影响当前起点。
eq(List.of(0),trapped(1,new int[][]{},0,Set.of()));
eq(List.of(),trapped(1,new int[][]{},0,Set.of(0)));
try{trapped(2,new int[][]{{0,2}},0,Set.of(1));throw new AssertionError();}catch(IllegalArgumentException expected){}
System.out.println("PASS 6 tests");
}
}这些特殊情况也要试一试
- 起点本身是终态时存在长度为零的终态路径。
- 不可达孤立节点不属于当前任务无出口报告。
- 带出口的循环仍可能在运行策略下无限循环。
进阶选读 · 后续再读:模型检查与公平性
场景需要证明所有允许运行都满足安全条件或最终结束。
做法把状态数据有限抽象,检查时序性质和反例路径;明确调度公平性与环境假设,或通过严格递减预算给出终止证明。
复杂度有限状态显式检查通常与状态图规模相关,但状态组合可能指数增长;不能把原代码行数当作状态数量。
好处与代价形式化检查能提供更强证据,却依赖抽象正确和假设完整;实际外部故障仍需集成演练。
05项目里怎么用
先用确定工作流实现可行基线,再把确实需要动态判断的节点交给模型。节点越多不自动代表更聪明,过细划分可能增加调用延迟与状态传递成本。保持每个节点的输入产出清楚,比一开始引入复杂多Agent更容易定位失败。
路由返回值限定在枚举集合,非法选择进入明确失败或补充信息分支。状态更新使用版本与可验证转换,避免并发节点互相覆盖证据或草稿。循环修订要累计全任务尝试次数,不能每次进入节点都把计数归零。
测试使用假模型产生固定路由序列,包括重复修订、未知节点、缺证据生成和取消。这样可以证明控制逻辑对已覆盖序列的行为,再将真实模型作为外部变量评估;一次自然语言回答成功不替代状态机测试。
容易踩的坑
- 把工作流与Agent当成互斥产品名称。
- 允许模型直接给任意节点名并执行。
- 图有终态路径就宣称所有运行一定结束。
- 修订节点每次重置计数,预算形同虚设。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 固定工作流 | 步骤可预测,便于验证 | 处理未知任务灵活性较低 | 明确业务流程 |
| 受控模型路由 | 能适应输入差异 | 需白名单、状态和预算 | 少量需要判断的分支 |
| 形式化状态分析 | 可找结构反例与性质问题 | 状态空间和建模成本较高 | 高复杂度或关键流程 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01Agent与工作流的核心差别是什么?
展开参考答案与推导
部分下一步决策是否由模型动态选择;实际系统可以把两者组合,不必按名称二选一。
02反向搜索从终态开始得到什么?
展开参考答案与推导
得到原图中存在路径能到达终态的节点集合,属于存在性而非必然性结论。
03状态字段为何不能只放聊天历史?
展开参考答案与推导
程序需要直接核对证据、版本、预算和终态,不能每次依赖模型从长文本推断这些控制事实。
换个情况,还会处理吗
01模型一直修订但图检查通过,怎么办?
展开参考答案与推导
图只证明存在出口,运行需严格轮数或截止时间预算;检查修订计数是否跨节点和重试保持。
02生成节点没有证据仍被执行,查哪里?
展开参考答案与推导
检查节点前置条件与路由数据,不仅检查边是否存在;合法图边不能保证状态字段满足要求。
03两个节点覆盖同一草稿怎么解决?
展开参考答案与推导
定义写入所有权或版本条件更新,冲突后重新核对状态;共享Map并行写入不构成正确工作流。
写代码试一试
01有出口循环应被报告吗?
- 输入 / 条件
1到1且1到终态2- 输出 / 预期
不属于无出口
提示 存在路径就通过此项。
展开参考答案与推导
反图从2可达1,因此1不在差集中;仍需另测实际策略是否总选自环。
02空终态集合意味着什么?
- 输入 / 条件
单节点0,无边- 输出 / 预期
[0]
提示 没有任何完成路径。
展开参考答案与推导
反向搜索起点为空,能到终态集合为空,所有从起点可达节点都会被报告。
03为何入队时标记?
- 输入 / 条件
菱形路径两个父节点指向同一子节点- 输出 / 预期
子节点只入队一次
提示 避免重复队列工作。
展开参考答案与推导
首个父节点发现时标记,后续父节点跳过;循环图也因此只处理有限节点。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能实现检索、草稿、校验工作流,并通过测试确认非法跳转和超过修订预算的操作会被拒绝。
本篇重点加练
状态数据与合法迁移是流程可靠性的核心,增加任务分支后仍要能解释每一次状态变化。
- 显式状态数据
- 合法状态转换
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
把“读取资料、生成初稿、检查引用、提交草稿”拆成固定工作流,另找一处需要动态决策的节点;说明两种控制权的判定依据。
- 提交内容
- 一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
- 怎样算完成
- 逐项回应题目要求,区分事实、假设与个人判断。
- 能用自己的例子解释适用条件,记录尚未弄懂的点。
练到的知识点工作流与 Agent 控制权 · 固定节点与动态决策
- 作业 02代码推演
定义运行状态和路由枚举,用三个合法与两个非法模型输出推演路由过程;记录每一步读取、修改的字段及拒绝条件。
- 提交内容
- 手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
- 怎样算完成
- 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
- 主动检查至少一个边界或异常条件,能解释手算与运行差异。
练到的知识点显式状态数据 · 枚举路由白名单
- 作业 03原理问答
为“节点产出已保存但完成事件尚未处理”定义一个精确状态,区分业务事实、执行阶段与展示状态,说明哪些状态组合应被拒绝。
- 提交内容
- 概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
- 怎样算完成
- 关键概念有明确边界,能用案例检验声明。
- 未获证据支持的假设单独标注。
练到的知识点显式状态数据 · 合法状态转换
综合应用
- 作业 04面试场景
审核节点收到没有来源列表的草稿,生成节点又声称已经审核。设计前置条件、产出契约和状态转换表,处理这种不一致。
- 提交内容
- 场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
- 怎样算完成
- 方案回应给定约束,列出至少一种替代方案及代价。
- 每项关键结论说明所需证据,不能核实的部分明确标注。
练到的知识点节点前置条件和产出 · 合法状态转换
- 作业 05编程实践
用假模型序列构造“连续要求修订、路由未知节点、成功退出”三组运行,写自动测试验证循环上限和终态不再执行节点。
- 提交内容
- 可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
- 怎样算完成
- 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
- 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。
练到的知识点循环修订上限 · 假模型驱动流程测试
- 作业 06排错实验
故意删去一次状态持久化或重复发送一个完成事件,在本地回放流程;提交首次违反业务不变量的步骤及最小复现输入。
- 提交内容
- 最小故障输入、复现步骤、观测记录、定位依据及回归用例。
- 怎样算完成
- 其他人可按记录复现故障,并区分现象与已验证原因。
- 定位过程有观测依据,回归用例可以再次暴露原问题。
练到的知识点显式状态数据 · 节点前置条件和产出 · 合法状态转换
- 作业 07编程实践
写一个状态快照校验器,只读取快照、不运行模型。自造缺少必需产物、含矛盾标志和合法暂停的样本,检查是否可从快照恢复合法流程。
- 提交内容
- 可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
- 怎样算完成
- 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
- 验证目标由合同确定,模拟结果不冒充真实服务证据。
练到的知识点显式状态数据 · 合法状态转换
- 作业 08排错实验
某节点修改共享状态后校验失败,调用方尝试继续执行另一节点。用最小程序检验失败迁移是否残留部分数据,并记录后续路径受何影响。
- 提交内容
- 最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
- 怎样算完成
- 故障可以稳定重现,现象与已验证原因分开。
- 观测记录足以判断声明的合同是否被违反。
练到的知识点显式状态数据 · 合法状态转换
- 作业 09面试场景
现有流程新增“人工驳回后回到草稿”,同时有旧运行正在等待审核。给出旧状态解释、新事件合法性和升级兼容的评审表。
- 提交内容
- 场景评审记录,包含约束、备选方案、未决信息和验证计划。
- 怎样算完成
- 方案回应题目约束,取舍能对应到可核验的事实。
- 给出失败或条件变化时的检查方式。
练到的知识点显式状态数据 · 合法状态转换
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
选择一个原本固定的节点改为模型决策,制定对照实验,比较成功率、调用数和失败类型,解释是否值得增加动态性。
- 提交内容
- 设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
- 怎样算完成
- 比较使用一致条件与事先声明的标准,记录失败与未知结果。
- 说明方案的适用前提、未覆盖风险及后续验证方法。
练到的知识点工作流与 Agent 控制权 · 固定节点与动态决策 · 循环修订上限
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
为“所有节点单测通过,所以完整工作流不会出现非法状态”设计一条跨节点反例轨迹,区分局部合同与组合约束。
- 提交内容
- 具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
- 怎样算完成
- 反例满足原主张所声称的前提,确实针对其结论。
- 说明一个反例能推翻什么、不能进一步证明什么。
练到的知识点显式状态数据 · 合法状态转换
- 作业 12思维拓展 · 跨域迁移
将任务状态机迁移到订单售后流程,找出可复用的状态建模规则和必须重新定义的业务不变量,不要求实现完整订单系统。
- 提交内容
- 原场景与目标场景的对应表、可迁移的机制、不能迁移的前提及验证设计。
- 怎样算完成
- 逐项核对两种场景的前提,不只比较相似术语。
- 至少一个不能直接迁移的边界有具体检验方法。
练到的知识点显式状态数据 · 合法状态转换
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读实践指南英文
LangGraph:Workflows and agents (新标签页)↗
阅读串联、路由、并行和编排者工作者模式,对比预设工作流与模型动态选择的适用范围。
- 实践参考文档英文
LangGraph:Graph API overview (新标签页)↗
重点阅读 State、Nodes、Edges 和更新规则,将业务转换表映射到图结构并控制循环。
- 深入参考文档英文
LangGraph:Persistence (新标签页)↗
阅读 checkpointer 与 store 的职责、会话作用域和内存存储限制,验证重启后是否真的可恢复。
- 深入参考文档英文
OpenJDK 17 ArrayDeque 源码 (新标签页)↗
按指定版本定位
ArrayDeque.addLast/removeFirst;对照源码分析理解实际调用边界。