给自己的 Agent 加记忆与技能积累——最小可用落地方案
- 理解文件式记忆(MEMORY.md + USER.md)的最小可用结构,知道各自存什么
- 掌握跨会话加载记忆并注入上下文的方式,让 Agent 每次启动都"认识你"
- 让 Agent 能把本次任务中学到的新方法沉淀成 Skill 文件,形成技能自积累
- 能在自己的 Agent 主循环里接上读写记忆的代码,并知道怎么避开三个主要坑
你已经把前几节的记忆原理过了一遍——向量检索、FTS5 摘要、用户建模,每种思路听起来都有道理。但说到底,理论读完之后,坐在键盘前还是一脸懵:我自己的 Agent,该从哪里下手?
这节不再讲原理,只讲怎么动手。目标很具体:给你正在写(或者准备写)的 Agent,接上三样东西——
- 文件式记忆:用两个 Markdown 文件存事实和用户偏好
- 跨会话加载:每次启动把记忆读进来,塞进上下文
- 技能自积累:Agent 把本次学到的新方法写成 Skill 文件,下次直接用
这三样组合起来,就是 Hermes Agent 里那套思路的简化版。完整的 Hermes 实现以 官方仓库 为准,本文给你一个可以直接搬进自己项目的骨架。
要加的三样东西
第一样:文件式记忆
两个文件搞定大部分场景:
MEMORY.md——存"关于这个任务/这个项目的事实"
# 记忆
## 项目事实
- 仓库路径:/home/user/myproject
- 主要技术栈:Python 3.11 + FastAPI
- 测试命令:pytest tests/ -v
## 已完成的事
- 2026-06-25:完成了用户认证模块,JWT 方案,token 有效期 24h
- 2026-06-25:发现 /api/upload 接口有并发锁问题,已记录但未修
## 待跟进
- 上传接口的并发锁需要用队列重写
USER.md——存"关于你这个人的偏好"
# 用户偏好
- 回复风格:简洁直接,不要废话
- 代码风格:Python,类型注解,不用 type: ignore
- 不喜欢:Agent 自作主张改文件名、删代码
- 工作节奏:早上专注写代码,下午处理沟通
为什么分两个文件?因为更新频率不同。MEMORY.md 每次任务后都可能追加新事实;USER.md 很少变,基本只在用户说"以后不要这样做"时才改。分开存,更新时不会互相污染,加载时也可以分别控制要不要读进来。
关于文件式记忆的更多背景,可以回看 用文件给 Agent 加记忆:Hermes 的思路 这节。
第二样:跨会话加载记忆
记忆文件有了,关键是每次启动 Agent 时要把它读进去,而且要放对位置。
最直接的办法:放进 system prompt。
def load_memory(memory_path="MEMORY.md", user_path="USER.md") -> str:
"""读取记忆文件,拼成 system prompt 的一部分"""
sections = []
for path, label in [(user_path, "用户偏好"), (memory_path, "项目记忆")]:
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read().strip()
if content:
sections.append(f"## {label}\n{content}")
except FileNotFoundError:
pass # 文件不存在就跳过,不报错
return "\n\n".join(sections)
加载进来的内容会放在 system prompt 的靠前位置,让模型在整个会话里都能"看到"它。这和 跨会话汇总与用户建模 里讲的思路一脉相承——把需要持续生效的上下文放 system,把会话内容放 messages。
第三样:技能自积累
Agent 自写 Skills:Hermes 的自学习机制 里讲过,Agent 可以把"怎么做某类事"写成 SKILL_XXX.md,下次遇到类似任务时直接加载,不用重新摸索。
最简单的触发方式:在任务完成后,让 Agent 判断"有没有值得沉淀的方法"。
SKILL_PROMPT = """
你刚完成了这个任务。请判断:这次解决问题的过程中,有没有一个"通用方法"值得记下来,以后遇到类似情况可以直接用?
如果有,用下面格式输出(只输出这个,不要其他话):
---SKILL---
文件名:SKILL_<简短英文名>.md
内容:
# 技能:<技能名>
## 适用场景
<什么情况下用这个技能,一两句话>
## 步骤
<具体怎么做,3-5步>
## 注意
<坑或边界,可选>
---END---
如果没有值得沉淀的通用方法,只输出:NO_SKILL
"""
Agent 回复 ---SKILL--- 块时,你的代码解析出来写成文件;回复 NO_SKILL 时什么都不做。这是 Hermes 完整自写 Skill 机制的简化版——完整实现涉及版本管理、技能评分、自动归并等,那些以官方仓库为准,这里给你的是能跑的骨架。
可跑代码骨架:读写记忆接进主循环
把上面三样东西组合起来,这是一个完整可跑的主循环骨架(把它保存成 agent_with_memory.py,填入 ANTHROPIC_API_KEY 就能跑):
import anthropic
import json
import os
import re
from pathlib import Path
# ── 配置 ──────────────────────────────────────────────
MEMORY_FILE = "MEMORY.md"
USER_FILE = "USER.md"
SKILLS_DIR = "skills" # Skill 文件存在这个目录里
client = anthropic.Anthropic() # 自动读取 ANTHROPIC_API_KEY
# ── 1. 读取记忆文件 ────────────────────────────────────
def load_memory() -> str:
sections = []
for path, label in [(USER_FILE, "用户偏好"), (MEMORY_FILE, "项目记忆")]:
try:
content = Path(path).read_text(encoding="utf-8").strip()
if content:
sections.append(f"## {label}\n{content}")
except FileNotFoundError:
pass
return "\n\n".join(sections)
def load_skills() -> str:
"""把 skills/ 目录里所有 SKILL_*.md 的内容拼起来"""
skill_dir = Path(SKILLS_DIR)
if not skill_dir.exists():
return ""
chunks = []
for f in sorted(skill_dir.glob("SKILL_*.md")):
try:
chunks.append(f.read_text(encoding="utf-8").strip())
except Exception:
pass
return "\n\n---\n\n".join(chunks)
# ── 2. 把新事实写回记忆文件 ────────────────────────────
def append_to_memory(new_fact: str) -> None:
"""把 Agent 总结的新事实追加到 MEMORY.md"""
if not new_fact.strip():
return
with open(MEMORY_FILE, "a", encoding="utf-8") as f:
f.write(f"\n- {new_fact.strip()}")
print(f"[记忆] 已追加:{new_fact.strip()}")
# ── 3. 解析并保存 Skill ────────────────────────────────
def try_save_skill(model_output: str) -> None:
"""如果模型输出里有 ---SKILL--- 块,解析后写成文件"""
m = re.search(r"---SKILL---\n文件名:(\S+)\n内容:\n(.*?)---END---", model_output, re.DOTALL)
if not m:
return
filename, content = m.group(1), m.group(2).strip()
Path(SKILLS_DIR).mkdir(exist_ok=True)
skill_path = Path(SKILLS_DIR) / filename
skill_path.write_text(content, encoding="utf-8")
print(f"[技能] 已保存:{skill_path}")
# ── 4. Agent 主循环 ────────────────────────────────────
def run_agent(user_message: str, tools: list = None) -> str:
if tools is None:
tools = []
# 读取记忆和已有技能,塞进 system prompt
memory_context = load_memory()
skill_context = load_skills()
system_parts = ["你是一个有记忆、有积累的助手,每次任务后会把新方法沉淀成技能。"]
if memory_context:
system_parts.append(memory_context)
if skill_context:
system_parts.append(f"## 已有技能\n{skill_context}")
system_prompt = "\n\n".join(system_parts)
messages = [{"role": "user", "content": user_message}]
print(f"\n用户:{user_message}")
# ── 主循环(想→做→看) ──────────────────────────────
max_rounds = 10
for _ in range(max_rounds):
kwargs = dict(
model="claude-opus-4-5", # 以官方文档为准(截稿 2026-06)
max_tokens=2048,
system=system_prompt,
messages=messages,
)
if tools:
kwargs["tools"] = tools
response = client.messages.create(**kwargs)
print(f"stop_reason: {response.stop_reason}")
if response.stop_reason == "end_turn":
final_text = "".join(
b.text for b in response.content if hasattr(b, "text")
)
print(f"\nAgent:{final_text[:200]}{'...' if len(final_text) > 200 else ''}")
# ── 5. 任务完成后:询问模型有没有新事实/新技能 ──────
new_fact = ask_for_new_fact(final_text, user_message)
if new_fact:
append_to_memory(new_fact)
try_save_skill(final_text) # 如果回复里带了 SKILL 块就保存
# 也可以再单独发一轮 SKILL_PROMPT,这里为简洁合并处理
return final_text
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" → 调用工具:{block.name},参数:{block.input}")
# 这里换成你真实的工具执行逻辑
result = f"(工具 {block.name} 的返回结果)"
print(f" ← 返回:{result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
})
messages.append({"role": "user", "content": tool_results})
else:
print(f"未预期的 stop_reason: {response.stop_reason}")
break
return "Agent 超出最大轮次,未能完成任务"
def ask_for_new_fact(agent_reply: str, original_task: str) -> str:
"""单独发一轮,让模型判断这次任务有没有值得记忆的新事实(一句话)"""
prompt = f"""
原始任务:{original_task}
你的回复:{agent_reply[:500]}
请判断:这次任务里有没有一条值得记入 MEMORY.md 的新事实(例如"发现了一个新路径"/"确认了某个配置")?
如果有,只输出那一句话,不加任何前缀。如果没有,只输出 NO_FACT。
""".strip()
r = client.messages.create(
model="claude-opus-4-5",
max_tokens=200,
messages=[{"role": "user", "content": prompt}],
)
result = r.content[0].text.strip()
return "" if result == "NO_FACT" else result
# ── 跑起来 ────────────────────────────────────────────
if __name__ == "__main__":
# 第一次跑:建空文件,让它感受到"没有记忆"
run_agent("帮我整理一下这个项目用的是什么技术栈,用一句话总结。")
# 再跑一次:这次它应该能看到上一轮写回来的记忆
# run_agent("上次我们讨论了什么?")
你应该看到什么
第一次跑完,如果 Agent 认为回复里有值得记录的事实,MEMORY.md 里会多出一行。第二次跑同一个脚本,system prompt 里就会带上这条记忆——Agent 不再是"每次都忘光"的状态。
skills/ 目录里,如果 Agent 判断本次任务有可复用的方法,会出现一个 SKILL_xxx.md 文件。下次加载时它会读进 system prompt,不需要再重新摸索同类问题。
这就是最小可用的"记忆+技能积累"完整循环:读 → 干活 → 写回 → 下次读进来。
怎么让它沉淀得更好
光有骨架还不够,沉淀质量决定这个机制有多大用。几个实践点:
写回时加时间戳
from datetime import date
def append_to_memory(new_fact: str) -> None:
today = date.today().isoformat()
with open(MEMORY_FILE, "a", encoding="utf-8") as f:
f.write(f"\n- {today}:{new_fact.strip()}")
加了时间后,你知道哪些记忆"比较新",清理时可以按时间保留近 3 个月的。
Skill 文件给它分类
SKILLS_DIR 里可以按主题建子目录,比如 skills/git/、skills/api/。加载时按需读,不需要每次把所有技能全塞进去。AI Agent 智能体阶梯 里后续章节会讲更细的按需检索。
USER.md 让用户自己写
第一次跑 Agent 之前,让用户手动填一遍 USER.md。模板给他,五分钟填完。比 Agent 自己猜用户偏好准多了,而且不会在错误的假设上越走越远。
技能不要让 Agent 自由命名
SKILL_XXX.md 的 XXX 如果让模型自由发挥,容易出现重复(SKILL_git_push.md 和 SKILL_git_commit.md 内容高度重叠)。可以在 SKILL_PROMPT 里加一行:「文件名格式:SKILL_<领域>_<操作>.md,领域和操作都用英文小写」,约束一下命名空间。
故障排查表
| 症状 | 原因 | 解法 |
|---|---|---|
| 记忆文件越来越大,几周后 system prompt 超 token | 每次都追加,从不清理,记忆无限膨胀 | 定期压缩:让 Agent 对 MEMORY.md 做一次「合并同类项+删过期条目」的汇总,再写回;或者用滑动窗口只保留最近 30 条 |
| 两个 Agent 进程同时写 MEMORY.md,内容混乱 | 文件级写回没有锁,并发写回冲突 | 加文件锁(fcntl.flock on Unix,msvcrt.locking on Windows);或改用 SQLite(自带行锁);单进程场景不会触发此坑 |
| 技能太多,每次全加载导致 system prompt 稀释,核心任务能力下降 | 加载太多无关技能,上下文被占满 | 按任务类型按需加载:先让模型判断「这次任务需要哪些技能」,只加载相关文件;其余技能不进 system prompt |
| Agent 每次都把没什么用的废话写进记忆 | ask_for_new_fact 没有限制条件 | 在 prompt 里加约束:「只记录影响未来任务的新事实,聊天内容和已知信息不记」;同时人工周期性审查一下 MEMORY.md |
| Skill 文件写出来格式乱,无法解析 | 模型没有严格遵守输出格式 | 把 SKILL_PROMPT 里的格式约束写得更死,加一行「注意:---SKILL--- 和 ---END--- 必须单独成行,文件名行必须以"文件名:"开头」 |
常见问题
Q:文件式记忆和向量数据库比,差在哪?
差在检索粒度和规模。记忆条数不多(几十到几百条)时,文件式读进来全量塞进 context 没问题,简单、可靠、不依赖额外服务。条数上千、需要语义搜索时,文件方案就撑不住了——那时候才需要向量库。前几节讲的 FTS5、向量检索是那个规模的解法,你现在不需要上来就搞。先把文件式跑通,再升级。
Q:MEMORY.md 放哪比较好?同一个项目里放一份,还是全局放一份?
两份都放:~/.agent/USER.md 放全局用户偏好(所有项目共用),./MEMORY.md 放当前项目事实(每个项目独立)。加载时两个都读,拼成 system prompt 的不同区块。这样用户偏好不用每个项目重填,项目事实也不会互相污染。
Q:这个骨架能接进 Hermes 吗?还是得重写?
骨架里的文件格式(MEMORY.md / USER.md / SKILL_*.md)和 Hermes 的设计思路一致,接起来改动不大。Hermes 完整实现还包括技能版本管理、自动评分、合并去重等,这些骨架里没有。如果你之后想升级到完整 Hermes,建议直接看 官方仓库,文件格式兼容,不用重写已有的记忆文件。
Q:Agent 写回的新事实,我需要每次审查吗?
建议早期审查,跑了二三十次之后可以信任它。刚开始 ask_for_new_fact 的 prompt 可能调得不够准,会写进一些废话;审查几次后调整 prompt,质量就会稳定。或者加一个"确认模式":每次写回前先打印出来让你确认,确认后再写——这在骨架里加三行就能做到。
小结
这节给你的不是原理,是可以直接搬进项目的三样东西:
- MEMORY.md + USER.md:按更新频率分开,一个存事实,一个存偏好
- 跨会话加载:读进来放 system prompt 靠前位置,Agent 每次都能"记住"
- 技能自积累:任务完成后让 Agent 判断值不值得写成 Skill,写了下次直接用
这三样的核心是读 → 干活 → 写回 → 下次读进来这个循环,一旦跑通了,你的 Agent 就从"每次都是新的"变成"越用越顺"。
下一步:把这个骨架接进你自己的 Agent,跑十次,看看 MEMORY.md 和 skills/ 目录里积累了什么,再决定哪里需要调。
👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务。