欢迎光临
我们一直在努力

LangChain4j 1.19 新特性:Agent 工具调用失败自动补偿(@CompensateFor 实战)

场景引入:你做了一个订餐 Agent,它先调用「下单」工具成功扣了钱,接着调用「通知商家」工具时接口突然 500,整个任务失败。此时订单已创建、钱已扣,用户却不知道发生了什么——这就是 Agent 工具调用最常见的「半途而废」问题。本文用 LangChain4j 1.19.0 刚发布的 @CompensateFor + compensateOnToolErrors 特性,解决「工具调用一半失败,前面成功的动作怎么办」的问题,全程附完整可运行代码。

一、这个问题到底是什么

先看一个真实场景。你给电商系统做个客服 Agent,它有三个工具:

  • createOrder:创建订单
  • deductBalance:扣用户余额
  • notifyWarehouse:通知仓库发货

正常流程是三个工具按顺序调用,全部成功。但大模型(LLM)调工具是「走一步看一步」的:它先调 createOrder,拿到结果后再决定要不要调 deductBalance。如果 deductBalance 调用到一半,下游数据库连接断了,抛了异常,这时候会发生什么?

  • createOrder 已经成功了,订单躺在数据库里
  • deductBalance 失败了,钱没扣
  • Agent 整个任务报错退出

结果就是:数据库里多了一个没付钱的订单,没有任何人处理它。这在真实业务里叫「部分成功」(partial success),是所有 Agent 工程化绕不开的坑。

传统 Java 事务解决的是「同一个数据库连接里,要么全成功要么全回滚」。但 Agent 的工具调用横跨多个系统:订单系统、支付系统、仓储系统,根本不在一个事务里,分布式事务(比如 Saga)太重了,小团队玩不转。

LangChain4j 1.19.0(2026-08-14 发布)给出的方案很轻量:给工具方法配一个「补偿方法」,标记 @CompensateFor("原动作名")。当 Agent 后续任何工具失败或抛出异常时,框架自动逆序执行之前所有成功的补偿方法——下单成功了就自动取消订单,扣款成功了就自动退款。这就是「工具动作补偿」(tool actions compensation),本质是给 Agent 内置了一个简易版 Saga。

注意区分两个概念:

  • 重试(retry):失败后把同一个动作再执行一遍,适合网络抖动
  • 补偿(compensation):失败后执行「撤销之前动作」的新动作,适合已经产生副作用(扣钱、下单、发消息)的操作

本文全部代码基于 LangChain4j 1.19.0 GA 版本,Spring Boot 3.5.x(langchain4j-spring-boot-starter 1.19.0 的基线版本),JDK 21。

二、底层原理到底怎么回事

