工作流与 AI Agent 编排

本文讲解如何用工作流引擎编排 AI Agent,回答哪些步骤该确定哪些该交给模型、Agent 循环与工作流如何取舍、非确定性步骤怎么做幂等。覆盖确定性与非确定性边界、工具调用与副作用隔离、持久化执行下的 LLM 步骤、人工审批节点、成本与延迟控制、多 Agent 协作、失败降级、评估与提示词版本化、安全与可观测,并给出可运行代码。

引言

AI Agent 的早期实现几乎都是「一个循环 + 一堆工具」:让模型决定下一步调什么工具,执行后再把结果喂回去,直到模型认为任务完成。这个模式在演示里很惊艳,在生产里很快暴露三个问题:循环可能不收敛(一直调用工具)、失败后无法恢复(进程重启就丢状态)、成本不可控(模型反复思考同一件事)。

工作流引擎恰好能解决这三个问题。它提供确定性的骨架(哪些步骤、什么顺序、失败怎么办),把模型的非确定性限制在特定的步骤里。这样既保留了模型的灵活性,又获得了可恢复、可观测、可控制的执行过程。

但两者结合有一个根本矛盾:工作流引擎(尤其是持久化执行引擎)要求代码确定性,而 LLM 调用本质上是不确定的。理解这个矛盾的处理方式,是用好「工作流 + Agent」的关键。

本文先讲确定性与非确定性的边界怎么划,再对比 Agent 循环与工作流编排的适用场景,然后讲工具调用、持久化执行、人工审批、成本控制、多 Agent 协作、降级与评估,最后讲安全与可观测。想先理解持久化执行的机制,可以从 Temporal 与持久化执行 开始。

目录

  1. 为什么 Agent 需要工作流
  2. 确定性与非确定性的边界
  3. Agent 循环与工作流编排的取舍
  4. 工具调用与副作用隔离
  5. 持久化执行下的 LLM 步骤
  6. 人工介入的审批节点
  7. 成本与延迟控制
  8. 非确定性步骤的幂等
  9. 多 Agent 协作的编排
  10. 失败重试与模型降级
  11. 评估与回归测试
  12. 提示词与配置的版本化
  13. 安全与权限边界
  14. 可观测:token、成本与质量
  15. 落地路线图
  16. 权衡取舍
  17. 常见坑清单
  18. 小结

1. 为什么 Agent 需要工作流

一个纯 Agent 循环在生产环境会遇到四类问题,而这四类恰好是工作流引擎的强项:

问题纯 Agent 循环工作流引擎
状态丢失进程重启后从头开始事件历史持久化,断点恢复
循环不收敛模型可能无限调用工具步骤数有上限,超时可控
成本不可控每轮都重新思考步骤边界清晰,可缓存与复用
不可审计只有对话日志每步的输入输出结构化记录

其中「循环不收敛」是最致命的问题。模型可能因为工具返回的错误信息不断尝试、或者陷入「再确认一次」的循环。工作流引擎的解法是「用代码控制循环」:for 循环的迭代次数由代码决定,模型只在循环体内做决策。

另一个重要原因是「人在回路」。Agent 执行敏感操作(发邮件、改数据、转账)前需要人工确认,而纯 Agent 循环里插入人工确认很别扭。工作流引擎里这只是一个「等待信号」的步骤。

2. 确定性与非确定性的边界

划分边界的原则是:能用代码确定的就用代码,只有真正需要判断的才交给模型。

确定性(工作流控制)          非确定性(模型决策)
---------------------------  ---------------------------
步骤顺序与分支条件            自然语言理解与意图识别
重试与超时策略                内容生成与摘要
数据校验与格式转换            工具选择的优先级判断
权限与安全校验                模糊输入的分类
持久化与幂等                  开放式问题的回答

一个常见的反模式是「让模型决定流程分支」:

# 反模式:让模型决定下一步走哪个节点
next_step = llm.invoke(f"根据订单 {order} 决定下一步是 A 还是 B")
if next_step == "A":
    do_a()

这个设计的三个问题:模型的输出不稳定(同样的输入可能给出不同答案)、不可测试(无法写确定性的单元测试)、成本高(每次分支判断都要调用模型)。

正确做法是用规则判断分支,只在「需要理解自然语言」的地方调用模型:

# 正例:规则判断分支,模型只做内容理解
category = classify_by_rules(order)      # 确定性
if category == "NEEDS_REVIEW":
    summary = llm.summarize(order.raw_text)   # 非确定性,但边界清晰

3. Agent 循环与工作流编排的取舍

