尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

字节面试:Agent 的记忆系统怎么设计?

发布时间:2026/9/29 7:22:34

资讯中心
01
ARTICLE

字节面试:Agent 的记忆系统怎么设计?

字节面试:Agent 的记忆系统怎么设计?
凌晨一点半故障群里还有六个人在线。客服 Agent 给一个老用户报了三个月前的活动价那个价早就下架了。用户下单财务第二天对账才发现差额。复盘会上有人问这个价格是从哪来的答案是三个月前做活动时运营让 Agent记住这次活动规则它就真的老老实实记了三个月一次都没忘。那天我记住了一件事——Agent 记不住不致命记错了才致命而绝大多数记错都不是没存好是没人管该忘什么。上周一位学员面字节问题正好撞在同一件事上Agent 的记忆系统你会怎么设计他答得挺完整向量库存历史对话检索出来拼进 prompt加个滑动窗口防爆。面试官只追了一句你这些历史什么时候删他卡住了。这期解析就从这个删字开始。第 11 期欠了五期的债今天还。先把「记忆」这个词拆开面试官问记忆系统八成的人第一句话就是用向量库存对话历史。这句话里有三个偷换一开口面试官就知道你没真做过把对话历史当记忆。对话历史是原料记忆是产品。原料不加工出来的是噪声。把存储当记忆系统。存储只管写得进去记忆系统要管写什么、取什么、什么时候删。把存得下当用得上。上下文窗口放得下不等于模型用得着——这一点下面有硬数据。先把这个地基立住⚠️认知冲击短期记忆是上下文工程长期记忆是数据工程。这是两套技术栈、两拨人、两种故障模式。混在一起讲面试官听到的就是一锅粥。如果只让我说一句Agent 记不住不是存得少是忘得不对。三种记忆三笔账先把能放记忆的地方数清楚一共三处成本差了两个数量级类型载体写入频率单条能删吗最常见误用参数记忆微调后的模型权重天 / 周级离线不能想用它记住用户偏好工作记忆上下文窗口每轮对话会随会话归零想用它承载长期事实外部记忆向量库 / 关系库 / KV每轮多次在线能只写不删不做门控三句话记住这三笔账参数记忆不可删。用户说别再给我推辣的了你重新训一遍权重做不到更做不到只删这一条。工作记忆不可靠。它随会话生灭。你把用户地址写在工作记忆里下次开新会话等于他从没说过。外部记忆不可不管。它最容易上手也最容易烂——因为写进去零成本烂掉还不报错。真正要设计的是第三层它的全部难点就四个字写、取、更新、忘。短期记忆这不是存是预算先破一个执念上下文窗口不是越大越好。斯坦福和 UC Berkeley 那篇《Lost in the Middle》arXiv:2307.03172做过一组很干净的对照实验——把答案文档放在输入上下文的不同位置其他一切不变。结果是 U 形曲线放在开头和结尾模型答得好放在中段性能显著掉档。论文里最扎人的一个数字是——GPT-3.5-Turbo 在多文档问答中当答案落在上下文中段时准确率掉到比闭卷作答56.1%还低。同一篇论文还有一组更实用的数据把检索回来的文档从 20 篇加到 50 篇GPT-3.5-Turbo 只涨了约 1.5%Claude-1.3 约 1%。翻译成人话往上下文里塞更多东西不是给模型更多信息是给它更多需要绕过的噪声。这就是为什么滑动窗口这种最笨的做法到今天还在用——它不是省钱手段是精度手段。窗口管理三档代价一目了然策略怎么做代价滑动窗口只留最近 N 轮丢掉最早的信息而最早往往正是用户第一次交代偏好的那轮递归摘要把老对话压成摘要有损且压缩发生在你还不知道用户要问什么之前分层记忆少量核心事实常驻其余按需检索实现复杂写入和检索都得做对第二档那个压缩发生在前是结构性缺陷很多人没意识到模型在不知道你未来会问什么的情况下替你把信息扔了。摘要里丢掉的那个数字往往就是后面被追问的那个数字。还有个细节摘要本身会引入幻觉。摘要里的错误会被当事实传给下一轮摘要一轮一轮卷下去最后没人知道原始版本长什么样。所以摘要必须带引用指向原始轮次否则你连排查的锚点都没有。最后是预算。别让模型看着办把 token 预算写进代码里的常量预算项经验占比超了怎么处理系统指令 Skill 正文10% 到 15%精简 description正文按需加载工具定义10% 到 20%工具多了改检索式加载别全量常驻检索结果 记忆注入30% 到 40%设 top-k 上限单条超长直接截断历史对话30% 到 40%超限走摘要或滑动输出预留约 10%别占满窗口模型得留地方写字占比是工程经验区间不是论文结论按你自己的窗口和任务调。但有一条必须做到四份预算加输出预留必须显式等于 100%超了就是你在赌模型自己会算账。长期记忆这是数据工程先给一个全行业都在抄的参考架构。UC Berkeley 的 MemGPTarXiv:2310.08560借了操作系统虚拟内存的思路模型当前能看到的那段叫主上下文外面那个大盘子叫外部上下文中间靠模型自己发起函数调用来回换页。它把记忆分成三类——常驻的核心记忆、可检索的历史记忆、全量归档。2024 年这套东西开源成了 Letta。它的关键不是分层这个结构而是记忆的调度权交给了模型存什么、取什么由模型在推理循环里自己决定。但这个设计里还缺一半——模型会决定存却基本不会主动决定删。删除必须由你的工程侧兜住。所以长期记忆落地就是四个动作。① 写门控。这是最容易被跳过的一步。不是所有对话都配进长期记忆先分类再决定类别例子处置FACT用户在上海做跨境电商存长期有效PREFERENCE回答要短不要用列表存可更新EVENT上周三在问退款存短期NOISE你好谢谢嗯嗯直接丢再叠三条硬规则这三条说出来面试官会点头异步写。记忆抽取不能挂在主链路上——用户是在等回答不是在等你写库。幂等写。同一事实重复出现要 upsert去重键用 tenant user kind 归一化文本。不做去重三天后你的向量库里躺着八条一模一样的用户不吃辣。溯源写。只允许用户自己说出来的内容升级为长期记忆。工具返回值、检索到的文档、网页正文永远不允许直接写进长期记忆。第三条是安全底线。单轮的 prompt 注入只污染这一次回答而如果你让被注入的内容写进了长期记忆它就变成永久污染——之后每一次对话都会把这句话当事实读出来。这叫记忆投毒比普通注入危险一个量级。插一句判断一条内容能不能进长期记忆问一个问题就够了这句话是用户亲口说的吗不是就只能活在这一轮上下文里。② 取从最相似改成最有用。纯向量检索有个死穴——词汇不匹配。用户说我最近忌口记忆里存的是不吃辣两句话字面零重叠向量距离可能很远。所以生产上基本是混合检索dense 向量召回 BM25 稀疏召回两路用 RRF倒数排名融合合并最后 rerank。这里有个代价必须说清面试里讲出来是加分项RRF 只看名次不看分数量级。一路检索给出 100 分、另一路给出 3 分在 RRF 眼里只差几个名次绝对相关性被抹平了。默认用 RRF 是因为它免校准、跨检索器通用但如果你需要精细控权——比如业务上要求关键词命中必须压过语义近似——就得换成 min-max 归一化加权代价是要拿评估集调参。选哪个取决于你要省事还是可控别把它当标准答案背。然后才是关键一步最终得分不是相似度是相似度乘以新鲜度再乘历史命中价值。③ 更新冲突要显式覆盖。用户三个月前说不吃辣上周说开始吃辣了。两条记忆同时躺在库里检索命中哪条纯看运气。这是长期记忆最隐蔽的 bug——它不报错只是偶尔答错。正确做法是按槽位覆盖diet.spicy这个槽位从 false 改成 true老记录标记为被覆盖而不是物理删除保留审计链。零散的 EVENT 攒够频次后可以晋升成结构化 FACT——记忆固化固化后不走检索、直接常驻既省 token 也更稳。④ 忘这才是判分点。三种忘法从软到硬衰减软遗忘权重随时间指数衰减命中一次就续期。本质是不删但排后面。TTL 分层中按记忆类型给不同保质期。硬删除硬合规要求。必须能按 tenant user agent 三元组级联删除并回答这条记忆是从哪句话来的。TTL 分层建议这么切记忆类型例子保质期到期动作会话事件刚才在问退款刚下过什么单7 天归档不参与默认召回临时偏好这次想吃辣的30 天衰减命中可续期稳定事实所在城市职业行业不过期冲突时覆盖敏感信息手机号身份证地址不存写入阶段直接拦掉为什么忘比记更关键因为检索质量不是被存得少拖垮的是被老数据挤掉新数据拖垮的。向量库里 1000 条记忆其中 800 条已经过期。你召回 top-5塞给模型的是 5 条历史噪声。模型不是不聪明是拿了一手错牌——然后你骂它幻觉。回到开头那个故障如果那条活动规则有一个 7 天的 TTL财务那笔差额根本不会发生。四个高频坑#坑现场长什么样正确做法1把对话历史全量回灌当记忆窗口越聊越满模型反而越来越糊工作记忆只留最近 N 轮加摘要长期事实走检索注入2只写不删老偏好盖掉新偏好老价格当现价TTL 分层加同槽位覆盖加定期合并3只用纯语义检索忌口召回不到不吃辣dense 加 BM25 加 RRF 融合再加 rerank4外部内容直接进长期记忆一次网页注入永久污染溯源门控只有用户原话可升级第 1 条最反直觉也最普遍日志里看到上下文很长第一反应是模型能力不够其实是你的记忆在抢预算。Java 实战三层记忆可运行骨架先看结构下面这份骨架用 Spring AI 1.1.x JDK 21。Spring AI 只负责两件事——LLM 调用和向量检索记忆策略全是普通 Java// 1. 记忆模型与写入门控不是所有对话都配进长期记忆 public enum MemoryKind { FACT, PREFERENCE, EVENT, NOISE } public record MemoryRecord(String id, String tenantId, String userId, MemoryKind kind, String slot, String text, String sourceQuote, // 溯源来自用户哪句原话 Instant createdAt, int hitCount) {} Component public class MemoryWriteGate { private static final SetMemoryKind KEEP Set.of(MemoryKind.FACT, MemoryKind.PREFERENCE, MemoryKind.EVENT); private final PiiFilter piiFilter; // 手机号 / 身份证 / 密钥 public boolean admit(MemoryKind kind, String text, String source, String sourceQuote) { if (!KEEP.contains(kind)) return false; // NOISE 直接丢 if (text.strip().length() 6) return false; // 好的嗯 不记 if (piiFilter.hits(text)) return false; // 隐私不进记忆 if (sourceQuote null || sourceQuote.isBlank()) return false; // 无溯源不写 return // 外部内容永不升级.equals(source); USER_UTTERANCE } }// 2. 混合召回 时效衰减目标不是最相似是最有用 public record ScoredMemory(MemoryRecord memory, double score, ListString hits) { public String render() { // 注给模型的一行文本 return // hits 记录哪路召回命中便于排查 memory.kind() // RRF 平滑常数 memory.text() // 半衰期按记忆类型可调 String.join(// 两路召回dense 管语义BM25 管词汇不匹配忌口 vs 不吃辣, hits) // 按名次累加 1 / (RRF_K rank); // 低分直接丢别硬凑 top-k } } Service public class HybridMemoryRecall { private static final int RRF_K 60; // 必须带租户过滤防串号 private static final double HALF_LIFE_DAYS 30; // 指数衰减 private final VectorStore vectorStore; private final Bm25Index bm25; public ListScoredMemory recall(String tenantId, String userId, String query, int topK) { // 命中越多越值钱 ListDocument dense vectorStore.similaritySearch(SearchRequest.builder() .query(query).topK(topK * 3).build()); ListDocument sparse bm25.search(tenantId, userId, query, topK * 3); MapString, Double rrf new HashMap(); addRank(rrf, dense, - [, tenantId, userId); ] addRank(rrf, sparse, , tenantId, userId); return rrf.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .map(e - scoreOf(e.getKey(), e.getValue(), tenantId, userId)) .filter(m - m.score() 0.01) .limit(topK) .toList(); } private ScoredMemory scoreOf(String id, double rrfScore, String tenantId, String userId) { MemoryRecord r load(id, tenantId, userId); long ageDays ChronoUnit.DAYS.between(r.createdAt(), Instant.now()); double decay Math.pow(0.5, (double) ageDays / HALF_LIFE_DAYS); dense double boost Math.min(Math.log1p(r.hitCount()) / Math.log1p(20), 1.0); sparse return new ScoredMemory(r, rrfScore * (0.6 * decay 0.4 * boost), List.of(rrf)); } }四个工程细节面试里说出来就是加分项写入口只有门控一个。任何绕过MemoryWriteGate的写路径都是后门代码层面就要堵死。召回必须带租户过滤。load里少一个tenantId条件就是跨租户数据泄漏这类事故在真实项目里出过不止一次。低分不硬凑 top-k。召回到 2 条相关记忆就注入 2 条凑满 5 条等于故意往上下文里掺噪声。长期记忆不进 ChatMemory。历史归 Spring AI 的窗口管理长期事实归召回注入。混在一起你事后根本分不清哪句话是用户说的、哪句话是你的记忆系统编的。30 行复现实验亲手看一次老数据挤掉新数据上面那套逻辑别信我跑一遍。池子里放 5 条候选记忆其中 4 条是 90 天前的过期活动价只有 1 条是当前有效的public static void main(String[] args) { ListMemoryRecord pool mockExpiredPricePool(); // 4 条 90 天前 1 条现行 System.out.println(// rank 里新鲜度 0.5 ^ (ageDays / halfLife)halfLife 越大衰减越慢 ids(rank(pool, 3, Double.MAX_VALUE))); System.out.println(A 纯相似度 - ids(rank(pool, 3, 90))); System.out.println(B 半衰期 90 天 - ids(rank(pool, 3, 30))); System.out.println(C 半衰期 30 天 - ids(rank(pool, 3, 7))); } D 半衰期 7 天 - static ListString rank(ListMemoryRecord pool, int topK, double halfLife) { return pool.stream() .map(r - new AbstractMap.SimpleEntry(r.id(), 0.6 * Math.pow(0.5, ageDays(r) / halfLife) 0.4 * 0.5)) .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(topK).map(Map.Entry::getKey).toList(); }跑完你会看到同一份数据、同一个 top-3 请求因为半衰期这一个参数不同注入给模型的记忆完全不同。把半衰期调到 90 天三个月前的活动价还在 top-3 里——这就是开头那个故障的最小复现。这不是严格 benchmark但它能让你在 30 行代码里看清一件事衰减半衰期不是一个优化参数它是你的 Agent 会记错多久的直接决定项。「记忆」这个类比失效的地方记忆是从人脑借来的词但 Agent 的记忆和人脑有三处根本不同。面试里主动说这三点比多背三个框架管用第一人会自动遗忘Agent 不会。人脑的遗忘是默认行为Agent 的遗忘是必须显式配置的功能。你不写 TTL它就真的记到天荒地老——自然而然忘掉这件事在工程里不存在。第二人的记忆有权重直觉Agent 的权重得你来定。相似度、时间、频次三路怎么加权、半衰期给多长全是你拍的数字。定错了不报错只答错。第三人的记忆删不掉Agent 的记忆必须能删。这反而是 Agent 更强的地方合规要求删除某用户的全部数据人做不到你的向量库必须做到。还有一个真边界要说清楚MemGPT 式让模型自己管记忆听着优雅代价是模型要额外消耗 token 去做内存管理决策而且它可能选择不存关键信息。所以生产上更常见的是混合——模型决定这条值不值得记工程侧兜住 TTL、覆盖和硬删除。方向判断交给模型纪律交给代码。追问连环炮Q1记忆和 RAG 有什么区别检索的都是外面的东西但三处不同。① 写入侧RAG 是离线 ETL 全量灌库记忆是在线高频写入、必须做门控② 归属RAG 的知识是公共的、所有人一样记忆是用户私有的、必须按租户和用户隔离③ 生命周期RAG 的知识可以常驻记忆必须过期、必须可删。一句话——RAG 管世界知道什么记忆管你和我之间发生过什么。Q2用户说我上周讲过不吃辣Agent 忘了怎么排查三段定位按链路顺序走顺序不能乱没写进去——查写入链路门控日志是门控太严拦掉了还是异步任务丢了写进去了没召回——查这个用户的召回日志是词汇不匹配dense 没命中还是被时效衰减挤出 top-k召回了没用上——查注入位置和 token 预算可能是被截断也可能是位置落在上下文中段Lost in the MiddleRAG 排障的经典归因是三分法——召回、组装、生成。记忆场景要在这条链前面再加一段写入变成四分法写入 → 召回 → 组装 → 生成。多出来的这一段恰恰是记忆系统独有的故障面也是最容易被漏查的一段。Q3记忆越堆越多检索越来越不准怎么办四个动作从便宜到贵依次上TTL 分层过期 → 同槽位覆盖去重 → 记忆固化高频 EVENT 晋升为 FACT profile→ 按访问频次加权。别一上来就搞聚类合并那个贵、难验证而且容易把不同时期的事实揉成一条错误结论。Q4怎么防记忆投毒写入侧只有一个原则溯源。只有用户原话可以升级为长期记忆工具返回和文档内容一律只进当轮上下文。再叠两道写入审计每条记忆记下来源句子和敏感信息过滤。注意这是写入侧的防御不是 prompt 侧的——靠 system prompt 说不要相信注入内容防不住。Q5多租户下记忆怎么隔离、怎么删key 用 tenant user agent 三元组物理隔离或强制过滤宁可多一次过滤也别靠 prompt 约束。删除必须级联并且可证明——合规问的从来不是你删了没而是你怎么证明你删了所以要留删除审计记录。答题骨架2 分钟版面试官问Agent 的记忆系统怎么设计别上来就报组件按这四拍说第一拍 · 立框架先把记忆拆开——短期记忆是上下文工程长期记忆是数据工程两套栈、两种故障模式。第二拍 · 短期窗口不是越大越好中段信息会掉档Lost in the Middle 里答案放中段比闭卷还差预算显式分配四份加起来留 10% 给输出。第三拍 · 长期四个动作——写、取、更新、忘。写要门控加幂等加溯源取要混合召回加时效衰减更新要槽位覆盖忘要 TTL 分层加硬删除。第四拍 · 给判断如果只让我先做一件事我先把遗忘策略做出来因为它决定了后面所有检索的天花板。没有遗忘策略的记忆系统本质上是一个不断恶化的噪声源。Fox 有话说回到那个凌晨的故障群。复盘到最后我们发现整件事里没有一处技术故障——向量库没崩检索没超时模型也没幻觉。它只是把一条三个月前的记忆当成了今天的事实。这才是记忆系统真正难的地方它的错误不会报错只会变旧。写这篇文章的时候我一直在想几乎所有讲 Agent 记忆的资料都在教你怎么存得更多、取更准。但我在真实的故障现场看到的是反过来的——问题从来不是记不住是记着不该记的。所以这期的最后一句请你记住你不是在给 Agent 加记忆你是在给它加一套遗忘策略。敢让它忘你才敢放心让它记。资料展示下面是我整理的AI大模型 学习资料和工具包预览适合收藏后按主题逐步学习
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。