云计算百科
云计算领域专业知识百科平台

AI Agent 真正的难点,不是会调用工具,而是能可靠地执行工作流

本文面向 Java 后端和 AI 应用开发者,讨论 AI Agent 从“能够调用工具”走向“能够可靠完成业务流程”时,必须补齐的状态管理、幂等、重试、人工确认和恢复机制。示例以 Java 21、Spring Boot 3.x 和常见工具调用链路为背景,代码用于说明架构方法,具体 API 需要按实际依赖版本调整。

摘要

很多 Agent Demo 都有相似的交互:模型先理解用户问题,再调用查询工具,拿到结果后继续调用另一个工具,最后生成回答。演示时流程很顺,但一旦接入真实业务,问题很快出现:工具调用超时后应该重试吗?重试会不会重复扣款?模型已经生成了“准备提交”的结果,但用户还没有确认,系统能不能继续执行?服务重启后,任务从哪里恢复?同一个请求重复发送时,是否会产生两张工单?

这些问题说明,工具调用循环不等于业务工作流。模型擅长理解自然语言、选择候选动作和解释结果,但不应该独自承担状态持久化、事务边界、幂等控制和高风险操作。可靠的 Agent 系统,需要让模型负责不确定的部分,让确定性工程逻辑负责状态、权限和执行。

本文用“售后退款 Agent”作为案例,设计一条从查询订单、计算退款、等待确认到创建退款任务的工作流,并给出 Java 状态机、工具契约、幂等处理、重试策略和上线检查清单。

目录

  • 一、工具调用循环为什么不能直接当工作流
  • 二、把 Agent 拆成模型决策层和确定性执行层
  • 三、实战案例:售后退款 Agent 的状态机设计
  • 四、幂等、重试和人工确认必须写进代码
  • 五、服务重启后,任务如何继续执行
  • 六、什么场景适合 Agent,什么场景应该使用传统流程
  • 结论:Agent 的可靠性来自工作流,而不是模型的自信

一、工具调用循环为什么不能直接当工作流

1. 模型的下一步不是系统状态

模型可以返回一个工具调用请求,例如查询订单、计算退款或创建售后单。这个请求只是一个建议,不代表业务动作已经成功完成。

