跳转至

让 AI 工作流可靠:把随机生成关进确定性外壳

模型擅长补全开放内容,却不擅长充当数据库、任务调度器和状态机。如果把“决定下一步、修改文件、记录进度、判断完成”全部交给模型,Demo 可能很惊艳,长流程却很难复现。

我在 woke_novel 和 woke_tutor 中逐渐形成了一条原则:允许内容生成具有随机性,但让任务边界、状态变化和验收保持确定性。

先把工作流拆成状态图

不要先写 Prompt,先回答四个问题:

  1. 一次任务的最小可重试单元是什么?
  2. 每个单元读取什么,成功后必须产出什么?
  3. 哪些状态允许继续,哪些状态必须人工处理?
  4. 进程在任意时刻终止后,凭什么恢复?

woke_tutor 的任务状态只有三种:

pending → completed
        → failed

恢复位置不是一个容易失真的“当前步骤数字”,而是所有尚未完成且未耗尽尝试次数的任务集合。每个状态变化立即持久化,因此中断后不需要猜测最后一次模型调用是否成功。

woke_novel 的章节流程依赖更强,使用项目状态文件记录当前幕、轮次和最后步骤。两种模型不同,但共同点是:恢复依据来自程序可以验证的产物与状态,不来自模型的自然语言总结。

确定性逻辑留在程序里

以下任务不需要创造力,应由普通代码完成:

  • 题量和权重分配;
  • 任务 ID、目录和文件名生成;
  • 路径变量替换;
  • 状态推进和重试计数;
  • JSON Schema 与业务不变量校验;
  • 去重、权限判断、发布门禁;
  • 原子写入和日志索引。

模型只接收已经规划好的任务,并填充题干、解释、创意或正文。以题库任务为例,程序固定以下字段:

{
  "task_id": "practice_t00001",
  "kp_id": "kp_001",
  "difficulty": 2,
  "question_type": "multiple_choice",
  "interaction_mode": "adaptive"
}

模型不能改变这些维度,只返回任务所缺的内容字段。这样模型偏航时,系统能明确指出违反哪条约束,而不是得到一份“看起来不错,但数量和覆盖率都不对”的结果。

结构化输出还需要业务校验

JSON Schema 可以确认字段存在、类型正确,却无法表达所有业务规则。例如:

  • 单选题只能有一个正确答案;
  • 多选题至少两个正确选项;
  • 题干不能和题池已有内容近似重复;
  • 数学朗读不能丢掉负号或单位;
  • 作答前话术不能暗示正确答案。

因此一条可靠的接收链路应是:

flowchart LR
    M[模型原始输出] --> P[协议解析]
    P --> S[Schema 校验]
    S --> B[业务不变量]
    B --> D[去重 / 安全检查]
    D --> W[原子写入]

任何阶段失败都保留 prompt、原始响应、错误摘要和任务 ID,只重试当前失败单元。

原子写入比“模型会写文件”更可靠

让模型直接修改项目文件看似省事,却会把三个动作揉在一起:生成内容、选择路径、执行持久化。失败后很难判断究竟是内容不合法、路径错误还是工具没有执行。

woke_tutor 采用更窄的边界:模型只返回 JSON,Python 校验后写临时文件,再用替换操作提交最终文件。这样进程在写入中间终止时,不会留下半份 JSON 破坏整个题池。

对于确实需要模型操作多个文件的 woke_novel,则通过路径解析器、步骤模板和预期产物检查限制写入范围。两者的差别说明:边界应根据任务选择,但必须能被程序检查。

重试要有预算,失败要可见

无限自动重试会造成三个问题:成本不可控、同一错误被重复放大、真实缺陷被“还在运行”掩盖。

更合理的设计是:

  • Provider 或网络失败不消耗内容任务的尝试次数;
  • 内容校验失败只增加对应任务次数;
  • 达到上限后进入 failed
  • 失败原因集中写入报告;
  • 用户检查后显式 retry,可缩小到题库或任务。

这里的目标不是“永不失败”,而是让失败有归属、有证据、有恢复动作。

长流程需要可观察性

一次模型调用可能几分钟没有输出。若终端完全沉默,用户无法区分“仍在推理”“子进程卡住”和“程序已经崩溃”。

实践中至少需要:

  • 启动时显示 Provider、任务 ID 与子进程 PID;
  • 固定间隔输出耗时心跳;
  • 保存模型原始事件和最终消息;
  • 每批完成后汇总成功、失败、待修复数量;
  • Web 控制台使用运行 ID 查询状态并支持取消。

日志不应记录 API Key 或完整敏感配置。可观察性与密钥隔离需要同时设计。

Dry Run 能验证什么

Dry Run 很有用,但必须正确理解它的边界。它可以验证:

  • 任务是否按预期展开;
  • 路径和变量是否解析正确;
  • 状态是否按顺序变化;
  • 预期目录和占位产物是否生成;
  • 中断恢复是否从正确位置继续。

它不能验证模型是否理解要求、内容质量是否达标、真实响应是否符合 Schema。因此 Dry Run 是编排测试,不是端到端质量测试。

一份可复用的检查清单

在把模型调用接入产品前,我会检查:

  • 是否定义最小可重试任务?
  • 是否有稳定 ID 和幂等语义?
  • 状态是否在每次外部调用后立即持久化?
  • 模型输出是否经过 Schema 与业务双层校验?
  • 写入是否原子,失败是否保留原始证据?
  • 是否有最大尝试次数与人工恢复入口?
  • 中断后能否只继续未完成任务?
  • 日志是否足够定位问题且不泄露密钥?
  • Dry Run 和真实 Provider 测试是否分层?

模型越强,越容易让人忽略它周围的工程。可靠 Agent 的核心并不是让模型承担更多职责,而是让每种职责落在最适合的执行者上。