大家好,我是张大鹏,10年全栈开发经验。在研究了GenericAgent的架构之后,最让我拍案叫绝的不是它的Agent循环,也不是它的工具设计,而是它的记忆系统。这篇文章我会带你从头到尾拆解这套四层记忆架构------它怎么存储、怎么检索、怎么进化,以及为什么我说这是目前见过设计最清醒的Agent记忆系统。
一、先说一个反常识的设计
大多数Agent框架处理记忆的方式很直接:把聊天历史全部塞进上下文。不够用了就扩窗口,200K不够就上1M。这就像为了找一本书,把整座图书馆背在身上。
GenericAgent的做法完全不同------它的核心记忆代码不到 100行,但设计了一套四层架构,让Agent每次只携带最必要的信息,需要时再按图索骥去取。
我看了这么多Agent项目,这是第一个让我感觉到"这个项目是真的在认真思考记忆问题"的。它不是拍脑袋想出来的分层,而是有一套完整的元规则在约束每一层该放什么、不该放什么。
二、四层记忆架构总览
先看整体结构,我整理了一张表:
| 层级 | 文件/位置 | 容量控制 | 核心作用 |
|---|---|---|---|
| L0 | memory/memory_management_sop.md |
~1页文档 | 记忆系统的"宪法"------定义规则 |
| L1 | memory/global_mem_insight.txt |
≤30行,<1K token | 极简索引,告诉Agent"知识在哪" |
| L2 | memory/global_mem.txt |
小,按需膨胀 | 环境事实库(路径、ID、配置) |
| L3 | memory/*.md + *.py |
多个文件 | 任务级SOP和工具脚本 |
| L4 | memory/L4_raw_sessions/ |
自动归档,ZIP压缩 | 历史会话存档 |
这套架构最精彩的设计是:上层不存细节,只存指针。
也就是所谓的"存在性编码"------L1不会告诉你"怎么操作浏览器",它只写一行:
text
浏览器特殊操作: tmwebdriver_sop(文件上传/图搜/PDF blob/物理坐标/HttpOnly Cookie/autofill突破/跨域iframe/CDP/跨tab)
Agent看到这行就知道:"哦,遇到浏览器特殊操作时,去读 tmwebdriver_sop 这个文件。" 至于具体怎么操作,那是L3的事。
三、L0:元规则------约束比能力更重要
当我读到 memory_management_sop.md 里的核心公理时,我就知道这个项目的作者是真正被Agent"坑"过的。
四条不可违反的规则:
| 公理 | 内容 | 一句话理解 |
|---|---|---|
| 行动验证原则 | 只有成功的工具调用结果才能写入记忆 | 没验证过的不算知识 |
| 神圣不可删改性 | 已验证信息在重构时严禁丢失 | 可以压缩,不能删除 |
| 禁止存储易变状态 | 不存时间戳、Session ID、PID等 | 存了就过期的东西不要存 |
| 最小充分指针 | 上层只留定位下层的最短标识 | 多一个词都是浪费 |
其中第一条 最狠------"No Execution, No Memory"。这意味着Agent不能在脑子里YY一些"我觉得可能是这样的"信息写入记忆。必须是 code_run 执行成功、file_read 确认内容存在之后,才能记下来。
这直接解决了Agent记忆系统最大的痛点:幻觉污染。没有这条规则,Agent的自学能力越强,记忆库里的垃圾就越多。
四、L1:存在性编码的实践
L1 文件 global_mem_insight.txt 是整个记忆系统的入口,也是唯一一个每次对话都会加载的文件。它长这样(这是项目当前的实际情况):
text
# [Global Memory Insight]
浏览器特殊操作: tmwebdriver_sop(文件上传/图搜/PDF blob/物理坐标/...)
键鼠: ljqCtrl_sop(禁pyautogui/先activate)
截图/视觉: ocr/vision_sop | 禁全屏截图,优先窗口
定时:scheduled_task_sop | 自主:autonomous_operation_sop
手机:adb_ui.py
需要时read L2 或 ls ../memory/ 查L3
L0(META-SOP): memory_management_sop
L2: 公众号(微信ID/目录路径)
L3: memory_cleanup_sop | skill_search | plan_sop | ...
L4: L4_raw_sessions/
[RULES]
1. 搜索先行: 搜文件名严禁不用es...
2. 交叉验证: 禁信摘要...
...
注意几个设计细节:
第一层:高频场景直接映射 。像"浏览器特殊操作 → tmwebdriver_sop"这种,括号里放的是场景触发词,不是操作方法。Agent读到的是一把钥匙,不是说明书。
第二层:低频场景只列关键词。对于不常用的能力,L1只写文件名,Agent需要时再去读。
RULES区:存放的是"红线规则"------那些违反不会报错但会导致结果错误的事情。比如"搜索用google不用百度"、"交叉验证不要信摘要"。这些是项目实践中踩过的坑凝结成的教训。
L1 的更新有一条硬性红线------只能 patch 修改,严禁 overwrite。因为让 LLM 重写整个文件,大概率会丢掉一些信息。每一次更新必须是精准的局部替换。
五、L2 + L3:事实库与技能库
L2:你不该让LLM猜的东西
L2 global_mem.txt 存放的是环境特异性事实------那些大模型不可能靠参数准确生成的信息:
text
## [公众号相关]
# 公众号微信ID: yggaibc
# 公众号文章目录: ../articles/公众号/
这类信息的特点是:零次学习失败率100%。你让LLM猜你的公众号ID,它不可能猜对。所以必须存下来。
L2禁止存储的内容也很有意思:通用常识。比如"Python列表怎么遍历"------这种信息大模型已经会了,存了就是浪费token。
L3:任务的"肌肉记忆"
L3是 memory/ 目录下的SOP文件(*_sop.md)和工具脚本(*.py)。每个文件对应一类任务的操作规范。
一个SOP文件的典型内容结构:
- 关键前置条件:开始前必须确认的事情
- 典型易踩坑点:上次在这个任务上反复失败的原因
- 操作要点:极简步骤,不是教程
设计原则很克制------只记录跨会话仍重要、且难以快速重建的要点。那些上网搜一下就能找到的东西,不记。
六、L4:历史的归档与遗忘
L4 memory/L4_raw_sessions/ 是历史会话层,也是唯一一个自动化运行的层级。
它的工作机制在 reflect/scheduler.py 中定义------每12小时自动触发一次归档流程:
python
# reflect/scheduler.py:63-74
def check():
global _l4_t
if _time.time() - _l4_t > 43200: # 12小时
_l4_t = _time.time()
from compress_session import batch_process
raw_dir = '../temp/model_responses'
r = batch_process(raw_dir, dry_run=False)
归档流程分四步,我在 compress_session.py 里梳理出了完整逻辑:
原始会话文件 → 压缩(剥离系统提示和冗余回声)
→ 提取 [USER]/[Agent] 历史摘要
→ 去重合并(滑动窗口匹配)
→ 追加到 all_histories.txt + 按月 ZIP 压缩
→ 删除原始文件
核心的压缩逻辑,对于"原始格式"的会话文件,会剥离系统提示和助手的冗余回声:
python
def _compress_raw(text):
sections = _parse_sections(text)
out = []
for i, (typ, line, body) in enumerate(sections):
if typ == 'prompt':
out.append(line + '\n')
if not (i+1 < len(sections) and sections[i+1][0] == 'user'):
out.append(body)
elif typ in ('user', 'response'):
out.append(line + '\n')
out.append(body)
# assistant → skip (redundant echo)
return ''.join(out)
这段代码我特别喜欢------就20行,干净利落地完成了"去掉系统提示、去掉助手回声、保留用户和助手交互"的压缩逻辑。不依赖任何第三方库。
L4目前有一个状态:只写不读。会话被压缩归档了,但Agent在后续任务中不会主动去检索这些历史。这是当前设计中最明显的"缺环"------archive 的目的应该是 future retrieval,但目前只完成了archive这一半。
七、运行时记忆:working + history 双通道
除了文件系统的持久化记忆,运行时还有两个"活"的记忆结构,定义在 ga.py 中。
工作记忆 (Working Memory)
python
# ga.py:263-268
class GenericAgentHandler(BaseHandler):
def __init__(self, parent, last_history=None, cwd='./temp'):
self.working = {}
self.history_info = last_history if last_history else []
self.working 是一个字典,最关键的是 key_info 字段------它通过 update_working_checkpoint 工具写入,每轮自动注入下一轮提示词:
python
# ga.py:432-442
def do_update_working_checkpoint(self, args, response):
key_info = args.get("key_info", "")
related_sop = args.get("related_sop", "")
if "key_info" in args: self.working['key_info'] = key_info
if "related_sop" in args: self.working['related_sop'] = related_sop
self.working['passed_sessions'] = 0
yield f"[Info] Updated key_info and related_sop.\n"
next_prompt = self._get_anchor_prompt(skip=args.get('_index', 0) > 0)
return StepOutcome({"result": "working key_info updated"}, next_prompt=next_prompt)
这相当于Agent的"便签纸"------任务进行到一半时,把关键的避坑点、当前进度、下一步计划记下来,防止在长任务中迷失方向。
<summary> 协议
这是我最喜欢的设计之一。每次LLM回复时,必须输出一个 <summary> 标签,内容是单行物理快照:
python
# llmcore.py:933-935
THINKING_PROMPT_ZH = """
每次回复(含工具调用轮)都先在回复文字中包含一个<summary></summary>
中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。
"""
# ga.py:523-534
def turn_end_callback(self, response, tool_calls, tool_results, turn, next_prompt, exit_reason):
rsumm = re.search(r"<summary>(.*?)</summary>", _c, re.DOTALL)
if rsumm: summary = rsumm.group(1).strip()
else:
# 没有summary时自动生成
summary = f"调用工具{tool_name}, args: {clean_args}"
self.history_info.append(f'[Agent] {summary}')
然后通过 _get_anchor_prompt() 将最近的40条摘要回注到下一轮:
python
# ga.py:511-521
def _get_anchor_prompt(self, skip=False):
h_str = "\n".join(self.history_info[-40:])
prompt = f"\n### [WORKING MEMORY]\n<history>\n{h_str}\n</history>"
if self.working.get('key_info'):
prompt += f"\n<key_info>{self.working.get('key_info')}</key_info>"
return prompt
这套"强制摘要 + 回注"机制的精妙之处在于:让LLM自己负责压缩自己的历史。不是用算法去截取原始对话,而是让LLM每轮输出一个它认为最重要的快照。压缩质量远高于简单的滑动窗口。
八、完整数据流:从启动到持久化
我把整个记忆系统的运行流程串起来,画成了一条完整的链路:
agentmain.py 启动
│
├── 初始化 L1/L2 文件(不存在则从模板创建)
│
├── get_global_memory() → 读取 L1 global_mem_insight.txt
│ + 固定结构模板
│ → 注入系统提示词
│
└── agent_runner_loop 开始循环
│
├── 每轮开始: _get_anchor_prompt()
│ ├── history_info[-40:] → <history> 块
│ ├── working['key_info'] → <key_info> 块
│ └── working['related_sop'] → SOP 提示
│
├── Agent 回复(包含 <summary>)
│
├── turn_end_callback()
│ ├── 提取 <summary> → 追加到 history_info
│ ├── 每10轮: 重新注入 L1 全局记忆(防遗忘)
│ └── 检查是否注入 _keyinfo / _intervene
│
├── 工具调用分支
│ ├── update_working_checkpoint → 更新 key_info
│ └── start_long_term_update → Agent 自行修改 L1-L3
│
└── 后台(scheduler.py 每12小时)
└── compress_session.batch_process()
→ 压缩 → 提取 → 去重 → 归档 L4
这个流程里有一个特别关键的"防遗忘"设计:每10轮自动重新注入全局记忆 (ga.py:539)。因为在长对话中,Agent很容易"忘记"自己在L1里有什么知识可用。定期重新注入相当于敲敲它的脑袋:"别忘了你还有这些工具。"
九、总结与评价
经过这一轮深入源码的拆解,我用一个表格总结GenericAgent记忆系统的设计:
| 维度 | 评价 |
|---|---|
| 设计哲学 | ★★★★★ --- "存在性编码"比"全量记忆"聪明得多 |
| 架构清晰度 | ★★★★★ --- L0-L4分层清晰,职责分明,不重叠 |
| token效率 | ★★★★★ --- 默认只加载L1(<1K token),按需取用深层 |
| 防污染机制 | ★★★★☆ --- "无行动不记忆"原则能有效阻挡幻觉 |
| 闭环完整性 | ★★★☆☆ --- L4归档后缺乏检索利用,只写了一半 |
| 代码精简度 | ★★★★★ --- 记忆核心代码不到100行,没有冗余 |
这套系统最让我佩服的不是技术有多复杂,而是设计上的克制。每一行代码都在问同一个问题:"这条信息真的值得存吗?"、"这个细节真的需要在L1里吗?"
在Agent开发中,"能存多少"不是问题,"该存什么"才是真正考验设计师功力的地方。GenericAgent用四层架构和一套元规则,给出了目前我看到的最诚实的答案。
参考资料:
作者 :张大鹏
团队 :大鹏 AI 教育
日期:2026-05-01我是张大鹏,10年全栈开发经验,目前专注于 AI + 全栈教育培训。关注我,每周分享AI和全栈开发领域的深度实战经验。