阅读前须知-点我下拉

本篇文章为ReAct智能体架构系列的第二篇——记忆篇。在入门篇基础上,为你的小智能体装上"海马体",让它不再金鱼记忆。

前置要求:已完成入门篇的代码搭建,或至少理解了 agent.py 的基本结构。

所需工具及版本:
Python 3.10
OpenAI 2.32.0
任意大模型API

引言

上一篇文章我们搭好了ReAct智能体的骨架——它能查天气、能调工具,像模像样。但如果你真的跑起 main.py 和它聊上几轮,会发现一个尴尬的场面:

1
2
3
4
你:你好,我叫柏青
AI:你好柏青!有什么可以帮你的?
你:我叫什么名字?
AI:抱歉,我不知道你的名字……

刚说完就忘,翻脸不认人——这不就是金鱼记忆吗?

入门篇末尾我也提了一句:“缺点:缺乏记忆功能(Memory)”。本篇就来填这个坑。我们会把messages从一个用完即弃的"便签纸",升级成智能体随身携带的"记忆本",让它真正具备多轮对话的能力。

记忆篇是入门篇的直接延续。如果你还没读过入门篇,建议先移步 ReAct智能体架构-入门篇 打好基础。

先诊断:为什么智能体会"失忆"?

让我们回到入门篇最终版的 agent.py,仔细看看 run() 方法的第一行:

1
2
3
4
5
def run(self, user_input):
messages = [
{"role": "user", "content": user_input}
]
# ... 后续全部基于这个 messages 列表操作

发现问题了吗?每次调用 run()messages 都被重新创建。就像你每次跟朋友聊天之前,先把聊天记录整本撕掉,只留下一句刚说的话——对方不懵才怪。

用一张图来感受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第一次 run("你好,我叫柏青")
┌──────────────────────────────
│ messages = [
│ {"role": "user",
│ "content": "你好,我叫柏青"}
│ ]
└──────────────────────────────
↓ 智能体回复,然后……撕掉!🗑️

第二次 run("我叫什么名字?")
┌──────────────────────────────
│ messages = [
│ {"role": "user",
│ "content": "我叫什么名字?"}
│ ] ← 前一条消息呢?没了!
└──────────────────────────────

每一轮对话之间是断裂的。模型只看到当前这一句话,完全没有上下文。这就是失忆的根源。

解决方案:给 messages 升个级

升级思路如下:

messagesrun() 里的局部变量,升级为 Agent 类的实例属性 self.messages

局部变量 = 一次性便签纸,函数结束就扔。
实例属性 = 绑定在对象身上的笔记本,只要智能体还活着,本子就在。

对比一下改动:

改造前(金鱼版🐟) 改造后(记忆版🧠)
messages 在哪 run() 方法内(局部变量) __init__() 中初始化(实例属性)
生命周期 每次调用 run() 重建 和 Agent 实例同生共死
多轮对话 ❌ 每次失忆 ✅ 完整保留上下文
引用方式 messages self.messages

逐步改造

1. 在 __init__() 中初始化记忆本

1
2
3
4
5
6
7
8
class Agent:
def __init__(self):
self.client = OpenAI(
api_key=API_KEY,
base_url=BASE_URL
)
# 新增:初始化记忆本,让它跟随实例一生
self.messages = []

self.messages = [] 这行就是智能体的"出厂记忆"——刚出生时一片空白,等着被写入。

2. 给记忆本加一条"出厂设定"——System Prompt

光有空本子还不够,我们最好给智能体定个人设。就像新员工入职培训,告诉它"你是谁、你要做什么、你有什么能力"。

在OpenAI的消息体系中,role 有四种身份:

role 身份 谁说的 例子
system 系统指令 开发者预设的规则 “你是一个友好的天气助手”
user 用户 正在和你聊天的人 “北京天气怎么样?”
assistant 助手 智能体自己 “北京今天是晴天,25度”
tool 工具 函数执行的结果 “北京今天是晴天,25度”

一个形象的类比:如果把对话比作一场戏——

  • system = 导演给演员的剧本大纲(幕后,观众看不见)
  • user = 观众对演员说的话
  • assistant = 演员的表演
  • tool = 演员从后台拿到的道具

我们在初始化时就把 system prompt 塞进记忆本:

1
2
3
4
5
6
7
8
9
10
11
12
class Agent:
def __init__(self):
self.client = OpenAI(
api_key=API_KEY,
base_url=BASE_URL
)
self.messages = [
{
"role": "system",
"content": "你是一个友好的智能助手,名叫小智。当用户询问天气时,请使用get_weather工具。请用中文回答。"
}
]

这条 system 消息会一直待在记忆本的最前面,每轮对话都会带上——模型每次看聊天记录时,第一眼就能看到自己的"人设",不会跑偏。

3. 改造 run() 方法:追加而非覆盖

关键改动:把原来的 messages = [...](新建)改成 self.messages.append(...)(追加)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def run(self, user_input):
# 旧版:messages = [{"role": "user", "content": user_input}] ← 每次新建,旧的全丢

