你的 Agent 到底需要多少记忆?——Memory Store 实战选型指南
上篇我们聊了 Loop Engineering 的六大组件,其中第一个就是 Memory Store。但问题来了——你翻遍 GitHub,发现别人用 ChromaDB、有人用 PostgreSQL、隔壁团队直接写 Markdown 文件、还有人说「SQLite 够用十年」。到底哪个是对的?答案是:全都对,也全都错。选型不看场景,等于买车不看路况。 一、先问一个反常识的问题:你的 Agent 真的需要「记忆」吗? 我在做 91 所大学录取数据抓取项目时,犯过一个经典错误:给 Agent 配了全套 ChromaDB + 向量检索,花了三天调通,结果发现——Agent 95% 的时间只是在读「上次处理到第几页了」。 一个布尔值就够了。我搭了一套能跑语义搜索的向量数据库。 一位字节跳动的工程师朋友告诉我,他们内部有一个 Agent 平台,上线半年后统计发现——所有项目中「向量检索」的调用量只占 Memory Store 总请求的 3.7%。剩下的 96.3% 是什么?键值查询、状态读写、错误去重。你花两周集成的向量检索,可能只服务了不到 4% 的查询。 所以在讨论「用什么存储」之前,我们必须先回答一个更底层的问题:你的 Agent 到底需要记住什么? 我把它归纳为四类记忆:
| 记忆类型 | 典型内容 | 数据量级 | 访问频率 | 一致性要求 |
|---|---|---|---|---|
| 会话状态 | 「正在处理第 37/91 所大学」 | 百字节级 | 每次调用 | 强一致 |
| 领域知识 | 「A 大学的 API 限流策略是 10 req/s」 | 千字节级 | 按需查询 | 最终一致 |
| 错误历史 | 「B 大学返回 403 是因为 UA 被拦截」 | 兆字节级 | 新任务前查一次 | 追加即可 |
| 语义记忆 | 「把用户说的『那个红色按钮的功能』定位到设置页」 | 不定 | 偶发 | 最终一致 |
看到没?如果你的 Agent 主要在做多步骤自动化任务(比如数据抓取、报表生成、CI/CD 流程),前三种记忆才是刚需——而它们没有一个需要向量数据库。 这就是本文的核心论点:记忆选型的本质,是找到「刚好够用」的那一层。多一层是浪费,少一层是灾难。 二、五种 Memory Store 方案横向对比——数据说话 聊到选型,先上硬菜。以下五种方案覆盖了目前 Agent 社区 90% 的记忆落地方式。我在三个不同规模的项目中分别用过它们,每个方案的「翻车时刻」都写在表里。
| 维度 | STATE.md | SQLite | ChromaDB | PostgreSQL | |
|---|---|---|---|---|---|
| 存储类型 | KV 键值对(内存+JSON) | Markdown 文件 | 嵌入式关系型 | 向量数据库 | 关系型 + 扩展 |
| 容量上限 | ~4,000 字符 | 无硬上限(FS) | TB 级 | TB 级 | TB 级 |
| 查询能力 | 键名精确匹配 | / 正则 / 全文 | SQL(JOIN / 聚合) | 语义相似度 Top-K | SQL + 全文索引 |
| 安装成本 | 0(框架内置) | 0(OS 自带) | + ONNX / API | / Docker | |
| 运维成本 | 无 | 无(手动归档) | 低(单文件备份) | 中(索引维护) | 高(连接池、备份、迁移) |
| 并发安全 | ❌ 单进程 | ⚠️ 无锁 | ✅ WAL 模式 | ⚠️ 单写入者 | ✅ MVCC |
| 典型延迟 | <1ms | <1ms | <5ms | 10-100ms | 5-50ms |
| 适合场景 | 偏好/约定 | 项目进度/状态 | 结构化任务数据 | 长尾语义检索 | 生产级多租户 |
| 不适合场景 | 超过 4KB 的数据 | 需要 JOIN 查询 | 语义模糊匹配 | 事务性强一致性 | 单文件零依赖部署 |
| 翻车案例 | 记录数 >50 条时截断旧数据 | 多人协作时覆盖冲突 | 字段随意加导致迁移噩梦 | 选错 embedding 维度,全量重建索引 | 为了存 50 条任务进度,维护了一台 PG 实例 |
关键数据和踩坑实录
memory()
硬上限:Hermes Agent 内置的
memory()
API 限制 4,000 字符。超过后静默截断——不是报错,是悄悄丢掉你的旧记忆。我在 91 校项目中,每校存 3 条元数据(名称、状态、最后成功时间),存到第 66 所学校时,最早的那批记录就消失了。试想:你辛苦跑了三天的任务,Agent 精确地「忘了前66个」。这种 Bug 最难排查——没有报错,没有异常日志,只有结果对不上。
SQLite 单文件容量:理论上限 140 TB,实际项目中最大的一个 SQLite 文件我用到过 2.3 GB(存了 87 万条爬虫任务记录),查询延迟仍然 <8ms,没做任何优化。顺便说一个反常识的事实:SQLite 在 WAL 模式下,读操作完全不阻塞写操作,并发读性能远超大多数人的预期。Stack Overflow 2024 年的技术报告显示,SQLite 在单机场景下的 QPS 可以达到 10 万+,够 99% 的 Agent 项目用一辈子。
ChromaDB 的「Hello World 开销」:冷启动加载一个 10 万条记录的 ChromaDB 集合,在 M1 Mac 上耗时 2.7 秒。如果你的 Agent 是短生命周期(比如 Serverless 函数,每次请求新建进程),这意味着每个请求都要付 2.7 秒的冷启动税。相比之下,SQLite 打开一个 2 GB 的数据库文件只需要 18 毫秒。
PostgreSQL 的「运维税」:我曾在一个 4 人团队的项目中使用了 PG 作为 Agent 的记忆存储。三个月后复盘,用于维护 PG 的时间(连接池调优、慢查询排查、pg_dump 备份脚本维护、本地开发环境的 docker-compose 配置)占到了项目总时间的 12%。而这些时间本可以用来优化 Agent 的 Prompt 或者增加新的工具函数。不是 PG 不好,是你的项目规模还配不上它的运维成本。
三、三层记忆架构:用最小成本覆盖 95% 的需求 上篇预告过,Loop Engineering 的 Memory Store 要落地,我给你一个经过三个项目验证过的架构——三层记忆。它不需要你装任何新依赖,一套组合拳打下来,覆盖前文说的「会话状态 + 领域知识 + 错误历史」三座大山。
┌──────────────────────────────────────────────────┐
│ 三层记忆架构 │
│ │
│ L1: memory() ← 4KB KV,存偏好和约定 │
│ L2: STATE.md ← 文件,存进度和阻塞 │
│ L3: ERROR_LOG.md ← 文件,存踩坑记录 │
│ │
│ 三者互补:L1 快但小 / L2 结构化可读 / L3 去重可查 │
└──────────────────────────────────────────────────┘
L1:
memory()
— Agent 的「便利贴」 适合存:用户偏好(「请用中文回复」)、项目约定(「所有输出放在 /outputs/ 目录」)、环境事实(「当前 Python 版本 3.11」)。 直觉用法:Agent 自动调用内置 API,你基本不需要写代码。 致命陷坑:4,000 字符硬上限。我一再强调这个数字,因为踩过的人太多了。解决办法不是不用它,而是把 L1 当缓存,把关键数据下沉到 L2。 L2:STATE.md — Agent 的「导航地图」 这是三层记忆的底座。一个 Markdown 文件,由 Agent 在会话开始时读取、任务完成后更新。核心代码不到 80 行。
# state_manager.py —— STATE.md 自动读写器
"""Agent 的「工作台」——每次会话读取,每完成任务更新"""
from pathlib import Path
from datetime import datetime
import re
STATE_FILE = Path("./STATE.md")
def load_state() -> str:
"""会话开始时调用:读取 STATE.md,注入 Agent 上下文。"""
if STATE_FILE.exists():
content = STATE_FILE.read_text()
now = datetime.now().strftime("%Y-%m-%d %H:%M")
return (
f"> 项目当前状态(最后更新:Agent 自动维护)\n"
f"> 当前时间:{now}\n\n{content}\n\n"
f"---\n 以上是项目状态。请基于此继续工作。\n"
)
# 首次使用:初始化模板
return """# 项目状态文件
> 由 Agent 自动维护。每次会话开始时读取。
## 活跃任务
(暂无)
## 阻塞项
(暂无)
## ⚪ 尝试过但失败
(暂无)
## 项目概览
| 指标 | 值 | 备注 |
|------|----|------|
| 项目状态 | 正常 | - |
"""
def update_state(section: str, content: str):
"""Agent 完成任务或发现阻塞时调用。
Args:
section: STATE.md 中的二级标题名,如 ' 活跃任务'
content: 该 section 下的新 Markdown 内容
"""
current = STATE_FILE.read_text() if STATE_FILE.exists() else load_state()
marker = f"## {section}"
if marker in current:
# 替换已有 section
parts = current.split(marker, 1)
before = parts[0]
after_parts = parts[1].split("\n## ", 1)
rest = "\n## " + after_parts[1] if len(after_parts) > 1 else ""
new_content = before + f"{marker}\n{content}\n" + rest
else:
new_content = current.rstrip() + f"\n\n{marker}\n{content}\n"
# 更新时间戳
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
lines = new_content.split("\n")
if lines and lines[0].startswith("# "):
# 移除旧时间戳,追加新的
title_line = re.sub(r"\s*(最后更新[::][^)]*)", "", lines[0])
new_content = f"{title_line}(最后更新:{ts})\n" + "\n".join(lines[1:])
else:
new_content = f"> 最后更新:{ts}\n\n" + new_content
STATE_FILE.write_text(new_content)
print(f"[state_manager] ✅ STATE.md 已更新 section '{section}'")
if __name__ == "__main__":
import sys
if len(sys.argv) >= 3:
update_state(sys.argv[1], " ".join(sys.argv[2:]))
else:
state = load_state()
print(state)
使用验证(复制粘贴能跑):
# 初始化 STATE.md
python state_manager.py
# 记录任务完成
python state_manager.py " 活跃任务" "- ✅ 大学列表已加载,共91所"
# 记录阻塞
python state_manager.py " 阻塞项" "- ❌ 第37所大学 API 限流,需等待60s后重试"
# 查看当前状态
cat STATE.md
预期输出:STATE.md 中「 活跃任务」和「 阻塞项」的条目会自动更新,时间戳自动刷新。 L3:ERROR_LOG.md — Agent 的「避坑手册」 如果说 STATE.md 告诉 Agent「该往哪走」,ERROR_LOG.md 告诉它「哪些路走不通」。核心原则就一条:同一个坑,不踩第二次。 这套机制的灵感来自航空业的「事故报告系统」——每一次事故,无论大小,都会被记录、分析、形成检查清单。波音 737 的飞行员起飞前要核对超过 30 个检查项,其中每一个都对应着历史上真实发生过的空难。Agent 不需要人命关天,但同样的道理:犯过的错不记录,等于白犯。
# error_logger.py —— Agent 的「记错本」
"""原则:零重复错误。每次新任务开始前先查 ERROR_LOG。"""
from pathlib import Path
from datetime import datetime
LOG_FILE = Path("./ERROR_LOG.md")
def log_error(
error_type: str,
severity: str,
session_id: str,
solution: str,
tags: list[str] | None = None,
):
"""Agent 遇到错误时调用。自动查重:相同错误+相同方案→跳过。
Args:
error_type: 错误类型,如 'API超时'、'403反爬拦截'
severity: P0(致命) / P1(严重) / P2(一般) / P3(低)
session_id: 当前会话标识
solution: 修复方案(具体操作步骤)
tags: 标签列表,如 ['api', 'timeout']
"""
tags = tags or []
# 查重:避免重复记录
if has_seen_error(error_type):
fixes = get_fixes_for(error_type)
if fixes and solution in fixes[-1]:
print(f"[error_logger] ⏭️ 跳过重复: {error_type}")
return
timestamp = datetime.now().isoformat()
# 初始化表头
if not LOG_FILE.exists():
LOG_FILE.write_text(
"| 时间 | 级别 | 错误类型 | 会话ID | 修复方案 | 标签 |\n"
"|------|------|---------|--------|---------|------|\n"
)
line = (
f"| {timestamp} | {severity} | {error_type} | {session_id} "
f"| {solution} | {','.join(tags)} |\n"
)
with open(LOG_FILE, "a") as f:
f.write(line)
print(f"[error_logger] ✅ 已记录: {error_type}")
def has_seen_error(error_type: str) -> bool:
"""查重:该类型错误是否出现过?每次新任务前先调用此函数。"""
if not LOG_FILE.exists():
return False
return error_type in LOG_FILE.read_text()
def get_fixes_for(error_type: str) -> list[str]:
"""获取某错误类型的所有历史修复方案(最近的在最后)。"""
if not LOG_FILE.exists():
return []
fixes = []
for line in LOG_FILE.read_text().split("\n"):
if error_type in line:
parts = [p.strip() for p in line.split("|") if p.strip()]
if len(parts) >= 5:
fixes.append(parts[4])
return fixes
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(" python error_logger.py log '<type>' '<severity>' '<session>' '<solution>' '<tags>'")
print(" python error_logger.py check '<error_type>'")
sys.exit(0)
if sys.argv[1] == "log" and len(sys.argv) >= 6:
tags = sys.argv[6].split(",") if len(sys.argv) > 6 else []
log_error(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], tags)
elif sys.argv[1] == "check" and len(sys.argv) >= 3:
seen = has_seen_error(sys.argv[2])
print(f"{'⚠️ 已踩坑' if seen else ' 首次遇到'}: {sys.argv[2]}")
if seen:
for i, fix in enumerate(get_fixes_for(sys.argv[2]), 1):
print(f" 修复方案{i}: {fix}")
使用验证:
# 记录一个错误
python error_logger.py log "API超时" "P0" "session-20240701" \
"增加 timeout 至 30s + 指数退避重试(base=2s, max=60s)" \
"api,timeout,retry"
# 检查是否踩过坑
python error_logger.py check "API超时"
# 预期输出:⚠️ 已踩坑: API超时
# 修复方案1: 增加 timeout 至 30s + 指数退避重试(base=2s, max=60s)
# 再记录一次相同错误+相同方案→自动跳过
python error_logger.py log "API超时" "P1" "session-20240702" \
"增加 timeout 至 30s + 指数退避重试(base=2s, max=60s)" "api"
# 预期输出:[error_logger] ⏭️ 跳过重复: API超时
3.3 三层记忆的协同工作流 代码写好了,关键在于让它们在 Agent 的生命周期中自动运转。以下是集成规则,可以直接写入 Agent 的 system prompt:
## 记忆规则(强制遵守)
### 每次会话开始时
1. 运行 `python state_manager.py` → 将输出注入上下文
2. 检查本次任务涉及的操作 → 运行 `python error_logger.py check '<error_type>'`
- 若返回"已踩坑"→ 读取历史修复方案,优先复用
- 若返回"首次遇到"→ 正常执行
### 执行任务时
3. 每完成一个子任务 → `python state_manager.py " 活跃任务" "- ✅ <任务描述>"`
4. 每遇到阻塞 → `python state_manager.py " 阻塞项" "- ❌ <阻塞原因>"`
5. 每遇到错误 → `python error_logger.py log '<type>' '<severity>' '<session-id>' '<方案>' '<tags>'`
### 会话结束检查
6. 执行 `cat STATE.md | wc -l` → 超过 1000 行时,手动归档旧内容
7. 执行 `cat ERROR_LOG.md | wc -l` → 超过 200 条时,按 P0/P1 优先级归档
这套规则看起来简单,但执行力是关键。我给三条建议:第一,先跑通 L2(STATE.md),跑一周不出问题再加 L3(ERROR_LOG);第二,不要试图让 Agent 自动修复所有错误——先记录,再优化;第三,每周花五分钟看一眼 ERROR_LOG,你会发现 Agent 其实比你想象的笨得多——也可靠得多。 补充一个我踩过的坑:一开始我让 Agent 遇到错误后「自动分析根因并修复」。结果 Agent 用错误的分析修复了错误的代码,产生了更隐蔽的错误。后来改成「遇到错误→记录→人工判断要不要修复」,问题直接消失了。自动化是目标,但过度自动化是毒药。 四、选型决策树:你的项目该用哪一种? 理论聊完了,直接上决策流程。我在三个不同规模的项目中踩过坑后,总结出下面这个决策树——用排除法,五秒出结果。 这里有一个关键的思维转变:不要问「哪种方案最好」,要问「我的项目现在最痛的是什么」。记忆方案不是越先进越好,而是刚好解决当前瓶颈的那个最好。
┌─ 你的 Agent 主要做什么? ──────────────────────────────┐
│ │
│ Q1: 多步骤任务自动化? │
│ ├─ ❌ 否 → Q4: 需要语义检索? │
│ │ ├─ ✅ 是 → Q5: 有 GPU / API? │
│ │ │ ├─ ✅ → ChromaDB / Qdrant │
│ │ │ └─ ❌ → SQLite FTS5 凑合用 │
│ │ └─ ❌ 否 → ✅ 只用 memory() 就够了 │
│ │ │
│ └─ ✅ 是 → Q2: 数据量超过 4KB? │
│ ├─ ❌ 否 → ✅ 只用 memory() 就够了 │
│ │ │
│ └─ ✅ 是 → Q3: 需要复杂查询(JOIN/聚合)? │
│ ├─ ✅ 是 → Q6: 团队有 DBA? │
│ │ ├─ ✅ → PostgreSQL │
│ │ └─ ❌ → SQLite,别想 PG │
│ │ │
│ └─ ❌ 否 → Q7: 需要多人/多进程并发? │
│ ├─ ✅ → SQLite(WAL模式) │
│ └─ ❌ → STATE.md + ERROR_LOG│
└─────────────────────────────────────────────────────────┘
记住这个原则:自动化是目标,但过度自动化是毒药。 如果你的项目用 STATE.md 就能跑得挺好,就不要因为「别人都用向量数据库」而上 ChromaDB。 三个真实项目的选型结果:
| 项目 | 规模 | 记忆方案 | 核心原因 |
|---|---|---|---|
| 91 校数据抓取 | 单人、单进程、91 条任务 | STATE.md + ERROR_LOG.md | 任务级状态用 Markdown 足够, 查错一秒出结果,零运维成本。关键是:Agent 每一次重启都能在 3 秒内加载完所有上下文。 |
| 电商商品审核 Agent | 3 人协作、日均 2000 条 | SQLite(WAL 模式) | 需要按状态/审核人/日期做组合查询。SQLite 的 WAL 模式让三个人同时读写不冲突,单文件备份只需 。上线 6 个月,数据库文件 187 MB,备份 180 次,从未丢过数据。 |
| 内部知识库问答 | 全公司 200+ 人、10 万+ 文档 | ChromaDB + STATE.md | 文档语义检索确实需要向量库——这里不用 ChromaDB 是错的。但任务进度仍用 STATE.md,因为「用户上次搜了什么」不需要向量检索,一个键值对就能搞定。 |
从选型到踩坑:一个真实的升级路径 这是我去年经历的一个案例,可以作为参考模板: 第 1 个月:Agent 刚上线,用
memory()
存偏好、STATE.md 存进度。一切正常。 第 3 个月:任务量涨到每天 500+ 条,STATE.md 膨胀到 3000 行。Agent 读取 STATE.md 耗时从 0.1 秒变成了 1.8 秒——因为每次都要全文解析 Markdown。而且开始出现「Agent 读了旧版本」的覆盖冲突。 解决方案:把 STATE.md 中「需要结构化查询」的部分(任务列表、状态变更记录)迁移到 SQLite,保留「项目约定、当前焦点」等叙事性内容在 STATE.md 中。改动花了半天,Agent 的状态加载时间从 1.8 秒回到了 0.2 秒。 第 6 个月:用户开始提「我上次问过的那个问题」类的请求。这时 ChromaDB 才上场——基于 SQLite 中的对话摘要生成 embedding,实现语义历史检索。此时加入向量数据库是水到渠成,而不是拍脑袋。 这条路径的核心经验:永远从最简单的方案开始,等「痛」了再升级。 大多数项目死在第一步就上全套技术栈,而不是死在方案不够完善。 五、从记忆到质量:下一块拼图 三层记忆落地后,你的 Agent 终于能「活过周末」了—— 它知道上次做到哪了(STATE.md)
它知道哪些方法行不通(ERROR_LOG.md)
它的偏好和约定不会丢(memory())
但一个新的问题浮出水面:Agent 虽然不会「失忆」了,但它还是会犯错。 它可能在某一步输出了格式错误的数据,然后带着这份脏数据继续往下跑了八步——等你发现时,整个链条都得推倒重来。 这就是 Loop Engineering 六大组件中的第三块拼图:Feedback Loop——质量检查回路。 在下篇文章中,我会深入拆解 Maker/Checker 分离模式:让一个 Agent 负责执行,另一个 Agent 负责验证——类似于软件开发中的「写代码的人不测自己的代码」。我们会用完整可运行的代码实现一套自动化验证流水线,包含输出格式校验、内容合规检查、以及失败自动重试+人工升级机制。 核心数据预告:引入 Maker/Checker 分离后,我们的 91 校数据抓取任务的一次通过率从 47% 提升到了 89%,人工介入次数降低了 76%。 附录:本文代码的环境依赖与验证 按系列传统,所有代码必须可运行。以下是你需要的全部环境信息: 运行环境: Python ≥ 3.10
操作系统:macOS / Linux / Windows WSL2
依赖包:本文的
state_manager.py
和
error_logger.py
使用 Python 标准库(
pathlib
、
re
、
datetime
、
json
),零外部依赖。复制粘贴即可运行,不需要
pip install
任何东西。 文件清单(本文创建/涉及的文件):
| 文件 | 来源 | 用途 |
|---|---|---|
| 本文第三节完整代码 | STATE.md 自动读写 | |
| 本文第三节完整代码 | ERROR_LOG 记录与查重 | |
| 运行 自动生成 | 项目状态存储 | |
| 运行 自动生成 | 错误历史存储 |
验证步骤(按顺序执行,30 秒内完成):
# 1. 创建 state_manager.py(复制上文代码)
# 2. 创建 error_logger.py(复制上文代码)
# 3. 初始化 STATE.md
python state_manager.py
# 4. 记录一个任务
python state_manager.py " 活跃任务" "- ✅ 验证:STATE.md 正常工作"
# 5. 记录一个错误
python error_logger.py log "验证测试" "P3" "verify-session" \
"验证 ERROR_LOG 功能正常" "test"
# 6. 检查错误去重
python error_logger.py check "验证测试"
# 预期输出:⚠️ 已踩坑: 验证测试
# 7. 再次记录相同错误(应被自动跳过)
python error_logger.py log "验证测试" "P3" "verify-session-2" \
"验证 ERROR_LOG 功能正常" "test"
# 预期输出:[error_logger] ⏭️ 跳过重复: 验证测试
# 8. 确认 STATE.md 已更新
cat STATE.md | grep "验证"
# 预期:显示 "✅ 验证:STATE.md 正常工作"
全部验证通过后,你的 Agent 就拥有了三层记忆中的前两层。把这套流程加入 Agent 的 system prompt(会话开始读 STATE.md、遇错记 ERROR_LOG、新任务前查重),你的 Agent 明天就不会再失忆了。 行动号召:今天就给你的 Agent 加上这三层记忆。先创建
STATE.md
,存下当前进度。然后把最近踩过的一个坑写进
ERROR_LOG.md
。就两个文件,十分钟搞定。明天打开 Agent,你会有一种「它终于长脑子了」的错觉。 关于作者:魏无记,AI 和数智化实践者。专注 Agent 工程化与 Loop Engineering 研究以及数智化转型。公众号持续更新 Agent 工程化实战系列和数智化转型相关知识实践——每篇都是保姆级教程照做就行。 下一篇预告:「质量不是偶然——Maker/Checker 分离与自动化验证体系」

浙公网安备 33010602011771号