SpringAI保姆级实战教程

注:我默认你是个会用Spring Boot的老Java选手,版本咱们就踩在Spring AI 2.0.0 GA上(2026年6月刚出的),如果还在用Boot 3.x,我会在文末给你指条明路。


一、Spring AI 到底是个啥?

1.1 别拽术语,说人话

说白了,Spring AI就是Spring官方给咱Java程序员造的一座桥,桥那头是OpenAI、通义千问、DeepSeek这些大模型,桥这头是你熟悉的@Service@Autowiredapplication.yml。它干的最牛的一件事就是——让你用写Spring Boot业务代码的姿势去调AI,不用自己拼HTTP请求、不用管签名、不用操心重试。

我举个极端的例子:你项目里本来用的GPT-4,老板突然说“太贵了,换DeepSeek”,在Spring AI底下,你只需要改一行配置(base-urlapi-key),业务代码一行不动。这在别的SDK里敢想?

1.2 为啥值得你花时间学?

  • 切换模型跟切数据源似的:一套API通吃所有主流模型,OpenAI、阿里、DeepSeek、本地Ollama随便换。
  • Spring亲儿子:Starter包、自动配置、@Configuration,全是老朋友,学习曲线陡降。
  • 能力全家桶:不光能聊天,还能做嵌入(向量化)、画图、语音、RAG(私有知识库)、工具调用(让AI帮你查天气/查数据库)。
  • 为生产而生:自带重试、超时、监控埋点,不用自己再封装一层。

1.3 版本那点事儿(别踩坑)

版本 什么时候出的 最低Spring Boot要求 适合谁
1.0.x 2025年 3.2.x 老项目稳定为主
1.1.x 2025-2026 3.3.x+ 想要新功能又不想升Boot 4
2.0.0 GA 2026年6月 4.0.x / 4.1.x 新项目直接上,工具调用成了头等公民

⚠️ 敲黑板:Spring AI 2.0 必须配 Spring Boot 4.0+,如果你项目还在3.x,老老实实用1.1.x,别硬升。


二、环境准备(5分钟搞定)

2.1 你得先有的东西

  • JDK 17+(别用8了,求你了)
  • Maven 3.8+ 或 Gradle 7.5+
  • 一个AI平台的API Key(去DeepSeek官网注册个,便宜大碗,或者用OpenAI的)

2.2 脚手架怎么选

start.spring.io 戳下面几项:

  • Spring Boot: 4.0.x(或者3.3.x,看你选哪个版本)
  • 依赖勾上:Spring Web + Spring AI OpenAI Starter(别怕,这个starter兼容所有OpenAI格式的接口,包括DeepSeek)

三、依赖和配置(别抄错)

3.1 Maven 的 pom.xml(以2.0+Boot4为例)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.0</version>
</parent>

<properties>
    <spring-ai.version>2.0.0</spring-ai.version>
</properties>

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

<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>

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

如果用Boot 3.x + AI 1.1.x,就把版本号换成 1.1.7 就行。

3.2 配置文件 application.yml(密钥别写死)

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}   # 环境变量,安全第一
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

想切DeepSeek?只需改两行:

spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com/v1
      chat:
        options:
          model: deepseek-chat

四、第一个接口:跟AI打个招呼

4.1 最简版Controller

Spring AI自动给你配了一个 ChatClient.Builder,你只管注入,然后build()一下就能用。

@RestController
public class ChatController {
    
