何为 Tools 和 Function

工具和函数调用是什么意思

在一些大语言模型(LLM)中,除了文本生成之外,还具备触发 工具(tools)或函数调用(function calling) 的能力,这才能让 LLM 被赋予它与外部世界交互的能力。

2023年 OpenAI 在 GPT API 中推出了Function Call,类似传统 API 设计思想,让 LLM 按 JSON 格式填写参数,由后端调用接口。这一机制允许 LLM 在需要时调用一个或多个由开发者预先定义的工具。工具的形式可以非常多样,例如执行网页搜索、调用外部 API、运行特定代码等。

模型被训练成能够理解函数描述,并精准输出符合 JSON Schema 的参数。它的输出不是给用户看的,而是给程序解析的。

而 Tool Calling 是一个更广义的、与具体模型实现无关的概念,通常在 Agent 中被提到。在 AI Agent 或 LLM 应用中,我们给模型提供一组工具(Tool),模型可以自主决定什么时候调用哪个工具以及用什么参数,从而完成一个复杂任务。工具通常情况下是可以被模型调用的函数。

举个例子:我们知道 LLM 在数学计算方面并不总是可靠。如果应用场景中偶尔涉及数学问题,可以为模型提供一个数学计算工具。当请求中声明了这一工具后,LLM 就能在必要时选择调用它。例如,面对一个数学问题时,模型可能会先调用数学工具进行计算,然后再基于结果生成最终答案。

这样一来,工具调用机制不仅弥补了 LLM 的局限性,还显著增强了其在实际应用中的可扩展性与准确性。

而且,随着 Agent 框架发展起来,可以由 Agent 根据目标自主决策使用什么工具,完成多步规划与工具调用。

但各家工具不兼容、生态碎片化。为了解决这一问题,Anthropic在2024年提出了 MCP 协议,将 LLM 连接外部数据源和工具抽象成统一协议,成为各大模型通用标准。

Tools 和 Function 有什么不同,那么 MCP 和它们又是什么关系

最大问题就是 Function Calling 可以理解为大语言模型(LLM)的一项原生能力。它让模型在理解用户意图后,能够决定需要调用哪个外部工具,并生成一个结构化的调用指令,通常是 JSON

Tools Calling 可以看作是 Function Calling 的扩展和升级。它将“工具”的概念从单一的函数扩展到了更广泛的实体。

  • Function Calling 关注怎么调用,而 Tools Calling 关注调用什么,也就是工具本身。
  • Tools Calling 支持更复杂的交互,例如有状态的工具嵌套调用(一个工具调用另一个Agent)等。

Function Call 是调用一个函数,而 Tool Call 是使用一个工具。这个工具可以是一个函数、一个数据库、一个完整的API,甚至另一个AI Agent。

其实 Function Calling 是实现 Tool Calling 的一种具体协议。对于 OpenAI 模型,Tool Calling 能力就是通过其 Function Calling 接口实现的。

而且 Tool Calling 的范畴更大:即使模型没有原生的 function calling 能力,我们也可以通过提示词工程(如 ReAct 模式)让模型输出特定格式(如 {"tool":"xxx","args":{...}}),然后由外部解析执行,这也叫 Tool Calling。

简单来说:

  • Function Calling 是模型能输出调用请求的能力;Tool Calling 是系统利用这种能力让模型使用工具的整套机制。

MCP(Model Context Protocol,模型上下文协议)是一个开放的标准协议,所以说他和上面两个内容都不在一层中,MCP 是协议层中的内容。它的目标是统一AI应用与外部数据源和工具之间的连接方式。MCP实现了工具与AI模型的解耦,让工具集成变得更标准、更开放

在没有 MCP 时,为每个 AI 模型(如GPT-4、Claude)接入同一个工具,需要写多份不同的适配代码。MCP 就像 AI 界的通用接口,工具提供方只需按MCP标准封装一次(成为MCP Server),任何支持 MCP 的 AI 客户端都能直接调用。

Function Calling、Tools Calling 和 MCP 在AI系统共同协作:

  • Function Calling(决策层):负责思考与决策,输出调用指令。
  • Tools Calling(抽象层):提供更丰富的工具定义,扩展AI的能力边界。
  • MCP(协议层):建立统一的通信标准,让决策和工具能够标准化对接。

Function Calling 的工作原理

Function Calling 的基本流程如下

image-20260708160510930

首先,模型商会经过大量的训练,通过大量这种多轮对话的训练,模型学会了:

  • 何时调用:当用户问题无法仅靠内置知识回答,且某个函数的描述与之匹配时,应调用函数而不是瞎编。
  • 调用哪个:从给定的函数列表中,选出最相关的那个。
  • 如何填参数:从用户问题和对话历史中抽取合适的参数值,并按照函数定义的类型/格式要求输出。

当你的应用实际调用模型 API 时,Function Calling 是一个多步交互的过程,通常需要两次模型请求。

  • 发送请求,带上工具定义

    你发出的请求不再只有消息列表,还附带一个 tools 数组,描述当前可用的函数及其参数 JSON Schema。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    "model": "gpt-4",
    "messages": [{"role": "user", "content": "北京天气怎么样?"}],
    "tools": [{
    "type": "function",
    "function": {
    "name": "get_weather",
    "description": "获取指定城市的天气",
    "parameters": {
    "type": "object",
    "properties": {
    "city": {"type": "string", "description": "城市名"}
    },
    "required": ["city"]
    }
    }
    }]
    }
  • 模型决策并生成“调用指令”

    模型处理这段上下文,它需要回答:“我应该生成普通文本,还是调用一个工具?”这是一个二分类决策。如果它决定调用工具,就开始生成工具调用的结构化输出。

  • 你的代码(比如一个 LangChain4j 代理)接收到这个响应,提取 namearguments,反射调用对应的本地函数:

  • 将结果发回模型,获得最终回答

    你再次调用模型 API,消息历史中追加两样东西:

    • 刚才助手生成的 tool_calls 消息,告诉模型“你刚才要求调用了这个工具”
    • 一个新的 tool 角色消息,包含函数执行结果:

如何在 LangChain4j 中进行 Tool Calling 和 Function Calling