两种模式各有适用场景:

维度Agent 循环工作流编排
流程可预测性低(模型决定)高(代码决定)
灵活性高(可处理未预期情况)低(只走定义好的路径)
可测试性差(输出不确定)好(步骤可独立测试)
成本高(每轮都推理)低(只有必要步骤调模型)
可审计差好
适用任务开放式探索、研究类业务流程、审批、数据管道

实用的判断标准是「这个任务的成功标准是否明确」。如果成功标准明确(比如「生成一份符合模板的报告」),用工作流编排更可靠;如果成功标准模糊(比如「帮我研究这个市场」),Agent 循环更合适。

一个折中方案是「工作流包裹 Agent 循环」:外层用工作流控制步骤(准备数据、执行 Agent、校验结果、人工确认、归档),内层用受限的 Agent 循环处理需要探索的部分。这样外层获得了可靠性与可观测性,内层保留了灵活性。

@workflow.defn
class ResearchWorkflow:
    @workflow.run
    async def run(self, topic: str) -> Report:
        docs = await workflow.execute_activity(fetch_docs, topic)      # 确定性
        draft = await workflow.execute_activity(run_agent_loop, docs)  # 内层 Agent 循环
        await workflow.execute_activity(validate_report, draft)        # 确定性校验
        approved = await workflow.wait_condition(lambda: self.approved)  # 人工确认
        return await workflow.execute_activity(archive, draft)

4. 工具调用与副作用隔离

Agent 的工具分两类:只读工具(查询、检索、计算)与写工具(发消息、改数据、下单)。两者的处理方式完全不同。

只读工具可以随意重试,因为它们没有副作用。写工具必须幂等,且应该有审批门槛。

TOOLS = {
    # 只读工具:可重试,无副作用
    "search_docs": Tool(fn=search_docs, read_only=True),
    "query_db": Tool(fn=query_db, read_only=True),
    # 写工具:必须幂等,需要审批
    "send_email": Tool(fn=send_email, read_only=False, requires_approval=True),
    "create_ticket": Tool(fn=create_ticket, read_only=False, requires_approval=True),
}

在工作流里,写工具应该被包装成独立的步骤,并带上幂等键:

@activity.defn
async def send_email(order_id: str, recipient: str, body: str) -> str:
    # 幂等键由工作流生成并传入,重试时不变
    idem_key = f"email:{order_id}"
    if await email_log.exists(idem_key):
        return await email_log.get_result(idem_key)
    msg_id = await smtp.send(recipient, body)
    await email_log.save(idem_key, msg_id)
    return msg_id

另一个重要的设计是「工具白名单」:不要让模型自由选择任意工具,而是按步骤限定可用工具集。比如「生成摘要」的步骤只给检索工具,「执行操作」的步骤才给写工具。这既降低了误操作风险,也让提示词更短更聚焦。

5. 持久化执行下的 LLM 步骤

在 Temporal 这类持久化执行引擎里,LLM 调用必须放在 Activity 里,因为它是非确定性的(同样的输入可能返回不同的输出,且调用有成本)。

// 反模式:在 Workflow 里直接调 LLM
public void execute(TaskInput input) {
    String result = llmClient.complete(input.prompt());  // 破坏确定性
    activities.save(result);
}

// 正例:LLM 调用在 Activity 里
public void execute(TaskInput input) {
    String result = activities.callLlm(input.prompt());
    activities.save(result);
}

Activity 的返回值会被写进事件历史,重放时直接读取历史而不重新调用模型。这带来两个好处:重放不重复计费;重放的输出与首次一致,保证了确定性。

代价是「历史膨胀」:LLM 的输出通常很长(几百到几千 token),写进事件历史会让历史迅速变大。解决方案是「Activity 返回引用而不是内容」:

@ActivityMethod
public String callLlm(String prompt) {
    String content = llmClient.complete(prompt);
    String key = blobStore.put(content);      // 大内容存对象存储
    return key;                               // 历史里只存 key
}

6. 人工介入的审批节点

Agent 的写操作应该有人工审批门槛,尤其是「不可逆」的操作。在工作流里,这是一个「等待信号」的步骤。

@workflow.defn
class AgentWithApproval:
    def __init__(self):
        self.approved = None

    @workflow.signal
    def approve(self, decision: bool, comment: str = ""):
        self.approved = (decision, comment)

    @workflow.run
    async def run(self, task: str):
        plan = await workflow.execute_activity(agent_plan, task)
        await workflow.execute_activity(notify_reviewer, plan)

        # 最多等 24 小时,超时视为拒绝
        ok = await workflow.wait_condition(
            lambda: self.approved is not None,
            timeout=timedelta(hours=24))
        if not ok or not self.approved[0]:
            return {"status": "REJECTED"}

        result = await workflow.execute_activity(agent_execute, plan)
        return {"status": "DONE", "result": result}

