手把手实现:Spring AI 全链路智能助手
手把手实现:Spring AI 全链路智能助手(DeepSeek + 千问 Embedding + Milvus + Redis)
基于本仓库实战项目整理。
技术栈:Java 17 · Spring Boot 3.5.9 · Spring AI 1.1.2
能力图谱:多轮对话 · Tool Calling · Redis 记忆 · Markdown RAG · 文档上传入库
如果你也想做一个「能聊天、有记忆、会调工具、还能依据私有文档回答」的 Spring 应用,这篇就是按本项目真实代码走通的全链路实现指南。
一、先画一张总图
不要一上来就堆依赖。先认清这条业务链路:
一句话概括:
千问负责把文本变成向量,Milvus 负责检索相关片段,DeepSeek 负责带着上下文和工具去生成回答;Redis 负责记住你们聊过什么。
这是刻意的「对话模型」与「向量模型」拆分,也是本项目最值得写进博客的设计点。
二、你会得到什么
完成后可具备:
| 能力 | 接口 / 入口 |
|---|---|
| 同步多轮对话 | POST /api/chat |
| SSE 流式输出 | GET /api/chat/stream |
| 清空会话记忆 | DELETE /api/chat/{conversationId} |
| 全量文档入库 | POST /api/rag/ingest |
| 上传 Markdown 落盘并入库 | POST /api/rag/upload |
| 仅看检索结果 | GET /api/rag/search |
启动时自动扫 docs/ 入库 |
ai.rag.ingest-on-startup |
配套还有简易 Web 页:src/main/resources/static/index.html。
三、环境与依赖
3.1 外部依赖(先跑通,再写代码)
| 组件 | 用途 | 本机默认 |
|---|---|---|
| Redis | 对话记忆 | localhost:6379 |
| Milvus | 向量库 | localhost:19530(需健康:/healthz) |
| DeepSeek API Key | 对话 | 环境变量 DEEPSEEK_API_KEY |
| DashScope API Key | Embedding | 环境变量 DASHSCOPE_API_KEY |
Milvus 务必用官方 standalone(含 etcd + MinIO + Milvus),不要只起一个裸 milvus 进程。若服务跑在 VirtualBox,还需配置 NAT 端口转发:19530、9091。
3.2 Maven 核心依赖
<!-- 对话:DeepSeek -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<!-- Embedding:阿里云 DashScope -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.0</version>
</dependency>
<!-- 向量库:Milvus -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>
<!-- RAG Advisor + Markdown 读取 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
<!-- 记忆:Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
用 spring-ai-bom / spring-ai-alibaba-bom 统一版本,减少「半个模块升、半个模块没升」的事故。
四、配置:用 YAML 选定谁聊天、谁做向量
业务代码几乎不写 new MilvusVectorStore(),而是:
- 引入对应 Starter → classpath 上有实现
- YAML 打开自动配置 → 容器里出现
VectorStore/ChatModelBean - 业务只注入接口
4.1 模型分工(最关键)
# application-dev.yml
spring:
ai:
model:
chat: deepseek # 对话走 DeepSeek
embedding: dashscope # 向量走千问
deepseek:
api-key: ${DEEPSEEK_API_KEY}
chat:
model: deepseek-chat
temperature: 0.7
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
enabled: false # 只要 Embedding,关掉千问 Chat
embedding:
options:
model: text-embedding-v3
dimensions: 1024
vectorstore:
milvus:
client:
host: ${MILVUS_HOST:localhost}
port: ${MILVUS_PORT:19530}
collection-name: spring_ai_rag
embedding-dimension: 1024 # 必须与 dimensions 一致
index-type: IVF_FLAT
metric-type: COSINE
initialize-schema: true
启动类再加一道保险,避免 DashScope Chat 自动配置掺和进来:
@SpringBootApplication(exclude = DashScopeChatAutoConfiguration.class)
public class AiApplication { ... }
4.2 业务参数
# application-params.yml
ai:
assistant:
system-prompt: 你是一个智能助手...优先依据检索到的文档内容。
chat:
memory-max-messages: 20
redis:
time-to-live: 7d
rag:
document-dir: docs
top-k: 4
similarity-threshold: 0.6
chunk-size: 800
chunk-overlap: 100
ingest-on-startup: true
配置拆成三层很实用:
application.yml:通用(profile、日志、上传大小)application-dev.yml:厂商与中间件application-params.yml:提示词与 RAG 调参
密钥请走环境变量,不要长期明文进仓库。
五、链路 1:ChatClient —— 对话的「总调度台」
Spring AI 推荐用 ChatClient,而不是到处直接调底层 ChatModel。本项目在 Configurationll 里组装:
@Bean
public ChatClient chatClient(
@Qualifier("deepSeekChatModel") ChatModel chatModel,
ChatMemory chatMemory,
ChatTools chatTools,
QuestionAnswerAdvisor questionAnswerAdvisor,
@Value("${ai.assistant.system-prompt}") String systemPrompt) {
return ChatClient.builder(chatModel)
.defaultSystem(systemPrompt)
.defaultTools(chatTools)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build(),
questionAnswerAdvisor
)
.build();
}
含义:
| 拼装项 | 作用 |
|---|---|
defaultSystem |
人设:助手名、工具能力、RAG 优先 |
defaultTools |
Function Calling:时间、天气(示例) |
SimpleLoggerAdvisor |
调试请求/响应 |
MessageChatMemoryAdvisor |
按 conversationId 注入历史 |
QuestionAnswerAdvisor |
提问前先检索 Milvus,把相关 chunk 塞进 Prompt |
Controller 变得很薄:
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
String conversationId = resolveConversationId(request.conversationId());
String content = chatClient.prompt()
.user(request.message())
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
return new ChatResponse(conversationId, content);
}
一次用户提问,幕后自动走完:取记忆 → 向量检索 →(必要时)调工具 → DeepSeek 生成。
流式只是把 .call() 换成 .stream().content(),并 produces = TEXT_EVENT_STREAM。
六、链路 2:Redis 对话记忆
6.1 两层抽象
| 层 | 类 | 职责 |
|---|---|---|
| 策略 | MessageWindowChatMemory |
只保留最近 N 条 |
| 存储 | RedisChatMemoryRepository |
Redis 读写 JSON 消息列表 |
ChatMemoryRepository 是你可替换的扩展点:内存 / Redis / DB 都可插拔。
6.2 要点
- Key 建议带业务前缀,例如
ai:chat:{conversationId} - 设置 TTL(本项目默认 7 天),避免会话无限膨胀
- 清除会话:
chatMemory.clear(conversationId)→DELETE /api/chat/{id}
记忆解决的是「多轮上下文」;知识库解决的是「模型没训练过的私有文档」。两者不要混为一谈。
七、链路 3:Tool Calling
用注解声明工具,Spring AI 会把方法暴露给模型:
@Component
public class ChatTools {
@Tool(description = "获取当前日期和时间...")
public String getCurrentDateTime() { ... }
@Tool(description = "查询指定城市的天气...")
public String getWeather(@ToolParam(description = "城市名称") String city) { ... }
}
挂到 ChatClient.defaultTools(chatTools) 后,用户问「现在几点 / 北京天气」时,模型可主动调用 Java 方法,再把结果编回自然语言。
生产落地建议:工具做真实远程调用时务必超时、鉴权、幂等;描述写清楚「何时该用」,减少乱调工具。
八、链路 4:RAG 全流程(这篇的核心)
8.1 离线入库:文档 → 向量
本项目服务类:RagIngestionService。
docs/*.md
→ MarkdownDocumentReader(按标题 / 分隔线切块)
→ 超长小节再 TokenTextSplitter
→ 生成稳定 MD5 id + metadata
→ 分批 vectorStore.add(每批 10 条)
→ DashScope Embedding → Milvus
为什么按 Markdown 结构切?
标题下的内容语义完整,检索命中后更容易「带着小节上下文」回答,而不是在句子中间硬砍。
表格怎么切?MarkdownDocumentReader 不会把表格当独立切点,表格通常跟所属标题小节走;超长小节二次 token 切分时存在从表中间切开的风险——写技术文时要心里有数。
为什么必须分批 add?
千问 Embedding 单次文本条数有上限(SDK 常见校验为 25)。本项目用:
private static final int EMBEDDING_BATCH_SIZE = 10;
一次性扔 100 个 chunk,启动就会炸:The input texts limit 25。
8.2 触发入库的三种方式
- 启动自动:
ApplicationRunner+ai.rag.ingest-on-startup=true - 全量重扫:
POST /api/rag/ingest - 上传单个 Markdown:
POST /api/rag/upload
上传逻辑:
- 校验扩展名(仅
.md/.markdown) - 防路径穿越,保存到
ai.rag.document-dir - 重名则加时间戳
- 对该文件执行切分 + 分批入库
返回示例:
{
"filename": "product-faq.md",
"savedPath": ".../docs/product-faq.md",
"ingestedChunks": 12
}
8.3 在线问答:检索 → 注入 Prompt → 生成
RagConfig 里把检索参数固化进 Advisor:
@Bean
public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore, RagProperties p) {
return QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(p.getTopK())
.similarityThreshold(p.getSimilarityThreshold())
.build())
.build();
}
效果:用户问「Spring Boot 自动配置怎么回事?」时:
- 问题经 DashScope 变成向量
- Milvus 按 COSINE 取 TopK
- 过滤掉低于
similarity-threshold的噪声 - 相关 chunk 进入 Prompt
- DeepSeek 综合生成回答
想单独验检索质量(不经过 LLM):
GET /api/rag/search?query=自动配置
可调参数:
| 参数 | 偏大时 | 偏小时 |
|---|---|---|
top-k |
更全,也可能更吵 | 更干净,可能漏信息 |
similarity-threshold |
更严,可能无上下文 | 更松,可能掺无关段落 |
九、VectorStore:代码里「怎么指定用了 Milvus」
答案:业务代码不写死厂商名。
pom 引入 spring-ai-starter-vector-store-milvus
+ application-dev.yml 配置 spring.ai.vectorstore.milvus.*
→ 自动配置产出 VectorStore Bean
→ RagIngestionService / RagController / RagConfig 注入接口即可
你自己实现的是「如何切文档、何时 add、如何搜」;Milvus 客户端与 Schema 初始化交给 Starter。
换 Redis / PGVector 向量库时,通常改依赖和 YAML,业务层改动很小。
十、工程目录与职责速查
com.zxx.ai
├── AiApplication 启动;排除 DashScope Chat 自动配置
├── config
│ ├── Configurationll ChatClient / ChatMemory
│ ├── RagConfig QuestionAnswerAdvisor
│ ├── RagProperties ai.rag.*
│ ├── RedisChatMemoryConfig Redis + Repository Bean
│ └── RedisChatMemoryProperties
├── controller
│ ├── ChatController 对话 API
│ └── RagController 入库 / 上传 / 检索
├── rag
│ └── RagIngestionService 读盘、切块、分批向量化
├── memory
│ └── RedisChatMemoryRepository
├── tool
│ └── ChatTools
└── dto 请求响应对象
文档目录建议:
docs/
├── SPRING_AI_LEARNING.md 学习路线
├── SPRING_COMMON_LESSONS.md (若有)常见类
├── SPRING_BOOT_MASTERY.md Boot 精通路线
└── SPRING_AI_FULL_PIPELINE_BLOG.md ← 本文
本地知识库同样可放在 docs/,被 ai.rag.document-dir 扫描入库(注意:学习文档也会被扫进去——若要隔离,可改成单独 knowledge/ 目录)。
十一、从零启动的验收清单
按这个顺序验,少走弯路:
- Redis
PING通 - Milvus 虚拟机内
curl http://127.0.0.1:9091/healthz→ OK;Windows 经端口转发再测一遍 - 配置好两个 API Key(建议环境变量)
- 启动应用,看日志是否完成分批 Ingest
GET /api/rag/search?query=xxx能返回带source、score的片段POST /api/chat问知识库问题,回答应引用文档语义- 连续两轮对话,确认 Redis 记忆生效
- 问「现在几点」,确认 Tool 被调用
POST /api/rag/upload上传新 md,再问新文档内容
十二、踩坑实录(本项目真实遇到过)
1. 启动失败:DEADLINE_EXCEEDED 连不上 Milvus
- 根因常是:端口转发存在,但容器内 Proxy 不健康 / 缺 etcd
- 判据:
healthz不是 200 OK - 处理:官方 Compose 起齐 etcd + minio + milvus;内存建议 ≥ 8GB
2. 拉镜像 connection refused(Docker Hub)
- 国内环境配置
registry-mirrors后再docker compose pull
3. The input texts limit 25
- 根因:一次
vectorStore.add塞了太多 chunk - 处理:按 10 条分批 Embedding
4. Chat 自动配置冲突
- 同时引入 DeepSeek 与 DashScope 时,关掉 DashScope Chat(exclude +
chat.enabled=false)
5. 维度不一致
dimensions: 1024必须等于 Milvusembedding-dimension: 1024- 换模型维度后,旧 Collection 不要硬混用
十三、架构取舍:为什么值得照这个样子做
| 决策 | 收益 |
|---|---|
| Chat / Embedding 厂商分离 | 各用所长,成本与质量可独立升级 |
| Advisor 组装能力 | 记忆、RAG、日志横切,Controller 保持干净 |
| Markdown 结构切块 | 检索语义更贴近「章节」 |
| Redis 做会话 | 水平扩展友好,带 TTL |
| 上传即落盘又入库 | 知识库可运营,而不是只能重启扫目录 |
只依赖 VectorStore 接口 |
换向量库时业务代码稳定 |
还可以继续演进的方向(本仓库阶段 8):Actuator 观测、检索评估防幻觉、鉴权与 Prompt 注入防护、把 Redis Memory 抽成独立 Starter。
十四、给读者的最小代码心智模型
实现这种项目,其实只反复在做四件事:
- 组装 ChatClient(System + Tools + Advisors)
- 把文档变成带 metadata 的 Document,并安全地写入 VectorStore
- 用 QuestionAnswerAdvisor 或手动 similaritySearch 把知识找回 Prompt
- 用 ChatMemoryRepository 把多轮会话落到可信存储
Spring AI 的价值,是把「模型厂商差异」和「向量库差异」都藏进 Starter 与自动配置;你的创造力,应该花在切块策略、提示词、工具设计与产品体验上。
十五、API 速查卡
# 同步对话
curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d "{\"message\":\"Milvus 的余弦相似度是干什么的?\"}"
# 流式
curl -N "http://localhost:8080/api/chat/stream?message=你好"
# 上传知识库
curl -F "file=@./faq.md" http://localhost:8080/api/rag/upload
# 全量入库
curl -X POST http://localhost:8080/api/rag/ingest
# 只看检索
curl "http://localhost:8080/api/rag/search?query=自动配置"
结语
这不是又一个「Hello ChatGPT」Demo,而是一条可落库、可记忆、可扩工具、可运营知识库的 工程化全链路:
DeepSeek 负责说人话,DashScope 负责把知识编成向量,Milvus 负责把相关段落找回来,Redis 负责把对话接着聊下去。
照着本仓库从配置 → ChatClient → Memory → Tools → RAG 入库 → 上传接口走一遍,你会同时练到 Spring Boot 自动配置与 Spring AI 的核心用法。下一步,把示例天气工具换成真实业务 API,把 docs/ 换成你们的产品手册——这套骨架就可以变成真正的内部助手。
更多推荐

所有评论(0)