9. Agent 的记忆与上下文管理
这是什么:上下文(context) = 每次请求发给模型的那段消息历史,它就是 Agent 全部的工作记忆。
为什么用:Agent 每走一步都要重发全部历史,跑十几步就撑爆上下文——记忆问题本质是上下文预算问题。
如何用:先用消息历史当短期记忆,历史的肥了就摘要压缩,跨会话要记的就落到文件或向量库。
9.1 上下文窗口 = Agent 的工作记忆
模型没有真正的记忆。每次请求,它能看到的只有你发过去的那段 messages:
messages = [
{user: "北京天气怎么样?需要穿什么?"},
{assistant: tool_calls[get_weather]},
{tool: "北京:晴,26℃"},
{assistant: tool_calls[get_tip]},
{tool: "穿薄外套就行"},
]
这份清单就是 Agent 的全部记忆。把它删了,模型就失忆了;把它撑太长,前面的内容就会被挤出去(或者干脆报错超长)。
| 上下文状态 | 后果 |
|---|---|
| 太短 | 忘了自己查过什么,重复劳动 |
| 刚好 | 最佳 |
| 太长 | token 成本飙升,还可能超出模型上限报错 |
9.2 短期记忆:就是消息历史
最简单也最常用的一招:把裁剪后的历史一起发过去。
messages.append(msg) # 模型的决定
messages.append({"role": "tool", ...}) # 工具的结果
# 下一轮把整个 messages 发过去 —— 记忆就是这么来的
但要注意:工具返回的大块原始数据是最占地方的部分,也是最该被裁掉的。
9.3 摘要压缩:把老对话折叠成一段话
当历史太长,用模型自己把老部分总结成一段话,替换掉明细:
def compress(messages, keep_recent=6):
"""老消息压成一条摘要,最近几条原样保留"""
if len(messages) <= keep_recent:
return messages
old, recent = messages[:-keep_recent], messages[-keep_recent:]
summary = client.chat.completions.create(
model=CHAT_MODEL,
messages=[{
"role": "user",
"content": "把下面的对话过程总结成一段话,保留关键结论、"
"已执行的动作和结果,200 字以内:\n\n"
+ "\n".join(str(m.get("content", "")) for m in old)
}],
).choices[0].message.content
return [{"role": "system", "content": f"【此前过程摘要】{summary}"}] + recent
调用处:
for step in range(max_steps):
resp = client.chat.completions.create(
model=CHAT_MODEL,
messages=compress(messages), # ← 每次发之前先压
tools=TOOLS,
)
压缩的代价:细节丢了。所以要留几条最近的消息原样不动——模型至少还记得「刚刚做了什么」。
9.4 长期记忆:写出去再读回来
Agent 要跨会话记住东西(比如「用户 prefers 简洁回答」),有两个办法:
① 简单做法:写文件
import json
from pathlib import Path
MEMORY_FILE = Path("memory.json")
def remember(fact: str):
facts = json.loads(MEMORY_FILE.read_text("utf-8")) if MEMORY_FILE.exists() else []
facts.append(fact)
MEMORY_FILE.write_text(json.dumps(facts, ensure_ascii=False, indent=2), "utf-8")
def recall() -> list[str]:
if not MEMORY_FILE.exists():
return []
return json.loads(MEMORY_FILE.read_text("utf-8"))
用法:给 Agent 一个 remember 工具,它觉得该记的就自己存;每次新会话开头,把 recall() 的内容作为 system 消息注入。
② 讲究做法:向量库
记忆条目多起来(几百条以上)时,用 RAG 教程 里的那套:切块 → 向量化 → 存 Chroma → 检索时按当前问题召回最相关的几条。本质就是给 Agent 配了个外接大脑。
9.5 该用哪种
| 场景 | 用什么 |
|---|---|
| 一次任务内的多步协作 | 消息历史(第 2、3 章那套) |
| 任务跑长了、历史太肥 | 摘要压缩 |
| 跨会话记住用户偏好/事实 | 写文件(少量) |
| 跨会话记忆量很大 | 向量库 + 检索(RAG 那套) |
先从最简单的开始:绝大多数 Agent 用「消息历史 + 偶尔压缩 + 一个 memory.json」就够了。别一上来就上向量库。
9.6 常见翻车现场
| 报错 / 现象 | 原因 | 修法 |
|---|---|---|
context_length_exceeded |
消息太长 | 压缩,或减少工具返回量 |
| Agent 重复做已经做过的事 | 老结果被压没了 | 保留最近几条,或在结果里标注「已查过」 |
| 摘要越压越糊 | 反复压缩导致信息层层丢失 | 只压一次老部分,别压摘要的摘要 |
| 记忆里混进垃圾 | 什么都被 remember 存了 |
记忆要筛选,别全存 |
| 换个会话就失忆 | 记忆没落到持久化 | 用文件/数据库存,别只放内存 |
9.7 动手改
- 给你的 Agent 循环加上
compress(),跑一个 10 步的任务,看看上下文长度变化 - 加一个
remember工具,让它把「用户喜欢简短回答」记下来,新会话时能想起来 - 把工具返回值里的原始 JSON 换成裁剪版,对比 token 消耗
9.8 小结
- 上下文窗口就是 Agent 的全部记忆,每轮都要重发
- 短期记忆 = 消息历史;太长就摘要压缩,但要保留最近几条
- 长期记忆 = 写到文件或向量库,跨会话时再读回来
- 压缩有代价:细节会丢,所以要留最近的原文
- 从最简单做起:消息历史 + 压缩 + 一个 memory.json 足够大多数场景
下一章:综合练习,把前五章拼成一个能干活的 Agent 🐸