【LangChain4j系列04】Tools 工具调用机制详解

Function Calling(工具调用)让 LLM 不仅能「说」,还能「做」------查询数据库、调用 API、执行计算。本章从底层到高层,全面解析 LangChain4j 的工具调用体系。


4.1 为什么需要工具调用?

LLM 的根本局限之一是 知识截止于训练数据 ,且无法执行操作。以一个简单的算术问题为例:

css 复制代码
❌ 没有工具:  "475695037565 的平方根是多少?"
   → LLM: "大约是 689710"(编造的,实际是 689706.486532)

✅ 有工具:   声明 squareRoot(double x) 工具
   → LLM: [调用 squareRoot(475695037565)]
   → 执行:  689706.486532
   → LLM: "475695037565 的平方根是 689706.486532"

工具调用的本质流程

css 复制代码
User: "明天伦敦天气怎么样?"
  ↓
LLM 响应: 不是文本,而是 ToolExecutionRequest(name="getWeather", arguments={"city":"London"})
  ↓
开发者执行: getWeather("London") → "预计伦敦明天下雨"
  ↓
发回 LLM: ToolExecutionResultMessage(result="预计伦敦明天下雨")
  ↓
LLM 最终回复: "伦敦明天预计会下雨,建议带伞。"

关键理解:LLM 不能自己调用工具。它只"表达调用意图",你必须执行并反馈结果。


4.2 两个层级的工具 API

makefile 复制代码
高层: @Tool 注解 + AI Services 自动执行
      → 一行注解,框架自动完成"声明→调用→执行→反馈"全流程

低层: ToolSpecification + ChatRequest.toolSpecifications()
      → 手动构造工具描述 → 解析 ToolExecutionRequest → 手动执行 → 手动反馈

4.3 高层 API:@Tool 注解

最简示例

java 复制代码
// 定义工具类
class Calculator {
    @Tool("计算两个整数的和")
    int add(int a, int b) {
        return a + b;
    }

    @Tool("计算一个数的平方根")
    double squareRoot(double x) {
        return Math.sqrt(x);
    }
}

// 创建带工具的 AI Service
interface MathGenius {
    String ask(String question);
}

MathGenius genius = AiServices.builder(MathGenius.class)
    .chatModel(model)
    .tools(new Calculator())   // 一行注入
    .build();

String answer = genius.ask("475695037565 的平方根是多少?");
// LLM 自动调用 squareRoot → 框架自动执行 → 返回精确结果

背后的自动行为

  1. 扫描 Calculator 中所有 @Tool 方法
  2. 自动生成 ToolSpecification(含参数 Schema)
  3. 每次请求发给 LLM
  4. LLM 返回 ToolExecutionRequest 时,反射调用对应方法
  5. 结果自动反馈给 LLM

4.4 @Tool 注解详解

java 复制代码
public @interface Tool {
    String value() default "";             // 工具描述(也是 name 的默认值)
    String name() default "";              // 工具名(默认取方法名)
    ReturnBehavior returnBehavior()         // 返回行为
        default ReturnBehavior.TO_LLM;
    String metadata() default "";          // JSON 格式的 Provider 特定元数据
    SearchBehavior searchBehavior()         // 工具搜索行为
        default SearchBehavior.SEARCHABLE;
}

工具描述最佳实践

java 复制代码
// ❌ 不够清晰
@Tool
String getWeather(String city) { ... }

// ✅ 清晰、具体、包含使用场景
@Tool("Returns the current weather forecast for a given city. " +
      "Use this when the user asks about weather conditions.")
String getWeather(
    @P("The city name, e.g. 'London', 'Tokyo'") String city,
    @P("Temperature unit: CELSIUS or FAHRENHEIT") TemperatureUnit unit
) { ... }

经验法则:如果一个人类能通过描述理解工具的用途和使用方式,LLM 通常也能。


4.5 @P 注解:参数描述与配置

