首页 / 文章

智能体记忆详解:上下文窗口、RAG与持久状态

智能体记忆包含三种不同的形式。了解上下文窗口、检索机制与持久状态之间的差异,各自的成本,以及如何首先选择合适的一种。

发布者:REACTAPP.TOP阅读约 10 分钟2195 词

来源: Agent Memory Explained

在智能体开发中,几乎没有哪个词会比“记忆”更易造成混淆。当有人说他们的智能体具备记忆功能时,他们可能指的是对话历史很长,或者接入了向量数据库,又或者事实被保存在能跨重启存活的文件中。其实这些都是三个独立的系统,各自有不同的成本、故障机制以及存在理由。

一句话就能澄清大部分困惑:模型没有记忆,只有输入;人们所说的记忆其实只是用于决定哪些内容会被纳入该输入的规则而已。

模型是无状态的。每次API调用都从零开始,不会记住你上一条消息、上一次会话的内容,也不会记得你给它起的名字。它在调用过程中所知道的仅限于请求中的内容,除此之外一无所知。

因此,关键问题不在于该智能体是否有记忆,而在于什么内容能进入这个请求以及其来源。对此有三种答案,任务就是将它们区分开来。

无状态模型,有状态应用

初学者会认为模型在维持对话,其实并非如此。实际上是应用在维持对话并重新播放它。每当你发送第四条聊天消息时,你的代码每次都会传输消息1、2、3和4全部内容。那种连续性的感觉来自于历史记录的重新发送,而模型只是接收到比之前更长的输入而已。

