AI Agent 开发实战(六):用 Spring AI 搭建你的第一个 Agent

AI Agent 开发实战(六):用 Spring AI 搭建你的第一个 Agent

这是「AI Agent 开发实战」系列的第 6 篇。前 5 篇我们聊了概念、LLM、记忆、工具和框架横评,今天终于要动手了------用 Spring AI 从零搭建一个能调用工具的 Agent,跑通 ReAct 循环。

读完本文,你会得到一个可运行的 Spring AI Agent 项目,它能让 LLM 自主决定何时调用工具、何时直接回答。


一、Spring AI 是什么,不是什么

先对齐预期:

维度 Spring AI LangChain4j
定位 Spring 官方 AI 抽象层 Java 版 LangChain
风格 Spring Boot 自动配置 + Bean 注入 Builder 模式 + 链式调用
上手成本 Spring 开发者几乎零成本 需要学一套新 API
灵活性 通过 Adapter 扩展 内置组件丰富
生态 Spring 全家桶天然集成 多框架适配

Spring AI 的核心理念 :把 LLM 当作 Spring 生态的一等公民------像用 JdbcTemplate 访问数据库一样用 ChatClient 访问大模型。

javascript 复制代码
Spring AI 架构全景
│
├── Model 层
│   ├── ChatModel(对话模型抽象)
│   ├── EmbeddingModel(向量模型抽象)
│   ├── ImageModel(图像模型抽象)
│   └── AudioModel(语音模型抽象)
│
├── API 层(各厂商适配器)
│   ├── OpenAI / Azure OpenAI
│   ├── Anthropic Claude
│   ├── 阿里通义 / 百度文心 / 智谱
│   └── Ollama(本地模型)
│
├── 功能层
│   ├── ChatClient(统一调用入口)
│   ├── Function Calling(工具调用)
│   ├── RAG(检索增强)
│   ├── Advisors(拦截器链)
│   └── Memory(对话记忆)
│
└── 集成层
    ├── Vector Store(向量存储)
    ├── Spring Boot AutoConfiguration
    └── Observability(Micrometer + Zipkin)

二、项目搭建:5 分钟跑起来

2.1 创建项目

使用 Spring Initializr(start.spring.io),选:

  • Java 17+
  • Spring Boot 3.3+
  • 依赖:Spring Web + Spring AI

pom.xml 关键依赖:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2.2 配置模型

application.yml

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.openai.com  # 或你的代理地址
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.7

如果用国内模型,换对应的 starter。比如用智谱:spring-ai-zhipuai-spring-boot-starter,配置 spring.ai.zhipuai.api-key

2.3 验证连通

java 复制代码
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    private final ChatClient chatClient;

    public AgentController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

启动后访问 http://localhost:8080/api/agent/chat?message=你好,能返回回复就说明通了。


三、给 Agent 加上"手脚":Function Calling

没有工具的 Agent 只是个 Chatbot。Spring AI 通过 @Tool 注解(或函数式注册)让 LLM 能调用你的 Java 方法。

3.1 定义工具

java 复制代码
@Component
public class WeatherTools {

    @Tool(description = "查询指定城市的当前天气信息,返回温度、天气状况和风力")
    public String getWeather(@ToolParam(description = "城市名称,如:北京、上海") String city) {
        // 实际项目中调用天气 API
        // 这里模拟返回
        Map<String, String> mockData = Map.of(
            "北京", "晴天,温度 28°C,北风 3 级",
            "上海", "多云,温度 31°C,东南风 2 级",
            "深圳", "雷阵雨,温度 33°C,南风 4 级"
        );
        return mockData.getOrDefault(city, city + ":暂无天气数据");
    }

    @Tool(description = "计算两个城市之间的距离(单位:公里)")
    public String getDistance(
            @ToolParam(description = "出发城市") String fromCity,
            @ToolParam(description = "目的城市") String toCity) {
        // 模拟距离计算
        return fromCity + " 到 " + toCity + " 约 1200 公里";
    }
}

3.2 注册工具并调用

Spring AI 1.0 的方式------在 ChatClient 调用时指定工具:

java 复制代码
@RestController
@RequestMapping("/api/agent")
public class AgentController {

    private final ChatClient chatClient;
    private final WeatherTools weatherTools;

    public AgentController(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.chatClient = builder.build();
        this.weatherTools = weatherTools;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .tools(weatherTools)  // 注册工具
                .call()
                .content();
    }
}

3.3 测试

arduino 复制代码
用户:北京今天天气怎么样?适合出门吗?

LLM 思考 → 需要查询天气 → 调用 getWeather("北京")
→ 拿到结果:"晴天,温度 28°C,北风 3 级"
→ 综合回答:"北京今天晴天,28°C,北风3级,非常适合出门!"

关键点:你不需要写任何 if-else 来判断"用户问的是不是天气"。LLM 自己根据工具描述决定调不调、传什么参数。这就是 Agent 的"自主"。


四、加记忆:让 Agent 记住上下文

