恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
不靠向量数据库:用SQLite和全文检索打造AI长期记忆层
首页
资讯中心
/
不靠向量数据库:用SQLite和全文检索打造AI长期记忆层
不靠向量数据库:用SQLite和全文检索打造AI长期记忆层
发布时间:2026/9/1 11:05:55
很多开发者在给 AI 助手加“长期记忆”时第一反应是先起一个向量数据库。这个直觉两年前还说得通但在今天大多数项目这么做是在给自己制造运维包袱。向量数据库能解决语义相似检索但它解决不了记忆的抽取、更新、冲突处理、遗忘和优先级排序。而后面的这些才是 AI 长期记忆真正难的地方。长期记忆问题本质上是一个信息管理问题不是一个向量化问题。这篇文章不是否定向量数据库而是想给你一条更务实的路线用 SQLite 或 PostgreSQL、全文检索、JSON 字段再配合 LLM 做记忆抽取和压缩搭出一个开源的记忆层Memory Layer。这套方案对大量真实业务场景已经够用而且部署简单、可解释性强、检索结果可控。全文关键词 结构化元数据过滤往往比 top-k 相似度召回更准尤其在用户偏好、事实、任务状态这类高频记忆场景里。读完这篇文章你可以得到三样东西一套不依赖向量数据库的 AI 长期记忆架构设计可直接复制的建表 SQL、核心 Python 代码和验证流程什么时候该坚持用结构化检索什么时候才值得引入向量数据库的判断依据。1. 这篇文章真正要解决的问题先看一个高频场景。你做了一个聊天机器人或者一个 AI 助手类应用。用户第一轮说“我生日是 3 月 15 日以后叫我小北推荐方案时优先便宜、性价比高的”。第二轮用户开启了一个新会话问“我叫什么过几天有什么重要日子”如果助手答不上来这就是典型的“失忆”问题。很多团队的第一版解决方案是把历史消息全部塞进上下文窗口。这个方案在对话轮数少的时候能用但很快就撑不住了token 成本随对话长度线性上涨响应延迟越来越大历史里的闲聊、临时任务、无关信息会成为噪声拉低模型效果用户不想每次都重复自己的偏好。于是很多人开始研究“长期记忆”。搜索一圈之后常见的结论是要上向量数据库把历史消息切成块embedding 之后存进去以后搜索。这个思路没有错但大多数项目忽略了一个问题向量数据库解决的是“模糊语义召回”而长期记忆业务的核心痛点经常是“精确读取结构化信息 按时间或类型过滤 控制注入上下文的预算”。一句话你需要的是一张能查的“记忆表”不是一个像搜索引擎一样的“语义索引”。所以这篇文章真正要解决的问题是在不引入独立向量数据库的前提下如何用开源组件构建一套可生产使用的 AI 长期记忆系统这套方案适合以下读者正在做 AI 助手、Agent、聊天机器人需要记忆用户偏好和上下文不想为了一个记忆功能引入 Milvus、Qdrant、ChromaDB、pgvector 等重量级组件想要一个可解释、可控、容易迁移到生产环境的记忆层或者已经用了向量数据库但发现命中率不稳定、运维成本高想了解替代和补充方案。2. AI 记忆系统的核心概念与适用场景在写代码之前先建立一个共识什么叫做“记忆”很多人的理解是“记住聊天历史”。这个理解过于粗糙。如果只是记住历史那上下文窗口越大记忆越好。现实并非如此。业界比较常用的记忆分层框架是这样的记忆类型通俗解释典型例子存储方式工作记忆当前对话的临时上下文正在讨论的这个需求背景对话窗口内不持久化情景记忆发生过的事件片段用户上周提到要换工作持久化可检索语义记忆稳定的事实和偏好用户叫小北生日在 3 月持久化结构化存储程序性记忆用户喜欢的工作方式/规则推荐方案时优先性价比持久化规则型存储大多数“AI 没记忆”的应用真正缺的是后三种尤其是语义记忆和情景记忆。“长期记忆”系统的核心能力不是存储而是五件事写入从对话中抽取值得记住的信息检索在需要时找到相关记忆更新当记忆与事实冲突时修正旧记忆优先级决定什么记忆进上下文什么记忆可以暂时不注入遗忘让过时、低频的记忆逐渐失效避免记忆库膨胀。这五件事里向量数据库只覆盖“检索”里的一部分语义相似度搜索。剩下几件事它反而做不好。2.1 为什么说向量数据库不是银弹向量数据库擅长的是把文本转成向量然后找一个与当前问题最相似的历史片段。这非常适合知识库问答、长文档召回、多语言同义表达匹配。但在记忆场景里它有几个明显的短板第一语义相似 ≠ 真实相关。用户问“我生日是哪天”和“我生日是 3 月 15 日”语义相似度很高向量检索大概率能命中。但用户问“去年项目什么时候上线”如果某个记忆片段里只提过一句“最终版本在 12 月初交付”语义相似度不一定排得上。这种问题用“时间范围 关键词 事件类型”过滤反而更准。第二标量过滤能力参差不齐。向量检索通常需要带 Filter 做类型、时间、用户维度的过滤。很多向量数据库的 Filter 能力写起来复杂过滤下推性能也一般。而传统数据库在这些维度上有几十年的积累。第三运维成本高。多一个中间件就多一套集群、监控、备份、升级方案。中小团队为了一个“记忆聊天偏好”的功能专门部署一套向量数据库性价比很低。第四记忆更新困难。用户改了口径比如“我喜欢喝美式”改成“我现在只喝拿铁”。向量数据库需要更新对应向量而结构化数据库里更新一条记录的字段是再自然不过的操作。2.2 什么时候该用向量数据库我需要把话说平衡一些不是所有场景都适合结构化方案。以下场景建议使用向量数据库知识库是大量非结构化长文档问题靠模糊语义匹配用户问法和原文之间差异很大关键词覆盖不了多语言内容混合需要跨语言语义召回需要做相似内容去重或推荐。以下场景建议先走结构化 全文检索方案用户偏好、事实、任务状态、日程、联系方式需要按用户维度精确定位记忆需要按时间、类型、重要性排序需要频繁更新和删除记忆团队希望记忆系统的行为可解释、可测试。这就是本文的核心判断记忆系统的主体应该是一张结构清晰的业务表而不是一个向量索引。3. 开源记忆层整体方案设计这篇文章讲的开源方案不是某个单一仓库而是一条由开源组件组合出来的落地路径SQLite / PostgreSQL FTS 全文检索 LLM 摘要压缩 一套 Python 记忆服务。整体架构可以拆成四个模块记忆抽取器Memory Extractor利用 LLM从用户对话中抽取结构化记忆记忆存储Memory Store使用 SQLite 数据库存事实、偏好、事件、任务状态并同步一张全文索引表记忆检索器Memory Retriever在用户发起新对话时按照用户 ID、关键词、类型、时间、重要性等条件召回记忆记忆维护器Memory Maintainer定期做记忆压缩、冲突更新、遗忘衰减。3.1 为什么选 SQLite从材料看很多中小团队既想要长期记忆又不希望引入复杂的运维基础设施。SQLite 是一个很合适的最小实现载体零部署单文件Python 标准库 sqlite3 直接用支持 FTS5 全文检索检索速度在百万行量级以内都可接受支持 JSON 字段适合存 tag 这类半结构化信息数据迁移简单后续想换 PostgreSQLSQL 结构也容易对齐。如果你的团队已经用 PostgreSQL可以直接用 PostgreSQL 的 tsvector / jsonb 替代本文的 SQLite FTS5设计思路完全一致。更稳妥的判断是先跑通单体 SQLite再按需替换。3.2 核心设计记忆不只是“内容”很多做记忆功能的团队容易犯一个错误只存一段文本。比如在表里加一个history字段拼上用户说过的所有话。这个设计有两个问题检索时无法精确过滤注入 context 时无法做预算控制。更合理的做法是给每条记忆打上结构化标签user_id记忆归属所有检索都带这个维度content去指代后的陈述句比如“用户名字是小北”而不是原始对话“我叫小北哦”memory_typefact / preference / event / taskimportance重要程度 0~1tags业务标签比如“用户偏好”“工作”“家庭”created_at/last_access_at/access_count用于召回排序和遗忘衰减is_pinned是否置顶不可遗忘。这套设计的价值在于当你决定给 LLM 注入记忆时不是把整个记忆库塞进去而是执行一条带过滤条件的 SQL。4. 环境准备与前置条件实践之前先把环境准备好。下面是本文示例的环境版本以你机器上的实际版本为准Python 3.10 及以上SQLite 3.34 及以上为了使用 FTS5 的 trigram tokenizer中文场景需要一个 LLM API可以是 OpenAI 兼容接口也可以是本地部署的 Ollama 等模型服务建议准备一个虚拟环境。安装依赖python -m venv venv source venv/bin/activate pip install openai说明这里只用了openai库因为它不仅调用 OpenAI 官方服务也兼容 Ollama 和许多本地模型的 OpenAI 格式接口。如果你的模型服务只支持原生 HTTP 接口也可以用requests直接调用逻辑是一样的。下面是项目文件结构memory-demo/ ├── schema.sql # 建表语句 ├── memory_service.py # 记忆存储与检索服务 ├── memory_extractor.py # LLM 记忆抽取器 ├── maintenance.py # 维护任务压缩与遗忘 └── demo.py # 验证脚本5. 数据库表设计与记忆写入5.1 建表 SQL创建一张记忆表和一张 FTS 全文索引表。-- schema.sql CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, memory_type TEXT NOT NULL DEFAULT fact, importance REAL NOT NULL DEFAULT 0.5, tags TEXT NOT NULL DEFAULT [], source_message_id TEXT, created_at TEXT NOT NULL, last_access_at TEXT, access_count INTEGER NOT NULL DEFAULT 0, is_pinned INTEGER NOT NULL DEFAULT 0, is_deleted INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_memories_user_type ON memories(user_id, memory_type); CREATE INDEX IF NOT EXISTS idx_memories_user_important ON memories(user_id, importance DESC); -- FTS 表trigram 分词器对中文检索更友好 -- 需要 SQLite 3.34 CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( user_id UNINDEXED, content, memory_type UNINDEXED, tags UNINDEXED, tokenize trigram );这几个设计点的原因content用陈述句保存避免检索和注入时还要做指代消解importance和access_count配合实现“高频记忆优先召回”is_pinned用来保护关键记忆例如用户身份证号、手机号、家庭信息这些不能因为长期未被访问而被遗忘is_deleted做软删除便于审计和误删恢复FTS 表用 trigram 分词中文连续文本也能建索引。5.2 记忆写入服务看一下存储层代码。这个类要承担插入记忆、检索记忆、更新访问次数三项基础能力。# memory_service.py import json import sqlite3 from datetime import datetime, timezone from typing import Optional SCHEMA_SQL open(schema.sql, encodingutf-8).read() class MemoryService: def __init__(self, db_path: str memory.db): self.conn sqlite3.connect(db_path) self.conn.row_factory sqlite3.Row self.conn.executescript(SCHEMA_SQL) self.conn.commit() def add_memory( self, user_id: str, content: str, memory_type: str fact, importance: float 0.5, tags: Optional[list] None, source_message_id: Optional[str] None, is_pinned: int 0, ) - int: now datetime.now(timezone.utc).isoformat() tags_json json.dumps(tags or [], ensure_asciiFalse) cur self.conn.cursor() cur.execute( INSERT INTO memories (user_id, content, memory_type, importance, tags, source_message_id, created_at, is_pinned) VALUES (?, ?, ?, ?, ?, ?, ?, ?) , (user_id, content, memory_type, importance, tags_json, source_message_id, now, is_pinned), ) memory_id cur.lastrowid # 同步写入 FTS 索引表 cur.execute( INSERT INTO memories_fts (rowid, user_id, content, memory_type, tags) VALUES (?, ?, ?, ?, ?) , (memory_id, user_id, content, memory_type, tags_json), ) self.conn.commit() return memory_id def touch_memory(self, memory_id: int) - None: 访问记忆时更新访问时间和次数用于召回排序与遗忘衰减。 now datetime.now(timezone.utc).isoformat() cur self.conn.cursor() cur.execute( UPDATE memories SET last_access_at ?, access_count access_count 1 WHERE id ? , (now, memory_id), ) self.conn.commit() def close(self): self.conn.close()注意一点这里的schema.sql是通过open读取的。在实际项目中建议把建表语句交给数据库迁移工具管理而不是每次启动时执行。5.3 从对话中抽取长期记忆用户输入的原始对话不能直接入库。原因是口语里有大量指代、噪声和前后文依赖。比如用户说“我最近在准备换工作压力很大但又不想裸辞”。直接存这句话后续检索“职业状态”时匹配不上。更合理的抽取结果是用户最近在准备换工作event用户不想裸辞preference所以这一步要用 LLM 做信息抽取。模型不是必须很强关键是 Prompt 要清晰。这里用 OpenAI 库兼容官方服务和 Ollama 本地服务。# memory_extractor.py import json from openai import OpenAI # 如果你使用 OpenAI 官方服务直接配置 OPENAI_API_KEY # 如果你使用本地 Ollama # client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) client OpenAI() EXTRACT_PROMPT 你是记忆抽取器。请从用户对话中抽取值得长期记住的信息输出 JSON 数组。 要求 1. content 使用简洁陈述句去掉指代词例如“用户”不要写成“我” 2. memory_type 取值fact | preference | event | task 3. importance 是 0 到 1 之间的浮点数越大越重要 4. tags 是字符串数组用于业务过滤 对话 {conversation} 只输出 JSON 数组不要输出多余内容。 def extract_memories(conversation: str) - list[dict]: resp client.chat.completions.create( model你的模型名称, # 例如 gpt-4o-mini、qwen2.5、llama3.1 等 temperature0, messages[ {role: user, content: EXTRACT_PROMPT.format(conversationconversation)} ], ) text resp.choices[0].message.content # 防止模型输出带代码块标记 text text.strip().removeprefix(json).removesuffix().strip() return json.loads(text)这段代码有两个值得注意的工程细节temperature0抽取任务要尽可能稳定不要每次结果都不一样模型可能输出 json 代码块所以要清理后再解析。抽取完成后把结果写入MemoryServiceconversation 我生日是3月15日以后叫我小北。推荐方案时优先性价比高的哦。 memories extract_memories(conversation) svc MemoryService() for m in memories: svc.add_memory( user_iduser_1001, contentm[content], memory_typem[memory_type], importancem.get(importance, 0.5), tagsm.get(tags, []), )6. 记忆检索与上下文注入记忆写入只是第一步。真正的难点在于用户开启新会话时如何把历史记忆按需取出来并注入到当前对话的上下文中。6.1 检索策略不只靠关键词我把检索设计成三个阶段精确匹配优先从 FTS 全文索引里按关键词命中的记忆结构化过滤按memory_type、importance、is_pinned、时间范围过滤重排截断按“置顶 相关性 重要性 最近访问时间”排序截取 top-k。有人会问为什么不用 embedding 做语义召回因为在这个设计里我们首先服务的是“用户叫什么、偏好是什么、生日是什么”这类精确记忆。关键词 结构化字段已经足够。等你真的遇到大量原文和问法完全不一样的模糊场景时再引入向量模块也不迟。6.2 检索函数实现# memory_service.py 追加方法 import re _FTS_SPECIAL_CHARS re.compile(r[*^():|!-]) def _sanitize_fts_query(text: str) - str: FTS5 MATCH 语法有保留字符用户输入需要清洗避免语法报错。 clean _FTS_SPECIAL_CHARS.sub( , text) tokens [t for t in clean.split() if t] return OR .join(tokens) if tokens else def search_memories( self, user_id: str, query: str, limit: int 5, memory_type: Optional[str] None, min_importance: float 0.0, ) - list[dict]: fts_query _sanitize_fts_query(query) cur self.conn.cursor() if fts_query: # 优先走 FTS5 全文检索 sql SELECT m.*, bm25(memories_fts) AS relevance FROM memories_fts JOIN memories m ON m.id memories_fts.rowid WHERE memories_fts.user_id ? AND memories_fts.content MATCH ? AND m.is_deleted 0 params: list [user_id, fts_query] if memory_type: sql AND m.memory_type ? params.append(memory_type) if min_importance 0: sql AND m.importance ? params.append(min_importance) sql ORDER BY m.is_pinned DESC, relevance ASC, m.importance DESC, m.last_access_at DESC LIMIT ? params.append(limit) cur.execute(sql, params) else: # 无法生成 FTS 查询时退化为 LIKE 模糊匹配 like_pattern f%{query}% sql SELECT m.*, 0 AS relevance FROM memories m WHERE m.user_id ? AND m.is_deleted 0 AND m.content LIKE ? params: list [user_id, like_pattern] if memory_type: sql AND m.memory_type ? params.append(memory_type) if min_importance 0: sql AND m.importance ? params.append(min_importance) sql ORDER BY m.is_pinned DESC, m.importance DESC, m.last_access_at DESC LIMIT ? params.append(limit) cur.execute(sql, params) rows [dict(r) for r in cur.fetchall()] for r in rows: # 访问后更新热度但不在查询链路里阻塞主流程 self.touch_memory(r[id]) return rows这里有个容易被忽略的性能细节touch_memory在每次检索时触发一次 UPDATE。如果记忆系统高频读写建议后续改成异步队列批量更新或者先只更新内存状态定时落盘。6.3 记忆注入 Prompt检索出来的记忆不能原样丢给 LLM要做格式化拼接。我给你一个推荐的注入模板# memory_injector.py def build_memory_section(memories: list[dict]) - str: if not memories: return lines [] for m in memories: lines.append( f- [{m[memory_type]}][重要性:{m[importance]}] {m[content]} ) return 以下是关于用户的长期记忆回答时务必参考\n \n.join(lines)实际调用时把这段记忆放在系统提示词的最前边再把当前消息放在后面system_prompt ( 你是一个有长期记忆的 AI 助手。\n build_memory_section(memories) )不是所有记忆都需要每次都注入。更稳的做法是设置一个“记忆预算”例如每次最多注入 10 条或者限制总 token 数不超过 500。原因很简单上下文里塞入太多历史记忆模型反而会忽视关键信息还会增加延迟。6.4 完整调用链下面这段代码完整演示了“新会话 记忆检索 助手回答”的流程# demo.py from memory_service import MemoryService from memory_injector import build_memory_section from openai import OpenAI svc MemoryService(memory.db) client OpenAI() def ask_with_memory(user_id: str, question: str) - str: memories svc.search_memories(user_id, question, limit8) system_prompt ( 你是一个有长期记忆的 AI 助手。\n build_memory_section(memories) ) resp client.chat.completions.create( model你的模型名称, messages[ {role: system, content: system_prompt}, {role: user, content: question}, ], ) return resp.choices[0].message.content这个函数的关键点用户没有提供任何聊天历史但系统能通过user_id从记忆表里找回之前存好的偏好和事实。这就是“长期记忆”的最小闭环。7. 记忆更新、压缩与遗忘机制长期记忆系统如果只写不维护三个月后会变成垃圾场。记忆会过时对话会重复重要信息会被噪声淹没。这里给出三个必须做的维护动作。7.1 冲突更新用户可能今天说“我每周三健身”下周说“我把健身改到周五了”。如果两条记忆同时存在检索时模型就会困惑。常见的处理方式有两种版本化把旧记忆标记为失效is_deleted1插入新记忆字段覆盖如果业务上有唯一标识维度则直接 UPDATE。在工程上推荐给每条记忆增加superseded_by或者is_deleted字段。发现冲突时把旧记忆软删除再写入新记忆。这样既能保留审计历史又不会影响检索。7.2 访问频率与遗忘衰减用户一年前说过“我喜欢爬山”这一年再也没有提过。如果系统每次都在检索结果里返回这条反而会干扰当前对话。简单有效的做法是引入时间衰减因子。定期执行一个 SQL把长期未访问的低重要性记忆软删除# maintenance.py from datetime import datetime, timedelta, timezone def decay_and_purge(svc, max_unaccessed_days: int 180) - int: 把超过规定天数未访问且非置顶的低优先级记忆标记为删除。 threshold datetime.now(timezone.utc) - timedelta(daysmax_unaccessed_days) cur svc.conn.cursor() cur.execute( UPDATE memories SET is_deleted 1 WHERE is_deleted 0 AND is_pinned 0 AND importance 0.7 AND (last_access_at IS NULL OR last_access_at ?) , (threshold.isoformat(),), ) svc.conn.commit() return cur.rowcount这个函数里is_pinned 0和importance 0.7是两个保护条件。如果你把用户手机号、家庭地址这类记忆的importance设为 0.95并且置顶就不会被遗忘机制误删。7.3 对话片段压缩另一个高价值操作是“记忆摘要化”。用户和管理员聊了 30 轮主题是“规划下周的出差”。这些对话里的中间过程没有必要长期保留但最终结论值得留下。做法每天或每周把某段时间内相同主题的对话片段抽取为一条 summary 记忆def summarize_segment(svc, user_id: str, segment_text: str) - None: memories extract_memories(segment_text) for m in memories: if m.get(memory_type) in (event, task, preference): svc.add_memory( user_iduser_id, contentm[content], memory_typesummary, importancem.get(importance, 0.5), tags[对话摘要], )然后把已摘要的原始对话片段标记为“已归档”不再参与全文检索召回。这能有效控制记忆库膨胀。8. 运行结果与效果验证按下面步骤可以复现一个完整验证流程。第一步初始化库并写入抽取结果python demo.py第二步执行一个最简单的验证脚本# verify.py from memory_service import MemoryService from memory_extractor import extract_memories svc MemoryService(memory.db) # 第一轮对话写入记忆 conversation 用户说我生日是3月15日以后叫我小北。推荐方案时优先便宜和高性价比。 for m in extract_memories(conversation): svc.add_memory(user_1001, m[content], m[memory_type], m.get(importance, 0.5), m.get(tags, [])) # 模拟全新会话不携带任何历史 hits svc.search_memories(user_1001, 我的名字和生日, limit5) for h in hits: print(h[content], h[memory_type], h[importance])预期输出类似用户名字叫小北 fact 0.9 用户生日是3月15日 fact 0.9 用户偏好推荐性价比高的方案 preference 0.8如果你的模型抽取结果字段不同输出顺序和内容会有差异但核心应该一致“名字”“生日”“偏好”这三条信息被成功存入并检索到。第三步验证端到端回答。用ask_with_memory发起新问题answer ask_with_memory(user_1001, 我叫什么我的生日是哪天) print(answer)预期回答你叫小北你的生日是 3 月 15 日。如果失败优先按下面顺序排查看memories表里有没有数据看search_memories返回结果是否为空看memory_extractor解析的 JSON 是否正常最后再看模型的 prompt 拼接是否正确。9. 常见问题与排查思路问题现象可能原因排查方式解决方案检索不到记忆用户 ID 不匹配查询memories表确认写入用户 ID统一从认证信息取用户 IDFTS MATCH 查询报语法错误用户输入包含 MATCH 特殊字符打印清洗后的查询语句使用_sanitize_fts_query清洗中文检索命中率低分词器不合适检查 SQLite 版本和 FTS tokenizer使用 trigram tokenizer或用 jieba 分词后建索引抽取出的记忆质量差Prompt 描述不清晰打印 LLM 返回的 JSON在 Prompt 里增加示例开启 few-shot记忆冲突旧信息和新信息共存没有处理冲突检查同 key 多条记录增加冲突检测软删除旧记忆记忆库越来越大检索变慢缺少遗忘和归档策略统计memories表行数定期执行衰减删除和摘要压缩检索结果包含无关记忆排序规则太简单打印命中记录的relevance和importance调整排序权重或增加memory_type过滤生产环境想从 SQLite 迁移数据量增长对比 PostgreSQL 全文检索使用 PostgreSQL tsvector jsonb9.1 中文全文检索的坑SQLite FTS5 默认分词器是 unicode61它会把中文按整句处理导致单个关键词匹配不上。解决方式是使用 trigram tokenizer它会把文本切成三个字符的子串适合中文但代价是索引体积膨胀。如果你的 SQLite 版本低于 3.34升级 SQLite 是一个选项。如果不想升级可以用 jieba 先做中文分词把分词后的结果存在一个search_text字段然后对这个字段建 FTS 索引。检索时把用户输入也分词然后用 OR 连接。10. 最佳实践与工程建议10.1 记忆写入要异步化不要把 LLM 的响应链路和记忆抽取链路绑在一起。用户问问题的时长通常几百毫秒到几秒你可以在返回答案后再把本轮对话放入消息队列让后台任务异步完成抽取和写入。推荐用 Redis Stream 或 SQS 作为缓冲避免阻塞主流程。10.2 记忆要有预算每次进入上下文的记忆总量必须控制。推荐按 token 预算动态截断而不是固定条数。例如系统提示词最多允许 500 token 的记忆那build_memory_section就根据 token 估算器截断。10.3 所有记忆必须可删除无论是为了隐私合规还是为了用户体验都要给用户提供“查看记忆、删除单条记忆、清空全部记忆”的入口。删除可以是软删除但用户侧感知必须是“真的删了”。10.4 本地模型优先如果你的记忆涉及公司敏感信息或用户隐私建议使用本地部署模型。Ollama 等方案支持 OpenAI 兼容接口前面代码里只要改base_url就行不用改核心逻辑。这也是这套设计的一个优势记忆抽取和记忆存储全部可以自托管不依赖外部服务。10.5 Java 服务端怎么接入如果你用的是 Spring AI 或 Spring Boot思路是一样的。你可以把MemoryService用 Java 重写利用 PostgreSQL 或 SQLite JDBC调用模型时走 Spring AI 的ChatClient。核心表结构和检索逻辑可以直接复用本文的 SQL。10.6 什么时候再上向量数据库我的建议很简单先把结构化方案跑起来记录两个指标记忆召回命中率用户问题里有多少能从记忆里找到相关信息用户反馈是否觉得助手“懂他”。当结构化方案已经无法满足需求而且你确认问题出在“语义相似度”而不是“抽取质量”“排序规则”时再引入向量数据库。到时候你可以把向量作为 FTS 检索之外的第二路召回器与结构化结果做融合排序。记住一句话向量数据库是记忆系统的补充模块而不是默认前提。11. 总结与后续学习方向这篇文章的核心结论可以收束成三点第一AI 长期记忆的本质是信息管理问题不止是检索问题。向量数据库只解决语义召回而记忆系统需要写入、更新、排序、压缩、遗忘全套机制。第二用 SQLite/PostgreSQL 全文检索 LLM 抽取就能搭建一套可用的开源记忆层。它在用户偏好、事实、任务状态等场景下效果和可解释性都优于单纯堆向量检索。第三随着项目复杂度提升记忆系统会越来越像一个独立的基础设施。后续值得深入研究的方向包括Agent 的层级化记忆、MemGPT 式的按需换页机制、记忆可视化面板、以及多 Agent 之间的记忆共享与隔离。如果你想继续深入下一步可以这样实践把本文的示例改成你自己的业务表结构增加用户身份字段写一个异步 worker把记忆抽取放到后台给记忆检索增加日志统计哪些记忆被命中、哪些永远没有命中等你真正遇到语义召回场景时再引入向量模块并对比 FTS 与向量的效果差异。这套方案最难的部分不是代码而是想清楚“哪些记忆值得长期保留、哪些该遗忘”。把这个想清楚了数据库、向量、模型都只是工具。建议收藏本文的建表 SQL 和检索代码实际做 AI 长期记忆时可以直接拿来改。