要让 AI 真正"有用",它必须学会行动------调工具、查数据、改系统、发请求。
这一篇,我们就从 CoT 一步步演进到 ReAct(Reasoning + Acting),然后用 Java + Spring Boot + LangChain4j + SSE 从零手写一个能思考、能行动、能流式响应的 Mini Agent。
目录
- 一、一个"不会行动"的 AI 有多鸡肋
- 二、从 CoT 到 ReAct:范式演进
- 三、ReAct 原理深度拆解
- 四、手写 Mini-ReAct:100 行 Java 代码看懂 Agent 骨架
- 五、工具设计:Function Calling 的正确姿势
- 六、工业级实现:LangChain4j + Spring Boot
一、一个"不会行动"的 AI 有多鸡肋
让我们看三个真实对话:
对话 1:查天气
我:明天北京适合户外跑步吗?
AI(无工具):一般来说,北京的春季适合户外跑步,但建议关注当天的空气质量和气温......(一大段正确的废话)
你想要的是"明天 AQI 45,最高温 18℃,适合跑步",它给你的是百科知识。
对话 2:算复杂数
我 :计算
(3.14 × 2026² - 1729 ³) / sqrt(98765)AI(无工具):让我一步一步算...... 结果约为 -5.2 × 10⁶。(实际是 -12,355,123.7,差了一个数量级)
大模型做算术靠"记忆"而不是"计算",数字一大就翻车。
对话 3:改数据
我 :把我数据库里
status=pending超过 7 天的订单改成expiredAI(无工具) :我理解你的需求,建议执行 SQL:
UPDATE orders SET status='expired' WHERE...(给 SQL,不执行)
它能写出来,但执行不了。
一句话总结这个"天花板"
只会思考、不会行动的 AI,本质是个百科全书,不是个助手。
Agent 要做的,就是给 AI 装上手和脚。
二、从 CoT 到 ReAct:范式演进
我们用一张图回顾一下这三个范式的递进关系:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 直接问答 │ → │ CoT │ → │ ReAct │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ 问题 │ │ 问题 │ │ 问题 │
│ ↓ │ │ ↓ │ │ ↓ │
│ 答案(一步) │ │ 思考 1 │ │ 思考 1 │
│ │ │ 思考 2 │ │ 行动 1(调工具)│
│ │ │ ... │ │ 观察 1 │
│ │ │ 答案 │ │ 思考 2 │
│ │ │ │ │ 行动 2 │
│ │ │ │ │ ... │
│ │ │ │ │ 答案 │
└──────────────┘ └──────────────┘ └──────────────┘
靠蒙 闭门思考 看+想+做循环
2.1 三个范式的对比表
| 维度 | 直接问答 | CoT | ReAct |
|---|---|---|---|
| 能否闭门造车 | ✅ | ✅ | ❌(必须调工具) |
| 能否获取实时信息 | ❌ | ❌ | ✅ |
| 能否操作外部系统 | ❌ | ❌ | ✅ |
| 算术能力 | 差 | 中 | 强(调计算器) |
| 幻觉风险 | 高 | 中 | 低(事实来自工具) |
| 延迟 | 低 | 中 | 高(多轮调用) |
| 成本 | 低 | 中 | 高(N 倍 Token) |
| 典型场景 | 闲聊、翻译 | 数学、分析 | 搜索、运维、Agent |
2.2 ReAct 的核心创新
ReAct 是 Princeton & Google 2022 年的论文。它的天才之处在于:
把 CoT 的"思考链"和传统程序的"工具调用"拼到同一条输出里。
也就是说,模型的输出不再是"纯思考"或"纯 API 调用",而是一种交织格式:
Thought: 我需要先知道北京明天的天气
Action: get_weather
Action Input: {"city": "Beijing", "date": "tomorrow"}
Observation: {"temp": "10-18°C", "aqi": 45, "weather": "晴"}
Thought: 气温适宜,空气良好,可以跑步
Answer: 明天北京适合跑步,气温 10-18℃,AQI 45,天气晴朗。
这个循环可以重复多次,直到模型觉得"我已经能回答了",输出 Answer: 退出循环。
三、ReAct 原理深度拆解
3.1 ReAct 的五个关键元素
| 元素 | 含义 | 在输出中的标记 |
|---|---|---|
| Thought(思考) | 模型对当前状态的分析 | Thought: ... |
| Action(行动) | 要调用的工具名 | Action: tool_name |
| Action Input(参数) | 工具的入参(通常 JSON) | Action Input: {...} |
| Observation(观察) | 工具的返回结果(由程序注入) | Observation: ... |
| Answer(最终答案) | 退出循环的信号 | Answer: ... |
3.2 ReAct 的执行循环(核心!)
这是整个 Agent 最重要的一段伪代码:
python
def react_loop(question, tools):
prompt = build_initial_prompt(question, tools)
for step in range(MAX_STEPS):
output = llm(prompt, stop=["\nObservation:"]) # ① 让模型生成到 Observation 前停下
if "Answer:" in output: # ② 模型自己说结束了
return extract_answer(output)
action, action_input = parse(output) # ③ 解析出要调的工具
observation = tools[action](action_input) # ④ 程序调工具拿结果
prompt += output + f"\nObservation: {observation}\n" # ⑤ 把结果拼回 Prompt
return "抱歉,超过最大步数仍未得到答案"
关键点:
stop参数至关重要 :让模型生成到Observation:这行就停下,因为Observation的内容必须由程序从工具拿到,不能让模型瞎编。这是 ReAct 不翻车的关键。- 每一轮把"上一轮的思考 + 行动 + 观察"拼回 Prompt:这就是 Agent 的"短期记忆"------整个对话历史就是模型当前的上下文。
- 退出条件 :模型吐出
Answer:或超过最大步数。
3.3 一个经典的 ReAct Prompt 模板
text
你是一个会使用工具的 AI 助手。你可以访问以下工具:
{tool_descriptions}
请严格按以下格式思考和回答:
Question: 用户的问题
Thought: 你的思考过程
Action: 要调用的工具名(必须是上面列出的工具之一)
Action Input: 工具的输入参数(JSON 格式)
Observation: 工具返回的结果(由系统提供,你不要自己填写)
... (Thought/Action/Action Input/Observation 循环 N 次)
Thought: 我已经得到了足够的信息
Answer: 最终给用户的答案
现在开始:
Question: {user_question}
Thought:
这个模板是 ReAct 的"圣经",LangChain、AutoGPT 用的都是它的变种。
3.4 Function Calling vs ReAct Prompting
现代大模型(GPT-4、Claude、Qwen-Max、DeepSeek)都支持原生 Function Calling------由模型输出结构化 JSON 来指定调用哪个函数,而不是靠 Prompt 解析。二者关系是:
| 实现方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ReAct Prompting | 靠 Prompt 约束 + 字符串解析 | 所有模型都能用、可调试、可自定义格式 | 解析脆弱、Token 开销大 |
| Function Calling | 模型原生输出结构化 JSON | 稳定、类型安全、生态好 | 依赖特定模型、不好 Debug |
真实的工业实现 :底层是 Function Calling,但外层的控制循环仍然是 ReAct 的那个循环 ------只是把 parse(output) 换成了读取结构化的 tool_calls 字段。
四、手写 Mini-ReAct:100 行 Java 代码看懂 Agent 骨架
理论讲完,上代码。我们先不依赖任何 Agent 框架,用 Java + 百炼(阿里云大模型 API,OpenAI 兼容)手写一个 ReAct 循环。
目标:让 AI 能回答 "帮我算一下今天北京的温度换成华氏度是多少" 这种既要查天气、又要做计算的问题。
4.1 项目依赖(Maven)
xml
<!-- pom.xml -->
<dependencies>
<!-- OpenAI 兼容 SDK(百炼、DeepSeek、GPT 都能用) -->
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>0.8.0</version>
</dependency>
<!-- JSON 解析 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
4.2 工具接口定义
所有工具都实现同一个接口,方便统一调度。
java
// Tool.java
public interface Tool {
String name(); // 工具名
String description(); // 描述(给 LLM 看)
String parametersJsonSchema();// 参数 schema(给 LLM 看)
String invoke(String jsonArgs); // 实际执行
}
4.3 两个示例工具:天气 + 计算器
java
// WeatherTool.java
public class WeatherTool implements Tool {
@Override public String name() { return "get_weather"; }
@Override public String description() {
return "查询指定城市指定日期的天气,返回温度(摄氏度)、空气质量、天气状况";
}
@Override public String parametersJsonSchema() {
return """
{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市中文名"},
"date": {"type": "string", "description": "日期,如 today/tomorrow"}
},
"required": ["city", "date"]
}""";
}
@Override public String invoke(String jsonArgs) {
// 真实场景应调用心知天气 / 高德等 API,这里 Mock
return "{\"temp_c\": 18, \"aqi\": 45, \"weather\": \"晴\"}";
}
}
// CalculatorTool.java
public class CalculatorTool implements Tool {
@Override public String name() { return "calculator"; }
@Override public String description() {
return "对给定的数学表达式求值,支持 +、-、*、/、sqrt 等";
}
@Override public String parametersJsonSchema() {
return """
{
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式"}
},
"required": ["expression"]
}""";
}
@Override public String invoke(String jsonArgs) {
try {
var node = new ObjectMapper().readTree(jsonArgs);
String expr = node.get("expression").asText();
// 用 JS 引擎简单求值(生产环境请用 exp4j 等安全库)
var engine = new javax.script.ScriptEngineManager()
.getEngineByName("JavaScript");
Object result = engine.eval(expr);
return String.valueOf(result);
} catch (Exception e) {
return "ERROR: " + e.getMessage();
}
}
}
4.4 ReAct 控制循环(核心)
这是整篇文章最重要的一段代码,把 ReAct 算法翻译成 Java:
java
// ReActAgent.java
public class ReActAgent {
private static final int MAX_STEPS = 8;
private final OpenAIClient client; // OpenAI 兼容客户端
private final Map<String, Tool> tools;
private final ObjectMapper mapper = new ObjectMapper();
public ReActAgent(OpenAIClient client, List<Tool> toolList) {
this.client = client;
this.tools = toolList.stream()
.collect(Collectors.toMap(Tool::name, t -> t));
}
public String run(String question) {
String prompt = buildInitialPrompt(question);
StringBuilder transcript = new StringBuilder(prompt);
for (int step = 0; step < MAX_STEPS; step++) {
// 1) 让模型生成到 Observation 前停下
String output = chat(transcript.toString(),
List.of("\nObservation:"));
transcript.append(output);
// 2) 模型自己说结束
if (output.contains("Answer:")) {
return extractAnswer(output);
}
// 3) 解析 Action + Action Input
String action = extract(output, "Action:\\s*(.+)");
String actionInput = extract(output, "Action Input:\\s*(\\{.*?\\})");
if (action == null || !tools.containsKey(action)) {
transcript.append("\nObservation: 未知工具 ")
.append(action).append(",请使用列出的工具。\n");
continue;
}
// 4) 调工具
String observation;
try {
observation = tools.get(action).invoke(actionInput);
} catch (Exception e) {
observation = "ERROR: " + e.getMessage();
}
// 5) 拼回 Prompt 进入下一轮
transcript.append("\nObservation: ").append(observation).append("\n");
log.info("[step {}] action={}, input={}, obs={}",
step, action, actionInput, observation);
}
return "抱歉,超过最大推理步数仍未能得出答案。";
}
/* ---------- 辅助方法 ---------- */
private String buildInitialPrompt(String question) {
String toolDesc = tools.values().stream()
.map(t -> String.format("- %s: %s\n 参数 schema: %s",
t.name(), t.description(), t.parametersJsonSchema()))
.collect(Collectors.joining("\n"));
return """
你是一个会使用工具的 AI 助手。可用工具如下:
%s
请严格按以下格式输出(每一轮只输出到 Action Input 就停):
Thought: <思考>
Action: <工具名>
Action Input: <JSON 参数>
Observation: <工具返回,由系统填写>
... (可重复多轮)
Thought: 我已经得到足够信息
Answer: <最终答案>
Question: %s
""".formatted(toolDesc, question);
}
private String chat(String prompt, List<String> stopWords) {
return client.chat().completions().create(ChatCompletionCreateParams.builder()
.model("qwen-plus")
.addMessage(ChatCompletionMessageParam.ofUser(prompt))
.temperature(0.0)
.stop(stopWords)
.build())
.choices().get(0).message().content().orElse("");
}
private static String extract(String text, String regex) {
Matcher m = Pattern.compile(regex, Pattern.DOTALL).matcher(text);
return m.find() ? m.group(1).trim() : null;
}
private static String extractAnswer(String text) {
int idx = text.indexOf("Answer:");
return idx >= 0 ? text.substring(idx + 7).trim() : text;
}
}
4.5 跑一下
java
public class Main {
public static void main(String[] args) {
OpenAIClient client = OpenAIOkHttpClient.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.build();
ReActAgent agent = new ReActAgent(client,
List.of(new WeatherTool(), new CalculatorTool()));
String answer = agent.run("帮我查一下明天北京的温度换成华氏度是多少?");
System.out.println("最终答案:" + answer);
}
}
预期输出(日志):
[step 0] action=get_weather, input={"city":"北京","date":"tomorrow"},
obs={"temp_c":18, "aqi":45, "weather":"晴"}
[step 1] action=calculator, input={"expression":"18 * 9 / 5 + 32"},
obs=64.4
最终答案:明天北京温度约 18℃,换算成华氏度为 64.4℉。
恭喜 ------你已经手写了一个 AI Agent。整个核心就是一个 for 循环 + 一个解析器。
五、工具设计:Function Calling 的正确姿势
手写 Agent 跑通后,接下来的瓶颈往往不在"循环逻辑",而在工具设计。一个工具没设计好,模型会频繁调错或死循环。
5.1 好工具的 5 条设计原则
| 原则 | 说明 | 反例 |
|---|---|---|
| ① 单一职责 | 每个工具只做一件事 | do_everything(cmd, args)------模型不知道啥时候用 |
| ② 描述精确 | description 要让模型"一眼懂" | "获取信息"------啥信息?模型蒙 |
| ③ 入参强 schema | 用 JSON Schema 限定类型、枚举、必填 | 所有参数都 string 类型------模型乱传 |
| ④ 返回结构化 & 紧凑 | JSON > 自然语言;字段越少越好 | 返回一大段说明文字------占 Token 还容易带偏 |
| ⑤ 错误友好 | 错误也要返回可读 JSON | 抛异常让循环 crash------Agent 死掉 |
5.2 反例 vs 正例:查订单工具
❌ 反例:
java
String getOrderInfo(String query); // "query" 是啥?
// 返回:"订单 12345 的详细信息如下:用户张三,2026 年 4 月 ..."
模型调用时完全不知道 query 该传啥,返回的自然语言还得二次解析。
✅ 正例:
java
@Tool(name = "query_order",
description = "根据订单号精确查询订单。只返回订单号已知的情况使用。")
public OrderDTO queryOrder(
@P("订单号,格式为 ORD- 开头的 16 位字符串") String orderId
);
// 返回 JSON:{"orderId": "ORD-...", "userId": 123, "status": "paid", "amount": 99}
精准描述 + 结构化返回,模型调用成功率从 60% 飙到 95%+。
5.3 工具粒度的黄金法则
太粗 合适 太细
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ manage_order │ │ query_order │ │ get_order_id │
│ (cmd,args) │ │ update_order │ │ get_order_usr│
│ │ │ cancel_order │ │ get_order_amt│
│ │ │ │ │ ...(10 个) │
└──────────────┘ └──────────────┘ └──────────────┘
模型不会用 模型一眼懂 调用次数爆炸
经验值:每个 Agent 给它 3~10 个工具最佳。超过 15 个就要考虑分层(主 Agent → 子 Agent)。
六、工业级实现:LangChain4j + Spring Boot
手写 ReAct 让你懂原理;生产环境推荐用 LangChain4j------Java 生态目前最活跃的 LLM 框架,和 Spring Boot 无缝集成。
6.1 为什么选 LangChain4j
- ✅ 原生 Java(不是 Python 移植,没有 JNI/子进程)
- ✅ Spring Boot Starter 官方支持,自动装配
- ✅ 同时支持 Function Calling 和 ReAct Prompting
- ✅ 内置 Memory、RAG、Tool、流式
- ✅ 社区活跃,兼容 OpenAI / Qwen / Claude / DeepSeek / Ollama
6.2 Maven 依赖
xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.devpotato</groupId>
<artifactId>java-agent-server-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>java-agent-server-demo</name>
<description>Minimal Java Agent Server demo</description>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<langchain4j.version>1.12.2-beta22</langchain4j.version>
</properties>
<dependencies>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- LangChain4j Spring Boot Starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI 兼容接入(百炼、DeepSeek、GPT 都能用) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliyun</id>
<name>Nexus aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
6.3 application.yml
yaml
server:
port: 8080
langchain4j:
open-ai:
chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${DASHSCOPE_API_KEY}
model-name: qwen-plus
temperature: 0.0
log-requests: true
log-responses: true
streaming-chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${DASHSCOPE_API_KEY}
model-name: qwen-plus
temperature: 0.0
6.4 用注解定义工具(优雅!)
LangChain4j 最香的地方:用注解把普通 Java 方法变成 LLM 可调用的工具。
java
// WeatherTools.java
@Component
public class WeatherTools {
@Tool("查询指定城市指定日期的天气,返回温度(摄氏度)、天气状况")
public WeatherInfo getWeather(
@P(value = "城市名称,如 北京、上海", required = true) String city,
@P(value = "日期,支持 today / tomorrow / YYYY-MM-DD", required = false) String date
) {
// 业务逻辑:调用天气 API
return new WeatherInfo(25, "晴");
}
public record WeatherInfo(int tempC, int aqi, String weather) {}
}
就这么简单 ------一个 @Tool 注解,Java 方法变身 Agent 工具。
6.5 定义 Agent 服务接口(AiServices 魔法)
java
// AgentService.java
public interface AgentService {
@SystemMessage("""
你是一个会使用工具的 AI 助手。
当你需要信息时,请优先调用工具而不是凭记忆回答。
最终答案要简洁、直接。
""")
String chat(String userMessage);
}
6.6 注册 Agent Bean
LangChain4j 的 AiServices 可以把一个 Java interface 反射变成 AI 服务。
java
// AgentConfig.java
@Configuration
public class AgentConfig {
@Bean
public AgentService agentService(
ChatModel chatModel,
WeatherTool weatherTools,
MathTools mathTools) {
return AiServices.builder(AgentService.class)
.chatModel(chatModel)
.tools(weatherTools, mathTools)
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.build();
}
}
6.7 写一个 Controller 接口
java
// AgentController.java
@RestController
@RequestMapping("/api/agent")
@RequiredArgsConstructor
public class AgentController {
private final AgentService agentService;
@PostMapping("/chat")
public Map<String, String> chat(@RequestBody ChatReq req) {
String answer = agentService.chat(req.message());
return Map.of("answer", answer);
}
public record ChatReq(String message) {}
}
6.8 测试
bash
curl -X POST http://localhost:8080/api/agent/chat \
-H "Content-Type: application/json" \
-d '{"message": "明天北京的气温换算成华氏度是多少?"}'
输出:
json
{
"answer": "北京明天的气温是25°C,换算成华氏度为77°F。"
}
背后发生了什么? LangChain4j 自动帮你:
- 把
@Tool方法转换成 OpenAI Function Calling 的 schema - 调用大模型,拿到
tool_calls - 反射调用你的 Java 方法,拿到结果
- 把结果回灌给大模型,继续对话
- 直到模型给出最终答案,返回给 Controller
你只写了业务逻辑,ReAct 循环、schema 转换、流式处理全被框架封装了。