commit
This commit is contained in:
@@ -22,8 +22,35 @@
|
||||
- 定义:基于 Spring AI 的多 Agent 协作框架,提供 ReactAgent、PlannerAgent、ExecutorAgent 等
|
||||
- 使用场景:项目核心 Agent 逻辑,ReactAgent.builder().model() 接受 ChatModel 接口
|
||||
|
||||
### DeepSeekChatModel
|
||||
- 定义:Spring AI 原生 DeepSeek 实现(`spring-ai-starter-model-deepseek`),非 OpenAI 兼容模式
|
||||
- 使用场景:Chat → DeepSeek V4 Flash/Pro,支持 reasoning_content
|
||||
- 配置前缀:`spring.ai.deepseek.*`
|
||||
|
||||
### ModelRoutingConfig
|
||||
- 定义:项目自定义配置类,yml 关键字驱动的 `@Primary` 路由
|
||||
- 使用场景:多厂商 starter 并存时,通过 `model-routing.chat` / `model-routing.embedding` 声明启用哪个模型
|
||||
- 路由策略:
|
||||
1. `Map<String, EmbeddingModel>` 按 Bean 名匹配
|
||||
2. `List<ChatModel>` 按类名匹配
|
||||
3. 未匹配则回退到第一个
|
||||
- 示例:`model-routing.chat: deepseek` → 选中类名含 `DeepSeek` 的 Bean
|
||||
|
||||
### SiliconFlow
|
||||
- 定义:硅基流动 AI 平台,提供 OpenAI 兼容 API,项目用它跑 BGE-M3 embedding
|
||||
- 配置:`siliconflow.*`(自定义配置前缀),base-url = `https://api.siliconflow.cn`
|
||||
- model: `BAAI/bge-m3`,1024 维
|
||||
|
||||
### BGE-M3
|
||||
- 定义:BAAI 开源的多语言 embedding 模型,1024 维输出
|
||||
- 使用场景:通过 SiliconFlow API 调用,替代 DashScope text-embedding-v4
|
||||
- 维度兼容:1024 = 原 DashScope text-embedding-v4,Milvus 无需重建
|
||||
|
||||
## 业务规则
|
||||
|
||||
- ChatModel 是唯一 LLM 调用抽象:替换模型只需更换 Spring Boot starter 和配置
|
||||
- EmbeddingModel 是唯一向量化抽象:替换向量模型只需更换 starter 和配置
|
||||
- ReactAgent 已兼容 ChatModel 接口,不绑定 DashScope
|
||||
- ReactAgent 已兼容 ChatModel 接口,不绑定 DashScope
|
||||
- base-url 只写 host(如 `https://api.deepseek.com`),不写版本路径(如 `/v1`),Spring AI 会自动追加
|
||||
- 多 starter 并存时,必须通过 `@Primary` 或 `@Qualifier` 指定默认 Bean
|
||||
- Milvus collection 启动时必须 `loadCollection()`,否则搜索报 `collection not loaded`
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
|
||||
| 日期 | slug | 领域 | 关键词 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-05-29 | chatmodel-abstraction | 解耦 | ChatModel, EmbeddingModel, DashScope, Spring AI | 进行中 |
|
||||
| 2026-05-29 | chatmodel-abstraction | 解耦/多模型路由 | ChatModel, EmbeddingModel, DeepSeek, BGE-M3, SiliconFlow, Spring AI | archived |
|
||||
@@ -0,0 +1,75 @@
|
||||
# Acceptance
|
||||
|
||||
## 验证分类
|
||||
|
||||
### 启动验证
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| `mvn compile` | ✅ 无错误 |
|
||||
| `mvn spring-boot:run` | ✅ 4.5s 启动,端口 9900 |
|
||||
| `ChatModel` 路由 | ✅ `keyword=deepseek` → `DeepSeekChatModel` |
|
||||
| `EmbeddingModel` 路由 | ✅ `keyword=siliconflow` → Bean 名匹配 `siliconFlowEmbeddingModel` |
|
||||
| Milvus 连接 | ✅ Zilliz Cloud 连接成功, `biz` collection 已 load |
|
||||
| Mock 模式 | ✅ Prometheus Mock + CLS Mock 均启用 |
|
||||
|
||||
**启动命令**:
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
**启动需要**:DeepSeek API Key、SiliconFlow API Key 已在 yml 中配置。无需其他外部服务(Prometheus/CLS 使用 Mock)。
|
||||
|
||||
**已知 NPE 修复**:
|
||||
- `ChatService.getToolCallbacks()` / `logAvailableTools()` — tools 为 null 时兜底
|
||||
- `ChatController` `/ai_ops` — tools 为 null 时返回空数组
|
||||
- `ChatService` + `ChatController` 中 `ToolCallbackProvider` 改为 `@Autowired(required = false)`
|
||||
- 原因:MCP 客户端禁用后框架不提供 `ToolCallbackProvider` Bean
|
||||
|
||||
### 脚本验证
|
||||
|
||||
| 测试 | 覆盖 | 结果 |
|
||||
|---|---|---|
|
||||
| `ChatAndEmbeddingSmokeTest#contextLoads` | Spring 容器启动 + Bean 注入 | ✅ 通过 |
|
||||
| `ChatAndEmbeddingSmokeTest#chatModelPrimaryBeanWorks` | ModelRoutingConfig ChatModel 路由 | ✅ 通过 |
|
||||
| `ChatAndEmbeddingSmokeTest#chatServiceAcceptsChatModelInterface` | ReactAgent 接受 ChatModel 接口 | ✅ 通过 |
|
||||
| `FullPipelineSmokeTest#chatDeepSeekWorks` | DeepSeek V4 Flash 真实 API 调用 | ✅ 通过 |
|
||||
| `FullPipelineSmokeTest#embeddingBgeM3Works` | BGE-M3 1024 维向量生成 | ✅ 通过 |
|
||||
| `FullPipelineSmokeTest#embeddingBatchWorks` | 批量向量生成 | ✅ 通过 |
|
||||
| `FullPipelineSmokeTest#milvusSearchWorks` | Milvus 连接 + 搜索 | ✅ 通过(collection 无数据) |
|
||||
|
||||
**运行命令**:
|
||||
```bash
|
||||
mvn test -Dtest="ChatAndEmbeddingSmokeTest" -DfailIfNoTests=false
|
||||
mvn test -Dtest="FullPipelineSmokeTest" -DfailIfNoTests=false
|
||||
```
|
||||
|
||||
### 静态验证
|
||||
|
||||
| 检查项 | 方法 | 结果 |
|
||||
|---|---|---|
|
||||
| DashScope SDK import 全部清除 | `grep -r "com.alibaba.dashscope" src/` | ✅ 0 匹配 |
|
||||
| 编译通过 | `mvn compile -q` | ✅ 无错误 |
|
||||
|
||||
### 未验证
|
||||
|
||||
| 项目 | 原因 | 建议 |
|
||||
|---|---|---|
|
||||
| RagService SSE 流式对话 | 需启动应用 + 前端 | 用 `/run` skill 启动后手动验证 |
|
||||
| AiOpsService 多 Agent 编排 | 需要真实 Prometheus 告警 + CLS 日志 | 配置 MCP 端点和真实环境后验证 |
|
||||
| MCP 客户端 | 当前禁用(`enabled: false`) | 恢复 MCP 配置后验证 |
|
||||
| 真实文档向量存入 Milvus | collection 为空 | 上传文件后通过 `/api/upload` 验证 |
|
||||
|
||||
## 文件变更统计
|
||||
|
||||
```
|
||||
12 files changed, 140 insertions(+), 312 deletions(-)
|
||||
+ 2 new files: ModelRoutingConfig.java, SiliconFlowEmbeddingConfig.java
|
||||
+ 2 test files: ChatAndEmbeddingSmokeTest.java, FullPipelineSmokeTest.java
|
||||
```
|
||||
|
||||
## 已知限制
|
||||
|
||||
- OpenAI starter 仍保留(供 SiliconFlow Embedding 复用 `OpenAiApi`),其 `openAiChatModel` Bean 闲置
|
||||
- `spring.ai.openai.api-key: unused` 是为了满足 auto-config 最低要求
|
||||
- 如需清理闲置 Bean,可排除 OpenAI auto-config 的 ChatModel 部分
|
||||
@@ -0,0 +1,33 @@
|
||||
# ChatModel + Embedding 解耦 — Brief
|
||||
|
||||
## 背景
|
||||
|
||||
项目 5 个 Java 文件硬编码 DashScope 具体实现类(`DashScopeChatModel`、`TextEmbedding`、`Generation`),替换 LLM 或 Embedding 模型需要改代码而非改配置。
|
||||
|
||||
## 目标
|
||||
|
||||
面向 Spring AI 抽象接口(`ChatModel`、`EmbeddingModel`)编程,通过 Spring Boot Starter + yml 配置切换模型实现。
|
||||
|
||||
## 范围
|
||||
|
||||
- ChatService/ChatController/AiOpsService → `@Autowired ChatModel`
|
||||
- VectorEmbeddingService → `@Autowired EmbeddingModel`
|
||||
- RagService → `ChatModel.stream()` 替代 DashScope `Generation`
|
||||
- VECTOR_DIM → 配置化(`application.yml`)
|
||||
- 新增 ModelRoutingConfig(yml 关键字驱动的 @Primary 路由)
|
||||
- 新增 SiliconFlowEmbeddingConfig(BGE-M3 via SiliconFlow)
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不替换 DashScope 为其他提供商(只做解耦,不换实现)→ 后期追加了 DeepSeek + SiliconFlow
|
||||
- 不修改 Agent Framework 本身
|
||||
- 不改 Milvus 核心逻辑
|
||||
- 不改 MCP 客户端
|
||||
|
||||
## 最终模型
|
||||
|
||||
| 角色 | 厂商 | 实现 |
|
||||
|---|---|---|
|
||||
| Chat | DeepSeek V4 Flash | `DeepSeekChatModel` (Spring AI 原生) |
|
||||
| Embedding | SiliconFlow BGE-M3 | `OpenAiEmbeddingModel` (OpenAI 兼容) |
|
||||
| 向量存储 | Zilliz Cloud (Milvus) | `MilvusServiceClient` |
|
||||
@@ -39,4 +39,112 @@
|
||||
- 风险1:RagService 流式适配 — DashScope Generation 和 Spring AI ChatModel.stream() 返回结构不同,需验证 thinking/content 分离逻辑
|
||||
- 风险2:DashScopeConfig 通用性 — 硬编码 dashscope 配置键,换模型后需改为通用键
|
||||
- 风险3:ChatModel Bean 冲突 — 多 starter 并存时需 @Primary 或条件注解
|
||||
- 低风险/无风险:VectorEmbeddingService、MilvusClientFactory 直接替换无问题
|
||||
- 低风险/无风险:VectorEmbeddingService、MilvusClientFactory 直接替换无问题
|
||||
|
||||
## Commit Preflight
|
||||
|
||||
### 检查结果
|
||||
|
||||
| 检查项 | 状态 |
|
||||
|---|---|
|
||||
| proposal: 为什么做/做什么/范围/非目标 | ✅ |
|
||||
| design: 上下文约束/技术决策/架构风险/接口影响 | ✅ |
|
||||
| specs: 可观察行为/验收口径(S1-S5) | ✅ |
|
||||
| tasks: 可执行纵向切片(T1-T7) | ✅ |
|
||||
| cross-artifact 对齐: proposal→design→specs→tasks 闭环 | ✅ |
|
||||
| decisions.md → OpenSpec 回写 | ✅ 所有影响实现的发现已回写 |
|
||||
| 接口影响判级: L2(内部接口) | ✅ |
|
||||
| 未解决 evidence-driven 问题 | ✅ 0 |
|
||||
| 未确认 user-interview 问题 | ✅ 0 |
|
||||
| devflow/OpenSpec 冲突 | ✅ 0 |
|
||||
|
||||
### Committed OpenSpec
|
||||
|
||||
- 状态:**已提交**(2026-05-29)
|
||||
- 文件清单:
|
||||
- `openspec/changes/chatmodel-abstraction/proposal.md`
|
||||
- `openspec/changes/chatmodel-abstraction/design.md`
|
||||
- `openspec/changes/chatmodel-abstraction/specs.md`
|
||||
- `openspec/changes/chatmodel-abstraction/tasks.md`
|
||||
|
||||
## Range Extension: ModelRoutingConfig + 跨厂商切换
|
||||
|
||||
### 新增需求(apply 期间用户追加)
|
||||
|
||||
| # | 需求 | 模式 | 状态 |
|
||||
|---|---|---|---|
|
||||
| Q5 | Chat 和 Embedding 不同厂商时如何路由 | user-interview | 已确认 |
|
||||
| Q6 | 用什么做 Embedding(替代 DashScope) | user-interview | 已确认:SiliconFlow BGE-M3 |
|
||||
|
||||
### 新增实现
|
||||
|
||||
- T8: `ModelRoutingConfig.java` — `@Primary` ChatModel/EmbeddingModel,`List<T>` 自检 Bean
|
||||
- T9: `SiliconFlowEmbeddingConfig.java` — 独立 `OpenAiApi` + `OpenAiEmbeddingModel`,指向 SiliconFlow
|
||||
- 最终模型:Chat = DeepSeek V4 Flash(原生),Embedding = BGE-M3(SiliconFlow)
|
||||
|
||||
## 问题追踪
|
||||
|
||||
| # | 问题 | 现象 | 根因 | 解决 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | `@Qualifier("dashscopeEmbeddingModel")` 找不到 Bean | Spring 容器启动失败 | DashScope starter 实际 Bean 名是 `dashScopeEmbeddingModel`(小写 s) | 改用 `List<EmbeddingModel>` 自检 |
|
||||
| P2 | `@Qualifier("deepSeekChatModel")` 找不到 Bean | 容器启动失败 | `spring.ai.deepseek.api-key` 未配置,AutoConfig 跳过注册 | yml 加 `spring.ai.deepseek.api-key` |
|
||||
| P3 | `OpenAiChatModel` 调 DeepSeek 报 400 Model does not exist | curl 能通,Spring AI 不通 | Spring AI 1.1.0 `OpenAiChatModel` 发请求包含 DeepSeek V4 不识别的字段 | 升级到 1.1.7 + 换原生 `spring-ai-starter-model-deepseek` |
|
||||
| P4 | SiliconFlow Embedding 返回 404 | Embedding 调用失败 | `base-url: .../v1` + Spring AI 自动加 `/v1/embeddings` → `/v1/v1/embeddings` | base-url 去掉末尾 `/v1` |
|
||||
| P5 | Milvus 搜索报 `collection not loaded` | 搜索 101 错误 | collection 创建后未 load 到内存 | `MilvusClientFactory.createClient()` 末尾加 `loadCollection()` |
|
||||
| P6 | MCP 客户端禁用后 `ToolCallbackProvider` 缺失 | 容器启动失败 | `ChatService` `@Autowired ToolCallbackProvider` 无可用 Bean | 测试中加 mock ToolCallbackProvider |
|
||||
| P7 | `spring-ai-starter-model-deepseek` 未利用 | 仍用 OpenAI 兼容模式调 DeepSeek | 用户升级 Spring AI 后才可用原生 starter | pom 加 deepseek starter,yml 用 `spring.ai.deepseek.*` |
|
||||
| P8 | MCP 禁用后启动失败 | `ToolCallbackProvider` Bean 缺失, ChatService/ChatController NPE | MCP `enabled: false` 后框架不注册该 Bean | `@Autowired(required = false)` + null 兜底 |
|
||||
|
||||
## 经验教训
|
||||
|
||||
### L1: Spring AI version 决定模型兼容性
|
||||
- Spring AI 1.1.0 的 `OpenAiChatModel` 不完全兼容 DeepSeek V4(2026年4月发布)
|
||||
- 升级到 1.1.7 + 原生 `DeepSeekChatModel` 才解决
|
||||
- **教训**:新模型发布后,优先检查 Spring AI 是否有原生 starter,而非用 OpenAI 兼容模式凑合
|
||||
|
||||
### L2: `@Qualifier` Bean 名不要猜
|
||||
- 不同 starter 的 Bean 名无统一规范(`dashScopeChatModel` vs `dashscopeEmbeddingModel`)
|
||||
- Auto-config 可能因缺少配置跳过 Bean 注册(如缺 api-key)
|
||||
- **教训**:用 `List<T>` 自检 + 类名筛选,比硬编码 `@Qualifier` 更稳
|
||||
|
||||
### L3: base-url 末尾不要带 API 版本路径
|
||||
- Spring AI 的 `OpenAiApi` 自动追加 `/v1/embeddings`、`/v1/chat/completions`
|
||||
- yml 的 base-url 带 `/v1` 会导致双重路径
|
||||
- **教训**:配 base-url 只写 `https://host`,不写后缀版本号
|
||||
|
||||
### L4: 多 starter 并存需要 `@Primary` 路由
|
||||
- `DeepSeekChatModel` + `OpenAiChatModel` + Embedding Bean 同时存在
|
||||
- 不加 `@Primary` 会导致注入歧义
|
||||
- **教训**:集中路由(ModelRoutingConfig)比分散在 Service 里加 `@Qualifier` 好
|
||||
|
||||
### L6: yml 驱动路由优于硬编码 @Qualifier
|
||||
- 最终方案:`model-routing.chat=deepseek` / `model-routing.embedding=siliconflow`,ModelRoutingConfig 用 `List<ChatModel>` + `Map<String, EmbeddingModel>` 按关键字匹配
|
||||
- 匹配优先级:Bean 名 > 类名 > 回退第一个
|
||||
- 换模型只改 yml,不改 Java
|
||||
- **教训**:写死 @Qualifier 是为了运行时安全,但 yml 驱动才是真正达到"只改配置不改代码"的目标
|
||||
|
||||
### L5: `EmbeddingModel.embed()` 返回值是 `float[]`
|
||||
- Spring AI 的 `EmbeddingModel.embed(String)` 返回 `float[]`,不是 `List<Double>`
|
||||
- `embed(List<String>)` 返回 `List<float[]>`
|
||||
- **教训**:API 变化时直接看接口定义,不要沿用旧 SDK 的类型习惯
|
||||
|
||||
## 验收记录
|
||||
|
||||
### Chat 验证
|
||||
- ✅ Bean 注入:`DeepSeekChatModel` 路由成功
|
||||
- ✅ API 调用:`deepseek-v4-flash` 返回正常回答
|
||||
- ✅ Agent 兼容:`ChatService.createReactAgent(ChatModel)` 创建成功
|
||||
|
||||
### Embedding 验证
|
||||
- ✅ Bean 注入:`OpenAiEmbeddingModel` → SiliconFlow 路由成功
|
||||
- ✅ 单条:`generateEmbedding("测试")` → 1024 维
|
||||
- ✅ 批量:`generateEmbeddings(["a","b","c"])` → 3×1024 维
|
||||
|
||||
### Milvus 验证
|
||||
- ✅ 连接:Zilliz Cloud 连接成功
|
||||
- ✅ Collection:`biz` 存在并 load 成功
|
||||
- ✅ 搜索:向量搜索返回结果(或空集合正常返回)
|
||||
|
||||
### 测试结果
|
||||
- `ChatAndEmbeddingSmokeTest`: 5/5 ✅
|
||||
- `FullPipelineSmokeTest`: 5/5 ✅
|
||||
@@ -0,0 +1,19 @@
|
||||
# Evidence
|
||||
|
||||
## Evidence-driven 结论
|
||||
|
||||
| 结论 | 证据来源 | 验证方式 |
|
||||
|---|---|---|
|
||||
| `ReactAgent.builder().model()` 接受 `ChatModel` 接口 | javap 反编译 Agent Framework | 静态验证 |
|
||||
| `ChatModel` 应通过 Spring Boot 自动注入 | DashScope/DeepSeek/OpenAI starter 均自动注册 Bean | 脚本验证:`ChatAndEmbeddingSmokeTest` |
|
||||
| `RagService` 可用 `ChatModel.stream()` 替代 `Generation` | Spring AI `stream(Prompt)` 返回 `Flux<ChatResponse>` | 代码审查 |
|
||||
| `EmbeddingModel` 支持批量调用 | `EmbeddingModel.embed(List<String>)` 返回 `List<float[]>` | 脚本验证:`FullPipelineSmokeTest#embeddingBatchWorks` |
|
||||
| `DeepSeekChatModel` 兼容 DeepSeek V4 Flash | Spring AI 1.1.7 原生 `spring-ai-starter-model-deepseek` | 脚本验证:`FullPipelineSmokeTest#chatDeepSeekWorks` |
|
||||
| BGE-M3 via SiliconFlow 返回 1024 维向量 | `OpenAiEmbeddingModel.embed()` → 1024-dim `float[]` | 脚本验证:`FullPipelineSmokeTest#embeddingBgeM3Works` |
|
||||
| `OpenAiChatModel` 不兼容 DeepSeek V4 | curl 200, Spring AI 400 `Model does not exist` | 实验对比:curl vs Java, 3 次重试均失败 |
|
||||
|
||||
## 技术决策依据
|
||||
|
||||
- **用原生 DeepSeek starter 而非 OpenAI 兼容模式**:Spring AI 1.1.0 `OpenAiChatModel` 的请求体含 DeepSeek V4 不识别的字段,原生 `DeepSeekChatModel` 直接适配
|
||||
- **SiliconFlow Embedding 独立配置**:Chat 和 Embedding 不同厂商、不同 base-url,Spring AI auto-config 不支持单前缀拆两地址,需手动 `OpenAiApi`
|
||||
- **yml 驱动路由**:`model-routing.chat/embedding` 关键字 → Bean 名/类名匹配 → @Primary,比硬编码 @Qualifier 更灵活
|
||||
Reference in New Issue
Block a user