Spring AI 实战指南:从零开始构建企业级 AI 应用(附完整代码)

前言

最近一年,AI 火得不行。作为 Java 后端开发,你是不是也想过:

  • “我想在公司做个 AI 客服,但不知道从哪开始?”
  • “面试被问到 AI 架构,我该怎么说?”
  • “直接调 OpenAI API 太简单,想学点企业级的方案?”

别慌!今天带你从 0 到 1 掌握 Spring AI——Spring 官方推出的 AI 框架,让你用熟悉的 Spring 生态快速构建 AI 应用。


一、为什么要学习 Spring AI(背景 + 痛点)

1.1 传统后端 vs AI 应用的差异

先看个对比:

传统 Web 应用 AI 应用
输入 → 处理逻辑 → 输出(确定性) 输入 → AI 模型 → 输出(概率性)
数据库存储结构化数据 需要处理非结构化文本
接口响应快(毫秒级) AI 推理慢(秒级)
结果可预测 结果有随机性(幻觉)

痛点:传统开发模式搞不定 AI 的特性!

1.2 为什么不用直接调 OpenAI API?

直接调 API 的问题:

// 直接调 OpenAI API 的代码
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.openai.com/v1/chat/completions";
// 手动拼 JSON、处理流式响应、管理上下文...

问题

  • ❌ 每次都要手动拼 Prompt
  • ❌ 多轮对话要自己管理上下文
  • ❌ 想换模型(从 GPT-3.5 换到通义千问)要改一堆代码
  • ❌ 没有统一的异常处理
  • ❌ 没有集成向量数据库

1.3 Spring AI 解决了什么问题?

Spring AI = Spring 生态 + AI 能力,让你:

✅ 像调用 MyBatis 一样调用 AI 模型(抽象统一接口)
✅ 开箱即用的 Prompt 管理
✅ 自动处理多轮对话上下文
✅ 集成向量数据库(Redis、ES、Milvus)
✅ 流式响应支持
✅ 模型切换零成本

类比

  • Spring AI 对 AI 应用,就像 Spring MVC 对 Web 应用
  • 就像你用 MyBatis 不用管底层 JDBC,Spring AI 让你不用管底层 HTTP 调用

二、Spring AI 核心概念讲解(通俗易懂)

2.1 Prompt(提示词)

类比:Prompt 就像 MyBatis 的 SQL 语句

// MyBatis 写 SQL
@Select("SELECT * FROM user WHERE name = #{name}")
User findByName(String name);

// Spring AI 写 Prompt
String prompt = "你是一个客服,回答用户问题:{question}";

2.2 Model(模型)

类比:Model 就像数据源(DataSource)

  • 可以是 OpenAI 的 GPT-4
  • 可以是阿里的通义千问
  • 可以是本地部署的 Llama

Spring AI 统一抽象,切换模型只需要改配置!

2.3 ChatClient / AI Client

类比:就像 JdbcTemplate / RestTemplate

// 像调用数据库一样调用 AI
String response = chatClient.prompt()
    .user("帮我写个排序算法")
    .call()
    .content();

2.4 Memory(上下文记忆)

类比:就像 HTTP Session

  • HTTP Session 记住用户登录状态
  • Memory 记住对话历史

没有 Memory,AI 每次都是 “失忆” 状态:

用户:我叫张三
AI:你好!
用户:我叫什么名字?
AI:我不知道你的名字... 😅

2.5 Embedding(向量化)

类比:就像全文搜索的索引

  • 传统搜索:关键词匹配
  • 向量搜索:语义理解
搜索 "苹果"
传统搜索:匹配 "苹果" 这个词
向量搜索:能理解是水果还是手机(看上下文)

2.6 RAG(检索增强生成)

类比:就像开卷考试

  • 没有资料:纯靠记忆(可能瞎编)
  • 有资料:查资料后回答(更准确)

RAG 流程

用户提问 → 向量数据库检索相关文档 → 把文档塞给 AI → AI 基于文档回答

三、快速上手(第一个 AI 接口)

3.1 项目初始化

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

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

注意:需要添加 Spring AI 仓库

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

3.2 配置文件

# application.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}  # 从环境变量读取
      chat:
        options:
          model: gpt-3.5-turbo
          temperature: 0.7  # 0-1,越低越稳定

3.3 完整代码示例

