# springai-skeleton **Repository Path**: dingwh/springai-skeleton ## Basic Information - **Project Name**: springai-skeleton - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-23 - **Last Updated**: 2026-09-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # spring-ai-demo 基于 **Spring AI 2.0.1(Spring Boot 4.1.1,Java 17)** 的本地工程骨架,覆盖五类核心能力: 1. **基础对话 + 流式输出**(SSE) 2. **多轮对话记忆**(ChatMemory + MessageChatMemoryAdvisor) 3. **工具调用 Tool Calling**(@Tool 注解) 4. **RAG 向量检索**(SimpleVectorStore + TokenTextSplitter + QuestionAnswerAdvisor) 5. **MCP 客户端 + 自研 MCP 服务器**(一套代码支持本地 `stdio` / 远程 `Streamable HTTP`,接入 `add` / `echo` / `now`) 骨架保持简洁,每个能力一个 Service + Controller,方便按你的业务直接扩展。 --- ## 一、环境要求 | 项 | 版本 | 说明 | |---|---|---| | JDK | 17+ | 项目编译目标为 17(本机已验证 17.0.20.1 可编译运行) | | Maven | 3.9+ | 使用 Maven 构建 | | 模型服务 | OpenAI 兼容端点 | 支持 OpenAI 官方、本地 vLLM / Ollama / one-api 等任何 OpenAI 兼容服务 | > 版本已实测验证:Spring AI 2.0.1(2026-08 发布)配 Spring Boot 4.1.1,字节码级别为 Java 17, > 无需升级到 JDK 21。 ## 二、快速启动 ### 1. 配置模型接入(application.yml) ```yaml spring: ai: openai: base-url: ${OPENAI_BASE_URL:https://api.openai.com} # 本地 vLLM/Ollama 等可改指向本地 api-key: ${OPENAI_API_KEY:sk-your-key-here} # 换成真实 Key;本地兼容端点可占位 chat: options: model: ${OPENAI_CHAT_MODEL:gpt-4o-mini} temperature: 0.7 embedding: options: model: ${OPENAI_EMBEDDING_MODEL:text-embedding-3-small} ``` 也支持通过环境变量覆盖: ```bash export OPENAI_API_KEY=sk-xxx export OPENAI_BASE_URL=https://api.openai.com export OPENAI_CHAT_MODEL=gpt-4o-mini export OPENAI_EMBEDDING_MODEL=text-embedding-3-small ``` ### 2. 启动 ```bash mvn clean package -DskipTests java -jar target/spring-ai-demo-0.0.1-SNAPSHOT.jar ``` 启动成功后监听 `http://localhost:8080`。 ## 三、接口清单 ### 1. 基础对话 + 流式输出 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/chat` | 非流式对话,body:`{"message":"你好"}` | | GET | `/api/chat/stream?message=你好` | SSE 流式输出,text/event-stream,逐 token 推送 | 响应示例(非流式):`text/plain` 直接返回回答文本。 ### 2. 多轮对话记忆 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/memory/chat` | body:`{"conversationId":"u-001","message":"我叫小明"}` | | GET | `/api/memory/chat/stream?conversationId=u-001&message=你好` | 流式版本 | 同一 `conversationId` 下后续提问可命中上文(如再问"我叫什么?"会回答"小明")。 记忆默认使用自动配置的 `InMemoryChatMemoryRepository` + `MessageWindowChatMemory`(内存版,重启即清空)。 ### 3. 工具调用 Tool Calling | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/tool/chat` | body:`{"message":"北京天气怎么样?"}` | | GET | `/api/tool/chat/stream?message=现在几点?` | 流式版本 | 系统自动触发 `WeatherTool` 中的 `getWeather` / `now` 两个 @Tool 方法,并把结果整合进回答。 ### 4. RAG 向量检索 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/rag/load` | 灌库,body:`{"content":"要写入知识库的文本..."}`,返回切块数 | | GET | `/api/rag/load-sample` | 一键加载内置示例文档 `docs/spring-ai-intro.txt` | | POST | `/api/rag/chat` | 问答,body:`{"message":"RAG 是什么?"}` | | GET | `/api/rag/chat/stream?message=...` | SSE 流式问答 | 典型流程: ```bash # 1. 灌入示例文档 curl http://localhost:8080/api/rag/load-sample # {"loadedChunks":8} # 2. 基于知识库提问 curl -X POST http://localhost:8080/api/rag/chat \ -H "Content-Type: application/json" \ -d '{"message":"Spring AI 的 RAG 链路由哪些组件组成?"}' ``` > 向量库默认 `SimpleVectorStore`(纯内存,重启清空,无需任何外部中间件,适合本地验证)。 > 换成 PgVector / Redis / Milvus 时,只需替换 `AiConfig#simpleVectorStore` 中的 Bean 实现并加对应依赖。 ### 5. MCP 客户端(接入外部 / 自研工具) | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/mcp/chat` | body:`{"message":"用工具算一下 3 加 4 等于几?"}` | | GET | `/api/mcp/chat/stream?message=现在几点了?` | 流式版本 | 本项目以 **MCP 客户端**身份,连接**三个自研 MCP 服务器**,并把工具一并注册进 `ChatClient`: | 连接名 | 服务器源码 | 语言 | 工具 | 端点 | |---|---|---|---|---| | `my-add-server-http` | `mcp-server/server.js` | Node | add / echo / now | `:8943/mcp`(Streamable HTTP) | | `java-math-server` | `mcp-server-java/math-server` | **Java** | add / multiply / divide / modulo | `:9001/sse`(SSE) | | `java-text-server` | `mcp-server-java/text-server` | **Java** | upper / reverse / now | `:9002/sse`(SSE) | > ⚠️ **传输端点不同**:自研 Node server 走 Streamable HTTP(`/mcp`);而 Spring AI 的 mcp-server 默认 > **暴露 SSE**(`spring.ai.mcp.server.sse.sse-endpoint` 默认 `/sse`,实测 `streamable-http` 的 `/mcp` 返回 404)。 > 故客户端对两个 Java server 用 `sse` 连接、对 Node server 用 `streamable-http` 连接(见 `application.yml`)。 > 两个 Java server 也支持 stdio:`--spring.ai.mcp.server.stdio=true`。 **启动顺序**(先起三个 server,再起 Spring;Spring 只在启动时建连接): ```bash # ① Node server(:8943) cd mcp-server && npm install && MCP_TRANSPORT=http node server.js # ② Java math server(:9001,另开终端) cd mcp-server-java && mvn -pl math-server spring-boot:run # ③ Java text server(:9002,另开终端) cd mcp-server-java && mvn -pl text-server spring-boot:run # ④ Spring Boot mvn spring-boot:run ``` 随后 Spring 日志应出现 `已接入外部 MCP Server,共注册 10 个工具:[now, reverse, upper, add, divide, modulo, multiply, alt_1_add, echo, alt_2_now]` (三个 server 的工具自动汇集;**仅当工具重名时** Spring 才自动加 `alt_N_` 前缀,如 Node 的 `add`/`now` 与 Java 重名 → `alt_1_add`/`alt_2_now`)。 > ⚠️ **顺序不能反**:Spring 只在启动那一刻建立 MCP 连接,若 server 没先起好, > 会降级为"无外部工具",且**已运行的 Spring 不会自动重连,需重启**。 > > 💡 嫌两条命令麻烦,直接用根目录一键脚本(它会先起 server 再起 Spring): > Windows 双击 `run-demo.cmd`;macOS/Linux 运行 `bash run-demo.sh`。 **切回 stdio(本地子进程、无需先起 server)**:注释掉 `application.yml` 里的 `streamable-http` 段、取消 `stdio` 段注释即可;`MCP_TRANSPORT` 缺省即为 stdio。 **两个独立的验证脚本**(只验证 server.js 本身,无需 Spring / LLM): ```bash cd mcp-server node demo-client.mjs # ① 通过 stdio 连 server.js:listTools + 调 add/echo/now # 发现工具: add(...) echo(...) now(...);add(3,4)=7;echo('Hi')=Hi node http-test.mjs # ② 通过 HTTP URL 连 server.js(需先以 http 模式启动) # 发现工具: add, echo, now;call add(5,6)=11 ``` > 提示:MCP 连接默认不阻塞启动。`McpChatService` **逐个探测每个 MCP server**,单个 server 不可达时 > **只跳过它自己的工具**(记一条 WARN),其余 server 的工具照常注册 —— 不会"一个 server 挂了导致全部读不到"。 ## 四、工程结构 ``` springai/ ├── run-demo.cmd / run-demo.sh # 一键启动:先起 MCP server 再起 Spring ├── pom.xml # Spring Boot 4.1.1 + Spring AI BOM 2.0.1 ├── mcp-server/ # 自研 Node MCP 服务器(独立子工程,非 Maven) │ ├── server.js # MCP 服务器:add / echo / now 三个工具 │ ├── demo-client.mjs # stdio 端验证脚本(连 server.js,无需 Spring) │ ├── http-test.mjs # HTTP 端验证脚本(连 server.js 的 http 模式) │ ├── package.json # 依赖: @modelcontextprotocol/sdk ^1.31 + zod │ └── .gitignore # 忽略 node_modules ├── mcp-server-java/ # 两个用 Java 实现的自研 MCP 服务器(Maven 多模块) │ ├── pom.xml # 父工程(spring-boot 4.1.1 + spring-ai BOM 2.0.1) │ ├── math-server/ # Server A:Java @Tool 数学工具 add/multiply/divide/modulo (:9001) │ └── text-server/ # Server B:Java @Tool 字符串/时间 upper/reverse/now (:9002) └── src/main/ ├── java/com/example/springai/ │ ├── SpringAiDemoApplication.java # 启动类 │ ├── config/AiConfig.java # SimpleVectorStore / TokenTextSplitter Bean │ ├── controller/ # 五个能力的 REST 入口 │ │ ├── ChatController.java │ │ ├── MemoryController.java │ │ ├── ToolController.java │ │ ├── RagController.java │ │ └── McpController.java # MCP 客户端接口(/api/mcp/*) │ ├── service/ # 对应 Service(ChatClient 封装) │ │ ├── ChatService.java │ │ ├── MemoryChatService.java │ │ ├── ToolChatService.java │ │ ├── RagService.java │ │ └── McpChatService.java # 注入 MCP 工具并注册给 ChatClient │ ├── tools/WeatherTool.java # @Tool 工具示例(天气/时间) │ └── dto/ # 请求体 record └── resources/ ├── application.yml └── docs/spring-ai-intro.txt # RAG 示例文档 ``` ## 五、关键实现点(扩展必读) - **ChatClient**:核心入口,通过自动配置的 `ChatClient.Builder` 构建;`.call()` 非流式,`.stream()` 返回 `Flux`。 - **多轮记忆**:`MessageChatMemoryAdvisor.builder(chatMemory).build()` 挂到 `defaultAdvisors`, 请求时用 `ChatMemory.CONVERSATION_ID`(`chat_memory_conversation_id`)作为上下文键传入 conversationId。 - **工具调用**:方法上加 `@Tool(name/description)` 即可,把 Bean 实例传给 `.defaultTools(...)`; 模型会自动判断何时调用、填参数、合并结果。 - **RAG**:`QuestionAnswerAdvisor.builder(vectorStore).build()` 注入时自动完成 "问题向量化 → 库内相似检索 → 相关片段注入 Prompt → 生成回答"全链路;灌库用 `TokenTextSplitter.split(doc)` 切块后 `vectorStore.add(chunks)`。 - **MCP 客户端**:`spring-ai-starter-mcp-client` 提供自动配置,应用以 MCP **客户端**身份 连接远程 `sse` / `streamable-http` 或本地 `stdio` 服务器;自动注册 `SyncMcpToolCallbackProvider` (bean 名 `mcpToolCallbacks`),`McpChatService` 取它的 `getToolCallbacks()` 传给 `ChatClient.defaultTools(...)`。 - 连接配置在 `spring.ai.mcp.client.*`(如 `stdio.connections.` / `sse.connections.`)。 - `initialized: false` + Service 层 try/catch:服务器不可达时降级为"无外部工具模式",不阻塞应用启动。 - **自研 Node MCP 服务器**(`mcp-server/server.js`):一个代码库两种传输 —— `stdio` (`StdioServerTransport`,Spring 用 `cmd /c node ...` 拉起当本地子进程)与 `Streamable HTTP` (`StreamableHTTPServerTransport`,独立端口按 URL 连接)。用 `registerTool(name, config, handler)` 暴露工具。 HTTP 模式有三个 SDK 要点:① stateless transport 不能跨请求复用;② `McpServer` 一次只能 connect 一个 transport; ③ sessionId 在真正 `handleRequest`(initialize)时才生成。因此按 `Mcp-Session-Id` 路由、每个会话各自 new 一个 `McpServer`+transport,并在 handleRequest 之后读取 sessionId 存 map。 - **自研 Java MCP 服务器**(`mcp-server-java/math-server`、`mcp-server-java/text-server`):与 Node 版精神一致,但用 **Spring AI 的标准方式** —— 在独立 Spring Boot 应用里,把 `@McpTool` 注解的方法(类 `@Component`,参数用 `@McpToolParam`) 交给 `spring-ai-starter-mcp-server-webmvc` 的自动配置,自动暴露成 MCP 服务器,**无需手写底层 SDK**。 - ⚠️ 工具注解是 **`org.springframework.ai.mcp.annotation.McpTool`**,**不是** ChatClient 工具调用用的 `@Tool`;用错了会报 `WARN SyncMcpToolProvider: No tool methods found`,工具不暴露。 - 默认暴露 **SSE**(端点 `/sse`,消息端点 `/mcp/message`);`--spring.ai.mcp.server.stdio=true` 切 stdio。客户端用 `sse` 连接连它。 ## 六、常见问题 - **启动时报 api-key 错误**:检查 `application.yml` 中 `spring.ai.openai.api-key` 是否设置。 - **接口一直转圈/超时**:确认 `base-url` 与模型名匹配,本地端点需支持 `/chat/completions` 与 `/embeddings` 两个 OpenAI 兼容接口。 - **记忆/向量库重启丢失**:当前为内存实现,属骨架预期;生产替换为 Redis / PgVector 等持久化方案。 - **`CreateProcess error=2 系统找不到指定的文件`**:Windows 下 JVM 无法直接启动 `.cmd`/`.bat` / shell 脚本, 配置里必须用 `command: cmd` + 参数 `[ /c, npx|node, ...]` 包装;macOS/Linux 直接写 `command: npx|node` 即可。 - **MCP 服务器连不上 / 提示 npx 找不到**:若是 `@modelcontextprotocol/server-everything`,其域名/包需网络可达; 更推荐直接用本项目自研的 `mcp-server/server.js`(纯本地、零外网依赖),或先 `where npx.cmd` 拿到绝对路径填进配置。