端侧AI长期记忆系统架构实践:从向量检索到记忆生命周期管理 1. 为什么要在端侧做长期记忆1.1 LLM无状态的天生短板做过AI Agent的朋友应该都有同感大模型本身是一个典型的无状态函数。你把同样的问题原样丢给GPT、Llama这类模型它每次给你的回答可以完全不一样因为它不会记得上周你和它聊过什么。这就是当前对话式AI最尴尬的地方——你每次都像是在跟一个陌生人重新介绍自己。解决这个问题的常规做法是外挂记忆。大部分云端方案直接往系统提示词里塞历史对话塞不下了就做摘要压缩。这个方法能用但问题也很明显第一Token成本随对话轮次线性增长第二当你需要跨会话调用用户三个月前提到的某个偏好时早期对话早就被截断或压缩掉了第三所有对话内容都经过云端很多用户对隐私是介意的。所以我做Zorv AI的时候定了两条硬性要求记忆必须持久化记忆必须尽量在端侧完成。目标很简单——让这个Agent在和一个用户长期相处之后越来越懂这个人而且不依赖每轮都把完整历史发给云端。1.2 端侧跑记忆的边界在哪里端侧做长期记忆系统很多人第一反应是“手机/平板那点算力能跑什么”确实你不能指望端侧跑一个70B的模型那是云端干的事。但我们要做的记忆系统真正吃算力的部分其实是三个文本向量化、向量检索、相关性排序。这三个任务在2024年以后的技术栈里都有轻量解法。embedding模型可以选择100MB以内的轻量模型向量检索可以用SQLite扩展实现排序可以用一个很小的rerank模型做粗排精排。整体算力开销相比跑生成式大模型来说是小一个数量级的。我做这套系统的时候目标硬件是8GB内存以上的Android设备和16GB内存的Apple Silicon Mac。在这个配置下embedding模型用CPU跑一次平均耗时控制在30ms以内单次向量检索耗时在5ms以内完全在可接受范围内。真要追求极致现在的手机NPU也能跑这类模型后面我会专门讲NPU适配的坑。1.3 整套系统到底要解决哪四个问题我从一开始就要求自己把问题拆干净。长期记忆系统本质上就是在回答四个问题第一记什么。对话、行为、偏好、事实陈述哪些信息值得进入长期记忆哪些只是过眼云烟。第二怎么存。决定了用什么数据结构、什么存储引擎、如何做索引让记忆既能被快速写入也能被快速找到。第三怎么取。用户提问的时候系统要从堆积如山的记忆里挑出当前对话最需要的几条兼顾相关性和时效性。第四怎么忘。记忆不能无限膨胀必须有一种机制让过时、冲突、低价值的记忆逐渐衰退、合并甚至删除。这四个问题对应到架构上就是记忆采集层、编码存储层、检索召回层和遗忘管理层。下面我会把这套架构的每一层拆开讲包含我实际写代码时踩过的坑。2. Zorv AI记忆库的总体架构2.1 五层架构总览Zorv AI的记忆库我最终设计成了五层架构从下往上分别是设备适配层、存储引擎层、记忆管理层、检索服务层、Agent接口层。可能有人会说“一个记忆库搞五层是不是过度设计”但端侧开发和云端最大的区别在于你没法假设目标设备是什么芯片、什么操作系统、多少内存所以最底层必须做设备适配抽象。设备适配层的职责是屏蔽CPU、GPU、NPU的差异统一提供嵌入计算和向量检索引擎的调用入口。存储引擎层选型是SQLite加sqlite-vec扩展没有自研存储引擎。记忆管理层做记忆生命周期管理包括写入、强化、衰减、合并、遗忘。检索服务层提供召回、重排、上下文组装这些能力向上暴露一个简单的query接口。Agent接口层则是给上层智能体用的提供remember(user_msg, assistant_msg)和recall(query, top_k)两个最核心的方法。这里我想强调一个设计决策整个系统对外接口极简复杂度全部收在内部。Agent不需要关心记忆是怎么存的、向量维度是多少、剪枝策略是什么它只需要告诉记忆库“我要回忆什么”。2.2 三级记忆划分工作、情景、语义心理学里有三分记忆模型——感觉记忆、短时记忆、长时记忆。做工程不能照搬心理学但这个思路值得借鉴。我在Zorv AI里把记忆分成三个层级工作记忆、情景记忆、语义记忆。工作记忆对应当前正在进行的对话上下文。它保存在内存里有固定长度的滑动窗口通常保留最近20轮到50轮对话。这一层的特点是读写极快、生命周期极短对话结束或超过窗口长度后就会被压缩成摘要降级到下一层。情景记忆对应具体的交互事件。比如“用户上周三在设置页把主题切换成了深色模式”“用户上个月问过如何配置CI/CD流水线”。每一条情景记忆就是一个带有时间戳和上下文标签的事件片段。语义记忆是从情景记忆中提炼出来的规律和结论。比如“用户更喜欢代码示例而不是理论讲解”“用户的项目主要用的是Python和Go”。语义记忆是最高价值层也是回答“用户是一个什么样的人”的核心数据来源。三层记忆不是并列平级而是逐层提炼的关系。工作记忆经过压缩变成情景记忆情景记忆经过周期性归纳变成语义记忆。2.3 记忆生命周期的状态机设计这一节是整套系统中最容易被低估的部分。大多数初版记忆系统只实现了写入和读取忘了设计遗忘结果就是存储无限膨胀检索质量越来越差。我在系统里给每条记忆定义了一个状态机包含四个状态活跃、强化、衰减、归档。一条新记忆写入时默认是活跃状态每次被成功召回并得到用户正向反馈这条记忆的权重就会增加如果长期未被召回权重会按时间衰减进入衰减状态当权重低于某个阈值这条记忆会被标记为归档归档记忆会被压缩合并最原始的记录进入冷存储或直接删除。这个状态机和搜索引擎的索引机制很像。本质上长期记忆系统就是要解决“热数据”和“冷数据”的分离问题让最相关的记忆始终处于高可用状态让冷记忆不拖累检索效率。3. 记忆采集与编码存储实现3.1 轻量Embedding模型的选型过程记忆要能被检索首先得能用向量表示。所以embedding模型是整套系统的地基。这个选型我花了不少时间前后对比了大概七八个模型最后锁定的标准其实只有四条参数量在100MB以内、支持中文和英文、有INT8量化版本、在CPU上单次推理延迟低于50ms。当时对比的几个候选bge-small-zh-v1.5、e5-small-v2、multilingual-e5-small都在考虑范围内。最后我选了bge-small系列原因是它在中英文混合场景下表现均衡而且国内开发者社区生态好量化工具链完善。如果你主要面对纯英文场景MiniLM系列也很能打体积可以压到更小。这里有一个经验想分享不要只看MTEB排行榜分数。端侧场景下模型中文语义理解的鲁棒性远比排行榜上零点几分的差距重要。我实际测试过拿用户口语化的输入去检索规范化的文档片段bge系列的表现明显更稳。3.2 SQLite就够用没必要上重型向量数据库很多朋友一听“向量检索”就想上Milvus或者Weaviate。但这是典型的云端思维。在端侧场景下你的记忆数据量级撑死也就是几万到几十万条在这个量级下重型向量数据库带来的分布式扩展能力完全用不上反而白白增加了几百MB的部署体积和运维复杂度。我的方案是SQLite加sqlite-vec扩展。这个组合的优雅之处在于普通的结构化数据、向量数据和元数据可以在同一个数据库文件里管理不用维护多套存储。sqlite-vec基于HNSW算法实现在十万条向量规模下单次检索延迟在几毫秒级别完全够用。库表设计上我建了三个核心表memories表存记忆文本和元数据memory_embeddings表存向量数据memory_refs表存记忆之间的关联引用。这三个表通过memory_id关联用事务保证一致性。关于向量维度我用的是384维。每个float需要4字节存储那一条记忆的向量原始数据就是1536字节。算一笔账一万条记忆向量存储大概是15MB加上原始文本和索引总体在50MB以内。这个体量对端侧完全无压力。3.3 记忆写入的完整链路一条记忆从原始对话变成可检索的向量中间要经过四个步骤。第一步是触发判断系统要决定这段话值不值得写入记忆库第二步是清洗归一化去掉无意义的语气词、指代词消解第三步是向量化用embedding模型把文本变成向量第四步是入库存档同步写入原始文本、向量、时间戳和权重初始值。触发判断是一个容易被忽视的环节。如果每轮对话的所有内容都不加筛选地写入记忆库很快就会被“今天天气不错”“嗯嗯”这样的无效信息污染。我的做法是写了一个轻量规则加模型的混合判断先通过规则过滤掉纯语气词和重复内容再让一个小分类模型判断这句话是否包含用户偏好、事实陈述、任务指令等可记忆信息。这四步里最耗时的是向量化。这里有一个优化技巧在用户输入结束后立刻异步执行记忆写入而不是等Agent回复完成后再同步处理。这样可以把向量化延迟隐藏到Agent生成回复的时间里用户完全感知不到。4. 记忆检索与上下文组装4.1 从向量召回到重排的完整链路回忆recall接口的设计直接决定了Agent的智能感。我之前第一版只用了向量相似度检索结果经常召回一些表面语义相似但实际没用的记忆。后来我把检索链路改成了召回加粗排加精排的三段式。召回阶段我用向量相似度取出Top 50候选记忆。粗排阶段用BM25关键词打分和向量得分的加权融合把候选集缩小到Top 20。精排阶段让一个轻量的交叉编码器模型对每个候选记忆和当前查询逐对打分最终取出Top 5到Top 10。你可能会问端侧跑交叉编码器会不会太慢我实测过在CPU上用ONNX Runtime跑一个MiniLM交叉编码器对20个候选做排序总耗时大概在80ms到150ms之间。这个延迟放在Agent思考链路里是可以接受的。如果不做精排检索质量确实会下降一截尤其是用户用词和记忆原文本差异比较大的时候。整个召回排序链路里我体会到最重要的一个原则是不要只相信向量相似度。向量检索擅长解决换一种说法的问题BM25擅长精确关键词匹配两者结合才能覆盖更多场景。4.2 时间衰减与重要性加权纯靠相似度排序的最大问题是没有时间观念。用户两个月前关注过一个技术方案和今天刚讨论的话题可能只是表面相似如果总是把老记忆排前面会显得Agent的记忆完全错乱。我给每条记忆维护两个分数静态重要度分数和动态时效性分数。静态重要度在记忆写入时由分类模型打分动态时效性则基于时间衰减函数计算。我采用的衰减函数是类艾宾浩斯遗忘曲线score importance * (0.5 ^ (days_since_last_access / half_life))。其中half_life是一条经验的半衰期我根据实际效果调成了30天。这个公式的工程意义在于一条记忆如果被频繁访问days_since_last_access会很小分数衰减得慢如果长期没用上分数就会快速降低最终进入遗忘流程。相比粗暴地按时间倒排或完全不考虑时间这个折中方案在实际测试中效果最好。4.3 上下文窗口裁剪策略召回出来的记忆不能无脑全部塞给大模型。上下文窗口是有限资源每一条记忆塞进去都会挤占模型注意力。所以上下文组装是检索之后的一道关键工序。我的做法是给记忆分优先级第一优先级是硬性事实包括用户个人信息、正在进行的任务状态、明确表达过的偏好这些一定进上下文第二优先级是与当前查询高度相关的用户历史行为记录默认带两到三条第三优先级是语义层归纳总结一般只带一条用于给模型兜底。组装的时候还需要给每条记忆标注来源时间、相关度分数让模型知道这段信息的上下文。这比纯粹拼接文本要有效得多——模型能依据时间信息判断“这条记忆可能是过时的”而不是拿错误历史信息一本正经地瞎聊。我自己常用的裁剪基准是最终上下文里记忆片段不超过1200个Token加上系统提示词和最近对话上下文整体控制在3000个Token以内。这个量保证了Agent响应速度也让模型注意力能集中在最关键的几段记忆上。5. Agent如何消费记忆库5.1 触发式召回与自主召回的配合我在Zorv AI里实现了两种召回模式。一种是显式触发式召回用户问“我之前是不是提过xxx”这种查询意图很明确系统直接调recall接口就能找到对应记忆。另一种是自主召回用户提问里没有说“我记得”之类的词但Agent需要主动去查相关的历史背景。自主召回是最难的部分因为你要决定什么时候值得去翻记忆库。翻少了Agent显得健忘翻多了每轮都查一次浪费时间也消耗电池。我的方案是让Agent先基于当前对话做一个是否需要回忆的二分类判断只有判断为需要时才触发检索链路。这个二分类模型同样做成了轻量级在端侧跑一次只要几毫秒。你可以理解成给Agent装了一个“回忆触发开关”而这个开关的灵敏度是可调的。在开发阶段我把灵敏度调高方便观察检索效果正式使用时调低避免每轮对话都浪费耗时。5.2 用户画像的增量式更新长期记忆的最终目标之一是构建用户画像。但这里有个坑用户画像是动态变化的去年用户听了大量摇滚乐今年可能已经开始研究古典乐。如果画像永远由最初几条记忆决定会越来越失真。我在系统里把用户画像建模成一张带权重的标签表。标签不是一次性写死的而是靠语义记忆层周期性归纳。每三十天系统会对过去一个月沉淀的情景记忆做一次聚类归纳出高频主题和偏好更新到用户画像表中。每次更新都保留旧标签的权重衰减版本新标签以较高权重覆盖入场。这个方案最直接的效果是——用户长期使用后Agent对用户风格的理解会越来越贴近当下的情况而不会停在三个月前的刻板印象里。5.3 记忆一致性同一事实不同时间的不同说法还有一个开发时必须处理的问题用户对不同时间提到的同一件事说法可能完全不一样。如果没有一致性检测系统会同时保留“用户住在北京”和“用户搬到了上海”两条矛盾记忆。我用了一个比较朴素的方案在写入新记忆前先从语义层召回与当前事实相关的存量记忆做一次事实验证和冲突检测。如果新记忆和旧记忆属于同一事实但时间更新就把旧的降权、新的提升为活跃。这个方案不算完美但工程上足够可靠吞吐量也够。不要一上来就追求完美的知识图谱和实体链接那是一个大坑。端侧资源有限优先用“时间戳最新优先相似度去重”的启发式规则把80%的冲突问题解决掉剩下的交给模型兜底。6. 端侧部署与性能优化经验6.1 CPU、GPU、NPU异构调度端侧硬件第一课就是手机上的CPU不是用来跑神经网络的NPU才是。但NPU生态碎片化严重不同厂商的SDK和算子支持差异很大。我的经验是开发阶段统一走CPU稳定之后再做NPU加速。Zorv AI在初始化的时候会做硬件能力探测优先尝试NPU如果NPU初始化失败或者某个算子不支持就自动回退到GPU再退到CPU。这个降级策略保证了系统在低端机上也能跑只是在高端机上更流畅。这里的坑在于NPU的图编译时间。有些NPU第一次跑模型时要花几秒钟做模型转换和图优化如果你在Agent响应关键路径上做这件事会直接卡死。我的做法是在App启动后立刻做NPU预热把embedding模型和排序模型都跑一遍空输入等真正用时延迟就低了。6.2 模型量化是自己动手还是用工具链量化是端侧AI绕不开的话题。把FP32模型压到INT8体积缩小四倍速度通常能提升两到三倍但精度会有轻微下降。对于embedding任务来说INT8量化带来的精度损失通常是可以接受的因为向量检索有召回排序阶段兜底。选型时我优先找官方提供量化版本或社区量化配置成熟的模型避免自己折腾量化工具链。如果你用ONNX Runtime部署可以直接用它的动态量化API几行代码就能完成量化对中文语义的影响实验下来很小。动手之前先量化一个小模型试一下精度损失不要盲目上INT4embedding模型的精度对检索质量影响很直接。6.3 功耗优化端侧AI不能忽视的问题端侧AI和云端最大的区别是功耗约束。一个持续在后台跑模型的应用如果每小时多消耗3%的电池用户一定会卸载你。我的功耗优化方案分两步。第一步是降低调用频次embedding计算和向量检索都改成按需触发不在每轮对话都无脑全跑。第二步是设置合理的空闲清理策略当系统检测到30分钟内没有交互时把不常用的缓存从内存释放掉让模型权重滞留内存的时间尽量短。实测下来的结果待机一小时额外功耗控制在0.5%以内持续对话时功耗略高但远低于游戏和视频类应用。这个数字我觉得是可以接受的毕竟用户获得的是一个有记忆能力的智能助手。7. 踩坑实录与调参心得7.1 检索结果千篇一律怎么办我第一版检索链路只跑了向量相似度召回结果就是不管问什么返回的总是那几条高频记忆。原因很简单相似度TopK在高频记忆上扎堆了。综合出境了但针对性没了。解决办法就是我前面说的三段式向量召回保证召回面BM25加权纠正词汇匹配精排模型重新排序。调完这个链路之后检索结果的多样性明显改善相似但不相关的记忆被压下去了。另外一个技巧是给召回阶段加一个基于MMR的多样性惩罚。MMR最大边际相关机制可以保证候选记忆之间不要过得太像让最终返回的记忆覆盖多个角度。这个调优在总结型问答场景下效果非常明显。7.2 记忆库膨胀速度超出预期初版系统上线之后我观察了一个月发现记忆库以每天几百条的速度增长比预估的翻了将近一倍。查了一圈问题出在触发判断太宽松——用户随口一句“这个看起来不错”都被当作偏好记录下来了。修复方式是给触发判断加了三道防线基于规则的关键词和句长过滤基于模型的意图分类最后一道是相似度去重——如果新记忆和已有记忆的向量余弦相似度超过0.92就不重复写入。三道防线叠加每天新增记忆量降到了原来的三分之一左右而且检索准确率反而是提升的。7.3 冷启动阶段记忆库形同虚设刚开始用的时候用户和Agent还没有积累足够的交互历史记忆库里面只有几十条记录召回质量很不稳定。这个阶段称为冷启动期任何检索方案都救不回来。我给Zorv AI设计了一个初始化策略首次启动时通过一组结构化问题引导用户主动提供自己的基本信息和偏好。这些问题包括“你平时的工作方向是什么”“你希望我以什么风格和你交流”“最近在忙什么项目”。用户的回答直接作为种子记忆写入语义记忆层让Agent从第一天起就有基本的用户认知。实践证明这个策略非常有效——用户跟Agent的第一轮深度对话体验明显提升因为Agent真的表现出了“我知道你大致是谁”的姿态。7.4 端侧推理延迟的压倒性因素最后聊一个性能优化上的经验。我调试了很久的embedding延迟最后发现最大的瓶颈不在模型本身而在数据预处理。字符串编码转换、文本过长的截断策略、每次推理前的内存分配这些细节叠加起来耗时比模型计算还高。优化方式是文本预处理结果加缓存重复文本直接复用向量推理前用内存池复用输入输出缓冲区避免反复申请释放长文本做分段embedding再取均值避免截断丢失关键语义。这些优化加完单次embedding延迟从80ms降到了30ms以内。说实话端侧AI很多时候比的不是谁的模型更先进而是谁把工程细节打磨得更到位。模型选型决定了系统的上限工程优化决定了能否接近这个上限。我在整个Zorv AI记忆库的开发过程中有一个体会很深端侧长期记忆难点不在“长期记忆”这四个字而在于“端侧”这两个字。所有在云上好用的成熟方案——大规模向量库、重型知识图谱、大参数精排模型——到了手机和PC上都需要做减法。但这个减法减到最后的收获是你被逼着把架构理得更清楚把每一个环节的效率都压榨到了极致。如果看完这篇文章你也想动手做一套端侧记忆系统我建议先从最小闭环开始一张SQLite表、一个embedding模型、一个TopK查询接口跑通之后再慢慢加入衰减、遗忘、重排这些进阶能力。架构是做出来的不是一开始想出来的。