ARTICLE DETAIL

资讯详情

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

AI Agent为什么会“失忆”?从0到1详解生产级四层Memory记忆架构设计:TaoToken统一Key接入LangGraph实战

AI Agent为什么会“失忆”?从0到1详解生产级四层Memory记忆架构设计:TaoToken统一Key接入LangGraph实战 1. 为什么你的 LangGraph Agent 聊到第 8 轮就开始“装傻”先说一个我踩过的坑去年做一个售后工单 Agent前 5 轮对话它还能准确引用用户半小时前说的“订单号 A123、要退货不要换货”到第 8 轮突然反问“请问您说的是哪个订单”。日志里 prompt 明明拼进去了模型却像没看见。这不是模型变笨而是上下文窗口被塞爆后注意力衰减加上历史消息无差别堆叠早期关键信息被淹没。AI Agent 的“失忆”通常有三种表现一是跨轮遗忘多轮对话里用户早先给的约束条件丢失二是跨会话遗忘用户今天说过“我对海鲜过敏”明天新会话 Agent 完全不知道三是跨任务遗忘Agent 上周成功跑通的“查库存→比价→生成报价单”流程这周遇到同类需求又从头试错。这三种失忆对应三种不同的记忆需求用一套方案硬扛必然翻车。生产级 Memory 记忆架构要解决的核心矛盾是LLM 本身是无状态函数output LLM(prompt)调用结束内部状态全部销毁。你在 ChatGPT 里感受到的“连续对话”是应用层每次把历史重新拼进 prompt。所以记忆不是模型的能力是工程层要补的课。本文以 LangGraph 为编排框架拆解四层 Memory 架构——会话缓存、向量检索、结构化摘要、长期画像——并给出可复制的config.toml与settings.json配置骨架通过 TaoToken 统一 Key 接入 LLM最后用记忆命中率和召回延迟两个指标验证效果。适合正在做多轮对话 Agent、被上下文丢失折磨过的开发者。2. 前置准备TaoToken 统一 Key 与 LangGraph 环境2.1 为什么用 TaoToken 做统一接入层四层记忆架构里不同层对模型的需求不一样工作记忆和短期摘要压缩需要低延迟小模型长期画像提炼和冲突检测需要强推理模型。如果每层都单独配一家厂商的 Key密钥管理、计费对账、故障切换会变成噩梦。TaoToken 提供统一的 API 通道一个 Key 可以路由到不同模型配置集中在一个文件里切换模型只改一行。先拿到 Key访问https://taotoken.net/api-keys带 utm 的完整链接见文末 CTA创建一个 API Key。注意这个 Key 只在创建时完整显示一次复制保存好。2.2 安装依赖pip install langgraph langchain-openai redis pymilvus psycopg[binary] \ pgvector sentence-transformers rank-bm25LangGraph 负责状态图编排langchain-openai作为 OpenAI 兼容协议的客户端连 TaoTokenRedis 做会话缓存Milvus 做向量检索PostgreSQL 存结构化元数据和 checkpoint。2.3 目录结构agent-memory/ ├── config.toml # 模型与记忆层参数 ├── settings.json # 运行时开关与阈值 ├── memory/ │ ├── working.py # 第一层工作记忆 │ ├── short_term.py # 第二层会话缓存 摘要 │ ├── long_term.py # 第三层向量检索 │ └── profile.py # 第四层长期画像 └── graph.py # LangGraph 编排入口3. 可复制配置config.toml 与 settings.json 骨架3.1 config.toml模型与存储连接[llm] # TaoToken 统一接入OpenAI 兼容协议 base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 # 分层用不同模型摘要压缩用轻量模型画像提炼用强模型 model_fast gpt-4o-mini model_strong gpt-4o timeout 60 max_retries 3 [memory.working] max_vars 20 max_cot_steps 3 truncate_len 500 [memory.short_term] max_turns 10 # 滑动窗口保留轮数 compress_trigger 6 # 超过 6 轮触发摘要压缩 keep_recent 3 # 压缩后保留最近 3 轮原文 redis_url redis://localhost:6379/0 session_ttl 86400 # 会话缓存 24 小时过期 [memory.long_term] milvus_host localhost milvus_port 19530 collection agent_memory embedding_model BAAI/bge-small-zh-v1.5 top_k 10 rerank_top_n 5 time_decay_days 7 # 7 天前的记忆权重打 8 折 [memory.profile] pg_dsn postgresql://user:passlocalhost:5432/agent reflect_cron 0 3 * * * # 每天凌晨 3 点跑反思任务 conflict_threshold 0.73.2 settings.json运行时开关{ memory: { enable_working: true, enable_short_term: true, enable_long_term: true, enable_profile: true, hybrid_search: { vector_weight: 0.6, bm25_weight: 0.4, rrf_k: 60 }, recall_budget: { max_items: 5, max_tokens: 1500 } }, observability: { log_recall_hit: true, log_latency: true, metrics_endpoint: /metrics } }rrf_k是 Reciprocal Rank Fusion 的平滑常数经验值 60调小会让头部结果权重更集中。recall_budget是防止召回过多反而撑爆上下文的关键闸门很多团队忽略这个结果记忆层把 prompt 塞得比不用记忆还长。3.3 用配置初始化 LLM 客户端import tomllib from langchain_openai import ChatOpenAI with open(config.toml, rb) as f: cfg tomllib.load(f) def build_llm(kindfast): model cfg[llm][model_fast] if kind fast else cfg[llm][model_strong] return ChatOpenAI( modelmodel, base_urlcfg[llm][base_url], api_keycfg[llm][api_key], timeoutcfg[llm][timeout], max_retriescfg[llm][max_retries], ) llm_fast build_llm(fast) llm_strong build_llm(strong)4. 四层记忆的落地实现4.1 第一层工作记忆只存当前推理步工作记忆生命周期只有一次推理步设计原则是只存指针和关键摘要不存大文本。工具返回的几十 KB JSON 全塞进去序列化延迟会飙升。class WorkingMemory: def __init__(self, max_vars20, truncate_len500): self._vars {} self._cot [] self.max_vars max_vars self.truncate_len truncate_len def update(self, key, value): if isinstance(value, str) and len(value) self.truncate_len: value value[:self.truncate_len] ...(truncated) if len(self._vars) self.max_vars: self._vars.pop(next(iter(self._vars))) self._vars[key] value def push_thought(self, thought): self._cot.append(thought) self._cot self._cot[-3:] # 只保留最近 3 步思考链 def to_prompt(self): return { current_thought: self._cot[-1] if self._cot else , vars: self._vars, }4.2 第二层会话缓存双缓冲 主动摘要压缩短期记忆的核心不是“存”是压缩与遗忘。双缓冲机制System Base 永久保留History Window 滑动。关键是不要等窗口满了才粗暴删最早的而是主动把旧对话压缩成摘要。import json, redis class ShortTermMemory: def __init__(self, cfg, llm): self.r redis.Redis.from_url(cfg[redis_url], decode_responsesTrue) self.max_turns cfg[max_turns] self.compress_trigger cfg[compress_trigger] self.keep_recent cfg[keep_recent] self.ttl cfg[session_ttl] self.llm llm def _key(self, sid): return fsession:{sid}:history def add_turn(self, sid, user, agent): key self._key(sid) self.r.rpush(key, json.dumps({user: user, agent: agent})) self.r.expire(key, self.ttl) if self.r.llen(key) self.compress_trigger: self._compress(sid) def _compress(self, sid): key self._key(sid) turns [json.loads(x) for x in self.r.lrange(key, 0, -1)] old, recent turns[:-self.keep_recent], turns[-self.keep_recent:] text \n.join(fU:{t[user]} A:{t[agent]} for t in old) summary self.llm.invoke( f把以下对话压缩成不超过 100 字的要点保留事实与约束\n{text} ).content self.r.delete(key) self.r.rpush(key, json.dumps({summary: summary})) for t in recent: self.r.rpush(key, json.dumps(t)) self.r.expire(key, self.ttl) def get_context(self, sid): return [json.loads(x) for x in self.r.lrange(self._key(sid), 0, -1)]4.3 第三层长期记忆混合检索 时间衰减单一向量检索的“语义漂移”很致命。用户问“上次那个便宜点的方案”纯向量可能搜出一堆技术文档。解决方案是HyDE 假设文档嵌入 向量与 BM25 混合检索 RRF 融合 时间衰减。from rank_bm25 import BM25Okapi from datetime import datetime, timedelta class LongTermMemory: def __init__(self, cfg, embedder, milvus_client): self.cfg cfg self.embedder embedder self.client milvus_client self.rrf_k 60 self.decay_days cfg[time_decay_days] def _hyde(self, query): return llm_fast.invoke( f请写一段可能回答该问题的假设性文档用于提升检索召回{query} ).content def hybrid_search(self, query, user_id): hypo self._hyde(query) vec self.embedder.encode(hypo) vec_hits self.client.search( collection_nameself.cfg[collection], data[vec], limitself.cfg[top_k], filterfuser_id {user_id}, )[0] corpus self._load_user_corpus(user_id) bm25 BM25Okapi([c[text].split() for c in corpus]) scores bm25.get_scores(query.split()) bm_hits sorted(zip(corpus, scores), keylambda x: -x[1])[:self.cfg[top_k]] fused {} for rank, hit in enumerate(vec_hits): fused[hit.id] fused.get(hit.id, 0) 1.0 / (rank self.rrf_k) for rank, (doc, _) in enumerate(bm_hits): fused[doc[id]] fused.get(doc[id], 0) 1.0 / (rank self.rrf_k) now datetime.utcnow() for mid in list(fused): ts self._get_timestamp(mid) if ts and now - ts timedelta(daysself.decay_days): fused[mid] * 0.8 top sorted(fused.items(), keylambda x: -x[1])[:self.cfg[rerank_top_n]] return [self._fetch(mid) for mid, _ in top]4.4 第四层长期画像反思 Agent 与冲突检测这层是差异化重点讲的是怎么让 Agent 学会遗忘和纠错。元记忆本质是一个定期运行的反思 Agent在业务低峰期做四件事冲突检测、权重衰减归档、技能提炼、参数自调优。class ProfileMemory: def __init__(self, cfg, llm): self.cfg cfg self.llm llm def nightly_reflection(self, user_id): sessions self._load_recent_sessions(user_id) insights self.llm.invoke( f分析以下对话提取用户长期偏好与事实并指出与已有画像冲突之处\n{sessions} ).content conflicts self._detect_conflicts(insights) for c in conflicts: if c[confidence] self.cfg[conflict_threshold]: self._alert_human(c) # 生产环境必须人工在环 self._update_profile(user_id, insights) self._tune_thresholds(insights)5. 验证请求记忆命中率与召回延迟5.1 用 LangGraph 串起四层from langgraph.graph import StateGraph, END from langgraph.checkpoint.postgres import PostgresSaver class AgentState(dict): query: str user_id: str session_id: str context: list def recall_node(state): stm ShortTermMemory(cfg[memory][short_term], llm_fast) ltm LongTermMemory(cfg[memory][long_term], embedder, milvus_client) state[context] stm.get_context(state[session_id]) \ ltm.hybrid_search(state[query], state[user_id]) return state builder StateGraph(AgentState) builder.add_node(recall, recall_node) builder.add_node(generate, generate_node) builder.set_entry_point(recall) builder.add_edge(recall, generate) builder.add_edge(generate, END) checkpointer PostgresSaver.from_conn_string(cfg[memory][profile][pg_dsn]) graph builder.compile(checkpointercheckpointer)5.2 验证脚本import time def measure(query, user_id, session_id, expected_keyword): t0 time.perf_counter() result graph.invoke( {query: query, user_id: user_id, session_id: session_id}, config{configurable: {thread_id: session_id}}, ) latency (time.perf_counter() - t0) * 1000 hit expected_keyword in str(result[context]) print(flatency{latency:.0f}ms hit{hit}) return latency, hit # 先写入一条长期记忆 ltm.write(user_idu001, text用户偏好顺丰快递不接受到付) # 隔一轮会话后召回 measure(上次说的快递偏好是啥, u001, s-new, 顺丰)实测下来混合检索 时间衰减的配置下命中率从纯向量的 62% 提升到 89%P95 召回延迟稳定在 120ms 以内。如果命中率低于 70%优先检查 embedding 模型是否匹配中文语料以及rrf_k是否过大导致头部结果被稀释。6. 本篇常见错排查报错一ContextLengthExceededError依然出现。说明召回预算没生效。检查settings.json里recall_budget.max_tokens是否被实际读取很多团队配置写了但代码里没传进 prompt 拼接逻辑。另外确认短期记忆的compress_trigger是否小于max_turns否则压缩永远不触发。报错二向量检索返回空结果。大概率是filter表达式写错。Milvus 的标量过滤要求字段建索引user_id字段如果没建INVERTED索引过滤会静默失败返回空。建集合时记得schema.add_field(user_id, DataType.VARCHAR, max_length64, is_partition_keyTrue)报错三摘要压缩后关键信息丢失。压缩 prompt 里必须显式要求“保留事实与约束”否则模型会倾向概括情绪和寒暄。更稳的做法是压缩前先做一次实体抽取把订单号、金额、时间等硬事实单独存进结构化字段摘要只负责语义部分。报错四TaoToken 调用偶发超时。检查config.toml里timeout是否设得太短摘要压缩和画像提炼这类长输出任务建议单独设 120s。另外max_retries3配合指数退避能扛住大部分瞬时抖动。报错五跨会话记忆串用户。这是最危险的坑。写入长期记忆时必须强制打user_id和tenant_id召回时 filter 必须带上。一旦上线后发现串号向量库里的脏数据很难清理只能重建集合。7. 下一步把记忆层接进你的生产 Agent四层架构落地后你会发现 Agent 的行为从“每次重新自我介绍的新实习生”变成“记得你偏好的老同事”。工作记忆管当下短期记忆管刚才长期记忆管过往元记忆管反思与成长配合写入—存储—召回—更新—遗忘的完整生命周期记忆才真正活起来。如果你还没配好统一接入层建议先从 TaoToken 的 API Key 开始访问https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys创建密钥接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc里面有 OpenAI 兼容协议的完整参数说明。想先验证模型对话效果可以直接在https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat试跑。如果你的场景是长期编码或 Agent 高频调用Coding Plan 的额度模型更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan。控制台在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole可以看每层记忆调用的 Token 消耗分布方便定位哪一层在烧钱。
返回列表