两种抽象级别

LangChain4j 提供了两种使用工具的抽象级别:

  • 低级:使用 ChatModelToolSpecification API
  • 高级:使用 AI Services@Tool 注解的 Java 方法

低级工具 API

在低级别中,你可以使用 ChatModelchat(ChatRequest) 方法。StreamingChatModel 中也有类似的方法。

在创建 ChatRequest 时,可以指定一个或多个 ToolSpecificationToolSpecification 是一个包含工具全部信息的对象,一般,尽可能提供详尽信息:清晰的名称、完整的描述、每个参数的说明等

  • 工具的 name
  • 工具的 description
  • 工具的 parameters 及其描述
1
2
3
4
5
6
7
8
9
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.description("Returns the weather forecast for a given city")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city") // 必填字段需要显式指定
.build())
.build();

当你有一个 List<ToolSpecification> 时,可以调用模型:

1
2
3
4
5
6
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();

如果 LLM 决定调用工具,返回的 AiMessage 会包含toolExecutionRequests 字段。

每个 ToolExecutionRequest 应包含:

  • 工具调用的 id(注意:某些 LLM 提供商,如 Google、Ollama,可能省略该 ID)
  • 要调用的工具名称,例如:getWeather
  • 调用参数,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }

你需要根据 ToolExecutionRequest 手动执行工具。

如果你想把执行结果返回给 LLM,需要为每个 ToolExecutionRequest 创建一个 ToolExecutionResultMessage,并与之前的所有消息一起发送:

1
2
3
4
5
6
7
String result = "It is expected to rain in London tomorrow.";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
.messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response2 = model.chat(request2);

流式的也类似,如果 LLM 决定调用工具,通常会先多次触发 onPartialToolCall(PartialToolCall) 回调,最终触发 onCompleteToolCall(CompleteToolCall),表示该工具调用的流式输出结束。

很明显,这种方式既不优雅,甚至都不自动,所以我们几乎不使用

高级工具 API

在更高层的抽象级别,你可以给任意 Java 方法添加 @Tool 注解,并在创建 AI Service 时指定它们。使用 @Tool 注解的方法可以是静态的或非静态的,可以是任意可见性(public、private 等)

1
2
3
4
5
6
7
@Tool("Calculates the square root of a non-negative number")
public double sqrt(@P("A non-negative number") double x) {
if (x < 0) {
throw new IllegalArgumentException("Cannot compute square root of a negative number: " + x);
}
return Math.sqrt(x);
}

任何带有 @Tool 注解并在构建 AI Service 时显式指定的方法,都可以被 LLM 执行

AI Service 会自动将这些方法转换为 ToolSpecification,并在每次与 LLM 交互时包含在请求中。

当 LLM 决定调用工具时,AI Service 会自动执行相应方法,方法的返回值(如果有)会自动发送回 LLM。

带有 @Tool 注解的方法可以返回任意类型,包括 void

  • 如果方法返回类型为 void,则执行成功后会向 LLM 返回 "Success" 字符串。
  • 如果返回类型为 String,则原样返回字符串给 LLM。
  • 其他返回类型会在返回前自动转换为 JSON 字符串

而且如果一个使用 @Tool 注解的方法抛出了 Exception,则异常的消息 (e.getMessage()) 会作为工具执行的结果传递给 LLM。

这样,LLM 就有机会修正错误并选择是否重试。

简单理解 LangChain4j 如何进行 Tools Calling

LangChain4j 对 Tool Calling 的支持非常优雅,且屏蔽了不同模型之间的差异。

  • 定义工具

    使用 @Tool 注解来声明一个 Java 方法为可供模型调用的工具,并用 @P 描述参数。

    @Tool 注解有两个可选字段:

    • name: 工具的名称。如果未提供,则方法名会作为工具名。
    • value: 工具的描述。

    方法的参数可以选择性地使用 @P 注解。@P 注解有两个字段:

    • value: 参数的描述。必填。
    • required: 参数是否必填,默认为 true。可选
    1
    2
    3
    4
    5
    6
    7
    class WeatherTools {
    @Tool("获取指定城市的实时天气")
    String getWeather(@P("城市名称") String city) {
    // 实际调用天气 API
    return city + "今天晴朗,25度";
    }
    }

    类和字段的描述可以使用 @Description 注解来指定:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Description("要执行的查询")
    class Query {

    @Description("要选择的字段")
    private List<String> select;

    @Description("过滤条件")
    private List<Condition> where;
    }

    @Tool
    Result executeQuery(Query query) {
    ...
    }

    默认情况下,所有工具方法参数都被视为 必填。这意味着 LLM 必须为这些参数生成值,但是有时候这并不需要,可以通过在参数上使用 @P(required = false),可以将其设为可选:

    1
    2
    3
    4
    @Tool
    void getTemperature(String location, @P(value = "Unit of temperature", required = false) Unit unit) {
    ...
    }

    复杂参数的字段和子字段默认也为 必填。可以通过 @JsonProperty(required = false) 将其设为可选:

    1
    2
    3
    4
    5
    6
    record User(String name, @JsonProperty(required = false) String email) {}

    @Tool
    void add(User user) {
    ...
    }
  • 模型集成(Function Calling)

    LangChain4j 为不同模型提供了统一接口。对于支持原生 function calling 的模型(如 OpenAI),框架会自动将 @Tool 方法的签名转换成 OpenAI 所需的 tools JSON Schema,并解析模型返回的 tool_calls,执行 Java 方法,再把结果送回模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    OpenAiChatModel model = OpenAiChatModel.builder()
    .apiKey("...")
    .modelName("gpt-4")
    .build();

    // 使用 AiServices 自动处理工具调用循环
    Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .tools(new WeatherTools()) // 注册工具
    .build();

    String answer = assistant.chat("北京明天天气怎么样?");
    // 内部流程:模型返回 tool_call -> 框架执行 getWeather("北京") -> 结果发回模型 -> 生成最终回答
    • 对于不支持原生 function calling 的模型,LangChain4j 可以切换到“提示词模式”,在系统消息中插入工具描述和格式要求,然后从模型回复中解析工具调用指令。开发者无需修改工具定义
  • 记忆处理

    如果 AI Service 方法中有使用 @MemoryId 注解的参数,那么你也可以在 @Tool 方法的参数上使用 @ToolMemoryId

    这样,AI Service 方法提供的值会自动传递给 @Tool 方法。

