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

7.8 KiB
Raw Blame History

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 ✅