1. 手机端 Agent 的记忆困局与破局思路
做过移动端 AI 应用的人都有一个共同感受:手机上的智能助手,记性实在太差了。你昨天跟它说“帮我订周五下午三点的会议室”,今天再问“那个会改到几点了”,它一脸茫然。这不是模型能力不行,而是整个记忆系统没有为移动场景做专门设计。我所在的团队在过去一年多时间里,一直在折腾手机端 Agent 的记忆工程,从最早的朴素向量检索,一路踩坑到现在的 Agentic-RAG 架构,中间经历了好几轮推倒重来。这篇文章就把我们趟过的路、踩过的坑、以及最终沉淀下来的方案,完整地摊开来讲。
先说清楚这个项目到底在做什么。简单讲,我们要解决的核心问题是:在手机这个算力有限、存储有限、隐私敏感的环境里,让 Agent 拥有长期、可检索、可推理的记忆能力。它要能记住用户说过的话、做过的操作、偏好设置、甚至是一些隐含的习惯,并且在需要的时候准确地把相关信息调出来,喂给大模型做推理。适合谁看?如果你正在做移动端 AI 助手、端侧 RAG、或者任何需要在资源受限设备上做记忆管理的系统,这篇内容应该能帮你少走不少弯路。
为什么手机端的记忆系统这么难做?我总结了三个核心矛盾。第一,上下文窗口和记忆容量的矛盾。大模型的上下文窗口再大也是有限的,你不可能把用户三个月的聊天记录全塞进去。第二,检索精度和响应速度的矛盾。手机用户对延迟极其敏感,超过两秒的等待就会让人觉得卡,但高精度的检索往往意味着更复杂的计算。第三,个性化需求和隐私保护的矛盾。记忆越丰富,个性化体验越好,但手机上的数据涉及大量个人隐私,不能随便往云端传。这三个矛盾贯穿了整个系统设计的始终,后面讲的每一个技术选型,本质上都是在这些矛盾之间找平衡。
我们最初的方案非常朴素:把所有对话历史做 embedding,存到本地的向量数据库里,每次用户提问就做一次相似度检索,把 top-k 的结果拼到 prompt 里。这个方案在 demo 阶段看起来还行,但一上真实场景就崩了。问题出在哪?我举一个真实的例子。用户先说“我下周三要去杭州出差”,过了两天又说“帮我把那个出差改到下周四”。朴素 RAG 在检索“那个出差”的时候,很可能召回的是最早那条“下周三去杭州”的记录,然后模型就会认为出差还是周三。它无法理解“那个”指代的是什么,也无法处理“修改”这个动作带来的状态变更。这就是典型的记忆碎片化问题——记忆被切成了独立的片段,丢失了片段之间的时序关系和逻辑关联。
所以我们的核心思路转变是:从“检索增强生成”走向“Agent 驱动的记忆管理”。区别在哪?传统 RAG 是被动的,用户问什么就检索什么,检索完就完事。Agentic-RAG 是主动的,Agent 自己会判断:当前这个任务需要用到哪些记忆?这些记忆之间有没有冲突?需不需要先做一轮记忆的整合和推理?检索回来的信息够不够?不够的话要不要换个策略再检一次?这个“主动判断”的能力,是解决手机端记忆问题的关键。打个比方,传统 RAG 像是一个图书馆管理员,你说要什么书他就去架上找;Agentic-RAG 更像是一个私人助理,他不仅知道书在哪,还记得你上次借了什么、你最近在研究什么方向、甚至能判断你这次要的书和上次那本是不是同一个主题。
2. 记忆系统的分层架构设计
2.1 为什么一定要做分层
在讲具体实现之前,我想先聊聊架构设计的思路。我们试过很多种方案,最后发现分层是唯一可行的路径。原因很简单:不同类型的记忆,它的生命周期、访问频率、精度要求、存储成本都不一样。如果你用一套统一的机制去管理所有记忆,要么是杀鸡用牛刀导致性能拉胯,要么是精度不够导致体验糟糕。
我们的分层方案大致是这样的:最底层是原始记忆层,存储用户的所有原始交互记录,包括对话文本、操作日志、时间戳、设备状态等。这一层的特点是数据量大、写入频繁、但读取频率相对较低。中间层是结构化记忆层,从原始记忆中抽取出来的实体、事件、偏好、关系等结构化信息。比如“用户偏好靠窗座位”就是一个结构化记忆,“2024年3月15日预定了去杭州的高铁”是一个事件记忆。最上层是工作记忆层,也就是当前对话或任务相关的短期记忆,容量很小但访问极其频繁。
这个分层不是拍脑袋想出来的,而是被实际需求逼出来的。早期我们只有一层,所有记忆都做 embedding 存向量库。结果发现两个问题:一是向量库越来越大,手机存储扛不住;二是检索的时候,很多高频访问的记忆(比如用户的姓名、常用地址)每次都要走一遍向量检索,浪费算力。分层之后,高频记忆放在工作记忆层,用键值对直接读取,毫秒级返回;中频的结构化记忆用轻量级索引;低频的原始记忆才走向量检索。整体响应速度提升了将近三倍。
2.2 各层的核心职责与交互关系
原始记忆层我把它定位为“不可变的事实来源”。所有写入这里的数据都是 append-only 的,不做修改,只做追加。这样做的好处是数据永远可追溯,不会因为某次错误的更新导致记忆丢失。存储格式上,我们用的是分段压缩的 JSONL,每段按时间窗口切分,比如每 1000 条记录或每 24 小时切一段。查询的时候先定位时间段,再在段内做检索。这个设计参考了日志系统的思路,实测下来在手机上的读写性能很稳。
结构化记忆层是整个系统的核心。它承担了三个关键职责:实体消解、事件抽取、偏好归纳。实体消解是指把“我”“我的”“本人”这些指代统一映射到同一个用户 ID;事件抽取是从对话中识别出“时间+地点+动作+对象”的四元组;偏好归纳则是从多次交互中总结出用户的稳定偏好。这一层的更新是增量的,每次有新对话进来,先做一轮抽取,然后和已有的结构化记忆做合并。合并的时候要处理冲突,比如用户之前说喜欢靠窗,后来说想试试靠走廊,那就要根据时间戳判断哪个是最新偏好。
工作记忆层最简单也最复杂。简单在于它的数据结构就是一个固定大小的环形缓冲区,存最近 N 轮对话的摘要和当前任务的状态。复杂在于它的淘汰策略——什么信息该保留,什么该丢弃,直接影响到 Agent 的短期表现。我们试过 LRU(最近最少使用)、LFU(最不经常使用)、以及基于重要性的加权淘汰,最后发现混合策略效果最好:高频访问的实体信息长期保留,低频的对话细节按时间淘汰,任务相关的状态在任务结束后保留一个衰减窗口。
三层之间的交互是通过一个记忆路由器来协调的。Agent 每次需要记忆的时候,不是直接去某一层查,而是把查询请求交给路由器,由路由器判断应该去哪些层、用什么策略检索。这个判断逻辑本身也是可学习的,我们用一个轻量级的分类模型来做路由决策,输入是当前查询的特征(长度、是否包含指代、是否涉及时间、是否涉及偏好等),输出是各层的检索权重。这个模型很小,量化之后不到 2MB,推理耗时在 5ms 以内。
3. Agentic-RAG 的核心机制拆解
3.1 从被动检索到主动记忆管理
传统 RAG 的流程是线性的:用户提问 → 生成查询向量 → 检索 top-k → 拼接 prompt → 生成回答。这个流程最大的问题是没有反馈闭环。检索回来的东西质量如何?够不够用?有没有冲突?传统 RAG 不关心,它只管检,检完就扔给模型。Agentic-RAG 的核心区别在于,它在检索和生成之间插入了一个记忆评估与再检索的环节。
具体来说,Agent 拿到初步检索结果后,会做三件事。第一,相关性评估:这些记忆和当前问题真的相关吗?我们用一个交叉编码器(cross-encoder)来打分,比单纯的向量相似度准得多。第二,充分性判断:这些记忆足够回答问题吗?如果不够,缺的是什么?是时间信息、地点信息、还是某个实体的属性?第三,冲突检测:检索回来的记忆之间有没有矛盾?比如两条记忆对同一个事件的时间描述不一致。如果检测到冲突,Agent 会触发一轮澄清检索,去原始记忆层找更多的上下文来判断哪个是对的。
这个机制听起来简单,但实现起来有很多细节。我举一个实际例子。用户问:“我上次说的那个餐厅,帮我订个位。”初步检索召回了三条记忆:一条是“用户提到想吃日料”,一条是“用户和朋友约了周五吃饭”,一条是“用户之前去过一家叫‘山葵’的日料店”。这三条记忆单独看都有点相关,但都不完整。Agent 的评估环节会发现:缺少“那个餐厅”的具体指代。于是触发再检索,去原始记忆里找最近提到的餐厅名称,最终定位到“山葵”。然后充分性判断发现还缺人数信息,再检索一次,找到“和朋友”这个线索,推断是两人。最后把“山葵、周五、两人”这些信息整合起来,生成订位请求。
3.2 记忆的写入、更新与遗忘策略
记忆系统不只是读,写同样重要。什么时候该写入新记忆?什么时候该更新旧记忆?什么时候该遗忘?这三个问题决定了记忆库的质量。我们的策略是事件驱动写入 + 定时批量更新 + 重要性衰减遗忘。
事件驱动写入是指,不是每一轮对话都写入记忆,而是当检测到“值得记住的事件”时才写。什么算值得记住?我们定义了几类触发条件:用户明确表达偏好(“我喜欢…”)、用户做出承诺或计划(“我明天要…”)、用户纠正之前的说法(“不对,应该是…”)、以及涉及重要实体(人名、地点、时间)的交互。这个判断用一个轻量级分类器来做,准确率大概在 92% 左右,误判的主要是反讽和假设性语句。
定时批量更新是指,结构化记忆层不是实时更新的,而是每隔一段时间(我们设的是 5 分钟)做一次批量合并。为什么要批量?因为实时更新会导致频繁的读写冲突,而且单条更新的信息量太少,容易产生碎片化的记忆。批量更新可以把短时间内相关的多条记忆一起处理,做更好的实体消解和事件合并。这个间隔是可以调的,5 分钟是我们实测下来在手机功耗和记忆新鲜度之间的平衡点。
遗忘策略是最容易被忽视但极其重要的部分。手机存储有限,不可能无限增长。我们的遗忘曲线参考了艾宾浩斯遗忘曲线的思路,但做了修改:访问频率越高、最近访问时间越近、情感强度越大的记忆,衰减越慢。具体公式是:保留概率 = 基础衰减 × 频率因子 × 新近度因子 × 情感因子。基础衰减按天计算,频率因子是访问次数的对数,新近度因子是距上次访问天数的倒数,情感因子由对话的情感分析得分决定。低于阈值的记忆会被归档到冷存储,再低于更低阈值就彻底删除。实测下来,这个策略能在保留 95% 有用记忆的同时,把存储占用控制在原始方案的 40% 左右。
3.3 端云协同的记忆同步机制
手机端做记忆,绕不开端云协同的问题。全放端侧,算力和存储不够;全放云端,隐私和延迟受不了。我们的方案是端侧为主、云端为辅、按需同步。端侧保存完整的原始记忆和结构化记忆,云端只保存脱敏后的聚合统计和模型更新。同步是双向的但不对称的:端侧向云端同步的是脱敏后的使用模式(用于改进路由模型和检索策略),云端向端侧同步的是模型参数更新和公共知识库的增量。
同步的触发条件也很讲究。我们不是定时同步,而是在设备空闲、充电、连接 WiFi 的时候才做批量同步。这样既不影响用户体验,也不消耗移动数据。同步的内容会做差分压缩,只传变化的部分。实测下来,一个活跃用户每天的同步数据量在 200KB 左右,完全可以接受。
隐私方面,所有原始对话和结构化记忆在端侧都是加密存储的,密钥由设备的安全芯片管理。云端拿到的数据是经过差分隐私处理的,无法反推到具体用户。这个设计在当时还引起过内部争议,有人觉得太保守会影响模型效果,但后来证明,用户对隐私的敏感度远超我们的预期,这个保守设计反而成了产品的一个卖点。
4. 实操落地中的关键环节与参数调优
4.1 记忆抽取的 Prompt 工程与模型选型
记忆抽取是整个系统的入口,抽不准后面全白搭。我们试过三种方案:纯规则、小模型微调、大模型 Prompt。最后的选择是大模型 Prompt 做冷启动,小模型微调做线上。原因很简单:大模型抽取质量高但延迟大、成本高,适合用来生成训练数据;小模型经过微调后,在特定任务上能达到大模型 90% 以上的效果,但延迟只有十分之一。
Prompt 的设计有几个关键点。第一,输出格式必须严格结构化,我们用 JSON Schema 约束输出,字段包括事件类型、时间、地点、人物、动作、对象、情感倾向。第二,要提供 few-shot 示例,而且示例要覆盖各种边界情况,比如否定句、条件句、指代句。第三,要明确告诉模型什么不该抽,比如闲聊、重复确认、系统提示等。我们踩过的一个坑是,早期 Prompt 没有排除系统消息,结果把“正在加载”这种提示也抽成了事件记忆,污染了记忆库。
小模型的选型上,我们对比了几个轻量级模型,最终选了一个 1.5B 参数量的模型做 4-bit 量化,在手机 NPU 上的推理速度大概是 30 tokens/秒。微调数据是我们用大模型跑了 5 万条真实对话生成的,人工校验了 5000 条做测试集。微调后的 F1 分数从基线的 0.72 提升到了 0.89,主要提升在时间表达式和指代消解上。
4.2 向量检索的索引构建与压缩技巧
手机上的向量检索,最大的约束是内存。一个 768 维的 float32 向量是 3KB,10 万条记忆就是 300MB,手机根本扛不住。所以向量压缩是必选项。我们试过几种方案:PCA 降维、乘积量化(PQ)、标量量化(SQ)。最后用的是PQ + SQ 的混合方案:先用 PQ 把 768 维压到 96 维,再用 SQ 把 float32 压到 int8。压缩后的向量只有 96 字节,是原来的 3%。检索精度损失在 3% 以内,完全可接受。
索引结构上,我们用的是HNSW 的简化版。标准 HNSW 的图结构太占内存,我们做了两个优化:一是限制了每层的最大连接数,从默认的 32 降到 16;二是对图结构做了量化压缩,把邻居 ID 从 int64 压到 int32。这样索引的内存占用从每千条 2MB 降到了 0.6MB。检索速度上,单次 top-10 检索在手机上的耗时大概是 8ms,完全满足实时性要求。
还有一个容易被忽视的点是索引的增量更新。HNSW 的插入是支持增量的,但删除很麻烦。我们的做法是标记删除 + 定期重建。标记删除就是在向量上打一个 tombstone 标记,检索时跳过;当 tombstone 比例超过 20% 时,触发一次后台重建。重建在设备空闲时进行,用户无感知。
4.3 记忆路由的决策逻辑与阈值设定
记忆路由是 Agentic-RAG 的大脑,它决定了每次查询去哪些层、用什么策略。我们的路由决策是一个两阶段的流程:粗筛 + 精排。粗筛用规则,精排用模型。
粗筛规则很简单:如果查询里包含“我叫什么”“我的地址”这类明确的属性查询,直接走工作记忆层的键值对;如果包含时间词(“昨天”“上周”),优先走结构化记忆层的时间索引;如果查询很长或者包含多个实体,走原始记忆层的向量检索。这些规则覆盖了大概 70% 的查询,剩下的 30% 交给精排模型。
精排模型是一个 3 层的 MLP,输入是查询的 embedding 加上一些手工特征(长度、是否含指代、是否含时间、是否含情感词等),输出是各层的权重。训练数据是我们人工标注的 2 万条查询-层匹配对。模型的准确率在 85% 左右,主要错误集中在跨层查询上——有些查询需要同时查多层,模型有时候会漏掉某一层。我们的补救措施是设置一个最低权重阈值,任何一层的权重低于 0.1 就不查,高于 0.1 的层都会查一遍,最后把结果合并。这个阈值是调出来的,0.1 是我们实测下来在召回率和延迟之间的最佳平衡点。
5. 常见问题排查与实战避坑指南
5.1 记忆冲突与时效性问题的处理
记忆冲突是实际运行中最常见的问题。我遇到过最典型的一个 case:用户先说“我下个月要去北京”,过了几天又说“北京那个行程取消了”。如果两条记忆都保留,Agent 就会困惑到底去不去。我们的解决方案是引入记忆的状态机。每条事件记忆都有一个状态字段:active、cancelled、completed、expired。当检测到“取消”类意图时,不是删除旧记忆,而是把旧记忆的状态改为 cancelled,同时写入一条新的状态变更记录。检索的时候,默认只返回 active 状态的记忆,但状态变更记录会作为上下文一起返回,让模型知道发生了什么。
时效性问题是另一个坑。用户说“明天要开会”,这个“明天”是相对于说话时刻的。如果记忆系统只存了“明天”这个字符串,过了一天再检索就会出错。我们的做法是所有时间表达式在写入时就归一化为绝对时间戳,同时保留原始文本作为元数据。检索的时候,如果查询涉及时间,会同时匹配绝对时间戳和相对时间描述。这个归一化用了一个专门的时间解析模型,支持中文的复杂时间表达,比如“下下周三下午”“这个月底之前”等。
还有一个隐蔽的问题是记忆的过度泛化。比如用户说过一次“我喜欢吃辣”,系统就把它归纳为“用户喜欢辣的食物”,然后每次推荐餐厅都推川菜。但用户可能只是那天想吃辣,并不是长期偏好。我们的解决方案是给偏好记忆加一个置信度分数,基于出现的频率、时间跨度和情感强度来计算。置信度低于阈值的偏好不会用于主动推荐,只在用户明确询问时才作为参考。
5.2 端侧性能瓶颈的定位与优化
手机端的性能问题,定位起来比服务端麻烦得多,因为你不能随便加日志、不能随时抓包。我们总结了一套分层定位法:先看是 IO 瓶颈、计算瓶颈还是内存瓶颈。
IO 瓶颈的典型表现是首次查询慢、后续查询快。因为首次查询要读磁盘,后续走缓存。如果发现这个模式,就要优化存储格式和缓存策略。我们把原始记忆的存储从 JSON 改成了 MessagePack,读取速度提升了 40%;缓存用了 LRU + 预读,根据用户的使用习惯提前加载可能用到的记忆段。
计算瓶颈的典型表现是所有查询都慢,而且 CPU 占用高。这时候要看是 embedding 计算慢还是检索慢。Embedding 计算慢的话,考虑用量化模型或者缓存 embedding 结果;检索慢的话,考虑减小索引规模或者降低检索精度。我们遇到过一次检索慢的问题,最后发现是 HNSW 的 efSearch 参数设太大了,从 200 降到 64 之后,速度提升了一倍,精度只掉了 1%。
内存瓶颈的典型表现是应用被系统杀掉或者频繁 GC。手机上的内存管理很严格,我们的经验是整个记忆系统的内存占用要控制在 50MB 以内。超过这个数,在低端机上就很容易出问题。优化手段包括:向量用 int8 存储、索引分片加载、及时释放不再使用的缓存。我们还做了一个内存监控模块,当内存超过阈值时自动触发清理。
5.3 用户无感知的记忆更新策略
用户最讨厌的事情之一,就是感觉到系统在“偷偷摸摸”做什么。记忆更新如果让用户感知到了,比如突然弹个提示说“我记住了你的偏好”,反而会让人觉得不舒服。我们的原则是静默更新、可查可删、透明可控。
静默更新是指,记忆的写入和更新都在后台完成,不打扰用户。但用户可以在设置里查看所有被记住的信息,也可以一键删除。这个“可查可删”的设计很重要,它给了用户控制感,即使他们从来不用这个功能,知道它存在也会更放心。
透明可控是指,当 Agent 使用某条记忆来回答问题时,如果用户追问“你怎么知道的”,Agent 要能说出这条记忆的来源和时间。这个功能我们叫记忆溯源,实现上就是在生成回答的时候,把用到的记忆 ID 一起返回,前端可以展示“根据你 3 天前说的…”。这个功能上线后,用户对 Agent 的信任度明显提升。
还有一个细节是记忆的冷启动。新用户刚装上应用,记忆库是空的,Agent 表现得像个陌生人。我们的做法是提供一个可选的“初始偏好设置”,让用户勾选一些基本偏好(比如饮食偏好、出行偏好),这些会作为初始记忆写入。同时,前几轮对话会有一个探索期,Agent 会更主动地询问和确认,加快记忆积累。实测下来,有冷启动引导的用户,达到“好用”状态的时间从平均 7 天缩短到了 2 天。
6. 演进方向与个人实操体会
这套系统从最早的朴素 RAG 到现在,前后迭代了四个大版本。如果让我总结最重要的一个经验,那就是:手机端的记忆系统,核心不是技术有多先进,而是对资源约束的敬畏。你在服务端可以随便用的方案,到手机上可能完全跑不通。每一个技术选型,都要先问一句:这个在低端机上跑得动吗?内存够吗?耗电吗?
另一个深刻的体会是,记忆的质量比数量重要得多。我们早期追求“记住一切”,结果记忆库膨胀到几百 MB,检索精度反而下降。后来做了大量的遗忘和压缩,记忆量减少了 60%,但 Agent 的表现反而更好了。这让我意识到,记忆系统的本质不是存储,而是筛选和整理。就像人的大脑,遗忘不是缺陷,而是功能。
后续的演进方向,我个人比较看好两个。一是多模态记忆,现在只处理文本,未来要把图片、语音、位置都纳入记忆体系。比如用户拍了一张餐厅的照片,Agent 要能记住“用户去过这家餐厅”并且关联到视觉特征。二是跨设备的记忆同步,用户在手机上的记忆,能不能无缝流转到平板、手表、车机?这涉及到分布式记忆一致性的问题,比单设备复杂得多,但价值也大得多。
最后分享一个实操中的小技巧。我们在调试记忆检索的时候,发现一个很有效的办法是构造“记忆探针”:设计一组标准问题,覆盖各种记忆类型和检索路径,每次改动后跑一遍探针,看召回率和准确率的变化。这个探针集我们维护了 200 多条,成了迭代过程中最可靠的回归测试工具。如果你也在做类似的事情,强烈建议从第一天就开始积累自己的探针集,它会帮你省下大量排查时间。