至少有四个事实需要由系统自己确认:

  • 工具是否真的执行了;
  • 工具执行了几次;
  • 结果是否已经持久化;
  • 当前用户是否有权继续下一步。
  • 如果系统把模型返回的工具调用直接当成状态变更,就会把“不确定的生成结果”和“确定的业务事实”混在一起。一次网络超时,模型可能认为工具没有执行,系统也可能认为工具执行失败,下一次重试就有机会造成重复操作。

    2. 三种系统的区别

    类型主要职责是否适合直接修改业务数据典型风险
    聊天问答 解释和生成文本 通常不适合 事实错误、引用不准
    Agent 循环 选择工具并组合结果 只能在受控范围内 工具选错、循环失控
    业务工作流 管理状态和执行边界 可以,但必须有策略 状态丢失、重复执行、无法恢复

    Agent 可以成为工作流中的智能节点,但不能替代工作流本身。尤其是退款、付款、发货、审批、权限变更等动作,应该由确定性服务根据状态和策略执行。

    3. 一个典型的重复退款事故

    用户说:“把这笔订单退掉。”

    系统流程如下:

  • 模型查询订单,确认订单满足退款条件;
  • 模型调用退款工具;
  • 退款服务已经收到请求并完成扣款回滚;
  • 返回结果的网络连接在响应前断开;
  • 模型收到工具超时,判断“退款没有成功”;
  • Agent 再次调用退款工具。
  • 如果退款接口没有幂等键,用户可能得到两次退款,或者系统进入人工对账。模型不是故意做错,而是它没有办法从网络超时中推断业务事实。

    二、把 Agent 拆成模型决策层和确定性执行层

    1. 模型适合做什么

    模型可以承担以下任务:

    • 从用户语言中识别意图;
    • 提取订单号、原因、时间等参数;
    • 在候选工具中选择可能的下一步;
    • 根据检索结果生成解释;
    • 在多个方案中提出建议;
    • 发现信息不足并向用户提问。

    这些任务本身存在语义不确定性,模型的优势正好在这里。

    2. 系统必须自己决定什么

    下面的内容不能只依赖模型输出:

    • 当前工作流处于哪个状态;
    • 用户是否有权限执行动作;
    • 请求是否已经执行过;
    • 工具是否允许重试;
    • 是否必须等待用户确认;
    • 业务数据是否已经提交;
    • 失败后应该暂停、补偿还是转人工。

    可以用一句话概括:

    模型提出候选动作,工作流决定动作是否允许,领域服务决定动作如何执行。

    3. 一条更稳的链路

    #mermaid-svg-JjQUhz0CVPumUJMR{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JjQUhz0CVPumUJMR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JjQUhz0CVPumUJMR .error-icon{fill:#552222;}#mermaid-svg-JjQUhz0CVPumUJMR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JjQUhz0CVPumUJMR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JjQUhz0CVPumUJMR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JjQUhz0CVPumUJMR .marker.cross{stroke:#333333;}#mermaid-svg-JjQUhz0CVPumUJMR svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JjQUhz0CVPumUJMR p{margin:0;}#mermaid-svg-JjQUhz0CVPumUJMR .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-JjQUhz0CVPumUJMR .cluster-label text{fill:#333;}#mermaid-svg-JjQUhz0CVPumUJMR .cluster-label span{color:#333;}#mermaid-svg-JjQUhz0CVPumUJMR .cluster-label span p{background-color:transparent;}#mermaid-svg-JjQUhz0CVPumUJMR .label text,#mermaid-svg-JjQUhz0CVPumUJMR span{fill:#333;color:#333;}#mermaid-svg-JjQUhz0CVPumUJMR .node rect,#mermaid-svg-JjQUhz0CVPumUJMR .node circle,#mermaid-svg-JjQUhz0CVPumUJMR .node ellipse,#mermaid-svg-JjQUhz0CVPumUJMR .node polygon,#mermaid-svg-JjQUhz0CVPumUJMR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JjQUhz0CVPumUJMR .rough-node .label text,#mermaid-svg-JjQUhz0CVPumUJMR .node .label text,#mermaid-svg-JjQUhz0CVPumUJMR .image-shape .label,#mermaid-svg-JjQUhz0CVPumUJMR .icon-shape .label{text-anchor:middle;}#mermaid-svg-JjQUhz0CVPumUJMR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JjQUhz0CVPumUJMR .rough-node .label,#mermaid-svg-JjQUhz0CVPumUJMR .node .label,#mermaid-svg-JjQUhz0CVPumUJMR .image-shape .label,#mermaid-svg-JjQUhz0CVPumUJMR .icon-shape .label{text-align:center;}#mermaid-svg-JjQUhz0CVPumUJMR .node.clickable{cursor:pointer;}#mermaid-svg-JjQUhz0CVPumUJMR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JjQUhz0CVPumUJMR .arrowheadPath{fill:#333333;}#mermaid-svg-JjQUhz0CVPumUJMR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JjQUhz0CVPumUJMR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JjQUhz0CVPumUJMR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjQUhz0CVPumUJMR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JjQUhz0CVPumUJMR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjQUhz0CVPumUJMR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JjQUhz0CVPumUJMR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JjQUhz0CVPumUJMR .cluster text{fill:#333;}#mermaid-svg-JjQUhz0CVPumUJMR .cluster span{color:#333;}#mermaid-svg-JjQUhz0CVPumUJMR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JjQUhz0CVPumUJMR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JjQUhz0CVPumUJMR rect.text{fill:none;stroke-width:0;}#mermaid-svg-JjQUhz0CVPumUJMR .icon-shape,#mermaid-svg-JjQUhz0CVPumUJMR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjQUhz0CVPumUJMR .icon-shape p,#mermaid-svg-JjQUhz0CVPumUJMR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JjQUhz0CVPumUJMR .icon-shape .label rect,#mermaid-svg-JjQUhz0CVPumUJMR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjQUhz0CVPumUJMR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JjQUhz0CVPumUJMR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JjQUhz0CVPumUJMR :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    查询类动作

    写入类动作

    不通过

    用户请求

    意图识别

    提取结构化参数

    加载工作流状态

    权限与规则校验

    模型提出候选动作

    确定性策略审批

    执行只读工具

    生成待确认计划

    用户确认

    带幂等键执行

    记录事件与结果

    更新状态

    生成下一步或结束

    拒绝、澄清或转人工

    注意这里有两个不同的“动作”:模型提出的候选动作,以及服务端批准后的实际动作。两者之间的策略层,就是保护业务系统的边界。

    三、实战案例:售后退款 Agent 的状态机设计

    1. 业务目标和边界

    售后助手需要帮助用户处理退款咨询。第一版只开放以下能力:

    • 查询当前用户自己的订单;
    • 判断订单是否满足“未发货可取消”的规则;
    • 计算预估退款金额;
    • 展示退款计划;
    • 用户明确确认后创建退款任务。

    以下能力暂不开放:

    • 模型直接执行退款;
    • 修改收款账户;
    • 绕过订单状态强制退款;
    • 对一批订单进行无确认批量操作。

    把第一版限制在低风险和可追踪范围内,可以减少工作流状态和异常分支,也方便建立评测集。

    2. 状态定义

    public enum RefundState {
    RECEIVED,
    ORDER_LOADED,
    ELIGIBLE,
    PLAN_READY,
    WAITING_CONFIRMATION,
    EXECUTING,
    SUCCEEDED,
    REJECTED,
    FAILED,
    NEEDS_HUMAN
    }

    状态不是页面上的标签,而是业务事实。每次状态变化都应该有来源、时间和操作者,不能只保存在一次请求的内存变量中。

    3. 约束状态转换

    public final class RefundStateMachine {

    private static final Map<RefundState, Set<RefundState>> ALLOWED = Map.of(
    RefundState.RECEIVED,
    Set.of(RefundState.ORDER_LOADED, RefundState.REJECTED),
    RefundState.ORDER_LOADED,
    Set.of(RefundState.ELIGIBLE, RefundState.REJECTED),
    RefundState.ELIGIBLE,
    Set.of(RefundState.PLAN_READY, RefundState.NEEDS_HUMAN),
    RefundState.PLAN_READY,
    Set.of(RefundState.WAITING_CONFIRMATION),
    RefundState.WAITING_CONFIRMATION,
    Set.of(RefundState.EXECUTING, RefundState.REJECTED),
    RefundState.EXECUTING,
    Set.of(RefundState.SUCCEEDED, RefundState.FAILED),
    RefundState.FAILED,
    Set.of(RefundState.EXECUTING, RefundState.NEEDS_HUMAN)
    );

    public static void assertAllowed(RefundState from, RefundState to) {
    if (!ALLOWED.getOrDefault(from, Set.of()).contains(to)) {
    throw new IllegalStateException(
    "Illegal transition: " + from + " -> " + to
    );
    }
    }
    }

    状态机的价值在于拒绝非法路径。例如一个已经成功的退款任务,不能因为模型又生成了一次“执行退款”就回到执行中。

    4. 状态变化要落成事件

    public record RefundEvent(
    UUID eventId,
    UUID workflowId,
    RefundState from,
    RefundState to,
    String reason,
    String actor,
    Instant occurredAt
    ) {}

    事件可以写入关系数据库或消息系统。具体选型取决于吞吐量、顺序要求和恢复方式,但至少要做到:

    • 事件有唯一 ID;
    • 同一个工作流可以按顺序读取;
    • 状态变化前后可审计;
    • 可以根据事件重建当前状态;
    • 用户确认和人工介入有明确操作者。

    四、幂等、重试和人工确认必须写进代码

    1. 工具契约需要区分查询和写入

    一个工具的描述不能只写“执行退款”。更完整的工具契约应该包含:

    字段作用
    name 工具唯一名称
    purpose 允许解决什么问题
    input schema 参数类型、格式和范围
    side effect 是否修改业务数据
    idempotency 是否支持幂等
    permission 哪类角色可以调用
    confirmation 是否必须用户确认
    timeout 超时后如何处理

    工具越多,描述越重要;但描述越清楚,也不能代替服务端权限和状态校验。

    2. 用幂等键保护写操作

    public RefundResult executeRefund(
    UUID workflowId,
    String idempotencyKey,
    RefundCommand command
    ) {
    Optional<RefundResult> existing =
    refundRepository.findByIdempotencyKey(idempotencyKey);
    if (existing.isPresent()) {
    return existing.get();
    }

    RefundResult result = paymentGateway.refund(command);
    refundRepository.saveResult(
    workflowId,
    idempotencyKey,
    result
    );
    return result;
    }

    真实实现必须使用数据库唯一约束或供应商提供的幂等能力,不能只靠先查询再插入。因为“查询”和“插入”之间仍然可能发生并发竞争。

    3. 重试策略不能一刀切

    操作类型默认策略原因
    查询订单 可有限重试 通常没有副作用
    读取库存 可重试,但要标注时间 数据可能在重试间变化
    创建工单 只有有幂等键时重试 避免重复工单
    扣款或退款 先查询最终状态 网络超时不代表业务失败
    修改权限 默认不自动重试 需要人工核查
    删除数据 不由模型直接执行 错误不可逆

    尤其要区分“请求失败”和“业务失败”。请求失败可能是网络、超时或连接中断;业务失败可能是余额不足、订单状态不允许或权限不足。两者的恢复路径完全不同。

    4. 用户确认必须绑定计划版本

    如果用户在等待确认期间修改了订单或退款金额,原来的确认不能无限期有效。确认应该绑定一个不可变的计划快照:

    public record RefundPlan(
    UUID planId,
    UUID orderId,
    BigDecimal amount,
    String currency,
    String ruleVersion,
    String planHash,
    Instant expiresAt
    ) {}

    用户确认时,服务端重新检查 planHash、订单状态、金额和过期时间。只要其中一项变化,就重新生成计划,而不是沿用旧确认。

    五、服务重启后,任务如何继续执行

    1. 不要把工作流状态放在内存

    如果 Agent 运行到一半服务重启,以下信息不能丢:

    • 工作流 ID;
    • 当前状态;
    • 用户身份摘要;
    • 已经执行过的工具;
    • 每个工具的请求 ID 和幂等键;
    • 当前计划版本;
    • 下次可重试时间;
    • 等待用户确认还是等待系统恢复。

    把这些信息放在单次 HTTP 请求的内存里,服务重启或请求超时就无法恢复。

    2. 采用“命令 + 事件 + 状态快照”

    一个简单的持久化模型可以包含三张表:

    表主要字段用途
    workflow workflow_id、state、version 保存当前状态
    workflow_event event_id、from、to、reason 保存状态变化
    tool_execution tool_call_id、idempotency_key、status 保存工具执行事实

    更新状态时使用乐观锁版本号,避免两个并发请求同时推进同一个工作流。

    public void transition(
    UUID workflowId,
    long expectedVersion,
    RefundState from,
    RefundState to
    ) {
    RefundStateMachine.assertAllowed(from, to);
    int updated = jdbc.update(
    """
    update refund_workflow
    set state = ?, version = version + 1
    where workflow_id = ?
    and version = ?
    and state = ?
    """
    ,
    to.name(),
    workflowId,
    expectedVersion,
    from.name()
    );
    if (updated != 1) {
    throw new ConcurrentModificationException(
    "Workflow was changed by another request"
    );
    }
    }

    这里的 SQL 使用了 Java 文本块,仅用于展示结构。生产代码需要结合事务、数据库方言和异常处理完善。

    3. 失败后要有明确的恢复动作

    恢复动作不应该只写成“稍后再试”,而应该根据失败类型决定:

    • 可重试错误:进入延迟队列,带退避时间;
    • 状态未知:先查询外部系统最终状态;
    • 业务拒绝:终止并把原因展示给用户;
    • 权限错误:停止执行并记录安全事件;
    • 多次失败:转人工并附带完整 Trace;
    • 数据不一致:进入对账或补偿流程。

    4. 工具调用的观测要能回答五个问题

    一次线上故障发生时,日志至少要回答:

  • 谁发起了工作流;
  • 模型提出了什么候选动作;
  • 系统批准了哪一个动作;
  • 工具实际执行了几次;
  • 最终业务状态是什么。
  • Spring AI 官方工具调用文档目前把工具定义、模型请求、应用执行、结果回传和继续生成拆成明确环节,同时支持框架控制和用户控制的执行方式。对于高风险工作流,建议保留足够的手动控制点,而不是把整个循环交给默认实现。

    参考:Spring AI Tool Calling 官方文档。

    六、什么场景适合 Agent,什么场景应该使用传统流程

    1. 适合 Agent 的部分

    Agent 更适合处理:

    • 用户说法不统一的意图识别;
    • 多轮澄清和信息补全;
    • 从多个候选工具中选择查询路径;
    • 对资料进行总结、解释和改写;
    • 为人工客服整理上下文;
    • 给出多个方案并说明差异。

    这些任务需要理解自然语言,且输出可以被后续规则校验。

    2. 不适合让 Agent 主导的部分

    以下场景更适合由传统工作流、规则引擎或人工审批主导:

    • 付款、扣款、退款;
    • 权限和账号变更;
    • 删除数据;
    • 合规审批;
    • 发送不可撤回的外部通知;
    • 大规模批量操作。

    并不是完全不能使用 AI,而是让 AI 做解释、提取和建议,把真正的执行放在确定性流程中。

    3. 什么时候可以放宽人工确认

    只有在以下条件同时满足时,才可以考虑降低确认频率:

  • 动作是低风险且可逆的;
  • 工具具备幂等键;
  • 权限可以由服务端验证;
  • 失败后可以恢复或补偿;
  • 有稳定的离线评测和线上监控;
  • 用户知道系统可能自动完成什么。
  • 任何一项缺失,都不适合仅凭“模型表现很好”取消确认。

    结论:Agent 的可靠性来自工作流,而不是模型的自信

    Agent 的价值不在于让模型连续调用多少个工具,而在于让用户用更自然的方式完成一项真实工作。要实现这一点,系统必须把智能和确定性分开:

  • 模型负责理解意图、提取参数和提出候选动作;
  • 工作流负责持久化状态、校验转换和恢复执行;
  • 领域服务负责权限、事务和业务规则;
  • 工具负责完成明确、可审计、可幂等的动作;
  • 人工负责高风险确认、异常处理和责任承担。
  • 如果一个 Agent 只能在服务不重启、网络不超时、用户不重复点击、模型不选错工具的理想环境里工作,它只是一个演示程序。真正可以上线的 Agent,应该允许失败,并且在失败后能够知道发生了什么、停止扩大损失、恢复到正确状态。

    所以,AI Agent 的下一道门槛不是让模型“更像一个人”,而是让整个系统更像一个可靠的业务流程。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » AI Agent 真正的难点,不是会调用工具,而是能可靠地执行工作流
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!