
整理这份 Spring-AI-Alibaba 学习笔记的时候我的项目正好卡在一个很尴尬的阶段业务方要求快速接入大模型能力做智能问答但团队里每个人对接模型的方式都不一样有人直接用 HTTP 调用通义千问有人在业务代码里裸写 RestTemplate 拼 prompt还有人为了一个流式输出把 WebFlux 的 API 啃了三天。说实话功能都能跑但代码风格五花八门换个人接手就得重新读一遍。后来我在调研时发现了 Spring-AI-Alibaba才意识到之前的问题不是模型难调而是缺一层统一封装。这个东西简单说就是一套基于 Spring AI 的大模型开发框架它对标的是 Spring 官方那套 AI 抽象但做了面向阿里云通义千问的一系列适配和增强。它能帮你把“调用大模型”这件事变成普通的 Spring 方法调用省掉大量重复的 HTTP 对接、JSON 解析、上下文管理、记忆处理等工作。这篇笔记我会从项目定位、环境准备、核心 API、多轮对话、知识库检索、踩坑实录这几个角度展开适合刚接触 Spring AI 生态、想在 Spring Boot 项目里快速集成大模型的开发者参考。如果你已经玩过 OpenAI 的 SDK或者用过 Spring AI 官方库看这篇笔记会更快上手因为很多概念是相通的区别主要在于模型适配和配置细节。1. 项目概述与核心定位1.1 为什么需要 Spring-AI-Alibaba在 Spring-AI-Alibaba 出现之前Java 生态里调用大模型基本靠两种方式第一种是直接用各家模型厂商提供的 SDK比如你选了通义千问就引入 DashScope 的 Java SDK想要支持多个厂商就得同时维护好几套依赖第二种是自己用 HTTP 封装把 prompt、参数、鉴权全部写死在代码里简单场景可行但一涉及流式响应、多轮记忆、向量检索这些能力代码量会迅速膨胀而且容易出错。这套框架解决的核心问题是把模型调用抽象成一套统一的接口让上层业务代码不用关心底层到底接的是哪家模型。你可以把它理解成 JDBC 在数据库领域的角色程序员面对的是统一的 Connection、Statement 接口至于底层连的是 MySQL 还是 PostgreSQL切换成本被降到了最低。Spring-AI-Alibaba 做的事情类似ChatClient、ChatModel、EmbeddingModel 这些抽象帮你屏蔽了不同模型厂商 API 的差异而 DashScope 的实现只是其中一个适配器。对我来说它还有个更实际的价值因为是基于 Spring AI 构建的所以天然继承了 Spring 生态的自动配置、依赖注入、统一配置管理等能力。你不用在业务代码里手动 new 一个客户端再到处传递这个客户端实例只要在配置文件里填好 api-key 和模型名Spring 容器会自动把所有组件装配好。1.2 它和 Spring AI 官方项目的关系这里要稍微展开一下因为很多人会搞混。Spring AI 是 Spring 官方推出的 AI 应用开发框架定位是给 JVM 生态提供一个统一的 AI 编程模型类似于 Spring Data 对数据库访问的抽象。但 Spring 官方负责的是抽象层和一部分基础实现具体到某个模型厂商、某个云平台的能力就需要各个厂商自己去适配。Spring-AI-Alibaba 就是阿里云在这个生态里的落地实现。它基于 Spring AI 的抽象层补充了 DashScope 的模型接入、阿里云的通义千问模型适配、本地向量库集成等能力。所以你在项目里引入 spring-ai-alibaba-starter 的时候实际上会同时拉入 Spring AI 的核心依赖和阿里云的适配实现。这也意味着如果你哪天想换成别的模型理论上只需要换掉 starter业务代码基本不用动前提是你用的是 Spring AI 的标准 API。这种设计思路对项目长期演进比较友好。短期看你只需要对接通义千问长期看如果公司要接入私有化部署的模型或者切换成其他云厂商的大模型你的业务代码不需要推翻重来。2. 快速上手前的准备工作2.1 环境要求与依赖引入先说你本地需要准备的东西。JDK 版本建议 17 以上Spring Boot 用 3.x因为 Spring AI 官方这个版本线是绑定 Spring Boot 3 的。如果你还在用 Spring Boot 2.x会很痛苦建议直接升级。Maven 或 Gradle 都可以我这边用 Maven 做示例。依赖本身很简单核心只需要一个dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M3.1/version /dependency注意这里有个关键点spring-ai-alibaba 的版本目前迭代比较快我写这篇笔记时用的是 1.0.0-M3.1但等你看到文章时可能已经有更新的版本甚至仓库地址都可能发生变化。所以强烈建议动手前先去 GitHub 搜一下 spring-ai-alibaba 官方仓库确认最新的版本号和 groupId、artifactId 有没有变动。版本问题是我踩过最无语的坑之一后面会详细说。如果你用的是 Maven还可能需要配置阿里云的仓库地址因为部分依赖在中央仓库可能同步不及时。这个看你的网络环境能拉下来就不用管。2.2 申请并配置 DashScope API Key写代码之前你得先去阿里云百炼控制台开通模型服务拿到 API Key。这个比较简单登录阿里云账号找到百炼平台开通后创建一个 API Key 就行。有一点要注意API Key 的权限范围尽量按最小化原则设置如果你只是本地开发测试就别开生产环境的全量权限避免泄露后影响线上业务。拿到 Key 之后在 Spring Boot 的 application.yml 里配置spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus把 api-key 直接写在配置文件里虽然方便但如果你要把代码提交到 Git 仓库建议用环境变量或者配置中心的方式管理像我上面那样用${DASHSCOPE_API_KEY}占位符就是比较常见的做法。另外不同版本的 spring-ai-alibaba 对配置前缀可能不一样有的版本用spring.ai.dashscope.*有的版本可能改成spring.ai.alibaba.dashscope.*。如果你发现注入的 ChatModel 没有正确读到配置先检查这一块。2.3 从“你好模型”开始的第一个示例配置好之后我们先写一个最基础的聊天接口验证整个链路是否通了。这一步的意义不是炫技而是确认你的依赖版本、配置项、网络环境都没问题。我先给你看一个最简单的 ControllerRestController 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(message).call().content(); } }这里最关键的是ChatClient。你可以把它理解成一个加强版的 RestTemplate专门用来跟大模型交互。ChatClient.Builder是自动注入的Spring 容器会通过自动配置帮你组装好。prompt(message)表示构造一个用户消息call()是同步等结果返回.content()是拿到模型返回的文本内容。启动项目后访问http://localhost:8080/chat?message你好如果一切正常你会收到一段正常的文本回复。如果报错大概率是 API Key 不对、网络不通或者模型名称不正确。这里要提醒一个细节ChatClient本身是有状态的你可以在构建时给它设置默认的系统提示词、默认参数、甚至挂载一些增强器Advisor这些在后面的多轮对话和知识库检索中会用得很频繁。所以实际项目中我不建议你到处builder.build()而是把配置好的 ChatClient 做成一个 Bean 注入使用。3. 核心 API 与关键功能拆解3.1 ChatClient 的三种调用姿势前面那个示例用的是最简单的同步调用。但实际上用户在网页聊天时更常见的是流式输出——模型一边生成前端一边打字体验更好。Spring-AI-Alibaba 的ChatClient提供了一套很大统一的调用风格我总结了一下高频使用的有三个第一种是同步调用前面已经演示过chatClient.prompt(...).call().content()。适合后端服务之间同步调用比如你封装一个接口给其他系统同步获取回答。第二种是流式调用返回类型是Flux适合聊天类前端页面。下面是示例GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String message) { return chatClient.prompt(message).stream().content(); }前端用 EventSource 或者 fetch 流式接口就能逐步拿到内容不用等全部生成完体感上确实比同步转圈好很多。第三种是携带系统提示词的调用。比如你要让模型扮演一个客服可以先通过系统消息固定它的角色再传入用户问题String response chatClient.prompt() .system(你是一个耐心的客服助手请用简洁友善的语气回答用户问题。) .user(message) .call() .content();这三种方式基本覆盖了绝大多数场景。需要注意的是如果你在构建 ChatClient 的时候已经通过defaultSystem()设置了系统提示词那么每次调用时再叠加.system()后者会覆盖前者而不是拼接。想叠加多个系统消息请使用system()的多个参数或自己拼接字符串。3.2 Prompt Template告别痛苦的字符串拼接很多刚接触大模型开发的程序员写 prompt 的时候习惯用字符串拼接比如String prompt 你是 role 请用 language 回答 question;这在参数少的时候还能忍一旦参数多了比如要动态构造一个包含用户信息、历史订单、当前商品的复杂 prompt字符串拼接会变得非常丑陋且很容易漏掉空格、引号导致语义偏差。更好的方式是使用 Prompt Template。Spring AI 的模板语法跟 Thymeleaf、FreeMarker 这类模板引擎非常像用{}占位符。看一个直观的例子PromptTemplate promptTemplate new PromptTemplate( 你是{role}请使用{language}回答以下问题{question} ); Prompt prompt promptTemplate.create(Map.of( role, 资深Java开发, language, 中文, question, 请介绍一下Spring AI ));有了模板之后你可以把 prompt 模板单独维护在资源文件里例如prompts/chat.st文件用new PromptTemplate(ClassPathResource(prompts/chat.st))加载。这样业务代码和提示词分离后续调整 prompt 文案时不需要重新编译 Java 代码上线成本低很多。我实际经验里模板的价值不只是省事更在于它能有效约束 prompt 的结构。团队协作时模板文件可以走评审业务人员也能看懂比埋在 Java 字符串里清晰得多。3.3 结构化输出让模型返回对象而不是散文第三个高频能力是结构化输出。很多业务场景下你并不想让模型输出一大段散文而是希望它返回固定的 JSON 结构比如{ name: 张三, age: 28, city: 杭州, hobbies: [编程, 摄影] }如果你自己去调模型 API你得在 prompt 里花大量篇幅强调“请只返回 JSON不要返回其他内容”然后拿到结果后再用 Jackson 或者 Hutool 解析。更烦的是模型偶尔会秀逗多给你输出一句“好的这是一个 JSON”你的解析器就直接崩了。Spring-AI-Alibaba 里可以用BeanOutputConverter或直接在ChatClient的调用链上指定返回类型。我推荐用后者代码干净得多。先定义一个 POJOpublic record PersonInfo(String name, Integer age, String city, ListString hobbies) { }然后这样调用PersonInfo person chatClient.prompt() .user(从这段文本中提取人物信息张三今年28岁住在杭州平时喜欢编程和摄影。) .call() .entity(PersonInfo.class);底层会自动帮你构造一个“请以 JSON 形式返回”的 prompt 约束并且把模型输出解析成你指定的类型。如果模型返回的不是合法 JSON框架会自动做重试或者抛出异常省去了你在业务代码里做防御式 JSON 解析的麻烦。这里有个小技巧如果你的字段比较复杂比如嵌套对象建议给 record 或者字段加上中文注释或描述。你可以在 record 的内部加静态方法描述字段含义框架会把这些描述拼进 prompt从而显著提高提取准确率。比如public record PersonInfo( JsonDescription(人物姓名) String name, JsonDescription(人物年龄) Integer age ) { }这个注解不是必须的但字段语义模糊时加上它效果会好很多。4. 记住上下文多轮对话的实现方案4.1 给 ChatClient 装一个“记忆”用过大模型的人都知道模型本身是没有记忆的。每次调用都是独立事件上一次对话说了什么模型完全不知道。你问一句“我叫张三”下一轮问“我叫什么”它大概率会给你一个礼貌但尴尬的回应。真实产品里这显然是不可接受的。Spring AI 生态里解决这个问题靠的是 ChatMemory 和 ChatMemoryAdvisor。前者负责存储和管理历史消息后者负责在每次发起模型请求前自动把历史消息塞进 prompt再把新一轮的模型回复追加到历史记录里。最简单的一种实现是MessageWindowChatMemory它在内存里维护一个固定窗口大小的消息列表。你设置 maxMessages 为 20它就只能记住最近 20 条超出部分自动丢弃。好处是资源占用可控坏处是程序重启内存就没了而且多实例部署下各自维护各的这一点在生产环境要特别注意。接入方式比较轻松几乎不用改动业务代码。在构建 ChatClient 的时候挂载上对应的 Advisor 就行ChatMemory chatMemory new MessageWindowChatMemory( ChatMemoryConfig.builder() .maxMessages(20) .build() ); ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();配置好之后同一个 ChatClient 发起的多次请求会默认共享上下文。不同用户之间想隔离可以给会话指定唯一 IDString userSessionId user-12345; String response chatClient.prompt() .user(message) .advisors(advisor - advisor.param(chat_memory_conversation_id, userSessionId)) .call() .content();这样同一用户的多轮会话会共享记忆用户之间互不干扰。具体的参数名是chat_memory_conversation_id不同版本微调过但大体保持一致如果你升级版本后失灵最先查这个。4.2 窗口大小设置和 token 消耗的取舍把 maxMessages 设得越大不代表越好。每一轮对话都会把前面所有历史消息重新发送给模型历史越长消耗的 token 越多响应速度也会变慢费用自然水涨船高。这里有一个非常现实的问题你不可能无限增加记忆窗口。实际项目里除了消息条数限制很多场景还要结合“时间”维度做清理比如只保留最近 30 分钟内的对话。我自己的习惯是先用条数限制简单直接。上线后观察 token 消耗曲线如果费用偏高再考虑引入摘要压缩机制也就是每隔几轮对话让模型把前面的历史总结成一段摘要用摘要替代原始记录。这个思路 Spring AI 也有对应的实现叫 SummarizingAdvisor不过需要你调用时去加载自己控制触发时机。在没有专门做记忆治理之前先让功能跑通比追求最优更重要。应用部署方面如果你有多实例同时跑内存版 ChatMemory 会导致用户上一次请求落在 A 实例下一次请求落在 B 实例上下文对不上。这时候要么做会话粘滞要么用 Redis 自己实现一个 ChatMemory 存消息。Spring AI 也预留了扩展接口辛苦一点但值得。5. RAG 实战给模型外挂一个“知识库”5.1 从向量化到向量检索的完整链路RAG检索增强生成是 Spring-AI-Alibaba 里最有实用价值的能力之一。大模型训练数据是有截止日期的你问它最新的产品手册、内部制度、私有资料它大概率答非所问。RAG 的方法是先把你的私有文档切块、向量化、存到向量数据库用户提问时系统先检索出最相关的文档片段把片段拼到 prompt 里再请模型基于这些片段作答。这套链路在 Spring-AI-Alibaba 里能跑通的依赖主要有三个EmbeddingModel、VectorStore、QuestionAnswerAdvisor。EmbeddingModel 负责把文本转成向量VectorStore 负责存储向量和做相似度检索QuestionAnswerAdvisor 是挂在 ChatClient 上的一层增强器自动完成“检索 → 组装上下文 → 生成回答”的流程。先搞定向量化与存储。最简单的做法是用本地内存效果先看代码Configuration public class RagConfig { Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel).build(); } }SimpleVectorStore 是 Spring AI 自带的简易向量库实现适合做原型验证。数据量大了之后建议换成真正的向量数据库比如 Redis Vector Similarity Search、Milvus 等Spring AI 都有对应的实现类。但核心 API 一样切换成本可控。然后导入文档。Spring AI 提供了文档读取器可以读 txt、markdown、PDF 等格式。下面是一个往向量库里写入文档的示例Component public class DocumentIngestor { private final VectorStore vectorStore; public DocumentIngestor(VectorStore vectorStore) { this.vectorStore vectorStore; } PostConstruct public void loadDocuments() { var reader new TextFileDocumentReader( new FileSystemResource(docs/user-manual.md) ); ListDocument documents reader.get(); vectorStore.add(documents); } }切块策略其实挺影响效果的Spring AI 默认的 TokenTextSplitter 会根据 token 数自动切但在实际操作里要根据文档结构调整 chunk size 和 overlap。产品手册这类按章节目录组织的文档最好按标题切块避免把一个主题拆到两个不连贯的块里。5.2 用 QuestionAnswerAdvisor 跑通“文档问答”文档导入向量库之后接下来就是查询侧。把 QuestionAnswerAdvisor 挂到 ChatClient 上绑定这个 VectorStore框架就会在每次发起请求前自动执行相似度搜索检索结果作为上下文传递给模型。代码如下ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); String answer chatClient.prompt() .user(根据我们公司的用户手册密码重置流程是什么) .call() .content();这句代码背后发生的事情粗略可以分为四步第一步把你的问题转成向量在向量库里做相似度检索默认取 topK 个最相关的文档片段第二步把检索到的片段按一定格式拼进 prompt比如用“以下是参考资料”这样的引导语第三步把原始问题和参考片段一起交给模型第四步模型根据参考片段组织回答因为 prompt 里已经明确做了约束所以如果资料里没有相关信息模型会倾向于告诉你它不知道而不是瞎编。如果你想更精细地控制检索过程可以手动先查一遍向量库再把结果传给 ChatClient。下面的代码演示了手动检索的方法SearchRequest request SearchRequest.builder() .query(密码重置流程) .topK(3) .similarityThreshold(0.5) .build(); ListDocument documents vectorStore.similaritySearch(request);.similarityThreshold(0.5)指定最小相似度阈值低于这个分数的文档直接丢弃目的是防止检索到完全不相关的内容反而污染模型的回答。阈值设置需要试验设太高会导致查不到资料设太低会引入噪声。关于参考资料后续是否带引用来源也就是让模型回答时标注这是来自哪篇文档这可以通过在 prompt 模板里要求模型输出时附带来源编号来实现。实际操作中产品对内容的可靠性要求越高越需要做这一层方便用户溯源查看原文。6. 常见问题与排查实践6.1 报错信息速查表Spring-AI-Alibaba 是一个快速迭代的项目依赖版本和模型配置时不时会踩坑。我把实际开发中遇到的典型问题整理了一个速查表每条都附上我的排查思路方便你少走弯路。异常信息可能原因解决办法401 UnauthorizedAPI Key 错误或未配置检查spring.ai.dashscope.api-key是否生效确认百炼平台 Key 是否还有效注意不能有空格404 model not found模型名称不存在或当前账号无权限确认模型名是否正确通义千问常见模型包括 qwen-turbo、qwen-plus、qwen-max不同版本可用范围不同Connection timed out本地网络无法访问 DashScope 服务确认公网连通性代理环境下检查是否需要对某些域名走直连JsonMappingException模型输出格式不规范解析失败修改 prompt 明确要求只输出 JSON或使用.entity()的重试机制必要时降低 temperature 参数内容随机性大temperature 设置过高将 temperature 调到 0.7 以下需要一致性强的场景调到 0.2上下文字段冲突同时用了多个 Advisor 且顺序不对调整 Advisor 顺序如果开了多个 Advisor注意 ChatMemoryAdvisor 和 QuestionAnswerAdvisor 的先后关系有一个版本问题单独说spring-ai-alibaba 整个过程迭代得很快我刚开始用的时候遇到一个诡异的问题代码照着网上教程写的但启动后总是提示找不到ChatClient这个类。折腾了半天发现是网上教程用的 groupId 是org.springframework.ai而 Spring-AI-Alibaba 的 starter 引入的版本和我 pom 里手动选的不一致产生了依赖冲突。所以遇到这种“类不存在”的报错先检查 Maven 依赖树看看有没有重复的 Spring AI 核心包。6.2 调优建议与稳定化手段功能跑通只是第一步生产环境要可控、可调、可观测。三个我建议尽早做起来的方向第一把模型参数抽到配置中心不要散落在代码里。比如 temperature、max-tokens、模型名这些通过 ConfigurationProperties 绑定到一个配置类后续调整不用改代码重新发布。我再强调一下Spring 的配置优先级很灵活环境变量、配置中心、本地 yml 都可以覆盖一开始就把参数集中管理后面省很多事。第二全链路加日志和监控。模型调用属于外部依赖而且响应时间波动很大必须记录每次请求的 prompt、输出、耗时、token 消耗。spring-ai 本身支持把调用抽象成事件流你可以通过监听这些事件把数据接进监控系统。刚开始不用做得很重至少把每次调用的 tokens 和耗时打出来否则上线后出问题完全无从下手。第三给模型调用加超时和重试机制。默认配置在弱网环境可能不够你可以通过 RestClient 或者 WebClient 的超时配置去调整。DashScope 的接口偶尔会抖动设置适当的超时时间和重试次数能显著提升接口可用性。但是重试也要注意幂等如果业务上不允许消息重复重试前要做幂等判断。我在实际使用中还有一个体会同一个提示词在不同模型版本上的表现可能差很多。我的习惯是每次升级模型版本都把核心 prompt 跑一遍回归测试看输出质量有没有明显下降。大模型不像传统代码那样有明确的兼容性承诺版本升级造成的语义漂移是真实存在的不要盲目追新跑通后再升级。另外如果你的业务涉及敏感数据谨慎使用外部模型 API。Spring-AI-Alibaba 的好处是抽象层做得比较干净后续想切换到私有化部署模型改动成本是可控的关键是业务代码里不要直接依赖某个模型的专属能力尽量用 ChatClient 标准 API。最后再分享一个很小的实战细节在线上的问题反馈里我经常看到用户问一些跟知识库完全无关的问题而 RAG 的答案又特别喜欢“若无其事”地乱答。后来我在 system prompt 里明确写了一句“如果参考资料中没有相关信息请直接回答‘未在资料中找到相关内容’不要自行编造”。就这一句话无效回答的比例下降了一大截。这类工程细节不是看文档能看出来的只能一个个踩坑总结。希望这篇笔记能让你在 Spring-AI-Alibaba 的学习道路上少一点踩坑的痛多一点跑通的爽。