[控场AI]
· 4 分钟阅读· 2,462 字

LLM是无状态函数:AI记忆机制的本质与陷阱

LLM是无状态函数:AI记忆机制的本质与陷阱

LLM是无状态函数,AI'记忆'不过是调用方决定往消息数组里放什么的策略。

文章借一个四十行代码地图代理的踩坑经历,讲透了AI记忆的本质:大模型是无状态函数,每次调用只看到请求中携带的消息数组,端点根本不记得任何先前交互。作者用一个看似正确的bug说明这一点——代码构造临时消息列表发出去就丢,调用方列表从未增长。修复只需三行,核心是让调用方拥有并修改这份对话记录,且必须在同一函数里同时追加用户消息和助手回复,否则模型会收到一串没有答案的问题,表现得像推理能力很差。每次请求携带完整数组导致输入成本按 T·n(n+1)/2 三角形增长。文章给出三种策略——全部发送、摘要旧轮次、保留指针按需重读,并强调把对话记录写到磁盘是首要的可观测手段。

每个AI产品中的"记忆",本质上都是某个人在某处决定往数组里放什么。这个洞察来自一篇关于构建代码地图代理的系列技术文章,作者用一个四十行程序踩出的坑,把大模型记忆这件事讲透了。

LLM是无状态函数:AI记忆的本质

一个看起来对的bug

作者写完四十行版本的代码地图工具后,让它读第二个文件并回答"这个文件和上一个什么关系"。模型反问:你说的是哪个文件?

第一反应是某个flag掉了、session id忘了传。于是花了远超预期的时间去找这个不存在的session id。结论很残酷:端点根本不知道90秒前调用过它,而且没有任何配置能改变这一点。

问题的根源藏在一段看起来完全正确的代码里。函数签名接收一个消息列表,返回回复,读起来像一段对话。但 messages + [...] 只是构建了一个临时列表发出去就丢了,调用方的列表从未被修改。每次调用都从同一个起点开始。

这段代码能通过代码审查,是因为类型注解暗示了"历史"的存在,你必须注意到列表从未增长。

没有会话,只有一个列表

修复只需要三行,核心思路是:调用方拥有这个列表。

第一行追加用户消息,中间调用模型,第三行追加助手的回复。函数故意修改传入的列表——只有一份对话记录,调用方持有它,每次追加都发生在一个可以打断点的函数里。

第二行追加是最容易漏掉的,而它的失败模式比没有记忆更糟糕。如果不追加助手回复,模型收到的是一串没有答案的问题——一段它似乎从未开口的对话记录。于是它会重新回答已经回答过的问题,推翻两轮前的结论,表现得像一个推理能力很差的模型。你会在重写系统提示词和初始指令上浪费一个晚上,才发现问题所在。

把两次追加放在同一个函数里,不是代码风格问题,而是消除"只记录了一半对话"这种状态。

每次请求携带完整数组,意味着第一轮在第二轮被重新计费,第三轮又被计费一次。

**无状态(Stateless)**是大模型API调用的一个基础特性:每次HTTP请求都是独立的,服务端不会为你保留任何跨请求的上下文,也不会自动把上一轮的用户消息和助手回复拼接到本次请求中。模型推理时接收的输入就是一个完整的消息数组,输出只是基于这个数组得到的一次纯计算结果,计算完成后服务端不保留该数组。这意味着所谓"多轮对话"完全是客户端通过反复发送增长中的数组伪造出来的效果。理解这一点非常关键:很多初学者会预设端点"记得"刚才说了什么,从而花时间去找并不存在的session id或隐式会话机制。事实是,只要调用方不在请求里带上历史,模型在物理上就无法看到历史。

你的智能体只有一份状态

因为模型是数组的纯函数,数组就是程序的完整状态。所以把它写到磁盘上。

这一行代码结束了main函数,transcript.json 进入 .gitignore。当输出出错时,你真正需要回答的问题不是"模型为什么这么说",而是"模型看到了什么"。只有一个产物能回答这个问题,现在它在磁盘上。

几乎所有在这个阶段追查的坏答案,三十秒内都能在文件里看到原因:某个文件从未被追加、源码因为路径相对于错误目录被读成空字符串、系统消息被后续赋值覆盖。这些都不是模型问题,但打开对话记录之前,它们看起来都像模型问题。

这还让运行变得可复现。输入是一个JSON数组,你可以逐字节重新发送,改一条消息再发一次。

账单是三角形,不是直线

每次调用携带整个数组,如果每轮大约增加 T 个token,第 n 次请求携带约 nT 个输入token,整轮运行的成本是 T·n(n+1)/2。十轮相当于五十五轮的输入量,二十轮是两百一十轮。

对于聊天助手这还能接受,因为一轮就是一句话。但对于读取源文件的智能体,每轮是一整个文件,T 极大,三角形在个位数轮次就变得可怕。

三种诚实的策略各有取舍:

  • 全部发送:成本随轮次平方增长,适合短会话和学习阶段
  • 摘要旧轮次:成本在阈值后持平,但丢失了当时不知道重要的细节,适合聊天场景
  • 保留指针按需重读:成本持平加每次查找一次调用,适合内容在磁盘上且重读成本低的场景

对于代码库,第三种明显正确——仓库还在那里,重读一个文件就是一次磁盘读取。这也是为什么智能体最终需要自己获取内容,而不是被喂内容。

Token是大模型处理文本的基本单位,一个英文单词通常被切分成一到几个token,一个中文字符大约对应一到两个token。计费按输入token和输出token分别统计,本段讨论的主要是输入侧。所谓"三角形"增长来自两个叠加效应:一是每一轮都把之前所有轮次的消息重新发送一遍,二是随着轮数增加,单次请求里承载的历史越来越长。两者相乘就得到了 T·n(n+1)/2 的累加公式。这也解释了一个常见困惑:为什么一次看起来只有十轮的智能体会话,账单却像是跑了几十轮。在智能体读取源文件的场景下,T本身可能高达数千甚至上万token,三角形很快就从"可以接受"变成"不可承受"。

实践建议

  1. 把对话记录作为参数传递。一旦它变成对象上的隐藏属性,你就再也看不到自己发了什么。
  2. 在同一个函数里追加双方消息。让"只记录一半"的状态无法被表达。
  3. 每次运行都把数组写到磁盘,从第一天就开始。四行代码,是你很长一段时间内唯一的可观测手段。
  4. 在测试中断言角色顺序。检查两轮对话产生 system、user、assistant、user、assistant 的序列,能立刻抓住上面那个bug。
  5. 在能说清需要哪一列策略之前,不要装记忆库。三种方案都是一页代码,难的是选择,没有库能替你做这个决定。

"记忆"是一个产品词汇。它的每一种实现,都是关于往数组里放什么的策略。选好这个策略,是后续大部分工作的核心。

#LLM无状态#AI记忆机制#大模型对话记录#token成本优化#AI Agent开发#上下文管理#chat completions
分享:

相关推荐