审批的内容设计很关键:审批人需要看到「Agent 打算做什么、为什么、影响范围」,而不是「是否同意」这一个按钮。建议把 Agent 的计划(工具调用序列与参数)结构化展示,让审批人能判断而不是盲签。

审批的粒度也要设计:每个操作都审批会让 Agent 失去价值(人成了瓶颈),完全不审批则风险不可控。实用做法是「按风险分级」:只读操作自动放行,写操作按影响范围决定是否需要审批(比如「发送内部通知」自动,「发送外部邮件」审批)。

7. 成本与延迟控制

LLM 步骤的成本与延迟是 Agent 工作流里最不可预测的部分。四种控制手段:

  • 缓存:相同输入直接返回缓存结果。注意缓存键要包含模型版本与提示词版本。
  • 模型分级:简单步骤用小模型,复杂步骤用大模型。
  • 步骤预算:给每个步骤设 token 上限与超时,超限则降级或失败。
  • 提前终止:设置总步数上限与总 token 预算,超过则终止并返回部分结果。
agent_workflow:
  total_budget:
    max_steps: 20
    max_tokens: 200000
    max_duration: 10m
  steps:
    classify:
      model: gpt-4o-mini
      max_tokens: 200
      cache: true
    summarize:
      model: gpt-4o
      max_tokens: 4000
      cache: true
    execute:
      model: gpt-4o
      max_tokens: 8000
      cache: false        # 有副作用的步骤不缓存

「有副作用的步骤不缓存」是一条重要规则:缓存一个「发送邮件」的步骤会导致「第一次发了,第二次不发了」,语义完全错乱。缓存只适用于纯计算步骤。

8. 非确定性步骤的幂等

LLM 调用本身是幂等的(同样的输入得到同样的输出,除非有随机性),但它的下游副作用不是。所以幂等设计的重点是「基于 LLM 输出的副作用」。

三种做法:

# 做法一:把 LLM 输出先持久化,副作用基于持久化结果执行
analysis = await workflow.execute_activity(llm_analyze, doc)   # 结果进历史
# 重放时直接读历史,不会重新分析,因此后续副作用也不会重复

# 做法二:副作用带幂等键
await workflow.execute_activity(
    send_notification,
    NotificationArgs(idem_key=f"notify:{task_id}", content=analysis))

# 做法三:把 LLM 输出当决策,用状态前置条件保护
await workflow.execute_activity(
    apply_decision,
    DecisionArgs(task_id=task_id, decision=analysis.decision))
# apply_decision 内部用「WHERE status='PENDING'」保证只生效一次

做法一是持久化执行引擎的天然优势:因为 LLM 的输出进了事件历史,重放时不会重新调用模型,所以整个链路是确定的。做法二与做法三适用于非持久化引擎。

要特别注意「LLM 输出被解析成结构化数据」的场景:如果模型输出 JSON 但格式不合法,解析会失败。这类失败应该重试(让模型重新生成),但重试会消耗成本,所以要有次数上限并配合更明确的提示词。

9. 多 Agent 协作的编排

多 Agent 协作有两种组织方式,与 Saga 的编排/协同之分类似。

编排式(Orchestrator-Workers):一个主 Agent 负责任务分解与结果汇总,多个 Worker Agent 执行子任务。

@workflow.defn
class OrchestratorWorkflow:
    @workflow.run
    async def run(self, goal: str):
        subtasks = await workflow.execute_activity(plan_subtasks, goal)
        # 并行执行子任务
        handles = [
            workflow.start_child_workflow(
                WorkerWorkflow.run, st, id=f"{goal}-{i}")
            for i, st in enumerate(subtasks)
        ]
        results = [await h for h in handles]
        return await workflow.execute_activity(synthesize, results)

协同式(Peer-to-Peer):Agent 之间直接传递消息,没有中心协调者。这种方式灵活但难以追踪,容易出现「两个 Agent 互相等待」的死锁。

实践建议:生产系统优先用编排式,因为「谁在做什么」是可见的;协同式只适合探索性场景。子工作流(Child Workflow)是编排式的基础设施,每个子任务有独立的历史与状态,可以单独重试与观测。

