一、问题背景

在使用 DeepSeek 或 MiMo 等推理模型时,API 返回的消息中同时包含 reasoning_content(推理过程)和 tool_calls(工具调用指令)。然而,在多轮会话场景下出现了一个棘手的问题:

  • 客户端回传的 assistant 消息中只携带了 tool_calls,缺失了 reasoning_content 字段

  • 上游 API 校验不过,直接返回 400 Bad Request

  • 一旦代理服务重启,内存缓存全部清空,历史消息全量触发降级逻辑,工具调用链彻底断裂


二、根因分析

问题的根源出在 inject_reasoning 函数的降级处理逻辑上:

  • 该函数会扫描整个会话历史,为每条消息注入推理内容

  • 缓存未命中时,降级策略不是保留原始结构,而是直接将 tool_calls 删除,替换为一句话 "[调用了 read]"

  • 这个破坏性操作并不局限于当前轮次——历史里所有的 tool_calls 全部遭殃

  • 降级后的消息从语义上彻底改变:原本是“调用某个工具”,现在变成“说了一句关于调用工具的话”,协议语义被完全破坏

  • 在 Anthropic 路径下情况更严重:连 tool_result 也一并被转成纯文本,进一步加剧了协议不一致

一句话总结:缓存 miss 时的降级策略,把结构化的工具调用链路暴力碾成了自然语言。


三、第一版修复:填充空推理字段

修复思路很直接——既然上游要求这个字段必须存在,那就给一个空值,但保留原始结构

  • 缓存 miss 时,将 msg["reasoning_content"] 赋值为空字符串 ""保留 tool_calls 不动

  • 字段存在 → 上游校验通过,不再 400

  • tool_calls 完整 → 工具调用链不断

  • 代价仅仅是当前轮的推理能力略有减弱,但整个流程完全可用

这是一个典型的“以最小代价换取系统稳定”的务实方案。


四、第二版:SQLite 持久化缓存

内存缓存的致命缺陷是重启即丢,因此引入 SQLite 作为持久化层:

  • WAL 模式(Write-Ahead Logging):读写可并发,性能远优于传统日志模式

  • 每次写入立即 commit:不依赖 shutdown hook,突然断电也不丢数据

  • 两张核心表设计:

    • cache:以内容哈希(hash)为 key,存储对应的推理内容(reasoning)

    • tc_index:以 tool_call_id 为索引,建立工具调用与推理内容的映射关系

  • tool_call_id 索引的精妙之处:即使消息文本不同,只要 tool_call_id 匹配,就能命中对应的推理内容,大幅提升缓存复用率


五、第三版:Tiered Cache 两级缓存架构

在前两版基础上,最终演进为完整的两级缓存架构:

  • L1 内存层:基于 LRU 的热数据缓存,查询延迟微秒级

  • L2 SQLite 层:冷数据缓存,磁盘持久化,重启不丢

  • 查询路径:内存 → SQLite → miss,逐层穿透

  • 写入路径:同时写入两层,保证一致性

  • 冷数据自动提升:SQLite 命中时,将数据加载回 L1 热层,加速后续访问

  • LRU 淘汰不丢数据:内存满时淘汰最久未使用的条目,但数据仍安全存储在 SQLite

  • 重启友好:内存清空后,热数据随着访问自然重新积累,无需预热


六、淘汰算法设计

三个层级各司其职:

  • MemoryCacheOrderedDict 实现 LRU + TTL 过期机制,访问即移到队尾

  • SQLiteCache:写入时检查 TTL 自然过期,超出 max_size 时自动驱逐最旧记录

  • TieredCache:内存层 max_size 控制热数据规模,SQLite 层容量设为 max_size * 5,为冷数据提供充裕的存储空间

  • 运行时热修改:通过仪表盘可动态调整 max_size 和 TTL,无需重启服务


七、性能与可靠性保障

  • SQLite WAL 模式:支持读写并发,不阻塞查询

  • 线程安全:使用 threading.local 确保每个线程拥有独立的连接,避免锁竞争

  • 断电安全:每次写入立即 commit,不依赖进程正常退出

  • 性能对比:内存层查询微秒级,SQLite 层查询毫秒级,对比网络请求的数百毫秒延迟,缓存层的开销几乎可忽略不计


八、仪表盘集成

将缓存管理能力可视化,方便运维监控:

  • 缓存统计:实时展示 size / max / ttl / tc_index 等关键指标

  • 缓存设置:支持在线调整内存上限、数据库上限、TTL 等参数

  • 清空缓存:一键清空,不影响上游服务列表,操作安全可控


九、代码结构概览

text

src/
├── cache.py      # MemoryCache / SQLiteCache / TieredCache / create_cache 工厂函数
├── proxy.py      # inject_reasoning / save_reasoning / UpstreamError 异常定义
└── config.py     # CacheConfig(backend / db_path / max_size / ttl 等配置项)

结构清晰,职责分明,易于扩展和维护。


十、总结

这套方案实现了三个关键跨越:

  • 从“重启即丢”到“重启不丢”:SQLite 持久化让历史推理数据有了安身之所

  • 从“降级破坏协议”到“填充空字段保协议”:用最小代价维护了工具调用链路的完整性

  • 从“单层缓存”到“两级热冷分离”:内存热层保性能,磁盘冷层保持久,各得其所

更重要的是,整个方案零外部依赖——SQLite 是 Python 标准库的一部分,无需引入 Redis 或其他中间件,部署和维护成本极低。对于跑在边缘设备或个人服务器上的代理服务而言,这种“内置即够用”的架构思路尤为可贵。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