java 复制代码
public @interface P {
    String value() default "";             // 参数描述
    String name() default "";              // 暴露给 LLM 的参数名
    boolean required() default true;       // 是否必需(默认 true)
    String defaultValue() default "";      // 默认值(设置后自动变为 optional)
}

参数类型支持

java 复制代码
class ParameterTypes {
    @Tool
    void demo(
        // 基本类型
        int count,
        double price,
        boolean enabled,

        // 包装类型
        String name,
        Integer age,
        Optional<String> nickname,    // Optional = 可选参数

        // 枚举
        SortBy sortBy,                // LLM 可以看到枚举值列表

        // 复杂类型(POJO)
        QueryParams params,           // 自动生成 JSON Schema

        // 集合类型
        List<String> tags,
        Set<UUID> ids,

        // 多态类型
        Animal animal,                // 密封接口 → anyOf Schema
        List<Animal> animals,

        // 框架注入(LLM 无感知)
        @ToolMemoryId Object memoryId,
        InvocationParameters invocationParams,
        InvocationContext context
    ) {}
}

默认值机制

java 复制代码
enum SortBy { RELEVANCE, DATE, RATING }

@Tool
List<Article> searchArticles(
    String query,
    @P(defaultValue = "10") int limit,                  // 省略时为 10
    @P(defaultValue = "[\"en\"]") List<String> languages, // 省略时 ["en"]
    @P(defaultValue = "RELEVANCE") SortBy sortBy          // 省略时 RELEVANCE
) {
    // ...
}

规则:

  • defaultValue 的参数自动从 JSON Schema 的 required 中移除
  • 值在 AI Service 注册时解析(非法值启动时即报错)
  • 每次调用都重新解析,所以修改默认 List/Map 不会污染下次调用

4.6 工具方法的返回值和 ReturnBehavior

默认行为:TO_LLM

java 复制代码
// 结果发给 LLM,让 LLM 基于结果生成最终回复
@Tool
String getWeather(String city) {
    return "上海今天 25°C,晴";
}
// LLM 收到结果后回复用户:"上海今天天气晴朗,气温 25°C。"

立即返回:IMMEDIATE

java 复制代码
// 跳过 LLM 二次处理,直接返回工具结果
@Tool(returnBehavior = ReturnBehavior.IMMEDIATE)
double add(int a, int b) {
    return a + b;
}
// 对于 "37+87=?" ,用户直接得到 124.0,无需 LLM 润色

限制 :使用 IMMEDIATE 时,AI Service 必须返回 Result<T>

java 复制代码
interface Calculator {
    Result<String> calculate(String expression);
}

Result<String> result = calculator.calculate("37+87=?");
// result.content() 为 null ,因为模型并未输出结果,而是工具调用输出的结果
// result.toolExecutions().get(0).result() 为 "124.0"

IMMEDIATE_IF_LAST

java 复制代码
class ScreenAutomation {
    @Tool
    String leftMouseClick(int x, int y) { ... }

    @Tool
    String typeText(String text) { ... }

    @Tool(returnBehavior = ReturnBehavior.IMMEDIATE_IF_LAST)
    String endExecutionAndGetFinalResult(String summary) { return summary; }
}

当 LLM 在一次响应中调用 [leftMouseClick, typeText, endExecutionAndGetFinalResult],三个工具顺序执行后直接返回最终结果,省去一轮 LLM 交互。

多模态返回值

java 复制代码
@Tool("Takes a photo and returns it")
Image takePhoto() {
    byte[] imageBytes = camera.capture();
    return Image.builder()
        .base64Data(Base64.getEncoder().encodeToString(imageBytes))
        .mimeType("image/png")
        .build();
}

@Tool("Returns a photo with description")
List<Content> analyzePhoto() {
    return List.of(
        TextContent.from("Photo taken at " + LocalDateTime.now()),
        ImageContent.from(camera.capture())
    );
}

4.7 低层 API:ToolSpecification