要理解补偿机制,先要理解 LangChain4j 1.x 的 Agent 执行循环(agentic loop)。用大白话说,Agent 执行一轮任务是这样的:

  • 把用户问题 + 工具清单发给大模型
  • 大模型决定:要么直接回答,要么说「我要调某个工具,参数是 XXX」
  • 框架执行这个工具,把结果回传给大模型
  • 大模型根据结果决定下一步,重复 2~3 步
  • 直到大模型觉得任务完成,给出最终答案
  • 这个循环在 LangChain4j 里由 AiServices 编排。AiServices 是 1.x 的核心入口,它把你定义的接口(比如 Assistant)动态实现成 Agent,把带 @Tool 注解的方法注册成工具。

    1.19.0 在 AiServices 上加了一个开关方法:

    AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .tools(agentTools) // 注册工具
    .compensateOnToolErrors(true) // 开启补偿
    .build();

    compensateOnToolErrors(true) 表示:开启后,只要这次任务里任何一个工具调用失败,或者 Agent 抛出异常,框架就进入补偿流程。

    补偿流程的机制,官方 release notes 的原话是:

    When compensateOnError(true) is set on an agentic system, all previously successful tool invocations with @CompensateFor actions are compensated in reverse order if any tool in any sub-agent fails or any agent throws.

    翻译成人话:开启后,之前所有成功的、带 @CompensateFor 的工具调用,会按逆序(后执行的先补偿)逐个执行对应的补偿动作。触发条件是「任何一个子 Agent 的工具失败,或任何一个 Agent 抛异常」。

    为什么逆序?这是补偿设计的核心思想,跟栈一样先进后出(LIFO)。比如你先 createOrder 再 deductBalance,补偿时应该先退钱(撤销扣款),再取消订单(撤销下单)。因为 deductBalance 依赖 createOrder 产生的订单号,必须先撤销依赖它的动作,再撤销被依赖的动作,否则退款时找不到订单。

    @CompensateFor 注解长这样(定义在 dev.langchain4j.agent.tool 包):

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Experimental
    public @interface CompensateFor {
    /** 指定要补偿的原始工具动作名 */
    String value();
    }

    用法:在普通工具方法(带 @Tool)旁边,再写一个补偿方法,方法上标 @CompensateFor("原工具名")。注意 value() 里填的是原工具的 name,不是方法名。@Tool 默认用方法名当工具名,如果 @Tool("cancelOrder") 显式指定了,@CompensateFor 就要填 cancelOrder。

    补偿方法本身可以带参数(比如订单号),框架会把原工具调用时的参数自动注入到补偿方法里(通过参数名匹配)。这非常关键:补偿方法不需要自己再去查一遍「刚才下单用的什么参数」,框架记得。

    还有一个细节:@CompensateFor 标记为 @Experimental,说明官方还在打磨这个 API,生产环境用的话要留意后续版本变更,但 1.19.0 已经 GA 可用。

    整体时序图可以这样理解:

    用户提问

    [Agent 循环开始]
    ↓ 第1轮
    createOrder(商品A) ──成功──▶ 记录"已下单"
    ↓ 第2轮
    deductBalance(订单1) ──成功──▶ 记录"已扣款"
    ↓ 第3轮
    notifyWarehouse(订单1) ──异常💥

    [框架捕获异常,触发补偿,逆序执行]
    deductBalance 的补偿 → refund(订单1) (撤销扣款)
    createOrder 的补偿 → cancelOrder(订单1) (撤销下单)

    Agent 抛出/返回错误信息

    三、实战:手把手写代码

    下面用一个「订餐 Agent」完整演示。场景:用户说「帮我订一份 58 元的牛肉面,送到公司」,Agent 依次执行:创建订单 → 扣款 → 通知商家。其中「通知商家」这个工具我故意让它抛异常(模拟第三方接口故障),验证补偿机制会不会自动把订单取消、把钱退掉。

    先看完整项目结构:

    agent-compensation-demo/
    ├── pom.xml
    └── src/main/java/com/example/agent/
    ├── AgentCompensationApplication.java (启动类)
    ├── OrderService.java (业务服务,模拟下单/扣款/通知)
    ├── OrderAgentTools.java (Agent 工具 + 补偿方法)
    ├── Assistant.java (AiServices 接口)
    └── Main.java (测试入口)

    3.1 创建 Maven 项目(pom.xml)

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.16</version>
    <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>agent-compensation-demo</artifactId>
    <version>1.0.0</version>
    <name>agent-compensation-demo</name>
    <description>LangChain4j 1.19.0 Agent 工具失败自动补偿示例</description>

    <properties>
    <java.version>21</java.version>
    <langchain4j.version>1.19.0</langchain4j.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>${langchain4j.version}</version>
    </dependency>
    <dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai</artifactId>
    <version>${langchain4j.version}</version>
    </dependency>
    <!– 日志 –>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <release>21</release>
    <parameters>true</parameters>
    </configuration>
    </plugin>
    </plugins>
    </build>
    </project>

    说明几个点:

    • spring-boot-starter-parent 3.5.16 是当前 Spring Boot 3.x 最新 GA(2026-08-15 实查 Maven Central),和 langchain4j 1.19.0 的 starter 基线兼容
    • 这里没有用 langchain4j-spring-boot-starter,因为要演示最核心的 AiServices 纯 Java 用法(更直观,不依赖 Spring 注入)。如果你要接 Spring Boot,把 langchain4j 换成 langchain4j-spring-boot-starter(1.19.0),配置 langchain4j.open-ai.chat-model.api-key 即可,核心代码不变
    • <parameters>true</parameters> 必须加:@CompensateFor 补偿方法靠参数名匹配注入原工具参数,编译时不开 -parameters 参数名就丢了,补偿方法会拿不到参数
    • logback-classic 必须显式声明:Spring Boot parent 只管理版本不引入依赖,没有日志实现跑不起来会报警告

    3.2 业务服务 OrderService

    package com.example.agent;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    /**
    * 模拟真实业务系统:订单、余额、商家通知。
    * 真实项目里这些方法内部会调用 RPC/数据库,这里用日志代替。
    */

    public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    /** 模拟创建订单,返回订单号 */
    public String createOrder(String itemName, double price) {
    String orderId = "ORDER-" + System.currentTimeMillis() % 1000000;
    log.info("【业务】创建订单成功: orderId={}, 商品={}, 价格={}元", orderId, itemName, price);
    return orderId;
    }

    /** 模拟扣款 */
    public void deductBalance(String orderId, double price) {
    log.info("【业务】扣款成功: orderId={}, 金额={}元", orderId, price);
    }

    /** 模拟通知商家——这里故意抛异常,模拟第三方接口故障 */
    public void notifyRestaurant(String orderId) {
    throw new RuntimeException("通知商家接口调用失败: 第三方服务 500 (orderId=" + orderId + ")");
    }

    /** 取消订单(createOrder 的补偿动作) */
    public void cancelOrder(String orderId) {
    log.info("【业务】补偿: 取消订单 orderId={}", orderId);
    }

    /** 退款(deductBalance 的补偿动作) */
    public void refund(String orderId, double price) {
    log.info("【业务】补偿: 退款 orderId={}, 金额={}元", orderId, price);
    }
    }

    这段代码没有魔法:createOrder 返回订单号,deductBalance 扣钱,notifyRestaurant 故意抛异常模拟故障,cancelOrder 和 refund 就是补偿动作。真实项目中这些方法对接的是支付网关、订单库,逻辑一样,只是把日志换成真实调用。

    3.3 Agent 工具类 OrderAgentTools(核心)

    package com.example.agent;

    import dev.langchain4j.agent.tool.CompensateFor;
    import dev.langchain4j.agent.tool.Tool;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    /**
    * Agent 的工具集合。
    * 每个 @Tool 方法对应一个「动作」,配一个 @CompensateFor 方法对应「撤销动作」。
    */

    public class OrderAgentTools {

    private static final Logger log = LoggerFactory.getLogger(OrderAgentTools.class);
    private final OrderService orderService;

    public OrderAgentTools(OrderService orderService) {
    this.orderService = orderService;
    }

    /**
    * 动作1:创建订单。
    * 工具名默认是方法名 createOrder,所以补偿注解 value 也填 createOrder。
    */

    @Tool("创建订单,参数:itemName 商品名,price 价格(元)")
    public String createOrder(String itemName, double price) {
    log.info("【工具】createOrder 被调用: itemName={}, price={}", itemName, price);
    return orderService.createOrder(itemName, price);
    }

    /**
    * createOrder 的补偿动作:取消订单。
    * 框架会逆序执行补偿,并把原工具调用参数按名字注入进来。
    */

    @CompensateFor("createOrder")
    public String cancelOrder(String orderId) {
    log.info("【补偿】cancelOrder 被调用: orderId={}", orderId);
    orderService.cancelOrder(orderId);
    return "订单已取消: " + orderId;
    }

    /**
    * 动作2:扣款。
    * 注意这里参数名是 orderId 和 price,正好和 createOrder 的返回值、入参对应。
    */

    @Tool("根据订单号扣款,参数:orderId 订单号,price 金额(元)")
    public void deductBalance(String orderId, double price) {
    log.info("【工具】deductBalance 被调用: orderId={}, price={}", orderId, price);
    orderService.deductBalance(orderId, price);
    }

    /**
    * deductBalance 的补偿动作:退款。
    */

    @CompensateFor("deductBalance")
    public void refund(String orderId, double price) {
    log.info("【补偿】refund 被调用: orderId={}, price={}", orderId, price);
    orderService.refund(orderId, price);
    }

    /**
    * 动作3:通知商家。故意抛异常,模拟第三方故障。
    */

    @Tool("通知商家备餐,参数:orderId 订单号")
    public void notifyRestaurant(String orderId) {
    log.info("【工具】notifyRestaurant 被调用: orderId={}", orderId);
    orderService.notifyRestaurant(orderId);
    }
    }

    关键点讲解:

  • @Tool 和 @CompensateFor 是一对:createOrder 配 cancelOrder,deductBalance 配 refund。@CompensateFor("createOrder") 里的字符串必须和工具名完全一致——工具名默认是方法名,如果 @Tool 里显式改了名字,要填改后的名字
  • 补偿方法的参数靠名字注入:cancelOrder(String orderId) 里的 orderId 是怎么来的?框架记录了 createOrder 那次调用的返回值和入参,把返回值 ORDER-123456 注入到同名参数 orderId。同理 refund 的 orderId、price 来自 deductBalance 的入参。参数名对不上,框架就注入不了(这也是为什么必须开 -parameters)
  • 补偿方法不需要返回值给用户看,框架只是执行它。返回 String 是为了日志里能看到结果
  • notifyRestaurant 没有补偿方法——因为它在整个链路最后,失败时没有更靠后的动作需要撤销(补偿只撤销「在它之前成功」的动作)
  • 3.4 Assistant 接口 + 组装入口

    package com.example.agent;

    import dev.langchain4j.service.AiServices;
    import dev.langchain4j.service.SystemMessage;

    /**
    * 定义 Agent 的对外接口。AiServices 会动态实现它。
    */

    public interface Assistant {

    @SystemMessage("你是订餐助手。为用户完成订餐流程:先创建订单,再扣款,最后通知商家。")
    String chat(String userMessage);
    }

    Assistant 接口只有一个 chat 方法,@SystemMessage 定义 Agent 的「人设和流程要求」。AiServices.builder() 会生成它的实现类,方法返回值 String 表示这是同步问答。

    package com.example.agent;

    import dev.langchain4j.memory.chat.MessageWindowChatMemory;
    import dev.langchain4j.model.chat.ChatModel;
    import dev.langchain4j.model.openai.OpenAiChatModel;
    import dev.langchain4j.service.AiServices;

    public class Main {

    public static void main(String[] args) {
    // 1. 构造 ChatModel(OpenAI 兼容接口,可换任意厂商)
    ChatModel chatModel = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY")) // 环境变量配 key
    .modelName("gpt-4o-mini")
    .build();

    // 2. 构造业务服务 + Agent 工具
    OrderService orderService = new OrderService();
    OrderAgentTools tools = new OrderAgentTools(orderService);

    // 3. 组装 Assistant:关键一行 compensateOnToolErrors(true)
    Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
    .tools(tools)
    .compensateOnToolErrors(true)
    .build();

    // 4. 跑一个注定失败的场景:notifyRestaurant 会抛异常
    String answer = assistant.chat("帮我订一份58元的牛肉面,送到公司");
    System.out.println("=== Agent 最终答复 ===");
    System.out.println(answer);
    }
    }

    运行前设置环境变量 OPENAI_API_KEY=sk-xxx,然后 mvn -q compile exec:java 或直接运行 Main。预期日志顺序:

    【工具】createOrder 被调用: itemName=牛肉面, price=58.0
    【业务】创建订单成功: orderId=ORDER-123456, …
    【工具】deductBalance 被调用: orderId=ORDER-123456, price=58.0
    【业务】扣款成功: orderId=ORDER-123456, …
    【工具】notifyRestaurant 被调用: orderId=ORDER-123456
    【业务】通知商家接口调用失败: 第三方服务 500 …
    【补偿】refund 被调用: orderId=ORDER-123456, price=58.0 ← 逆序:先退钱
    【业务】补偿: 退款 orderId=ORDER-123456, 金额=58.0元
    【补偿】cancelOrder 被调用: orderId=ORDER-123456 ← 再取消订单
    【业务】补偿: 取消订单 orderId=ORDER-123456

    看到没:notifyRestaurant 失败后,框架没有直接崩掉,而是把之前成功的 deductBalance 和 createOrder 逆序补偿掉了——先退款再取消订单,业务回到了「什么都没发生」的状态。

    如果把 compensateOnToolErrors(true) 改成 false 或删掉这行,同样跑这个场景:notifyRestaurant 抛异常后任务直接失败,订单和扣款留在系统里,这就是「脏数据」。

    3.5 验证「部分工具没补偿方法」的行为

    再验证一个边界:如果 deductBalance 不配 refund 补偿方法,只给 createOrder 配 cancelOrder,失败时框架只补偿有 @CompensateFor 的方法——createOrder 会被取消,deductBalance 的扣款没人管。所以每个有副作用的工具都要配补偿方法,这是使用铁律。

    四、踩坑经验和最佳实践

    这一节全是真实会踩的坑,逐个说。

    坑1:参数名注入失败(最常见)。补偿方法参数名必须和原工具入参/返回值的名字一致,且编译要开 -parameters。不开的话,Spring Boot 里用 langchain4j-spring-boot-starter 时会看到补偿方法参数为 null 或直接报错。Maven 配 <parameters>true</parameters>,或 Spring Boot parent 默认就带(Spring Boot 3.x parent 默认开启 -parameters),纯 Java 项目必须手动加。验证方法:给补偿方法加个空值判断,跑一次失败场景看日志有没有「参数为 null」。

    坑2:@CompensateFor 的值写错。填的是工具名不是方法名。@Tool("下单") 改了名,@CompensateFor("下单") 才对,写 createOrder 匹配不上,补偿静默不执行——框架不会报错,只会不补偿。检查方法:把工具名打印出来看,或者统一不显式改名,用默认方法名,最省心。

    坑3:补偿动作本身失败怎么办。补偿方法里调退款,退款接口也挂了,框架会怎样?当前版本补偿执行失败会向上抛,Agent 任务最终报错。实践建议:补偿方法内部做「幂等 + 重试」(同一订单重复退款要返回成功),并且给退款、取消订单这类操作做本地持久化(比如记一张 compensation 表),失败后由定时任务补跑。补偿是「尽力而为」的,不是强一致事务。

    坑4:不要拿它当分布式事务用。补偿机制解决的是「工具调用序列中途失败」的清理问题,它不保证「两个服务同时提交」的原子性——那是分布式事务的范畴。判断标准:你的场景是「一个 Agent 调一串工具」→ 补偿合适;「跨服务强一致扣款」→ 别用,上 Seata 之类。

    坑5:无副作用的工具不需要补偿。比如「查天气」「算价格」这类只读工具,失败就失败,没有副作用可撤销,配 @CompensateFor 是浪费。

    最佳实践清单:

    • 每个有副作用的 @Tool(写库、扣款、发消息、下单)都配一个幂等的补偿方法
    • 补偿方法参数尽量精简:能传订单号就传订单号,别依赖原工具的全部入参
    • 工具按依赖顺序设计:被依赖的(下单)放前面,依赖别人的(扣款)放后面,逆序补偿正好先撤销后者
    • 日志里把「工具调用 + 补偿调用」都打出来,线上排查全靠它
    • 观察 compensateOnToolErrors 对延迟的影响:补偿是同步执行的,工具链长、补偿多的场景会拖慢失败路径的响应,超时阈值要留够

    五、性能对比和技术选型

    和「手动补偿」比:以前没有这个特性,你只能自己在每个工具调用外面 try-catch,维护一个「已执行动作栈」,失败时手动逆序调撤销方法。代码量差不多,但分散在业务逻辑里,容易漏——新加一个工具忘写补偿,出事故才知道。@CompensateFor 把补偿声明和工具放在一起,声明式、可检查,心智负担小很多。性能上两者没有本质区别,都是同步顺序调用,多不了几个毫秒。

    和「Saga 分布式事务」比:Saga 靠消息队列异步推进、有事务协调器,能处理跨服务、长流程、需要持久化恢复的场景,但引入 MQ、状态机,复杂度高一个量级。LangChain4j 的补偿是进程内的、同步的、无状态的——适合「单应用内 Agent 调用外部系统」的中短流程。选型建议:

    维度@CompensateFor 补偿Saga 分布式事务
    适用范围 单 Agent 进程内的工具链 跨服务长流程
    复杂度 低(一个注解) 高(协调器+MQ+状态机)
    持久化 无(内存态) 有(可恢复)
    强一致 否(尽力而为) 否(最终一致)
    适合规模 中小团队、工具 3~10 个 大团队、跨团队协作

    结论:大多数「Agent 调工具」场景,@CompensateFor 够用且省事;只有当补偿链跨多个独立部署的服务、且需要故障恢复能力时,才升级到 Saga。

    六、总结

    这篇文章讲了 LangChain4j 1.19.0 的工具动作补偿特性,解决 Agent「工具调用一半失败,前面成功的副作用没人管」的问题:

    • 问题本质:Agent 的工具调用横跨多个系统,传统事务管不了,部分成功会产生脏数据
    • 核心 API:@CompensateFor("工具名") 声明补偿方法,AiServices.compensateOnToolErrors(true) 开启;失败时框架逆序执行所有成功的补偿动作
    • 关键细节:补偿参数按名字注入(要开 -parameters)、value 填工具名、有副作用的工具都要配补偿、补偿方法要幂等
    • 适用边界:进程内工具链用注解补偿;跨服务强一致场景才上 Saga

    一个完整可运行的订餐 Agent 示例(创建订单 → 扣款 → 通知商家 → 通知失败自动退款取消订单)已经贴在上面,复制 pom.xml + 三个类 + 接口 + Main 就能跑。核心就一行开关 + 一对注解,比手写 try-catch 栈干净得多。

    如果你正在用 LangChain4j 1.18 或更早版本,这个特性是 1.19.0 新增的(@Experimental 状态),升级后记得把有副作用的工具都补上补偿方法。

    赞(0)
    未经允许不得转载:171主机测评 » LangChain4j 1.19 新特性:Agent 工具调用失败自动补偿(@CompensateFor 实战)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址