从 ReAct 到 MCP:用 Java 写 AI Agent 的正确姿势

大模型应用真正落地企业,靠的不是 ChatGPT 式的对话框,而是能调系统、查数据、写工单的 Agent。本文用 Spring AI 1.0 从零搭建一个企业级 Agent,并对比 LangChain4j 的差异。读完你能拿到一份可直接跑通的工程模板。

一、先看效果:3 行自然语言搞定原本要点 5 个菜单的事

一位 HR 想知道:

“把张伟上个月请假超过 2 天的记录导一份给我,顺便算下他剩余年假。”

传统做法:登录 HR 系统 → 选员工 → 切到请假明细 → 筛选日期 → 看年假余额。至少 5 次点击。

接上 Agent 之后,控制台输出是这样的:

[Thinking] 需要分两步:先查请假记录,再查年假余额
[Tool Call] queryLeaveRecords(employee="张伟", startDate="2026-04-01", endDate="2026-04-30", minDays=2)
[Tool Result] [{"date":"2026-04-08","days":3,"type":"事假"}]
[Tool Call] getAnnualLeaveBalance(employee="张伟")
[Tool Result] {"total":15, "used":7, "remaining":8}
[Final Answer] 张伟上月共有 1 条超过 2 天的请假记录:4 月 8 日起事假 3 天。
              当前年假总额 15 天,已用 7 天,剩余 8 天。

这就是 Agent——它不是更大的模型,而是给模型加了"手脚":一个能感知(Observation)、能思考(Thought)、能执行(Action)的循环。

下面我们一步步把它造出来。

二、Agent 的核心:把 ReAct 循环讲清楚

任何 Agent 框架,剥到底层都是同一张图:

用户输入

LLM 思考 Thought

是否需要工具?

选择工具 + 生成参数

执行工具 Action

获取结果 Observation

生成最终回答

这就是 2022 年 Princeton 提出的 ReAct 框架:Reasoning + Acting 交替进行。

OpenAI 后来把它工程化为 Function Calling,再演化为今天的 Tool Use。Anthropic 在 2024 年底进一步抽象出 MCP(Model Context Protocol),把工具变成了可热插拔的服务。Spring AI 1.0 同时支持这三层。

记住一句话:Agent = LLM + 工具 + 循环控制。框架做的事,就是把这个循环写好,让你只关心工具本身。

三、Java 生态两强对比:Spring AI vs LangChain4j

国内 Java 团队在 2026 年做 Agent,绕不开这两个选择:

维度 Spring AI 1.0 LangChain4j 1.x
设计哲学 Spring 风格,声明式、依赖注入 LangChain 风格,Builder + 接口代理
Agent 定义 @Tool + ChatClient @Tool + AiServices 接口
记忆机制 ChatMemory + Advisor ChatMemory 内建
国产模型支持 通过 OpenAiChatModel 兼容 DeepSeek、通义、Kimi 同左,社区扩展更激进
MCP 支持 官方 spring-ai-mcp-client 社区实现
学习曲线 熟悉 Spring 的人 1 小时上手 偏向 Python 经验迁移

怎么选? 如果项目是 Spring Boot 单体或微服务(绝大多数国内中后台都是),选 Spring AI——它和 @Configuration@BeanConfigurationProperties 浑然一体。如果是新项目想贴近 Python 社区生态,选 LangChain4j。

下文以 Spring AI 1.0 为主线,末尾会给一段 LangChain4j 对照代码。

四、动手:30 分钟搭一个"HR 助手 Agent"

4.1 项目依赖

pom.xml 关键部分:

<properties>
    <java.version>21</java.version>
    <spring-ai.version>1.0.0</spring-ai.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 用 OpenAI 协议兼容层接 DeepSeek/通义/Kimi 都行 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <!-- 对话记忆 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

4.2 配置文件

application.yml

spring:
  ai:
    openai:
      base-url: https://api.deepseek.com   # 国内首选,便宜稳定
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        options:
          model: deepseek-chat
          temperature: 0.2                 # Agent 任务温度要低

小坑:很多新人把 temperature 留默认值 0.7,结果工具参数老飘。Agent 类任务建议压到 0.0–0.3。

4.3 定义工具

这是 Agent 最核心的部分。把你已有的业务方法直接标注成工具:

