人工智能(十五)- 从 CoT 到 ReAct,用 LangChain4j 手写一个能思考 + 行动的 Agent

要让 AI 真正"有用",它必须学会行动------调工具、查数据、改系统、发请求。

这一篇,我们就从 CoT 一步步演进到 ReAct(Reasoning + Acting),然后用 Java + Spring Boot + LangChain4j + SSE 从零手写一个能思考、能行动、能流式响应的 Mini Agent。


目录


一、一个"不会行动"的 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 天的订单改成 expired

AI(无工具) :我理解你的需求,建议执行 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 "抱歉,超过最大步数仍未得到答案"

关键点:

  1. stop 参数至关重要 :让模型生成到 Observation: 这行就停下,因为 Observation 的内容必须由程序从工具拿到,不能让模型瞎编。这是 ReAct 不翻车的关键。
  2. 每一轮把"上一轮的思考 + 行动 + 观察"拼回 Prompt:这就是 Agent 的"短期记忆"------整个对话历史就是模型当前的上下文。
  3. 退出条件 :模型吐出 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 CallingReAct 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 自动帮你:

  1. @Tool 方法转换成 OpenAI Function Calling 的 schema
  2. 调用大模型,拿到 tool_calls
  3. 反射调用你的 Java 方法,拿到结果
  4. 把结果回灌给大模型,继续对话
  5. 直到模型给出最终答案,返回给 Controller

你只写了业务逻辑,ReAct 循环、schema 转换、流式处理全被框架封装了。

相关推荐
xixixi777771 小时前
《从心理诱导突破Claude到AI仿冒直播首张拘留单:AI安全、监管与商用的三重转折点》
大数据·网络·人工智能·安全·ai·大模型·风险
爱吃香芋派OvO1 小时前
ComfyUI 视频创作实战手册:节点搭建 + 性能优化 + 批量生成
人工智能·算法·机器学习
立控信息LKONE1 小时前
门禁机、控制器等库室安防设施、实现库室智能联动,一体报警
大数据·人工智能·安全
数智工坊1 小时前
【深度学习RL】A3C:异步强化学习的革命——用CPU打败GPU的深度RL算法
论文阅读·人工智能·深度学习·算法·transformer
小真zzz1 小时前
中立第三方:搜极星的突围之路
大数据·人工智能
Jackzaker1 小时前
Prompt工程在代码中的实现
人工智能·python·prompt
数智工坊1 小时前
【深度学习RL】DQN:深度强化学习的里程碑——让AI从像素中学会玩Atari游戏
论文阅读·人工智能·深度学习·游戏·transformer
Xpower 171 小时前
从PHM到AI Agent-如何用OpenClaw构建设备健康诊断智能体
网络·人工智能·学习·算法
yzx9910131 小时前
软件脚本定制开发:从需求到交付的技术实战指南
大数据·人工智能·数据挖掘