# 新版:把用户的新消息追加到记忆本末尾
self.messages.append({"role": "user", "content": user_input})

# 第一次调用:询问模型是否需要使用工具
response = self.client.chat.completions.create(
model="gpt-4.1-mini",
messages=self.messages, # 传入完整的记忆本
tools=tools
)

message = response.choices[0].message

if message.tool_calls:
# 把模型的工具调用请求也记下来
self.messages.append(message)

for tool_call in message.tool_calls:
func_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
result = tool_map[func_name](**args)

# 把工具执行结果也记下来
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})

# 第二次调用:汇总最终答案
final_response = self.client.chat.completions.create(
model="gpt-4o",
messages=self.messages
)
final_message = final_response.choices[0].message
# 最后:把模型的最终回复也记入记忆本
self.messages.append(final_message)
return final_message.content

# 不需要工具时:把模型的回复记下来再返回
self.messages.append(message)
return message.content

关键细节:注意代码末尾,当不需要工具调用时,我们也执行了 self.messages.append(message)。这一步很多初学者会漏掉——漏掉的后果是:智能体的回复没有被记入记忆本,下一轮对话时模型会"看到用户说了两句话,但自己一句都没回过",上下文就乱了。

4. 数据流对比:改造前 vs 改造后

理解一个概念最好的方式是看它怎么流动。下面用同一个对话场景对比:

场景:用户先自我介绍,再问名字。

改造前(金鱼版🐟)

1
2
3
4
5
6
7
8
第1轮 run("我叫柏青")
messages = [user: "我叫柏青"] ← 新建
→ AI回复:"你好柏青!"
→ messages 销毁 💥

第2轮 run("我叫什么名字?")
messages = [user: "我叫什么名字?"] ← 又是新建,上一轮没了
→ AI回复:"我不知道你的名字。"

改造后(记忆版🧠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第1轮 run("我叫柏青")
self.messages = [
system: "你是一个友好的智能助手...",
user: "我叫柏青", ← 追加
]
→ 不需要工具
→ AI回复:"你好柏青!"
self.messages = [
system: "你是一个友好的智能助手...",
user: "我叫柏青",
assistant: "你好柏青!", ← 追加
]

第2轮 run("我叫什么名字?")
self.messages = [
system: "你是一个友好的智能助手...",
user: "我叫柏青", ← 还在!
assistant: "你好柏青!", ← 还在!
user: "我叫什么名字?", ← 追加
]
→ AI回复:"你叫柏青!" ✅

看到区别了吗?记忆版的多轮对话就像一个不断滚动的剧本,每一句话都被记录下来,模型每次都能"回顾前情提要"。

上下文窗口:记忆本也有页数限制

现实总是残酷的——记忆本不是无限大的。大模型都有一个叫做 上下文窗口(Context Window) 的概念,也就是它一次最多能"看到"多少内容。

用一本书来类比:

概念 类比 说明
Context Window 书的"可见页数上限" 模型一次最多读 N 页
Token 一个字/词 中英文混合约 1 个 token ≈ 0.75 个汉字
self.messages 你写过的所有笔记 越写越厚
超出窗口 笔记太厚,最早的几页自动被遮住 模型看不到最早的内容

常见模型的上下文窗口:

模型 上下文窗口 大约能记住
DeepSeek-V4-Pro 1M tokens 150万~250万汉字
Gemini 3.1 Pro 1M tokens 150万~250万汉字
ChatGPT 5.5 128K tokens 20万~30万汉字
Qwen3.7 Max 256K~1M tokens 40万~250万汉字
豆包等模型 128K~256K tokens 20万~60万汉字

对于我们的教程场景,聊几百轮都超不了——所以暂时不用担心。但如果你以后要拿记忆版智能体去做更复杂的项目,知道这个概念很重要。

进阶:简单的记忆管理策略

当对话实在太长时,有几种简单的"瘦身"策略:

  1. 保留 system + 最近 N 轮:始终保留 system prompt,只保留最近 20 轮对话,旧的扔掉。
  2. 摘要压缩:每隔一段对话,让模型自己把历史总结成一段摘要,用摘要替代原文。
  3. 滑动窗口:始终只保留最近的 N 条消息,像一扇在聊天记录上滑动的窗。

这些属于进阶内容,本篇不展开——先跑通基础版,后面自然会遇到这些需求。

完整代码示例

基础ReAct架构(记忆版)的树形项目目录结构:

1
2
3
4
5
6
7
ReactAgentAI/
├── tools/
│ ├── __init__.py
│ └── get_weather.py
├── config.py
├── agent.py
└── main.py

config.py 代码示例(不变):

1
2
BASE_URL = "https://api.xxxxx/v1"
API_KEY = "sk-T3xxxxxxxxw3o"

agent.py 完整代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from openai import OpenAI
from config import BASE_URL, API_KEY
from tools import tools, tool_map
import json

class Agent:

def __init__(self):
# 初始化客户端
self.client = OpenAI(
api_key=API_KEY,
base_url=BASE_URL
)
# 初始化记忆本——system prompt 作为出厂设定
self.messages = [
{
"role": "system",
"content": "你是一个友好的智能助手,名叫小智。当用户询问天气时,请使用get_weather工具。请用中文回答。"
}
]

def run(self, user_input):
# 追加用户消息到记忆本
self.messages.append({"role": "user", "content": user_input})

# 第一次调用:询问模型是否需要使用工具
response = self.client.chat.completions.create(
model="gpt-4.1-mini",
messages=self.messages,
tools=tools
)

message = response.choices[0].message

# 模型想调用工具
if message.tool_calls:
# 记下模型的工具调用请求
self.messages.append(message)

for tool_call in message.tool_calls:
func_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
result = tool_map[func_name](**args)

# 记下每个工具的执行结果
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})