而当需要多步推理或调用多个工具时,LangChain4j 提供了 ToolExecutionRequestHandlerReActAgent 等方式来管理思考-行动-观察循环,直到模型认为可以给出最终答案。这个我们下面提

Function Calling 和 Tools Calling 在 LangChain4j 中的实现差异

简单来说,在 LangChain4j 中 Function Calling 和 Tools Calling 是同一个意思,都指让 LLM 调用外部工具的能力。

实际上,上面展示的低级 API,就手动构建 ToolSpecification 对象那种,是给人一种更偏向于 Function Calling 的感觉,官方推荐使用 高级别 API ,实际上,它更符合 Java 开发习惯,能让你专注于业务逻辑。

剩下的想不出啥想说的了

实际例子使用 LangChain4j 中的工具调用

纯 Tools Calling

低级API

两种 API 的关系是这样的

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────┐
│ 高级 API(AiServices) │
│ 你只写 @Tool 方法 → 框架自动完成下面 4 步 │
└─────────────────────────────────────────────────────────────┘

│ 内部封装了 ↓

┌─────────────────────────────────────────────────────────────┐
│ 低级 API(ChatModel 直接调用) │
│ 步骤1 → 步骤2 → 步骤3 → 步骤4 (你手动控制每一步) │
└─────────────────────────────────────────────────────────────┘

高级 API 是黑盒的自动化,低级 API 是白盒的手工组织,理解低级 API 就懂了 Tool Calling 的底层原理。

完整的代码示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
public Map<String, Object> lowLevelDemo(String userMessage) {
Map<String, Object> result = new LinkedHashMap<>();
List<Map<String, Object>> steps = new ArrayList<>();

// ---- 准备:将 @Tool 方法转换为 ToolSpecification ----
// ToolSpecifications.toolSpecificationsFrom() 通过反射扫描 @Tool 注解
// 生成符合 OpenAI Function Calling 规范的 JSON Schema
List<ToolSpecification> toolSpecs = new ArrayList<>();
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(Calculator.class));
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(WeatherTool.class));

// ---- 步骤 1:发送请求(用户消息 + Tool 声明) ----
List<ChatMessage> messages = new ArrayList<>();
messages.add(SystemMessage.from("""
You are a helpful assistant with access to calculation and weather tools.
When a user asks a math question, use the calculator tools.
When a user asks about weather, use the weather tools.
Always use tools when appropriate — don't guess answers you can compute."""));

messages.add(UserMessage.from(userMessage));

// ChatRequest 在 dev.langchain4j.model.chat.request 包中
ChatResponse response = chatModel.chat(ChatRequest.builder()
.messages(messages)
.toolSpecifications(toolSpecs)
.build());

AiMessage aiMessage = response.aiMessage();
messages.add(aiMessage);

// 记录步骤 1
Map<String, Object> step1 = new LinkedHashMap<>();
step1.put("step", 1);
step1.put("title", "发送请求(用户消息 + Tool 声明)");
step1.put("toolCount", toolSpecs.size());
step1.put("toolNames", toolSpecs.stream()
.map(ToolSpecification::name).toList());
step1.put("hasToolExecutionRequests", aiMessage.hasToolExecutionRequests());
if (!aiMessage.hasToolExecutionRequests()) {
step1.put("textResponse", aiMessage.text());
}
steps.add(step1);

// ---- 步骤 2 & 3:LLM 返回 ToolExecutionRequest + 手动执行 ----
List<Map<String, Object>> toolCalls = new ArrayList<>();

if (aiMessage.hasToolExecutionRequests()) {
for (ToolExecutionRequest toolRequest : aiMessage.toolExecutionRequests()) {
Map<String, Object> call = new LinkedHashMap<>();
call.put("toolName", toolRequest.name());
call.put("arguments", toolRequest.arguments()); // JSON 字符串
toolCalls.add(call);

// 步骤 3:手动执行 Java 方法
Map<String, Object> step3 = new LinkedHashMap<>();
step3.put("step", 3);
step3.put("title", "执行 Tool: " + toolRequest.name());
step3.put("arguments", toolRequest.arguments());

String executionResult;
try {
executionResult = executeTool(toolRequest);
step3.put("status", "success");
} catch (Exception e) {
executionResult = "Error executing tool: " + e.getMessage();
step3.put("status", "error");
step3.put("error", e.getMessage());
}
step3.put("executionResult", executionResult);
step3.put("resultType", executionResult.startsWith("{") ? "JSON" : "text");
steps.add(step3);

// 将执行结果作为 ToolExecutionResultMessage 加入消息列表
messages.add(ToolExecutionResultMessage.from(toolRequest, executionResult));
}
}

// 记录步骤 2
Map<String, Object> step2 = new LinkedHashMap<>();
step2.put("step", 2);
step2.put("title", "LLM 返回 ToolExecutionRequest(调用意图)");
step2.put("toolCalls", toolCalls);
step2.put("callCount", toolCalls.size());
steps.add(step2);

// ---- 步骤 4:将执行结果发回 LLM,生成最终回答 ----
Map<String, Object> step4 = new LinkedHashMap<>();
step4.put("step", 4);
step4.put("title", "将 Tool 执行结果发回 LLM,生成最终回答");

if (!toolCalls.isEmpty()) {
ChatResponse finalResponse = chatModel.chat(ChatRequest.builder()
.messages(messages)
.toolSpecifications(toolSpecs) // 可能还有下一轮 Tool 调用
.build());

AiMessage finalAiMessage = finalResponse.aiMessage();

// 检查 LLM 是否需要继续调用更多工具(Tool Chaining / ReAct 模式)
if (finalAiMessage.hasToolExecutionRequests()) {
step4.put("note", "LLM 请求了额外的工具调用(Tool Chaining),"
+ "这里仅展示第一轮,实际框架会循环直到 LLM 输出纯文本");
}

if (finalAiMessage.text() != null) {
step4.put("finalAnswer", finalAiMessage.text());
}
} else {
step4.put("note", "LLM 没有请求任何工具调用,直接用内置知识回答了问题");
if (aiMessage.text() != null) {
step4.put("finalAnswer", aiMessage.text());
}
}
steps.add(step4);