@Component
public class HrTools {

    private final HrApiClient hrApi;   // 你现成的 Feign / RestClient

    public HrTools(HrApiClient hrApi) {
        this.hrApi = hrApi;
    }

    @Tool(description = "查询指定员工在某段时间内的请假记录。"
                      + "minDays 用于过滤天数下限,0 表示不过滤。")
    public List<LeaveRecord> queryLeaveRecords(
            @ToolParam(description = "员工姓名或工号") String employee,
            @ToolParam(description = "起始日期,格式 yyyy-MM-dd") String startDate,
            @ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate,
            @ToolParam(description = "最小天数") int minDays) {

        return hrApi.listLeaves(employee, startDate, endDate).stream()
                .filter(r -> r.days() >= minDays)
                .toList();
    }

    @Tool(description = "获取员工当前年假余额")
    public LeaveBalance getAnnualLeaveBalance(
            @ToolParam(description = "员工姓名或工号") String employee) {
        return hrApi.balanceOf(employee);
    }
}

注意三个关键点:

  1. description 不是注释,是 Prompt。模型靠它判断什么时候调用。写得越清楚,调用越准。
  2. 参数描述要具体到格式。日期写明 yyyy-MM-dd,否则模型可能给你 "上个月" 这种字符串。
  3. 返回值用 Record 或 POJO 都行,Spring AI 会自动序列化成 JSON 给模型。

4.4 装配 ChatClient

@Configuration
public class AgentConfig {

    @Bean
    public ChatMemory chatMemory() {
        return MessageWindowChatMemory.builder()
                .maxMessages(20)
                .build();
    }

    @Bean
    public ChatClient hrAgent(ChatClient.Builder builder,
                              ChatMemory memory,
                              HrTools hrTools) {
        return builder
                .defaultSystem("""
                    你是一名企业 HR 助手。
                    - 涉及员工数据时,必须通过工具查询,不要凭记忆作答。
                    - 日期一律用 yyyy-MM-dd 格式。
                    - 回答前请先在心里列出步骤,但只向用户输出最终结论。
                """)
                .defaultTools(hrTools)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
                .build();
    }
}

System Prompt 决定 Agent 的"人格"和"行为边界"。我习惯写明:

  • 必须用工具的场景(防止幻觉)
  • 格式约束(日期、货币、单位)
  • 思考方式(让模型内部 CoT,但不啰嗦输出)

4.5 暴露接口

@RestController
@RequestMapping("/api/hr")
public class HrAgentController {

    private final ChatClient hrAgent;

    public HrAgentController(ChatClient hrAgent) {
        this.hrAgent = hrAgent;
    }

    @PostMapping("/ask")
    public Map<String, String> ask(@RequestBody AskRequest req) {
        String reply = hrAgent.prompt()
                .user(req.question())
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, req.sessionId()))
                .call()
                .content();
        return Map.of("answer", reply);
    }

    record AskRequest(String sessionId, String question) {}
}

启动项目,调用:

curl -X POST http://localhost:8080/api/hr/ask \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"u-1001","question":"张伟上个月请假超过2天的记录有哪些?顺便看看年假还剩多少"}'

至此,一个能调用真实业务系统的 Agent 已经跑起来了。整个工程不到 200 行代码。

五、生产化:四个绕不开的工程问题

Demo 跑通离上线还很远。下面是我在多个企业项目里反复踩过的坑。

5.1 工具调用要做幂等与超时

LLM 会出现"循环调用同一个工具"或"参数微调后重试"的情况。所有写操作工具必须:

@Tool(description = "提交请假申请")
public String submitLeave(@ToolParam String requestId, /* ... */) {
    // 用 requestId 做幂等键
    if (cache.exists("leave:" + requestId)) {
        return "该申请已提交,无需重复";
    }
    // ...
}

读操作也建议加超时:

@Bean
public ChatClient hrAgent(ChatClient.Builder builder, ...) {
    return builder
            .defaultOptions(OpenAiChatOptions.builder()
                    .toolCallReturnDirect(false)
                    .build())
            // ...
            .build();
}

5.2 Token 成本:会话越长越烧钱

MessageWindowChatMemory(20) 听起来够用,但一旦工具返回 JSON 比较大,20 条消息就能把上下文撑到几万 token。

