ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent记忆四层模型:Context、Memory、Session、State

Agent记忆四层模型:Context、Memory、Session、State 做 AI Agent 开发的同学大概率都遇到过这样的尴尬场景一个电商客服 Agent用户刚说完“我上周买的订单还没发货”Agent 回答了查询方式用户紧接着说“那我要退掉”。结果 Agent 像失忆一样反问“请问您的订单号是多少”用户瞬间火大“你不是刚问过吗”这种“边做边忘”的现象本质不是模型太笨而是 Agent 的记忆体系没有设计好。真正让 Agent“越用越聪明”的不是换一个更大的模型而是把 Context、Memory、Session、State 这四层概念分清楚并且用工程手段把它们组合起来。很多人把这四个词混为一谈导致代码越写越乱回答越来越“飘”。本文会把四者的区别、分工、落地方式和超限处理讲透最后带大家手把手实现一个最小可用的电商客服助手。读完之后你能在这些方面有明确收获第一彻底分清 Context、Memory、Session、State 的区别第二知道短期状态和长期记忆分别该存什么、怎么存第三掌握上下文超限的四种处理策略第四跑通一个带记忆能力的电商客服示例项目。1. 先看一个场景Agent 为什么总是“边做边忘”设想你正在开发一个电商客服 Agent。用户第一次进来说“你好我上周买的那双鞋怎么还没到”Agent 根据订单号查到了物流信息答复“您的订单正在运输中预计明天送达”。用户又说“那我明天不在家想申请退款来得及吗”此时如果 Agent 没有保存状态这一轮它可能完全不知道“那双鞋”是哪双鞋也不知道用户刚才已经查过物流。这不是个别现象。很多 Agent 项目在演示时表现很好一到真实长对话就崩原因通常出在三个层面1.1 会话内的短期记忆缺失用户在当前会话里说过的话Agent 没有完整传给模型。一些开发者在拼接 prompt 时只放了系统指令和最近一条用户消息模型自然无法理解上下文。这个问题看似低级却非常普遍。1.2 跨会话的长期记忆缺失用户今天咨询完明天再来Agent 已经完全不认识他。用户画像、历史订单、商品偏好、上次未解决的问题全部丢失。用户被迫重复描述体验直线下降。1.3 上下文窗口的物理限制即使你愿意把所有历史消息都传给模型大模型的 Context Window 也不是无限的。你可能会在线上看到这类报错api error: 400 this models maximum context length is 1048576 tokens context length exceeded (169,692 tokens). cannot compress further.当对话过长、文档过多、工具返回结果过大时记忆再多也塞不进一次请求。所以Agent 要“越用越聪明”必须建立一套分层记忆机制哪些信息只活在当前会话哪些信息要存到长期哪些信息需要压缩哪些信息要主动丢弃。这恰恰是 Context、Memory、Session、State 四者要解决的问题。2. 一次讲清四个核心概念Context、Memory、Session、State很多开发者把这四个词当成同义词实际它们描述的是不同层级的东西。可以先记住一句话Session 是一条时间线State 是这条时间线上的快照Context 是每次请求递给模型的“工作材料”Memory 是跨时间线存在的“长期档案”。2.1 Context一次请求里的全部输入Context 是模型在单次调用中看到的所有内容包括系统提示词、用户输入、历史对话、检索到的知识、工具返回结果。它本质上是“这一次请求的工作台”。Context 的特点是有长度上限即模型的 Context Window。每次请求都需要重新组装。组装成本直接影响响应延迟和费用。很多人误以为“只要把 Context 塞得越多越好”但实际是塞得越多模型越容易受到无关信息干扰响应也越慢。上下文越长成本往往也越高。2.2 Session一次连续交互的会话载体Session 是用户与 Agent 之间一段连续交互的载体。它解决的问题是如何把多轮消息归拢到同一个逻辑单元中。Session 与 Web 开发里的 Session 概念很接近但它更偏 Agent 业务语义。一个用户打开对话框创建 Session连续对话Session 延续用户离开一段时间Session 过期重新回来可以创建新 Session。Session 的生命周期一般由服务端控制。你可以在 Redis 中维护session:{id}的键值对并设置过期时间。Session 本身不负责“记忆”它只是一个容器。2.3 State某一时刻的数据快照State 是 Agent 在某一时刻的数据状态。它回答的不是“你们刚才聊了什么”而是“现在事情进行到哪一步了”。在电商客服场景里State 可以包含当前用户是否已登录。当前正在处理哪个订单。用户是否处于售后流程。对话是否已经完成。State 与 Session 的关系是Session 是会话线State 是会话线上的节点快照。Session 可以贯穿始终State 则随着业务流转不断更新。2.4 Memory跨请求、跨会话的长期信息Memory 是真正让 Agent “记得你”的部分。它不受单个会话限制可以跨用户、跨会话存在。Memory 可以包括用户档案。历史订单。偏好信息。历史问题解决记录。过去的对话摘要。Memory 的存储方式多种多样常见的是业务数据库加向量库的组合。重要信息落业务表模糊信息做向量化后做语义检索。四者的对比可以看这个表维度ContextSessionStateMemory生命周期单次请求一次会话随业务变化跨会话长期存储位置请求体Redis/内存/数据库Redis/数据库数据库/向量库是否持久化否短暂可过期短暂可恢复是核心作用提供决策材料归拢消息记录进度积累经验典型问题超长、超限会话丢失状态字段混乱召回不准确看到这里你应该能理解为什么不能把这四者混为一谈如果你把 Memory 当成 Context 无限塞会遇到超限如果你把 State 当成 Session用户状态会混乱如果你把 Context 当成 Memory每次请求都会重复浪费 token而且跨会话毫无积累。3. Agent 记忆分层短期状态与长期记忆的职责划分既然四个概念分工不同Agent 的记忆就应该分层设计。比较稳妥的分层思路是三层恰好对应三类存储介质3.1 工作记忆层Context Window承载单次请求中模型需要的全部信息。这一层的特点是“快、小、贵”所以要控制内容量级只放与当前问题最相关的材料。作者建议工作记忆层一定要做预算控制比如设定单次请求 token 上限。系统指令占多少、用户历史占多少、检索结果占多少都应该在组装层固定下来而不是自然增长。3.2 短期状态层Session 与 State承载一次会话内的上下文和业务进度。这一层的特点是“临时、可恢复、有生命周期”。典型实现是Session 存消息列表和元信息State 存业务状态。两者可以放在 Redis、内存或关系型数据库里过期时间根据业务调整。3.3 长期记忆层持久化存储承载跨会话的用户信息、历史摘要和业务数据。这一层的特点是“慢、大、持久”需要检索手段配合。典型实现是 MySQL/PostgreSQL 存业务数据向量库存对话摘要和知识片段。查询时先做关联检索再放入 Context。三层之间的关系可以理解为长期记忆是“图书馆”短期状态是“桌面便签”工作记忆是“当前正在批阅的文件”。每次请求Agent 先从图书馆里查资料放到桌面便签上再挑最相关的放上桌面参与批注。这个分层的价值在于每一层各司其职可以独立优化。比如长期记忆召回不准不会影响 Session 状态Context 超限不会破坏长期记忆的持久性。写代码前我们先把记忆的“写入”和“召回”两个方向想清楚写入对话过程中产生的用户关键信息要异步落库而不是每次请求都全量写。召回发起新请求时根据当前会话 state 和用户问题去长期记忆里检索再组装到 Context。很多 Agent 项目失败是因为只在“召回”上下了功夫却忽略了“写入”。模型每轮输出的信息如果不被结构化保存下一轮就无从召回。4. 短期状态落地Session 与 State 的工程实现下面进入代码部分。我们先解决短期状态的问题。4.1 Session 生命周期设计Session 的生命周期至少包含四个动作创建、读取、续期、过期清理。创建时分配唯一 session_id读取时从 Redis 取数据每次接收到新消息时续期当 Redis TTL 到期自动删除。用 Redis 存 Session 的典型流程如下import json import redis redis_client redis.Redis(hostlocalhost, port6379, db0) def create_session(session_id: str, ttl: int 1800): key fsession:{session_id} redis_client.set(key, json.dumps({history: [], updated_at: None}), exttl) def get_session(session_id: str): key fsession:{session_id} raw redis_client.get(key) if not raw: return None return json.loads(raw) def update_session(session_id: str, data: dict, ttl: int 1800): key fsession:{session_id} redis_client.set(key, json.dumps(data), exttl)这段代码的关键点是exttl。每次更新都重置过期时间用户持续对话时 Session 不会消失用户离开半小时后自动清理。4.2 State 的数据结构设计State 建议用独立的数据结构管理不要和 Session 的原始 message 混在一起。下面是一个电商客服场景的 State 定义from dataclasses import dataclass, field from typing import Optional dataclass class CustomerState: session_id: str user_id: Optional[str] None status: str initial # initial / consulting / after_sale / finished order_id: Optional[str] None pending_intent: Optional[str] None history: list field(default_factorylist) def intents(self): return self.pending_intent字段解释status业务状态决定 Agent 下一步走什么流程。order_id当前正在处理的订单避免用户反复报单号。pending_intent尚未完成的意图比如“申请退款”在等待用户确认。history当前会话的短消息列表用于 Context 组装。4.3 状态流转在电商客服中State 的流转大致如下initial - consulting - after_sale - finished用户第一次进入是initial发来咨询后变为consulting当识别到退换货意图时进入after_sale问题解决后进入finished。在代码里状态流转应该显式进行而不是在任意函数里随意改字段。推荐用一个状态机或至少一个集中的update_state函数class SessionManager: def __init__(self, redis_client): self.redis redis_client def get_state(self, session_id: str) - CustomerState: data get_session(session_id) if data and data.get(state): return CustomerState(**data[state]) return CustomerState(session_idsession_id) def save_state(self, state: CustomerState): session_data get_session(state.session_id) or {history: []} session_data[state] state.__dict__ update_session(state.session_id, session_data)注意这里有一个容易被忽略的细节业务状态和会话历史分开存储但放在同一个 Session key 下。这样读取时一次性拿到Redis 请求次数少。如果你在使用 LangGraph 之类的 Agent 编排框架需要特别注意State 的更新原则是“不可变更新”。不要在节点函数内部直接修改已有的 state dict 内容而是返回一个包含更新字段的新字典。比如return {status: after_sale}。如果你原地修改state[status]后不返回框架很可能不会感知到变化节点执行完后状态依旧没变。这是 LangGraph 中经常出现的问题。5. 长期记忆落地让 Agent 记住用户与业务上下文短期状态解决了“这一轮不忘”长期记忆解决的是“下一轮还记得”。常见的长期记忆有三种落地形态业务数据库、向量库、对话摘要。5.1 业务数据库结构化用户信息适合存储用户 ID、姓名、地址、历史订单、售后记录等结构化数据。这类数据适合用 MySQL 或 PostgreSQL 管理查询条件明确。示例表结构CREATE TABLE user_memory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, memory_key VARCHAR(64) NOT NULL, memory_value TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_key (user_id, memory_key) );写入时机当 Agent 从对话中识别到关键信息比如“我住在杭州”“我的订单号是 12345”就应该通过结构化抽取写入这张表。5.2 向量库非结构化对话记忆用户历史消息、历史对话摘要、商品知识文档这类内容不适合放在关系表里精确匹配更适合用向量检索。下面是一个最小长期记忆模块示例使用向量库保存用户对话摘要# memory_store.py class LongTermMemory: def __init__(self, collection_nameuser_memory): # 这里以任意支持持久化的向量库为例 self.collection_name collection_name def remember(self, user_id: str, memory_text: str, metadata: dict): # 将 memory_text 编码成向量并写入向量库 # 不同向量库 API 不同核心是 # 1. 对文本做 embedding # 2. 将向量与 metadata 一起写入 pass def recall(self, query: str, top_k: int 3): # 将 query 编码成向量 # 在向量库中检索最相似的 top_k 条记忆 # 返回记忆文本列表 return []这个示例故意没有绑定具体向量库因为不同项目的选型差异很大。比较常见的组合是 Embedding 模型加开源的向量数据库或者使用云厂商的向量检索服务。你需要根据项目实际环境把remember和recall内部实现替换成对应 SDK 的调用。召回时需要注意几点top_k不要设太大3 到 5 条通常足够。需要对召回结果做相关性阈值过滤避免引入无关内容。召回结果要拼接在 Context 的“记忆区域”而不是混在系统指令里。5.3 对话摘要压缩旧消息当单次会话消息过长可以把较早的消息用模型做一次摘要然后保留摘要丢弃原文。这样长期记忆里有摘要短期 Context 也放得下。摘要的典型实现def summarize_messages(messages: list) - str: # 实际项目里调用一次 LLM # prompt f请总结以下对话保留订单号、用户诉求、商品信息\n{messages} # return llm_response return 摘要不能只简单地截断前 N 个字符而是要有目的的提取。电商场景至少要保留订单号、用户诉求、售后进度、商品名称、价格信息。否则摘要不完整后面的业务流程就可能断掉。6. 上下文超限处理Context Window 不够用的四个策略模型接触到的外网报错里最经典的就是Context length exceeded (169,692 tokens). Cannot compress further.出现这种问题的根源是Context 里塞了太多东西或者消息长度增长速度超过了预期。解法不是只靠“压缩”而是要在请求组装阶段就做好预算。6.1 策略一长度截断最简单粗暴的办法。超过上限时优先丢弃最老的用户消息保留系统指令和最近消息。适合短期会话。需要明确的一点永远不要把系统指令和最关键的当前用户问题去掉否则模型会失去行为约束和回答目标。6.2 策略二关键消息摘要会话很长时把前面的消息做摘要替换成一句话。例如用户问过 20 个问题摘要成“用户咨询了物流延迟、退款流程、优惠券使用条件最终选择退款订单 12345”。模型仍能抓住重点token 占用大幅下降。6.3 策略三向量检索召回把历史对话全部向量化存起来每次请求只召回最相关的几个片段进入 Context。这样理论上无论历史多长Context 都不会无限膨胀。适合大量知识库场景。6.4 策略四分治与重开会话上下文已经无法压缩或者 Agent 进入另一个业务域时主动结束当前 Session开启新 Session把关键信息通过长期记忆传递下去。这听上去很“不优雅”却是很多生产系统的实际兜底方案。下面是一个组装 Context 的简化实现把“截断 摘要”结合在一起# context_builder.py def build_context(system_prompt, history, long_term_memories, max_tokens4096): memory_block \n.join(long_term_memories) history_block \n.join(history) # 预估 token 数量这里用简单字符数近似 def estimate_tokens(text): return len(text) // 3 while estimate_tokens(system_prompt memory_block history_block) max_tokens: if len(history) 2: # 丢弃最旧的一条用户消息 history.pop(0) history_block \n.join(history) else: # 历史已经很少说明是单条内容过长需要摘要 history_block summarize_messages(history) break return { system_prompt: system_prompt, memory_block: memory_block, history_block: history_block, }这段代码不是生产级实现但它演示了一个重要的原则先在组装层限制而不是等到请求发出去后让模型处理超限。如果你等到模型报context length exceeded再来想办法已经浪费了一次请求时间和费用。在实际项目中用 tiktoken 或其他分词器精确统计 token而不是用字符数估算会更稳妥。7. 完整实战电商客服助手从零搭建现在我们把所有概念串起来实现一个最小电商客服助手。这个例子不依赖某个特定大模型 SDK你可以根据自己的项目替换模型调用部分。7.1 环境准备建议环境Python 3.9 及以上版本。Redis 服务用于 Session 存储。一个可调用的大模型 API或本地部署的模型服务。一个嵌入模型 向量库用于长期记忆检索。如果项目还没有向量库可以先省略长期记忆部分用 Redis 存用户画像和对话摘要先跑通流程再逐步替换。依赖安装以实际项目为准至少需要 redis 客户端和 HTTP 客户端。7.2 项目结构ecommerce_agent/ ├── app.py # 主入口 ├── state.py # 状态定义 ├── session_manager.py # Session 与 State 管理 ├── memory_store.py # 长期记忆接口 ├── context_builder.py # Context 组装与超限处理 └── agent.py # Agent 主逻辑7.3 核心代码第一步定义状态模块。# state.py from dataclasses import dataclass, field dataclass class CustomerState: session_id: str user_id: str status: str initial order_id: str pending_intent: str history: list field(default_factorylist)第二步实现会话管理。# session_manager.py import json import redis redis_client redis.Redis(hostlocalhost, port6379, db0) SESSION_TTL 1800 # 30 分钟 def create_session(session_id): redis_client.set(fsession:{session_id}, {}, exSESSION_TTL) def get_session(session_id): raw redis_client.get(fsession:{session_id}) if raw: return json.loads(raw) return {} def update_session(session_id, data): redis_client.set(fsession:{session_id}, json.dumps(data), exSESSION_TTL) def save_state(session_id, state: CustomerState): data get_session(session_id) data[state] state.__dict__ update_session(session_id, data)第三步实现长期记忆接口。# memory_store.py class LongTermMemory: def __init__(self, collection_nameecommerce_memory): self.collection_name collection_name def remember(self, user_id, memory_text): # 写入向量库或数据库 pass def recall(self, query, top_k3): # 从长期记忆检索与 query 相关的片段 return []第四步实现 Context 组装。# context_builder.py def build_context(system_prompt, history, memory_snippets, max_tokens4096): memory_block \n.join(memory_snippets) history_block \n.join(history) def estimate_tokens(text): return len(text) // 3 while estimate_tokens(system_prompt memory_block history_block) max_tokens: if len(history) 2: history.pop(0) history_block \n.join(history) else: history_block summarize_messages(history) break return f{system_prompt}\n\n【长期记忆】\n{memory_block}\n\n【对话历史】\n{history_block}第五步实现 Agent 主逻辑。# agent.py from session_manager import get_session, save_state from context_builder import build_context from state import CustomerState from memory_store import LongTermMemory def call_llm(messages): # 这里替换成你自己的模型调用 # 例如 # response your_model_client.chat.completions.create( # modelyour-model, # messagesmessages, # ) # return response.choices[0].message.content return 已收到您的请求正在为您处理。 class EcommerceAgent: def __init__(self): self.memory LongTermMemory() def handle_message(self, session_id, user_message): session_data get_session(session_id) or {} state CustomerState(**session_data.get(state, {session_id: session_id})) state.history.append(f用户: {user_message}) # 1. 从长期记忆召回用户信息 memory_snippets self.memory.recall(user_message) # 2. 组装 Context system_prompt 你是一个电商客服助手。如果用户询问物流、退款、商品信息请根据记忆和上下文作答。 context build_context(system_prompt, state.history, memory_snippets) # 3. 调用模型 reply call_llm(context) state.history.append(f助手: {reply}) save_state(session_id, state) return reply这个示例代码中call_llm是一个待替换函数你可以根据自己的项目选择接入方式。只要把最终组装好的context传入模型接口并取回模型回复即可。7.4 运行与验证在app.py里写一个简单的命令行入口# app.py from agent import EcommerceAgent if __name__ __main__: agent EcommerceAgent() session_id test-session-001 while True: user_input input(用户: ) if user_input exit: break reply agent.handle_message(session_id, user_input) print(f助手: {reply})运行python app.py验证重点第一轮输入“订单 12345 还没发货”检查 state 中是否记录 order_id。第二轮输入“我要退款”检查 Agent 是否能知道用户说的是订单 12345。检查 Redis 中session:test-session-001是否存在State 是否更新。如果已经接入长期记忆重启进程后再输入相同内容观察 Agent 是否还能召回历史信息。如果第二轮 Agent 依然问“您的订单号是多少”说明 State 没有正确写入或 Context 没有把 order_id 带上优先检查save_state和build_context两个函数。8. 常见问题与排查思路问题现象可能原因排查方式解决方案同一 Session 内 Agent 忘记刚才的对话消息历史没有传给模型打印最终 context检查是否包含历史将历史消息拼入 Context不要只传最新一条提示 context length exceeded 或 cannot compress further历史消息或检索内容过长统计每次请求的 token 数量增加截断、摘要、检索召回逻辑提前控制长度LangGraph 节点内改了 state 却没生效原地修改 state 后未返回新值检查节点返回值与状态不可变性返回包含更新字段的新字典例如return {status: after_sale}Redis 中 Session 丢失TTL 设置过短或 Redis 重启检查 Redis 内存策略与 key 过期时间按业务调整 TTL必要时将 Session 持久化长期记忆检索不到有用信息写入时机不对或向量召回阈值过低检查 recall 返回结果确保关键信息在对话完成时异步写入增加阈值过滤Agent 被无关长期记忆干扰检索 top_k 过大查看召回片段是否与 query 相关调小 top_k增加相关性和时间过滤auto-compaction 后上下文仍难以恢复上下文已经接近不可用状态观察压缩前后的 token 和语义在压缩失败时主动提示用户开启新会话丢弃中间历史9. 最佳实践与工程建议9.1 保持三层记忆职责单一不要试图用一个 Redis key 解决所有记忆问题。Session 管消息State 管业务进度Memory 管跨会话积累。每一层独立存储独立优化出了问题也能快速定位。9.2 控制每次请求的 token 预算推荐给每次请求设一个 token 预算并在组装层强制约束。系统指令占固定比例历史消息和记忆片段按比例分配。宁可少带一些历史也不要让请求超限后反复重试。9.3 关键信息及时落库当 Agent 从对话中识别到订单号、用户 ID、地址等结构化信息时应立即写入业务数据库或长期记忆。不要等到会话结束才批量落库因为一旦进程崩溃这批关键信息就会丢失。9.4 敏感信息脱敏与最小权限电商场景涉及用户隐私长期记忆里不要保存明文敏感信息。用户手机号、身份证号、地址等字段要做脱敏处理访问时遵循最小权限原则。涉及用户数据的删除、导出操作需要先确认权限和合规要求。9.5 生产环境变更先在测试环境验证Agent 的提示词、记忆写入逻辑、上下文组装策略任何一项改动都可能影响线上回答质量。建议在测试环境用固定测试用例回归观察状态流转和记忆召回是否符合预期再发布到生产。9.6 为记忆设置上限和时间衰减长期记忆不是越多越好。超过一定条数的旧记忆可以按时间衰减或合并成摘要。这样可以避免记忆库无限膨胀也降低检索噪声。9.7 建立可观测性每一轮请求至少要记录三样东西使用了哪些记忆片段、组装后的 token 数、模型回复内容。有了这三样日志线上出了问题才能追溯是什么记忆影响了回答。10. 结语很多 Agent 项目最初都死在“模型能力不足”上但实际排查下来多半是记忆链路不完整。Context、Session、State、Memory 四者不是一个概念的不同说法而是四个层级、四种生命周期、四种存储策略。建议你先不要急着上向量库和复杂编排框架把手上的“会话上下文 业务状态 简单长期记忆”最小闭环跑通再逐步加入摘要压缩、向量召回、记忆编辑这些增强能力。这样既容易排查问题也能更快看到 Agent 从“记不住事”到“越用越聪明”的变化。如果你正在做电商、客服、个人助理类的 Agent 项目建议收藏这篇文章下一次遇到上下文超限或状态丢失时按这个思路排查大概率能少走很多弯路。
返回列表