Workflow 与 Agent Loop:确定性控制和动态决策怎样组合

# Workflow 与 Agent Loop:确定性控制和动态决策怎样组合

本篇目标

把一次 Tool Calling 扩展为有状态、有停止条件、能够暂停和恢复的任务闭环,并学会判断哪些步骤交给代码、哪些局部决策交给模型。

区分 Workflow 与 Agent设计状态与停止条件处理重试和恢复形成面试表达

# 先记住一句话

Workflow 用代码保证关键路径可预测,Agent Loop 让模型在局部开放问题中动态决定下一步;生产系统通常不是二选一,而是 “外层 Workflow 控制边界,内层 Agent 处理不确定性”。

Workflow:接收任务 → 校验 → 检索 → 生成 → 评测 → 发布
                              └─ Agent Loop ─┘
                         决策 → 工具 → 观察 → 再决策
1
2
3

# Workflow 和 Agent Loop 的本质区别

对比项 Workflow Agent Loop
下一步由谁决定 代码、状态机或规则 模型结合当前状态决定
适合任务 路径已知、规则明确、风险较高 路径难预先穷举、需要语义判断
优点 可预测、易测试、成本稳定 灵活,能根据观察结果调整
风险 新分支需要改代码 可能绕路、失控、成本波动
常见停止方式 流程到达结束节点 完成、失败、需确认或预算耗尽

能写成明确 if/else 的步骤,优先留给 Workflow。只有当下一步依赖开放式语义判断,并且分支很难穷举时,才值得交给模型。

# 一个可靠循环需要什么

# 1. 显式状态

消息历史不等于任务状态。宿主应用(后端或 Agent Runtime)至少应保存:

  • goal:目标和验收条件;
  • phase:当前阶段;
  • facts:已经验证的事实;
  • artifacts:任务已经生成的产物,例如文件、草稿或引用;
  • attempts:重试次数和失败原因;
  • status:running、requires_action、completed、incomplete、failed。

# 2. 明确停止条件

停止不能只相信模型说 “完成了”。宿主应用要检查:

  • 成功:业务验收条件是否真的满足;
  • 暂停:是否缺少用户输入或敏感操作确认;
  • 不完整:是否达到步数、时间或成本上限;
  • 失败:错误是否不可恢复,或重试预算已经耗尽。

# 3. 每步都产生 observation

工具调用后必须把真实结果写回状态。成功返回结构化数据;失败返回可分类错误,而不是只抛一句模糊异常。

# 4. 预算和护栏

循环至少需要最大步数、总超时、Token 或费用预算、工具级超时、允许调用的工具集合,以及写操作审批。

# 框架无关的最小实现

type RunStatus = 'running' | 'requires_action' | 'completed' | 'incomplete'

type TaskState = {
  goal: string
  step: number
  facts: string[]
  status: RunStatus
  pendingApproval?: { tool: string; args: unknown }
}