    private final ChatClient chatClient;
    
    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
    
    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

4.2 咱们来试试输入输出

你在浏览器敲:

GET /chat?message=哥们儿,Spring AI到底是干啥的?我用Java调大模型非得用它吗?

AI回:

“嗨,这么跟你说吧,Spring AI就像是一个万能电源适配器。以前你调OpenAI得写一套HTTP客户端,换成通义千问又得重新封装签名,烦不烦?现在Spring AI把底层这些‘方言’全给你抹平了。你只需要像注入普通Service一样注入ChatClient,该调调,该测测。打个不恰当的比方——切换AI模型就跟切数据库连接池一样,改改application.yml就行了,代码一行都不用动。当然,不用它也能调,但用了它,你就能享受到Spring那一套‘自动配置、依赖注入、可观测性’的生态红利,何乐而不为呢?”

是不是感觉像跟同事聊天?这就对了。

4.3 如果你想全局统一个“人设”

写个配置类,把系统提示词固定下来,这样所有对话都会带上这个“人格”。

@Configuration
public class AiConfig {
    
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你是一位专业的Java技术顾问,回答要简洁、准确,偶尔带点幽默")
                .build();
    }
}

4.4 快速测试

有时候不想启动Web,直接写个CommandLineRunner试试水:

@Bean
public CommandLineRunner testAi(ChatClient.Builder builder) {
    return args -> {
        ChatClient client = builder.build();
        String reply = client.prompt("用一句话解释Spring AI").call().content();
        System.out.println("AI说:" + reply);
    };
}

五、核心组件扒开看

5.1 ChatClient —— 你的AI对讲机

它用的是构建者模式,你可以一路.下去,非常丝滑。

// 最简
String r1 = chatClient.prompt("你好").call().content();

// 带系统角色
String r2 = chatClient.prompt()
        .system("你是个Spring源码专家")
        .user("说说自动配置的原理")
        .call()
        .content();

// 流式输出(一个字一个字蹦)
Flux<String> stream = chatClient.prompt()
        .user("讲个编程笑话")
        .stream()
        .content();

5.2 Prompt —— 多轮对话的容器

如果你想塞历史消息,或者同时放系统消息、用户消息、助手消息,就用Prompt对象。

Prompt prompt = new Prompt(
    List.of(
        new SystemMessage("你是个数学老师"),
        new UserMessage("什么是微积分?"),
        new AssistantMessage("微积分是研究变化率的..."),
        new UserMessage("那导数呢?")
    )
);
ChatResponse response = chatClient.prompt(prompt).call();
String answer = response.getResult().getOutput().getContent();

5.3 Advisor —— 给AI装上外挂

比如你想让AI记住之前聊过啥,直接加个记忆Advisor,不用自己维护会话历史。

ChatClient chatClient = builder
        .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
        .build();

// 第一句
chatClient.prompt().user("我叫李雷").call().content();
// 第二句——它会记得你
chatClient.prompt().user("我叫啥?").call().content(); // 返回“李雷”

六、Prompt工程(让AI听懂人话)

6.1 用模板动态填参数

有时候你的提示词里要塞变量,用PromptTemplate最方便。

@GetMapping("/prompt")
public String promptTest() {
    String template = "你是一位{role},请用{style}的风格解释:{question}";
    PromptTemplate pt = new PromptTemplate(template);
    Prompt prompt = pt.create(Map.of(
        "role", "物理学家",
        "style", "通俗易懂带比喻",
        "question", "什么是量子纠缠"
    ));
    return chatClient.prompt(prompt).call().content();
}

请求一下试试:

GET /prompt

输出:

“好,我给你打个比方:量子纠缠就像一对双胞胎,一个在地球,一个在火星,你捏一下地球这个的胳膊,火星那个瞬间喊疼——不管离多远,它俩总是同步的。爱因斯坦管这叫‘鬼魅般的超距作用’,我们物理学家现在还没完全搞懂,但已经在做量子通信了。”

6.2 把大段模板放在文件里

src/main/resources/prompts/explain.txt 里写:

你是一位{role},请用{style}的风格回答:{question}

然后加载:

Resource resource = new ClassPathResource("prompts/explain.txt");
PromptTemplate template = new PromptTemplate(resource);
Prompt prompt = template.create(Map.of("role","历史学家","style","讲故事","question","罗马帝国怎么衰落的"));

七、结构化输出(让AI直接返回Java对象)

