feat(llm): T2.3 输出缓存 —— 同输入命中跳过 LLM 调用(省成本+提速)

在 eino model 层加 cachingModel 装饰器,包在 failover 链最外层:命中直接跳过整条链。

- 只缓存 Generate(非流式):Stream 是用户可见的创作型输出、重复率低且回放复杂,透传不缓存。
- 键 = 模型名 + 绑定工具哈希 + 消息内容哈希,不同模型/工具集/输入互不串味。
- 默认 TTL 60s(env LLM_CACHE_TTL_S,0=关):只覆盖短窗内的重试/双发/重复点击 —— 这类
  几乎一定同一意图,命中省成本+提速;又短到不让助手对同一问题长期"复读"。容量上限
  LLM_CACHE_MAX(512) 满则随机淘汰。换激活模型 → 键含模型名自然失效。

测试:命中跳底层/不同输入不串味/TTL 过期重调/流式不缓存/工具集不同键/TTL=0 禁用。

诚实说明:本平台以创作型流式为主、且 Generate 输入(专家简报/评测)每次都变,**真实命中率
天然偏低**——主要吃"短窗内逐字相同"的重试/双发。机制正确、零风险(可 env 关),但不是大
成本杠杆;更大的省钱项(语义缓存/确定性工具结果缓存)是后续。Prompt 版本管理(T2.3 另一半)
未做。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-30 09:40:20 +08:00
parent 3519570178
commit e80e481f9f
3 changed files with 258 additions and 8 deletions
+11 -8
View File
@@ -71,9 +71,11 @@ func buildWithFallbacks(cfg *contract.ModelConfig) model.BaseChatModel {
return nil
}
ptcm, ok := primary.(model.ToolCallingChatModel)
if !ok || len(cfg.Fallbacks) == 0 {
return primary // 不支持 WithTools 或无备用 → 直接用主模型
if !ok {
return primary // 不支持 WithTools(无法包 failover/cache→ 直接用主模型
}
// 主链:主模型 +(可用的)备用模型串成 failover。
chain := ptcm
models := []model.ToolCallingChatModel{ptcm}
for i := range cfg.Fallbacks {
fb := cfg.Fallbacks[i]
@@ -89,13 +91,14 @@ func buildWithFallbacks(cfg *contract.ModelConfig) model.BaseChatModel {
models = append(models, t)
}
}
if len(models) == 1 {
return primary // 无有效备用
if len(models) > 1 {
fmt.Printf("[llm] 启用模型 failover:主 %s + %d 个备用\n", cfg.Model, len(models)-1)
chain = newFailoverModel(models, func(idx int, ferr error) {
fmt.Printf("[llm] 模型 failover:第 %d 个模型失败(%v),切下一个\n", idx, ferr)
})
}
fmt.Printf("[llm] 启用模型 failover:主 %s + %d 个备用\n", cfg.Model, len(models)-1)
return newFailoverModel(models, func(idx int, ferr error) {
fmt.Printf("[llm] 模型 failover:第 %d 个模型失败(%v),切下一个\n", idx, ferr)
})
// 缓存包在最外层:命中直接跳过整条 failover 链(省成本+提速)。键含模型名 → 换模型自然失效。
return withCache(chain, cfg.Model)
}
// buildChatModel 据 provider 归一化连接参数后构建 OpenAI 兼容 ChatModel。