实战做法:

  • 工具返回值在送给模型前做摘要(比如只保留 5 条最相关记录)。
  • VectorStoreChatMemoryAdvisor 替代滑窗,把历史向量化存储,按需检索。
  • 接 DeepSeek 这种带 上下文缓存 的厂商,重复 system prompt 部分能省 80% 费用。

5.3 防止"工具地狱":分层 Agent

当工具数量超过 20 个,模型选错工具的概率快速上升。解决方案是分层:

RouterAgent (判断意图)
   ├── HrAgent (HR 工具 8 个)
   ├── FinanceAgent (财务工具 6 个)
   └── ItOpsAgent (运维工具 10 个)

Spring AI 里实现很简单——把每个子 Agent 包装成一个 @Tool 暴露给 Router:

@Tool(description = "处理一切人力资源相关问题:请假、考勤、薪酬、年假")
public String askHr(@ToolParam String question) {
    return hrAgent.prompt().user(question).call().content();
}

5.4 可观测:必须打全链路日志

Agent 的故障定位比普通接口难十倍。最少要记三类日志:

  1. 每一轮 LLM 输入输出(含 token 数)
  2. 每一次工具调用的参数与返回
  3. 整条会话的 trace_id

Spring AI 1.0 提供了 ChatModelObservationConvention,能直接接 Micrometer + SkyWalking。生产环境务必打开。

六、进阶:用 MCP 把工具"插件化"

到目前为止,工具都是写在自己工程里的。但企业里更常见的是:HR 系统是 A 团队的、ERP 是 B 团队的、报销系统是外采的。每接一个就改 Agent 代码?不现实。

MCP(Model Context Protocol) 解决的就是这个问题。它把工具变成独立的"工具服务",通过标准协议暴露。Agent 想用哪个工具,连过去拿就行——就像 USB-C 之于外设。

Spring AI 1.0 接 MCP 服务只需一行配置:

spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            hr-server:
              url: http://hr-mcp.internal:8090
            finance-server:
              url: http://finance-mcp.internal:8091

然后注入 ToolCallbackProvider

@Bean
public ChatClient agent(ChatClient.Builder builder,
                        ToolCallbackProvider mcpTools) {
    return builder.defaultToolCallbacks(mcpTools).build();
}

新增一个业务系统?让对方部署一个 MCP Server 即可,Agent 端零改动。这是 2026 年企业 Agent 架构的主流方向。

七、对照:同样的功能用 LangChain4j 怎么写

为了完整性,给一段等价代码:

interface HrAssistant {
    @SystemMessage("你是一名企业 HR 助手...")
    String chat(@MemoryId String sessionId, @UserMessage String question);
}

HrAssistant assistant = AiServices.builder(HrAssistant.class)
        .chatLanguageModel(OpenAiChatModel.builder()
                .baseUrl("https://api.deepseek.com")
                .apiKey(System.getenv("DEEPSEEK_API_KEY"))
                .modelName("deepseek-chat")
                .build())
        .tools(new HrTools(hrApi))
        .chatMemoryProvider(sid -> MessageWindowChatMemory.withMaxMessages(20))
        .build();

String reply = assistant.chat("u-1001", "张伟上个月...");

LangChain4j 把 Agent 抽象成接口代理,写起来更紧凑,但少了 Spring 的依赖注入弹性。两个框架我都用过,结论:团队里 Spring 经验越深,越应该选 Spring AI

八、写在最后

一年前做大模型应用,主战场还是"Prompt 调到天荒地老"。今天,焦点已经转移到:

  • 工具怎么设计才能让模型用得准(描述比代码更重要)
  • 多 Agent 怎么协作才能不打架(分层路由)
  • 工具怎么解耦才能横向扩张(MCP)

Spring AI 1.0 已经把这些能力做成了 Spring 开发者熟悉的形态。门槛真正消失的不是模型能力,而是工程化路径

如果你正在评估"要不要在公司项目里引入 Agent",我的建议是:先挑一个查询场景(报表、知识库、流水),用本文模板花一周做个 POC。跑通后你会发现,最难的不是模型,而是把业务接口梳理成"模型能理解的样子"。


转载请注明出处。文中观点基于作者在多个 AI 项目中的实践,仅供参考。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