7.8 KiB
7.8 KiB
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.mdopenspec/changes/chatmodel-abstraction/design.mdopenspec/changes/chatmodel-abstraction/specs.mdopenspec/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—@PrimaryChatModel/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 名无统一规范(
dashScopeChatModelvsdashscopeEmbeddingModel) - 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 ✅