22-LangGraph实战:可暂停可恢复的材料分析子流程

📅 发布时间:2026/10/1 8:31:20
22-LangGraph实战:可暂停可恢复的材料分析子流程
LangGraph 实战做一个可暂停、可恢复的材料分析子流程周五下午星河设备的一份维保服务申请卡在材料检查。合同编号已经提取出来付款回执编号却缺失。运营人员给销售留言补件然后关闭网页。周一重新打开系统时分析不应从零开始也不应因为“恢复了一个 AI 工作流”就自动完成审批更不应把旧合同的建议写到已经更新的申请上。这个看似简单的等待把 AI 子流程与企业主流程的边界全部暴露出来谁保留分析进度谁提供最新业务事实谁判断补件属于哪一版资料谁有权批准和开通第 21 篇确定了 AI 的职责它可以整理材料、提出发现和证据不拥有申请主状态。本篇把这项职责做成一个独立的 LangGraph 子流程重点验证两件事。第一程序进程真正结束后另一个进程仍能从持久检查点继续等待中的分析。第二申请资料改版后旧分析即使保存完好也不能继续代表当前申请。本地实验故意使用确定性的材料解析器代替大模型这样读者无需账号和模型费用就能重复暂停、恢复和版本冲突。模型识别质量是另一组评测不应混进持久化实验的结论。图 1职责图是本系列采用的工程分工。AI 分析只产出候选结果申请状态与开通动作仍由主流程和受信命令边界控制。可编辑 SVG。一、先定义一次分析而不是先画 Agent主流程中的业务对象是一份开通申请它有租户、申请 UUID、业务键、资料版本和状态。一次材料分析则是围绕这份申请的某个资料快照执行的一项任务。申请可以因为销售补件从资料 v1 变成 v2分析任务不能默默跟随变化否则检查点里保存的是 v1 的合同编号恢复后收到的却可能是 v2 的付款回执。两者拼成一个“材料齐全”的结论看起来格式正确实际上没有任何人审核过这个组合。关联协议因此至少包含tenant_id、application_id、material_version和analysis_id。前两个定位业务对象版本定位被分析的材料快照任务 ID 区分同一快照下的重新分析。本例把这些字段同时放进图状态与thread_idxinghe-demo/申请UUID/v1/analysis-001。thread_id是 LangGraph 检查点的游标并不是给用户展示的业务编号将多个标识组合起来是本例的约定不是 LangGraph 强制规定的格式。把版本写在标识里能减少误把 v2 补件送给 v1 线程的机会但仍不能代替数据库中的当前版本查询。应用层必须在每次恢复、每次接收结果时重新核对业务库。一个好看的关联键只负责帮助查找不能证明数据仍然有效。输入材料在实验中是一个包含contract_no的字典解析器列出合同号和付款回执号是否存在。真实项目会先有上传、病毒扫描、文档解析、OCR 或结构化输入这些步骤各自可能失败并有独立的数据保留策略。这里选择两个字段是为了把“为什么等待”和“恢复后检查什么”看清楚。把模型调用直接塞进图里的每一个节点虽然方便演示但会掩盖模型输出版本、提示词版本、超时预算、重试成本和数据归属。我们在后面的接入说明里保留模型适配点本地验收只覆盖控制流。图 2extract、human、validate是分析内部步骤。它们不直接写主流程状态。可编辑 SVG。二、图状态要能解释“现在等什么”图从extract开始。它读取当前快照形成found和missing两组字段如果无缺项就到validate生成结果。如果缺项就进入human节点调用interrupt()把缺失字段和资料版本交给调用方并等待人工补充。恢复值成为这次interrupt()的返回值节点把它合并进材料再回到extract重新计算缺项。重新计算很重要不能只相信用户说“已经补齐”应以合并后的状态为准。有些字段也可能是空字符串、空白或错误类型在真实系统里还需要字段级校验和来源验证。LangGraph 官方文档把interrupt()描述为暂停图执行、借助检查点保存图状态并使用相同的thread_id与Command(resume...)恢复。文档同时明确恢复时被中断的节点会从节点开头重新执行因此中断之前的代码会再次运行。这个细节直接决定了工程实现不能在interrupt()前发送邮件、创建 ERP 记录或把审批写入数据库然后假设恢复只从下一行开始。我们把human节点设计为仅构造提示并等待节点内没有业务副作用。LangGraph Interrupts 官方文档。关键实现很短完整可运行文件在 materials_graph.py。下面只摘出暂停与恢复的边界避免把代码片段误当成完整应用defhuman(state):supplementinterrupt({missing:state[missing],material_version:state[material_version],})return{materials:{**state[materials],**supplement}}config{configurable:{thread_id:analysis_thread_id}}graph.invoke(initial_state,config)graph.invoke(Command(resume{receipt_no:RC-2026-047}),config)这段代码里最值得注意的并不是函数名而是两次调用使用同一个配置。如果恢复时换了新的thread_id运行时没有办法定位之前暂停的状态。另一方面同一个thread_id也绝不该让两个不同申请复用否则会把一个人的补件填进另一份申请。多租户场景必须在调用图之前校验调用者可访问的租户和申请不能把字符串拼接本身当作隔离机制。我们的 CLI 只有一个固定的教学租户真正的身份认证尚未实现这一边界在 README 中明确写出。三、检查点保存的是执行进度不是全部业务事实本篇采用SqliteSaver把图检查点写入checkpoints.sqlite。另有一个business.sqlite存放当前资料版本和申请状态。两个文件故意分开图的持久化解决“分析停在哪里”业务库解决“申请现在是哪一版、处于什么状态”。真实生产环境中可以改用更适合并发和运维的持久后端但即便它们都部署在同一 PostgreSQL 集群也必须在逻辑上区分图状态与业务事实。不能因为检查点里写着material_version1就认定主业务库仍是 v1。SQLite checkpointer 的官方包说明其适用于本地开发、测试或轻量部署并提示配置严格的消息包反序列化策略。示例运行时设置LANGGRAPH_STRICT_MSGPACKtrue减少检查点数据库被篡改时的反序列化风险。这个设置不是替代文件权限、备份、数据库加密和访问审计的万能开关教学代码也没有演示生产级多进程并发写检查点。SQLite checkpointer 包说明。检查点会保存图执行所需的状态包括缺失字段、材料和当前节点。它不应该保存无关的长期敏感原文尤其不应该成为合同原件的第二份无限期副本。实际接入时可以只在图状态存文档版本 ID、受控对象存储引用和最小必要的结构化发现明确数据留存期限访问证据时再经过业务系统授权。为便于离线复现本例保存了两个模拟编号全部是虚构数据。把这种最小实验迁移到真实合同库前必须重新检查数据分类与权限。图 3补件属于同一申请和同一资料版本时恢复才有意义结果只是待业务网关复核的数据。可编辑 SVG。四、运行一次真正跨进程的暂停与恢复进入code目录安装 requirements.txt 中固定的 LangGraph 与 SQLite checkpointer 版本再运行run_acceptance.py。脚本使用subprocess分别启动 Python 进程来执行start、resume、bump而不是在同一个对象生命周期里调用两次函数。start进程关闭时图对象和数据库连接都结束resume进程重新打开 SQLite 文件按相同thread_id查到暂停的human节点。这里检验的是客户端进程退出后的检查点恢复不是检查点数据库所在主机宕机、文件损坏或分布式故障切换。python-m venv.venv.\.venv\Scripts\python.exe-m pip install-r requirements.txt.\.venv\Scripts\python.exe run_acceptance.py本机实际运行的摘要保存在 run-output.txt输出为{paused_next:[human],resumed_next:[],evidence_count:2,business_status:SUBMITTED,chapter21_recorded_without_state_change:true,stale_resume_rejected:true}paused_next表明第一次运行的待执行节点是human不是代码已经分析完成resumed_next为空表明恢复后图走到终点。证据数为二分别对应合同号和补充的付款回执号。最关键的业务观察是business_status仍为SUBMITTED分析过程完成没有隐式批准、签署、到账确认或权益开通。这里的付款回执号只是用户补上的材料字段不等于财务系统的“款项已经到账”事实。将“文档里有回执”与“财务已确认到账”混为一谈是 AI 流程里非常危险的一类偷换。本例不把生成的 JSON 宣称为真实 OCR 或模型输出。解析器只检查模拟字段存在与否不评估签署页是否真实、金额是否相符、回执能否追溯或合同主体是否一致。它验证的是控制流与关联协议。未来换成模型时建议保留相同的输入输出契约并新增评测集字段准确率、证据定位质量、漏检率、幻觉率和人工返工成本。只有这些指标和权限检查一起成立才有理由让模型参与更靠前的业务环节。图 4真正结束进程后再次加载检查点图节点可重入因此中断前没有不可重复的业务写入。可编辑 SVG。五、恢复之后为什么还要检查当前资料版第二个实验更能说明检查点和业务版本的区别。脚本在另一份临时目录中先让 v1 分析停在人工补充再把业务库里的当前资料版本改为 v2最后尝试按原 v1 线程恢复。CLI 在调用Command(resume...)之前先读业务库发现版本不一致就拒绝输出stale analysis: expected v1, current v2。旧检查点并没有神奇地消失它仍可用于调查历史甚至可以保留一段时间供审计但其结果不允许推动 v2 的申请。正确下一步是以 v2 的材料快照和新analysis_id发起分析并由当前授权人员处理。为什么不直接把 v1 线程里的合同号保留下来只重新提取改动部分因为这需要明确的增量语义哪些字段受本次修改影响旧证据与新文档是否仍可追溯人工补件是否经过重新确认。没有这种证明简单复用就是在不同版本之间拼接结论。本教程选择整次分析作废以更容易解释和验收的规则换取少量重复工作。对真实高成本模型调用可以设计字段级缓存但缓存键至少需要文档哈希、解析器版本、提示词版本和租户范围命中缓存也应当重新执行最终业务版本检查。版本检查既要在恢复之前做也要在结果送回主流程时做。两个检查之间仍存在并发窗口刚读到 v1销售就提交了 v2图随后输出了 v1 结果。因此接收结果的主流程要在一次受控命令里再次比较当前版本和状态。这里的 CLI 只覆盖恢复前检查主流程的接收门禁承接第 21、23 篇的契约不能因为本地脚本显示stale_resume_rejectedtrue就声称所有并发竞态已解决。真正接入时应在数据库事务或带版本条件的更新中完成最后校验。图 5图中列出的数值来自随文脚本的实际运行它们证明的是本地检查点控制流不是模型质量。可编辑 SVG。六、把子流程的结果交还主流程分析结果使用与第 21 篇一致的七个字段租户、申请 UUID、资料版本、分析 ID、模型或解析器版本、findings和evidence。本例发现项采用第 21 篇的枚举值NO_ISSUE其含义仅为本次字段检查未发现缺项绝非“已允许开通”。第 22 篇为了展示字段与证据来源evidence保存带field、source、value的对象第 21 篇的最小 SQLite 沙盒只接收证据来源字符串。因此它们字段名相同证据值类型并不相同。随文to_ch21_record()显式把对象转换为来源字符串验收脚本实际调用第 21 篇的Gateway.record_analysis()确认一条分析被记录、申请仍为SUBMITTED。第 23 篇则使用对象证据作为扩展门禁输入不直接调用第 21 篇的存储函数。证据来源字符串本身并未验证原文真实生产接入仍需回可信文档库查验。即便某次模型输出了approvedtrue接收方也只能把它当不可信内容不能用它替代运营审批记录。主流程收到分析结果后至少做四类检查。先对关联字段做全等比较阻止错租户和错申请回执再比较当前资料版本拒绝旧建议然后检查结果结构、证据引用和分析任务 ID防止重复或伪造提交最后才把可用的发现展示给有权限的人。记录“已收到分析建议”与“批准申请”应是两个不同动作拥有不同审计记录。模型关闭或超时时运营审核员应能直接查看资料并完成相同的人工任务不让整个客户开通中心因外部模型不可用而永久停摆。一个现实集成里主流程可能运行在 TemporalLangGraph 是独立服务。不要让一个 Temporal Activity 同步占着连接等待人补件数天。更合适的协议是主流程创建分析任务持久等待一个关联回执或期限事件LangGraph 在自己的持久检查点里等人补件完成后通过可靠投递机制提交分析结果主流程按任务 ID、资料版本和当前状态验收。投递允许重复接收端要去重。启动任务与提交主流程命令之间的失败窗口也要用第 06 篇的 Outbox/Inbox、幂等键或同等机制处理。画一条“LangGraph → Temporal”箭头并不能自动获得跨系统原子性。七、人工等待的超时、撤销和所有权官方interrupt()能让图等待外部输入应用却仍需要定义业务截止时间。销售可能永远不补付款回执也可能在补件前撤回申请。此时不能仅依赖图线程“还在等待”来判断工作是否有效。主流程拥有申请撤回和截止策略到期后可以关闭分析任务、通知人工队列或根据合同规则延长期限。图服务恢复时必须先查主流程当前状态如果申请已关闭就不得提交一份迟到的“材料齐备”建议。即便图框架允许技术上继续恢复业务上也可以拒绝使用该结果。谁来补件也需要身份校验。示例Command(resume{...})是本地教学命令直接传入回执编号既无登录态也无证据上传。生产界面必须先验证操作者与申请的关系、材料上传权限、文件来源和审计理由才能把补件提交给图。补件内容不应允许顺便携带“把状态设成 APPROVED”之类的控制指令我们在代码中仅保留当前缺失字段键。这个白名单是减少误操作的一个措施最终状态仍由主业务规则决定。人机交互的“暂停”并不意味着审核职责可以交给 UI 按钮自身。重复补件也要处理。同一个用户可能连续点击提交网络重试可能把相同补件发送两次恢复后图可能已经完成再次resume不该生成第二条业务建议或业务动作。示例没有实现对外结果投递只验证图一次恢复因此没有把它包装为端到端幂等服务。接入时要给补件请求一个稳定的请求键并给分析结果一个稳定的analysis_id由接收端在同一事务内记录已处理 ID 和对应效果。读者可以把这个要求与第 06 篇的事件去重实现作对照。图 6检查点仍在并不表示旧分析仍有效恢复前和主流程接收结果时都要核对当前资料版。可编辑 SVG。八、把故障实验写成验收标准正常路径的验收不能只看屏幕上出现一段建议。首先启动分析检查输出的next是human且缺失字段为receipt_no接着结束这一进程使用第二个进程和同一线程 ID 补件检查结果含正确申请 UUID、v1、分析任务 ID 和两条证据。然后到业务库查询状态确认仍是SUBMITTED。这一步把“分析完成”与“业务推进”清楚分离。读者如果替换了模拟解析器也应保留这些断言避免一改模型就把业务边界改丢。异常路径至少覆盖四类。资料版本由 v1 变 v2旧任务必须拒绝使用另一个申请的线程 ID不能读取本申请的检查点补件只给空白值图应继续等待而不是误报齐全解析节点或检查点数据库不可用时主流程应继续保留人工处理入口。随文脚本实际执行了第一类版本拒绝和跨进程恢复其余是交付检查清单不能写成已经自动化通过。区分“已运行”和“待集成”是工程文档的信用基础。如果把SqliteSaver放在单台开发机磁盘丢失会丢检查点如果同时有多个服务实例写同一个本地文件也会遇到部署与锁的问题。生产环境需要数据库型持久后端、备份恢复、访问控制、容量和清理策略还要演练检查点版本与图代码升级后的兼容性。LangGraph 的 Persistence 官方文档说明检查点与线程的关系但“用了 checkpointer”不是一条生产可用性证明。本篇证明的是最小原理和一个明确故障窗口不替读者完成部署方案评审。九、恢复接口本身也是一条需要治理的命令在演示脚本里任何能够运行命令行的人都可以输入resume。这便于复现却不等于企业里任何知道线程 ID 的人都可以补资料。生产接口收到补件时应该先从认证中间件取得当前身份再从业务库取得申请所属租户、客户与待办再确认此人有补件权限、待办仍有效、资料版本仍一致。只有通过这些检查才把有限的补件字段送到图。拒绝时应返回可以理解的原因例如“申请已撤回”“资料已更新请刷新页面”“您不是当前待办的候选人”并记录一次安全审计。用图检查点替代待办权限查询会让已转派的旧用户继续提交资料。检查点线程 ID 尤其不适合作为秘密令牌。它往往会出现在日志、任务消息、客服截图或追踪链路里。知道 ID 只能帮助定位状态不能自然获得读取或修改权限。多租户服务还必须把查库条件中的租户约束放进数据库查询而不是先按申请 ID 读出数据再检查返回对象的租户属性。后一种写法容易因为错误路径、缓存或日志输出泄露跨租户信息。图状态里如果保留了文档片段inspect一类运维工具也要执行同等访问控制运维排障并不自动意味着可以阅读客户合同全文。人工补件的来源也应是可追踪的。真实材料可以记录上传对象 ID、文件哈希、上传者、时间、扫描结果和被替换的旧版本而不只是一个receipt_no字符串。分析结果引用证据时应引用这些不可混淆的来源并能在权限允许时回到原件否则“证据列表”只是模型自己写的另一段话。若补件来自邮件或外部表单入口需要验证来源与签名并按申请和任务 ID 关联。销售把另一个客户的回执误传进来模型也许仍能读出一个格式正确的号码但业务系统必须在来源和关联层面拦住它。十、三种恢复失败要分别处理第一种是进程退出但检查点完好。这是本篇已经运行的路径重建图对象、打开同一数据库、使用原线程 ID 即可继续。第二种是检查点数据库不可用或损坏。此时不应凭业务库里“曾经启动过分析”就伪造图结果主流程应把分析任务标记为技术待处理保留人工材料检查路径等待数据库恢复或重新发起一条有审计的分析任务。第三种是图代码升级后旧检查点的状态结构或节点名与新代码不兼容。简单地重新运行新版图可能改变决策路径需要像第 12 篇处理长流程版本一样明确旧线程继续用旧实现、提供受控迁移或终止旧任务后创建新任务。这些失败在用户界面上不应全叫“系统错误”。进程恢复成功意味着用户可以继续原补件动作检查点损坏意味着需要人工接管资料改版则是正常业务冲突应该引导用户打开新版材料。运维人员看到的状态也需要分层WAITING_HUMAN表示业务等待CHECKPOINT_UNAVAILABLE表示技术故障STALE_MATERIAL表示结果不再适用。没有这种分类报警会把正常等待当成故障或者把真正需要修复的任务淹没在大量正常补件队列里。让每一种状态有明确责任人比给所有错误加自动重试更有效。尤其不要把“恢复”解释为自动重放所有外部调用。图节点可能重入网络调用可能已经成功而响应丢失业务系统也可能在等待期间改变。每个对外操作都要有自己的幂等键、查询接口与结果状态若远端结果未知流程应进入对账而不是因为检查点回到某个节点就再创建一条 ERP 服务记录。这个原则与第 07、10、11 篇的故障语义相同技术上的重新执行与业务上的再做一次是两件不同的事。本例把所有业务动作排除在图外正是为了让这个边界在最小代码里仍然清晰。十一、接入真实模型时还要补一套质量证据替换解析器之前先冻结一批经过人工核对的合同与回执样例给每个字段标出正确值、页码、文件版本和允许为空的条件。评测不能只问模型的总结听起来是否合理合同号提取正确但来源页错误仍会妨碍人工复核把两份合同的客户名称拼在一起即使语句通顺也应判为关联失败。应按业务风险分别看漏检与误报尤其把“缺少付款回执”误判为“材料齐备”列为高优先级错误。模型自报的置信度不能代替这些外部标注和业务门禁。然后再测流程层面的质量模型超时是否自动进入人工待办、人工补件后是否只重新分析受影响的字段、同一材料快照重复运行是否能关联到同一任务、模型版本变更是否记录在输出里。一个离线字段准确率高的模型如果平均需要两次人工纠错可能仍没有节省运营时间反过来一个只负责把证据页准确找出的模型也许比尝试全自动审批更有价值。评测指标要与这项子流程承担的职责对应不能把整个客户开通成功率直接归功或归咎于材料分析节点。最后要测非功能约束。合同可能包含个人信息和商业秘密模型适配器需要明确数据是否离开企业边界、调用日志保存多久、提示词和响应由谁可读、超时预算如何影响客户等待、模型供应方不可用时如何切回人工。若使用第三方模型还应隔离测试数据和真实客户资料。本文给出的model_versionfixture-parser-1是诚实标记输出来自固定规则不暗示任何未执行的 OCR、RAG 或模型推理。只有在质量、权限、可用性和成本都得到证据后才应把模型适配器接到生产材料链路。Workflow Thinking为什么不让分析图直接批准因为“看懂材料”与“有权承担业务后果”是两种不同能力。材料齐全可能只是形式上齐全合同签署可能尚未完成到账事实可能在财务系统里仍为未知客户套餐可能需要额外合规审核。把这些条件藏在一个 Agent 提示词里短期看减少代码长期却使审计、版本、责任和故障恢复都失去清晰边界。分析图可以使用模型做非确定性的理解主流程仍以结构化事实和授权命令维护确定的业务状态。两者相连时接口要比自然语言窄明确关联键、证据、版本、超时和失败状态。本篇的取舍是把复杂度压在最有价值的地方真实跨进程检查点、真实旧版拒绝、明确无业务副作用。没有为了“展示 AI”调用一个无法测评的在线模型也没有为了“展示工作流”把所有审批逻辑复制进图节点。下一篇将进一步处理一个关键时刻AI 输出“看起来可以开通”之后服务端怎样验证结构、证据、身份、审批有效期和业务事实最后才决定是否产生一条开通命令。参考与复现边界本文核验了 LangGraph Interrupts、Persistence 与 SQLite checkpointer 包说明代码依赖固定在langgraph1.2.12和langgraph-checkpoint-sqlite3.1.1。测试结果来自本地 Python 3.12 多进程脚本输入全为虚构编号无模型调用、无真实合同、无财务系统和生产级多节点演练。完整命令、文件结构与限制见 本篇 README。