二、《从零手撸 Agent》 — 聊聊 LLM 的失忆真相

《从零手撸 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 多智能体开发特训营》

相关推荐
打呵欠的猫17 分钟前
前端接口超时从 15s 改到动态值后,用户投诉降了 60%——AI 帮我分析出的方案
前端·ai编程
苏灵凯24 分钟前
IT疑难杂症诊疗室:从故障定位到根治的技术实战指南
笔记·ai·域名·agent·deepseek
0xBADCODE24 分钟前
动态DEX加载+反射+DES硬编码密钥:安卓三层逆向实战
android·java·python·安全·网络安全·逆向·ctf
郭邯24 分钟前
用纯前端实现一个代码压缩美化工具,我踩了这三个坑
前端
用户2832096793727 分钟前
从进程线程到 async/await:JS Event-loop事件循环机制底层解析
前端·javascript
岁月宁静36 分钟前
一、《从零手撸 Agent》 我用 10 行代码跑通了第一次大模型调用(顺便踩了 4 个坑)
前端·python·agent
大连好光景37 分钟前
【AI Agent案例开发项目01解读】
chrome·python·langchain
武子康38 分钟前
删掉邮箱后,Agent Trace 仍可能泄露什么:一条可重放脱敏流水线
人工智能·llm·agent
青 春 记 忆40 分钟前
零基础入门python43:Django接口测试与库存边界
python·django·后端开发