Controller 层

@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class ChatController {
    
    private final ChatService chatService;
    
    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request) {
        return chatService.chat(request.getMessage());
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class ChatRequest {
    private String message;
}

Service 层

@Service
@RequiredArgsConstructor
public class ChatService {
    
    private final ChatClient chatClient;
    
    public String chat(String message) {
        return chatClient.prompt()
            .user(message)
            .call()
            .content();
    }
}

3.4 测试接口

curl -X POST http://localhost:8080/api/ai/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "你好,请用 Java 写一个冒泡排序"}'

响应

public class BubbleSort {
    public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

🎉 恭喜!你的第一个 AI 接口完成了!


四、进阶:实现多轮对话(上下文记忆)

4.1 为什么需要 Memory?

没有上下文的问题:

用户:我的订单号是 12345
AI:好的,已记录
用户:订单状态是什么?
AI:请问您的订单号是多少?😅

4.2 实现上下文对话

方案一:使用 ChatMemory(推荐)

@Service
@RequiredArgsConstructor
public class ConversationService {
    
    private final ChatClient chatClient;
    
    // 使用 InMemoryChatMemory(生产环境建议用 Redis)
    private final ChatMemory chatMemory = new InMemoryChatMemory();
    
    public String chat(String sessionId, String message) {
        return chatClient.prompt()
            .user(message)
            .advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 10)) // 保留最近10条
            .call()
            .content();
    }
}

方案二:使用 Redis 存储

@Configuration
public class RedisChatMemoryConfig {
    
    @Bean
    public ChatMemory redisChatMemory(RedisTemplate<String, String> redisTemplate) {
        return new RedisChatMemory(redisTemplate);
    }
}

// Redis 实现
public class RedisChatMemory implements ChatMemory {
    
    private final RedisTemplate<String, String> redisTemplate;
    private static final String KEY_PREFIX = "chat:memory:";
    
    @Override
    public void add(String conversationId, List<Message> messages) {
        String key = KEY_PREFIX + conversationId;
        List<String> jsonMessages = messages.stream()
            .map(this::serializeMessage)
            .collect(Collectors.toList());
        redisTemplate.opsForList().rightPushAll(key, jsonMessages);
        redisTemplate.expire(key, 1, TimeUnit.HOURS); // 1小时过期
    }
    
    @Override
    public List<Message> get(String conversationId, int lastN) {
        String key = KEY_PREFIX + conversationId;
        Long size = redisTemplate.opsForList().size(key);
        if (size == null || size == 0) {
            return Collections.emptyList();
        }
        
        // 获取最后 N 条
        int start = Math.max(0, size - lastN);
        List<String> messages = redisTemplate.opsForList()
            .range(key, start, -1);
        
        return messages.stream()
            .map(this::deserializeMessage)
            .collect(Collectors.toList());
    }
    
    // ... 序列化/反序列化方法
}

4.3 Controller 调用

@RestController
@RequestMapping("/api/conversation")
@RequiredArgsConstructor
public class ConversationController {
    
    private final ConversationService conversationService;
    
    @PostMapping("/{sessionId}/chat")
    public String chat(
        @PathVariable String sessionId,
        @RequestBody String message
    ) {
        return conversationService.chat(sessionId, message);
    }
}

4.4 测试多轮对话

# 第一轮
SESSION_ID="user-123"
curl -X POST http://localhost:8080/api/conversation/$SESSION_ID/chat \
  -H "Content-Type: text/plain" \
  -d "我的名字叫张三"

# 第二轮(AI 能记住)
curl -X POST http://localhost:8080/api/conversation/$SESSION_ID/chat \
  -H "Content-Type: text/plain" \
  -d "我叫什么名字?"

# 响应:你的名字是张三 ✅

五、实战:基于 RAG 的知识库问答

5.1 业务场景

场景:企业内部文档问答系统

  • 员工问:“报销流程是什么?”
  • 系统自动检索《财务报销手册》
  • AI 基于文档内容回答

5.2 向量数据库原理(通俗讲)

传统搜索 vs 向量搜索

搜索词:"怎么退款"

传统搜索(关键词匹配):
✅ "退款流程"
❌ "如何申请售后"(语义相同但词不同)

向量搜索(语义匹配):
✅ "退款流程"
✅ "如何申请售后"
✅ "货不对版怎么办"