并行执行子任务时要注意「一个失败是否取消其他」:如果子任务之间有依赖,一个失败应该取消其他(避免浪费成本);如果相互独立,可以让它们各自完成再汇总。

10. 失败重试与模型降级

LLM 调用的失败模式比普通 API 更多:

失败类型例子处理策略
限流429 Too Many Requests退避重试
超时请求超过 60 秒重试或降级到小模型
内容过滤被安全策略拦截不重试,转人工或改写提示词
格式错误输出不是合法 JSON重试并强化格式约束
能力不足输出质量不达标升级到大模型或转人工
上下文超限输入超过模型窗口截断或分段处理
@activity.defn
async def llm_with_fallback(prompt: str) -> str:
    for model in ["gpt-4o", "gpt-4o-mini", "local-model"]:
        try:
            return await call_llm(model, prompt)
        except RateLimitError:
            await asyncio.sleep(backoff())
        except (TimeoutError, ServiceUnavailable):
            continue          # 降级到下一个模型
        except ContentFilterError:
            raise             # 内容问题不降级,直接上抛
    raise AllModelsFailed("所有模型均不可用")

降级的顺序应该是「能力从高到低」,且降级后要记录「本次用了降级模型」,因为输出质量可能不同。这与 重试幂等与补偿设计 里的错误分类是同一条原则:可自愈的错误重试,不可自愈的错误上抛。

「内容过滤」这类错误不要重试:重试同样的提示词大概率还是被拦,应该转人工或修改提示词后重试。

11. 评估与回归测试

Agent 工作流最大的工程挑战是「怎么知道改动没有让效果变差」。传统单元测试不适用(输出不确定),需要评估集(Eval Set)。

# 评估集:输入 + 期望的关键特征(不是精确输出)
EVAL_CASES = [
    {"input": "退款申请:订单 1001,原因商品破损",
     "expect": {"category": "REFUND", "requires_approval": True}},
    {"input": "咨询:什么时候发货",
     "expect": {"category": "INQUIRY", "requires_approval": False}},
]

async def run_eval(pipeline) -> EvalReport:
    results = []
    for case in EVAL_CASES:
        out = await pipeline(case["input"])
        results.append({
            "case": case,
            "passed": matches(out, case["expect"]),
        })
    return EvalReport(results)

评估集的设计要点:断言「关键特征」而不是「精确输出」。比如断言「分类正确」「是否触发审批」,而不是断言「生成的文本完全一致」。这样既稳定又有意义。

评估要进 CI,且要有「回归门禁」:关键用例的通过率低于阈值则阻塞发布。这是 Agent 系统能持续迭代的前提,否则每次改提示词都是赌博。

12. 提示词与配置的版本化

提示词是代码,必须版本化。三个层次:

  • 提示词模板放进 Git,走 PR 评审。
  • 每次发布带版本号,工作流实例记录使用的是哪个版本。
  • 支持灰度与回滚,出问题能快速切回。
# prompts/refund-classify/v3.yaml
version: v3
model: gpt-4o-mini
temperature: 0
system: |
  你是退款申请分类器。只输出 JSON,不要解释。
  字段:category(REFUND/INQUIRY/COMPLAINT)、requires_approval(bool)
user: |
  申请内容:{{ text }}
  订单金额:{{ amount }}

在工作流里记录版本,便于事后追溯「这个实例用的是哪版提示词」:

result = await workflow.execute_activity(
    llm_classify,
    ClassifyArgs(text=text, prompt_version="refund-classify/v3"))

13. 安全与权限边界

Agent 的安全风险主要来自「模型被诱导执行不该执行的操作」(提示词注入)。防御手段有四层:

  • 工具白名单:每个步骤只暴露必要的工具,模型无法调用未授权的工具。
  • 参数校验:工具的参数在服务端校验,不信任模型给出的值(比如「删除用户」的 ID 必须在允许列表内)。
  • 权限收敛:Agent 使用的凭证权限最小化,比如只能读特定表、只能发特定域名。
  • 人工审批:不可逆操作强制人工确认。
def execute_tool(name: str, args: dict, allowed: set[str], principal: str):
    if name not in allowed:
        raise PermissionError(f"tool {name} not allowed in this step")
    tool = TOOLS[name]
    validated = tool.schema.validate(args)      # 参数结构校验
    if not tool.authorize(principal, validated):  # 业务权限校验
        raise PermissionError(f"principal {principal} cannot {name}")
    return tool.fn(validated)

