很多 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 很舒服,但在业务系统里要谨慎。

如果一个应用里有“订单助手”“售后助手”“运营助手”,每个助手可用的工具范围应该不同。订单助手不应该拥有退款审批工具,运营助手不应该拥有用户隐私查询工具。

所以建议:

  1. Demo 阶段可以使用自动接线,快速验证能力。
  2. 进入业务项目后,优先使用显式接线。
  3. 每个工具 Bean 按业务域拆分,例如 orderTools、refundTools、productTools。
  4. 高风险工具不要只靠 Prompt 限制,后端必须做鉴权、参数校验和审计日志。
  5. 工具返回值要做脱敏,尤其是手机号、地址、证件号、支付信息。

工具调用不是绕过后端工程规范的新通道,而是多了一个由模型触发的入口。入口越智能,后端边界越要清楚。

上线前检查三件事

第一,检查工具描述是否能被“不了解你项目的人”看懂。模型看到的不是你的业务背景,而是工具名、描述、参数 Schema 和对话上下文。描述越含糊,误调用概率越高。

第二,检查参数是否有后端校验。尤其是 String、包装类型、复杂对象,不要以为写了 required 就一定不会是 null。能用枚举就不要用自由字符串,能用默认值就不要让模型猜。

第三,检查工具是否有日志和观测。至少记录工具名、参数摘要、耗时、结果类型、异常原因。不要直接记录完整敏感数据。LangChain4j 也提供了 Spring Boot 下的监听器、Micrometer metrics 和 Observation 相关支持,可以逐步接入到现有 Actuator、日志和链路追踪体系里。

把 @Tool 写好,本质上不是 LangChain4j 技巧,而是 Java 后端接口设计能力的迁移。模型可以负责理解意图,但参数契约、权限边界、失败处理、可观测性,仍然是后端工程的责任。AI 应用越往真实业务走,这些看起来“不够炫”的细节越决定系统能不能长期运行。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