向量是什么?

把文本转换成数字数组(比如 1536 维),语义相似的文本向量距离近。

"猫" → [0.1, 0.2, 0.8, ...]
"狗" → [0.1, 0.3, 0.7, ...]  (距离近)
"汽车" → [0.9, 0.1, 0.1, ...]  (距离远)

5.3 接入向量数据库(Redis)

依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
    <version>1.0.0-M4</version>
</dependency>

配置

spring:
  data:
    redis:
      host: localhost
      port: 6379
  ai:
    vectorstore:
      redis:
        index: knowledge-base
        prefix: "doc:"
    embedding:
      options:
        model: text-embedding-ada-002

5.4 完整 RAG 实现

步骤 1:文档向量化

@Service
@RequiredArgsConstructor
public class DocumentService {
    
    private final VectorStore vectorStore;
    private final EmbeddingModel embeddingModel;
    
    /**
     * 上传文档并向量化
     */
    public void uploadDocument(String title, String content) {
        Document document = new Document(content, 
            Map.of("title", title, "timestamp", System.currentTimeMillis()));
        
        vectorStore.add(List.of(document));
    }
    
    /**
     * 批量导入文档
     */
    public void importDocuments(List<DocumentData> docs) {
        List<Document> documents = docs.stream()
            .map(doc -> new Document(doc.getContent(), 
                Map.of("title", doc.getTitle())))
            .collect(Collectors.toList());
        
        vectorStore.add(documents);
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class DocumentData {
    private String title;
    private String content;
}

步骤 2:RAG 问答

@Service
@RequiredArgsConstructor
public class RAGService {
    
    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    
    /**
     * RAG 问答
     */
    public String ask(String question) {
        // 1. 检索相关文档
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(3) // 取最相关的3条
        );
        
        // 2. 构造 Prompt
        String context = relevantDocs.stream()
            .map(Document::getContent)
            .collect(Collectors.joining("\n\n"));
        
        String prompt = """
            你是一个专业的客服助手,请基于以下文档内容回答用户问题。
            如果文档中没有相关信息,请明确告知用户。
            
            文档内容:
            %s
            
            用户问题:%s
            """.formatted(context, question);
        
        // 3. 调用 AI
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
}

步骤 3:Controller

@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RAGController {
    
    private final DocumentService documentService;
    private final RAGService ragService;
    
    // 上传文档
    @PostMapping("/document")
    public String uploadDocument(@RequestBody DocumentData data) {
        documentService.uploadDocument(data.getTitle(), data.getContent());
        return "文档上传成功";
    }
    
    // 问答
    @PostMapping("/ask")
    public String ask(@RequestBody Map<String, String> request) {
        return ragService.ask(request.get("question"));
    }
}

5.5 测试 RAG

# 1. 上传文档
curl -X POST http://localhost:8080/api/rag/document \
  -H "Content-Type: application/json" \
  -d '{
    "title": "退款政策",
    "content": "退款流程:1. 登录账号进入订单中心 2. 选择需要退款的订单 3. 点击申请退款 4. 填写退款原因 5. 等待审核(1-3个工作日)"
  }'

# 2. 提问
curl -X POST http://localhost:8080/api/rag/ask \
  -H "Content-Type: application/json" \
  -d '{"question": "怎么申请退款?"}'

# 响应:基于文档内容回答 ✅

六、架构设计(重点)

6.1 如何接入现有系统

场景 1:IM 聊天机器人

用户 → IM(微信/钉钉) → Webhook → Spring AI → 响应

实现

@RestController
@RequestMapping("/webhook")
@RequiredArgsConstructor
public class IMWebhookController {
    
    private final ChatService chatService;
    
    @PostMapping("/dingtalk")
    public void dingTalkWebhook(@RequestBody DingTalkMessage message) {
        String response = chatService.chat(message.getContent());
        // 调用钉钉 API 回复
        dingTalkService.reply(message.getConversationId(), response);
    }
}

场景 2:电商智能客服

用户 → 咨询按钮 → RAG(商品文档) → AI 回答

6.2 高并发架构设计