考虑到这一点,这三层就不再那么抽象了:

  • 上下文窗口就是当前存在于请求中的内容。
  • 检索(RAG)则是用来查找材料并放入请求中的方法。
  • 持久状态指的是让事实在请求结束后依然存在,以便日后再次使用。
  • 容器、查询机制和存储系统:这是三种不同的概念。如果将它们混为一谈,就会把向量数据库用于解决本应由preferences.json文件处理的问题。

    第一层:窗口,即模型的工作台

    可以把窗口视为模型当前能够看到的所有内容。系统提示、历史记录、工具定义、工具输出、检索到的文档以及正在生成的回复,全都共享同一份令牌预算。

    想象一下工作台——模型需要处理的任何信息都必须放在工作台上,另一间的文件柜里的内容则不算数。

    其优势在于:即时性。窗口中的所有内容一目了然,无需额外查询步骤,模型可以同时处理这些信息。对于那些资料量有限且已知的任务,将所有内容放入窗口是最简单的办法,往往也是最佳选择。

    成本:

    1. 金钱与延迟取决于你重新发送的内容。重新发送长对话中的第51条消息时,需要重新传输前50条的历史记录。仅仅因为内容能够容纳,并不意味着长上下文是免费的。
    2. 注意力分配不均。模型通常最可靠地使用长输入的起始和结尾部分,而中间部分则较少被利用,这就是常见的“中间内容被忽略”现象。在极长的提示词中位于60%位置的事实虽然存在,但可能永远无法被有效利用。能够容纳并不等同于会被读取。
  • 它结束了。会话一旦结束,该窗口就会消失。上下文窗口从不保存任何内容。这是初学者最常犯的错误:他们以为长时间的对话会被记住,但实际上那只是缓冲区,并非存储空间。
  • 整个机制只需几行代码即可实现:

    messages = []
    
    def turn(user_text):
        messages.append(
            {"role": "user",
             "content": user_text}
        )
        # every call resends
        # the ENTIRE history
        reply = client.messages.create(
            model="claude-opus-5",
            max_tokens=1024,
            messages=messages,
        )
        messages.append(
            {"role": "assistant",
             "content": reply.content[0].text}
        )
        return reply
    
    # messages grows forever.
    # so does your bill.
    

    那个列表就是这里的“内存”。它仅存在于你的进程内部,因此一旦进程结束,该列表也会随之消失。

    当任务时间较短、所需材料数量有限且事先已知,且无需任何内容延续到会话结束后时,仅使用窗口功能就足够了。用于总结、单个文件审核或一次性提取的任务根本不需要更多功能,不必添加额外的复杂结构。

    第二层:检索,即参考库

    迟早,这些材料会变得过多而难以处理,或者不再值得投入成本。可能是10,000条支持工单、一个内部维基或一个代码库。你不可能在每次请求中都加载所有这些内容,也不应该这么做。

    检索增强生成技术将工作分为两步:提前准备索引,然后在有查询时再从中查找所需信息。

    首先将文档切割成若干片段,把每个片段转换成嵌入向量(一种能够体现含义的数值向量)并存储起来。当有问题出现时,先对该问题进行嵌入处理,找出向量距离最近的那些片段,然后将这些片段粘贴到问题旁边的上下文窗口中。

    # 1. AHEAD OF TIME (once, offline)
    for chunk in split(documents):
        store.add(
            text=chunk,
            vector=embed(chunk),
        )
    
    # 2. AT QUESTION TIME (every request)
    def answer(question):
        hits = store.search(
            embed(question), top_k=5
        )
        context = "\n\n".join(h.text for h in hits)
        return client.messages.create(
            model="claude-opus-5",
            max_tokens=1024,
            # retrieved chunks land in the same request
            messages=[{
                "role": "user",
                "content": f"{context}\n\nQ: {question}",
            }],
        )
    

    注意实际发生了什么。RAG并没有为模型提供记忆功能,而是给出了窗口内容的选择策略。被检索出的片段会与其他所有内容一同出现在同一请求中,因此RAG只是通往第一层的通道,没有其他替代方案。

    优势:扩展性与时效性。数以百万计的文档可以独立于你的代码发生变化,而模型只需处理与当前问题相关的那少量文档即可。

    成本:

    1. 结果在早期就已确定。在构建索引时,片段大小、嵌入模型以及索引设计就已经固定下来。如果某个事实分散在两个不同的片段中,或者表述方式与用户的提问不一致,那么该事实就根本无法被找到,模型就会基于已获取的信息自信地给出答案。在观察者看来,糟糕的检索结果与幻觉表现并无二致。
  • 它主要是只读的。传统的RAG就像借书证而非笔记本——智能体可以查询信息但不会记录任何内容,且在单次会话中所学到的信息除非被构建到索引中,否则不会进入索引。
  • 还需要额外步骤。对查询进行嵌入处理并搜索索引,这会为每个请求增加延迟。
  • 当知识库规模庞大、所有用户都能使用且几乎不会变化时,检索式方法较为适用,比如文档、政策、产品目录和代码等。其特点是所有用户共享同一个语料库。如果需要存储的内容属于特定用户且会随着对话不断变化,那么检索式方法就不合适,这时就需要第三层解决方案。

    第三层:持久状态,即笔记本

    持久状态是指进程终止后仍然存在的信息。它并非智能体对世界的认知,而是那些应当延续到本次会话之外的学习成果:用户偏好、任务检查点、已做出的决策、尝试过又放弃的内容,以及之前运行过程的总结。这些都是由智能体自身记录下来的具体且有价值的小信息。

    其模式是一个围绕会话展开的读取、修改、写入循环:

    # session start: load
    with open("state.json") as f:
        state = json.load(f)
    
    system = (
        "Known about this user:\n"
        f"{json.dumps(state)}"
    )
    
    messages = []
    
    def turn(user_text):
        messages.append(
            {"role": "user",
             "content": user_text}
        )
        reply = client.messages.create(
            model="claude-opus-5",
            max_tokens=1024,
            system=system,   # state in
            messages=messages,
        )
        return reply
    
    # ... along the way ...
    state["prefers"] = "metric units"
    state["last_checkpoint"] = "step_4_done"
    
    # session end: save
    with open("state.json", "w") as f:
        json.dump(state, f)
    

    从概念上讲,无需更多东西。JSON 文件即算是一种真正的实现方式;数据库记录、键值存储或 Markdown 笔记文件夹也同样适用。载体本身并无特别之处,有趣的是由谁来决定该保存什么以及何时重新加载。

    如何做出正确选择才是难点,这也是各种智能体框架存在差异的地方。目前最直接的方法是将这项工作交给模型:通过 Anthropic 的记忆工具,Claude 可以使用一组简单的文件操作命令(查看、创建、替换、插入、删除和重命名),这些命令在隔离的 /memories 目录中运行,这样智能体就能自行判断哪些内容值得保存,并在下次会话开始时读取这些笔记。而你的应用程序则会在你选择的存储介质上执行相应操作。该工具在客户端运行,因此文件始终由你掌控。

    优势:连贯性、个性化以及长期运行的能力。那些需要从中断处继续执行多日任务的智能体离不开这些特性,同样,那些不必反复询问用户时区的智能体也需要它们。

    成本:

    1. 必须有人进行整理。如果把所有内容都记录下来,就会得到一份无法搜索的文本;记录太少则会导致连贯性中断。要构建良好的持久状态实属不易。
    2. 过时的信息比没有信息更糟糕。八个月前保存且从未被修改过的偏好设置仍会被全盘采用。数据检索失败只会带来模糊性,而过时的状态则会导致错误的操作行为。
    3. 加载并非免费。无论加载什么内容,都会占用界面空间,因此第三层也会影响到第一层。

    当同一用户或任务反复触发智能体时,应采用持久状态机制,这样第 n 次运行所学到的内容应当能够影响第 n+1 次运行。

    三种机制的对比

    由应用程序在每次调用时生成 通过索引任务生成 由智能体或代码生成 不保留 保留 保留主要风险 成本过高、数据丢失 无法获取部分内容 事实过时
    上下文窗口 检索机制 持久状态
    作用 存储当前请求中的所有内容 查询大型共享语料库 存储智能体记录的少量事实
    存储内容 本次请求中的所有信息 庞大的共享语料库 智能体或代码记录的简短事实
    生成方式
    重启后是否保留

    最重要的那一行是关于重试的那一行。它标志着普通聊天与具备记忆功能的智能体之间的区别。

    它们的工作方式

    真正的智能体会同时使用这三种机制,观察它们在单个请求中的协同工作很有帮助。客服人员会被询问某笔订单是否符合延长的退换货期限。

    1. 持久状态会首先被加载:该客户有企业套餐,且喜欢简短的回复。包含两个事实信息,大约四十个标记。
    2. 检索功能会根据问题提取企业套餐的退换货政策内容以及最新的两条例外规则。共三个部分,约一千二百个标记。
    3. 上下文窗口会整合以上内容,再加上实时聊天记录和工具定义,让模型对整个情况进行综合分析。
  • 回复之后,智能体会在持久状态中写入一行信息:由于用户在8月中旬询问过退货政策,因此下一次对话时不会将其视为首次来电的用户。
  • 一个请求,三层结构。窗口是所有信息的最终归宿,而检索与持久状态则是通往它的两条独立路径。

    这也正是为什么通常建议长时间运行的智能体将内存功能与上下文编辑结合使用:旧的工具处理结果会被从窗口中清除以保持界面简洁,而需要保留的事实早已被保存下来,这样就能在不丢失知识的前提下保持窗口的整洁。

    你实际上需要哪一个?

    初学者几乎总是首先选择向量数据库。它是整个技术栈中最显而易见的部分,却往往并非解决问题的关键。不如逐步深入学习吧。

    从窗口大小开始考虑。所有相关内容是否都能在单个请求中以你接受的价格处理完毕?如果是,那就停止。无需索引,也无需存储。

    在代理程序第二次运行时引入持久化状态。当第二个会话需要了解第一个会话所学的内容时,就需要第三层机制,而JSON文件正是理想的解决方案。应在数据检索之前完成这一操作,这样更便宜、更简单,也符合大多数人在希望代理程序记住他们信息时的真实需求。

    当语料库规模超过窗口容量时再引入检索功能。这意味着有大量文档需要在用户之间共享,并且要独立于你的代码进行编辑。这是构建和维持运行最为复杂的层面,因此只有在确实有必要时才采用它。

    需要避免的一个陷阱是:为实际上只是偏好设置文件的资料构建 RAG 流水线,却还困惑于为什么智能体仍然会忘记某人的名字。检索功能是在语料库中查找信息,而语料库里根本不可能存储某人更喜欢公制单位这样的信息。

    简述版

    • 上下文窗口: 就像办公桌。内容可立即查看,每个标记的处理成本较高,且会随着会话结束而清空。
    • 检索功能: 就像图书馆。规模庞大、可搜索,会按固定时间表自动更新,智能体仅能从中读取信息。
    • 持久化状态: 就像笔记本。容量较小,由智能体自行记录,是唯一能将当天的经验传递到第二天的层次。

    模型本身不会保留任何信息。所谓的“记忆”其实是一种工程上的选择,每次接收请求时都需要重新决定:哪些内容应该放入这个窗口,又应从何处获取?

    如需进一步阅读,Anthropic 关于高效上下文构建及有效智能体开发的工程技术文章,以及其关于记忆工具和上下文编辑的文档,能更深入地探讨这些内容。