result.put("mode", "低级 API(手动控制完整的 Function Calling 生命周期)");
result.put("userMessage", userMessage);
result.put("totalMessages", messages.size());
result.put("steps", steps);
return result;
}

首先,我们需要把 @Tool 注解变成 LLM 能懂的 JSON Schema

1
2
3
List<ToolSpecification> toolSpecs = new ArrayList<>();
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(Calculator.class));
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(WeatherTool.class));

核心 API:ToolSpecifications.toolSpecificationsFrom(Class<?>)

这是低级 API 的第一个关键方法。它通过 Java 反射扫描传入类上所有 @Tool 注解的方法,将每个方法转换为一个 ToolSpecification 对象。

ToolSpecification 本质上就是 OpenAI Function Calling 格式的 JSON Schema,这个上面也提到了,包含三个关键信息:

字段 来源 作用
name @Toolname 属性,默认取方法名 LLM 用它来标识”我要调用哪个工具”
description @Toolvalue 属性 LLM 据此判断”这个工具是干什么的”
parameters 方法参数 + @P 注解 → JSON Schema LLM 据此知道”这个工具需要什么参数”

Calculator.add() 为例:

1
2
3
@Tool("Adds two integers and returns the sum")
public int add(@P("The first number") int a,
@P("The second number") int b) { ... }

ToolSpecifications.toolSpecificationsFrom() 会自动生成等价于以下 JSON Schema 的 ToolSpecification

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "add",
"description": "Adds two integers and returns the sum",
"parameters": {
"type": "object",
"properties": {
"a": { "type": "integer", "description": "The first number" },
"b": { "type": "integer", "description": "The second number" }
},
"required": ["a", "b"]
}
}

所以说,@Tool + @P 注解 = 声明式 JSON Schema 生成。写 Java 注解框架就能生成 LLM 能消费的函数签名。

然后,发送用户消息 + Tool 声明给 LLM

1
2
3
4
5
6
7
8
9
10
List<ChatMessage> messages = new ArrayList<>();
messages.add(SystemMessage.from("""
You are a helpful assistant with access to calculation and weather tools.
..."""));
messages.add(UserMessage.from(userMessage));

ChatResponse response = chatModel.chat(ChatRequest.builder() // ← 核心 API
.messages(messages) // ← 消息历史
.toolSpecifications(toolSpecs) // ← 工具声明!
.build());

核心 API:ChatRequest.Builder

ChatRequest 是 LangChain4j 中发送给 LLM 的完整请求封装,它的 Builder 提供了两个与 Tool Calling 直接相关的方法:

Builder 方法 类型 说明
.messages(List<ChatMessage>) List<ChatMessage> 对话历史(至少包含 SystemMessage + UserMessage)
.toolSpecifications(List<ToolSpecification>) List<ToolSpecification> 告诉 LLM “你可以调用这些工具”

这一步发送到 LLM 的完整 payload 结构大致就是这样

1
2
3
4
5
6
7
8
9
10
11
12
{
"model": "deepseek-v4-pro",
"messages": [
{ "role": "system", "content": "You are a helpful assistant..." },
{ "role": "user", "content": "What is 15 + 27?" }
],
"tools": [
{ "type": "function", "function": { "name": "add", "description": "...", "parameters": {...} } },
{ "type": "function", "function": { "name": "sqrt", "description": "...", "parameters": {...} } },
... // 所有 Calculator + WeatherTool 的方法
]
}

toolSpecifications 会随每一次 chat() 调用发送给 LLM。这就是为什么工具太多会非常的浪费 Token,每个工具的完整 JSON Schema 都占用上下文窗口。

然后,LLM 返回 ToolExecutionRequest

1
2
3
4
5
6
7
8
9
AiMessage aiMessage = response.aiMessage();
messages.add(aiMessage);

if (aiMessage.hasToolExecutionRequests()) {
for (ToolExecutionRequest toolRequest : aiMessage.toolExecutionRequests()) {
// toolRequest.name() → "add" (要调哪个工具)
// toolRequest.arguments() → "{\"a\":15,\"b\":27}" (传什么参数,JSON字符串)
}
}

核心 API:AiMessage 的两个方法

方法 返回类型 说明
hasToolExecutionRequests() boolean LLM 是否想调用工具?true = LLM 表达了调用意图
toolExecutionRequests() List<ToolExecutionRequest> LLM 想调用哪些工具(可能多个)

核心 API:ToolExecutionRequest 的两个关键字段

1
2
3
4
5
6
// ToolExecutionRequest 是 LLM 返回的"函数调用请求"
public interface ToolExecutionRequest {
String name(); // 工具名称,对应 ToolSpecification.name
String arguments(); // 参数 JSON 字符串,如 {"a": 15, "b": 27}
String id(); // 调用 ID(某些模型支持,用于并行工具调用)
}

LLM 在这一步到底做了什么?LLM 不是执行了 Java 方法,Java 方法的执行要靠反射或者自己,它只是做了两件事:

  1. 决策:判断需要调用哪个工具(通过匹配用户意图 ↔︎ Tool 的 description)
  2. 参数提取:从用户消息中提取参数值,填入 JSON

然后,需要我们手动执行 Java 方法

1
2
3
4
5
6
7
8
9
10
11
12
// ToolCallDemoService.java 第 148-161 行
String executionResult;
try {
executionResult = executeTool(toolRequest); // ← 你手动调用 Java 方法
step3.put("status", "success");
} catch (Exception e) {
executionResult = "Error executing tool: " + e.getMessage();
step3.put("status", "error");
}

// 将执行结果包装为 ToolExecutionResultMessage,加入消息历史
messages.add(ToolExecutionResultMessage.from(toolRequest, executionResult));

ToolExecutionResultMessage.from()它创建一个特殊的消息类型,告诉 LLM:“你要的工具我执行完了,这是结果”。

消息列表此时的状态会变成:

1
2
3
4
5
6
messages = [
SystemMessage("You are a helpful assistant..."),
UserMessage("What is 15 + 27?"),
AiMessage( [ToolExecutionRequest{name:"add", arguments:'{"a":15,"b":27}'}] ),
ToolExecutionResultMessage( request=↑, result="42" ), ← 新加入的
]

然后将执行结果发回 LLM,生成最终回答

1
2
3
4
5
6
7
ChatResponse finalResponse = chatModel.chat(ChatRequest.builder()
.messages(messages) // ← 包含 ToolExecutionResultMessage 的完整历史
.toolSpecifications(toolSpecs) // ← 仍要带上,LLM 可能还需要调用其他工具
.build());

AiMessage finalAiMessage = finalResponse.aiMessage();
// finalAiMessage.text() → "15 + 27 = 42" (LLM 综合了工具结果后的自然语言回

测试一下,可以清楚的看到每一步发生什么,数据进行了怎么样的流转

image-20260708180101443

高级 API

LangChain4j 中 Tool Calling 的高级 API 特指 基于 AiServices 构建 AI 服务接口 的声明式的内容

首先,在 ToolCallConfig 中,所有Tool Calling 需要使用的内容的 Bean 都通过 AiServices.builder(接口.class) 构建。这是起点,例如我们随便看一个 Tool Calling 的注册的 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public BasicAssistant basicAssistant(
@Qualifier("toolCallChatModel") ChatModel chatModel,
Calculator calculator,
WeatherTool weatherTool) {
return AiServices.builder(BasicAssistant.class)
.chatModel(chatModel)
.tools(calculator, weatherTool)
.chatMemoryProvider(sessionId -> MessageWindowChatMemory.builder()
.id(sessionId)
.maxMessages(maxMessages)
.chatMemoryStore(toolCallMemoryStore())
.build())
.build();
}

高级 API 最大的便利性在于工具就是普通的 Spring Bean 然后加上普通的 Java 方法,你只需加注解就可以了

1
2
3
4
5
6
7
8
@Component
public class Calculator {
@Tool("Adds two integers and returns the sum") // 描述给 LLM 看
public int add(@P("The first number") int a, // 描述参数给 LLM 看
@P("The second number") int b) {
return a + b;
}
}
  • 框架通过反射扫描 @Tool 方法,自动生成符合 OpenAI/DeepSeek 标准的 JSON Schema,基本上,最后还是会回到低级 API 中手动写的 ToolSpecifications.toolSpecificationsFrom(...)

然后就定义 AI 服务接口就可以了

1
2
3
4
public interface BasicAssistant {
String chat(@MemoryId String sessionId, // ① 会话隔离标识
@UserMessage String message); // ② 用户输入
}

高级 API 就是这么简单,实际上,跟学 AI Service 的适合,只需要多两项配置,多写俩注解就可以了

而且之前我们也提到过,当你把返回值声明为 Result<String> 而非 String 时,框架会额外返回本次调用的完整元数据,调用后,你可以通过以下方法检查本次交互究竟发生了什么:

1
2
3
4
public interface InspectableAssistant {
Result<String> chat(@MemoryId String sessionId,
@UserMessage String message);
}

那么效果差不多就是这样

image-20260717175544275

如果希望使用流式 api,那么可以这样使用,在构建 bean 的时候使用 StreamingChatModel,然后在 Controller 写接口的时候注意使用流式的 api 即可

大致在服务层中,被调用的方法是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public SseEmitter streamingWithToolsDemo(String userMessage) {
SseEmitter emitter = new SseEmitter(120_000L); // 2 分钟超时

List<ToolSpecification> toolSpecs = new ArrayList<>();
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(Calculator.class));
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(WeatherTool.class));

List<ChatMessage> messages = new ArrayList<>();
messages.add(SystemMessage.from(
"You are a helpful assistant. Use tools for calculations and weather queries."));
messages.add(UserMessage.from(userMessage));