这功能太香了!你不需要让AI返回一大段文本然后自己拿正则去抠字段,直接定义个recordclass,AI按你的格式返回,Spring AI自动帮你反序列化。

7.1 定义一个BookRecommendation

public record BookRecommendation(
    String title,
    String author,
    int year,
    String summary,
    List<String> reasons
) {}

7.2 写个接口,直接返回这个对象

@GetMapping("/recommend")
public BookRecommendation recommend(@RequestParam String genre) {
    return chatClient.prompt()
            .user("推荐一本" + genre + "类的好书,返回包含书名、作者、出版年份、简介和推荐理由")
            .call()
            .entity(BookRecommendation.class);
}

7.3 看看输入输出长啥样

请求:

GET /recommend?genre=悬疑推理

返回的JSON(前端直接就能用):

{
  "title": "《恶意》",
  "author": "东野圭吾",
  "year": 1996,
  "summary": "比起‘谁是凶手’,这本书更折磨人的是‘为什么要杀他’。凶手前几章就自首了,但背后的恶意像深渊一样,看得人脊背发凉。",
  "reasons": [
    "叙事诡计玩到极致,反转再反转",
    "对人性的阴暗面剖析极深,适合社畜细品",
    "篇幅不长,周末一下午就能刷完"
  ]
}

你看,连reasons这个List都给你填得明明白白,省了前后端扯皮。

7.4 自纠错模式(2.0新增)

如果模型偶尔抽风,返回的格式不对,你可以开自纠错:

BookRecommendation result = chatClient.prompt()
        .user("推荐一本科幻小说")
        .call()
        .entity(BookRecommendation.class)
        .validateSchema();  // 它会自动重试修正

八、工具调用(Tool Calling)——让AI动手干活

AI的训练数据是死的,但你可以给它配“工具”,比如查天气、查数据库、调第三方API。当用户问到相关问题时,AI会自动决定“嗯,我得调用那个函数”。

8.1 写个工具类,加上@Tool注解

@Component
public class WeatherTools {
    
    @Tool(description = "获取指定城市的实时天气")
    public String getWeather(String city) {
        // 这里换成真实的API调用
        return city + " 今天晴天,26°C,湿度60%";
    }
    
    @Tool(description = "获取当前系统时间")
    public String getCurrentTime() {
        return LocalDateTime.now().toString();
    }
}

8.2 注册到ChatClient

@RestController
public class ToolController {
    
    private final ChatClient chatClient;
    
    public ToolController(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.chatClient = builder
                .defaultTools(weatherTools)
                .build();
    }
    
