ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent会话内记忆:让AI在连续对话中不“失忆”的工程指南

Agent会话内记忆:让AI在连续对话中不“失忆”的工程指南 你有没有遇到过这种场景自己写的Agent单轮问答表现还行但只要用户开始说“刚才那个结果再解释一下”“把上一段的语气调得正式一点”“我前面说过我不吃辣”它就完全掉线。不是模型不够聪明而是你的Agent没有记忆。这篇笔记想先把Agent记忆体系里最基础的一层拆开讲透——会话内记忆也就是让Agent在同一个对话任务里能记住自己说过什么、用户提过什么、中间拿到过什么结果。这个能力是所有AI应用的地基不管你做的是聊天机器人、代码助手还是数据分析Agent绕不开这一层。适合正在做Agent开发、刚接触Agent框架或者想系统补齐Memory知识链路的朋友。1. 会话内记忆解决的不是“记忆”而是“会话的连续性”1.1 没有记忆的Agent本质上只是个“高级函数”大多数Agent API在设计上都是无状态的。你给它一段输入它返回一段输出这次调用和下一次调用之间没有任何关联。从工程角度看Agent就是一个函数输入是prompt输出是文本。这种设计的好处是简单可靠坏处就是你没法指望它自己记得上一轮聊了什么。我在早期做Agent原型时犯过一个特别典型的错误。我以为只要把用户当前的问题丢给模型模型就能像人一样理解“刚才”指的是什么。结果用户连续问了三轮“那个方案的成本是多少”“如果量再翻倍呢”“那换成另一个方案呢”模型在前两轮还能接住到第三轮就完全不知道“另一个方案”是哪个。原因很简单我没有把前两轮的上下文传给模型模型眼中的世界永远只有当前这一句话。所以会话内记忆的第一层价值就是让Agent从“每次重新开始”变成“连续对话”。它把用户在这一轮会话里说过的话、模型自己的回答、工具返回的结果按顺序组织起来再一起交给模型去生成下一次回复。1.2 会话内记忆的边界它只管当前对话不管明天你是否还记得这里需要先把概念边界划清楚。会话内记忆英文里也常叫conversation memory或short-term memory它的生命周期是“一次会话”。用户打开聊天窗口到关闭或超时这之间发生的所有消息都属于会话内记忆的管辖范围。一旦会话结束这层记忆理论上就该被清理或压缩。和它相对的是长期记忆long-term memory也就是Agent跨会话记住用户的偏好、历史事实、项目背景。比如用户昨天让你写了个爬虫脚本今天回来说“把昨天的脚本加个重试机制”这需要的是长期记忆已经超出会话内记忆的范畴了。这两者经常被搞混导致很多人在设计Agent时把所有历史数据一股脑塞进一个巨大的存储里既不区分会话边界也不做生命周期管理最后模型的表现反而更差。记住一句话会话内记忆解决的是“这一场对话能不能聊得下去”长期记忆解决的是“下一次对话还能不能接上”。这篇笔记只聊前者后者放在系列后面再展开。2. 会话内记忆的实现方式三大流派的选择2.1 全量拼接最简单但最贵的上下文管理最直观的实现方式就是把整个对话历史消息列表全部传给模型。也就是每次请求时把系统提示词、用户消息、助手消息、工具结果按时间顺序全部拼进prompt里。我见过不少初学者包括我自己一开始就是这么干的。代码写起来非常快def build_prompt(system_prompt: str, history: list[dict], current_query: str) - list[dict]: messages [{role: system, content: system_prompt}] messages.extend(history) messages.append({role: user, content: current_query}) return messages这样做的优点是信息完全保真。模型能看到每一轮对话的原始内容不会因为摘要而丢失细节。缺点也很明显token消耗会随着对话轮数线性增长。假设每轮对话平均消耗500个token聊到20轮时一次请求就要往模型里塞10000多个token的历史消息。如果模型上下文窗口是8k那很快就撑爆了就算模型支持128k成本也会让你肉疼。所以全量拼接适合对话轮数少、单轮信息量低的场景比如客服工单初审、简单的FAQ问答。一旦你的Agent需要完成复杂任务全量拼接必然会撞上上下文窗口的天花板。2.2 滑动窗口用截断换空间却容易“失忆”既然历史消息不能全留那就留最近的一部分。滑动窗口的思路是只保留最近N条消息更早的消息直接丢弃。这个N可以是按消息条数算也可以按token数算。比如用LangChain里的ConversationBufferWindowMemory就是典型的窗口记忆。你把k设为10它就只保留最近10条消息。优点是token占用可控实现也简单缺点是一旦早期消息中包含关键信息比如用户说“不要用Python我用的是Java”后面第11轮时这条信息被丢弃了Agent就会忘掉这个约束继续给出一堆Python代码。实际项目中窗口大小怎么定是个经验活。我自己的做法是按token而不是条数来截断。因为条数差异太大了同样一条消息用户可能只说了“好的”也可能贴了两千字的日志。按token截断更稳定。一般我会先把系统提示词和最近几轮消息加进去然后从最久远的消息开始逐条移除直到总token数小于某个阈值比如上下文窗口的60%。2.3 摘要记忆与结构化记忆让Agent自己整理笔记全量拼接是“全记住”滑动窗口是“记最近的”。那有没有既保留关键早期信息、又控制token量的办法有就是对早期的对话做摘要。摘要记忆的思路是当对话历史过长时先把老消息丢给一个LLM让它生成一段概括性的文本这段文本代替老消息作为“历史摘要”保留下来后续再跟模型对话时模型看到的是“以前的对话内容是……接下来是最近几轮原始消息”。这样既保留了核心信息又不会让token无限膨胀。结构化记忆则更进一步。它不只是总结成一段话而是把对话里的关键实体、用户偏好、任务目标、约束条件等抽成结构化的字段比如JSON。经典场景是用户说“我叫小明喜欢简洁风格的回复”Agent就记录{user_name: 小明, style_preference: 简洁}之后每次都用这条结构化记忆去约束模型输出。这两种方式不是互斥的我在实际项目里经常叠加使用底层用滑动窗口控制消息条数老消息再定期抽成摘要同时把最关键的几条用户偏好单独存成结构化字段在系统提示词里永远保留。3. 工程落地会话内记忆的取舍与关键参数3.1 上下文窗口不是越大越好token预算与成本核算很多人以为模型上下文窗口越大会话内记忆就越简单因为可以塞更多历史了。但实际工程里上下文窗口是共享资源不是给你白嫖的。你需要把窗口拆成几份系统提示词通常不变可能占用500-2000 token会话内记忆历史消息你要管理的就是这部分工具调用结果如果Agent会调搜索、代码执行器返回结果可能一下子吃掉几千token当前轮用户输入不能截断模型输出必须预留空间否则可能生成一半就被截断。所以可用记忆空间 模型总窗口 − 系统提示词 − 工具结果 − 用户输入 − 输出预留。举个例子你用的是128k上下文窗口的模型系统提示词占2k用户输入占1k工具结果占10k模型输出预留8k那么真正能用来放历史消息的只有大约107k。如果每轮平均消耗800 token大概能存130轮。听起来不少但如果你在Agent里塞了一份大文档或者工具返回了详细API响应历史消息可能几轮就能把预算耗尽。因此我的建议是别等到窗口满了再处理先设定一个软阈值比如达到可用记忆空间的60%就开始做压缩。这样能避免一次请求触顶带来的截断风险。3.2 系统提示词、历史消息和工具结果怎么排布在大模型API里消息顺序会影响输出质量。我自己踩过的一个坑是把工具返回结果放在了用户消息后面结果模型误以为工具结果就是用户说的内容导致回答方向跑偏。后来我养成了一套固定的排布规范系统提示词永远在最前面里面可以包含固定的指令、角色设定、长期约束历史消息按时间顺序排列消息类型要区分清楚——用户消息、助手消息、工具消息不能混用角色字段工具结果紧跟在触发工具调用的那一条助手消息后面保持因果链清晰最新一条用户消息放在最后紧跟着就是模型生成区。这种排布方式对大多数模型都适用。如果你用的是支持原生tool calling的API工具消息和助手消息的角色字段一定要按规范填不要图省事全部填成user。3.3 什么时候该触发摘要压缩阈值与策略摘要压缩不是每轮都要做做太频繁反而增加成本和延迟。我一般用一个简单的策略每次append新消息后计算当前历史消息总token数如果超过预设阈值就触发一次压缩。触发阈值怎么定我会把它设成可用记忆空间的70%。比如通过上面的公式计算出可用记忆空间是100k那历史消息达到70k时就压缩。压缩时的具体做法是把最老的一半消息交给一个专门的“总结模型”或同一个模型要求生成一段200-500字的摘要用这段摘要替换掉那部分老消息只保留系统提示词、摘要、以及最近N条完整消息清理解析出来的结构化记忆把新的关键信息合并进已有的用户画像字段。这套做法在成本和时间上都是可控的。压缩只会在会话长度达到临界点时触发不会频繁发生。而且一旦触发后面很长一段时间内历史消息都不会再爆。4. 从代码层面看会话内记忆一个最小可运行的设计4.1 数据模型设计消息列表、角色标记和时间戳会话内记忆落到代码上核心是一个消息列表。但列表里的每条消息不能只存一个content字符串。我在实践里至少会保存这些字段role角色是system、user、assistant还是toolcontent消息内容timestamp时间戳便于按时间排序和做超时清理metadata附加信息比如消息对应的会话ID、工具调用ID、消息来源等。用Python的dataclass来定义会很清晰from dataclasses import dataclass, field from typing import Any, Dict import time dataclass class Message: role: str content: str timestamp: float field(default_factorytime.time) metadata: Dict[str, Any] field(default_factorydict)4.2 记忆操作的三个核心函数append、compact、rollback我把会话内记忆的常用操作抽象成三个核心函数。append负责把新消息加入列表compact负责对老消息做摘要压缩rollback负责回滚到某个历史节点这在Agent生成失败或用户撤回时很有用。class ConversationMemory: def __init__(self, system_prompt: str, max_history_tokens: int 20000): self.messages [Message(rolesystem, contentsystem_prompt)] self.max_history_tokens max_history_tokens def append(self, role: str, content: str, **metadata) - None: self.messages.append(Message(rolerole, contentcontent, metadatametadata)) def compact(self, summarize_fn) - None: # 保留系统提示词和最近 10 条更早的部分压缩成摘要 keep_count 10 old_messages self.messages[1:-keep_count] if len(old_messages) 5: return summary summarize_fn(old_messages) summary_msg Message(rolesystem, contentf[历史摘要] {summary}) self.messages [self.messages[0], summary_msg] self.messages[-keep_count:] def rollback(self, message_id: int) - None: self.messages self.messages[:message_id]实际使用中compact里的summarize_fn可以是任何能处理消息列表并返回文本的函数。我在项目里用的是同一个Agent模型给它一个固定的提示词“请总结以下对话中与当前任务相关的关键信息包括用户的明确要求、已确认的结论、尚未完成的事用简洁的中文分点列出。”4.3 多轮对话中的状态管理如何避免记忆串线如果你只做一个单用户的命令行Agent用上面这个类就够了。但一旦要部署成Web服务就需要考虑多用户多会话并发的情况。最典型的错误是把所有人的消息都塞进同一个ConversationMemory实例里导致用户A的问题被用户B看到。解决办法是给每一轮会话分配一个唯一的session_id然后用一个字典或独立存储来维护多个会话的记忆对象。简单来说就是一个MemoryStoreclass MemoryStore: def __init__(self): self._sessions {} def get_memory(self, session_id: str, system_prompt: str) - ConversationMemory: if session_id not in self._sessions: self._sessions[session_id] ConversationMemory(system_prompt) return self._sessions[session_id]更进一步你还可以给每个session存一个last_active_time定期清理长时间不活跃的会话避免内存泄漏。5. 实际开发中踩过的坑上下文污染、记忆漂移和重复输出5.1 工具调用结果混入记忆导致的角色混乱这是一个非常隐蔽的坑。我在做一个联网搜索Agent时把搜索API返回的网页摘要接到了用户消息后面结果模型认为这些内容都是用户自己说的于是回答时说“根据您提供的资料……”听着就很怪。后来我专门排查才发现消息列表里没有区分“用户输入”和“工具输出”。工具返回的内容是模型生成回复所需的资料但在记忆里它应该是独立的tool角色。解决办法很简单append时严格区分角色不要怕麻烦。只要角色标记正确模型就能理解“这段内容是工具返回的事实不是用户说的话”。5.2 Agent“忘了”前提条件上下文被截断后的隐性失败有一次我在做代码生成Agent用户在第一轮提供了自己的项目技术栈“用JavaSpring Boot”后面聊了十几轮都是关于数据库设计的细节。结果滑动窗口把第一轮的消息截掉了模型在回答中开始推荐Python方案。用户当场就疑惑了。这不是模型笨而是记忆管理策略的问题。解决办法有两个方向一是给记忆压缩加上“关键信息保护”机制。在生成摘要时专门抽取出用户偏好、硬性约束单独固化到系统提示词里而不是等到它们被截断后才补救。二是把滑动窗口做得“软”一点。不是无脑丢最老的消息而是先判断哪些消息包含关键约束把它们保留或升级到系统提示词中。5.3 测试会话内记忆的方法构造连续依赖的多轮用例会话内记忆最怕的就是“看似在记实际没记”。我在项目里会专门写一组多轮依赖的测试用例保证记忆逻辑不是摆设。最基本的三类用例信息保持让用户在第一轮抛出一个事实比如“我的昵称是阿哲”然后隔三轮问模型“我的昵称是什么”看能否正确回答。约束记忆第一轮说“回复都控制在50字以内”聊十轮后再次验证输出长度。任务状态记忆让Agent先完成一个子步骤比如“先计算2024年的销售额”再让它基于结果做下一步“对比2023年的增长率”确认Agent没有忘掉前面计算出的数值。这些用例手写起来很简单但能帮你躲过绝大部分“记忆失效”的回归问题。6. 会话内记忆之后从对话记忆走向长期记忆的过渡6.1 会话内记忆与会话级总结的关系会话内记忆不会一直存在但会话结束后它留下的信息不应该被直接丢掉。更合理的做法是在会话即将结束时把这轮对话生成一份结构化总结存到长期存储中。这样下次用户再发起会话时Agent可以先读取历史总结再结合新的问题继续工作。这个“总结-存储-回放”的链路是会话内记忆演化为长期记忆的关键一步。我通常会用一个大模型在会话结束时对整段对话做概括内容包括用户的核心目标、已经达成的结论、遗留的待办事项、重要的偏好与约束。然后以JSON或Markdown格式存入数据库。6.2 什么时候该上长期记忆场景与成本边界如果你只是在做一次性问答工具没必要上长期记忆。但如果你发现用户会重复提到同一个项目、同一个需求或者你的Agent需要在多次对话中持续跟踪一个长期任务那就该考虑长期记忆了。上长期记忆的典型场景包括个人知识助手、代码库长期维护Agent、客户关系管理助手等。它们都需要跨会话记住用户身份、历史偏好和项目上下文。但长期记忆不是免费的存储、检索、更新都会带来额外复杂度和成本。我的建议是先明确你产品的核心场景是否真的需要跨会话记忆如果只是“多聊几轮不懵”那当前这套会话内记忆已经足够。6.3 为后续系列留个引子记忆分层的完整蓝图把会话内记忆理解清楚之后就可以尝试构建一个完整的记忆分层系统。我设想的体系里最底层是原始消息日志中间是会话内记忆短期记忆再往上是会话级摘要最顶层是长期语义记忆和用户画像。每一层都有不同的生命周期、访问频率和存储方式。后面的笔记我会继续拆解长期记忆的实现方式包括向量数据库检索、知识图谱记忆、记忆写入与遗忘策略等。如果你也正在折腾Agent Memory欢迎在评论区聊聊你在会话内记忆上踩过的坑我大概率也碰到过。最后分享一点我的个人经验会话内记忆看着简单做起来很容易翻车。别一上来就想着搞复杂框架先用一个消息列表把流程跑通再按需加入滑动窗口和摘要压缩。你能看到很多Agent框架内置了ConversationBufferMemory、ConversationSummaryBufferMemory它们都挺方便的但只有你亲手把每个字段、每次压缩的触发条件调过一遍遇到问题时才真正知道该改哪里。
返回列表