当需要完全控制工具声明时(如动态生成、从配置文件加载、跨系统传输):

手动构建 ToolSpecification

java 复制代码
ToolSpecification toolSpec = ToolSpecification.builder()
    .name("getWeather")
    .description("Returns the weather forecast for a given city")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("city", "The city name")
        .addEnumProperty("temperatureUnit",
            List.of("CELSIUS", "FAHRENHEIT"))
        .required("city")
        .build())
    .build();

// 使用
ChatRequest request = ChatRequest.builder()
    .messages(UserMessage.from("伦敦明天天气?"))
    .toolSpecifications(toolSpec)
    .build();

ChatResponse response = model.chat(request);

if (response.aiMessage().hasToolExecutionRequests()) {
    for (ToolExecutionRequest req : response.aiMessage().toolExecutionRequests()) {
        String result = executeTool(req); // 你需要自己实现这个
        ToolExecutionResultMessage resultMsg = ToolExecutionResultMessage.from(req, result);

        // 反馈给 LLM
        ChatRequest followUp = ChatRequest.builder()
            .messages(userMsg, aiMsg, resultMsg)
            .toolSpecifications(toolSpec)
            .build();
        response = model.chat(followUp);
    }
}

从 @Tool 类生成 ToolSpecification

java 复制代码
class WeatherTools {
    @Tool("Returns weather forecast")
    String getWeather(
        @P("City name") String city,
        @P("Temperature unit") TemperatureUnit unit
    ) { ... }
}

// 一行生成
List<ToolSpecification> specs = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);

// 也可以从单个方法生成
Method method = WeatherTools.class.getMethod("getWeather", String.class, TemperatureUnit.class);
ToolSpecification spec = ToolSpecifications.toolSpecificationFrom(method);

4.8 编程式工具与动态工具

编程式工具(Programmatic Tools)

适合从数据库、配置文件动态生成工具:

java 复制代码
// Step 1: 定义 ToolSpecification
ToolSpecification spec = ToolSpecification.builder()
    .name("get_booking_details")
    .description("Returns booking details by number")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("bookingNumber", "Booking number in B-12345 format")
        .required("bookingNumber")
        .build())
    .build();

// Step 2: 实现 ToolExecutor
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
    Map<String, Object> args = fromJson(toolExecutionRequest.arguments());
    String bookingNumber = args.get("bookingNumber").toString();
    Booking booking = bookingService.findByNumber(bookingNumber);
    return booking.toString();
};

// 或通过反射绑定已有方法
BookingTools tools = new BookingTools();
Method method = BookingTools.class.getMethod("getBookingDetails", String.class);
ToolExecutor executor2 = new DefaultToolExecutor(tools, method);

// Step 3: 封装为 AiServiceTool
AiServiceTool tool = AiServiceTool.builder()
    .toolSpecification(spec)
    .toolExecutor(executor)
    .returnBehavior(ReturnBehavior.TO_LLM)  // 可选
    .build();

// Step 4: 注入
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .tools(List.of(tool))
    .build();

动态工具提供者(ToolProvider)

每次调用时动态决定提供哪些工具:

java 复制代码
ToolProvider toolProvider = (toolProviderRequest) -> {
    // 获取用户消息和上下文
    String userText = toolProviderRequest.userMessage().singleText();
    Object memoryId = toolProviderRequest.memoryId();

    if (userText.contains("booking")) {
        return ToolProviderResult.builder()
            .add(bookingToolSpec, bookingExecutor)
            .build();
    } else if (userText.contains("weather")) {
        return ToolProviderResult.builder()
            .add(weatherToolSpec, weatherExecutor)
            .build();
    }
    return null; // 不提供工具
};

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .toolProvider(toolProvider)
    .build();

4.9 工具搜索(Tool Search)

当工具数量庞大(数十个甚至上百个),每次都把所有工具描述发给 LLM 会消耗大量 Token。Tool Search 让 LLM 按需发现工具:

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemory(chatMemory)     // 必须有记忆
    .tools(allTools)            // 所有可用工具
    .toolSearchStrategy(new SimpleToolSearchStrategy())  // 关键字匹配
    .build();