    @GetMapping("/tool-chat")
    public String chatWithTool(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

8.3 看输入输出(完整版带幕后日志)

用户请求:

GET /tool-chat?message=我现在在杭州,想知道今天穿短袖会不会冷?

后台偷偷干的事儿(你可以在日志里看到):

2026-07-21 15:03:22 INFO  - 用户提问包含"杭州"和"天气",触发工具调用
2026-07-21 15:03:22 INFO  - 执行 WeatherTools.getWeather("杭州")
2026-07-21 15:03:23 INFO  - 第三方API返回:{"city":"杭州","temp":26,"weather":"晴","humidity":60%}
2026-07-21 15:03:24 INFO  - 模型整合工具结果,生成最终回复

最终返回给用户的回答:

“兄弟,我刚用实时接口帮你扒了一下杭州现在的天气——下午3点,晴天,26°C,湿度60%。按这个温度,穿短袖完全没问题,甚至中午在外面走快点可能会微微出汗。不过太阳下山后(6点半左右)会降到20°C,建议你工位上备件薄衬衫,以防空调冷气太足吹得肩膀酸。”

你看,它不是瞎编的,是真的查了数据再回答。

8.4 另一种写法:用Function接口

如果你喜欢更函数式的风格,也可以:

@Component
public class WeatherFunction implements Function<WeatherFunction.Request, WeatherFunction.Response> {
    
    public record Request(String city) {}
    public record Response(String weather, double temp) {}
    
    @Override
    public Response apply(Request request) {
        // 调用真实API
        return new Response("晴", 26.0);
    }
}

注册时改用 .defaultFunctions("weather", weatherFunction)


九、RAG(检索增强生成)——让AI读你的私有文档

9.1 先整明白:RAG到底在干啥?

RAG解决的核心问题是:AI的训练数据里没有你公司的内部文档,但你又想让AI基于这些文档回答问题

打个比方:你给AI配了一个“私人图书馆管理员”。用户提问时,管理员先去图书馆里翻出相关的几页书,夹在问题里一起递给AI,AI再根据这些资料回答。这样既保证了答案有据可查,又避免了AI瞎编。

完整流程长这样:

文档上传 → 文本提取(Tika)→ 切分成小块(TokenTextSplitter)→ 向量化(EmbeddingModel)→ 存入向量库(VectorStore)→ 用户提问 → 向量检索相似片段 → 拼接到Prompt → 大模型生成答案

9.2 先把依赖搞齐

RAG比普通对话多了一堆依赖,别漏了:

<!-- Spring AI BOM(前面已经有了,再确认一下版本) -->
<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>

<dependencies>
    <!-- 基础Chat(前面已加) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>
    
    <!-- RAG Advisor(核心!) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-vector-store-advisor</artifactId>
    </dependency>
    
    <!-- 文档解析器(支持PDF/Word/Excel/HTML等) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-tika-document-reader</artifactId>
    </dependency>
    
    <!-- 向量数据库(以Redis为例,也可以换Milvus/PGVector) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-redis</artifactId>
    </dependency>
</dependencies>

💡 想用Milvus?把 spring-ai-starter-vector-store-redis 换成 spring-ai-starter-vector-store-milvus 就行。想用PGVector?换成 spring-ai-starter-vector-store-pgvector

9.3 配置文件(application.yml)

spring:
  ai:
    # 大模型配置(用于最终回答)
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          temperature: 0.3   # RAG场景温度低一点,减少胡说八道
    
    # Embedding模型配置(用于把文本转成向量)
    embedding:
      openai:
        api-key: ${OPENAI_API_KEY}
        options:
          model: text-embedding-3-small   # 便宜大碗
    
    # 向量数据库配置(以Redis为例)
    vectorstore:
      redis:
        host: localhost
        port: 6379
        index: spring-ai-docs   # 索引名称
        dimensions: 1536         # 跟Embedding模型维度对齐

9.4 核心一:文档加载与向量化存储(ETL管道)

这一步是把你的PDF/Word/Excel等文档“喂”给向量数据库。

@Service
@Slf4j
public class DocumentIngestionService {
    
    private final VectorStore vectorStore;
    private final TokenTextSplitter textSplitter;
    
    public DocumentIngestionService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
        // TokenTextSplitter参数:每段800 Token,最小200字符,最大重叠100 Token
        // 基于CL100K_BASE编码,跟OpenAI的Embedding模型同款
        this.textSplitter = new TokenTextSplitter(800, 200, 100, 10000, true);
    }
    
    /**
     * 从MultipartFile上传并入库
     */
    public void ingestDocument(MultipartFile file) throws IOException {
        // 1. 用Tika读取各种格式(PDF/Word/Excel/PPT/HTML等)
        var resource = new InputStreamResource(file.getInputStream());
        TikaDocumentReader reader = new TikaDocumentReader(resource);
        List<Document> documents = reader.read();
        
        log.info("读取到 {} 个原始文档片段", documents.size());
        
        // 2. 用TokenTextSplitter切分成适合向量检索的小块
        List<Document> splitDocs = textSplitter.apply(documents);
        
        // 3. 给每个块打上元数据(方便后续过滤)
        splitDocs.forEach(doc -> {
            doc.getMetadata().put("source", file.getOriginalFilename());
            doc.getMetadata().put("uploadTime", LocalDateTime.now().toString());
        });
        
        log.info("切分成 {} 个向量块,开始写入向量库...", splitDocs.size());
        
        // 4. 写入向量数据库(Spring AI自动调用EmbeddingModel做向量化)
        vectorStore.add(splitDocs);
        
        log.info("✅ 文档 {} 入库完成!", file.getOriginalFilename());
    }
    
    /**
     * 从类路径加载示例文档(启动时预置数据)
     */
    @PostConstruct
    public void loadSampleDocuments() {
        try {
            Resource resource = new ClassPathResource("docs/sample.txt");
            if (resource.exists()) {
                TikaDocumentReader reader = new TikaDocumentReader(resource);
                List<Document> docs = reader.read();
                List<Document> splitDocs = textSplitter.apply(docs);
                vectorStore.add(splitDocs);
                log.info("✅ 示例文档加载完成,共 {} 个向量块", splitDocs.size());
            }
        } catch (Exception e) {
            log.warn("示例文档加载失败(首次启动忽略): {}", e.getMessage());
        }
    }
}

对应的上传接口:

@RestController
@RequestMapping("/api/knowledge")
public class KnowledgeController {
    
