Files
SuperBizAgent-java/devflow/projects/2026-05-29-chatmodel-abstraction/decisions.md
T
2026-05-31 21:45:14 +08:00

150 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ✅