Rust+SQLite:为AI应用构建持久化缓存存储层
做 AI 应用开发,数据落地的坑迟早要踩一遍。模型返回的 embedding 结果、Agent 的会话记忆、用户侧的配置状态,这些数据如果只扔在内存 HashMap 里,进程一重启就全没了,日志里除了报错什么都留不下来。前面课程里我们留了一个 store 的抽象层,当时它还是个空壳子,这一课我来给它填上真实现——用 SQLite 做持久化和本地缓存,让 Rust 这边真正拥有一个能扛得住生产使用的存储层。
这课的内容很简单:先把为什么选 SQLite 这件事讲透,再对比 Rust 生态里几套接入方案,然后从建表、迁移、并发控制一直讲到性能实测,最后落到一个真实 AI 场景里跑通全流程。适合正在做 AI Agent、本地知识库、RAG 管道,又不想为“存几个缓存文件”就引入一堆基础设施的同学。你会拿到一份可以直接抄走的 Rust + SQLite 实现,不只是增删改查,还包括缓存过期、批量写入、WAL 并发这些真正影响体验的细节。
1. 为什么是 SQLite:AI 场景下的存储选型逻辑
1.1 先想清楚 store 到底在存什么
很多 AI 项目的存储需求,一开始看起来简单,等做深了才发现根本不是那么回事。以常见的 AI Agent 应用为例,需要持久化的东西至少包括这几类:
- 聊天会话的上下文,包括用户输入、工具调用记录、模型响应。这类数据访问模式是顺序追加、按会话读取,天然适合用带主键的表存储。
- embedding 向量的缓存。重复文本做向量化很贵,无论是调用外部 API 还是本地跑模型,都会消耗时间和算力。缓存命中率一旦上去,延迟能降低一个数量级。
- 模型调用的元信息。比如某个 prompt 在什么参数下返回了什么结果,这既是缓存,也是审计日志,还要支持后续离线分析。
- 用户配置和功能开关。这类数据量很小,但要求可靠,断电不丢,重启能恢复。
这些需求有一个共同点:单机、数据量不大(十万条级别)、结构多样。如果为这个场景引入 MySQL 或 Redis,还得配服务器、配集群、维护网络连接,对一个本地运行、打包成单一二进制的 AI 工具来说,运维成本太高了。
1.2 SQLite 凭什么适合 AI 开发落地
SQLite 最核心的优势,是它把所有数据存成一个普通的单文件。这个特性放在 AI 应用场景里,几乎是量身定做的。
想想看,模型文件、配置文件、缓存文件本来就在本地,SQLite 只是把其中一部分变成了有索引、支持事务的表。用户把一个应用目录从一台机器拷贝到另一台机器,整个数据库就跟着过去了,不需要导出导入,不需要服务进程,连端口都不用开。这一点对桌面 AI 工具和本地推理服务特别关键。
除了文件化,SQLite 还有一个很实惠的优点:零运维。没有独立的数据库进程,连接就是打开文件,崩溃恢复靠自带的事务日志,日常维护靠几条 PRAGMA 命令。对 AI 工程师来说,这意味着不需要养一个 DBA,也不需要在部署脚本里操心数据库服务有没有起来。它支持标准 SQL,事务语义完整,读多写少时并发表现相当不错,到了百万行级别只要索引设计合理依然能保持毫秒级响应。对绝大多数 AI 应用的持久化需求,SQLite 不仅够用,而且是性能最优解。
1.3 持久化与缓存的一体两面
这里有个容易混淆的点:持久化和缓存经常被当成两件事单独设计,其实在本地存储场景里,它们完全可以共用同一张表。
持久化的核心诉求是“写入后不丢”,缓存的核心诉求是“读的时候快”,但在单机 AI 应用里,历史聊天记录和 embedding 缓存本来就住在同一个 SQLite 文件里。把缓存过期时间(TTL)作为表的一列,写入时带上截止时间戳,读取时用 WHERE 条件过滤过期数据,这样一套逻辑同时完成了持久化和缓存淘汰。按这个思路设计,store 不用拆成两个模块,整个存储层只有一张表、一套访问接口、一套清理策略。
关键是要想清楚哪些数据可以给 TTL、哪些必须永久保留。我的标准很简单:可重算的派生数据(embedding、重试结果)给 TTL,原始数据(用户消息、配置)不给 TTL。把这两类混在一张表里,只要 expires_at 允许为 NULL,就能区分“永久”和“临时”,很方便。
2. Rust 侧方案选型:rusqlite 还是 sqlx
2.1 两个主流 crate 的底层差异
Rust 生态里接 SQLite 最常用的两个库,一个叫 rusqlite,一个叫 sqlx。它们代表了两种完全不同的设计哲学。
rusqlite 是对 SQLite C 接口的直接绑定,同步 API,用起来非常直白:打开连接,执行 SQL,拿结果。因为绑定了官方 C 库,很多 SQLite 特性都能直接用,比如 PRAGMA 设置、自定义函数、批量 statement。它不依赖异步运行时,你可以在多线程里用 Mutex 包住连接,或者干脆在需要的地方 new 一个连接出来,心智模型简单。
sqlx 走的是 async 路线,本身不直接绑定 SQLite C 库,而是通过 libsqlite3-sys 链接系统库或自带编译的 SQLite,同时提供了一套编译期检查 SQL 的宏。项目里如果用 tokio 做异步运行时,sqlx 和整体架构融合更顺,连接池、事务这些功能开箱即用。代价是编译时间更长,抽象更重,排错时要穿透的层数也更多。
选哪个,取决于项目现状。如果是一个已经用 tokio 跑异步推理的 AI 服务,想跟异步主循环保持一致的风格,sqlx 会很舒服。如果只是想在 Rust 二进制里内嵌一个可靠的本地存储,不想引入 async 依赖,rusqlite 是更轻的选择。这一课的实现我选择 rusqlite,因为此前的课程里 store 接口是同步的,而且本地缓存这个场景根本不需要异步,减少依赖层级就是减少潜在问题。
2.2 选型决策参考表
我把判断逻辑整理成一个简单表格,可以照着这个对标自己的项目情况:
| 项目特征 | 推荐方案 | 原因 |
|---|---|---|
| 项目已接入 tokio,需要连接池 | sqlx | async 集成好,连接池自带 |
| 只需要本地单文件存储,追求简单 | rusqlite | 同步调用直观,零多余依赖 |
| 需要编译期 SQL 校验,防手写错误 | sqlx | query! 宏能在编译期抓 SQL 错误 |
| 需要 SQLite 特有的 PRAGMA、自定义函数 | rusqlite | 底层绑定完整,功能裸露无封装 |
| 想快速验证一个 AI 项目原型 | rusqlite | 代码量最少,改起来快 |
| 要跑在 WebAssembly 或其他受限环境 | rusqlite | 编译配置更灵活,支持目标更多 |
这个表不是说你只能二选一,真实项目里两个 crate 混用也不少见。不过刚开始做持久化时,我建议只选一个,先把稳定的路径跑通,再考虑抽象层面的花活。
2.3 关于依赖与编译这档子事
Rust 里接 SQLite,被问次数最多的其实是编译问题。rusqlite 默认会尝试链接系统里的 libsqlite3,如果系统版本过旧,会出现运行时报错。保险做法是打开 bundled feature,让它自动编译内置的 SQLite 源码: