用IndexedDB做AI对话离线缓存实战

一句话结论:AI 对话历史别只存内存,刷新就没了;也别一股脑塞 localStorage,几十轮对话很快撑爆它那 5MB。IndexedDB 才是对的容器------异步、容量大(几百 MB)、能按会话分库查。我给一个移动端 AI 助手加离线缓存,用的就是它。

为什么非它不可?AI 对话有三个特点:消息多、单条可能很长(贴代码贴文档)、用户会回看历史。localStorage 同步读写会卡主线程,而且 JSON.stringify 整个历史每次写,数据一大就肉眼可见地顿。

封装一个极简的存取层

不引库,原生 IndexedDB 够用,就是 API 啰嗦。我封了几个 Promise 函数:

ini 复制代码
function openDB() {
  return new Promise((res, rej) => {
    const req = indexedDB.open('ai-chat', 1);
    req.onupgradeneeded = (e) => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains('messages')) {
        const store = db.createObjectStore('messages', { keyPath: 'id' });
        store.createIndex('byConv', 'convId', { unique: false });
      }
    };
    req.onsuccess = () => res(req.result);
    req.onerror = () => rej(req.error);
  });
}

async function saveMessage(msg) {
  const db = await openDB();
  return new Promise((res, rej) => {
    const tx = db.transaction('messages', 'readwrite');
    tx.objectStore('messages').put(msg);
    tx.oncomplete = res;
    tx.onerror = () => rej(tx.error);
  });
}

keyPath: 'id' 让每条消息按自己的 id 做主键,byConv 索引让我能按会话 id 批量查。

按会话拉历史

回看某个会话时,走索引一次性拿出来:

ini 复制代码
async function loadConv(convId) {
  const db = await openDB();
  return new Promise((res) => {
    const tx = db.transaction('messages', 'readonly');
    const idx = tx.objectStore('messages').index('byConv');
    const out = [];
    idx.openCursor(IDBKeyRange.only(convId)).onsuccess = (e) => {
      const cur = e.target.result;
      if (cur) { out.push(cur.value); cur.continue(); }
      else res(out.sort((a, b) => a.ts - b.ts));
    };
  });
}

游标遍历完再按时间戳排序。IndexedDB 不保证返回顺序跟插入顺序一致,这个 sort 不能省------我省过一次,历史消息顺序乱了,被测试逮到。

流式消息怎么存?边流边存还是流完再存

这是真正的坑。AI 回复是一个 token 一个 token 流回来的。如果每个 chunk 都写一次 IndexedDB,几百次事务把性能拖垮;如果只在流结束写,中途用户切走应用、回来发现这条没了。

我的折中:内存里累积,每 500ms 落一次盘,流结束再补一次:

javascript 复制代码
function streamSink(msgId, convId) {
  let buf = '';
  let timer = null;
  return {
    push(chunk) {
      buf += chunk;
      if (!timer) timer = setTimeout(flush, 500);
    },
    done() { clearTimeout(timer); timer = null; flush(); }
  };
  function flush() {
    timer = null;
    saveMessage({ id: msgId, convId, role: 'ai', content: buf, ts: Date.now() });
  }
}

500ms 这个数我调过,太短了写太频繁,太长了崩溃丢得多。500 是体感平衡点。

一个没绕过去的小缺点

Safari 的隐私模式下 IndexedDB 可能直接抛错或静默失败,我加了降级:开库失败就退回纯内存模式,功能不挂只是刷新丢历史。这部分代码不优雅,但总比白屏强:

ini 复制代码
let dbAvailable = true;
openDB().catch(() => { dbAvailable = false; });

后端对话能力我是在一个能拖拽搭智能体、不用写后端的平台上配的,前端只管把消息落进 IndexedDB。模型 API 直连讯飞 MaaS,没自己起服务。

相关推荐
sarasuki10 分钟前
MCP 客户端接入:一行注册一个 GitHub 工具
人工智能·agent·mcp
kolyle13 分钟前
万级 QPS 下的 Token 分发系统架构:从 0 到 1 跑通 AI 时代的“水电煤“
开发语言·人工智能·系统架构·token·qps·极智词元·大模型私有化部署
古少侠43 分钟前
deepseek转word工具怎么选?DS随心转与4种方案对比实测
人工智能·word·powerpoint
Code_Artist1 小时前
☢︎自然语言 → 机器码:这到底是 AI 编程的终极形态,还是一个伪命题?
人工智能·llm·ai编程
老纪的技术唠嗑局1 小时前
Agent 习惯性删库跑路,数据库纷纷学 Git 续命
数据库·人工智能
开发笔记-阿牛1 小时前
做工业报警器语音提示,CK6159A 为什么更合适?
人工智能·stm32·单片机·嵌入式硬件·音频
dehuisun1 小时前
第 06 篇:RAG 混合召回策略:向量检索 + ES 关键词 + Rerank 重排
人工智能
DeepAgent1 小时前
AI Agent 面试篇(01):AI 岗位面试到底怎么走——从网申到 Offer 的完整流程
人工智能·面试·职场和发展
程序员清风1 小时前
LangGraph 入门:用状态机设计可靠的 Agent 工作流
人工智能·python
南京兴帝文化传媒有限公司1 小时前
本地生活服务GEO优化实战:宁国花店地图关键词布局带来订单翻倍的技术路径
大数据·人工智能·生活·geo 优化·geo优化避坑·ai搜索获客