给 Agent 做记忆系统别让 history 无限长下去摘要上一篇我们用history让模型拥有上下文但这个办法有天然上限对话越长、工具输出越多history 就越重。真正能长期工作的 Agent需要把原始记录、情景记忆和长期记忆分开并在上下文接近上限前自动压缩。本文拆解一个文件化的三层记忆系统history.jsonl、YYYY-MM-DD.md和MEMORY.md。标签Agent 记忆、Context Engineering、自动压缩、Python、LLMhistory 带来了记忆也带来了膨胀在最小 Agent 里history是让模型“记得前文”的关键。每次用户输入我们把 user 消息 append 进去每次模型回答再把 assistant 消息 append 进去下一轮调用时把整个列表重新发给模型。这很有效也很诚实。模型本身没有跨请求状态所谓上下文就是调用方传进去的消息列表。问题也同样明显history 会一直增长。一开始只是几句对话成本很低。后来模型开始调用工具history 里会出现命令输出、文件内容、网页正文、错误日志。再后来你和 Agent 在一个项目里聊上几十轮它每次调用都要背着越来越多历史。速度会变慢成本会上升最终还会撞上上下文窗口。所以记忆系统要解决的不是“让模型神奇地记住一切”而是把不同类型的信息放到不同位置完整记录保留下来稳定事实常驻上下文中间过程在合适的时候压缩。三层记忆不要让所有东西挤在 history 里项目里的记忆模块在agent/memory.py。它采用了一个非常朴素但好维护的三层结构Raw 原始层 memory/history.jsonl Episodic 情景层 memory/YYYY-MM-DD.md Core 长期层 memory/MEMORY.md这三层的职责完全不同。Raw 原始层保存完整对话档案。每轮 user、assistant 以及必要的非字符串内容都会追加到history.jsonl。它不进入上下文主要用于审计和回查。以后你想知道某个结论是怎么来的可以翻这份底账。Episodic 情景层保存按天归档的对话摘要。文件名类似2026-04-23.md。它不是每轮都写而是在压缩触发时把一段旧对话整理成“今天发生了什么、做过什么、留下什么线索”。Core 长期层就是MEMORY.md。它会被注入 system prompt让 Agent 每轮都能看到关键事实、长期目标和当前项目背景。因为它常驻上下文所以必须克制不能写成流水账。一个简单判断是必须完整保留但不必每轮看见 → history.jsonl 需要按时间沉淀未来可能检索 → YYYY-MM-DD.md 每轮都必须知道的稳定信息 → MEMORY.mdMemoryStore先把落盘路径做稳记忆系统的第一步不是复杂算法而是稳定读写。MemoryStore.append_history()负责追加原始记录defappend_history(self,role:str,content:Any)-None:row{ts:datetime.now(_UTC8).isoformat(timespecseconds),role:role,content:contentifisinstance(content,str)else_json_safe(content),}withself.history_file.open(a,encodingutf-8)asf:f.write(json.dumps(row,ensure_asciiFalse)\n)这里有两个小细节很实用。第一时间使用 UTC8。个人 Agent 面向人的日常工作“今天做了什么”最好和本地日期一致。第二content不一定是字符串。工具调用结果、模型 content block、对象结构都可能进入记录。_json_safe()会尽量把它们转成可序列化内容避免因为一条特殊消息导致记忆写入失败。情景记忆按日期写deftoday_episode_path(self)-Path:datedatetime.now(_UTC8).strftime(%Y-%m-%d)returnself.memory_dir/f{date}.md长期记忆则整体覆写defwrite_memory(self,content:str)-None:self.memory_file.write_text(content.strip()\n,encodingutf-8)为什么不是 append因为长期记忆不是日记。它应该像工作台上的便签只保留下一轮推理真正需要的稳定信息。一直追加只会把MEMORY.md变成另一个膨胀的 history。什么时候该压缩压缩不能只靠感觉。项目里用TokenTracker记录每次模型调用的 token 用量row{model:model,input:getattr(usage,input_tokens,0)or0,output:getattr(usage,output_tokens,0)or0,cache_read:getattr(usage,cache_read_input_tokens,0)or0,cache_create:getattr(usage,cache_creation_input_tokens,0)or0,}是否触发压缩由最近一次输入 token 决定defshould_compact(self,max_context:int,threshold:float0.7)-bool:returnself._last_input_tokensmax_context*threshold默认策略是如果最大上下文按 200K 算输入超过 70% 左右就触发压缩。这样做的好处是压缩由运行时资源状态触发而不是让模型自己猜“我是不是快满了”。Compactor把旧对话变成可用记忆压缩逻辑在agent/compactor.py。当压缩触发时系统不会把全部 history 一刀切掉而是保留最近K10轮把更早的消息交给 compactoroldhistory[:-self.K]recenthistory[-self.K:]随后compactor 把旧对话、现有MEMORY.md、用户偏好文件和今日情景记忆一起放进提示词让模型输出三段结构化内容episode.../episode updated_memory.../updated_memory updated_user.../updated_user解析后分别写入episode追加到当天YYYY-MM-DD.mdupdated_memory覆写MEMORY.mdupdated_user覆写用户偏好文件最后写入compact_event标记。这样旧对话没有消失而是从“逐字历史”变成“情景摘要 长期事实”。内存里的 history 只保留最近几轮下一轮对话仍然连贯但上下文负担被降下来。为什么不一开始就上向量库一提到 Agent 记忆很多人会马上想到向量数据库。向量检索当然有价值尤其适合在海量文档或历史归档中按语义找线索。但在这个阶段最先要解决的不是“如何检索所有旧信息”而是“当前上下文怎么不爆”。如果所有原始消息都一直堆在工作记忆里旁边接一个向量库也救不了主上下文膨胀。所以这里先做三层文件记忆。它解决的是生命周期原始消息完整保存 旧对话按天沉淀 核心事实常驻上下文等这个边界稳定后再把旧 episode 或项目资料放进向量检索会更自然。向量库应该服务于“按需召回”而不是替代工作记忆、情景记忆和长期记忆的分工。如何验证记忆系统真的在工作不要只靠“模型好像记得”来判断。更可靠的验证方式是看文件和触发点。第一看memory/history.jsonl是否每轮追加。用户输入和助手输出都应该能找到。第二看memory/tokens.jsonl是否记录每次调用的输入输出 token。压缩是否触发依赖这里的最近输入 token。第三调低阈值或模拟长对话确认 compactor 会写入当天YYYY-MM-DD.md并更新MEMORY.md。第四重启程序确认最后一次 compact 之后的未归档历史能被启动归档处理。长期 Agent 不只是当前进程里记得还要跨会话接住上下文。如果这四点都成立记忆系统就不再是 prompt 里一句“请记住”而是一套有文件、有阈值、有归档结果的运行机制。记忆内容应该写多细做记忆系统时最难的不是写文件而是决定什么值得留下。原始层可以完整因为它不进入上下文主要用于审计。工具输出很长也没关系最多占磁盘空间。情景层就要开始筛选它应该记录今天发生的重要事件、做出的关键决策、遇到的阻塞、后续可能要回查的线索而不是复述每一轮对话。长期层更要克制。MEMORY.md应该只放稳定事实例如当前项目目标是什么 用户长期偏好是什么 已经确认的重要架构约束是什么 当前任务的稳定背景是什么不适合放进长期记忆的内容包括一次性命令输出、临时错误日志、已经解决的中间状态、模型当时的详细推理过程。这些内容要么留在 raw history要么压进当天 episode。如果长期记忆写得太细它会变成另一个上下文垃圾桶。每轮都注入就每轮都消耗注意力。长期记忆越短、越稳定价值越高。Compact 不是简单摘要很多人把 compact 理解成“把旧对话总结一下”这还不够。真正有用的 compact 至少要同时完成三件事。第一把旧对话变成情景记录。它应该回答“那段时间发生了什么”方便未来回查。第二更新长期记忆。它要判断哪些信息已经稳定到值得每轮注入哪些旧信息已经过期应该删除。第三更新用户偏好。比如用户喜欢什么输出风格、项目里有哪些长期习惯、哪些信息不应该重复问。这就是为什么 compactor 的输出用标签拆成episode、updated_memory、updated_user。它不是一段随意摘要而是一次记忆整理。失败时应该怎么处理记忆系统也会失败。比如模型输出没有包含预期标签写文件失败压缩调用超时或者生成的长期记忆质量很差。教学项目里先保持简单能提取到哪个标签就写哪个。更严谨的实现可以继续加几层保护压缩前备份 MEMORY.md 提取不到标签时不覆写长期记忆 长期记忆超过字数上限时拒绝写入 压缩失败时保留原 history 记录 compact 错误供人工检查这些不是概念重点但决定了系统能不能长期运行。记忆系统越接近基础设施越不能只看成功路径。什么时候应该触发 compact最直接的触发条件是 token 数。项目里通过tokens.jsonl记录最近调用消耗再判断是否超过阈值。这个办法简单、可观察也容易调试。但真实项目里compact 还可以结合任务边界。比如一个小任务完成后、一次工具探索结束后、用户明确切换主题时都适合整理一次。因为这些时刻天然有边界模型更容易把“刚才发生了什么”压成一个清楚 episode。相反在任务中间频繁 compact 会有风险。模型可能还需要前面某个细节继续推理如果过早摘要就会丢失原始上下文。所以 compact 的节奏不是越勤越好而是要在“上下文快撑不住”和“任务语义刚好收束”之间找平衡。记忆要允许被修正长期记忆听起来像真理库但它应该允许被更新。用户偏好会变项目目标会变前面记录的事实也可能后来被证明不准确。所以MEMORY.md不应该只追加。更合理的方式是每次 compact 都产出一份新的稳定记忆保留仍然成立的删除已经过期的改写描述不准确的。这也是为什么长期记忆最好短一点。越短越容易维护越长越像没人敢碰的旧文档。Agent 需要的不是一本百科而是一份能在每轮推理里真正起作用的高密度背景。记忆和隐私边界只要系统开始长期保存信息就要考虑隐私边界。并不是用户说过的每句话都应该长期保存也不是每个工具输出都适合进入MEMORY.md。一个保守策略是raw history 用于本地审计长期记忆只保存完成任务确实需要的稳定信息。涉及密钥、隐私、临时口令、一次性路径、敏感日志的内容即使出现在对话或工具输出里也不应该被压进常驻记忆。记忆系统的目标不是“什么都记住”而是“记住应该被带到下一次推理里的东西”。小结history 让模型看见前文但 history 不能无限增长。一个能长期工作的 Agent需要把记忆拆开原始记录完整保存情景记忆按天沉淀长期记忆高密度常驻。这一篇的核心不是炫技而是工程卫生。记忆系统首先让上下文成本、历史审计和长期状态各有位置其次才改善“模型好像更懂我”的体验。有了记忆之后Agent 能在更长时间里工作。但当任务步骤变多时另一个问题会出现模型可能跳步、重复、半途自称完成。下一步我们需要把“计划”也从模型脑内拿出来变成可观察、可校验的外部状态。视频与源码如果你想看完整演示可以在主页的《从零手搓 Agent》合集里按顺序观看视频版抖音https://www.douyin.com/user/MS4wLjABAAAAk5lgbm96yoPPEoGXoY3MIp4S8voya0dzcnG0Lom5-SI?from_tab_namemainB站https://space.bilibili.com/1452412374文章里的示例代码和完整项目也放在这里 教学仓库https://github.com/TheSyart/claude-agent-examples⚔️ 实战项目https://github.com/TheSyart/emperor-agent我会持续更新 Agent 教学与实战内容。觉得有用的话欢迎给项目点个 Star ⭐也谢谢你一路看到这里。
从零实现自己的agent第三期: 个人 Agent 记忆系统的实现
发布时间:2026/5/19 23:09:12
给 Agent 做记忆系统别让 history 无限长下去摘要上一篇我们用history让模型拥有上下文但这个办法有天然上限对话越长、工具输出越多history 就越重。真正能长期工作的 Agent需要把原始记录、情景记忆和长期记忆分开并在上下文接近上限前自动压缩。本文拆解一个文件化的三层记忆系统history.jsonl、YYYY-MM-DD.md和MEMORY.md。标签Agent 记忆、Context Engineering、自动压缩、Python、LLMhistory 带来了记忆也带来了膨胀在最小 Agent 里history是让模型“记得前文”的关键。每次用户输入我们把 user 消息 append 进去每次模型回答再把 assistant 消息 append 进去下一轮调用时把整个列表重新发给模型。这很有效也很诚实。模型本身没有跨请求状态所谓上下文就是调用方传进去的消息列表。问题也同样明显history 会一直增长。一开始只是几句对话成本很低。后来模型开始调用工具history 里会出现命令输出、文件内容、网页正文、错误日志。再后来你和 Agent 在一个项目里聊上几十轮它每次调用都要背着越来越多历史。速度会变慢成本会上升最终还会撞上上下文窗口。所以记忆系统要解决的不是“让模型神奇地记住一切”而是把不同类型的信息放到不同位置完整记录保留下来稳定事实常驻上下文中间过程在合适的时候压缩。三层记忆不要让所有东西挤在 history 里项目里的记忆模块在agent/memory.py。它采用了一个非常朴素但好维护的三层结构Raw 原始层 memory/history.jsonl Episodic 情景层 memory/YYYY-MM-DD.md Core 长期层 memory/MEMORY.md这三层的职责完全不同。Raw 原始层保存完整对话档案。每轮 user、assistant 以及必要的非字符串内容都会追加到history.jsonl。它不进入上下文主要用于审计和回查。以后你想知道某个结论是怎么来的可以翻这份底账。Episodic 情景层保存按天归档的对话摘要。文件名类似2026-04-23.md。它不是每轮都写而是在压缩触发时把一段旧对话整理成“今天发生了什么、做过什么、留下什么线索”。Core 长期层就是MEMORY.md。它会被注入 system prompt让 Agent 每轮都能看到关键事实、长期目标和当前项目背景。因为它常驻上下文所以必须克制不能写成流水账。一个简单判断是必须完整保留但不必每轮看见 → history.jsonl 需要按时间沉淀未来可能检索 → YYYY-MM-DD.md 每轮都必须知道的稳定信息 → MEMORY.mdMemoryStore先把落盘路径做稳记忆系统的第一步不是复杂算法而是稳定读写。MemoryStore.append_history()负责追加原始记录defappend_history(self,role:str,content:Any)-None:row{ts:datetime.now(_UTC8).isoformat(timespecseconds),role:role,content:contentifisinstance(content,str)else_json_safe(content),}withself.history_file.open(a,encodingutf-8)asf:f.write(json.dumps(row,ensure_asciiFalse)\n)这里有两个小细节很实用。第一时间使用 UTC8。个人 Agent 面向人的日常工作“今天做了什么”最好和本地日期一致。第二content不一定是字符串。工具调用结果、模型 content block、对象结构都可能进入记录。_json_safe()会尽量把它们转成可序列化内容避免因为一条特殊消息导致记忆写入失败。情景记忆按日期写deftoday_episode_path(self)-Path:datedatetime.now(_UTC8).strftime(%Y-%m-%d)returnself.memory_dir/f{date}.md长期记忆则整体覆写defwrite_memory(self,content:str)-None:self.memory_file.write_text(content.strip()\n,encodingutf-8)为什么不是 append因为长期记忆不是日记。它应该像工作台上的便签只保留下一轮推理真正需要的稳定信息。一直追加只会把MEMORY.md变成另一个膨胀的 history。什么时候该压缩压缩不能只靠感觉。项目里用TokenTracker记录每次模型调用的 token 用量row{model:model,input:getattr(usage,input_tokens,0)or0,output:getattr(usage,output_tokens,0)or0,cache_read:getattr(usage,cache_read_input_tokens,0)or0,cache_create:getattr(usage,cache_creation_input_tokens,0)or0,}是否触发压缩由最近一次输入 token 决定defshould_compact(self,max_context:int,threshold:float0.7)-bool:returnself._last_input_tokensmax_context*threshold默认策略是如果最大上下文按 200K 算输入超过 70% 左右就触发压缩。这样做的好处是压缩由运行时资源状态触发而不是让模型自己猜“我是不是快满了”。Compactor把旧对话变成可用记忆压缩逻辑在agent/compactor.py。当压缩触发时系统不会把全部 history 一刀切掉而是保留最近K10轮把更早的消息交给 compactoroldhistory[:-self.K]recenthistory[-self.K:]随后compactor 把旧对话、现有MEMORY.md、用户偏好文件和今日情景记忆一起放进提示词让模型输出三段结构化内容episode.../episode updated_memory.../updated_memory updated_user.../updated_user解析后分别写入episode追加到当天YYYY-MM-DD.mdupdated_memory覆写MEMORY.mdupdated_user覆写用户偏好文件最后写入compact_event标记。这样旧对话没有消失而是从“逐字历史”变成“情景摘要 长期事实”。内存里的 history 只保留最近几轮下一轮对话仍然连贯但上下文负担被降下来。为什么不一开始就上向量库一提到 Agent 记忆很多人会马上想到向量数据库。向量检索当然有价值尤其适合在海量文档或历史归档中按语义找线索。但在这个阶段最先要解决的不是“如何检索所有旧信息”而是“当前上下文怎么不爆”。如果所有原始消息都一直堆在工作记忆里旁边接一个向量库也救不了主上下文膨胀。所以这里先做三层文件记忆。它解决的是生命周期原始消息完整保存 旧对话按天沉淀 核心事实常驻上下文等这个边界稳定后再把旧 episode 或项目资料放进向量检索会更自然。向量库应该服务于“按需召回”而不是替代工作记忆、情景记忆和长期记忆的分工。如何验证记忆系统真的在工作不要只靠“模型好像记得”来判断。更可靠的验证方式是看文件和触发点。第一看memory/history.jsonl是否每轮追加。用户输入和助手输出都应该能找到。第二看memory/tokens.jsonl是否记录每次调用的输入输出 token。压缩是否触发依赖这里的最近输入 token。第三调低阈值或模拟长对话确认 compactor 会写入当天YYYY-MM-DD.md并更新MEMORY.md。第四重启程序确认最后一次 compact 之后的未归档历史能被启动归档处理。长期 Agent 不只是当前进程里记得还要跨会话接住上下文。如果这四点都成立记忆系统就不再是 prompt 里一句“请记住”而是一套有文件、有阈值、有归档结果的运行机制。记忆内容应该写多细做记忆系统时最难的不是写文件而是决定什么值得留下。原始层可以完整因为它不进入上下文主要用于审计。工具输出很长也没关系最多占磁盘空间。情景层就要开始筛选它应该记录今天发生的重要事件、做出的关键决策、遇到的阻塞、后续可能要回查的线索而不是复述每一轮对话。长期层更要克制。MEMORY.md应该只放稳定事实例如当前项目目标是什么 用户长期偏好是什么 已经确认的重要架构约束是什么 当前任务的稳定背景是什么不适合放进长期记忆的内容包括一次性命令输出、临时错误日志、已经解决的中间状态、模型当时的详细推理过程。这些内容要么留在 raw history要么压进当天 episode。如果长期记忆写得太细它会变成另一个上下文垃圾桶。每轮都注入就每轮都消耗注意力。长期记忆越短、越稳定价值越高。Compact 不是简单摘要很多人把 compact 理解成“把旧对话总结一下”这还不够。真正有用的 compact 至少要同时完成三件事。第一把旧对话变成情景记录。它应该回答“那段时间发生了什么”方便未来回查。第二更新长期记忆。它要判断哪些信息已经稳定到值得每轮注入哪些旧信息已经过期应该删除。第三更新用户偏好。比如用户喜欢什么输出风格、项目里有哪些长期习惯、哪些信息不应该重复问。这就是为什么 compactor 的输出用标签拆成episode、updated_memory、updated_user。它不是一段随意摘要而是一次记忆整理。失败时应该怎么处理记忆系统也会失败。比如模型输出没有包含预期标签写文件失败压缩调用超时或者生成的长期记忆质量很差。教学项目里先保持简单能提取到哪个标签就写哪个。更严谨的实现可以继续加几层保护压缩前备份 MEMORY.md 提取不到标签时不覆写长期记忆 长期记忆超过字数上限时拒绝写入 压缩失败时保留原 history 记录 compact 错误供人工检查这些不是概念重点但决定了系统能不能长期运行。记忆系统越接近基础设施越不能只看成功路径。什么时候应该触发 compact最直接的触发条件是 token 数。项目里通过tokens.jsonl记录最近调用消耗再判断是否超过阈值。这个办法简单、可观察也容易调试。但真实项目里compact 还可以结合任务边界。比如一个小任务完成后、一次工具探索结束后、用户明确切换主题时都适合整理一次。因为这些时刻天然有边界模型更容易把“刚才发生了什么”压成一个清楚 episode。相反在任务中间频繁 compact 会有风险。模型可能还需要前面某个细节继续推理如果过早摘要就会丢失原始上下文。所以 compact 的节奏不是越勤越好而是要在“上下文快撑不住”和“任务语义刚好收束”之间找平衡。记忆要允许被修正长期记忆听起来像真理库但它应该允许被更新。用户偏好会变项目目标会变前面记录的事实也可能后来被证明不准确。所以MEMORY.md不应该只追加。更合理的方式是每次 compact 都产出一份新的稳定记忆保留仍然成立的删除已经过期的改写描述不准确的。这也是为什么长期记忆最好短一点。越短越容易维护越长越像没人敢碰的旧文档。Agent 需要的不是一本百科而是一份能在每轮推理里真正起作用的高密度背景。记忆和隐私边界只要系统开始长期保存信息就要考虑隐私边界。并不是用户说过的每句话都应该长期保存也不是每个工具输出都适合进入MEMORY.md。一个保守策略是raw history 用于本地审计长期记忆只保存完成任务确实需要的稳定信息。涉及密钥、隐私、临时口令、一次性路径、敏感日志的内容即使出现在对话或工具输出里也不应该被压进常驻记忆。记忆系统的目标不是“什么都记住”而是“记住应该被带到下一次推理里的东西”。小结history 让模型看见前文但 history 不能无限增长。一个能长期工作的 Agent需要把记忆拆开原始记录完整保存情景记忆按天沉淀长期记忆高密度常驻。这一篇的核心不是炫技而是工程卫生。记忆系统首先让上下文成本、历史审计和长期状态各有位置其次才改善“模型好像更懂我”的体验。有了记忆之后Agent 能在更长时间里工作。但当任务步骤变多时另一个问题会出现模型可能跳步、重复、半途自称完成。下一步我们需要把“计划”也从模型脑内拿出来变成可观察、可校验的外部状态。视频与源码如果你想看完整演示可以在主页的《从零手搓 Agent》合集里按顺序观看视频版抖音https://www.douyin.com/user/MS4wLjABAAAAk5lgbm96yoPPEoGXoY3MIp4S8voya0dzcnG0Lom5-SI?from_tab_namemainB站https://space.bilibili.com/1452412374文章里的示例代码和完整项目也放在这里 教学仓库https://github.com/TheSyart/claude-agent-examples⚔️ 实战项目https://github.com/TheSyart/emperor-agent我会持续更新 Agent 教学与实战内容。觉得有用的话欢迎给项目点个 Star ⭐也谢谢你一路看到这里。