从零搭建企业级 Multi-Agent 系统:Spring AI Alibaba + RAG + MCP 实战全记录

8 阅读7分钟

本文基于个人项目实战,完整记录了如何用 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 返回 404base-url 写了 /v1,Spring AI 自动追加变成双 /v1base-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 还在 recoveringwaitForCollectionReady()

十、总结

这个项目从 2025 年 11 月开始做,到现在经历了 18 个迭代阶段。核心收获:

  1. Multi-Agent 不是银弹,但确实比单 Agent 在复杂场景下准确率更高,关键是 Supervisor 的 Prompt 要写好
  2. RAG 的效果 80% 取决于检索质量,HyDE + Reranker 是性价比最高的优化手段
  3. MCP 协议让跨语言工具协作变得很优雅,比自己写 HTTP 接口规范得多
  4. 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