
很多 Agent 项目的通病是模型越换越大能力却没见长进。原因往往不在模型而在记忆。用户第二次来咨询Agent 不记得他上次买过什么对话超过几十轮模型开始把早期指令忘掉两个用户同时提问会话串线。这些问题的根子都指向四个词Context、Memory、Session、State。这次我们就围绕这四个概念把 Agent 记忆拆开讲清楚然后手把手落地一个电商客服助手。这个助手的核心不是单纯调用大模型而是让大模型具备短期工作状态、会话隔离能力、长期用户记忆以及上下文超限时的自动处理。做到这四件事Agent 才谈得上“越用越聪明”。文章会覆盖架构设计、代码实现、功能测试、API封装和批量任务适合正在做 Agent 开发、任务编排、客服机器人或知识库问答的读者。1. Agent 记忆核心能力速览在动手之前先把四个概念放进一张表里。它们不是同义词而是四层不同生命周期的记忆结构。记忆层级对应概念存储位置生命周期典型用途Context上下文窗口模型请求体内部单次请求结束即消失系统提示词、最近对话、检索结果State工作状态编排框架的图状态或内存一次任务流从开始到结束任务进度、临时变量、当前意图Session会话服务端存储如 Redis、SQLite一次会话周期会话消息历史、会话隔离Memory长期记忆数据库、向量库跨会话长期保留用户偏好、历史订单、身份信息四个层级的关系可以用一句话概括Context 是模型“眼前能看到的东西”Session 决定“这次对话从哪里来到哪里去”State 是 Agent 执行任务时的临时工作台Memory 是沉淀下来跨会话复用的业务事实。在电商客服场景里四者的落点非常清晰。Context 放当前轮系统提示词和最近几条消息Session 标记是哪位用户在哪个会话中提问避免串线State 记录订单查询进度、是否已经索取订单号、是否转人工Memory 记住用户常买的品类、默认收货地址、历史售后记录。下一节先明确这个设计适合谁用。2. 适用场景与使用边界这套 Agent 记忆方案最适合多轮、个性化、需要跨会话复用信息的业务场景。典型例子就是电商客服用户不会每次都完整说出“我上周在你们店买了一个黑色手机壳现在裂了”他更可能只说“我上周买的手机壳坏了”。如果 Agent 有 Memory它能把“上周买的手机壳”自动解析成具体的订单记录如果只有 Context 没有 Memory这一句话就会让模型无从下手。它同时适合知识库问答、售后工单处理、用户运营助手、教育辅导机器人等场景。这些场景的共同点是用户之间存在明显差异且历史信息对当前回答有决定性影响。不适合的场景也很明确。单轮一次性任务比如“把这段英文翻译成中文”不需要 Session 和 Memory引入反而增加延迟和存储成本。对数据隐私极其敏感、不允许存储任何用户信息的场景也不能照搬这套设计。另外如果业务要求严格实时、必须把响应压到几百毫秒以内那么长期记忆检索和上下文压缩带来的额外耗时需要仔细评估。使用边界必须强调电商客服会接触到订单号、收货地址、手机号等个人信息。长期记忆存储不是无上限收集数据而是要有授权、有脱敏、有过期清理策略。记忆分层设计的意义之一就是可控该记的记不该记的一律不落库。3. 环境准备与前置条件这套方案的代码不绑定特定平台Windows、macOS、Linux 都可以跑。核心依赖包括Python 3.10 及以上具体版本以依赖包要求为准。一个可用的大模型服务可以是 OpenAI 兼容接口也可以是本地部署的模型。任务编排框架推荐使用 LangGraph 或自研状态机。本文示例用 LangGraph 风格编写但核心思想不依赖任何框架。存储组件SQLite 用于本地测试Redis 用于 Session 缓存向量数据库用于记忆检索。可选 Docker用于统一运行环境。先检查基础环境python --version pip --version如果使用 LangGraph 生态可以按需安装依赖。下面只是示例命令缺失哪个模块装哪个pip install langgraph langchain-openai fastapi uvicorn redis chromadb不需要严格照抄整条命令。更稳妥的做法是根据项目需要逐个安装并验证。安装完成后下一步是先把 Agent 记忆的四层架构理解透再进入代码实现。4. 四层记忆架构拆解4.1 Context模型唯一能看到的“眼前”Context 是大模型一次推理能接收的全部内容包括系统提示词、历史聊天记录、检索回来的用户信息、工具返回结果。模型本身没有持久记忆它能依赖的只有请求中的 Context。因此所有上层记忆最终都要在合适的时机被转换成 Context 的一部分模型才能看到。Context 管理直接决定 Agent 是“聪明”还是“糊涂”。Context 太短关键信息放不进去模型只能猜Context 太长超过模型的最大窗口就会报出类似maximum context length exceeded的错误。这是 Agent 开发群里最常见的报错之一问题根源通常不是模型不行而是没有做上下文裁剪和压缩。4.2 Session会话隔离的边界Session 表示一段连续对话。同一个用户连续咨询通常放在同一个 Session 中不同用户之间的 Session 必须隔离。电商客服助手最常见的串线事故就是因为只用了一个全局消息列表A 用户的订单被 B 用户看到了。实现 Session 的常见方式是用 Redis 或 SQLite 保存消息以session_id作为 key并设置过期时间。Session 存储的消息可以理解为“会话的原始素材”但真正进入模型 Context 的往往只是经过裁剪后的最新片段。4.3 StateAgent 任务流的工作台在 LangGraph 这类编排框架中State 是节点之间传递数据的核心载体。它既保存用户输入也保存任务过程中的临时结果比如“是否已经查过订单”“当前匹配到哪个订单”“是否需要转人工”。很多初学者问“LangGraph 如何在节点函数改变 state 状态值”答案很简单节点函数接收当前状态返回一个字典返回的字段会更新到状态中。没有返回的字段保持原样。State 的生命周期通常只在一次任务流内任务结束后要么归档要么丢弃。它不等同于长期记忆。4.4 Memory跨会话的长期沉淀Memory 是“用户过去是谁、买过什么、有什么偏好”。它不直接进 Context而是在需要时被检索出来拼接到当前上下文中。Memory 的存储通常分两类结构化数据存数据库表比如订单记录、用户地址非结构化偏好存向量库比如“用户喜欢深色系商品”“用户倾向于先问价格”。Memory 写入需要克制。不是每句话都值得进入长期记忆。一次闲聊中的“我今天心情不好”不应该被写进用户画像但“以后别给我发短信了”就是明确的偏好信号。记忆写入要设计判据不然后续每一次请求都会把无关信息拉进 Context白白浪费 Token还会干扰模型判断。5. 电商客服助手实战从短期状态到长期记忆下面进入代码实现。这里用 LangGraph 风格展示核心逻辑代码是演示性的具体 API 以你当前安装的版本为准。5.1 定义 State 结构先定义 Agent 的工作状态。状态里既要有会话基本信息也要有任务执行过程中的临时变量。from typing import TypedDict, List class AgentState(TypedDict): user_id: str # 用户标识 session_id: str # 会话标识 messages: List[dict] # 当前会话消息 memory: dict # 从长期记忆加载的用户信息 intent: str # 当前用户意图 matched_order: dict # 匹配到的订单 need_human: bool # 是否需要转人工这里有一个容易忽视的点messages是短期会话消息但它不能无限制增长。每次请求前都需要裁剪否则列表会越来越大最终触发模型上下文超限。5.2 Session 初始化与消息记录Session 需要持久化到存储层。用 SQLite 做本地测试最简单Redis 适合生产环境。import sqlite3 from datetime import datetime def save_message(session_id: str, role: str, content: str): conn sqlite3.connect(agent_memory.db) conn.execute( INSERT INTO messages(session_id, role, content, created_at) VALUES (?, ?, ?, ?), (session_id, role, content, datetime.utcnow().isoformat()), ) conn.commit() conn.close()Session ID 的设计直接影响隔离效果。推荐使用user_id session_id的组合作为查询条件而不是只依赖其中一个。否则可能出现同一个用户开多个会话时数据互相干扰或者不同用户因为 session 生成规则不良而读到对方消息。5.3 加载长期记忆每个用户进入会话时先加载他的长期记忆。这一步发生在调用大模型之前。def load_memory(state: AgentState) - AgentState: user_id state[user_id] profile memory_db.get_profile(user_id) recent_orders memory_db.get_recent_orders(user_id, top_k5) preferences vector_memory.search(fuser:{user_id}, top_k3) return { memory: { profile: profile, recent_orders: recent_orders, preferences: preferences, } }注意这里的返回字典里包含memory字段LangGraph 会把它更新到状态中。如果节点函数没有返回这个字段状态中的memory就会保持原样这也是很多“状态没更新”问题的根源返回了结果但字段名和状态定义不一致。5.4 组装 Context加载完记忆后把长期记忆、系统提示词、最近消息组装成完整的模型输入。def assemble_context(state: AgentState) - AgentState: system_prompt 你是电商客服助手只能基于用户订单和售后政策回答不要编造订单信息。 memory_text format_memory_text(state[memory]) recent_messages state[messages][-6:] # 默认只保留最近6条 messages [ {role: system, content: system_prompt memory_text}, ] recent_messages return {messages: messages}这里有一个关键经验系统提示词中必须把业务约束写死否则早期对话被压缩后模型可能忘记自己只能基于订单回答。压缩的是对话历史不是系统指令。5.5 记忆写入什么值得记长期记忆不能全量写入。设计一个简单的判据只有命中关键信号才写库。def store_memory(state: AgentState): user_id state[user_id] user_message state[messages][-1][content] if any(keyword in user_message for keyword in [地址, 喜欢, 不要, 习惯, 希望]): memory_db.upsert_profile(user_id, user_message)这个判据非常朴素实际生产中可以换成更细的意图识别规则也可以用一个小模型做信息抽取。核心原则是写入长期记忆的内容必须对后续对话有复用价值。5.6 上下文超限处理电商客服对话天然长用户反复追问物流、售后、价格消息很容易堆积。超限处理有四种常用手段截断只保留最近 N 条消息。实现简单但早期指令容易丢失。滑动窗口保留最近 K 轮消息加上关键业务字段。适合高频短对话。摘要压缩把早期对话交给模型生成摘要替代原始消息。适合长会话。检索增强把历史记忆向量化按需取回。适合跨会话长期记忆。下面是一个典型的压缩逻辑def compress_if_needed(messages, max_tokens8000): estimated_tokens sum(len(m[content]) for m in messages) if estimated_tokens max_tokens: return messages head messages[:1] # 系统提示词不变 tail messages[-6:] # 保留最近6条 middle messages[1:-6] # 中间部分需要压缩 summary llm.summarize(middle) return head [ {role: system, content: f早期对话摘要{summary}} ] tail这个方案解决的是“单次请求内 Context 超限”的问题。如果多次请求后早期摘要本身变得很长还需要对摘要做二次压缩或者把摘要转入长期记忆库按需检索。5.7 完整任务图把以上节点连接起来形成完整执行流程。from langgraph.graph import StateGraph, START, END graph StateGraph(AgentState) graph.add_node(load_memory, load_memory) graph.add_node(assemble_context, assemble_context) graph.add_node(call_llm, call_llm) graph.add_node(store_memory, store_memory) graph.add_edge(START, load_memory) graph.add_edge(load_memory, assemble_context) graph.add_edge(assemble_context, call_llm) graph.add_edge(call_llm, store_memory) graph.add_edge(store_memory, END) app graph.compile()call_llm节点可以自行实现它负责把组装好的 messages 发给模型并把回复写回状态。整个流程的路径很清晰先加载记忆再组装上下文再调用模型最后沉淀新记忆。6. 功能测试与效果验证完成代码实现后建议按下面的测试用例验证。这些用例覆盖了新老用户、会话隔离、超限恢复和记忆修正五个关键场景。编号测试场景输入示例预期结果通过标准T1新用户咨询“我要退货”Agent 询问订单号或手机号不抛出无根据的订单匹配T2老用户咨询“我上周买的手机壳坏了”Agent 自动匹配近期订单返回订单号并给出售后入口T3会话隔离两个用户同时提问各自看到自己的订单不串线、不串数据T4上下文超限恢复长对话后再问售后进度摘要保留关键售后信息仍能回答早期订单问题T5记忆修正“收货地址改成新地址”长期记忆更新下一轮使用新地址验证时建议写一个脚本用不同user_id和session_id连续跑多轮检查每轮的返回内容。重点关注三件事是否调用了订单查询、是否出现订单串线、上下文压缩后是否丢失了关键业务信息。测试中发现 T4 失败优先检查压缩逻辑。常见问题是早期对话中包含了用户明确授权信息比如“可以查看我的订单”压缩时这个授权被丢掉了后续模型就不敢继续查订单。解决方案是把授权状态提升到 State 的字段中不依赖聊天历史保存。7. 接口 API 与批量任务电商客服助手最终要接入业务系统建议封装成 HTTP 接口。使用 FastAPI 非常简单from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): user_id: str session_id: str message: str app.post(/chat) def chat(req: ChatRequest): result app_graph.invoke({ user_id: req.user_id, session_id: req.session_id, messages: [{role: user, content: req.message}], }) return {reply: result[reply], intent: result[intent]}启动服务uvicorn main:app --host 127.0.0.1 --port 8000调用示例curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {user_id: u10001, session_id: s20001, message: 我上周买的手机壳坏了}接口能跑通后面就可以接到自己的客服页面、企业微信机器人、小程序或者工单系统里。批量任务的典型场景是离线构建长期记忆。把历史客服工单按user_id分组重放一次消息让 Agent 提取每轮值得记忆的信息写入长期库。def rebuild_memory_from_logs(logs): for user_id, messages in group_by_user(logs).items(): for msg in messages: extract_and_store_memory(user_id, msg)批量重放要设计幂等性。同一个工单如果重复重放不能重复写入同一条记忆。实现上可以给记忆记录加source_id唯一索引存在则跳过。没有幂等保护批量任务跑第二遍时记忆库会膨胀检索质量也会下降。8. 资源占用与性能观察Agent 记忆方案比单次调用大模型多出来的开销主要在存储、Token 和检索延迟三个方向。Token 消耗是最大的隐性成本。每次请求进入模型前要统计输入 Token 数。可以在assemble_context节点中打印日志estimated_tokens sum(len(m[content]) for m in messages) print(finput_messages{len(messages)}, estimated_tokens{estimated_tokens})长期记忆检索会增加响应时间。向量检索通常需要几十到几百毫秒如果记忆库很大要设置检索超时不能因为检索失败拖垮整个对话。更稳妥的做法是检索失败时返回空记忆不阻断主流程。存储增长需要监控。SQLite 中的消息表会随会话数量持续增长要给 Session 设置清理策略。比如 Redis 中只保留最近 7 天的会话超过后自动删除SQLite 中定期清理三个月前的临时消息长期记忆保留更久。压缩逻辑触发时会额外调用一次模型做摘要这同样消耗延迟和 Token。性能观察时要分别记录正常请求耗时为多少、触发压缩后耗时为多少、记忆检索耗时为多少。如果压缩频繁触发优先优化滑动窗口的大小而不是每次都做摘要压缩。9. 常见问题与排查方法Agent 记忆相关的问题集中在上下文超限、状态更新失败、会话串线和存储泄露这几个方向。下面列出一线开发中最常见的表现和排查思路。问题现象可能原因排查方式解决方案模型返回 maximum context length exceeded输入消息超过模型上下文窗口打印每次请求的 token 估算值触发截断或摘要压缩报错 context length exceeded无法继续压缩摘要本身过长且无法再次压缩检查摘要长度和保留消息数量把摘要转存到长期记忆按需检索多轮对话后模型丢失早期指令早期消息被裁剪或压缩查看压缩日志确认指令是否被移除把关键指令放入系统提示词LangGraph 节点 state 没有更新节点返回字段名与状态定义不一致检查节点返回值返回与状态字段同名的字典多个用户订单互相串线Session key 使用不当查看消息查询 SQL 的条件用 user_id session_id 组合隔离Session 数据反序列化失败存储格式与读取逻辑不匹配查看存储序列化方式统一序列化工具或重置 Session进程内存持续增长消息列表无限累积监控进程内存占用限制消息列表长度并定期清理批量记忆重放后数据重复缺少幂等控制检查记忆表是否有重复记录加 source_id 唯一索引其中“无法继续压缩”是比较容易踩的坑。它的本质是早期摘要加最近消息总长仍然超过模型窗口。此时不能再靠压缩解决必须把早期摘要的内容搜索化转成向量库中的记忆条目每次只检索与当前用户问题相关的片段。排查时建议先打日志再改代码。日志至少包含每次请求的总 Token 估算、压缩触发次数、记忆检索耗时、Session 查询的 user_id 和 session_id。没有日志直接猜问题通常会把时间浪费在错误的方向上。10. 最佳实践与使用建议从工程落地角度这里给出几条可复用的建议。第一次上线建议先关闭长期记忆只保留 Session 和 State跑通完整对话流程后再逐步打开 Memory。这样做的好处是出了问题时可以先排除长期记忆的干扰确认基础链路没问题。记忆写入要设计白名单字段。与其把用户整段输入存入长期库不如只提取结构化字段订单号、地址、品类偏好、售后诉求。整段入库虽然实现简单但会让记忆库很快充满噪声检索质量也会下降。上下文压缩必须保留三类内容系统指令、用户身份信息、业务授权信息。这三类信息一旦丢失即使对话历史都在模型也可能不敢继续操作。涉及订单、地址、手机号等个人信息时必须做脱敏、授权和过期策略。脱敏指存储时隐藏关键中间位授权指用户明确同意后才能读取历史订单过期指确定不再需要的记忆要及时清理。合规不只是避免风险过度的信息收集本身也会降低记忆检索的准确率。批量任务压测时先小批量重放比如先跑 100 条工单检查记忆库数据是否准确再全量执行。全量任务要有日志和失败重试不能跑一半报错后直接重来那样会造成大量重复记忆。11. 总结与下一步这个项目最值得尝试的点是用很轻的工程手段把“模型无状态”变成“业务有记忆”。不需要换更强的模型电商客服的体验就能明显提升。最先应该验证的功能是老用户二次咨询用户说“我上周买的手机壳坏了”Agent 能否从长期记忆里找到历史订单并给出正确售后路径。这个功能跑通说明 Context、Memory、Session、State 四层设计已经形成了闭环。最容易踩的坑是不加区分地存储记忆。什么内容都进长期库会让记忆库迅速被噪声淹没后续检索出来的内容既浪费 Token又干扰模型判断。后续可以继续扩展的方向有接入向量知识库管理商品信息和售后政策接入订单履约状态实时查询实现多客服协同共享用户记忆以及基于记忆做用户生命周期运营。建议先把文中第 6 节的五个测试用例跑完再考虑扩展功能。这套记忆架构一旦跑通后面很多业务场景都能复用。