工作方式

  1. 初次请求只包含搜索工具toolSearch, toolSearchInBatch
  2. LLM 调用搜索工具描述它需要的功能
  3. 框架匹配并注入对应工具
  4. LLM 使用找到的工具完成实际调用

两种内置策略:

  • SimpleToolSearchStrategy --- 关键字匹配,零依赖
  • VectorToolSearchStrategy --- 语义搜索,需要 EmbeddingModel

Always-Visible 工具:始终在首轮请求就暴露的工具:

java 复制代码
@Tool(searchBehavior = SearchBehavior.ALWAYS_VISIBLE)
String getWeather(String city) { ... }

4.10 错误处理

工具名幻觉(Hallucinated Tool Name)

java 复制代码
// LLM 编造了一个不存在的工具名时:
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .tools(new MyTools())
    .hallucinatedToolNameStrategy(toolExecutionRequest ->
        ToolExecutionResultMessage.from(toolExecutionRequest,
            "Error: there is no tool called " + toolExecutionRequest.name())
    )
    .build();
// 错误消息发回 LLM,让 LLM 重新选择

参数错误

java 复制代码
.toolArgumentsErrorHandler((error, errorContext) ->
    // 返回错误消息给 LLM,让它修正参数后重试
    ToolErrorHandlerResult.text("参数错误: " + error.getMessage())
)

执行异常

java 复制代码
.toolExecutionErrorHandler((error, errorContext) ->
    // ⚠️ 生产环境永远不要返回原始异常信息(可能泄露堆栈/路径/凭证)
    ToolErrorHandlerResult.text("工具执行失败,请稍后重试。")
)

4.11 补偿机制(Compensation)

多工具调用链中,后面的工具失败时需要回滚前面的操作:

java 复制代码
class BankAccountService {
    @Tool("credits money to a bank account")
    String credit(String name, double amount) {
        accounts.merge(name, amount, Double::sum);
        return createTransactionId();
    }

    @CompensateFor("credit")
    void uncredit(String name, double amount) {
        accounts.merge(name, -amount, Double::sum);
    }

    @Tool("withdraws money from a bank account")
    String withdraw(String name, double amount) {
        if (accounts.getOrDefault(name, 0.0) < amount) {
            throw new RuntimeException("Insufficient funds");
        }
        accounts.merge(name, -amount, Double::sum);
        return createTransactionId();
    }

    @CompensateFor("withdraw")
    void unwithdraw(String name, double amount) {
        accounts.merge(name, amount, Double::sum);
    }
}

// 启用补偿
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .tools(new BankAccountService())
    .compensateOnToolErrors(true)   // 开启!
    .build();

行为

  • 工具执行失败时,所有已成功执行且有 @CompensateFor 的工具按逆序补偿
  • LLM 收到每条补偿结果的消息,可以决定后续操作
  • 补偿是 best-effort(失败记录 WARN 日志,继续补偿剩余的)

LLM 负责主动决策,代码负责被动兜底。


4.12 InvocationParameters:传递上下文数据

工具方法可以接收 InvocationParameters 获取调用方传入的上下文:

java 复制代码
interface Assistant {
    String chat(@UserMessage String userMessage, InvocationParameters parameters);
}

class Tools {
    @Tool
    String getWeather(String city, InvocationParameters parameters) {
        String userId = parameters.get("userId");
        String tenant = parameters.get("tenant");
        // 根据用户偏好和租户配置返回天气
        UserPreferences prefs = getUserPreferences(userId, tenant);
        return weatherService.getWeather(city, prefs.temperatureUnits());
    }
}

// 调用
InvocationParameters params = InvocationParameters.from(Map.of(
    "userId", "12345",
    "tenant", "acme-corp"
));
String response = assistant.chat("上海天气?", params);