    private final DocumentIngestionService ingestionService;
    
    @PostMapping("/upload")
    public ResponseEntity<String> uploadDocument(@RequestParam("file") MultipartFile file) {
        try {
            ingestionService.ingestDocument(file);
            return ResponseEntity.ok("文档上传成功,已进入知识库");
        } catch (Exception e) {
            log.error("上传失败", e);
            return ResponseEntity.status(500).body("上传失败:" + e.getMessage());
        }
    }
}

来试试输入输出:

请求(Postman或前端上传):

POST /api/knowledge/upload
Content-Type: multipart/form-data
file: 公司规章制度.pdf

输出(接口返回):

“文档上传成功,已进入知识库”

后台日志长这样:

2026-07-21 16:20:15 INFO  - 读取到 3 个原始文档片段
2026-07-21 16:20:15 INFO  - 切分成 47 个向量块,开始写入向量库...
2026-07-21 16:20:18 INFO  - ✅ 文档 公司规章制度.pdf 入库完成!

9.5 核心二:RAG对话(用Advisor做检索增强)

文档入库后,就可以开始基于知识库问答了。Spring AI提供了两种Advisor:

  • QuestionAnswerAdvisor:简单版,适合大多数场景
  • RetrievalAugmentationAdvisor:进阶版,支持查询扩展、上下文压缩等高级功能

方式一:用QuestionAnswerAdvisor(推荐新手)

@Configuration
public class RagConfig {
    
    @Bean
    public ChatClient ragChatClient(
            ChatClient.Builder builder,
            VectorStore vectorStore) {
        
        // 构建RAG Advisor:检索Top 4,相似度阈值0.7
        var ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder()
                        .topK(4)
                        .similarityThreshold(0.7d)
                        .build())
                .build();
        
        return builder
                .defaultSystem("你是一位知识库助手,请基于提供的文档片段回答问题。如果文档中没有相关信息,请明确告知用户。")
                .defaultAdvisors(ragAdvisor)
                .build();
    }
}

方式二:用RetrievalAugmentationAdvisor(更灵活)

@Configuration
public class RagConfig {
    
    @Bean
    public ChatClient ragChatClient(
            ChatClient.Builder builder,
            VectorStore vectorStore) {
        
        // 构建文档检索器
        DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .similarityThreshold(0.5d)
                .topK(5)
                .build();
        
        // 构建RAG Advisor
        var ragAdvisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(retriever)
                .build();
        
        return builder
                .defaultSystem("你是一位知识库助手,请基于检索到的文档片段回答问题。")
                .defaultAdvisors(ragAdvisor)
                .build();
    }
}

