150 lines
7.8 KiB
Markdown
150 lines
7.8 KiB
Markdown
# ChatModel Abstraction Decisions
|
||
|
||
## Question Pool
|
||
|
||
| # | 维度 | 问题 | 模式 | 状态 |
|
||
|---|---|---|---|---|
|
||
| Q1 | 术语 | ChatModel 注入方式:Spring Boot 自动注入 vs 手动工厂创建 | evidence-driven | 已解决 |
|
||
| Q2 | 边界 | RagService 流式对话:Spring AI ChatModel.stream() 替代 DashScope Generation | evidence-driven | 已解决 |
|
||
| Q3 | 验收 | VECTOR_DIM 是否需要动态化 | user-interview | 已解决 |
|
||
| Q4 | 边界 | VectorEmbeddingService 批量向量化:EmbeddingModel 支持批量调用 | evidence-driven | 已解决 |
|
||
|
||
## Evidence-driven
|
||
|
||
| 结论 | 证据来源 | 是否已汇报用户 |
|
||
|---|---|---|
|
||
| ReactAgent.builder().model() 接受 ChatModel 接口 | javap 反编译 | 已汇报 |
|
||
| ChatModel 应通过 Spring Boot 自动注入 | DashScope starter 自动注册 ChatModel Bean | 已汇报 |
|
||
| RagService 可用 ChatModel.stream() 替代 Generation | Spring AI 接口有 stream(Prompt) 返回 Flux | 已汇报 |
|
||
| EmbeddingModel 支持批量调用 | EmbeddingModel.call(EmbeddingRequest) 接受多条文本 | 已汇报 |
|
||
|
||
## User-interview
|
||
|
||
| 问题原文 | 用户原话 | 确认状态 | OpenSpec 回写 |
|
||
|---|---|---|---|
|
||
| VECTOR_DIM 怎么处理? | "配置文件动态化" | 已确认 | 已回写 proposal |
|
||
|
||
## 关键取舍
|
||
|
||
- 决策:本次只解耦不替换实现
|
||
- 原因:先验证抽象层正确再换模型
|
||
- 影响:代码改动不改变运行行为
|
||
- 风险接受:用户同意先只做解耦
|
||
- 决策:VECTOR_DIM 从配置文件读取
|
||
- 原因:换模型时改 yml 即可
|
||
- 影响:MilvusConstants.VECTOR_DIM 改为从 MilvusProperties 读取
|
||
|
||
## 架构审计
|
||
|
||
- 风险1:RagService 流式适配 — DashScope Generation 和 Spring AI ChatModel.stream() 返回结构不同,需验证 thinking/content 分离逻辑
|
||
- 风险2:DashScopeConfig 通用性 — 硬编码 dashscope 配置键,换模型后需改为通用键
|
||
- 风险3:ChatModel Bean 冲突 — 多 starter 并存时需 @Primary 或条件注解
|
||
- 低风险/无风险: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 ✅ |