上面的 Agent 每次对话都是失忆的。加上对话记忆:

4.1 对话记忆(Chat Memory)

java 复制代码
@Configuration
public class AgentConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(new InMemoryChatMemory())
                        .build()
                )
                .build();
    }
}

4.2 区分会话

多用户场景下,需要按会话 ID 隔离记忆:

java 复制代码
@GetMapping("/chat")
public String chat(
        @RequestParam String message,
        @RequestParam(defaultValue = "default") String sessionId) {

    return chatClient.prompt()
            .user(message)
            .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
            .tools(weatherTools)
            .call()
            .content();
}

记忆内部结构:

erlang 复制代码
InMemoryChatMemory
│
├── session "user-001"
│   ├── [UserMessage] "北京天气怎么样?"
│   ├── [AssistantMessage] "北京今天晴天,28°C..."
│   └── [UserMessage] "那上海呢?"
│
├── session "user-002"
│   └── [UserMessage] "深圳到北京多远?"
│
└── ...

五、完整 Agent 示例:出行助手

把工具、记忆、提示词模板组合起来,搭一个"出行助手" Agent:

5.1 Agent 服务层

java 复制代码
@Service
public class TravelAssistant {

    private final ChatClient chatClient;
    private final WeatherTools weatherTools;

    public TravelAssistant(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.weatherTools = weatherTools;
        this.chatClient = builder
                .defaultSystem("""
                    你是一个出行助手。用户会问你天气、距离等出行相关问题。
                    你需要根据问题自主决定调用哪些工具,综合信息后给出建议。
                    回答时:
                    1. 先给出事实数据(天气、距离等)
                    2. 再给出出行建议(穿什么、带伞、交通方式等)
                    3. 语气友好,像一个懂行的朋友
                    """)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(new InMemoryChatMemory()).build()
                )
                .build();
    }

    public String chat(String sessionId, String userMessage) {
        return chatClient.prompt()
                .user(userMessage)
                .advisors(a -> a.param(
                    MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY,
                    sessionId))
                .tools(weatherTools)
                .call()
                .content();
    }
}

5.2 多轮对话效果

scss 复制代码
第 1 轮:
用户:我明天要从北京去上海出差,帮我看看两边的天气
Agent → 调用 getWeather("北京") + getWeather("北京")
Agent:北京明天晴天28°C,上海多云31°C。建议北京穿薄外套,上海比较闷热穿短袖即可,不用带伞。

第 2 轮:
用户:北京到上海多远?
Agent → 调用 getDistance("北京", "上海")
Agent:北京到上海约1200公里。高铁大约4.5小时,飞机约2小时。出差的话高铁更稳当,不受天气影响。

第 3 轮:
用户:那我还是坐高铁吧
Agent:好选择!高铁准点率高,而且市中心到市中心,比飞机省了机场往返时间。记得提前买票,热门时段容易售罄。

注意第 3 轮------Agent 没有调用任何工具,纯靠记忆上下文就给出了合理建议。这就是 LLM 的自主决策:它知道什么时候需要工具、什么时候不需要。


六、Advisor 机制:Agent 的中间件

Spring AI 的 Advisor 机制类似 Servlet Filter,可以在 LLM 调用的前后插入逻辑:

scss 复制代码
用户请求
│
▼
Advisor 链
│
├── 1. MessageChatMemoryAdvisor(注入对话记忆)
├── 2. 你的自定义 Advisor(日志/限流/兜底)
├── ...
│
▼
ChatModel.call() → LLM API
│
▼
Advisor 链(逆向)
│
├── 2. 你的自定义 Advisor(处理响应)
├── 1. MessageChatMemoryAdvisor(保存本轮对话到记忆)
│
▼
返回给用户

自定义 Advisor 示例:调用日志

java 复制代码
public class LoggingAdvisor implements CallAroundAdvisor {

    @Override
    public String getName() {
        return "LoggingAdvisor";
    }

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        long start = System.currentTimeMillis();

        // 打印用户输入
        log.info("用户输入: {}", request.userText());

        // 继续调用链
        AdvisedResponse response = chain.nextAroundCall(request);

        // 打印 LLM 输出和耗时
        long cost = System.currentTimeMillis() - start;
        log.info("LLM 输出: {}, 耗时: {}ms", response.response().getResult().getOutput().getText(), cost);

        return response;
    }
}

注册:

java 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
    return builder
            .defaultAdvisors(
                new LoggingAdvisor(),
                MessageChatMemoryAdvisor.builder(new InMemoryChatMemory()).build()
            )
            .build();
}

七、生产环境的注意事项

7.1 超时与重试

java 复制代码
@Bean
public ChatClient chatClient(ChatClient.Builder builder, WeatherTools weatherTools) {
    return builder
            .defaultOptions(ChatOptionsBuilder.builder()
                    .withMaxTokens(2048)
                    .build())
            .build();
}

// 外层用 Spring Retry 包裹
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public String chat(String sessionId, String message) {
    // ...
}

7.2 工具调用的安全边界

