LangChain4j 的 @Tool 不是贴个注解:工具参数才是稳定性的关键
很多 Java 开发者第一次接 LangChain4j 的工具调用时,最容易低估一件事:@Tool 方法不是普通 Service 方法换个注解,它更像一份暴露给大模型的接口契约。
普通接口调用里,调用方是确定的代码;工具调用里,调用方变成了大模型。模型会根据工具名、描述、参数名、参数说明和上下文,决定是否调用、调用哪个、传什么参数。于是问题就来了:如果工具方法写得太随意,线上表现往往不是“完全不可用”,而是“偶尔调错、偶尔漏参数、偶尔传 null”。这种问题最难排。
本文只讲一个小切口:在 Spring Boot 项目里,如何把 LangChain4j 的 @Tool 方法写得更像稳定接口,而不是一个靠模型理解能力硬撑的魔法方法。
真正容易翻车的不是模型会不会调用工具
LangChain4j 官方文档把工具调用说得很清楚:大模型并不会真的执行代码,它只是返回“我想调用某个工具,以及参数是什么”的意图,真正执行工具的是应用程序。
这和 Java 后端很像。Controller 收到 HTTP 请求,参数绑定、校验、鉴权、调用 Service,最后返回结果。区别在于,HTTP 请求通常来自前端或其他服务,而工具调用请求来自模型生成的结构化参数。
所以工具调用的稳定性,关键不是“让模型变聪明”,而是把工具契约写清楚:
| 设计项 | 差写法 | 更稳的写法 |
|---|---|---|
| 工具名 | get、query | getOrderStatus |
| 工具描述 | “查询订单” | “当用户询问当前登录用户的订单状态、物流状态时使用” |
| 参数名 | arg0、id | orderNo |
| 参数说明 | 不写 | 写清格式、来源、限制 |
| 可选参数 | 依赖模型猜 | Optional<T>、@P(required = false) 或 P(defaultValue = "...") |
| 安全参数 | 让模型传 userId | 从登录上下文获取 |
LangChain4j 文档也特别提醒:如果没有保留 Java 方法参数名,反射可能只能拿到 arg0、arg1 这类无语义名称。Spring 和 Quarkus 通常会启用 -parameters,但在工具方法上显式写 P(name = ..., description = ...) 依然更利于维护,尤其是多人协作和代码审查时。
一个订单查询工具的最小写法
下面这个例子不是为了做完整客服 Agent,只展示工具方法的关键写法。
<properties>
<langchain4j.version>1.16.2-beta26</langchain4j.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
</dependencies>
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY}
model-name: gpt-4o-mini
AI Service 可以像普通 Spring Bean 一样注入。项目里如果存在多个模型、多个工具集,建议使用显式接线,避免所有工具被自动塞进同一个 AI Service。
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
wiringMode = EXPLICIT,
chatModel = "openAiChatModel",
tools = "orderTools"
)
public interface OrderAssistant {
@SystemMessage("""
You are an order assistant.
Use tools only when order status or delivery information is needed.
Do not guess order data.
""")
String chat(String userMessage);
}
工具方法这样写会更稳:
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@Component("orderTools")
public class OrderTools {
private final OrderService orderService;
private final CurrentUser currentUser;
public OrderTools(OrderService orderService, CurrentUser currentUser) {
this.orderService = orderService;
this.currentUser = currentUser;
}
@Tool("""
Query the current user's order status.
Use this tool when the user asks about order status, shipping,
delivery progress, or whether an order has been paid.
""")
public OrderSnapshot getOrderStatus(
@P(
name = "orderNo",
description = "Business order number provided by user, for example O202606110001"
)
String orderNo,
@P(
name = "includeItems",
description = "Whether to include order line items in the result",
defaultValue = "false"
)
boolean includeItems
) {
Assert.hasText(orderNo, "orderNo must not be blank");
String userId = currentUser.userId();
return orderService.getOrderForUser(userId, orderNo, includeItems);
}
}
这里有几个细节值得注意。
第一,userId 不让模型传。用户身份、租户 ID、权限范围、数据隔离条件,都应该来自后端上下文,而不是来自模型参数。模型可以帮你理解“用户想查哪个订单”,但不能成为权限事实的来源。
第二,includeItems 使用 defaultValue = "false"。如果用户只是问“订单到哪了”,没有必要返回完整明细。默认值既能减少模型遗漏参数带来的异常,也能控制返回数据量。
第三,工具返回值最好是结构清晰的 DTO,不要直接返回数据库实体。实体里可能带出内部字段,也可能因为懒加载、循环引用、序列化格式问题造成不必要的麻烦。
必填参数不是强校验
LangChain4j 当前文档里有一个很实际的提醒:required 更多是发送给模型的 JSON Schema 约束,模型理论上应该遵守,但实践中仍可能省略参数。
在 LangChain4j 1.x 中,缺失 primitive 参数会触发工具参数错误处理;但对象参数缺失时可能以 null 进入工具方法。官方也提到,2.0 计划统一这类校验行为。
这对 Java 开发者的启发很直接:不要把工具参数校验完全交给框架或模型。
@Tool("Query invoice by invoice number")
public InvoiceSnapshot getInvoice(
@P(name = "invoiceNo", description = "Invoice number from user message")
String invoiceNo
) {
Assert.hasText(invoiceNo, "invoiceNo must not be blank");
return invoiceService.getInvoice(invoiceNo);
}
如果参数确实可选,可以明确表达:
@Tool("Search current user's orders")
public List<OrderSnapshot> searchOrders(
@P(name = "keyword", description = "Order keyword, product name or order number")
String keyword,
@P(name = "status", description = "Order status filter", required = false)
Optional<OrderStatus> status
) {
Assert.hasText(keyword, "keyword must not be blank");
return orderService.search(keyword, status.orElse(null));
}
required = false、Optional<T>、defaultValue 不是一个意思。required = false 表示参数可以缺;Optional<T> 让 Java 代码层面表达缺失;defaultValue 是缺失时由 LangChain4j 填一个默认值。真实项目里不要混着乱用,最好在团队里约定一套规则。
Spring Boot 自动装配方便,但别失去边界

