返回学习路线/阶段 05 · 编排可控的工作流
LESSON 13 / 18

用状态机编排任务

让研究任务经过计划、检索、审核和交付,支持从中断处恢复。

35 分钟 · 含动手练习进阶Workflow状态机

学完这一课,你将能够

  • 定义节点输入与状态转移
  • 保存可恢复的执行快照
  • 避免重试时重复产生副作用

学习目标

  • 用状态和转移定义一个多步任务。
  • 保存执行快照,并从快照恢复。
  • 区分安全重试与可能重复产生外部影响的操作。

前置要求:工具调用、Agent 循环与 JSON。 本节用 Python 标准库写一个固定工作流,不需要编排框架。

把自由度放在真正需要的位置

不是所有任务都需要模型选择下一步。 研究简报可以先固定为“收集问题 → 检索 → 整理证据 → 审核 → 交付”。 模型只在整理证据等需要语言理解的节点工作,路线由程序控制。

状态机明确回答三个问题:现在在哪一步,进入下一步需要什么,失败后去哪里。 相比一串互相调用但没有记录的函数,它更容易恢复、观察和解释。

先设计状态

{
  "schema_version": 1,
  "run_id": "demo-001",
  "question": "报销有哪些要求?",
  "stage": "retrieve",
  "evidence": [],
  "draft": null,
  "errors": []
}

阶段名应稳定、明确,不使用含义模糊的数字。 每个节点只修改自己负责的字段;输入不满足条件时明确失败。 状态结构变化后用版本号处理旧快照,避免旧数据被新代码误读。

运行一个能恢复的工作流

保存为 workflow.py 并运行。 示例会在当前目录创建 workflow-checkpoint.json,第二次运行会读取已完成状态。

import json
from pathlib import Path

CHECKPOINT = Path("workflow-checkpoint.json")

def save(state):
    temporary = CHECKPOINT.with_suffix(".tmp")
    temporary.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
    temporary.replace(CHECKPOINT)

def advance(state):
    if state["stage"] == "retrieve":
        state["evidence"] = [{"id": "expense-v2", "text": "报销须附有效发票。"}]
        state["stage"] = "draft"
    elif state["stage"] == "draft":
        if not state["evidence"]:
            state.update(stage="failed", error="no_evidence")
        else:
            state["draft"] = "\n".join(
                f'[{item["id"]}] {item["text"]}' for item in state["evidence"]
            )
            state["stage"] = "review"
    elif state["stage"] == "review":
        # 本例是格式检查节点,不代表真实人工审核。
        state["review"] = {"citation_present": "[expense-v2]" in state["draft"]}
        state["stage"] = "completed" if state["review"]["citation_present"] else "failed"
    else:
        raise ValueError("无法推进当前阶段")
    return state

if CHECKPOINT.exists():
    state = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
    if state.get("schema_version") != 1:
        raise ValueError("不支持的快照版本")
else:
    state = {
        "schema_version": 1, "run_id": "demo-001",
        "question": "报销有哪些要求?", "stage": "retrieve", "evidence": [],
    }

while state["stage"] not in ("completed", "failed"):
    state = advance(state)
    save(state)
    print("已保存阶段:", state["stage"])
print(json.dumps(state, ensure_ascii=False, indent=2))

临时文件替换减少了写到一半留下不完整 JSON 的风险。 它不能代替生产数据库事务,也没有解决多个进程同时更新同一任务的问题。 本例只适合单进程本地实验。

重试为什么可能危险

假设节点发送邮件成功,但保存“已发送”状态时崩溃。 恢复后重复执行这个节点,就可能发送第二封邮件。 因此,有副作用的操作需要幂等键、执行记录,或能查询既有结果的外部服务。

只读查询通常更容易重试,但也要考虑查询费用和限流。 把每个节点的重试次数、超时和可重试错误写清楚。 不要用“失败后重新跑全部步骤”覆盖所有情况。

什么时候引入 LangGraph

当你需要复杂分支、持久化、暂停恢复或多节点协作时,可以考虑编排框架。 先理解状态契约,再把节点映射到框架,可以减少对隐含行为的依赖。 框架不会替你决定哪些操作可重试、哪些字段属于可信事实。

练习与验收

在保存 draft 阶段后主动结束程序,再运行一次,确认从该阶段继续。 增加一个 no_evidence 分支,确认不会生成无依据草稿。

  • 恢复后保留原始问题和证据。
  • 已完成任务不会重复执行节点。
  • 不认识的快照版本会明确报错。
  • 能解释邮件节点为何需要额外幂等设计。

延伸阅读

下一课认识 MCP,用一致的方式发现和调用外部工具。

让这一课,真正成为你的收获

完成练习后标记完成,也可以随时回来复习。

笔记与进度保存在当前浏览器,无需登录

AgentStudy · Learn by building.以理解为起点,以作品为答案