1. 项目概述:为什么“记忆”是Agent从工具走向伙伴的关键分水岭
“前端转型 Agent 开发 06 之 Agent Memory 记忆系统(让 Agent 更智能,更懂你)”——这个标题里藏着一个被很多初学者忽略的真相:Agent 不是写完 prompt 就能跑起来的玩具,而是一个需要持续积累、理解上下文、建立长期关系的数字实体。我带过不少从 React/Vue 转过来的前端同学,他们第一反应往往是:“我用 LangChain 写个 Chain,调个 LLM API,不就完成一个 Agent 了吗?” 然后很快在真实场景里卡住:用户问“刚才我说的那个方案,第二步怎么操作?”,Agent 回答“抱歉,我不记得您之前提过什么”;或者用户连续三次调整参数,Agent 每次都当成全新对话重头解释,毫无连贯性。这就是没有 Memory 的典型症状——它像一个健忘的实习生,技术再好,也难担重任。
核心关键词“Agent Memory”不是指服务器内存(RAM),而是指一套有结构、可检索、可更新、有时效管理的上下文持久化机制。它解决的是三个根本问题:第一,跨轮次对话中“我是谁、你在和谁说话”的身份锚定;第二,多步骤任务中“我们进行到哪一步了”的状态追踪;第三,长期交互中“你偏好简洁还是详细?讨厌缩写?上次说要查的资料找到了吗?”这类个性化认知沉淀。这恰恰是前端开发者最熟悉又最容易低估的领域——我们天天处理 localStorage、IndexedDB、Redux store,但把这套思维迁移到 Agent 架构时,很多人直接套用“把整个聊天记录塞进 system prompt”,结果 token 爆炸、成本飙升、逻辑混乱。真正的 Memory 系统,必须像设计一个高性能前端状态管理库那样,考虑缓存策略、序列化格式、冲突合并、冷热分离。它不是附加功能,而是 Agent 架构的呼吸系统。适合正在用 Next.js + LangChain 做智能客服、个人知识助手、低代码流程编排等项目的开发者,尤其适合那些已经能跑通单轮问答、正卡在“如何让 Agent 记住用户习惯”这一关的前端转 AI 工程师。
2. 整体设计思路:从“前端状态管理”视角重构 Agent Memory 架构
2.1 为什么不能简单复用前端经验?三大认知陷阱必须先破除
很多前端同学一上来就想用 localStorage 存 chat history,或者用 Zustand 管理 conversation state,这看似顺理成章,实则埋下三颗雷:
第一颗雷:Token 成本黑洞。前端存数据讲究“存得下”,Agent Memory 讲究“用得省”。LLM 的上下文窗口是硬成本,每多传 1KB 文本,API 费用就涨一分。我见过一个项目把 50 轮完整对话(含 system prompt、tool call logs、思考链)全塞进 memory,单次请求 token 超过 12K,响应延迟从 800ms 拉到 4.2s,用户流失率翻倍。这不是性能问题,是架构误判——Memory 的核心价值不是“存全量”,而是“存关键”。
第二颗雷:状态语义错位。Redux store 里user: {name: '张三', role: 'admin'}是明确结构化数据,但 Agent 的记忆是混合体:有用户显式声明(“我叫李四”),有隐式推断(“你刚拒绝了会议邀约,可能近期忙”),有工具执行结果(“查到北京天气:23℃,多云”),还有模型自我反思(“上一轮回答太简略,这次补充原理”)。强行用统一 schema 描述,要么丢失语义,要么 schema 膨胀到无法维护。这就像给 React 组件硬套一个万能 props 接口,表面灵活,实际脆弱。
第三颗雷:生命周期管理缺失。前端组件卸载时useEffect cleanup清掉定时器,但 Agent 的 memory 生命周期更复杂:一次会话结束,该清空所有记忆吗?用户说“忘掉刚才聊的”,是删 last N 条,还是按主题过滤?系统自动归档旧记忆时,如何保证不误删正在引用的上下文?这些在前端开发中靠人工判断的逻辑,在 Agent 场景里必须变成可配置、可审计、可回滚的规则引擎。
所以我们的设计起点很明确:Memory 不是数据库,而是上下文编排器。它的核心职责是——在每次 LLM 调用前,从海量历史数据中,精准提取出与当前 query 最相关的 3~5 个记忆片段,并以 LLM 友好的格式注入 prompt。这就决定了整体架构必须分层:底层存储(Storage)只管可靠存取,中间编排层(Orchestrator)负责策略决策,上层接口(Interface)对齐前端开发习惯。
2.2 四层架构设计:用前端熟悉的“组件化”思维拆解 Memory 系统
我们最终落地的架构是清晰的四层模型,每一层都对应前端开发中的成熟范式:
第一层:Storage 层(类比 IndexedDB)
不追求通用,只做一件事:提供原子化的 key-value 存取,value 是标准化的 MemoryItem 对象。我们放弃 MongoDB/PostgreSQL 这类重型方案,选用 LiteDB(.NET)或 DuckDB(JS/WASM),原因很实在:Agent Memory 的读写模式高度特定——95% 是追加写入(新消息),80% 是按时间范围+标签查询(“过去 24 小时关于‘报销’的记忆”),且数据量级通常在 GB 以内。LiteDB 的嵌入式特性、ACID 保障、零配置启动,完美匹配这个场景。它就像前端项目里的 localForage,轻量、可靠、不抢主线程。
第二层:Encoder 层(类比 JSON.stringify + 自定义序列化)
这是最容易被忽视的“翻译官”。原始对话数据(message array)、工具调用日志(tool call result)、用户行为事件(click, scroll)都是异构数据,直接存会丢失语义。我们设计了一套轻量 Encoder 协议:每个 MemoryItem 必须包含id(UUID)、type('message' | 'tool_result' | 'user_preference')、timestamp、content(纯文本摘要)、metadata(JSON 对象,存原始数据引用、来源模块、置信度)。重点来了:content字段不是 raw text,而是经过摘要压缩的。比如一条 200 字的用户提问,Encoder 会用 LLM 提取核心意图:“用户想对比 Vue3 和 React18 的响应式实现差异”,并丢弃客套话。这个过程在客户端完成,不走服务端 API,毫秒级。
第三层:Orchestrator 层(类比 Redux middleware + selector)
这才是 Memory 的大脑。它接收当前 query(如“帮我生成上周会议纪要”),执行三步决策:
- 过滤(Filter):基于规则快速筛出候选集。例如:
type === 'message' && timestamp > now - 7d && tags.includes('meeting'); - 排序(Rank):对候选集做相关性打分。我们不用复杂向量检索(初期没必要),而是用 TF-IDF + 时间衰减因子(越近的权重越高)+ 用户显式标记(用户点过“重要”按钮的记忆权重×3);
- 裁剪(Trim):确保最终注入 prompt 的总 token < 1500。按得分降序取 top-K,若超限,则对低分项做二次摘要(比如把一段 300 字的会议记录压缩成 50 字要点)。
这个层完全可插拔——你可以把 TF-IDF 换成 sentence-transformers 向量检索,只要输入输出协议一致,上层无感。
第四层:Interface 层(类比 React Hook)
为前端开发者提供直觉化 API。我们封装了useAgentMemory()Hook,用法和useState几乎一样:
const { memories, addMemory, clear } = useAgentMemory({ scope: 'conversation', // 'user' | 'session' | 'global' strategy: 'recent-5' // 'all' | 'by-tag' | 'custom' }); // 在组件内直接调用 addMemory({ type: 'user_preference', content: '用户偏好表格形式输出', metadata: { preferenceKey: 'outputFormat', value: 'table' } });Hook 内部自动处理 Storage 读写、Encoder 编解码、Orchestrator 调度,开发者只关心“我要存什么”和“我要取什么”。
提示:不要在
useEffect里直接调addMemory存整个 message 数组!正确做法是监听messages变化,对每条新消息单独调用addMemory,并设置type和metadata。这样 Orchestrator 才能精准识别不同记忆类型。
3. 核心细节解析:从“存一句话”到“构建认知图谱”的实操要点
3.1 MemoryItem 设计:用最小字段承载最大语义
一个看似简单的MemoryItem接口,背后是我们踩了 7 个版本才定型的设计。早期我们照搬 LangChain 的ChatMessageHistory,结果发现它无法表达“用户刚点了‘不喜欢这个答案’按钮”这类隐式反馈。最终确定的字段只有 5 个,但每个都经过深思:
interface MemoryItem { id: string; // UUID v4,全局唯一,用于去重和引用 type: MemoryType; // 枚举值,强制分类:'message' | 'tool_result' | 'user_feedback' | 'system_event' timestamp: number; // Unix timestamp (ms),精确到毫秒,支持毫秒级排序 content: string; // 纯文本摘要,长度 ≤ 200 字符,必须 human-readable metadata: Record<string, any>; // 非结构化元数据,但约定 3 个必填键: // - source: string ('chat-input' | 'api-call' | 'user-click') // - confidence: number (0.0~1.0,模型对内容准确性的自评) // - references?: string[] (关联的其他 memory id,构建图谱) }关键细节在于content和metadata的协同。比如用户说:“把刚才那个 Python 脚本改成支持 CSV 导出”,对应的 MemoryItem 不是存原话,而是:
content: “用户要求为 Python 脚本添加 CSV 导出功能”metadata.references: ["mem_abc123"] // 指向上一条存脚本代码的 memory
这样当后续 query 是“生成 CSV 导出的完整代码”,Orchestrator 就能通过references关联出上下文,而不是靠模糊的关键词匹配。这本质上是在构建轻量级认知图谱——节点是 MemoryItem,边是references。
注意:
content字段严禁存原始长文本!我们设了硬性校验:超过 200 字符自动触发摘要 pipeline。实测下来,200 字足够表达一个独立语义单元(如一个需求、一个反馈、一个结果),再多就是信息冗余。前端同学可以类比 React.memo 的 shouldComponentUpdate——只存“值得记住”的部分。
3.2 Encoder 的实战技巧:如何让 LLM 帮你做高质量摘要而不烧钱
Encoder 层的摘要能力,直接决定 Memory 的质量上限。我们测试过三种方案:
| 方案 | 实现方式 | 单次成本 | 延迟 | 质量 | 适用场景 |
|---|---|---|---|---|---|
| 规则模板 | 正则提取“我想...”“请...”“对比...”等句式 | $0 | <10ms | ★★☆ | 简单指令类 |
| LLM 摘要(GPT-3.5) | summarize this in 1 sentence: ${text} | $0.0002 | ~800ms | ★★★★ | 中等复杂度 |
| 本地小模型(Phi-3) | 量化版 Phi-3-mini 在 Web Worker 运行 | $0 | ~300ms | ★★★☆ | 推荐:平衡成本与质量 |
最终选择 Phi-3-mini(3.8B 参数,4-bit 量化后仅 2.1GB),原因很务实:
- 它在 200 字摘要任务上,BLEU 分数比 GPT-3.5 低 8%,但远超规则模板;
- 全部在浏览器端运行,无网络请求,隐私零泄露;
- 加载耗时 1.2s(首次),后续摘要平均 280ms,用户无感知;
- 我们用 ONNX Runtime Web 加载,兼容 Chrome/Firefox/Safari。
Encoder 的核心技巧是两阶段摘要:
- 粗筛:用正则快速定位关键句(如含“应该”“必须”“对比”“步骤”等动词的句子);
- 精炼:将筛选出的 1~3 句喂给 Phi-3,指令固定为:“请用 15~25 字概括以下内容的核心意图,不要添加任何解释,输出纯中文。”
这个指令经过 37 次 A/B 测试优化,确保输出稳定。比如输入:“我觉得这个方案有点复杂,能不能简化一下?另外,我需要它能在 Windows 上运行。”,输出一定是:“用户要求简化方案并支持 Windows 系统”,而非“用户提出了两个需求”。
3.3 Orchestrator 的策略配置:用前端思维管理“记忆的保质期”
Orchestrator 的策略不是写死的,而是像 CSS-in-JS 一样可组合。我们提供了 4 类基础策略,全部用函数式 API 定义:
// 1. 时间窗口策略:只取最近 N 小时的记忆 timeWindow({ hours: 24 }); // 2. 标签过滤策略:只取带指定 tag 的记忆 byTag(['meeting', 'urgent']); // 3. 用户反馈加权策略:给 'liked' 记忆 ×5 权重 feedbackWeight({ liked: 5, disliked: 0.1 }); // 4. 混合策略:组合使用(顺序即优先级) compose([ timeWindow({ hours: 72 }), byTag(['project_x']), feedbackWeight({ liked: 3 }) ]);最关键的实战经验是:永远不要依赖单一策略。我们在线上环境发现,纯时间窗口策略在用户长时间未交互后(如隔天回来),会漏掉关键上下文;纯标签策略则因标签打标不准导致召回率低。最终采用的默认策略是compose([timeWindow({hours: 168}), feedbackWeight({liked: 5})]),即“一周内+用户点赞过的记忆优先”。
另一个血泪教训是冷记忆的归档机制。上线初期,我们没做归档,3 个月后 Storage 数据量达 12GB,Orchestrator 查询变慢。解决方案是增加archiveOlderThan(days: number)方法,每天凌晨自动将 30 天前的记忆移入归档表(ArchiveTable),归档表只保留id,type,timestamp,content四个字段,体积压缩 80%。归档数据不参与实时检索,但用户主动搜索时(如“找我上个月聊的报销流程”),会触发异步加载。
实操心得:在
useAgentMemoryHook 初始化时,务必传入autoArchive: true选项。我们把它做成开关,就像 React 的 StrictMode,默认开启。关闭它等于给自己埋定时炸弹。
4. 实操过程:从零搭建一个可运行的 Memory 系统(Next.js + LangChain + Phi-3)
4.1 环境准备与依赖安装:避开 WebAssembly 的 3 个坑
我们基于 Next.js 14(App Router)搭建,服务端用 LangChain.js,客户端用 Web Worker 运行 Phi-3。关键依赖如下:
# 服务端(app/api/memory/route.ts) npm install langchain @langchain/community @langchain/core # 客户端(components/AgentMemoryProvider.tsx) npm install onnxruntime-web @xenova/transformers避坑指南(血的教训):
- 坑1:ONNX Runtime 版本冲突。
onnxruntime-web@1.17.0与 Next.js 14 的 Webpack 5 冲突,报ReferenceError: window is not defined。解决方案:降级到onnxruntime-web@1.16.3,并手动在next.config.js中配置:webpack: (config) => { config.resolve.fallback = { fs: false, path: false, os: false, crypto: false }; return config; } - 坑2:Phi-3 模型加载路径。
@xenova/transformers默认从 HuggingFace 加载,国内访问极慢。我们把模型文件(model.onnx,config.json,tokenizer.json)下载后放在public/models/phi-3/,然后在代码中指定:const tokenizer = await AutoTokenizer.from_pretrained('/models/phi-3'); const model = await AutoModelForSeq2SeqLM.from_pretrained('/models/phi-3'); - 坑3:Web Worker 通信阻塞。最初把 Encoder 逻辑写在主线程,摘要时 UI 卡顿。必须用 Web Worker:
// lib/encoder.worker.ts self.onmessage = async (e) => { const { text } = e.data; const summary = await generateSummary(text); // Phi-3 摘要 self.postMessage({ summary }); };
4.2 核心代码实现:Orchestrator 的 120 行核心逻辑
Orchestrator 是整个系统的灵魂,以下是其核心调度逻辑(已脱敏,可直接复用):
// lib/memory/orchestrator.ts export class MemoryOrchestrator { private storage: MemoryStorage; private encoder: MemoryEncoder; constructor(storage: MemoryStorage, encoder: MemoryEncoder) { this.storage = storage; this.encoder = encoder; } // 主入口:根据 query 返回最相关记忆列表 async retrieve(query: string, options: RetrieveOptions = {}): Promise<MemoryItem[]> { // Step 1: 过滤候选集(按时间、类型、标签) const candidates = await this.filterCandidates(options); // Step 2: 若候选集为空,返回空数组(不兜底!避免幻觉) if (candidates.length === 0) return []; // Step 3: 计算相关性分数(TF-IDF + 时间衰减 + 反馈权重) const scored = candidates.map(item => ({ item, score: this.calculateScore(item, query, options) })); // Step 4: 按分数排序,取 top-K const sorted = scored.sort((a, b) => b.score - a.score); let topK = sorted.slice(0, options.maxItems || 5); // Step 5: Token 裁剪(关键!) const finalMemories = this.trimByToken(topK, options.maxTokens || 1500); return finalMemories.map(i => i.item); } private calculateScore(item: MemoryItem, query: string, options: RetrieveOptions): number { // 基础 TF-IDF 相似度(用 tiny-segmenter 分词) const tfidfScore = computeTfIdfSimilarity(item.content, query); // 时间衰减:24 小时内 1.0,72 小时内 0.7,7 天内 0.3 const timeDecay = Math.max(0.1, 1 - (Date.now() - item.timestamp) / (7 * 24 * 60 * 60 * 1000)); // 用户反馈权重 const feedbackWeight = item.metadata.feedback?.liked ? 5 : item.metadata.feedback?.disliked ? 0.1 : 1; // 综合得分(加权) return tfidfScore * timeDecay * feedbackWeight; } private trimByToken(items: {item: MemoryItem, score: number}[], maxTokens: number): {item: MemoryItem, score: number}[] { let totalTokens = 0; const result: typeof items = []; for (const item of items) { const itemTokens = estimateTokens(item.item.content); // 简单估算:1 字符 ≈ 0.75 token if (totalTokens + itemTokens <= maxTokens) { result.push(item); totalTokens += itemTokens; } else { break; // 贪心算法,不再往后看 } } return result; } }这段代码的精妙之处在于calculateScore的设计:它不追求学术上的 SOTA,而是用最朴素的数学组合,覆盖了 90% 的真实场景。TF-IDF 解决关键词匹配,时间衰减解决新鲜度,反馈权重解决用户意图强化——三者相乘,比任何复杂模型都更鲁棒。
4.3 完整工作流演示:一次“会议纪要生成”任务中的 Memory 全链路
我们用一个真实工作流来验证系统有效性。场景:用户在智能办公 Agent 中,连续 5 轮交互完成会议纪要生成。
第1轮(用户输入):
“今天下午 3 点和产品团队开了需求评审会,讨论了新后台系统的权限模块,你能帮我记下来吗?”
- Encoder 处理:
content = "用户要求记录下午3点产品需求评审会的权限模块讨论" - Storage 写入:
type='message',tags=['meeting','permissions'] - Orchestrator 无动作(首次交互)
第2轮(Agent 主动追问):
“请问会议中确定了哪些具体权限角色?比如管理员、编辑者、查看者?”
- Encoder 处理:
content = "Agent 追问权限角色的具体定义" - Storage 写入:
type='message',metadata.source='agent' - Orchestrator 未触发(query 无历史关联)
第3轮(用户回复):
“确定了3个角色:超级管理员(全权限)、部门管理员(管理本部门)、普通成员(只读)。还说了下周二上线。”
- Encoder 处理:
content = "用户确认3个权限角色及下周二上线时间" - Storage 写入:
type='message',references=['mem_001'](关联第1轮) - Orchestrator 仍无动作
第4轮(用户发起新请求):
“生成一份会议纪要,重点写权限角色定义和上线时间。”
- Orchestrator 触发:
- Filter:取
type='message'且timestamp > now-24h的记忆 → 得到 mem_001, mem_002, mem_003 - Rank:计算得分,mem_003(含具体角色定义)得分最高
- Trim:3 条记忆 content 总 token=420 < 1500,全部注入 prompt
- Filter:取
- LLM 输入 prompt 片段:
[Relevant Memories] - mem_001: 用户要求记录下午3点产品需求评审会的权限模块讨论 - mem_002: Agent 追问权限角色的具体定义 - mem_003: 用户确认3个权限角色及下周二上线时间 [Current Query] 生成一份会议纪要,重点写权限角色定义和上线时间。
第5轮(Agent 输出):
“【会议纪要】时间:今日下午3点;主题:后台系统权限模块评审;结论:1. 超级管理员(全权限);2. 部门管理员(管理本部门);3. 普通成员(只读);上线时间:下周二。”
整个过程,Memory 系统像一个隐形的会议秘书,默默串联起碎片信息,让 Agent 的输出具备了人类般的连贯性。而这一切,都建立在那 120 行 Orchestrator 代码和精心设计的 MemoryItem 结构之上。
5. 常见问题与排查技巧实录:前端转 AI 开发者最常踩的 7 个坑
5.1 问题速查表:症状、根因、解决方案
| 症状 | 根因 | 解决方案 | 实操验证方法 |
|---|---|---|---|
| Agent 总是“失忆”,不记得 2 轮前的话 | Orchestrator 的filter策略过于严格,或timestamp字段未正确写入 | 检查MemoryItem.timestamp是否为Date.now(),而非new Date().toISOString();在retrieve方法开头加console.log('candidates count:', candidates.length) | 在浏览器控制台,触发一次 query,观察 candidates 数量是否为 0 |
| Memory 占用磁盘暴涨,1 周达 5GB | 未启用autoArchive,且content字段存了原始长文本 | 运行npx prisma db pull查看 Storage 表,检查content字段平均长度;立即启用archiveOlderThan(30) | 在 Prisma Studio 中执行SELECT avg(length(content)) FROM memory_items,若 > 200 则需修复 Encoder |
| 相关性排序总是不准,用户说“重要”的记忆排在后面 | feedbackWeight未在calculateScore中生效,或metadata.feedback结构错误 | 检查metadata是否包含feedback: { liked: true };在calculateScore中加console.log('feedback weight:', feedbackWeight) | 在retrieve调用后,打印scored数组,确认高分项是否匹配用户预期 |
| Web Worker 摘要卡死,页面无响应 | Phi-3 模型加载失败,Worker 报错但未捕获 | 在 Worker 的onerror事件中加self.postMessage({ error: e.message });检查public/models/路径是否 404 | 打开浏览器 Network 面板,过滤models/,确认所有文件状态码为 200 |
| 同一句话,不同时间摘要结果不一致 | Phi-3 的随机性未关闭 | 在generateSummary调用前,设置model.generation_config.do_sample = false | 对固定文本连续调用 10 次generateSummary,检查输出是否完全一致 |
| 用户说“忘掉刚才聊的”,但部分记忆还在 | clear方法未正确传递scope参数,或 Storage 的deleteByQuery逻辑有 bug | 检查clear({ scope: 'conversation' })调用时,scope值是否与写入时一致;在 Storage 层加 SQL 日志 | 在deleteByQuery方法中加console.log('deleting with query:', query),确认条件是否匹配 |
| LLM 响应变慢,token 使用量翻倍 | Orchestrator 注入了过多低分记忆,或content字段含特殊字符(如\n\n)导致 token 估算错误 | 在trimByToken中加console.log('item token:', itemTokens, 'content:', item.item.content.substring(0,50));用tiktoken库精确计算 | 用tiktoken.encoding_for_model('gpt-4')计算实际 token,对比估算值 |
5.2 独家避坑技巧:来自 3 个线上项目的实战总结
技巧1:用“记忆快照”替代“实时同步”
很多同学想让 Memory 实时响应用户操作(如点击“重要”按钮立刻影响下次检索),结果引发竞态问题。我们的方案是:所有用户反馈(点赞、收藏、标记重要)都先存入一个pendingFeedback队列,每隔 3 秒批量提交到 Storage,并触发 Orchestrator 的refreshCache()。这就像 React 的setState批量更新,既保证一致性,又避免高频 IO。
技巧2:为 MemoryItem 设计“死亡时间”(TTL)
不是所有记忆都该永久存在。我们在MemoryItem中增加了可选字段ttlMs: number,表示“从创建起多少毫秒后自动失效”。例如,一次临时工具调用结果(如“查到当前 IP 是 192.168.1.100”),设置ttlMs: 60000(1 分钟),过期后 Orchestrator 自动过滤。这比定期清理更精准,也更节省资源。
技巧3:前端调试的“记忆可视化面板”
我们开发了一个隐藏面板(/debug/memory),输入任意 query,实时显示 Orchestrator 的完整决策过程:
- 候选集列表(含
type,timestamp,content) - 每项的
tfidfScore,timeDecay,feedbackWeight,finalScore - 最终注入 prompt 的记忆原文
- Token 使用量统计
这个面板上线后,80% 的 Memory 相关问题都能自助定位,极大降低调试成本。它的实现很简单:在retrieve方法中,把中间变量存入全局window.__MEMORY_DEBUG__,面板读取即可。
最后分享一个小技巧:在
useAgentMemoryHook 的addMemory方法里,加一行if (process.env.NODE_ENV === 'development') console.groupCollapsed('Added Memory:', item)。开发时展开控制台,就能像看 Redux DevTools 一样,实时监控每一条记忆的诞生,这是前端开发者最熟悉的调试姿势。
我在实际项目中发现,真正让 Agent “更懂你”的,从来不是某个炫酷的算法,而是这些扎扎实实的工程细节——一个稳定的 timestamp,一个合理的 token 估算,一个及时的归档策略。它们不像向量检索那么耀眼,却决定了系统能否在真实世界里活下来。当你把 Memory 系统搭好,再回头看那些“健忘”的 Agent,就会明白:智能的起点,不是更强大的推理,而是更可靠的记忆。