随着大模型能力的提升越来越多的应用不再满足于做一个一问一答的 ChatBot而是想做成能长期陪伴用户、记住用户偏好、跨会话理解用户意图的 Agent。但大模型本身并没有真正意义上的记忆它每一次回答都只是基于当前输入的 Prompt 做一次推理所谓的“记住了什么”其实都是开发者把历史信息重新组织好之后又喂给了模型。于是一个 Agent 做得好不好很大程度上并不取决于底层模型而取决于它背后那套记忆管理系统设计得是否合理。在做这类系统的过程中我越来越确信一件事一套成熟的记忆系统绝不是简单地把聊天记录存进数据库再原样发回去而应该像人脑一样对信息做分类、筛选、归纳和关联。本文记录的是一套面向 LangChain、LangGraph 或类似 Agent 框架的分层记忆设计思路包括记忆该分几层、多主题对话怎么处理、跨主题的背景信息怎么共享、超长输入怎么切分以及最后怎么在有限的 Token 预算里拼出一个真正有效的 Prompt。系统全貌记忆是怎么被用起来的在拆开每个模块之前先把整套流程过一遍后面每一节其实都是在填这张图里的某一格。用户发来一条消息系统先判断这条消息的长度如果在模型能直接处理的范围内就走 Topic Routing判断它属于哪个正在进行的话题或者要不要开一个新话题如果消息长到明显超过模型上下文比如用户直接甩过来一份合同或者一段日志就先进入切分和摘要流程不会直接怼给模型。否是用户输入长度是否超过模型上下文Topic Routing切分成 Chunk 并建索引递归摘要 Chunk Summary 到 Final Summary检索 Topic 历史摘要检索 Global Memory检索 Constraint Memory检索相关 ChunkContext Builder 打分排序与预算裁剪拼装 PromptLLM 生成回答记忆提取写回 Memory / Constraint刷新 Topic Summary图中每一个节点后面都会在对应小节里展开切分摘要对应第四节Topic 相关的检索对应第三节Context Builder 的打分和拼装对应第五节记忆提取和写回则对应第五节最后一部分。确定话题之后系统会检索与这个话题相关的记忆包括话题自己的历史摘要、跨话题的全局背景以及可能影响当前话题的约束条件。这些检索出来的信息连同用户当前的问题一起交给 Context Builder 拼装成最终发给模型的 Prompt。模型给出回答之后流程并不会就此结束系统会从这轮对话里再提取一遍看看有没有产生新的偏好、约束或事实把这些更新写回记忆库同时刷新话题摘要。整个过程形成一个闭环用户的每一次输入既是这一轮的问题也是下一轮记忆的原材料。二、记忆该存什么而不是聊天记录本身这一部分先解决一个最基础也最容易被忽视的问题也就是记忆系统里到底应该存什么东西。很多团队一开始都会走一条最直接的路把所有聊天记录原样存库下次聊天再整段发给模型这条路在 Demo 阶段没有问题但走得越远问题越大因此需要先说清楚它错在哪再引出应该怎么分层存放。2.1 为什么不能直接保存聊天记录把历史聊天原样保存并全部回灌给模型会随着对话轮数增加暴露出几个问题。Token 会越来越多大量与当前问题无关的历史仍然占据着上下文模型容易被这些旧信息干扰甚至给出混乱的回答同时成本和响应时间也会持续上升。这个问题在实际项目里往往不是线性恶化而是突然爆发比如某个用户和 Agent 已经聊了两三个月某一天历史消息拼接起来直接超过了模型的上下文上限请求直接报错这时候如果没有提前设计好的降级方案只能临时在代码里粗暴截断最早的消息而这些被截断的内容里很可能藏着用户之前明确交代过的重要偏好截断之后模型就会“失忆”用户明显能感觉到助手前后不一致。更本质的问题是人类其实也不是这样记事情的昨天有人和你一起吃饭聊天一个月后你大概率想不起完整对话内容但你会记得这个人喜欢吃川菜或者他正在装修房子。人类的大脑天生就会对信息做压缩和筛选只保留对未来有用的那部分这不是记忆力不好而是一种进化出来的效率机制。如果把这套机制类比到 Agent 上聊天记录相当于人的“短时感知”而真正沉淀下来的用户画像才相当于长期记忆两者不应该用同一种方式存储也不应该用同一种方式检索。人类记住的是事实不是对话记录一个好的 Agent 也应该按这个方式设计记忆而不是简单地把数据库当成聊天记录的备份盘。2.2 记忆保存的是事实不是对话具体来说如果用户说“今天突然想吃辣”这句话如果原样写入长期记忆模型以后可能一直认为用户口味偏辣但它其实只是一次临时表达不该进入长期记忆。真正应该保存的是经过提取后的结构化信息比如用户连续多次表达喜欢现代简约风格之后可以沉淀成一条{type:user_preference,content:用户偏好现代简约装修风格}而如果用户明确说过家里养了一只猫这类信息属于稳定事实会影响后续很多话题值得单独记录成{type:constraint,content:用户家里有猫,applies_to:[装修,家具,清洁]}这两条记录的区别在于前者是偏好后者是会跨话题生效的约束applies_to字段决定了这条记忆在哪些话题下应该被自动召回这个设计在后面讲 Constraint Memory 时会再展开。判断一条信息值不值得沉淀本质上是问模型给出回答之后该不该做“记忆提取”如果只是一句“今天想吃火锅”这种一次性表达一般不需要进入长期记忆这一步会在后面的记忆更新策略里再具体说明。实践中判断“值不值得记”比想象中麻烦单次表达和稳定偏好之间没有一条清晰的界线。比较稳妥的做法是给同一类信息设置一个出现次数或者时间窗口的门槛比如同一个偏好在最近三次相关对话里都被提到或者用户用了“一直”、“平时”、“通常”这类强调持续性的词才把它从候选提升为正式记忆而像“今天”、“突然”、“心情不好想吃”这类带明显时间限定或情绪色彩的表达即使被记录下来也应该打上较低的confidence并设置较短的expires_at让它随时间自然过期而不是长期占用上下文预算。另外要注意用户明确说出的信息和模型推测出的信息置信度不应该相同例如用户直接说“我家有猫”属于高置信度事实而模型从“猫抓板”“猫粮”这类词推测出用户养猫就应该标记成较低置信度后续在 Context Builder 打分时也要体现这种差异。2.3 分层记忆短期、中期、长期在“存事实而不是存对话”这个前提下记忆本身也不该是扁平的一堆事实而应该按生命周期分层这样才能既保证当前上下文的连续性又避免历史数据无限增长。短期记忆负责当前会话只保存最近几轮聊天、当前任务状态和一些临时变量生命周期很短可以直接放在内存、Redis 或者 LangGraph 的 Checkpointer 里不需要做长期持久化。这一层的设计目标是低延迟读写因为每一轮对话都要访问它一般不需要向量检索直接按时间顺序取最近 N 轮或者按 Token 数截断即可LangGraph 里可以直接依赖checkpointer保存的state不用另外建表。中期记忆保存近期项目和最近几天的重要聊天摘要比如用户最近一直在讨论装修装修相关的信息就适合放在这一层可以设置 TTL比如七天、三十天或者随项目结束而失效避免无限增长。中期记忆本质上是按话题分组维护的摘要等下一节引入话题这个维度之后会看到它其实就对应到每个话题各自的历史摘要。这一层比短期记忆活得久但不是永久保留如果一个话题超过设定的 TTL 都没有被重新触发比较合理的处理方式不是直接删除而是先归档或者进一步压缩成一条更短的摘要这样即使用户几个月后突然又提起装修系统还能从归档里找回大致背景而不是完全从零开始。长期记忆保存的是稳定事实比如用户的职业、装修预算、语言偏好、家庭情况这些信息不会因为一次聊天轻易改变更适合作为用户画像长期保留。长期记忆的写入门槛应该明显高于中期记忆因为一旦写错纠正成本很高用户很可能不会主动提起“我并没有说过我是程序员”这类错误只能靠后续对话里的矛盾信息去修正因此长期记忆的每条记录都应该保留来源比如是哪一轮对话、哪一句话触发的提取方便出问题时回溯。三层记忆各自负责不同时间尺度的信息短期保证连续性中期承载正在进行的任务长期沉淀稳定画像三者的读写频率和一致性要求也不一样短期允许频繁覆盖中期允许周期性刷新长期则应该谨慎更新、频繁读取。三、主题管理让 Agent 在多个话题间自由切换分好层之后还有一个现实问题现实中的聊天不会一直围绕同一个主题用户可能上午聊装修下午聊宠物晚上又聊工作第二天继续回到装修。如果系统只按时间顺序保存聊天模型很容易在装修话题里引用宠物的内容或者在讨论工作时又扯出装修预算回答会越来越离谱这一节要解决的就是怎么给聊天引入“主题”这个维度以及主题之间怎么互相借用信息。3.1 多主题聊天带来的上下文混乱解决办法是给每条消息标记一个 Topic比如装修、宠物、工作、旅游、健身每个 Topic 各自维护自己的摘要、关键词、Embedding 和历史记忆。当用户再次讨论装修时不需要恢复整段聊天记录只需要恢复“装修”这一组历史即可这样既能减少 Token 消耗也能显著提升上下文的准确性。这里可以举一个更具体的反例来说明不分 Topic 会出什么问题。假设用户前一天连续问了十几条关于换工作的问题聊到薪资谈判、offer 对比、离职交接第二天突然问“阳台那边放个书架合适吗”如果系统仍然把最近几十条消息原样塞进上下文模型很可能把“书架”和前一天的工作话题强行关联给出诸如“考虑到你正在权衡两个 offer建议先不要急着添置家具”这种驴唇不对马嘴的回答。引入 Topic 之后这条关于书架的消息会被路由到“装修”或者新建的“家居”话题下模型看到的上下文就只有装修相关的历史不会被前一天的工作话题干扰。3.2 如何判断当前消息属于哪个主题这里有个绕不开的问题系统怎么知道用户这句话该归到哪个 Topic而不是简单按时间顺序堆在最后一个话题下面。比较实用的做法是先对当前这句话做 Embedding去和已有 Topic 的摘要做相似度检索找出最相关的几个候选。如果候选里有明显更接近的一个再交给模型做最终判断因为语义相似不代表真的是同一件事比如聊装修预算和聊旅游预算向量距离可能很接近但其实是两个话题这一步交给模型判断会比单纯依赖向量距离更可靠。如果所有候选的相似度都很低说明用户开了一个新话题系统就新建一个 Topic而不是勉强塞进某个旧话题里。这套两阶段的路由可以简化成下面这样一段伪代码先做向量召回缩小候选范围再交给模型做语义确认。defroute_topic(message,existing_topics,embed,llm_classify,threshold0.75):query_vecembed(message)# 先用向量相似度粗筛候选话题避免每条消息都要过一遍 LLMcandidatestop_k_by_cosine_similarity(query_vec,existing_topics,k3)ifnotcandidatesorcandidates[0].scorethreshold:returncreate_new_topic(message)# 相似度接近的候选交给模型做最终判断防止“预算”这类词把不同话题误判成同一个returnllm_classify(message,candidates)这里的threshold需要根据实际数据调设得太低会导致新话题很难被建立用户明明换了话题系统却把它塞进旧话题里历史摘要被无关内容污染设得太高又会导致同一个话题被反复拆成多个新 Topic检索时反而找不全历史。比较稳妥的做法是先用一批真实对话跑离线评估观察不同阈值下误合并和误拆分的比例再结合线上反馈慢慢调整而不是一开始就拍脑袋定一个数字。这套流程跑起来之后用户其实感觉不到“话题”这个概念的存在他们只是自然地在几个话题之间来回切换系统在背后默默把每句话归位。3.3 Topic 不能完全隔离但仅仅按 Topic 分类还不够甚至可以说如果 Topic 划分得越干净隔离带来的副作用反而越明显。假设用户连续聊了一个月养猫然后突然开始讨论装修虽然整个过程中用户从没说过“装修的时候请考虑猫”但一个合格的助手应该意识到家里有猫的话装修建议应该优先考虑地板耐抓、家具耐磨、猫砂盆位置这些细节。如果装修 Topic 只加载自己的历史摘要完全看不到宠物 Topic 里的信息那么无论装修话题聊得多深入模型也不会主动提到猫因为它压根不知道这个背景存在。也就是说宠物这个 Topic 实际上会影响装修 TopicTopic 之间不能完全隔离这就需要引入独立于 Topic 之外的两层记忆专门负责把这类跨话题才有意义的信息挑出来单独管理。3.4 Global Memory 承载跨话题背景第一层是 Global Memory用来保存那些会影响很多不同话题的背景信息比如用户职业、家庭情况、预算、城市、语言偏好、健康状况、是否有宠物、是否有老人小孩。这些信息不属于某一个具体 Topic而是整个用户画像的一部分无论后面讨论装修、家具还是旅行只要相关这些背景都可以自动参与上下文构建。Global Memory 和长期记忆经常被混为一谈但两者的检索方式其实不太一样。长期记忆更多是按 Topic 或者按需检索出来的具体事实而 Global Memory 更像是每次对话都默认加载的一小段用户画像摘要类似于系统提示词的一部分不需要额外做相似度检索。实践中可以把 Global Memory 维护成一段结构化的简短文本例如“用户是一名后端工程师居住在上海家中有一只猫预算相对宽裕”每次构建 Prompt 时都直接拼进去而不是每次都重新检索一遍这样既省了一次检索开销也保证了不同 Topic 看到的背景是一致的。当然Global Memory 也不能无限增长字段太多之后同样需要做筛选和压缩避免它自己也变成一份臃肿的档案。3.5 Constraint Memory 管理约束条件第二层是 Constraint Memory专门管理会限制回答方向的约束条件比如预算只有二十万、租房、有猫、家里有老人、对花粉过敏、厨房面积很小。这些约束通常不对应某个具体 Topic却会影响很多话题的回答质量因此建议单独维护一张 Constraint 表每条记录至少包含以下字段。content约束的具体内容比如“用户家里有猫”confidence这条约束的置信度来自用户明确表达还是模型推测importance重要程度决定它在打分排序时的权重applies_to会影响哪些 Topic比如装修、家具、清洁updated_at最近一次更新时间用于判断是否过期以后进入装修 Topic 时系统只需要按applies_to字段查一遍 Constraint Memory就能自动把“用户家里有猫”带进上下文模型也就会自然推荐耐抓地板而不是普通木地板。applies_to这个字段值得多说一句它既可以人工预设也可以让模型在提取约束时顺带给出。比如从“我家有猫”这句话提取约束时可以在提取的 Prompt 里直接要求模型输出这条约束可能影响的话题列表模型基于常识大概率会给出装修、家具、清洁这几个选项遗漏或者过度覆盖都是正常现象因此上线初期建议保留人工审核或者允许用户在设置里查看和修正这些约束避免一条判断错误的约束长期影响不相关话题的回答质量。另外约束和偏好不同约束一旦确立通常具有较强的排他性比如“预算只有二十万”这类约束如果被误判后续推荐很容易超出用户实际承受范围因此 Constraint Memory 的更新应该比普通偏好更保守倾向于覆盖而不是简单追加。3.6 Topic 关联图Global Memory 和 Constraint Memory 解决的是背景信息怎么跨话题生效但 Topic 与 Topic 之间其实还有更直接的关联值得单独拎出来说。装修这个话题天然会牵扯到家具、预算、收纳宠物这个话题又会牵扯到家具、清洁、旅行如果把每个 Topic 都当成一个孤立的箱子讨论装修时系统不会主动想到去看看宠物话题里有没有相关信息即便 Constraint Memory 里已经记着用户家里有猫。更好的做法是把 Topic 之间的关联也显式记录下来形成一张图而不是一条单向链条装修连着家具、预算、收纳、宠物宠物又连着家具、清洁、旅行、装修。这张图既可以人工预设一批常见领域的关联规则比如装修和家具、宠物和清洁这类常识性关联可以直接写死也可以随着系统运行不断积累比如发现某个用户的装修话题和旅游话题反复出现相似的检索命中就把这两个 Topic 之间的边权重加高。有了这张图检索记忆时就不再只看当前话题自己的历史还能顺着关联边去看相邻话题里有没有值得带进来的信息具体做法通常是在检索当前 Topic 的历史之外再按边的权重取一到两个关联最紧密的 Topic各自召回少量相关内容一并纳入候选池而不是把所有关联 Topic 的全部历史都拉进来否则又会回到“信息太多挤占预算”的老问题。这和 Constraint Memory 按影响范围广播是两回事Constraint Memory 解决的是某条具体事实该不该被别的话题看到Topic 关联图解决的是话题本身的检索路径该怎么走两者配合起来才能让系统在装修话题里自然想起猫、想起预算而不需要用户重复说一遍。四、超长内容的处理切分、检索与递归摘要主题和记忆解决的是“对话该怎么组织”但还有另一类问题跟对话轮数无关而是单次输入本身就超出了模型能处理的长度比如聊天轮数堆积到几百轮或者用户一次性甩过来几十万 Token 的日志、合同、代码仓库。这一节要解决的是这类超长内容怎么在不丢信息的前提下塞进有限的上下文里。4.1 超长聊天历史怎么办聊天轮数越积越多模型上下文终究有限正确做法不是删除历史而是总结历史。比如最早的三百轮聊天可以压缩成一句“用户一直在讨论装修重点关注预算、客厅、灯光和收纳”然后只保留最近几十轮原始聊天真正发给模型的是历史摘要加最近聊天这样既保留了连续性又不会浪费大量 Token。这个总结过程不建议做成一次性任务而应该做成滚动更新。比较常见的做法是设一个滑动窗口比如始终保留最近二十轮原始消息一旦超出这个窗口就把被挤出去的那部分消息追加进已有摘要重新生成一版新摘要而不是等聊天堆到几百轮才一次性总结一次性总结成本高而且模型面对几百轮原始文本时容易遗漏中间部分的信息。滚动摘要的另一个好处是可以让摘要本身携带一些结构比如按“当前进展”“已确定事项”“待解决问题”分段总结这样即使原始聊天已经被丢弃模型也能从摘要里判断出当前任务推进到了哪一步而不只是一段模糊的背景描述。4.2 用户一次性发送超长内容怎么办更极端的情况是用户直接发来几十万 Token 的日志、一份合同或者一个代码仓库这些内容显然不能直接塞给模型。正确的处理链路是先完整保存原文前端展示时使用完整内容然后按规则切分成 Chunk对每个 Chunk 建立向量索引必要时再做摘要形成 Chunk 到 Chunk Summary 再到 Group Summary 最后到 Final Summary 的层级结构。真正进入模型的是摘要和与当前问题相关的原文片段而不是全部原始内容这样既保证信息完整性又能实现精确检索。切分规则本身也有讲究不能简单按固定字符数硬切。日志类内容适合按时间戳或者按单条日志记录切分保证每个 Chunk 是一条完整的日志而不是被从中间截断合同类内容适合按条款编号切分保证第八条不会被拆到两个 Chunk 里检索命中时至少能拿到完整的一条代码仓库则适合按文件甚至按函数切分并在每个 Chunk 里附带文件路径和所属类名之类的元信息方便后续检索结果能定位到具体位置。切分粒度太细会导致检索命中的片段缺乏上下文太粗又会让无关内容混进本该精确命中的 Chunk 里一般建议给每种内容类型单独调切分参数而不是全局用同一套规则。4.3 摘要本身也超长怎么办如果 Chunk Summary 数量本身也多到超过上下文限制答案很简单继续摘要也就是让摘要本身也可以递归。这是典型的 Map-Reduce 思路先分别总结各个分组再总结这些总结直到最终结果能放入模型上下文为止。这种递归摘要不仅适用于超长文档也同样适用于前面提到的超长聊天历史。递归摘要的层数不是越多越好每多压缩一层细节丢失就会累积一次因此实践中一般会限制递归深度比如最多做两到三层如果三层之后总结果仍然超出预算说明单纯靠摘要已经无法解决问题这时候应该转而依赖下一节讲的 Chunk 检索来补细节而不是继续无限制地压缩摘要。另外Map 阶段各个分组的总结应该尽量保持独立避免让某个分组的总结依赖另一个分组的上下文否则并行处理时容易出现信息错位Reduce 阶段汇总时也更难判断哪些细节是可以舍弃的。4.4 Chunk 不只是用来摘要的还要用于精确检索递归摘要能解决信息太多装不下的问题但摘要有个副作用越往上层压缩细节丢得越多如果用户问的恰好是一个具体细节摘要就帮不上忙了。比如用户上传的是一份合同几轮摘要之后系统记得的是“这是一份租赁合同涉及押金、违约责任、维修义务”但如果用户问第八条第三款的违约金具体是多少摘要里根本不会保留这么细的数字。这时候真正该做的不是让摘要写得更细而是回到 Chunk 这一层去做检索原始文本切出来的每个小块本身都建了 Embedding 索引针对用户这句具体的问题去找最相关的几个 Chunk把命中的原文片段连同摘要一起交给模型。摘要负责让模型知道文档整体是什么、背景是什么Chunk 负责把命中的原文细节递过去两者分工不同摘要保连贯Chunk 保精确缺一个都不行。这套摘要加检索的组合本质上和检索增强生成RAG的思路是一致的区别只在于这里的“知识库”不是固定的外部文档而是用户当次对话临时上传的内容生命周期通常和这次任务绑定任务结束后可以归档或者清理索引。实践中还有一个细节容易被忽略用户追问细节时除了命中的 Chunk 本身最好再带上它前后相邻的一到两个 Chunk因为很多细节的完整含义依赖上下文比如违约金条款前面可能有一句“除非另有约定”如果只召回命中的那一个 Chunk模型给出的答案可能忽略掉这类前提条件反而造成误导。五、Context Builder在有限 Token 内组装最优上下文前面几节分别解决了记忆该存什么、话题怎么组织、超长内容怎么切分但这些信息最终都要汇总到同一个地方也就是真正发给模型的那个 Prompt。这一节讲的是这个汇总环节该怎么做包括候选信息怎么打分、Token 预算怎么算、模型回答之后记忆又该怎么更新以及支撑这一整套系统的数据结构大致长什么样。5.1 候选信息打分与 Token 预算能进入 Context Builder 候选池的信息已经不少了短期上下文、当前 Topic 的摘要、关联 Topic 带来的信息、Global Memory、Constraint Memory、检索出来的相关 Chunk如果全部塞进去很可能又超过模型的上下文长度所以在真正拼 Prompt 之前还需要做两件事一是给候选打分排序二是算清楚还有多少预算可用。打分可以综合当前问题的相似度、记忆本身的重要程度、更新时间的新旧、以及提取时的置信度一个常见的组合方式是scoresimilarityimportancerecencyconfidence \text{score} \text{similarity} \text{importance} \text{recency} \text{confidence}scoresimilarityimportancerecencyconfidence按分数排序后只取排名靠前的部分而不是一股脑全部塞进去。预算这边则要先算清楚模型的上下文总长度减去系统提示词减去要给模型输出留的空间剩下的再分给记忆和最近几轮聊天可用预算模型上下文长度−系统提示词−输出预留−记忆预算−最近聊天 \text{可用预算} \text{模型上下文长度} - \text{系统提示词} - \text{输出预留} - \text{记忆预算} - \text{最近聊天}可用预算模型上下文长度−系统提示词−输出预留−记忆预算−最近聊天如果候选记忆的总长度已经超出这部分预算就得继续做裁剪而不是等发给模型时才发现超长报错。这两步做完Context Builder 才真正知道自己手上有哪些牌可以打也知道这些牌加起来占多少格子。打分公式里每一项的权重也不是一成不变的不同类型的问题应该调整权重的侧重点。用户问一个具体细节问题时相似度应该占更高权重因为这时候找到最相关的那条记忆或者 Chunk 比覆盖面更重要用户开启一个全新话题时重要程度和跨话题的约束反而更值得优先展示因为这时候还没有明确的相似度信号可以依赖。裁剪预算时也要注意留出余量实际调用模型时 System Prompt 和工具调用描述往往也会占用不少 Token如果预算计算时只考虑了记忆和聊天历史忽略了这部分固定开销线上还是会偶尔遇到超长报错比较稳妥的做法是把这部分固定开销也当成一项预留额度提前扣除而不是等真正拼接时才发现算漏了。5.2 Context Builder 的职责打完分、算完预算之后Context Builder 的职责就是在这个预算之内根据当前问题动态组合系统提示词、当前短期上下文、当前 Topic 摘要与历史、Global Memory、Constraint Memory、与当前问题最相关的 Chunk以及用户当前的输入最终拼出一个信息密度足够高的 Prompt。它不是简单拼接聊天记录而是每一轮都要重新判断哪些信息该进、哪些该舍这也是现代 Agent 和传统聊天机器人最大的区别之一。实现上Context Builder 通常会被拆成几个明确的阶段先并行发起各类检索Topic 历史、Global Memory、Constraint Memory、相关 Chunk 各自互不依赖可以同时查询以降低延迟再统一汇总候选并按前面提到的打分公式排序最后按预算截断同时保证被截断的部分优先舍弃分数最低的候选而不是简单按加入顺序砍掉末尾。这个阶段结束后拼出来的 Prompt 结构应该是稳定的比如固定按系统提示词、全局背景、约束条件、话题摘要、相关片段、最近聊天、当前问题这样的顺序排列这样即使每次填充的内容不同模型也更容易适应固定的 Prompt 结构减少因为格式变化带来的效果波动。5.3 回答之后的记忆更新模型给出回答之后流程并没有结束系统还要做一次记忆提取判断这轮对话里有没有出现新的偏好、约束、项目状态或者长期事实。如果只是“今天想吃火锅”这类临时表达一般不需要写入长期记忆只有反复出现或者用户明确表达的信息比如“我家养了一只猫”才值得沉淀成结构化记忆并挂上对应的applies_to和confidence。这一步做完之后Topic 的摘要也需要同步更新让下一次进入这个话题时能拿到最新的背景。记忆提取本身一般也是靠一次单独的模型调用完成输入是这一轮的用户消息和模型回答输出是结构化的候选记忆列表同时要求模型给出memory_type、confidence和可能的applies_to。这一步容易踩的坑是让提取模型和对话模型共用同一次调用直接在回答末尾附带一段记忆提取结果这样做省了一次调用但会导致输出格式不稳定有时候模型会把提取结果混进正常回答里泄露给用户。更稳妥的做法是拆成两次独立调用对话模型只管生成回答提取模型专门负责结构化输出两者互不干扰即使提取失败也不影响当前这轮的正常回答。写回记忆库时还要处理好去重和更新如果某条约束已经存在新提取出的内容只是重复确认应该更新updated_at和confidence而不是重复插入一条内容相同的记录否则同一个事实会在库里越堆越多反而增加后续检索和打分的负担。5.4 推荐的数据结构支撑上面这一整套流程底层至少需要 Conversation、Topic、Message、Chunk、Summary、Embedding、Memory、Constraint、GlobalProfile 这几张表。其中 Memory 表建议统一采用下面这套字段不管是偏好、约束还是全局背景都走同一张表只靠memory_type和scope区分类型。id、user_id、topic_id标识这条记忆归属谁、属于哪个话题memory_type、scope区分偏好、约束、事实等类型以及生效范围content、embedding记忆的具体内容和用于检索的向量confidence、importance置信度和重要程度供打分排序使用created_at、updated_at、expires_at生命周期相关的时间戳applies_to会影响哪些 Topic跨话题生效的关键字段统一用一张表管理各种类型的记忆好处是打分、检索、过期这些逻辑可以复用同一套代码不需要针对偏好、约束、全局背景各写一遍。除了 Memory 表本身Topic 表一般还需要维护summary、keywords、embedding、last_active_time和指向关联 Topic 的边表Chunk 表则需要保留source_id指回原始文档、sequence记录在原文中的顺序方便命中之后回溯到前后文。这些表之间的关系并不复杂核心思路是让 Message 归属 TopicMemory 和 Constraint 可以关联到具体 Topic 也可以是全局的Chunk 归属某一次超长输入Summary 则可能同时对应 Topic 或者 Chunk 分组只要把这几条关系理清楚具体用关系型数据库还是文档数据库实现都是次要问题向量部分单独接入一个向量数据库或者带向量索引扩展的关系库即可。六、结语这套设计做下来核心其实可以归纳成几件事。记忆不再是原样保存的聊天记录而是经过提取的结构化事实按短期、中期、长期分层存放多主题对话通过 Topic Routing 和 Topic Memory 各自维护历史同时靠 Global Memory、Constraint Memory 和 Topic 关联图让不同话题之间的背景信息能够互相借用超长输入通过切分、向量检索和递归摘要处理既不丢细节也不超预算最后由 Context Builder 在有限 Token 内对所有候选信息打分排序动态拼出真正相关的 Prompt而不是把历史一股脑塞进去。即使未来模型的上下文窗口继续变大这套记忆架构也不会因此失去意义因为更大的上下文并不等于更好的回答真正决定 Agent 表现的是它能否从大量信息里挑出当下最相关、最可靠的那一部分。这才是 Agent 从一个能连续对话的接口走向一个真正拥有长期记忆和持续学习能力的助手需要迈出的一步。