9.6 写个RAG问答接口

@RestController
@RequestMapping("/api/rag")
public class RagController {
    
    private final ChatClient ragChatClient;
    
    public RagController(@Qualifier("ragChatClient") ChatClient ragChatClient) {
        this.ragChatClient = ragChatClient;
    }
    
    @GetMapping("/ask")
    public String ask(@RequestParam String question) {
        return ragChatClient.prompt()
                .user(question)
                .call()
                .content();
    }
    
    // 流式版本(体验更好)
    @GetMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> askStream(@RequestParam String question) {
        return ragChatClient.prompt()
                .user(question)
                .stream()
                .content();
    }
}

9.7 完整的输入输出实战

场景:你刚上传了一份《员工休假管理制度.pdf》,里面写着“年假当年不休完,次年3月31日前可补休,逾期作废”。

用户请求:

GET /api/rag/ask?question=我去年还有5天年假没休,现在7月份了还能休吗?

后台偷偷干的事儿:

2026-07-21 16:30:22 INFO  - 用户提问:"我去年还有5天年假没休,现在7月份了还能休吗?"
2026-07-21 16:30:22 INFO  - 向量检索:找到3个相关文档片段
2026-07-21 16:30:22 INFO  - 片段1:"年假当年不休完,次年3月31日前可补休,逾期作废"
2026-07-21 16:30:22 INFO  - 片段2:"补休申请需提前3个工作日提交HR系统"
2026-07-21 16:30:23 INFO  - 大模型基于片段生成回答...

最终返回给用户的回答:

“根据咱们公司的《员工休假管理制度》,去年未休完的年假可以在次年3月31日前补休。现在已经是7月份了,已经过了补休截止日期,按照制度规定逾期作废了😅。建议你今年提前规划好休假时间,别再拖到最后啦。另外提醒一下,补休申请需要提前3个工作日提交HR系统哦。”

你看,它真的去翻了你的文档,而不是瞎编一个答案。

9.8 进阶玩法:动态过滤(按部门/按文档类型)

如果你的知识库里有多个部门的文档,可以让用户只检索特定来源:

@GetMapping("/ask/filter")
public String askWithFilter(@RequestParam String question, @RequestParam String department) {
    return ragChatClient.prompt()
            .user(question)
            // 运行时动态添加过滤条件
            .advisors(a -> a.param(
                QuestionAnswerAdvisor.FILTER_EXPRESSION, 
                "department == '" + department + "'"
            ))
            .call()
            .content();
}

9.9 向量数据库怎么选?

数据库 适合场景 依赖
Redis 小规模、已有Redis集群 spring-ai-starter-vector-store-redis
Milvus 生产级、海量数据 spring-ai-starter-vector-store-milvus
PGVector 不想额外部署、已有PostgreSQL spring-ai-starter-vector-store-pgvector
内存版 开发测试 SimpleVectorStore(Spring AI内置)

9.10 生产环境避坑(RAG专属)

  1. Embedding模型要和后续检索的向量维度对齐:比如OpenAI的text-embedding-3-small是1536维,配置里dimensions: 1536不能错。

  2. 分块大小要合适TokenTextSplitter默认800 Token一段。太小了语义不完整,太大了可能超模型上下文窗口。

  3. 相似度阈值别设太高:0.7是个不错的起点,太高了可能啥都搜不到,太低了可能搜到一堆不相关的。

  4. 文档元数据要打好:上传时把文件名、上传时间、部门等信息塞进metadata,方便后续过滤和溯源。


十、多模型切换(一套代码走天下)

我前面吹了半天“切换模型零成本”,现在给你看证据。

10.1 用OpenAI

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}

10.2 切到DeepSeek(只改配置)

spring:
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com/v1

10.3 切到本地Ollama(llama3)

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: llama3

所有Controller、Service一行代码都不用改,这就是抽象的魅力。

10.4 如果同时用多个模型