                    ┌─────────────┐
                    │   负载均衡   │
                    └──────┬──────┘
                           │
         ┌─────────────────┼─────────────────┐
         │                 │                 │
    ┌────▼────┐       ┌────▼────┐       ┌────▼────┐
    │ Spring  │       │ Spring  │       │ Spring  │
    │ AI 实例1│       │ AI 实例2 │       │ AI 实例3 │
    └────┬────┘       └────┬────┘       └────┬────┘
         │                 │                 │
         └─────────────────┼─────────────────┘
                           │
                    ┌──────▼──────┐
                    │  Redis 集群  │  ← 上下文 + 缓存
                    └─────────────┘
                           │
                    ┌──────▼──────┐
                    │ 向量数据库    │  ← RAG 检索
                    └─────────────┘
                           │
                    ┌──────▼──────┐
                    │  OpenAI API │
                    └─────────────┘

6.3 缓存策略

@Service
@RequiredArgsConstructor
public class CachedChatService {
    
    private final ChatService chatService;
    private final RedisTemplate<String, String> redisTemplate;
    
    @Cacheable(value = "chat", key = "#question.hashCode()")
    public String chat(String question) {
        return chatService.chat(question);
    }
    
    /**
     * 语义缓存(相似问题直接返回)
     */
    public String semanticCache(String question) {
        // 1. 计算问题的向量
        // 2. 在 Redis 中搜索相似问题
        // 3. 如果相似度 > 0.9,直接返回缓存答案
        // 4. 否则调用 AI
    }
}

6.4 降级与限流

@Configuration
public class ResilienceConfig {
    
    @Bean
    public ChatClient resilientChatClient(ChatClient.Builder builder) {
        return builder
            .defaultAdvisors(
                new RequestRateLimitAdvisor(100, Duration.ofMinutes(1)), // 限流
                new RetryChatAdvisor(3), // 重试
                new SimpleLoggerAdvisor() // 日志
            )
            .build();
    }
}

/**
* 降级策略
*/
@Service
@RequiredArgsConstructor
public class FallbackChatService {
    
    private final ChatService chatService;
    
    @CircuitBreaker(name = "ai", fallbackMethod = "fallbackChat")
    public String chatWithFallback(String message) {
        return chatService.chat(message);
    }
    
    public String fallbackChat(String message, Exception e) {
        return "抱歉,AI 服务暂时不可用,请稍后再试。";
    }
}

6.5 Token 成本控制

@Service
public class TokenControlService {
    
    private final ChatClient chatClient;
    private final TokenUsageTracker tokenTracker;
    
    /**
     * 限制单次 Token 消耗
     */
    public String chatWithTokenLimit(String message) {
        // 1. 预估 Token 数量
        int estimatedTokens = estimateTokens(message);
        
        // 2. 检查用户配额
        if (!tokenTracker.checkQuota("user-123", estimatedTokens)) {
            throw new QuotaExceededException("Token 配额不足");
        }
        
        // 3. 调用 AI
        ChatResponse response = chatClient.prompt()
            .user(message)
            .call()
            .chatResponse();
        
        // 4. 记录实际消耗
        tokenTracker.recordUsage("user-123", 
            response.getMetadata().getUsage().getTotalTokens());
        
        return response.getResult().getOutput().getContent();
    }
    