也可以在 @ToolMemoryId 中获取 Memory ID,实现多用户工具调用隔离。


4.13 AI Service 作为工具

一个 AI Service 可以作为另一个 AI Service 的工具方法(Agentic 路由):

java 复制代码
// 专家服务
interface MedicalExpert {
    @Tool("A medical expert for health-related questions")
    String answer(String question);
}

interface LegalExpert {
    @Tool("A legal expert for law-related questions")
    String answer(String question);
}

// 路由代理
interface RouterAgent {
    String askToExpert(String question);
}

// 组装
MedicalExpert medical = AiServices.create(MedicalExpert.class, model);
LegalExpert legal = AiServices.create(LegalExpert.class, model);

RouterAgent router = AiServices.builder(RouterAgent.class)
    .chatModel(model)
    .tools(medical, legal)   // 两个 AI Service 作为工具!
    .build();

router.askToExpert("我腿骨折了该怎么办?");
// → LLM 调用 MedicalExpert → 返回医疗建议

注意:这种方式的缺点是 LLM 需要把用户请求完整地作为工具参数传递(可能出错),且被调用的 AI Service 无法访问调用者的 ChatMemory。


💡 Q & A

Q1:.tools(...) 方法接收的参数到底是什么类型?

A1: 表面上是 Object...(任意 Java 对象) 。只要该对象的方法上标有 @Tool 注解,无论是普通的 Service、DAO,还是通过 AiServices 动态代理生成的另一个 Agent,都可以作为工具传入。

Q2:为什么不同类型的 Java 对象都能被 LLM 识别和调用?

A2: 因为 LLM 只认 JSON 。框架会通过反射扫描传入对象中带有 @Tool 的方法,提取其名称、描述及参数,统一序列化为 JSON Schema 暴露给大模型。

Q3:当代码传入另一个 AiService 作 Tool 时(如嵌套 Agent),底层是如何运行的?

A3: 这是一个双向 JSON 交互的过程:

  1. 主 Agent(LLM)根据 JSON 描述决定调用该 Tool,返回调用指令 JSON。
  2. 框架拦截指令并通过反射触发子 AiService 的代理方法。
  3. AiService 内部发起一次全新的、独立的 LLM 请求获取结果。
  4. 框架将子 Agent 的返回结果作为 ToolMessage 喂回给主 Agent,由主 Agent 整合输出。

Q4:一句话总结 Tool Calling 的底层逻辑是什么?

A4: 代码用 JSON 声明能力,LLM 用 JSON 选择并传参,框架用反射执行真实方法。

相关推荐
用户69371750013842 小时前
深夜炸场!DeepSeek 没发新模型,却重构了整个 Agent 生态
前端·后端·github
fthux2 小时前
装闭 RenoPit 源码解析(08):多模态AI调用、重试与文本降级
人工智能·ai·开源·github·open source·renopit
Raas1002 小时前
MAIGateway,魔芋企业级AI网关的安全基建化设计
大数据·人工智能·网关·网络安全·api网关·mai gateway·魔芋
冬奇Lab2 小时前
开源项目第186期:Open Ontologies — Rust 实现的 AI 原生本体工程 MCP 服务器
人工智能·开源·资讯
tachibana22 小时前
文件上传分布式限流如何做?
人工智能·ai·大模型·llm·prompt
武子康2 小时前
实现 GPT-Live-like:两条路线、一个控制面和六阶段验收
人工智能·chatgpt·agent
郑州光合科技余经理2 小时前
餐饮预定系统架构拆解:订单链路、权限组织与私有化源码交付
java·开发语言·前端·数据库·人工智能·系统架构·php
Wang's Blog2 小时前
AI Agent白手起家71: LangGraph 云平台与本地开发实战
人工智能
九硕智慧建筑一体化厂家2 小时前
打破系统孤岛!IBMS 一体化平台,构建智慧楼宇全域管控体系
运维·网络·人工智能·笔记·智慧城市