LangChain4j 的 Spring Boot starter 会扫描 @AiService 接口,也能把上下文里的 ChatModel、ChatMemory、RetrievalAugmentor、ToolProvider,以及带 @Tool 的 Spring Bean 方法接进去。
这对 Demo 很舒服,但在业务系统里要谨慎。
如果一个应用里有“订单助手”“售后助手”“运营助手”,每个助手可用的工具范围应该不同。订单助手不应该拥有退款审批工具,运营助手不应该拥有用户隐私查询工具。
所以建议:
- Demo 阶段可以使用自动接线,快速验证能力。
- 进入业务项目后,优先使用显式接线。
- 每个工具 Bean 按业务域拆分,例如 orderTools、refundTools、productTools。
- 高风险工具不要只靠 Prompt 限制,后端必须做鉴权、参数校验和审计日志。
- 工具返回值要做脱敏,尤其是手机号、地址、证件号、支付信息。
工具调用不是绕过后端工程规范的新通道,而是多了一个由模型触发的入口。入口越智能,后端边界越要清楚。
上线前检查三件事
第一,检查工具描述是否能被“不了解你项目的人”看懂。模型看到的不是你的业务背景,而是工具名、描述、参数 Schema 和对话上下文。描述越含糊,误调用概率越高。
第二,检查参数是否有后端校验。尤其是 String、包装类型、复杂对象,不要以为写了 required 就一定不会是 null。能用枚举就不要用自由字符串,能用默认值就不要让模型猜。
第三,检查工具是否有日志和观测。至少记录工具名、参数摘要、耗时、结果类型、异常原因。不要直接记录完整敏感数据。LangChain4j 也提供了 Spring Boot 下的监听器、Micrometer metrics 和 Observation 相关支持,可以逐步接入到现有 Actuator、日志和链路追踪体系里。
把 @Tool 写好,本质上不是 LangChain4j 技巧,而是 Java 后端接口设计能力的迁移。模型可以负责理解意图,但参数契约、权限边界、失败处理、可观测性,仍然是后端工程的责任。AI 应用越往真实业务走,这些看起来“不够炫”的细节越决定系统能不能长期运行。
更多推荐

所有评论(0)