提示词注入的典型场景是「Agent 读取了外部内容(网页、邮件、文档),内容里藏着指令」。防御的核心是「不把外部内容当指令」:在提示词里明确区分「数据」与「指令」,并在服务端做权限校验而不是依赖模型自觉。

14. 可观测:token、成本与质量

Agent 工作流的观测比普通工作流多三个维度:

# 每个步骤的 token 消耗
agent_tokens_total{workflow="RefundFlow", step="classify", model="gpt-4o-mini", direction="input"} 125000
agent_tokens_total{workflow="RefundFlow", step="classify", model="gpt-4o-mini", direction="output"} 8000

# 每个工作流实例的成本
agent_cost_usd_sum{workflow="RefundFlow"} 42.17

# 质量指标
agent_eval_pass_rate{workflow="RefundFlow", eval_set="core"} 0.94
agent_fallback_total{workflow="RefundFlow", from="gpt-4o", to="gpt-4o-mini"} 12
agent_human_review_total{workflow="RefundFlow", reason="content_filter"} 3

「按工作流类型统计成本」是最实用的成本视图:它能回答「处理一个退款申请平均花多少钱」,从而判断这个 Agent 是否值得上线。

工作流级的可观测(实例查询、事件历史、停滞告警)与普通工作流一致,参考 工作流可观测与调试 。

15. 落地路线图

  • 第 1 周:把一个确定性的流程(比如「分类 → 生成草稿 → 人工确认 → 发送」)用工作流引擎实现,LLM 步骤作为 Activity。
  • 第 2 周:加入幂等键与缓存,验证「重放不重复调用模型、不重复产生副作用」。
  • 第 3 周:加入成本预算与模型降级,配置 token 与步数上限。
  • 第 4 周:建立评估集与 CI 门禁,接入 token 与成本指标。

不要从「完全自主的 Agent」开始。先做一个「工作流 + 少量 LLM 步骤」的系统,把可靠性基础打牢,再逐步把更多的决策交给模型。这样每一步的复杂度都是可控的。

16. 权衡取舍

选择收益代价
工作流编排 Agent 循环可靠、可观测、可审计灵活性下降,需要预先定义步骤
纯 Agent 循环灵活,能处理未预期情况不收敛风险,成本不可控
LLM 步骤放 Activity重放不重复计费,输出稳定历史膨胀,需要存引用
大模型直出质量最好成本高、延迟大
模型分级 + 降级成本可控质量不稳定,需要评估兜底
结果缓存成本显著下降需要版本化缓存键
全自动执行效率最高不可逆操作风险高
提示词进 Git可版本化、可回滚迭代速度受发版流程限制

17. 常见坑清单

  1. 让模型决定流程分支,输出不稳定导致行为不可复现,应该用规则判断。
  2. 在持久化执行的 Workflow 里直接调 LLM,破坏确定性导致重放失败。
  3. LLM 的长输出直接进事件历史,历史膨胀到几十 MB,重放极慢。
  4. LLM 步骤的重试重新生成输出,导致下游副作用与首次不一致。
  5. 有副作用的步骤开启缓存,出现「第一次执行了、第二次不执行」。
  6. 缓存键不含模型版本与提示词版本,模型升级后返回过时结果。
  7. 不设 token 与步数预算,一个失控的 Agent 循环烧掉大量成本。
  8. 内容过滤错误被无限重试,同样的提示词反复被拦。
  9. 写操作没有幂等键,工作流重试导致重复发邮件、重复下单。
  10. 工具权限过大(能删任意数据),提示词注入后造成严重破坏。
  11. 提示词直接写在代码里且没有版本号,出问题无法回滚到上一版。
  12. 只测「能跑通」,没有评估集与回归门禁,改提示词靠感觉。

18. 小结

工作流与 AI Agent 的结合,本质是「用确定性的骨架包裹非确定性的步骤」。骨架负责顺序、重试、幂等、审批、可观测;模型负责理解与生成。边界划得越清楚,系统越可靠,也越容易评估与优化。

三条落地原则:能用代码确定的绝不交给模型;LLM 调用必须放在有持久化与幂等保护的步骤里;写操作必须有权限校验与审批门槛。这三条做到了,Agent 工作流就能从演示走向生产。

下一步建议读 重试幂等与补偿设计 把 LLM 步骤的幂等做扎实,读 人工任务与审批流表单 把审批节点的体验与粒度设计好;如果 Agent 的写操作涉及跨服务回滚,则应该结合 Saga 与分布式事务补偿 一起设计补偿链路。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「工作流引擎」更多文章

  1. 工作流成本优化
  2. 执行器与资源隔离
  3. 调度、回填与补数