《从零手撸 Agent》系列第 2 篇。上一篇跑通了第一次调用,这一篇讲 messages 这个参数。它是你和模型沟通的唯一通道,也是后面所有记忆机制的地基。
先看一个翻车现场
上一篇末尾我埋了个伏笔,现在揭晓。连续问它两个问题:
python
1from openai import OpenAI
2client = OpenAI(api_key="sk-xxx", base_url="https://api.deepseek.com")
3
4def ask(msgs):
5 resp = client.chat.completions.create(model="deepseek-chat", messages=msgs)
6 return resp.choices[0].message.content
7
8print(ask([{"role": "user", "content": "我叫阿康"}]))
9# 你好,阿康!
10
11print(ask([{"role": "user", "content": "我叫什么名字?"}]))
12# 抱歉,您没有告诉我您的名字。
上一句刚说完,它翻脸不认人。
我第一次碰到这个的时候,第一反应是"这模型有 bug 吧"。后来想明白了:没有 bug,这就是 API 的设计。LLM 接口是无状态的,每次调用完全独立,服务器不会为你在两次调用之间保留任何东西。
那 ChatGPT 为什么记得住对话?因为它的客户端程序把历史对话每次都重新发了一遍。你看到的"连续对话",是每次都带着完整历史重新问。
这个事实是理解 Agent 的地基。后面讲记忆系统、上下文管理,全都是在解决"无状态"带来的问题。
messages 的结构:一个数组,每条两个键
ini
1messages = [
2 {"role": "system", "content": "你是一个简洁的编程助手。"},
3 {"role": "user", "content": "什么是列表?"},
4 {"role": "assistant", "content": "列表是Python中可变的有序集合。"},
5 {"role": "user", "content": "给我一个例子。"},
6]
role 有四种,分工很清楚:
| role | 谁说的 | 干什么用 |
|---|---|---|
system |
你写的系统设定 | 定规矩,服从权重最高 |
user |
用户 | 提问、下任务 |
assistant |
模型 | 模型的历史回答(由你回传) |
tool |
工具程序 | 工具执行结果,第 6 篇出现 |
重点说说 system。它不是"第一句话",而是行为约束。训练的时候它被赋予了更高的服从权重,所以规矩放 system,事情放 user。我见过有人把所有要求都挤在 user 消息里,短对话还行,聊长了模型就开始阳奉阴违------user 消息里的约束,会随着上下文变长被稀释掉。
还有个冷知识:assistant 消息可以手动伪造。你插入一条模型从没说过的 assistant 消息,它会以为自己说过,顺着往下接。这个技巧在 Agent 开发里挺常用,比如预填输出格式的开头,逼模型按格式续写。
修复失忆:把历史传回去
python
1history = []
2
3def chat_with_memory(user_input: str) -> str:
4 history.append({"role": "user", "content": user_input})
5 resp = client.chat.completions.create(model="deepseek-chat", messages=history)
6 answer = resp.choices[0].message.content
7 history.append({"role": "assistant", "content": answer})
8 return answer
9
10print(chat_with_memory("我叫阿康")) # 你好阿康!
11print(chat_with_memory("我叫什么名字?")) # 你叫阿康。
所谓记忆,就是重传历史。没有魔法。
但这 8 行代码里藏着两个新手必踩的坑。
坑一:忘了 append 模型的回答。 第二个 history.append 漏掉的话,下一轮发出去的就是连续两条 user 消息,模型看不到自己说过什么,轻则答非所问,重则直接报错。每轮对话,user 和 assistant 两条要成对追加。
坑二:历史无限膨胀。 每轮全量重传,聊得越久,输入 token 越多,越慢越贵,最后撑爆上下文窗口直接报错。这不是 bug,是"重传式记忆"的固有代价。解法(截断、摘要压缩)我放到第 19 篇细说,这里先记下这个隐患。
一个值得做的实验:伪造记忆
css
1history = [2 {"role": "user", "content": "推荐一本书"},3 {"role": "assistant", "content": "我推荐《三体》。"},4]
5history.append({"role": "user", "content": "你刚才为什么推荐这本?"})
模型会煞有介事地解释推荐《三体》的理由------编得有鼻子有眼。它根本分不清历史是真是假。
这个实验我印象很深,因为它说明了一件事:模型对历史照单全收,没有任何辨伪能力。Agent 系统里所有的"记忆",本质都是往这个数组里塞内容。塞什么、什么时候塞、塞多少,就是后面要讲的记忆架构。
多轮对话的全过程,画出来是这样
ini
第1轮:
发送: [S, U1] → 返回 A1
本地: [S, U1, A1]
第2轮:
发送: [S, U1, A1, U2] ← 历史完整重传
返回: A2
本地: [S, U1, A1, U2, A2]
注意一个细节:发送内容的开头部分永远不变,只在末尾追加。这个规律现在看着不起眼,第 11 篇讲 KV Cache 的时候,你会发现它直接关系到你的 API 账单能便宜多少。
收尾
这一篇的信息量不大,但那个"无状态"的事实值得反复咀嚼。后面 20 多篇,一半以上的内容都在跟这个事实较劲。
下一篇讲 system prompt 怎么写才有效,以及 temperature 这个参数到底在调什么------我做了个实验,同一个问题在不同 temperature 下跑三遍,差异大到离谱。
动手环节
- 跑一遍开头的失忆实验,亲眼看到它不记得
- 修复它,跑通带记忆的版本
- 做伪造记忆实验,观察它编理由的架势
写在最后
如果想系统、完整地吃透 Harness、Hermes 整套前沿智能体开发体系,完成从只会调模型到可控、高质量、可落地的 AI 工程交付进阶,可以关注慕课网近期上新的《Harness&Hermes 多智能体开发特训营》。