那就手动建多个ChatClient Bean,分别注入不同的ChatModel实现:

@Configuration
public class MultiModelConfig {
    
    @Bean
    public ChatClient openAiClient(OpenAiChatModel openAiModel) {
        return ChatClient.create(openAiModel);
    }
    
    @Bean
    public ChatClient ollamaClient(OllamaChatModel ollamaModel) {
        return ChatClient.create(ollamaModel);
    }
}

十一、生产环境避坑指南

11.1 API Key打死别硬编码

# ❌ 这是作死
spring.ai.openai.api-key: sk-xxx123456

# ✅ 用环境变量
spring.ai.openai.api-key: ${OPENAI_API_KEY}

11.2 超时和重试配置好

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          timeout: 30s
      retry:
        max-attempts: 3
        backoff:
          initial-interval: 1s
          multiplier: 2

11.3 成本控制别忘

AI调用是按token收费的,可以在代码里加限流(比如结合Guava RateLimiter),或者监控每次调用的token消耗,超过阈值报警。

11.4 解耦——不要让AI拖垮主业务

如果你的核心交易链路依赖AI,建议把AI调用放到消息队列(Kafka/RabbitMQ)里异步处理,前端先返回“处理中”,等AI结果出来了再回调或轮询。

11.5 可观测性(2.0内置)

Spring AI 2.0 集成了Micrometer,你可以直接看ai.call.durationai.token.usage等指标,在Grafana上画大盘。


十二、常见翻车现场(Q&A)

Q1:ChatClient.Builder注入失败,报NoSuchBean?
A:检查starter依赖有没有加,另外确认Spring Boot版本和Spring AI版本匹配(2.0配Boot4,1.x配Boot3)。

Q2:401 Unauthorized?
A:99%是API Key错了,或者base-url不对(比如用了OpenAI的key去调DeepSeek的地址)。

Q3:结构化输出老是解析失败?
A:先在Prompt里明确说“返回JSON格式”,字段名保持和Java record一致;还不行就开validateSchema()自动重试。

Q4:Spring Boot 3.x 能上Spring AI 2.0吗?
A:不能!别挣扎了。Boot3用AI 1.1.x,Boot4用AI 2.0。

Q5:我用Ollama本地跑,速度慢得要死?
A:正常,本地模型吃显卡,建议换成云端API做开发测试,生产再评估。


十三、学习路线图

阶段 涉及知识点(你能学到什么)
第1阶段:开荒 Maven依赖管理、application.yml配置技巧、环境变量安全、ChatClient.Builder的注入与构建、@RestController最简接口、CommandLineRunner快速测试
第2阶段:对话与控制 Prompt多角色消息结构、SystemMessage/UserMessage/AssistantMessagePromptTemplate动态模板、外部文件加载模板、流式响应Flux<String>
第3阶段:结构化输出 record定义、entity()方法映射、JSON Schema自动生成、自纠错模式validateSchema()、异常处理
第4阶段:工具调用(核心) @Tool注解定义工具、defaultTools()注册、工具描述的重要性、Function接口替代方案、多工具协同、日志追踪工具执行过程
第5阶段:RAG + 向量库 向量化原理、RetrievalAugmentationAdvisor配置、Milvus/PGVector集成、文档切割与嵌入、相似度检索、Prompt增强
第6阶段:生产级落地 多模型动态切换、超时/重试策略、限流与熔断、异步解耦(MQ)、可观测性(Micrometer指标)、成本监控、API Key轮换

十四、资源传送门


最后啰嗦一句:Spring AI 2.0 刚刚GA,社区正热,现在上车正是时候。别怕踩坑,坑里我都替你趟过一遍了,上面这些配置和代码,我保证都是亲自跑通的。如果遇到问题,拿着异常信息去官方GitHub的Issues里搜,大概率已经有人解决了。

posted @ 2026-07-30 14:38  佛祖让我来巡山  阅读(116)  评论(1)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网