1. 项目概述为什么一个前端工程师要在第16天突然“闯入”数据库世界“前端转 AI 100 天”这个标题本身就像一道分水岭——它不是在讲“如何用 React 写个聊天框”而是在标记一个真实职业转型的刻度从 DOM 操作者走向具备系统级认知的智能体构建者。Day 16 这个时间点很关键前15天大概率已跑通了 LLM API 调用、Prompt 工程基础、本地模型加载比如 Ollama Llama3甚至可能搭出了一个能“自问自答”的简易 Agent 框架。但很快就会撞上那堵看不见的墙每次对话重启Agent 就像失忆病人前一秒说“我叫小张”后一秒又问“你叫什么名字”这根本不是模型能力问题而是架构缺陷。LLM 本身没有状态存储能力它的“上下文窗口”只是临时缓存不是持久化记忆。你喂给它的 8K token关掉页面就清零你让它记住用户偏好、历史订单、设备配置它只能靠你手动拼进 prompt——这不仅效率极低更会快速耗尽上下文额度导致关键信息被截断。真正的“记忆”必须脱离 prompt下沉到独立、可靠、轻量、可查询的数据层。SQLite 就是此刻最精准的解药。它不是“数据库入门”的泛泛之谈而是直指 Agent 构建中那个最痛的刚需给无状态的 AI 加一个有状态的、嵌入式的、零运维的“脑区”。它不依赖服务端进程单个 .db 文件就是完整数据库它支持标准 SQL意味着你可以用SELECT * FROM memories WHERE user_id ? AND tag preference精准召回而不是在一堆 JSON 字符串里正则匹配它被 Node.js、Python、甚至 Electron 应用原生支持前端工程师用better-sqlite3或sqlite3包三行代码就能初始化连接。这不是在学“数据库原理”这是在给你的 Agent 安装第一块可写入的“海马体”。我试过用纯文件系统JSON 文件模拟记忆每次写入都重写整个文件用户一多就卡死也试过内存 Map进程一崩全归零。直到把 SQLite 嵌进一个 Next.js App Router 的 Server Action 里让 Agent 在generateResponse()前自动查user_memories表、在响应后自动插入conversation_log行那种“它真的记住了”的实感比第一次调通 OpenAI API 还让人头皮发麻。所以 Day 16 不是“学数据库”而是前端工程师亲手为 AI 装上第一颗可持久化、可检索、可演化的记忆细胞——这才是转 AI 最硬核的第一步落地。2. 核心设计思路为什么选 SQLite 而不是 MySQL、PostgreSQL 或向量数据库2.1 从 Agent 架构视角看“记忆库”的四重约束给 Agent 设计记忆库不能套用传统 Web 应用的数据库选型逻辑。我拆解出四个刚性约束它们像四把尺子直接筛掉了 90% 的选项嵌入式EmbeddedAgent 很可能运行在用户本地桌面 App、边缘设备树莓派、或 Serverless 环境Vercel Edge Functions。它不能依赖一个需要单独部署、维护、监控的数据库服务进程。MySQL 启动要 100MB 内存PostgreSQL 需要 systemd 服务管理——这对一个想“开箱即用”的 Agent 来说是灾难性的耦合。零配置Zero-Config前端工程师最怕“先装环境”。你不会为了存几条对话记录去配 MySQL 的 my.cnf、开防火墙端口、建用户权限。SQLite 就是一个.db文件npm install better-sqlite3后new Database(./memories.db)一行搞定。连CREATE TABLE都可以放在首次连接时自动执行。ACID 事务保障AtomicityAgent 的记忆操作常是复合动作。比如一次用户提问后你要同时做三件事① 插入新对话记录② 更新用户偏好表如“用户上次说喜欢简体中文”③ 删除过期的临时缓存。这三步必须原子性完成否则数据错乱。SQLite 的 WAL 模式在单文件下提供强 ACID而很多轻量级键值库LevelDB、RocksDB只保证最终一致性。结构化查询能力Structured QueryAgent 的记忆不是“扔进去就完事”。你需要按条件召回“找出用户过去 7 天所有关于‘报销流程’的提问”、“合并所有带#travel标签的行程建议”。这要求WHERE、JOIN、ORDER BY、GROUP BY—— 键值对KV或文档数据库MongoDB的模糊查询太弱向量数据库Chroma、Pinecone又过度设计你不需要语义相似度只需要精确匹配user_id123 AND categoryorder。提示别被“向量数据库很火”带偏。向量库解决的是“用户问‘怎么修打印机’我该召回哪篇技术文档”本质是语义检索而 Agent 记忆解决的是“用户说‘把上周五的报销单发我’我该精准定位那条created_at BETWEEN 2024-04-01 AND 2024-04-07 AND typereimbursement的记录”。前者是“找相似”后者是“找确定”。2.2 SQLite 如何完美命中这四把尺子约束维度SQLite 实现方式对比 MySQL/PostgreSQL对比向量数据库对比纯文件/内存嵌入式单文件无服务进程C 语言库直链需独立 daemon 进程端口监听需独立服务或云 API文件需手动锁内存无持久化零配置无需安装服务无配置文件new Database()即创建需mysqld --initialize配my.cnf需docker run或云控制台创建实例JSON 文件需手动fs.writeFileSync易崩溃丢失ACID 事务WAL 模式下支持多语句原子事务BEGIN IMMEDIATE防写冲突支持但需显式START TRANSACTION多数不支持跨向量操作的原子事务文件写入无法回滚内存 Map 无事务概念结构化查询完整 SQL-92 标准支持复杂 JOIN、子查询、全文索引FTS5同样强大但大材小用仅支持query_embeddings无 WHERE 逻辑JSON 需Array.filter()性能差且无法索引我实测过一个场景在 Next.js App Router 中一个 Server Action 处理用户提交的“设置默认语言为英文”。代码需① 查当前用户 ID② 更新user_profiles表的default_lang字段③ 插入一条audit_log记录。用 SQLite 的事务封装// 使用 better-sqlite3 const stmt db.transaction((userId: number, lang: string) { db.prepare(UPDATE user_profiles SET default_lang ? WHERE id ?).run(lang, userId); db.prepare(INSERT INTO audit_log (user_id, action, timestamp) VALUES (?, ?, ?)) .run(userId, update_language, new Date().toISOString()); }); stmt(123, en-US);这三行在一个原子事务中执行。如果第二步插入日志失败第一步的更新自动回滚。换成 JSON 文件你得自己实现文件锁、备份、错误恢复——这已经超出一个前端工程师该写的代码范畴。2.3 为什么不是其他嵌入式方案LevelDB、RocksDB、DuckDB 的取舍有人会问LevelDB 更快DuckDB 支持 SQL 且列式存储我的答案很直接对 Agent 记忆场景SQLite 是唯一平衡点。LevelDB/RocksDB纯 KV没有 SQL。你想查“用户 123 的所有订单”得遍历所有 keyorder_123_20240401,order_123_20240402...无法用WHERE user_id 123一句解决。前端工程师写业务逻辑时得自己维护 key 的命名规范、序列化反序列化心智负担远超 SQL。DuckDB确实是惊艳的分析型嵌入式 DB但它是为 OLAP联机分析处理设计的擅长SELECT COUNT(*) FROM huge_table GROUP BY category。Agent 记忆是 OLTP联机事务处理高频、小数据量、强一致性、单行读写。DuckDB 的写入延迟比 SQLite 高 3-5 倍实测 10ms vs 2ms且不支持 WAL 模式下的高并发写入——当多个 Agent 并行处理请求时SQLite 的BEGIN IMMEDIATE能优雅排队DuckDB 可能直接报错。SQL.jsWebAssembly 版 SQLite适合纯浏览器环境但限制明显。它无法直接访问本地文件系统浏览器沙箱.db文件必须通过input typefile上传或存在 IndexedDB且所有操作在主线程阻塞 JS。而 Node.js 环境Next.js Server Actions、Electron 主进程用原生better-sqlite3性能提升 10 倍以上且可无缝对接fs模块。所以结论很清晰如果你的 Agent 运行在 Node.js 环境95% 的前端转 AI 场景SQLite 是不可替代的“记忆基座”。它不是“将就”而是经过十年以上工业验证的、最贴合轻量级智能体需求的存储引擎。3. 核心细节解析SQLite 记忆库的表结构设计与字段深意3.1 四张核心表为什么是这四张每张表解决什么具体问题一个健壮的 Agent 记忆库绝不是一张messages表就完事。我基于半年来十几个 Agent 项目的实战提炼出四张必建表。它们不是凭空想象而是对应 Agent 运行时的真实数据流表名核心职责典型使用场景为什么必须独立conversations存储一次完整对话的元信息“用户开启新会话”、“结束当前会话”分离会话生命周期管理避免在messages表里冗余存储session_idmessages存储每条消息的详细内容与时序“显示聊天记录”、“按时间倒序加载”消息是原子单位需独立索引conversation_id和created_atuser_profiles存储用户的长期静态偏好“用户设置默认语言”、“记住邮箱地址”用户属性极少变更高频读取独立表便于缓存和权限控制memory_facts存储从对话中提取的、可复用的结构化事实“用户说‘我公司叫星辰科技’→ 提取 company_name星辰科技”事实需跨会话复用且常被多个 Agent 模块如身份识别、知识图谱引用注意不要试图用一张表如agent_memory存所有东西。我见过用type字段区分“消息/用户/事实”的设计结果WHERE type user_profile查询慢如蜗牛因为没索引UPDATE时还容易误改其他类型数据。分表是数据库设计的基本功不是过度设计。3.2messages表字段详解每一列都是为 Agent 行为埋下的伏笔这张表是记忆库的心脏字段设计直接影响 Agent 的智能程度。以下是我推荐的最小完备字段集基于 SQLite 的INTEGER PRIMARY KEY AUTOINCREMENT作为隐式 rowidCREATE TABLE messages ( id INTEGER PRIMARY KEY, -- SQLite 隐式 rowid无需额外声明但显式写出更清晰 conversation_id INTEGER NOT NULL, -- 关联 conversations 表标识归属会话 role TEXT NOT NULL CHECK(role IN (user, assistant, system)), -- 角色严格限定值域 content TEXT NOT NULL, -- 消息正文UTF-8 完全支持 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 时间戳用于排序、过期清理 metadata_json TEXT, -- JSON 字符串存动态字段{ model: llama3, tokens: 123 } is_edited BOOLEAN DEFAULT FALSE, -- 标记是否被用户编辑过如修正错别字 parent_id INTEGER, -- 指向上一条消息 id构建消息树支持追问、修正 FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE );role字段的CHECK约束看似多此一举实则关键。Agent 的 Prompt 通常依赖role: user/assistant的严格格式。如果某条数据role humanLLM 解析时可能出错。CHECK在数据库层强制校验比在应用层if (role ! user role ! assistant) throw更可靠。metadata_json字段的妙用不建单独的message_metadata表是因为这些字段高度稀疏90% 的消息不需要tokens只有调试时才记录。JSON 字符串灵活且 SQLite 3.38 原生支持json_extract(metadata_json, $.model)查询。实测比建 5 张扩展表快 3 倍。parent_id字段构建消息树这是让 Agent 支持“上下文感知追问”的基石。用户问“上一条说的报价单能发 PDF 吗”Agent 需通过parent_id快速定位上一条消息可能是“报价单已生成”再调用 PDF 生成函数。没有它Agent 只能机械地按时间倒序找“最近一条”极易出错。ON DELETE CASCADE级联删除当conversations表某条会话被删除如用户点击“清空历史”所有关联的messages自动清除。不用写DELETE FROM messages WHERE conversation_id ?减少应用层代码杜绝漏删。3.3memory_facts表Agent 的“知识图谱”起点这是最容易被忽略却最具潜力的一张表。它把非结构化的对话转化为 Agent 可复用的结构化知识CREATE TABLE memory_facts ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, -- 所属用户 fact_key TEXT NOT NULL, -- 事实键如 company_name, preferred_contact fact_value TEXT NOT NULL, -- 事实值如 星辰科技, email confidence REAL DEFAULT 1.0, -- 置信度0.0-1.0来自 LLM 提取时的 self-evaluation source_message_id INTEGER, -- 来源消息 id便于溯源验证 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, fact_key), -- 每个用户每个键唯一自动覆盖旧值 FOREIGN KEY (source_message_id) REFERENCES messages(id) );UNIQUE(user_id, fact_key)是灵魂确保“用户 123 的公司名”永远只有一条记录。当新对话中用户说“我公司改名叫银河科技了”Agent 提取后插入(company_name, 银河科技)SQLite 自动替换旧值。无需UPDATE ... WHERE user_id ? AND fact_key ?简化逻辑。confidence字段驱动智能决策LLM 提取事实时可要求其输出置信度如confidence: 0.92。Agent 在后续回答中若confidence 0.7可主动确认“您之前提到公司名是‘星辰科技’请问现在是否已更改为‘银河科技’”——这比盲目相信更人性化。source_message_id支持审计当用户质疑“你为什么说我公司叫星辰科技”Agent 可直接查出这条事实来自哪条原始消息如messages.id 456并展示原文“您在 2024-04-01 的对话中说‘我是星辰科技的张经理’”。我曾用这张表实现一个“客户跟进 Agent”它自动从每次对话中提取next_followup_date下次跟进日期存入memory_facts。每天凌晨一个简单的SELECT * FROM memory_facts WHERE fact_key next_followup_date AND fact_value date(now)就能拉出今日待办列表触发邮件提醒。这就是结构化记忆带来的自动化能力。4. 实操过程从零搭建一个可运行的 Agent 记忆库Next.js TypeScript4.1 环境准备Node.js 与 SQLite 驱动的精准选择绝对不要用sqlite3npm 包。它基于 Node.js 的node-gyp编译Windows 用户常卡在 Python 环境、VS Build Tools 上Mac M1/M2 用户也常因架构问题编译失败。我踩过太多坑最终锁定better-sqlite3—— 它是纯 C 编写、预编译二进制包npm install10 秒完成兼容所有平台。# 推荐命令Next.js 14 App Router 环境 npm install better-sqlite3 # 如果用 TypeScript还需类型定义 npm install -D types/better-sqlite3注意better-sqlite3是同步 APIdb.prepare(...).run()这在 Server Actions 中是优势——无需await代码更线性。但它不能在浏览器中运行无 Node.js fs 模块。如果你需要纯前端记忆如 PWA 离线模式请用sql.jsWebAssembly 版但本文聚焦主流 Node.js 场景。4.2 数据库初始化单例模式与首次运行自动建表在 Next.js 中数据库连接必须是单例Singleton避免多次new Database()创建多个连接导致 WAL 日志冲突。我把它封装成一个db.ts模块// lib/db.ts import Database from better-sqlite3; import path from path; // __dirname 在 ES Module 中不可用用 import.meta.url 替代 const dbPath path.join(process.cwd(), data, agent_memory.db); // 确保 data 目录存在 const fs require(fs); if (!fs.existsSync(path.dirname(dbPath))) { fs.mkdirSync(path.dirname(dbPath), { recursive: true }); } // 创建单例 const db new Database(dbPath); // 启用 WAL 模式提升并发写入性能 db.pragma(journal_mode WAL); // 创建表仅首次运行 db.exec( CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY, title TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY, conversation_id INTEGER NOT NULL, role TEXT NOT NULL CHECK(role IN (user, assistant, system)), content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, metadata_json TEXT, is_edited BOOLEAN DEFAULT FALSE, parent_id INTEGER, FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_profiles ( id INTEGER PRIMARY KEY, user_id INTEGER UNIQUE NOT NULL, preferences_json TEXT, -- { language: zh-CN, theme: dark } updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS memory_facts ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, fact_key TEXT NOT NULL, fact_value TEXT NOT NULL, confidence REAL DEFAULT 1.0, source_message_id INTEGER, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, fact_key), FOREIGN KEY (source_message_id) REFERENCES messages(id) ); ); // 创建索引大幅提升查询速度 db.exec( CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_messages_time ON messages(created_at); CREATE INDEX IF NOT EXISTS idx_memory_facts_user_key ON memory_facts(user_id, fact_key); ); export default db;关键细节说明process.cwd()获取当前工作目录Next.js 项目根目录data/agent_memory.db是推荐路径避免写入node_modules或app/目录。db.pragma(journal_mode WAL)是性能关键。默认 DELETE 模式下并发写入会锁整个数据库WAL 模式允许多个读者 一个写者实测 10 并发请求下响应时间稳定在 5ms 内。CREATE INDEX语句必须显式添加。SQLite 不会自动为外键创建索引WHERE conversation_id ?查询若无索引会全表扫描10 万条消息时耗时从 2ms 暴涨到 200ms。4.3 Agent 记忆读写封装一个函数搞定“记住”与“回想”有了数据库下一步是让 Agent 代码能自然调用。我设计了两个核心函数remember()和recall()它们隐藏了所有 SQL 细节暴露的是语义化接口// lib/memory.ts import db from ./db; // 记住一条消息用于 Agent 响应后存档 export function rememberMessage( conversationId: number, role: user | assistant | system, content: string, metadata: Recordstring, any {}, parentId?: number ) { const stmt db.prepare( INSERT INTO messages (conversation_id, role, content, metadata_json, parent_id) VALUES (?, ?, ?, ?, ?) ); return stmt.run(conversationId, role, content, JSON.stringify(metadata), parentId); } // 回想最近 N 条消息用于构建 Prompt 的上下文 export function recallMessages( conversationId: number, limit: number 10, offset: number 0 ) { const stmt db.prepare( SELECT id, role, content, created_at, metadata_json, parent_id FROM messages WHERE conversation_id ? ORDER BY created_at DESC LIMIT ? OFFSET ? ); const rows stmt.all(conversationId, limit, offset) as Array{ id: number; role: string; content: string; created_at: string; metadata_json: string; parent_id: number | null; }; // 自动解析 JSON 字段 return rows.map(row ({ ...row, metadata: row.metadata_json ? JSON.parse(row.metadata_json) : {}, })); } // 记住一个用户事实用于从对话中提取关键信息 export function rememberFact( userId: number, factKey: string, factValue: string, confidence: number 1.0, sourceMessageId?: number ) { const stmt db.prepare( INSERT OR REPLACE INTO memory_facts (user_id, fact_key, fact_value, confidence, source_message_id, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ); return stmt.run(userId, factKey, factValue, confidence, sourceMessageId); } // 回想某个用户事实用于个性化响应 export function recallFact(userId: number, factKey: string) { const stmt db.prepare( SELECT fact_value, confidence, updated_at FROM memory_facts WHERE user_id ? AND fact_key ? ); return stmt.get(userId, factKey) as { fact_value: string; confidence: number; updated_at: string; } | undefined; }为什么这样设计rememberMessage()的parentId参数是可选的符合实际用户首条消息无父节点Agent 的回应可设为parentId 用户消息 id用户追问时再设为parentId Agent 上条回应 id。recallMessages()返回metadata已解析为对象调用方直接msg.metadata.model即可不用每次都JSON.parse()。rememberFact()用INSERT OR REPLACE代替UPSERTSQLite 3.24 支持兼容更老版本且语义更清晰——就是“有则更新无则插入”。4.4 集成到 Next.js Server Action让 Agent 真正“活”起来现在把记忆库注入到真实的 Agent 流程中。假设你有一个app/actions/generateResponse.tsuse server; import { rememberMessage, recallMessages, rememberFact } from /lib/memory; import { generateLLMResponse } from /lib/llm; // 你的 LLM 调用函数 export async function generateResponse( conversationId: number, userId: number, userInput: string ) { // 步骤1记住用户输入 rememberMessage(conversationId, user, userInput); // 步骤2构建上下文召回最近 8 条避免超长 const contextMessages recallMessages(conversationId, 8); // 步骤3调用 LLM传入 contextMessages 和 userInput const llmResponse await generateLLMResponse({ messages: contextMessages, userInput, }); // 步骤4记住 Agent 回应 rememberMessage( conversationId, assistant, llmResponse.content, { model: llmResponse.model, tokens: llmResponse.tokens } ); // 步骤5尝试从回应中提取事实示例检测到公司名 if (llmResponse.content.includes(公司名)) { const companyName extractCompanyName(llmResponse.content); // 你的提取函数 if (companyName) { rememberFact(userId, company_name, companyName, 0.85); } } return { content: llmResponse.content, conversationId }; } // 辅助函数简单提取实际可用 LLM 提取或正则 function extractCompanyName(text: string): string | null { const match text.match(/公司名[:\s]([^\n])/i); return match ? match[1].trim() : null; }实操心得上下文长度控制是生命线recallMessages(conversationId, 8)不是随意定的。我测试过LLMLlama3-8B在 4K context 下召回 10 条消息平均耗时 120ms召回 20 条因 SQLite 查询变慢 LLM 解析压力耗时飙升至 350ms且易出现 token 截断。8 条是性能与效果的黄金平衡点。事实提取要“保守”extractCompanyName只是示意。生产环境强烈建议用小型 LLMPhi-3做专门的事实抽取或用规则 LLM 双校验。宁可漏提不可错提——错误事实一旦写入memory_facts会污染后续所有回答。错误处理必须加真实代码中rememberMessage()等调用需包裹try/catch捕获db抛出的SqliteError并记录日志。我见过因磁盘满导致INSERT失败Agent 却静默返回空响应用户以为功能坏了。4.5 本地开发与调试技巧让 SQLite 成为你最顺手的“记忆探针”SQLite 的最大优势是“所见即所得”。.db文件就是数据库你可以用任何 SQLite GUI 工具如 DB Browser for SQLite 直接打开实时查看、编辑、查询数据。这是调试 Agent 记忆的终极武器。我的调试三板斧实时监控表变化在 DB Browser 中打开data/agent_memory.db切换到Browse Data标签页勾选Auto-refresh。每当你在 Next.js 中调用rememberMessage()表格立刻刷新看到新行插入。比 console.log 看日志直观 10 倍。手写 SQL 验证逻辑当 Agent 表现异常如“为什么没记住我的邮箱”直接在 DB Browser 的Execute SQL标签页写SELECT * FROM memory_facts WHERE user_id 123 AND fact_key email;如果无结果说明rememberFact()没被调用如果有结果但fact_value是空说明提取逻辑有 bug。5 秒定位问题。模拟极端场景在 DB Browser 中手动DELETE FROM messages清空消息或UPDATE user_profiles SET preferences_json {theme:light}然后刷新网页看 Agent 是否立即响应主题切换。这种“破坏性测试”能快速暴露代码中的隐性依赖。提示在package.json中加一个脚本一键打开数据库scripts: { db:open: open data/agent_memory.db || start data/agent_memory.db }开发时npm run db:open效率翻倍。5. 常见问题与排查技巧实录那些只有亲手踩过才知道的坑5.1 问题速查表高频故障现象、原因与解决方案故障现象可能原因解决方案我的实测经验Error: SQLITE_BUSY: database is locked多个 Server Action 并发写入WAL 模式未启用或未正确配置① 确认db.pragma(journal_mode WAL)已执行② 在写入前加db.prepare(BEGIN IMMEDIATE).run()显式获取写锁③ 避免长事务如循环中多次INSERT我最初没加BEGIN IMMEDIATE10 并发请求下 30% 失败率。加上后降至 0.1%且平均延迟从 15ms 降到 8ms。messages表查询变慢100ms缺少conversation_id索引或created_at未索引运行CREATE INDEX idx_messages_convo_time ON messages(conversation_id, created_at);10 万条消息时无索引查询 220ms加复合索引后 3ms。索引不是可选项是必选项。memory_facts中同一user_id/fact_key出现多条记录未声明UNIQUE(user_id, fact_key)约束或用了INSERT而非INSERT OR REPLACE检查建表 SQL确保UNIQUE存在代码中必须用INSERT OR REPLACE我曾因忘记OR REPLACE导致用户修改邮箱后数据库里存了两条email记录Agent 随机返回旧邮箱。better-sqlite3安装失败Windows/macOSnode-gyp编译环境缺失或 Node.js 版本与预编译包不匹配① 优先用npm install better-sqlite3latest最新版预编译包最全② Windows 用户装 Windows Build Tools ③ macOS M1 用户确保arch -arm64 npm install曾为一个客户远程解决他用 Node.js 18.17better-sqlite38.6.0 不兼容降级到 8.5.0 立刻解决。Agent 记住的内容在页面刷新后消失数据库路径错误如写入./agent.db而非path.join(process.cwd(), data, agent.db)或data目录权限不足① 打印dbPath路径确认文件真实存在②ls -la data/检查权限③ Next.js 生产环境process.cwd()是.next/server确保data目录随构建一起复制最惨一次路径写成../data/agent.db开发时正常部署 Vercel 后data目录不在.next内Agent 彻底失忆。5.2 独家避坑技巧来自 12 个真实项目的血泪总结技巧1为messages表的content字段加全文索引FTS5实现“模糊搜索记忆”Agent 用户常问“我之前问过打印机的事吗” 这需要全文检索而非精确LIKE %打印机%。SQLite 的 FTS5 是神器-- 创建虚拟表需在初始化时执行 CREATE VIRTUAL TABLE messages_fts USING fts5(content, content_rowidid); -- 创建触发器自动同步 messages 表变更 CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content); END; CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES(delete, old.id, old.content); END; CREATE TRIGGER messages_au