第2044篇:LangChain4j的Tool Calling——让AI真正调用你的业务代码
2026/4/30大约 8 分钟
第2044篇:LangChain4j的Tool Calling——让AI真正调用你的业务代码
适读人群:需要让LLM调用Java业务逻辑的工程师 | 阅读时长:约20分钟 | 核心价值:掌握LangChain4j Tool的完整实现,理解工具调用的执行流程和异常处理
有一次做项目演示,给客户展示AI客服。AI能回答各种产品问题,但客户问"我的快递到哪了",AI只能说"请您查看物流信息",完全答不上来。
客户就一句话:"这有什么用?我还不如自己去查。"
这就是只有对话能力没有工具调用能力的AI的局限。Tool Calling让AI能真正"做事",不只是"说话"。
Tool Calling的工作原理
先理解整个执行流程,这对后面排查问题很重要:
关键点:LLM并不直接调用你的代码。LLM"决定"要调用什么工具、传什么参数,LangChain4j框架负责实际调用,然后把结果反馈给LLM,让LLM生成最终的自然语言回答。
基础Tool定义
/**
* 订单相关工具集合
* 一个@Component里可以定义多个@Tool方法
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderTools {
private final OrderRepository orderRepo;
private final ShippingService shippingService;
/**
* 工具名称和描述非常重要:LLM根据描述决定是否调用这个工具
* 描述要准确、具体,避免歧义
*/
@Tool("查询订单状态和物流信息。返回订单状态、快递单号和最新物流位置")
public OrderStatusResult queryOrderStatus(
@P("订单编号,格式为数字") String orderId
) {
log.info("查询订单状态: {}", orderId);
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("订单不存在: " + orderId));
OrderStatusResult result = new OrderStatusResult();
result.setOrderId(orderId);
result.setStatus(order.getStatus().getDisplayName());
result.setCreatedAt(order.getCreatedAt().toString());
// 已发货的订单查物流
if (order.getStatus() == OrderStatus.SHIPPED) {
ShippingInfo shipping = shippingService.track(order.getTrackingNumber());
result.setTrackingNumber(order.getTrackingNumber());
result.setLatestLocation(shipping.getLatestLocation());
result.setEstimatedDelivery(shipping.getEstimatedDelivery());
}
return result;
}
/**
* 工具可以执行写操作,但要加适当的前置校验
*/
@Tool("申请订单退款。只有已支付未发货的订单才能退款,退款将在3-5个工作日内到账")
public RefundResult applyRefund(
@P("订单编号") String orderId,
@P("退款原因,如:不想要了、买错了、质量问题") String reason
) {
log.info("申请退款: orderId={}, reason={}", orderId, reason);
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("订单不存在: " + orderId));
// 前置校验
if (order.getStatus() != OrderStatus.PAID) {
return RefundResult.failed("订单状态为" + order.getStatus().getDisplayName()
+ ",不符合退款条件");
}
// 执行退款
String refundId = orderRepo.createRefund(orderId, reason);
return RefundResult.success(refundId, "退款申请已提交,预计3-5个工作日到账");
}
/**
* 返回值类型的设计:结构化对象比String更好
* LLM能从结构化数据中提取信息,生成更准确的回答
*/
@Data
public static class OrderStatusResult {
private String orderId;
private String status;
private String createdAt;
private String trackingNumber;
private String latestLocation;
private String estimatedDelivery;
}
@Data
public static class RefundResult {
private boolean success;
private String refundId;
private String message;
public static RefundResult success(String refundId, String message) {
RefundResult r = new RefundResult();
r.setSuccess(true);
r.setRefundId(refundId);
r.setMessage(message);
return r;
}
public static RefundResult failed(String message) {
RefundResult r = new RefundResult();
r.setSuccess(false);
r.setMessage(message);
return r;
}
}
}把Tool注册到AI Service
/**
* 客服AI Service:集成工具调用能力
*/
@Configuration
@RequiredArgsConstructor
public class CustomerServiceAiConfig {
private final ChatLanguageModel chatModel;
private final OrderTools orderTools;
private final ProductTools productTools; // 其他工具集合
@Bean
public CustomerServiceAssistant customerServiceAssistant() {
return AiServices.builder(CustomerServiceAssistant.class)
.chatLanguageModel(chatModel)
.tools(orderTools, productTools) // 注册多个工具类
.chatMemoryProvider(userId ->
MessageWindowChatMemory.withMaxMessages(30))
.build();
}
}
@AiService
interface CustomerServiceAssistant {
@SystemMessage("""
你是XX平台的客服助手。
你有以下能力:
- 查询订单状态和物流信息
- 代替用户申请退款(已支付未发货的订单)
- 查询商品信息
处理原则:
- 先通过工具获取准确信息,再回答用户
- 对于退款操作,确认用户意图后才执行
- 无法处理的问题,告知用户转接人工客服
""")
String serve(@MemoryId String userId, @UserMessage String message);
}工具执行的异常处理
工具执行失败时,LangChain4j会把异常信息作为工具结果反馈给LLM,让LLM据此回答用户。但这个默认行为有时不够好:
/**
* 工具异常处理的最佳实践
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class RobustOrderTools {
private final OrderRepository orderRepo;
@Tool("查询订单状态")
public String queryOrderSafe(
@P("订单编号") String orderId
) {
// 方式一:返回错误描述字符串(让LLM自然表达)
// 不要直接抛异常,异常堆栈对LLM没用
if (!orderId.matches("\\d{10,18}")) {
return "错误:订单编号格式不正确,应为10-18位数字";
}
try {
Order order = orderRepo.findById(orderId)
.orElse(null);
if (order == null) {
return "未找到该订单,请确认订单编号是否正确";
}
return String.format(
"订单%s状态:%s,下单时间:%s",
orderId,
order.getStatus().getDisplayName(),
order.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
);
} catch (Exception e) {
log.error("查询订单失败: {}", orderId, e);
// 不把技术错误暴露给用户,返回友好提示
return "查询订单信息时遇到系统问题,请稍后重试或联系人工客服";
}
}
/**
* 方式二:返回结构化结果(含错误码)
* 当LLM需要根据错误类型做不同处理时使用
*/
@Tool("申请退款")
public RefundResponse requestRefund(
@P("订单编号") String orderId,
@P("退款原因") String reason
) {
try {
Order order = orderRepo.findById(orderId)
.orElse(null);
if (order == null) {
return new RefundResponse("ORDER_NOT_FOUND", null, "订单不存在");
}
if (order.getStatus() != OrderStatus.PAID) {
return new RefundResponse("INVALID_STATUS", null,
"当前订单状态为" + order.getStatus().getDisplayName() + ",无法退款");
}
String refundId = orderRepo.createRefund(orderId, reason);
return new RefundResponse("SUCCESS", refundId, "退款申请成功");
} catch (Exception e) {
log.error("退款操作失败: {}", orderId, e);
return new RefundResponse("SYSTEM_ERROR", null, "系统繁忙,请稍后重试");
}
}
public record RefundResponse(String code, String refundId, String message) {}
}需要用户确认的工具操作
有些操作需要在AI执行前让用户确认(比如退款、下单)。LangChain4j没有内置的"暂停等待确认"机制,但可以用两步对话来实现:
/**
* 需要确认的敏感操作处理
* 通过System Prompt的行为约束 + 状态管理来实现
*/
@AiService
interface ConfirmableAssistant {
@SystemMessage("""
你是订单服务助手。
对于以下操作,必须先向用户确认,用户明确同意后才能执行:
- 申请退款
- 取消订单
确认流程:
1. 描述将要执行的操作(如:将为订单XXXX申请退款)
2. 询问用户:"请问确认要这样处理吗?(是/否)"
3. 用户说"是"、"确认"、"好的"等肯定词后,才调用相应工具执行
4. 如果用户否定,取消操作并询问用户需要什么帮助
对于查询类操作(查订单状态、查物流等),不需要确认,直接执行。
""")
String chat(@MemoryId String userId, @UserMessage String message);
}这个方案依赖System Prompt的约束,对模型能力有一定要求。比较稳定的方案是在业务层面做控制——敏感操作先返回"待确认"状态,等用户二次确认后再真正执行。
工具的参数设计
工具参数的描述质量直接影响LLM能否正确调用:
@Component
public class SearchTools {
private final ProductRepository productRepo;
/**
* 参数描述的好坏对比
*/
// 差的参数描述
@Tool("搜索商品")
public List<Product> searchBad(
@P("关键词") String keyword, // 太简单,LLM不知道格式
@P("价格") String price // 不明确,单个价格还是范围?
) { ... }
// 好的参数描述
@Tool("按关键词和价格范围搜索商品,返回最多10条结果")
public ProductSearchResult searchGood(
@P("搜索关键词,支持商品名称、品牌、型号,例如:iPhone 15、耐克跑鞋")
String keyword,
@P("最低价格(元),填0表示不限制,例如:100")
double minPrice,
@P("最高价格(元),填99999表示不限制,例如:1000")
double maxPrice,
@P("每页返回数量,1-10之间的整数,默认填5")
int pageSize
) {
return productRepo.search(keyword, minPrice, maxPrice, pageSize);
}
@Data
public static class ProductSearchResult {
private int totalCount;
private List<ProductSummary> products;
@Data
public static class ProductSummary {
private String productId;
private String name;
private String price; // 格式化为"¥199.00",比double更好理解
private String stock; // "充足"/"库存紧张"/"已售罄"
}
}
}参数设计的几个原则:
- 描述要具体:包含格式说明和示例值
- 返回值用结构化对象:方便LLM提取关键信息
- 返回值里用人类可读的格式:价格用"¥199.00"而不是199.00,时间用"2024-01-15 14:30"而不是毫秒时间戳
- 工具数量不要太多:一个AI Service注册超过10个工具,LLM选择正确工具的准确率会下降
工具调用的监控和调试
/**
* 工具调用监控:记录调用链路,方便排查问题
*/
@Aspect
@Component
@Slf4j
public class ToolCallMonitorAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(tool)")
public Object monitorToolCall(ProceedingJoinPoint pjp, Tool tool) throws Throwable {
String toolName = pjp.getSignature().getName();
long startTime = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - startTime;
// 记录成功指标
meterRegistry.counter("tool.call.success", "tool", toolName).increment();
meterRegistry.timer("tool.call.duration", "tool", toolName)
.record(duration, TimeUnit.MILLISECONDS);
log.debug("工具调用成功: {}, 耗时: {}ms", toolName, duration);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
// 记录失败指标
meterRegistry.counter("tool.call.failure",
"tool", toolName,
"error", e.getClass().getSimpleName()
).increment();
log.warn("工具调用失败: {}, 耗时: {}ms, 错误: {}",
toolName, duration, e.getMessage());
// 不重新抛异常:工具内部错误应该被处理,不应该影响AI Service的运行
return "工具执行失败: " + e.getMessage();
}
}
}Tool Calling用好了,AI从"话痨"变成了"实干派"。关键是:工具定义要清晰、参数描述要具体、异常处理要友好、敏感操作要有确认机制。