try {
streamingChatModel.chat(ChatRequest.builder()
.messages(messages)
.toolSpecifications(toolSpecs)
.build(), new StreamingChatResponseHandler() {

private final List<ToolExecutionRequest> completedToolRequests = new ArrayList<>();
private AiMessage lastAiMessage;

@Override
public void onPartialResponse(String partialResponse) {
try {
....
} catch (Exception e) {
// SSE 连接可能已关闭
}
}

@Override
public void onPartialToolCall(PartialToolCall partialToolCall) {
// 注意:并非所有模型都支持流式 Partial Tool Call
try {
...
} catch (Exception e) {
// ignore
}
}

@Override
public void onCompleteToolCall(CompleteToolCall completeToolCall) {
// 工具调用完成,拿到完整的 ToolExecutionRequest
ToolExecutionRequest req = completeToolCall.toolExecutionRequest();
completedToolRequests.add(req);

// 执行 Tool
String execResult = executeTool(req);
try {
...
} catch (Exception e) {
// ignore
}

messages.add(ToolExecutionResultMessage.from(req, execResult));
}

@Override
public void onCompleteResponse(ChatResponse completeResponse) {
...
}

@Override
public void onError(Throwable error) {
emitter.completeWithError(error);
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}

实际上,流式模式下 Tool Calling 的处理与非流式不同:

  • LLM 先流式输出文本
  • 遇到 Tool Call 时发出 onPartialToolCall,开始调用工具
  • 工具调用完成后触发 onCompleteToolCall,拿到完整的 ToolExecutionRequest
  • 执行 Tool 后,需要将结果发回 LLM 继续流式输出
  • 最终所有内容输出完毕时触发 onCompleteResponse

动态调用工具

动态调用工具就是我们构建一个动态的 ToolProvider 据用户消息内容按需调用不同的工具。

为什么需要这样呢?实际上,工具非常多时,这种方式能够减少每次请求的 Schema 大小,而且通常情况下,Agent 中不同权限调用的工具通常也不一样,管理员肯定不能和不受信者用一样的工具你说是吧。

而且这样也能够按上下文提供不同工具集

那么,我们在配置类中构建这样的一个动态的 ToolProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Bean
public ToolProvider dynamicToolProvider(Calculator calculator, WeatherTool weatherTool) {
// 预生成所有工具的 ToolSpecification 和 ToolExecutor 映射
Map<String, ToolSpecification> allSpecs = new LinkedHashMap<>();
Map<String, DefaultToolExecutor> allExecutors = new LinkedHashMap<>();

// 扫描 Calculator 的 @Tool 方法
for (ToolSpecification spec : ToolSpecifications.toolSpecificationsFrom(Calculator.class)) {
allSpecs.put(spec.name(), spec);
allExecutors.put(spec.name(),
createExecutor(calculator, Calculator.class, spec.name()));
}
// 扫描 WeatherTool 的 @Tool 方法
for (ToolSpecification spec : ToolSpecifications.toolSpecificationsFrom(WeatherTool.class)) {
allSpecs.put(spec.name(), spec);
allExecutors.put(spec.name(),
createExecutor(weatherTool, WeatherTool.class, spec.name()));
}

return (toolProviderRequest) -> {
String userMessage = toolProviderRequest.userMessage().singleText().toLowerCase();

ToolProviderResult.Builder resultBuilder = ToolProviderResult.builder();

// 数学计算相关的消息 → 暴露 Calculator 的方法
if (containsAny(userMessage, "add", "sum", "subtract", "minus", "multiply",
"times", "divide", "sqrt", "square root", "calculate", "math", "compute",
"+", "-", "*", "/", "number", "integer")) {
for (String name : List.of("add", "subtract", "multiply", "divide", "sqrt")) {
if (allSpecs.containsKey(name)) {
resultBuilder.add(allSpecs.get(name), allExecutors.get(name));
}
}
}

// 天气相关的消息 → 暴露 WeatherTool 的方法
if (containsAny(userMessage, "weather", "temperature", "rain", "sunny",
"cloudy", "wind", "humidity", "forecast", "celsius", "fahrenheit",
"city", "climate")) {
for (String name : List.of("getWeather", "convertTemperature")) {
if (allSpecs.containsKey(name)) {
resultBuilder.add(allSpecs.get(name), allExecutors.get(name));
}
}
}

// 如果都不匹配 → 返回空的 ToolProviderResult(不暴露任何工具)
// LLM 将直接用自己的知识回答
return resultBuilder.build();
};
}

实际上这是一种比较低端的方式,因为工具的调用不能依靠大模型自己的判断来处理,只能够根据消息的情况来选择不同的工具,所以说,真正想要实现动态选择工具的,必须需要使用 Agentic 模式

那么,这个模式如何实现呢?实际上,不做任何关键词过滤,把 Calculator 和 WeatherTool 的全部方法都暴露给 LLM 即可,这样,LLM 收到用户消息后,会用自己的推理能力判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public AgenticDynamicAssistant agenticDynamicAssistant(
@Qualifier("toolCallChatModel") ChatModel chatModel,
Calculator calculator,
WeatherTool weatherTool) {
return AiServices.builder(AgenticDynamicAssistant.class)
.chatModel(chatModel)
.tools(calculator, weatherTool) // ← 全部工具
.chatMemoryProvider(sessionId -> MessageWindowChatMemory.builder()
.id(sessionId)
.maxMessages(maxMessages)
.chatMemoryStore(toolCallMemoryStore())
.build())
.build();
}

然后,在编写服务层的时候,最好在服务层的调用方法中填写上一些相关的提示词,这样以至于每轮把全部工具发给 LLM 的时候提升 LLM 调用工具的效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SystemMessage systemMessage = SystemMessage.from("""
You are an autonomous AI assistant with access to calculation and weather tools.

IMPORTANT — Agentic decision-making principles:
1. You decide WHICH tools to call based on the user's request — no one pre-selects tools for you.
2. You decide HOW MANY tools to call — one, many, or none at all.
3. You decide the ORDER — call tools in whatever sequence makes sense.
4. You decide when to STOP — when you have enough information to answer, stop calling tools.

Think step by step:
- What does the user actually need?
- Which tools can help fulfill that need?
- After getting tool results, is more information needed?
- When ready, synthesize a clear natural-language answer.

Be autonomous. Be thorough. Use tools when they help, skip them when they don't.""");

但是这种方式就不适合一些边界性强,推理空间少的情况

那么,上面提到的这些内容有条件地将一个 AI Service 作为工具暴露,因此一个 AI Service 完全也可作为另一个 AI Service 的工具,下面讲。

将 AI Service 作为其他 AI Service 的工具

AI Service 还可以作为工具供其他 AI Service 使用。

这在许多 Agentic AI 的应用场景中非常有用:一个 AI Service 可以请求另一个更专业的 AI Service 来完成特定任务。这时候,被请求的 AI Service 实际上就会成为请求者服务的一个工具被调用

使用 @Tool 包装 AI Service 是比较常见的方式,将另一个 AI Service 封装为 @Tool 方法,供主 AI Service 调用。这样就实现了类似「专家系统」的模式,一个通用 AI 将特定任务委托给专用 AI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class AiServiceToolWrapper {
......
/**
* 将 BasicAssistant 包装为一个 Tool。
* LLM 看到这个工具后,可以选择调用它来完成复杂的计算或天气查询。
*/
@Tool("""
Delegates complex calculation or weather queries to a specialized assistant.
Use this when you need to perform calculations or get weather information.
The specialist has access to calculator and weather tools.
""")
public String askSpecialist(
@P("The question to ask the specialist assistant") String question,
@ToolMemoryId String memoryId) {

// 这里调用的实际上是一个 AI Service
// 它内部可以再调用 Calculator 和 WeatherTool
return specialistAssistant.chat(memoryId, question);
}
}

然后,在 ToolCallConfig 中添加这个新 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ToolCallConfig 中添加这个新 Bean
@Bean
public AiServiceToolWrapper aiServiceToolWrapper(BasicAssistant basicAssistant) {
return new AiServiceToolWrapper(basicAssistant);
}

// 新增一个更高级的 AI Service,它把上面的 Wrapper 作为工具
@Bean
public AdvancedAssistant advancedAssistant(
@Qualifier("toolCallChatModel") ChatModel chatModel,
AiServiceToolWrapper aiServiceToolWrapper) {
return AiServices.builder(AdvancedAssistant.class)
.chatModel(chatModel)
.tools(aiServiceToolWrapper) // ← 关键:把 AI Service 包装成工具
.chatMemoryProvider(sessionId -> MessageWindowChatMemory.builder()
.id(sessionId)
.maxMessages(maxMessages)
.chatMemoryStore(toolCallMemoryStore())
.build())
.build();
}

public interface AdvancedAssistant {
String chat(@MemoryId String sessionId, @UserMessage String message);
}

可以看到,将 AI Service 包装为 Tool 关键是创建一个包装类,因为LangChain4j 对 @Tool 方法的扫描是基于实例方法的,要么包装成包装类,要么ToolSpecifications + DefaultToolExecutor

这是一种比较常规的方式,但是,如果完全体现它的精髓,还是需要 Agentic 模式,这种方式完全符合所谓的专家委派,至于如何使用 Agentic 模式,其实跟上面的动态调用工具的内容相当类似

访问已执行的工具

如果你希望访问 AI Service 调用过程中执行的工具,可以将返回类型包装在 Result

首先我们定义相关的契约接口,使用 Result 包装

1
2
3
4
5
public interface InspectableAssistant {
dev.langchain4j.service.Result<String> chat(
@dev.langchain4j.service.MemoryId String sessionId,
@dev.langchain4j.service.UserMessage String message);
}

声明了这样的一个接口之后,那么我们就这样声明其接口然后被作为方法的类型,通过 AiServices 生成代理实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public InspectableAssistant inspectableAssistant(
@Qualifier("toolCallChatModel") ChatModel chatModel,
Calculator calculator,
WeatherTool weatherTool) {
return AiServices.builder(InspectableAssistant.class)
.chatModel(chatModel)
.tools(calculator, weatherTool)
.chatMemoryProvider(sessionId -> MessageWindowChatMemory.builder()
.id(sessionId)
.maxMessages(maxMessages)
.chatMemoryStore(toolCallMemoryStore())
.build())
.build();
}

然后请求体中使用我们的实现,就可以访问这个 Result 对象,拿到你想要的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private final InspectableAssistant inspectableAssistant;    

@PostMapping("/result-inspect")
public ResponseEntity<Map<String, Object>> resultInspect(@RequestBody ToolCallRequest req) {
String sessionId = req.getSessionId() != null ? req.getSessionId() : UUID.randomUUID().toString();
Result<String> result = inspectableAssistant.chat(sessionId, req.getMessage());

Map<String, Object> response = new LinkedHashMap<>();
response.put("mode", "Result<T>(检查 Tool 调用详情)");
response.put("message", req.getMessage());
response.put("answer", result.content());

// Result.toolExecutions() 返回本次调用的所有 Tool 执行记录
var toolExecutions = result.toolExecutions().stream()
.map(te -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("toolName", te.request().name());
m.put("arguments", te.request().arguments());
m.put("result", te.result());
return m;
})
.toList();
response.put("toolExecutions", toolExecutions);
response.put("totalToolCalls", result.toolExecutions().size());

return ResponseEntity.ok(response);
}

Result 中包含的内容如下:

image-20260729164044138
字段 类型 说明
content T AI 服务的最终返回内容(LLM 的自然语言回答),类型由您定义(StringMyPojo 等)
tokenUsage TokenUsage Token 使用统计(输入 Token 数 + 输出 Token 数),聚合了所有对话轮次的总和
sources List<Content> RAG 检索来源(如果使用了 rag() 配置),返回检索到的文档片段
finishReason FinishReason 最终响应的结束原因(如 STOPTOOL_EXECUTIONLENGTH 等)
toolExecutions List<ToolExecution> 本次调用中执行的所有工具记录(包含请求参数 + 执行结果)
intermediateResponses List<ChatResponse> 中间对话响应(所有包含 ToolExecutionRequest 的 LLM 中间轮次响应)
finalResponse ChatResponse 最终对话响应(不包含工具调用请求的最终 LLM 响应)

在流式模式中,需要把返回值声明为 TokenStream,通过 .onToolExecuted() 回调获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public SseEmitter streamingAIServiceWithToolsDemo(String userMessage) {
SseEmitter emitter = new SseEmitter(120_000L);
String sessionId = UUID.randomUUID().toString();
// 获取 TokenStream
TokenStream tokenStream = streamingToolAssistant.chat(sessionId, userMessage);
// 链式注册回调,桥接到 SSE
tokenStream
.onPartialResponse(token -> {
try {
Map<String, Object> event = new LinkedHashMap<>();
event.put("type", "token");
event.put("data", token);
emitter.send(SseEmitter.event().name("token").data(event));
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onToolExecuted(toolExecution -> {
// 工具执行完成时触发
// ToolExecution 包含 request(工具名+参数)和 result(返回值)
try {
Map<String, Object> event = new LinkedHashMap<>();
event.put("type", "toolExecuted");
event.put("toolName", toolExecution.request().name());
event.put("arguments", toolExecution.request().arguments());
event.put("result", toolExecution.result());
emitter.send(SseEmitter.event().name("tool").data(event));
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onCompleteResponse(response -> {
// 全部完成 发送 done 事件
try {
Map<String, Object> doneEvent = new LinkedHashMap<>();
doneEvent.put("type", "done");
doneEvent.put("finalAnswer", response.aiMessage().text());
// TokenStream 的 onCompleteResponse 也提供 ChatResponse,
// 可以检查其中是否包含 toolExecutionRequests 等信息
emitter.send(SseEmitter.event().name("done").data(doneEvent));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onError(error -> {
...
})
.start(); // ← 启动
return emitter;
}

其实吧,ToolExecutionResult<T> 中用的是同一个类,包装一下的事

思考-行动-观察循环

这是 LangChain4j Tool Calling 的第一性原理

理解了它,就理解了 Agentic AI,实际上,无论是简单的 AI Service 还是复杂的多 Agent 编排,底层都是这个循环在驱动。

  • 思考:分析用户需求,推理需要什么工具?什么参数?要不要调用?
  • 行动:根据 Thought 的决策,调用对应的 @Tool 方法
  • 观察:收到 Action 的结果,将这个结果作为上下文,进入下一轮思考

AI Services 内部如何实现这个循环?当你调用 AiServices.builder().tools(...).build() 返回的 Assistant,其实会有这样的一个 while 循环

1
2
3
4
5
6
7
8
9
10
while (true) {
response = chatModel.chat(messages, toolSpecifications)
if (!response.hasToolExecutionRequests()) {
return response.text() // Thought 决定停了 → 返回答案
}
for (request : response.toolExecutionRequests()) {
result = tool.execute(request) // Action
messages.add(result) // Observation(加入上下文)
}
}

这个 while 循环就是 ReAct 循环的引擎,框架帮你做了,你只需要写 @Tool 方法就行了

那么,体现在代码中,就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public Map<String, Object> reactCycleDemo(String userMessage) {
Map<String, Object> result = new LinkedHashMap<>();
List<Map<String, Object>> rounds = new ArrayList<>();

// 所有工具
List<ToolSpecification> toolSpecs = new ArrayList<>();
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(Calculator.class));
toolSpecs.addAll(ToolSpecifications.toolSpecificationsFrom(WeatherTool.class));

// 系统提示词引导 LLM 展示推理过程
List<ChatMessage> messages = new ArrayList<>();
messages.add(SystemMessage.from("""
You are a reasoning AI assistant. Before calling any tool, think about:
1. What does the user actually need?
2. Which specific tools can help?
3. Can this be done in one round, or do I need multiple rounds?

After receiving tool results, think about whether you need more information.
Only give the final answer when you're confident you have everything."""));
messages.add(UserMessage.from(userMessage));

int maxRounds = 10;
int roundNum = 0;

// ====== ReAct 循环 ======
while (roundNum < maxRounds) {
roundNum++;
Map<String, Object> round = new LinkedHashMap<>();
round.put("round", roundNum);

// ─── Phase 1: THOUGHT(思考)───
// LLM 收到当前消息上下文 + 全部工具声明,进行推理
// 产物:要么是 ToolExecutionRequest(需要行动),要么是纯文本(任务完成)
ChatResponse response = chatModel.chat(ChatRequest.builder()
.messages(messages)
.toolSpecifications(toolSpecs)
.build());
AiMessage aiMessage = response.aiMessage();
messages.add(aiMessage);

Map<String, Object> thought = new LinkedHashMap<>();
if (aiMessage.hasToolExecutionRequests()) {
// LLM 思考后决定:需要调用工具!
List<Map<String, Object>> decisions = new ArrayList<>();
for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) {
Map<String, Object> d = new LinkedHashMap<>();
d.put("tool", req.name());
d.put("arguments", req.arguments());
decisions.add(d);
}
thought.put("decision", "CALL_TOOLS");
thought.put("reasoning", "LLM 分析用户需求后,决定调用 "
+ decisions.size() + " 个工具来获取信息");
thought.put("plannedActions", decisions);
} else {
// LLM 思考后决定:不需要工具,直接回答
thought.put("decision", "STOP_AND_ANSWER");
thought.put("reasoning", "LLM 认为已经拥有足够信息(或不需要工具),"
+ "决定输出最终答案");
}
round.put("thought", thought);

// 如果 Thought 决定停止 → 输出最终答案
if (!aiMessage.hasToolExecutionRequests()) {
round.put("finalAnswer", aiMessage.text());
rounds.add(round);
break;
}

// ─── Phase 2: ACTION(行动)───
// 框架(Java)根据 Thought 阶段的决策,执行对应的 @Tool 方法
List<Map<String, Object>> actions = new ArrayList<>();
for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) {
Map<String, Object> action = new LinkedHashMap<>();
action.put("tool", req.name());
action.put("arguments", req.arguments());

String execResult;
try {
execResult = executeTool(req);
action.put("status", "SUCCESS");
} catch (Exception e) {
execResult = "Error: " + e.getMessage();
action.put("status", "FAILED");
action.put("error", e.getMessage());
}
action.put("result", execResult);
actions.add(action);

// 将结果加入消息历史 → 成为下一轮 Observation 的输入
messages.add(ToolExecutionResultMessage.from(req, execResult));
}
round.put("actions", actions);

// ─── Phase 3: OBSERVATION(观察)───
// Action 的结果已经加入 messages(ToolExecutionResultMessage),
// 下一轮 Thought 会看到这些观察结果,据此推理下一步
Map<String, Object> observation = new LinkedHashMap<>();
observation.put("summary", "工具执行完毕,结果已加入上下文。"
+ "下一轮 LLM 将看到这些 Observation 数据,"
+ "据此判断:信息是否足够?还需要更多工具吗?");
List<String> observationData = new ArrayList<>();
for (Map<String, Object> a : actions) {
observationData.add(a.get("tool") + " → " + a.get("result"));
}
observation.put("data", observationData);
round.put("observation", observation);

rounds.add(round);
}

if (roundNum >= maxRounds) {
result.put("warning", "达到最大循环轮数 (" + maxRounds + "),可能存在循环调用问题");
}

result.put("mode", "思考—行动—观察(ReAct)循环");
result.put("description", "每个回合由 Thought → Action → Observation 三个阶段组成,"
+ "LLM 自主决定何时停止循环");
result.put("userMessage", userMessage);
result.put("totalRounds", roundNum);
result.put("availableTools", toolSpecs.stream().map(ToolSpecification::name).toList());
result.put("rounds", rounds);

// 提取最终答案
if (!rounds.isEmpty()) {
Map<String, Object> lastRound = rounds.get(rounds.size() - 1);
if (lastRound.containsKey("finalAnswer")) {
result.put("finalAnswer", lastRound.get("finalAnswer"));
}
}

return result;
}

最后,可以看出,LangChain4j 的每个工具调用回合由 3 个阶段 组成

1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────────────────────────────┐
│ ReAct 循环 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ THOUGHT │ ──→ │ ACTION │ ──→ │ OBSERVATION │ │
│ │ LLM做 │ │ 框架做 │ │ LLM做 │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ ↑ │ │
│ └─────────── 循环 ─────────────────┘ │
│ 直到 LLM 输出纯文本(不再有 ToolExecutionRequest) │
└──────────────────────────────────────────────────────────┘