本文基于个人项目实战,完整记录了如何用 Spring AI Alibaba 构建一个支持多智能体协作、知识库问答、跨语言工具调用的 LLM 编排系统。不是 Demo 级别的 Hello World,而是一个从架构设计到生产防护都考虑过的系统。
一、为什么要做这个系统?
现在市面上大模型应用的文章很多,但大部分停留在"调一下 API,拼一下 Prompt"的阶段。实际企业场景中,你会遇到这些问题:
- 用户问的问题横跨多个领域(查数据、搜知识库、导 Excel),单个 Agent 搞不定
- RAG 直接拿用户 query 去向量检索,召回率很差(用户说"加班费",文档写"延时工作报酬")
- LLM 不稳定,高并发下 API 超时/限流,不做防护整个系统会雪崩
- Java 生态做 AI 的资料远少于 Python,Spring AI 还在快速迭代,踩坑成本高
这个项目就是为了解决这些问题,从 0 到 1 搭了一套完整的方案。
二、系统架构总览
用户请求
↓
Spring Cloud Gateway(路由 + 鉴权 + 限流)
↓
┌─────────────────────────────────────────┐
│ StateGraph 多智能体编排 │
│ │
│ SupervisorNode(LLM 自主路由决策) │
│ ↓ ↓ ↓ │
│ KnowledgeAgent DataQueryAgent ExportAgent
│ (知识库问答) (Text-to-SQL) (Excel导出)
│ │ │ │ │
│ ↓ ↓ ↓ │
│ RAG Pipeline SQL生成+执行 MCP调用 │
│ (HyDE+Milvus (安全校验) (Java→Python)
│ +Reranker) │
└─────────────────────────────────────────┘
↓
Kafka 异步 → 记忆写入 + LLM-as-a-Judge 评分
↓
Langfuse 可观测性(Trace + Score + Token 追踪)
技术栈:Spring Boot 3.x + Spring AI 1.1.x + Spring AI Alibaba Graph + Milvus + Kafka + Redis + MinIO + Ollama + Langfuse
三、核心设计:Supervisor 多智能体模式
为什么不用单 Agent?
单 Agent 的问题:你给一个 Agent 塞 10 个工具,Prompt 又臭又长,LLM 选工具的准确率直线下降。
Supervisor 模式的思路是:一个"调度员"负责理解用户意图,把任务分发给专门的子 Agent。每个子 Agent 只做一件事,Prompt 短而精准。
// StateGraph 定义
StateGraph graph = new StateGraph(stateFactory)
.addNode("supervisor", supervisorNode)
.addNode("knowledge_agent", knowledgeAgentNode)
.addNode("data_query_agent", dataQueryAgentNode)
.addNode("export_agent", exportAgentNode)
.addNode("memory", memoryNode)
.addEdge(START, "supervisor")
.addConditionalEdges("supervisor", routingFunction, routeMap)
.addEdge("knowledge_agent", "memory")
.addEdge("data_query_agent", "memory")
.addEdge("export_agent", "memory")
.addEdge("memory", END);
Supervisor 的 System Prompt 大概长这样:
你是一个任务调度器。根据用户输入选择最合适的 Agent:
- KnowledgeAgent:知识库搜索、文档问答
- DataQueryAgent:数据查询、报表统计
- ExportAgent:文件导出、Excel 生成
请返回 JSON:{"next": "agent名字", "reason": "选择理由"}
关键设计点:Supervisor 可以多轮路由。比如用户说"帮我查上个月的销售数据,导出成 Excel",Supervisor 会先路由到 DataQueryAgent 查数据,拿到结果后再路由到 ExportAgent 导出。
四、RAG:为什么直接向量检索不够用?
问题:语义鸿沟
用户问:"公司加班有加班费吗?" 文档原文:"员工延时工作按照劳动法规定支付相应报酬。"
直接把用户 query 向量化去检索,相似度很低,因为"加班费"和"延时工作报酬"的表述差异太大。
解决方案:HyDE + 两阶段检索
用户问题
↓
HyDE(用 Ollama 本地模型生成假设性回答)
→ "根据公司规定,加班按照劳动法支付加班工资,工作日1.5倍..."
↓
Milvus ANN 向量检索(bge-m3 Bi-Encoder)→ top-10 粗召回
↓
bge-reranker-v2-m3(Cross-Encoder)→ top-3 精排
↓
拼入 Prompt 作为上下文 → LLM 生成最终回答
为什么要两阶段?
| 阶段 | 模型类型 | 速度 | 精度 | 作用 |
|---|---|---|---|---|
| 粗召回 | Bi-Encoder(bge-m3) | 快(毫秒级 ANN) | 一般 | 从百万文档中快速筛出候选 |
| 精排 | Cross-Encoder(bge-reranker) | 慢(每对都要过模型) | 高 | 对 top-10 精细排序取 top-3 |
类比:粗召回 = 海选,精排 = 面试。你不可能让每个人都面试,但也不能只靠简历筛。
HyDE 的代价和收益
代价:多调一次 LLM(我用 Ollama 本地跑 deepseek-r1:8b,延迟 200~500ms) 收益:召回准确率提升明显,特别是专业术语多的文档场景
文档摄入流程
上传文件
├── 图片 → Tesseract OCR → 纯文本
└── 其他 → Apache Tika 解析
├── 有文字 → 纯文本
└── 为空 → OCR 兜底
→ 递归分块(3000 字符 / 300 overlap)
→ bge-m3 Embedding → 存入 Milvus(带 owner_id 用户级隔离)
踩坑记录:
- Milvus VarChar 的
maxLength默认 8192 不够,大文档分块后要改成 32768 - Milvus collection 处于 recovering 状态时操作会失败,需要加
waitForCollectionReady()等待
五、MCP:Java 怎么调 Python?
场景
DataQueryAgent 查完数据库后,需要把结果导出成 Excel。Java 的 Excel 库(POI)用起来很笨重,Python 的 openpyxl 更灵活。
MCP 协议
MCP(Model Context Protocol)是一个标准化的"LLM 调工具"协议。简单说就是:工具注册自己的名字、参数、描述 → LLM 看到工具列表后自主决定调哪个 → 框架负责执行。
通信方式:Stdio
Java 启动 Python 子进程 → 通过 stdin 发 JSON 请求 → Python 处理 → stdout 返回结果。
# application.yml
spring:
ai:
mcp:
client:
stdio:
connections:
python-skills:
command: py
args:
- python/server.py
Python 端的 Skill 用 SKILL.md 声明(工具名、参数、描述),server.py 启动时自动扫描 skills/ 目录注册所有 Skill。Java 端通过 MCP Client 自动发现可用工具。
好处:加新工具只需要在 Python 端加一个 Skill 文件 + SKILL.md,Java 端不用改任何代码。
六、记忆架构:参考 MemGPT 的三层设计
Core Memory(核心记忆)
→ 用户基本信息、偏好 → Redis Hash,常驻
→ 每次对话都注入 Prompt
Working Memory(工作记忆)
→ 当前对话的上下文摘要 → Redis String,会话级 TTL
→ 对话结束后由 LLM 压缩成摘要
Long-Term Memory(长期记忆)
→ 历史对话的语义索引 → Milvus 向量存储
→ 通过语义检索召回相关历史
记忆的写入全部走 Kafka 异步——用户对话结束后发消息到 Kafka,Consumer 负责压缩、向量化、存储。不阻塞主对话流程。
七、生产防护:4 层防御体系
LLM API 不像传统接口那么稳定(DeepSeek 偶尔超时、限流),不做防护系统会雪崩。
第一层:JWT 双 Token 认证
→ Access Token(30min)+ Refresh Token(7天)
→ 拦截未登录请求
第二层:用户级限流(Redis INCR)
→ 单用户每分钟最多 N 次请求
→ 防止单个用户刷爆 API
第三层:LLM 熔断器
→ 连续失败 3 次 → 熔断 30 秒 → 返回兜底响应
→ 防止一个挂掉的 LLM 拖垮整个系统
第四层:Semaphore 全局并发控制
→ 控制同时到达 LLM 的请求数(如最多 10 个)
→ 超出的请求排队或快速失败
八、可观测性:Langfuse + LLM-as-a-Judge
系统跑起来之后,怎么知道 LLM 回答得好不好?
用户提问 → LLM 回答 → 返回给用户(主流程结束)
↓ (Kafka 异步)
评分服务 → 用 Claude 评分(0-10 分)
↓
上报 Langfuse → Trace + Score 可视化
Langfuse 上能看到:每次对话的完整链路(哪个 Agent 处理的、调了什么工具、Token 用了多少)、质量评分趋势、成本统计。
九、踩坑合集
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Ollama 返回 404 | base-url 写了 /v1,Spring AI 自动追加变成双 /v1 | base-url 不带 /v1 |
| Ollama 连接超时 | 走了系统代理 | 配置 Proxy.NO_PROXY 绕过 |
| Kafka 反序列化失败 | Producer 没发类型头 | 保留 add.type.headers: true |
| POI 3.16 报错 | 和 commons-compress 1.27 不兼容 | 升 POI 到 5.2.5 |
| Milvus 操作失败 | collection 还在 recovering | 加 waitForCollectionReady() |
十、总结
这个项目从 2025 年 11 月开始做,到现在经历了 18 个迭代阶段。核心收获:
- Multi-Agent 不是银弹,但确实比单 Agent 在复杂场景下准确率更高,关键是 Supervisor 的 Prompt 要写好
- RAG 的效果 80% 取决于检索质量,HyDE + Reranker 是性价比最高的优化手段
- MCP 协议让跨语言工具协作变得很优雅,比自己写 HTTP 接口规范得多
- LLM 应用的工程化和传统后端一样重要——限流、熔断、异步、可观测性一个都不能少
Spring AI 生态还在快速发展,Java 做 AI 应用的体验已经比一年前好太多了。希望这篇文章能给同样在探索这个方向的同学一些参考。
技术栈完整清单:Spring Boot 3.5.x / Spring AI 1.1.x / Spring AI Alibaba Graph 1.1.x / Milvus / Kafka / Redis / MySQL / MinIO / Ollama / Langfuse / Docker