手把手实现:Spring AI 全链路智能助手(DeepSeek + 千问 Embedding + Milvus + Redis)

基于本仓库实战项目整理。
技术栈:Java 17 · Spring Boot 3.5.9 · Spring AI 1.1.2
能力图谱:多轮对话 · Tool Calling · Redis 记忆 · Markdown RAG · 文档上传入库

如果你也想做一个「能聊天、有记忆、会调工具、还能依据私有文档回答」的 Spring 应用,这篇就是按本项目真实代码走通的全链路实现指南。


一、先画一张总图

不要一上来就堆依赖。先认清这条业务链路:

用户 / 前端

ChatController

RagController

ChatClient

SimpleLoggerAdvisor

MessageChatMemoryAdvisor

QuestionAnswerAdvisor

ChatTools

DeepSeek ChatModel

Redis 对话记忆

VectorStore

Milvus

DashScope Embedding

RagIngestionService

本地 docs/*.md

一句话概括:

千问负责把文本变成向量,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 端口转发:195309091

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(),而是:

  1. 引入对应 Starter → classpath 上有实现
  2. YAML 打开自动配置 → 容器里出现 VectorStore / ChatModel Bean
  3. 业务只注入接口

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 触发入库的三种方式

  1. 启动自动:ApplicationRunner + ai.rag.ingest-on-startup=true
  2. 全量重扫:POST /api/rag/ingest
  3. 上传单个 Markdown:POST /api/rag/upload

上传逻辑:

  1. 校验扩展名(仅 .md / .markdown
  2. 防路径穿越,保存到 ai.rag.document-dir
  3. 重名则加时间戳
  4. 对该文件执行切分 + 分批入库

返回示例:

{
  "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 自动配置怎么回事?」时:

  1. 问题经 DashScope 变成向量
  2. Milvus 按 COSINE 取 TopK
  3. 过滤掉低于 similarity-threshold 的噪声
  4. 相关 chunk 进入 Prompt
  5. 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/ 目录)。


十一、从零启动的验收清单

按这个顺序验,少走弯路:

  1. Redis PING
  2. Milvus 虚拟机内 curl http://127.0.0.1:9091/healthz → OK;Windows 经端口转发再测一遍
  3. 配置好两个 API Key(建议环境变量)
  4. 启动应用,看日志是否完成分批 Ingest
  5. GET /api/rag/search?query=xxx 能返回带 sourcescore 的片段
  6. POST /api/chat 问知识库问题,回答应引用文档语义
  7. 连续两轮对话,确认 Redis 记忆生效
  8. 问「现在几点」,确认 Tool 被调用
  9. 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 必须等于 Milvus embedding-dimension: 1024
  • 换模型维度后,旧 Collection 不要硬混用

十三、架构取舍:为什么值得照这个样子做

决策 收益
Chat / Embedding 厂商分离 各用所长,成本与质量可独立升级
Advisor 组装能力 记忆、RAG、日志横切,Controller 保持干净
Markdown 结构切块 检索语义更贴近「章节」
Redis 做会话 水平扩展友好,带 TTL
上传即落盘又入库 知识库可运营,而不是只能重启扫目录
只依赖 VectorStore 接口 换向量库时业务代码稳定

还可以继续演进的方向(本仓库阶段 8):Actuator 观测、检索评估防幻觉、鉴权与 Prompt 注入防护、把 Redis Memory 抽成独立 Starter。


十四、给读者的最小代码心智模型

实现这种项目,其实只反复在做四件事:

  1. 组装 ChatClient(System + Tools + Advisors)
  2. 把文档变成带 metadata 的 Document,并安全地写入 VectorStore
  3. 用 QuestionAnswerAdvisor 或手动 similaritySearch 把知识找回 Prompt
  4. 用 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/ 换成你们的产品手册——这套骨架就可以变成真正的内部助手。


Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