记忆
智能体的短期记忆就是消息列表,长期记忆要自己存下来、需要时再取出。给智能体加上"记住"和"回想"两个工具,演示它在一次全新的对话里用上了上次记住的事实。
- 约 35 分钟
- 难度:进阶
- 实测:2026-09-14 deepseek-flash
第 03 模块第 1 课讲过,模型本身什么都不记得,"记忆"只是把之前的消息重新发一遍。对话结束,消息列表一扔,一切都没了。
对一个答疑助手来说,这很不方便。用户上周告诉过它"我们公司的项目还在用 Python 3.9,所有请求都要走代理",这周再问问题,它全忘了,给出的代码用了 3.10 的新语法,也没有配代理,在用户的环境里根本跑不起来。
这一课讲智能体的两种记忆:短期的和长期的,以及管理它们的一门手艺,叫上下文工程。
短期记忆:消息列表
智能体的短期记忆就是它的消息列表:用户的问题、每一步的工具调用和结果。第 2 课的循环里,模型每一步都能看到之前所有的步骤,所以它知道已经查过什么、还缺什么。
短期记忆的问题是会越来越长。第 2 课回答一个问题,3 步就用了 3700 多个输入词元;第 4 课那个多步任务,7 步用了 5 万个。任务再复杂一点,几十步下来,就会超出上下文窗口,也会贵得离谱。
控制短期记忆的办法,前面陆续讲过一些:
- 限制工具返回的长度。第 2 课的
read_doc一次最多 80 行,循环里还有 3000 字符的截断。 - 压缩旧的内容。第 03 模块第 1 课的"截断"和"摘要"同样适用于智能体:已经用过的工具结果,可以替换成一句摘要,比如"读了 timeouts.md 第 1 到 80 行,得知有四种超时"。
- 把子任务交给子智能体。子智能体在自己的上下文里完成工作,只把结论交回来,第 6 课会讲。
长期记忆:存下来,需要时取出
要跨越多次对话记住东西,就得把它存到消息列表之外:一个文件、一个数据库。下次对话时,再在需要的时候取出来,放进上下文。
最简单的实现,是给智能体两个工具,让它自己决定什么时候存、什么时候取:
MEMORY_FILE = Path(__file__).parent / "memory.json"
def load():
return json.loads(MEMORY_FILE.read_text()) if MEMORY_FILE.exists() else []
@tool("把一条关于用户的长期有用的事实存下来,比如用户的环境、偏好、项目情况。"
"只存以后的对话可能用到的事实,不要存一次性的问题。",
fact="一句话描述的事实,例如:用户的项目运行在 Python 3.9 上")
def remember(fact):
facts = load()
facts.append({"fact": fact, "time": time.strftime("%Y-%m-%d %H:%M")})
MEMORY_FILE.write_text(json.dumps(facts, ensure_ascii=False, indent=2))
return f"已记住:{fact}"
@tool("查看之前记住的关于用户的事实。回答涉及用户自己的环境、项目、偏好时,先调用它。")
def recall():
facts = load()
return "\n".join(f"- {f['fact']}({f['time']})" for f in facts) or "还没有记住任何事实"
@tool 是第 2 课写的装饰器,注册之后,这两个工具就和查文档的三个工具一起出现在智能体的工具箱里。说明书里写清楚了"存什么"(长期有用的事实,不是一次性的问题)和"什么时候取"(回答涉及用户自己的情况时),这是按第 3 课的方法写的。
system 提示词里再补一句,提醒它有这两个工具:
agent_loop.SYSTEM += "\n你有长期记忆工具:用户告诉你关于他自己的长期信息时,用 remember 记下来;回答前如果需要了解用户的情况,先用 recall。"
两次对话
code/05-agents/memory.py 模拟两次完全独立的对话。第一次,用户交代自己的情况;第二次是一个全新的消息列表,模型看不到第一次说过的任何话:
print("===== 第一次对话")
run_agent(model, "先跟你交代一下:我们公司的项目还在用 Python 3.9,所有外网请求都必须走公司代理 "
"http://10.1.2.3:8080。今天先不问问题,你记一下。")
print("\n===== 第二次对话(全新的消息列表,模型看不到第一次说过的话)")
run_agent(model, "帮我写一段用 httpx 请求 https://api.github.com 的代码,要能在我们的环境里直接跑。")
运行结果(第二次对话的回答较长,删减了一部分):
===== 第一次对话
[第 1 步] remember({"fact": "用户公司项目使用 Python 3.9"}) → 已记住:用户公司项目使用 Python 3.9
[第 1 步] remember({"fact": "用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080"}) → 已记住:用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080
[第 2 步] 回答:
已记下两条:
1. 公司项目使用 Python 3.9
2. 所有外网请求必须走公司代理 `http://10.1.2.3:8080`
下次你问 httpx 相关问题(比如代理配置),我会默认按这个环境来考虑。
共 2 步,输入 1606 词元,输出 159 词元
记忆文件的内容:
[
{
"fact": "用户公司项目使用 Python 3.9",
"time": "2026-09-14 22:25"
},
{
"fact": "用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080",
"time": "2026-09-14 22:25"
}
]
===== 第二次对话(全新的消息列表,模型看不到第一次说过的话)
[第 1 步] recall({}) → - 用户公司项目使用 Python 3.9(2026-09-14 22:25) | - 用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080(2026-09
[第 1 步] list_docs({}) → advanced/authentication.md | advanced/clients.md | advanced/event-hooks.md | advanced/exte
[第 2 步] read_doc({"path": "advanced/proxies.md", "start": 1, "end": 80}) → 1: HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web
[第 2 步] grep_docs({"keyword": "proxies"}) → advanced/clients.md:27: - Sending requests through HTTP proxies. | advanced/extensions.md:
[第 3 步] read_doc({"path": "quickstart.md", "start": 1, "end": 70}) → 1: # QuickStart | 2: | 3: First, start by importing HTTPX: | 4: | 5: ```pycon | 6: >>> i
[第 3 步] read_doc({"path": "environment_variables.md", "start": 1, "end": 40}) → 1: # Environment Variables | 2: | 3: The HTTPX library can be configured via environment
[第 4 步] think({}) → 错误:没有叫 think 的工具
[第 5 步] 回答:
抱歉,上一步是我调用工具时写错了(并不存在 `think` 工具),忽略即可。下面是正文。
## 说明你的环境的两个关键点
根据我记住的你的环境信息:
- 项目使用 **Python 3.9**
- 所有外网请求必须走公司代理 **http://10.1.2.3:8080**
所以代码里必须显式设置代理,否则请求会失败。
## 代码
```python
import httpx
# 公司要求所有外网请求走代理,无法直连时请确认这个参数
PROXY = "http://10.1.2.3:8080"
with httpx.Client(proxy=PROXY, timeout=10.0) as client:
r = client.get("https://api.github.com")
r.raise_for_status()
print(r.status_code)
print(r.json())
```
(后面还有顶层 API 的写法、文档依据和补充说明,省略)
共 5 步,输入 11449 词元,输出 1170 词元
第一次对话,模型调用了两次 remember,把两条事实分别存下来,没有把"今天先不问问题"这种一次性的话也存进去。
第二次对话,它第一步就调用了 recall,拿到了这两条事实。然后去文档里查怎么配置代理,最后写出的代码直接用上了公司的代理地址。它还提到了一个很实用的细节:如果环境变量里已经设置了 HTTP_PROXY,代码里的 proxy= 可以省略(它引用了 environment_variables.md)。
这次运行里还出了一个小插曲:第 4 步,模型调用了一个根本不存在的工具 think。第 2 课的循环把它变成了一条"错误:没有叫 think 的工具"交还给模型,模型在最终回答里道了个歉,然后正常给出了结果。这正是"把错误变成观察结果"的价值:一个意外没有让整个任务失败。
这个简单实现的问题
全部取出。recall 每次把所有事实都返回。事实少的时候没问题;记了几百条之后,每次都全部塞进上下文就不现实了。那时要按相关性检索:用第 04 模块的办法,给每条事实算向量,只取出和当前问题相关的几条。
只增不减。用户换了工作、项目升级到了 Python 3.12,旧的事实还在里面,而且和新的事实互相矛盾。需要能更新和删除,存的时候记下时间,冲突时以新的为准。
存什么全靠模型判断。它可能漏存重要的事,也可能把不该存的存下来,比如用户随口说的密码。长期记忆里的内容会在以后的每次对话中被取出,一旦存了敏感信息,风险会一直存在。实际产品里,通常要让用户能看到、能删除自己的记忆,敏感信息在存之前就要过滤掉。
有被污染的风险。如果智能体会读网页、读文件,那里面的内容可能诱导它"记住"一些错误的或者恶意的东西,这些东西会在以后的对话里一直影响它。第 8 课会讲这类攻击。
上下文工程
回头看,这一课和前面几课其实在做同一件事:决定模型每一步能看到什么。
- 放什么进去:检索到的文档、记住的事实、工具的结果。
- 放多少:截断、限制工具返回的长度。
- 放在哪里:固定的内容放在开头,能命中缓存;每次变化的内容放在最后。
- 什么时候拿出去:用过的工具结果压缩成摘要,子任务的细节留在子智能体里。
这门手艺现在常被叫作上下文工程(context engineering)。提示词工程关注的是"怎么写指令",上下文工程关注的是"每一步给模型看哪些信息"。对智能体来说,后者往往更重要:模型的能力是固定的,它能不能做对,很大程度上取决于它在做决定的那一刻,眼前有没有正确的信息,以及有没有被太多无关的信息淹没。
练习
- 运行
memory.py,然后在第二次对话里问一个和用户环境无关的问题(比如"httpx 的超时有几种"),看看它还会不会调用recall。 - 在第一次对话里再告诉它一条会过时的信息,然后在第二次对话里说"我们已经升级到 Python 3.12 了"。看看它会不会更新记忆,旧的那条怎么处理。给
remember设计一个办法,让新事实能覆盖旧事实。 - 在第一次对话里说"我的 GitHub token 是 ghp_xxxx,记一下",看看模型会不会把它存下来。修改
remember的说明书,禁止它存密码、token 这类信息,再试一次。
自测
1. 智能体的短期记忆和长期记忆分别是什么?
短期记忆是当前任务的消息列表,包括问题、每一步的工具调用和结果,任务结束就没有了。长期记忆是存到消息列表之外(文件、数据库)的信息,下次对话时在需要的时候取出来放进上下文。
2. 长期记忆里的事实越来越多之后,"每次全部取出"会有什么问题?怎么改进?
每次都把所有事实放进上下文,会越来越长、越来越贵,大部分内容和当前问题无关,还会干扰模型。改进办法是按相关性检索:给每条事实算向量或者建关键词索引,只取出和当前问题相关的几条。
3. 什么是上下文工程?它和提示词工程有什么区别?
上下文工程是决定模型每一步能看到哪些信息:放什么、放多少、放在哪里、什么时候移除。提示词工程关注怎么写指令。对需要多步执行、不断获取新信息的智能体来说,上下文工程往往更关键。
提问与讨论
这一课没看懂的地方,在这里问。看到别人的问题,也欢迎你来回答。
提问 +3 积分,回答别人 +6 积分。内容经审核后公开。
正在加载讨论…