java 复制代码
@Component
public class SafeWeatherTools {

    private static final Set<String> ALLOWED_CITIES = Set.of("北京", "上海", "深圳", "广州");

    @Tool(description = "查询指定城市的当前天气")
    public String getWeather(@ToolParam(description = "城市名称") String city) {
        // 防止 LLM 幻觉出非法城市名
        if (!ALLOWED_CITIES.contains(city)) {
            return "不支持查询该城市的天气";
        }
        return doQueryWeather(city);
    }

    private String doQueryWeather(String city) {
        // 调用真实 API
        return "...";
    }
}

7.3 记忆持久化

InMemoryChatMemory 重启就丢。生产环境用持久化实现:

java 复制代码
// 基于 Redis 的记忆存储
public class RedisChatMemory implements ChatMemory {

    private final RedisTemplate<String, String> redisTemplate;

    @Override
    public void add(String conversationId, List<Message> messages) {
        String key = "chat:memory:" + conversationId;
        // 序列化并存储,设置过期时间
        messages.forEach(msg -> redisTemplate.opsForList().rightPush(key, serialize(msg)));
        redisTemplate.expire(key, Duration.ofHours(24));
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        String key = "chat:memory:" + conversationId;
        // 取最近 N 条
        List<String> raw = redisTemplate.opsForList().range(key, -lastN, -1);
        return raw.stream().map(this::deserialize).toList();
    }

    // ... 序列化/反序列化省略
}

八、Spring AI vs 手写 Agent:什么时候用框架

场景 推荐 理由
快速验证 Agent 概念 手写 MiniAgent 理解原理,没有黑盒
Spring 技术栈团队 Spring AI 零学习成本,Bean 注入
需要丰富的内置组件 LangChain4j RAG 管道、文档加载等开箱即用
生产级 Agent 服务 Spring AI + 自定义 Advisor 既有框架支撑,又能灵活扩展
需要精细控制 Agent 行为 手写 + 框架混合 核心循环自己写,工具调用用框架

我的建议 :先用 Spring AI 跑通一个 Agent(就是本文的做法),理解 ChatClient + Tool + Advisor + Memory 这四件套怎么协作。遇到框架限制再手写核心部分,不要一上来就造轮子。


九、小结

css 复制代码
Spring AI Agent 四件套
│
├── ChatClient → 统一调用入口,替代手写 HTTP
│
├── @Tool → 声明式工具注册,LLM 自主选择调用
│
├── Advisor → 拦截器链,记忆/日志/限流/兜底
│
└── ChatMemory → 对话记忆,支持按会话隔离
要点 说明
工具注册 @Tool + @ToolParam 声明式,零配置
LLM 自主决策 不需要写 if-else,LLM 根据 Tool 描述自动选择
记忆隔离 通过 sessionId 区分不同用户的对话上下文
Advisor 机制 类似 Filter 的中间件,可组合扩展
安全边界 工具方法内部做参数校验,防止 LLM 幻觉注入

下一篇我们聊 Harness Engineering 与约束管理:怎么用 Plan 模板、工具注册约束、输出 Schema 让 Agent 的行为更可控------毕竟"自由"和"可控"之间需要找到一个平衡。


这是「AI Agent 开发实战」系列第 6 篇,系列目录:

  1. 别再把 LLM 当聊天机器人了,这才是 Agent 的正确打开方式
  2. 三大基石之 LLM 调用与 Prompt 工程
  3. 三大基石之记忆系统
  4. 三大基石之工具调用
  5. Java 生态 Agent 框架横评
  6. 本文:用 Spring AI 搭建你的第一个 Agent
相关推荐
zhangjw342 小时前
第36篇:Spring Boot进阶:Web开发+参数校验+全局异常处理
前端·spring boot·后端
完美火龙篇 四月的友2 小时前
SpringBoot 即时聊天 IM 完整实现(HTTP会话管理 \+ WebSocket实时推送 \+ 离线消息)
spring boot·websocket·http
李剑一2 小时前
Kimi暂时关上新用户订阅渠道你以为是缺钱吗?其实可能更缺卡!
aigc·openai·ai编程
Bug收容所3 小时前
12305项目学习day5
java·spring boot·redis·mysql·spring·rocketmq
小Ti客栈15 小时前
Spring Boot 集成 Springdoc-OpenAPI 与 Knife4j实现接口文档与可视化调试
java·spring boot·后端
chaors15 小时前
DeepResearchSystem 0x02:Graph 构建
langchain·openai·ai编程
东小西16 小时前
第11篇:《上生产前夜的恐惧:Prompt注入、敏感词、Token成本,我一个一个填坑》
openai·ai编程
paopaokaka_luck20 小时前
基于Springboot3+Vue3的高校选课系统(AI选课助手、协同过滤算法、分享到微博、扣扣、Echarts图形化分析)
网络·spring boot·网络协议·echarts
小Ti客栈1 天前
Spring Boot 整合 Swagger2 和 Knife4j实现接口文档与可视化调试
java·spring boot·后端