    /**
     * 自动截断超长 Prompt
     */
    public String truncateIfNeeded(String prompt, int maxTokens) {
        if (estimateTokens(prompt) <= maxTokens) {
            return prompt;
        }
        // 按比例截断
        int maxChars = maxTokens * 4; // 粗略估算 1 token ≈ 4 字符
        return prompt.substring(0, Math.min(prompt.length(), maxChars));
    }
}

七、常见坑总结(非常重要)

坑 1:Token 超限

现象

Error: This model's maximum context length is 4097 tokens

解决方案

// 方案 1:自动截断
String truncatedPrompt = truncateIfNeeded(prompt, 3000);

// 方案 2:使用摘要压缩
String summary = summarizeHistory(conversationHistory);
String newPrompt = summary + "\n" + currentQuestion;

// 方案 3:限制历史记录条数
.advisors(new MessageChatMemoryAdvisor(memory, sessionId, 5)) // 只保留5条

坑 2:AI 幻觉问题

现象:AI 编造不存在的答案

解决方案

// 方案 1:RAG(最有效)
String prompt = """
    请基于以下文档回答问题,不要编造内容。
    如果文档中没有答案,请直接说"我不知道"。
    
    文档:%s
    问题:%s
    """.formatted(context, question);

// 方案 2:降低 Temperature
ChatOptions options = ChatOptions.builder()
    .temperature(0.1) // 越低越稳定
    .build();

// 方案 3:要求 AI 引用来源
String prompt = "回答时请注明信息来源";

坑 3:响应慢

现象:用户等 10 秒才收到回复

解决方案

// 方案 1:流式响应(推荐)
Flux<String> stream = chatClient.prompt()
    .user(message)
    .stream()
    .content();

// 方案 2:异步 + WebSocket
@Async
public CompletableFuture<String> asyncChat(String message) {
    return CompletableFuture.completedFuture(chat(message));
}

// 方案 3:缓存常见问题
@Cacheable("common-questions")
public String getCommonAnswer(String question) {
    return chat(question);
}

坑 4:Prompt 不稳定

现象:同样的问题,有时回答好,有时回答差

解决方案

// 方案 1:使用 Prompt Template
String prompt = PromptTemplate.from("""
    你是一个{role},请{task}。
    
    用户问题:{question}
    """).create(Map.of(
    "role", "Java 技术专家",
    "task", "提供简洁准确的代码示例",
    "question", question
));

// 方案 2:Few-shot Learning
String prompt = """
    示例 1:
    输入:怎么排序?
    输出:可以使用 Arrays.sort() 方法...
    
    示例 2:
    输入:怎么连接数据库?
    输出:可以使用 JDBC 或连接池...
    
    现在请回答:
    输入:{question}
    输出:
    """;

// 方案 3:A/B 测试优化 Prompt

八、总结 + 面试加分点

8.1 核心知识点回顾

概念 作用 类比
Prompt 指导 AI 行为 SQL 语句
ChatClient 调用 AI 的入口 JdbcTemplate
Memory 记住对话历史 Session
Embedding 文本向量化 全文索引
RAG 基于知识库回答 开卷考试

8.2 面试如何讲 Spring AI?

面试官:“你用过 AI 技术吗?”

你的回答

"我在项目中使用 Spring AI 构建了智能客服系统。具体来说:

  1. 技术选型:选择 Spring AI 是因为它与 Spring 生态无缝集成,团队学习成本低。
  2. 核心实现
    • 使用 RAG 技术接入了公司知识库,准确率提升到 85%
    • 实现了多轮对话上下文管理(基于 Redis)
    • 做了流式响应优化,首字响应时间从 3s 降到 500ms
  3. 架构设计
    • 做了 Token 成本控制,月成本降低 40%
    • 实现了降级策略,AI 不可用时自动切换规则引擎
    • 接入了钉钉 Webhook,实现智能客服机器人"

8.3 项目亮点怎么说?

亮点 1:RAG 知识库

  • “通过 RAG 技术接入公司 500+ 文档,客服准确率提升 85%”

亮点 2:性能优化

  • “流式响应 + 语义缓存,首字响应时间从 3s 降到 500ms”

亮点 3:成本控制

  • “Token 智能配额管理,月成本降低 40%”

亮点 4:高可用

  • “实现降级策略,AI 不可用时自动切换规则引擎,可用性 99.9%”

8.4 学习路线图

第 1 周:基础
├── 搭建 Spring AI 项目
├── 实现简单对话接口
└── 理解 Prompt、Model 等核心概念

第 2 周:进阶
├── 实现多轮对话
├── 接入向量数据库
└── 实现 RAG 问答

第 3 周:实战
├── 对接 IM(钉钉/微信)
├── 做性能优化(流式响应)
└── 做成本控制

第 4 周:架构
├── 高并发设计
├── 降级与限流
└── 监控与告警

写在最后

Spring AI 让 Java 开发者能快速进入 AI 领域,不用从头学习 Python、PyTorch。

记住

  • AI 不是万能的,要结合业务场景
  • RAG 是解决幻觉问题的最佳方案
  • 成本控制很重要(Token 很贵!)
  • 降级策略是高可用的保障

行动起来

  1. 跟着文章搭建第一个 AI 接口
  2. 尝试接入公司知识库做 RAG
  3. 在简历上加上 “Spring AI” 技能

如果这篇文章对你有帮助,欢迎点赞、收藏、转发!

有问题欢迎评论区讨论,我看到会回复~


参考资源


⭐ 文章标签:Spring AI、Java、AI、LLM、RAG、架构设计

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