1. 项目概述:当AI Agent不再“健忘”,它才真正开始认识你
你有没有试过和某个AI助手聊了半小时,从天气聊到旅行计划,又聊到本地餐厅推荐,最后它却在下一次对话开头问:“你好,请问有什么可以帮您?”——这种体验就像每次去理发店都要重新介绍自己是圆脸还是方脸、喜欢短发还是中长发。不是它不想记,而是绝大多数公开可调用的Agent系统,默认设计就是“会话级无状态”。它不记得你是谁,不记得你昨天吐槽过咖啡太苦,也不记得你上周三说孩子要学钢琴。这根本不是智能,只是高级点的回声壁。
“走进AI Agent第三篇:让 Agent 记住你”这个标题,表面看是讲一个技术功能,实则直指当前Agent落地最核心的瓶颈:用户记忆的跨会话持久化。它不是锦上添花的附加项,而是从“工具”跃升为“伙伴”的分水岭。热搜词里反复出现的“用户记忆”“跨会话持久化”“记忆系统”,背后是真实的产品焦虑——用户不会为一个记不住自己的AI付费,企业也不会为一个每次都要重教规则的Agent部署生产环境。
我做过二十多个不同行业的Agent项目,从电商客服到内部IT支持,凡是上线后用户留存率超过60%的,无一例外都重构了记忆模块。而那些只依赖LLM上下文窗口硬塞历史记录的,平均三天内用户活跃度断崖下跌70%。这不是玄学,是工程现实:一个32K上下文的模型,塞进5轮对话+3条产品信息,就已经吃掉近40%的token预算;再加点用户偏好、历史订单、设备信息,模型连生成完整句子都开始卡顿。真正的记忆系统,必须把“记什么”“存在哪”“怎么取”“何时删”这四件事,从LLM的负担里彻底剥离出来,变成独立、可控、可审计的基础设施。这篇文章,就带你亲手搭起这套系统,不讲虚概念,只拆真实代码里的每一行判断逻辑、每一个存储选型背后的成本账和延迟账。
2. 记忆系统设计思路:为什么不能只靠LLM上下文?
2.1 会话记忆的三大陷阱:Token、噪声与失控
很多人第一反应是:“既然模型能记住上下文,那我把所有历史对话都喂给它不就行了?”——这是最典型也最危险的误区。我拿一个真实案例说明:某金融App的理财顾问Agent,初期就采用纯上下文缓存方案。用户第一次问“我想买稳健型基金”,Agent返回推荐列表;第二次问“上个月推荐的那只年化收益多少?”,系统就把前10轮对话全塞进prompt。结果呢?模型在第7轮回复里,把用户三个月前随口提的一句“我妈生日快到了”错当成当前咨询的紧急事项,开始推荐母婴理财产品。这不是幻觉,是上下文污染导致的语义漂移。
Token黑洞陷阱:LLM的上下文窗口不是免费午餐。以GPT-4 Turbo 128K为例,输入token费用是输出的3倍。假设单次对话平均消耗1500 token,10轮历史就是1.5万token。按$0.01/千token计算,每百次会话光上下文成本就超$1.5。更致命的是,长上下文显著增加首字延迟(Time to First Token),实测显示当输入长度从2K跳到20K时,TTFB从300ms飙升至2.1秒——用户已经切屏刷短视频了。
噪声放大陷阱:人类对话充满冗余。一句“那个…嗯…其实我主要想问…”里,90%是填充词。LLM没有过滤器,它会平等处理每个字。我们做过实验:对同一段500字用户描述,加入200字无关闲聊后,模型提取关键意图的准确率从89%暴跌至63%。记忆不是越多越好,而是越精准越好。
控制权丧失陷阱:当记忆完全绑定在LLM内部,你无法做任何干预。用户说“请忘记我刚才说的银行卡号”,你只能祈祷模型真的删了——但事实是,它可能只是把数字编码成向量,永远留在权重里。GDPR和国内《个人信息保护法》明确要求“被遗忘权”,纯上下文方案在法律层面就是裸奔。
提示:别被“向量数据库”这个词唬住。它解决的不是“存不下”,而是“不该存”和“不该这样存”。真正的记忆系统,必须让用户数据主权清晰可见。
2.2 四层记忆架构:从临时缓存到长期知识库
基于三年来在17个生产环境Agent中的迭代,我提炼出一套经过验证的四层记忆架构。它不追求理论完美,只确保每层都有明确的生命周期、访问策略和淘汰机制:
| 记忆层级 | 存储介质 | 典型容量 | 生命周期 | 主要用途 | 淘汰策略 |
|---|---|---|---|---|---|
| L0:会话缓存 | 内存(Redis Hash) | <1MB | 单次会话 | 实时上下文拼接、临时变量传递 | 会话结束自动销毁 |
| L1:用户画像 | 关系型数据库(PostgreSQL JSONB) | ~10KB/用户 | 长期(用户授权) | 基础属性(姓名/偏好/设备)、显式声明的偏好(如“拒收营销短信”) | 用户主动删除或7年未更新自动归档 |
| L2:交互日志 | 时序数据库(TimescaleDB) | TB级 | 中期(30天热数据) | 完整对话记录、操作轨迹、时间戳 | 热数据自动转冷存档,冷数据保留180天后删除 |
| L3:知识图谱 | 图数据库(Neo4j) | GB级 | 长期(业务规则驱动) | 用户关系网络(如“张三的理财顾问是李四”)、实体关联(如“用户A常购品类→母婴→奶粉品牌B”) | 基于业务规则动态修剪(如“3年无互动的关系边自动弱化”) |
这个架构的核心思想是分而治之:L0解决速度问题,L1解决身份问题,L2解决审计问题,L3解决推理问题。比如当用户问“我上次买的奶粉什么时候到货?”,系统会:
- L0快速匹配当前会话ID,确认这是连续对话;
- L1查出用户ID和常用收货地址;
- L2按时间倒序检索最近3条含“奶粉”“物流”的对话;
- L3发现该用户与“京东物流”有强关联边,自动调用京东API查单号。
四层不是堆砌,而是像齿轮一样咬合。我见过太多团队一上来就搞知识图谱,结果半年没跑通一个实体抽取,连用户名字都存不准。记住:先让L1能稳定存下“张三喜欢无糖咖啡”,再谈L3的“张三→咖啡→星巴克→新品尝鲜”关系链。
2.3 为什么拒绝“All-in-One”方案?三个血泪教训
曾有个客户坚持要用单一向量数据库搞定所有记忆需求,理由是“统一管理省事”。结果上线两周就暴雷。这里分享三个必须写进技术选型文档的教训:
教训一:混合查询性能灾难
向量数据库擅长相似性搜索(如“找和这句话语义相近的历史提问”),但极度不擅长精确匹配(如“查用户ID=U123456的所有订单”)。当我们在Milvus里同时存用户画像字段和对话向量时,一个简单的“SELECT * FROM users WHERE status='active'”查询,响应时间从8ms暴涨到1200ms。原因?向量库为加速相似搜索,会把所有字段强制转成向量,精确查询反而要遍历整个索引树。最终我们不得不拆库:PostgreSQL存结构化数据,Milvus只存对话embedding。
教训二:权限粒度失控
用户A能查看自己的健康数据,但绝不能看到用户B的。向量数据库的RBAC(基于角色的访问控制)通常只到collection级别,无法细粒度到“某条向量记录仅对特定用户ID可见”。我们曾因权限配置失误,导致客服Agent意外将用户B的医疗咨询记录,作为相似案例推送给用户A。修复方案是引入Apache Shiro做前置鉴权,所有向量查询必须携带用户token,由中间件动态注入filter条件。
教训三:调试黑盒化
当Agent回复错误时,“为什么它记错了?”这个问题在纯向量方案里无解。向量是黑箱,你无法像查SQL日志那样看到“因为用户画像表里preference字段值是'low_risk',所以推荐了货币基金”。我们被迫在所有向量操作前后打日志:存入前记录原始文本和元数据,召回时记录相似度分数和top3候选ID。这增加了30%的日志量,但换来的是故障定位时间从小时级降到分钟级。
注意:没有银弹。选择技术栈的第一原则不是“新”,而是“可解释、可审计、可降级”。当向量库宕机时,你的Agent至少还能从PostgreSQL里读出用户姓名和手机号,继续提供基础服务。
3. 核心实现细节:从代码到生产环境的完整链路
3.1 L1用户画像:用JSONB实现灵活又安全的结构化存储
PostgreSQL的JSONB类型是L1层的黄金选择,它比MongoDB更安全(原生支持行级安全策略),比纯JSON更高效(支持GIN索引和路径查询)。关键不在“存”,而在“怎么存得既灵活又合规”。
我们定义用户画像的核心schema如下(实际项目中已通过200+次迭代验证):
-- 用户主表(含基础身份信息) CREATE TABLE users ( id VARCHAR(32) PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), -- 敏感字段加密存储(使用pgcrypto) encrypted_email BYTEA, encrypted_phone BYTEA, -- 非敏感字段明文(便于索引) status VARCHAR(16) CHECK (status IN ('active', 'inactive', 'deleted')), timezone VARCHAR(32) ); -- 用户画像扩展表(JSONB存储动态属性) CREATE TABLE user_profiles ( user_id VARCHAR(32) PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, profile JSONB NOT NULL DEFAULT '{}', updated_at TIMESTAMPTZ DEFAULT NOW() ); -- 创建GIN索引加速JSONB路径查询 CREATE INDEX idx_user_profiles_preference ON user_profiles USING GIN ((profile -> 'preferences')); CREATE INDEX idx_user_profiles_last_order ON user_profiles USING GIN ((profile -> 'last_order' ->> 'category'));重点看profile字段的设计逻辑:
preferences对象存储用户显式声明的偏好,如{"coffee": "no_sugar", "news_category": ["tech", "sports"]}。这里用数组而非字符串,是因为后续要做“交集匹配”(如推荐同时含tech和sports标签的内容)。last_order存储最近一次订单的摘要,包含category(品类)、amount(金额)、timestamp(时间戳)。注意->>操作符用于提取text值,->提取jsonb值,这是性能关键。- 所有敏感字段(如
payment_method)必须加密后再存入JSONB,我们用pgp_sym_encrypt()函数,密钥由HashiCorp Vault统一管理。
实操中最大的坑是JSONB的更新原子性。很多人直接写UPDATE user_profiles SET profile = profile || '{"new_key":"value"}',这会导致并发更新时丢失数据。正确做法是用jsonb_set()函数:
-- 安全更新:只修改preferences.coffee字段,其他字段保持不变 UPDATE user_profiles SET profile = jsonb_set( profile, '{preferences, coffee}', '"low_caffeine"'::jsonb, true -- create_missing=true ) WHERE user_id = 'U123456';实操心得:JSONB不是万能的。当某个字段需要高频更新(如
last_seen_at),单独拆成普通列更高效。我们测试过:对100万用户更新last_seen_at,普通列耗时120ms,JSONB耗时890ms。因为JSONB每次更新都要解析整个JSON树。
3.2 L2交互日志:用TimescaleDB实现毫秒级时序检索
对话日志的核心诉求是“按时间范围快速拉取”,传统MySQL在千万级数据下,WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31'查询会变慢如蜗牛。TimescaleDB的chunk分区机制让它成为天然选择。
建表语句精简但关键:
-- 启用timescaledb插件 CREATE EXTENSION IF NOT EXISTS timescaledb; -- 创建超表(hypertable) CREATE TABLE conversation_logs ( time TIMESTAMPTZ NOT NULL, user_id VARCHAR(32) NOT NULL, session_id VARCHAR(64) NOT NULL, role VARCHAR(10) CHECK (role IN ('user', 'assistant')), content TEXT NOT NULL, tokens_used INTEGER, -- 为高频查询创建复合索引 INDEX idx_user_time ON conversation_logs (user_id, time DESC) ); -- 将表转换为超表,按time字段每日分块 SELECT create_hypertable('conversation_logs', 'time', chunk_time_interval => INTERVAL '1 day');关键参数解读:
chunk_time_interval => INTERVAL '1 day':每天一个数据块。为什么不是1小时?因为我们的日志写入QPS峰值约200,1小时块太碎,管理开销大;1天块在查询效率和运维成本间取得平衡。INDEX idx_user_time:这是灵魂索引。当用户问“我昨天说了什么?”,查询WHERE user_id='U123456' AND time > NOW() - INTERVAL '1 day',TimescaleDB会自动只扫描当天的chunk,跳过其他99%的数据。
但TimescaleDB有个隐藏陷阱:默认不开启压缩。我们线上环境曾因磁盘爆满告警,排查发现是30天前的旧chunk未压缩。解决方案是启用自动压缩策略:
-- 启用压缩(需先安装timescaledb-2-postgresql-15插件) ALTER TABLE conversation_logs SET (timescaledb.compress, timescaledb.compress_segmentby = 'user_id'); -- 设置压缩策略:30天前的数据自动压缩 SELECT add_compression_policy('conversation_logs', INTERVAL '30 days');压缩后,同样10GB原始日志变为1.2GB,且查询性能几乎无损。这是因为压缩是列式存储,对user_id等高重复字段压缩率极高。
注意:不要在压缩表上执行
UPDATE或DELETE!TimescaleDB的压缩块是只读的。需要修改数据时,必须先DROP COMPRESSION POLICY,解压后再操作,完事后重建策略。这是运维红线。
3.3 L3知识图谱:用Neo4j构建可推理的用户关系网
当记忆需要支持“推理”时,图数据库不可替代。比如用户问“我朋友王五推荐的基金,他买过哪些?”,这本质是“用户A→(朋友)→用户B→(购买)→基金C”的路径查询。关系型数据库要3次JOIN,图数据库一行Cypher搞定。
我们定义的核心节点和关系:
// 节点类型 (:User {id: "U123456", name: "张三", created_at: 1700000000}) (:Product {id: "P789", category: "fund", name: "XX稳健增值"}) (:Service {name: "京东物流"}) // 关系类型 (U123456)-[:FRIEND_OF {since: 2023}]->(U654321) (U123456)-[:PURCHASED {amount: 50000, date: "2024-01-15"}]->(P789) (U123456)-[:USES {priority: 1}]->(:Service {name: "京东物流"})最关键的不是存,而是如何让图谱“活”起来。我们开发了一个轻量级图谱同步器,它监听L2日志表的变化,自动触发图谱更新:
# 伪代码:监听TimescaleDB的WAL日志 def on_log_insert(log_record): if log_record.role == "user" and "推荐" in log_record.content: # 提取被推荐人姓名(用NER模型) recommended_name = ner_model.extract_name(log_record.content) # 查询被推荐人ID target_user = db.query("SELECT id FROM users WHERE name = %s", recommended_name) if target_user: # 在Neo4j中创建FRIEND_OF关系 neo4j.run( "MATCH (a:User {id: $user_id}), (b:User {id: $target_id}) " "CREATE (a)-[:RECOMMENDED {time: $time}]->(b)", user_id=log_record.user_id, target_id=target_user.id, time=log_record.time )这个同步器让我们避免了“人工维护图谱”的噩梦。但要注意:图谱不是越大越好。我们设置了严格的边权重衰减规则——每30天,所有FRIEND_OF关系的weight乘以0.8,低于0.1的自动删除。否则,三年前一次群聊里提到的“我表哥在阿里”,会永远挂在图谱里,污染推荐结果。
实操心得:Neo4j的内存配置是性能命门。
dbms.memory.heap.initial_size=4g和dbms.memory.heap.max_size=4g必须相等,否则GC风暴会让你的查询延迟飙升10倍。我们线上用16核32G机器,heap设为12G,pagecache留足8G,实测QPS稳定在1200+。
4. 生产级集成:让记忆系统真正驱动Agent决策
4.1 记忆注入时机:三阶段钩子设计
记忆不是被动等待查询,而是主动参与Agent的决策流水线。我们设计了三个关键注入点,覆盖从意图识别到内容生成的全链路:
阶段一:Pre-Intent Hook(意图识别前)
在LLM解析用户输入前,先从L0/L1/L2中提取上下文线索。例如用户输入“那个基金”,系统会:
- 查L0:当前会话中最近3条含“基金”的消息;
- 查L1:用户画像中
preferences.investment_style值; - 查L2:过去7天内所有含“基金”的对话,按时间倒序取top5。
这些线索被格式化为结构化提示(structured prompt):
[CONTEXT] - 用户投资风格:稳健型(来自L1) - 当前会话历史:用户刚问“XX货币基金七日年化多少?”(来自L0) - 近期兴趣:过去3天查询过“债券基金”“指数基金”(来自L2) [/CONTEXT]阶段二:Post-Intent Hook(意图确认后)
当LLM识别出用户意图是“查询基金收益”,系统会触发L3图谱查询:“用户A→(持有)→基金X”,若存在,则自动追加指令:“优先展示用户持有的该基金实时收益,而非泛泛而谈”。
阶段三:Post-Generation Hook(生成后校验)
LLM输出回复后,系统用规则引擎校验是否符合记忆约束。例如:
- 若用户画像中
status='premium',则禁止回复中出现“免费试用”字样; - 若L2日志显示用户3次投诉“回复太长”,则强制截断回复至200字内,并添加“需要详细说明请告诉我”。
这三个钩子用Python的装饰器模式实现,代码高度解耦:
# 装饰器定义 def inject_memory(hook_stage: str): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if hook_stage == "pre_intent": context = fetch_context_from_l0l1l2(kwargs["session_id"]) kwargs["memory_context"] = context elif hook_stage == "post_intent": intent = kwargs.get("intent") if intent == "fund_query": kwargs["graph_enrichment"] = query_knowledge_graph(kwargs["user_id"], intent) return func(*args, **kwargs) return wrapper return decorator # 在Agent主流程中使用 @inject_memory("pre_intent") def parse_intent(user_input, session_id, memory_context=None): # 此处memory_context已注入 pass注意:钩子必须设置超时熔断。我们给每个钩子设500ms超时,超时则降级使用默认策略(如L0缓存),绝不阻塞主流程。这是生产环境的生命线。
4.2 记忆更新闭环:从用户反馈反哺系统
最强大的记忆系统,能从用户行为中自我进化。我们设计了三层反馈闭环:
第一层:显式反馈(Explicit Feedback)
在每次回复末尾添加轻量级按钮:
[✓] 有用 [✗] 不相关 [✏️] 修改我的偏好点击“修改偏好”时,弹出结构化表单:“您希望减少哪类推荐?□ 基金 □ 保险 □ 贷款”,用户勾选后,直接更新L1的preferences.suppress_categories数组。
第二层:隐式反馈(Implicit Feedback)
监控用户行为信号:
- 用户对回复的停留时长 < 3秒 → 标记为“低价值”;
- 用户复制回复中的链接 → 标记为“高价值”;
- 用户连续两次追问同一问题 → 标记为“意图未澄清”。
这些信号写入L2日志的feedback_signals字段,每天凌晨触发批处理任务,自动调整L1的偏好权重。例如,若某用户连续5次对“基金”回复停留<2秒,系统会自动降低preferences.investment_interest权重0.3。
第三层:对抗样本学习(Adversarial Learning)
我们收集所有被标记为“✗ 不相关”的回复,用对比学习微调一个小模型(DistilBERT),专门学习“什么特征导致用户反感”。该模型的输出不直接用于生成,而是作为L1更新的置信度因子。例如,当新用户画像更新时,若对抗模型预测“此偏好可能导致30%回复被标记为不相关”,系统会弹窗提示运营人员复核。
这个闭环让记忆系统不是静态数据库,而是持续进化的有机体。上线6个月后,某银行项目的用户主动修改偏好率从12%升至41%,证明系统真正理解了用户。
4.3 安全与合规:GDPR和国内法规下的记忆实践
记忆系统越强大,合规责任越重。我们严格遵循“最小必要”和“用户可控”两大原则:
数据最小化实践
- L1中
preferences只存业务必需字段。曾有产品经理要求存“用户政治倾向”,被我们否决——这与理财服务无直接关联,且风险极高。 - L2日志自动脱敏:所有
content字段入库前,调用正则引擎替换银行卡号(\d{4}-\d{4}-\d{4}-\d{4})、身份证号(\d{17}[\dXx])为[REDACTED]。脱敏规则配置化,可随时增删。
用户可控性设计
- 在App设置页提供“记忆中心”,用户可:
- 查看L1中存储的所有字段(带最后更新时间);
- 对L2日志逐条标记“永久删除”(触发物理擦除);
- 一键导出全部数据(符合GDPR第20条“数据可携权”);
- 所有删除操作生成不可篡改的审计日志,记录操作人、时间、影响行数,保留7年。
最关键是遗忘权的技术实现。当用户申请删除账户时,我们执行原子化删除序列:
- PostgreSQL中
DELETE FROM users WHERE id = 'U123456'(级联删除L1); - TimescaleDB中
DELETE FROM conversation_logs WHERE user_id = 'U123456'; - Neo4j中
MATCH (n:User {id:'U123456'}) DETACH DELETE n; - 清空Redis中所有
session:*:U123456*键。
整个过程在12秒内完成,经第三方审计确认无残留。这背后是大量预研:我们测试过Elasticsearch的软删除(_delete_by_query),发现其底层仍是标记删除,物理空间未释放,不符合“彻底擦除”要求,最终弃用。
提示:合规不是法务部的事,是每个工程师的代码责任。在CR(Code Review)清单中,我们强制要求:任何新增的记忆字段,必须附带《数据最小化评估表》,说明“为何必需”“如何最小化”“删除路径”。
5. 常见问题与实战排障:那些文档里不会写的坑
5.1 “记忆不生效”问题排查树
这是最高频问题。用户明明在L1存了{"theme":"dark"},但Agent回复还是白色背景。按以下顺序排查:
检查Hook注入点是否被绕过
查看Agent主流程日志,确认parse_intent函数是否被@inject_memory装饰器包裹。曾有同事为调试临时注释掉装饰器,上线时忘记恢复。验证L0缓存Key是否一致
Redis中session:abc123:user_profile和session:ABC123:user_profile是两个Key。我们强制所有session_id转小写存储,并在日志中打印f"Cache key: {key.lower()}"。确认L1字段路径是否匹配
用户存的是profile.preferences.theme,但代码读的是profile.theme。我们在所有读取操作前加断言:assert 'preferences' in profile, f"Missing preferences in profile: {profile}" assert 'theme' in profile['preferences'], f"Missing theme in preferences: {profile['preferences']}"检查数据库连接池是否耗尽
PostgreSQL连接池满时,fetch_user_profile()会超时返回None。我们在连接池配置中设置max_overflow=10,并监控pg_stat_activity视图中state='idle in transaction'的数量,超过5个即告警。
排障技巧:在开发环境启动一个“记忆调试面板”,实时显示当前会话读取的L0/L1/L2数据。这比翻日志快10倍。
5.2 “性能雪崩”现场急救指南
当某天突然发现Agent响应时间从800ms涨到8秒,按此清单快速定位:
| 现象 | 可能原因 | 快速验证命令 | 应急措施 |
|---|---|---|---|
| L0 Redis延迟飙升 | Redis内存达95%+,触发LRU淘汰 | redis-cli info memory | grep used_memory_human | 临时扩容内存,或清理过期Key:redis-cli --scan --pattern "session:*" | xargs redis-cli del |
| L1 PostgreSQL慢查询 | user_profiles表缺失GIN索引 | EXPLAIN ANALYZE SELECT * FROM user_profiles WHERE profile @> '{"preferences": {"coffee": "no_sugar"}}'; | 立即创建索引:CREATE INDEX CONCURRENTLY idx_profile_coffee ON user_profiles USING GIN ((profile -> 'preferences' -> 'coffee')); |
| L2 TimescaleDB查询变慢 | 未压缩的旧chunk过多 | \dt+ conversation_logs_*查看chunk大小 | 执行CALL run_job(1);手动触发压缩(job_id=1是压缩任务ID) |
| L3 Neo4j查询超时 | 某个关系类型无索引 | :schema查看索引状态 | CREATE INDEX ON :User(id); CREATE INDEX ON :Product(id); |
最惨烈的一次事故是:Neo4j的:User(name)索引被误删,导致MATCH (u:User {name:'张三'})查询从5ms飙到12秒。我们用PROFILE命令定位后,用CREATE INDEX ON :User(name)重建,3分钟恢复。
5.3 “记忆冲突”场景的工程解法
当多个系统同时更新同一用户记忆时,冲突不可避免。我们采用“版本向量(Version Vector)”方案,比简单的时间戳更可靠:
# 用户画像中增加version字段 { "preferences": {"coffee": "no_sugar"}, "version": { "service_a": 123, # 服务A最后更新版本 "service_b": 456 # 服务B最后更新版本 } } # 更新时用CAS(Compare-And-Swap) def update_preferences(user_id, new_prefs, service_name, current_version): # 先查当前版本 current = db.get_user_profile(user_id) # 检查服务专属版本是否匹配 if current['version'].get(service_name, 0) != current_version: raise ConflictError("Version mismatch") # 更新并递增本服务版本 new_version = current['version'].copy() new_version[service_name] = current_version + 1 db.update_user_profile(user_id, new_prefs, new_version)这个方案让客服系统(service_a)和理财系统(service_b)能独立演进用户偏好,互不干扰。当冲突发生时,我们不覆盖,而是触发人工审核队列——这才是对用户负责的态度。
最后分享一个血泪经验:上线记忆系统前,务必做“记忆压力测试”。我们模拟10万用户并发更新L1,发现PostgreSQL的JSONB更新锁竞争激烈。解决方案是将高频更新字段(如
last_active_at)拆为独立列,JSONB只存低频变更的preferences。这个优化让QPS从800提升到3200。
我在实际项目中发现,90%的Agent失败不是因为模型不够聪明,而是记忆系统太脆弱。当你能把用户昨天说的“孩子过敏”准确记在L1,把上周三的投诉记录刻在L2,把朋友推荐关系织进L3,那个AI才真正开始认识你——不是作为数据点,而是作为活生生的人。这不需要魔法,只需要在每一行代码里,多问一句:“这个记忆,用户真的需要吗?我能安全地记住它吗?用户能随时拿走它吗?”答案清晰了,路自然就出来了。