async function runAgent(initialState: TaskState) {
  const state = initialState

  // 宿主应用控制硬上限,模型不能自行取消这个限制。
  while (state.step < 8 && state.status === 'running') {
    const decision = await decideNextAction(state)

    if (decision.type === 'final') {
      // 模型声称完成后仍要由确定性规则验收。
      state.status = await verifyGoal(decision.answer, state)
        ? 'completed'
        : 'running'
      continue
    }

    const tool = toolRegistry.get(decision.toolName)
    const args = tool.inputSchema.parse(decision.arguments)

    if (tool.risk === 'write') {
      state.pendingApproval = { tool: tool.name, args }
      state.status = 'requires_action'
      break
    }

    const observation = await executeWithTimeout(tool, args)
    state.facts.push(summarizeObservation(observation))
    state.step += 1
  }

  // 达到预算并不等于前端只能显示 “失败”。保留已有成果和可恢复状态。
  if (state.status === 'running') state.status = 'incomplete'
  return state
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

达到最大轮数时,后端应返回结构化的 incomplete,同时携带已完成步骤、当前结果、未完成原因和恢复入口。前端再把它解释成 “本次未在预算内完成,可继续或调整目标”,而不是丢失进度后只弹失败。

# 重试、恢复和幂等

先理解幂等

幂等(Idempotency)是指同一个业务动作即使因为超时、重试或消息重复而被请求多次,最终也只产生一次预期副作用。例如,同一次 “保存薄弱知识点” 被提交三次,数据库中仍然只能有一条记录。

幂等键(Idempotency Key)是宿主应用为一次业务动作生成的唯一标识。第一次请求到达时,服务端以该键执行并保存结果;后续收到相同键时,直接返回第一次的结果,不再重复写入。新的业务动作必须使用新的幂等键,否则会被误认为旧请求。

可以记成一句话:同一个动作使用同一把钥匙;服务端见过这把钥匙,就返回旧结果,不再执行第二次。

重试只适合暂时性错误,例如网络抖动、限流和服务短暂不可用。参数错误、权限不足或业务冲突需要修正或人工处理,盲目重试只会重复失败。

写操作还需要幂等键:同一个任务因为超时重放时,不能重复发邮件、扣款或创建记录。幂等记录应配合数据库唯一约束或事务,防止两个并发请求同时发现 “没有执行过” 后都完成写入。长任务应在关键节点保存 checkpoint,恢复时从最近的可靠状态继续,而不是把整段对话重新跑一遍。

# 贯穿项目:笔记与面试助手

项目可以采用下面的混合流程:

  1. Workflow 校验问题、用户范围和预算;
  2. Agent 判断需要搜索哪些主题;
  3. Workflow 并行检索笔记和学习记录;
  4. Agent 根据证据生成答案或追问;
  5. Workflow 执行引用检查和答案评测;
  6. 写入薄弱点前暂停并等待用户确认;
  7. 保存结果、指标和完整 trace。

这里固定步骤由代码掌控,只有 “检索什么、是否需要补查、怎样追问” 交给模型。

# 高频面试题与回答

回答顺序:先说结论,再解释原因,最后补一个例子或工程边界。不要逐字背诵,记住这条表达主线即可。

1. 30 秒回答:Workflow 与 Agent 有什么区别参考答案

Workflow 是开发者提前把步骤和分支写好,优点是稳定、可预测、容易测试。Agent Loop 则让模型根据当前状态和工具结果决定下一步,更适合无法提前列完所有路径的任务,但成本和调试难度也更高。实际项目通常混合使用:外层 Workflow 管流程、权限和停止条件,局部 Agent 只负责开放式判断。

2. 项目回答:如何防止 Agent 无限循环参考答案

我不会只设置一个最大轮数,而是同时限制步数、总时间和费用。每轮都记录调用和状态变化,如果相同工具、相同参数或相同错误不断重复,就提前停止。模型说完成后还要用业务规则验收;达到上限则返回 incomplete 并保存 Checkpoint,让用户可以继续,而不是假装成功。

3. 为什么消息历史不能代替任务状态?参考答案

因为消息历史是给模型理解对话的文本,可能被裁剪、摘要,也很难准确查询。任务状态则是程序恢复和判断任务的依据,必须结构化保存目标、当前阶段、已验证事实、重试次数和审批状态。简单说,消息历史回答 “聊过什么”,状态回答 “任务现在走到哪里”。

4. 什么情况下不应该使用 Agent Loop?参考答案

如果步骤固定、规则可以写清楚,或者结果必须高度确定,就不应该让模型循环决定。例如付款主流程、数据库迁移和固定审批链,更适合普通代码或 Workflow。Agent 应该解决真正需要语义判断的问题,而不是把已经明确的流程变得不确定。

5. 达到最大轮数应该怎样返回?参考答案

后端应该明确返回 incomplete,并带上已经完成的步骤、当前结果、停止原因、能否恢复以及继续需要什么。前端再据此让用户选择继续、缩小目标或人工接管。不能只返回一句失败,也不能把达到上限包装成完成。

6. 为什么重试必须配合幂等?参考答案

因为请求超时只说明客户端没收到响应,不代表服务端没有执行。如果直接重试,可能重复发邮件、扣款或建单。幂等键给同一次业务动作一个唯一标识,服务端再次收到相同标识时复用第一次结果,从而做到 请求可以重复,业务效果只发生一次。

# 接下来学什么

下一篇学习 Agent 设计模式,重点理解怎样把循环中的动态决策整理成 Router、ReAct、Plan-and-Execute 等可选择、可组合的结构。

# 参考资料

上次更新时间: 2026年09月10日 00:05:23