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 → 框架自动执行 → 返回精确结果
背后的自动行为:
- 扫描
Calculator中所有@Tool方法 - 自动生成
ToolSpecification(含参数 Schema) - 每次请求发给 LLM
- LLM 返回
ToolExecutionRequest时,反射调用对应方法 - 结果自动反馈给 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();
工作方式:
- 初次请求只包含搜索工具 (
toolSearch,toolSearchInBatch) - LLM 调用搜索工具描述它需要的功能
- 框架匹配并注入对应工具
- 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 交互的过程:
- 主 Agent(LLM)根据 JSON 描述决定调用该 Tool,返回调用指令 JSON。
- 框架拦截指令并通过反射触发子
AiService的代理方法。- 子
AiService内部发起一次全新的、独立的 LLM 请求获取结果。- 框架将子 Agent 的返回结果作为
ToolMessage喂回给主 Agent,由主 Agent 整合输出。
Q4:一句话总结 Tool Calling 的底层逻辑是什么?
A4: 代码用 JSON 声明能力,LLM 用 JSON 选择并传参,框架用反射执行真实方法。