# 第二次调用:汇总最终答案
final_response = self.client.chat.completions.create(
model="gpt-4o",
messages=self.messages
)
final_message = final_response.choices[0].message
# 记下最终回复
self.messages.append(final_message)
return final_message.content

# 不需要工具时:记下回复并返回
self.messages.append(message)
return message.content

tools/__init__.py 代码示例(不变):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from .get_weather import get_weather

tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取某个城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京"
}
},
"required": ["city"]
}
}
}
]
tool_map = {
"get_weather": get_weather
}

tools/get_weather.py 代码示例(不变):

1
2
def get_weather(city):
return f"{city}今天是晴天,25度"

main.py 代码示例(不变):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from agent import Agent

agent = Agent()

while True:
user_input = input("你:")
if user_input.strip().lower() in ["exit", "quit", "q"]:
print("AI:程序结束")
break
if not user_input.strip():
print("AI:你还没输入内容呢")
continue
result = agent.run(user_input)
print("AI:", result)

改造前后核心差异一览

对比维度 入门篇(金鱼版🐟) 记忆篇(记忆版🧠)
messages 存储位置 run() 内局部变量 self.messages 实例属性
每次 run() 调用 新建空列表 追加到已有列表
System Prompt 有(出厂人设)
多轮对话 ❌ 每次失忆 ✅ 完整上下文
能不能记住你的名字 不能 能(直到上下文窗口满)
代码改动量 核心只改 3 处

测试一下

跑起 main.py,试试这些对话:

1
2
3
4
5
6
7
8
你:你好,我叫柏青
AI:你好柏青!有什么可以帮你的?
你:我叫什么名字?
AI:你叫柏青!😊
你:北京天气怎么样?
AI:北京今天是晴天,25度。
你:我刚才问了哪两个城市的天气?
AI:你刚才只问了北京的天气。还没有问其他城市哦。

第一轮自我介绍 → 第二轮验证记忆 → 第三轮调用工具 → 第四轮验证上下文追溯。四个维度全部通过,你的智能体正式告别金鱼记忆!🎉

本篇小结

我们做的事情本质上就是一次"记忆搬家"

1
2
改造前:messages 是便签纸,用完就扔 
改造后:self.messages 是笔记本,一页一页往上加

这个改动只有几行代码,但意义重大——它让智能体从一个"无状态的工具调用器"变成了一个"有上下文的对话伙伴"。入门篇我们搭好了骨架,记忆篇我们给它注入了连续性

当然,这还只是"短期记忆"——对话关掉就没了。如果你想让智能体在下次启动时还记得你叫柏青,那就需要长期记忆(持久化存储),那是后续篇章的内容了。

你已经学会了:
✅ 为什么入门篇的智能体会"翻脸不认人"
✅ 如何把 messages 升级为实例属性 self.messages
✅ System Prompt 的作用和写法
✅ 四种消息角色的含义(system / user / assistant / tool)
✅ 多轮对话的数据流
✅ 上下文窗口的基本概念
✅ 如何在不需要工具时也正确 append 消息

下一篇文章,我们将在记忆版的基础上继续扩展——让智能体具备更丰富的工具能力和更强的自主决策能力。敬请期待!

AI总结

本篇为ReAct智能体架构系列第二篇——记忆篇。

核心问题:入门篇的智能体每次调用 run() 都会重建 messages 列表,导致"金鱼记忆"——说完就忘。

解决方案:将 messages 从 run() 内的局部变量升级为 Agent 类的实例属性 self.messages,同时在初始化时写入 system prompt 作为"出厂人设"。

关键改动

  1. __init__() 中初始化 self.messages 并写入 system prompt
  2. run() 中用 self.messages.append() 替代 messages = [...]
  3. 明确四种消息角色:system(系统指令)、user(用户)、assistant(助手)、tool(工具结果)

额外科普:上下文窗口(Context Window)的概念——记忆本有"页数上限",短期对话不用管,长期项目需要管理策略。

成果:智能体从"无状态工具调用器"升级为"有上下文连续性的对话伙伴",能够记住用户的名字和历史对话内容。