AI Agent Harness Engineering 记忆管理:短期缓存+长期RAG融合,解决上下文丢失问题

1. 引入与连接

1.1 从一个令人困扰的场景开始

想象一下,你正在与一个AI助手进行一场深入的技术讨论。你们已经聊了20分钟,探讨了复杂的系统架构设计,你刚刚分享了一个关键的创新点,希望AI能基于前面的讨论给出深入分析。然而,AI的回应却让你大失所望:它似乎完全忘记了你们刚才讨论的内容,给出的建议与之前的对话毫无关联。

这种"健忘症"在今天的AI应用中太常见了。无论是智能客服、编程助手还是个人AI伙伴,我们都或多或少经历过这种令人沮丧的上下文丢失问题。当对话变得复杂、交互次数增多时,AI似乎就像一个注意力短暂的学生,无法记住之前讨论的关键信息。

那么,为什么会出现这种情况?我们能否构建一个拥有更好"记忆力"的AI系统?这正是本文要探讨的核心问题——AI Agent的记忆管理系统,特别是如何通过短期缓存与长期RAG(检索增强生成)的融合,有效解决上下文丢失问题。

1.2 与你的知识建立连接

如果你曾经使用过任何现代AI应用,你已经对这个问题有了直观的体验。但让我们从更技术的角度来建立连接:

  • 如果你熟悉计算机体系结构,你会发现这就像CPU的缓存层次结构——L1、L2缓存速度快但容量小,内存和硬盘容量大但速度慢。AI的记忆系统也面临类似的权衡。

  • 如果你有机器学习背景,你知道Transformer模型的上下文窗口是有限的(如GPT-4的8K或32K token),这就是为什么长对话会导致信息丢失的物理限制。

  • 如果你从事过软件开发,你可能使用过Redis等缓存系统和数据库的组合来优化应用性能——这与我们将要讨论的短期缓存+长期RAG架构有着惊人的相似之处。

在这篇文章中,我们将借鉴这些领域的思想,构建一个高效的AI Agent记忆管理系统。

1.3 为什么这很重要:学习价值与应用场景

解决AI的记忆问题不仅仅是为了让对话更流畅,它实际上开启了一系列新的可能性:

  1. 持久化的个人助手:一个能够记住你所有偏好、历史对话和专业知识的AI助手,可以提供真正个性化的服务。

  2. 连续的问题解决:对于复杂任务,如软件开发、研究分析或战略规划,AI需要能够跟踪长时间线的进展和决策。

  3. 知识积累与进化:能够从交互中持续学习和积累知识的AI系统,会随着时间变得越来越智能和有用。

  4. 专业领域应用:在医疗、法律、金融等专业领域,AI需要能够记住大量的历史案例、专业知识和上下文信息,才能提供有价值的辅助。

我们不仅要解决当前的问题,还要为下一代更智能的AI应用奠定基础。

1.4 我们的学习路径概览

在接下来的内容中,我们将按照以下路径逐步深入:

  1. 概念地图:首先建立整体认知框架,了解AI Agent记忆系统的关键概念和组成部分。

  2. 基础理解:从直观角度理解短期记忆和长期记忆的作用,以及它们各自的局限性。

  3. 层层深入:逐步探索记忆系统的工作原理、技术细节和实现机制。

  4. 多维透视:从历史、实践、批判和未来多个角度审视这个问题。

  5. 实践转化:提供具体的实现方案、代码示例和最佳实践。

  6. 整合提升:总结核心观点,构建完整的知识体系,并指出进一步学习的方向。

让我们开始这段探索之旅,一起构建更智能、更有"记忆力"的AI系统。

2. 概念地图:建立整体认知框架

在深入技术细节之前,让我们先构建一个整体的概念地图,了解AI Agent记忆系统的核心概念、它们之间的关系以及在更大的AI生态系统中的位置。

2.1 核心概念与关键术语

首先,让我们定义一些在本文中会反复使用的关键术语:

  1. AI Agent (智能代理):一个能够感知环境、做出决策并采取行动的自主系统。在本文中,我们主要关注与人类进行交互的对话式AI Agent。

  2. 上下文窗口 (Context Window):Transformer模型一次能够处理的最大token数量。这是模型"注意力"能够覆盖的范围。

  3. 短期记忆 (Short-term Memory):也叫工作记忆,是AI Agent在当前交互中能够即时访问的信息,类似于人类的短期记忆。

  4. 长期记忆 (Long-term Memory):AI Agent能够长期存储和检索的信息,类似于人类的长期记忆。

  5. 缓存 (Caching):一种存储机制,用于临时保存频繁访问的数据,以便更快地检索。

  6. RAG (Retrieval-Augmented Generation,检索增强生成):一种将信息检索与文本生成相结合的技术,用于增强AI模型的知识和上下文感知能力。

  7. 向量数据库 (Vector Database):一种专门用于存储和检索高维向量的数据库,常用于语义相似性搜索。

  8. 嵌入 (Embedding):将文本等数据转换为高维向量的过程,这些向量能够捕捉数据的语义含义。

  9. 记忆检索 (Memory Retrieval):从存储系统中查找和获取相关记忆的过程。

  10. 记忆更新 (Memory Update):将新信息添加到记忆系统中的过程。

这些概念构成了我们讨论的基础,接下来我们将看到它们如何相互关联,形成一个完整的记忆管理系统。

2.2 概念间的层次与关系

AI Agent的记忆系统不是一个单一的组件,而是一个多层次、相互关联的复杂系统。让我们从不同维度来理解这些概念间的关系:

2.2.1 时间维度:从短期到长期

从时间维度看,记忆可以按其持续时间和访问频率组织成一个层次结构:

  1. 即时上下文:当前正在处理的信息,直接在模型的上下文窗口中。
  2. 短期缓存:最近的交互历史,存储在高速缓存系统中。
  3. 工作记忆:当前任务相关的关键信息,可能从短期缓存或长期记忆中提取。
  4. 长期记忆:所有过去的交互和学到的知识,存储在持久化存储系统中。

这个层次结构类似于计算机的存储层次:寄存器 → L1缓存 → L2缓存 → 内存 → 硬盘。每一层都有不同的容量、速度和持久性。

2.2.2 功能维度:从存储到应用

从功能维度看,记忆系统包括以下几个关键环节:

  1. 感知与编码:将输入信息转换为适合存储的形式。
  2. 存储与组织:将编码后的信息存储在适当的位置,并建立索引。
  3. 检索与激活:根据当前上下文,找到并激活相关的记忆。
  4. 整合与生成:将检索到的记忆与当前输入整合,生成响应。
  5. 学习与更新:根据交互结果更新记忆系统。

这些功能形成了一个完整的循环,确保记忆系统能够持续学习和适应。

2.2.3 技术维度:从基础组件到完整系统

从技术实现的角度,记忆系统由以下组件构成:

  1. 嵌入模型:将文本转换为向量表示。
  2. 向量数据库:存储和检索向量表示的记忆。
  3. 缓存系统:提供高速访问的短期存储。
  4. 检索引擎:实现各种检索策略的算法组件。
  5. 记忆管理器:协调各个组件,实现整体记忆功能。

这些技术组件需要精心设计和集成,才能构建一个高效、可靠的记忆系统。

2.3 学科定位与边界

AI Agent的记忆管理是一个跨学科领域,它融合了以下多个学科的思想和技术:

  1. 认知科学:借鉴人类记忆的模型和理论,如Atkinson-Shiffrin记忆模型。
  2. 自然语言处理:提供文本处理、语义理解和生成的技术。
  3. 信息检索:贡献了高效的索引和检索算法。
  4. 数据库系统:提供了数据存储、管理和查询的基础设施。
  5. 机器学习:特别是深度学习,提供了嵌入模型和表示学习的技术。
  6. 系统设计:提供了构建复杂软件系统的方法论。

同时,我们也需要明确这个领域的边界:

  • 它不是关于"意识"或"主观体验"的哲学讨论,而是关于工程实现的技术探讨。
  • 它不追求模拟人类记忆的所有复杂性,而是专注于解决实际应用中的上下文丢失问题。
  • 它是AI系统的一个组件,而不是完整的AI系统本身,需要与其他组件如规划、推理等协同工作。

2.4 概念图谱

为了更直观地展示这些概念之间的关系,让我们构建一个概念图谱:

核心功能

支撑技术

记忆系统

AI Agent 系统

AI Agent

感知模块

推理模块

记忆系统

行动模块

短期记忆/缓存

长期记忆/RAG

最近交互

当前工作集

语义记忆

情景记忆

缓存技术
Redis/Memcached

向量数据库
Pinecone/Weaviate

嵌入模型
OpenAI/Cohere

检索算法
相似度搜索/混合搜索

记忆管理器

编码

存储

检索

更新

上下文丢失问题

短期缓存+长期RAG融合

这个概念图谱展示了记忆系统在AI Agent中的位置,其内部组成,支撑技术,以及核心功能。它为我们后续的讨论提供了一个清晰的路线图。

在接下来的章节中,我们将深入探讨这个图谱中的每个组件,特别是如何通过短期缓存和长期RAG的融合来解决上下文丢失问题。

3. 基础理解:建立直观认识

现在我们已经有了整体概念框架,让我们从最直观的角度来理解AI Agent的记忆问题,以及短期缓存和长期RAG如何协同工作来解决这个问题。

3.1 核心概念的生活化解释

让我们先把技术放在一边,用一些日常生活中的类比来理解这些概念。

3.1.1 AI的"注意力"与"记忆":一场鸡尾酒会对话

想象你在一个热闹的鸡尾酒会上,正在与几个人同时交谈。你的大脑需要:

  1. 关注当前说话的人(处理当前输入)
  2. 记住刚才你们讨论的内容(短期记忆)
  3. 回忆起你对这个话题已有的知识(长期记忆)
  4. 可能还需要记住房间里其他人在说什么,以防有人叫你的名字(更广泛的上下文)

现在,想象你的注意力有一个严格的限制:你只能专注于最近2-3分钟内的对话内容。任何超过这个时间范围的信息,你就会完全忘记。这就是今天大多数AI模型的处境——它们的"注意力"被限制在一个固定大小的上下文窗口内。

当对话变长时,AI就像一个只有短暂注意力的鸡尾酒会参与者,无法记住之前讨论的关键内容。这就是我们要解决的上下文丢失问题。

3.1.2 短期记忆就像你的办公桌,长期记忆就像你的书房

让我们继续用办公场景来类比:

  • 上下文窗口就像你手里正在拿着和处理的几张纸。你可以立即访问和处理这些信息,但数量非常有限。
  • 短期缓存就像你的办公桌。你可以把当前项目相关的文件、笔记和参考资料放在桌上,方便快速取用。桌面空间有限,所以你只能放最相关的材料。
  • 长期RAG记忆就像你的书房或图书馆。这里可以存储大量的书籍、文件和笔记,但检索需要一些时间和 effort。

当你工作时,你会:

  1. 首先看手里的纸张(上下文窗口)
  2. 如果需要更多相关信息,从办公桌上拿(短期缓存)
  3. 如果还需要更深入或更早的信息,去书房查找(长期RAG)
  4. 完成工作后,把重要的结果整理好,放回书房,同时更新办公桌上的材料(记忆更新)

这个类比很好地捕捉了我们记忆系统的工作原理:不同的存储层次有不同的容量和访问速度,我们需要智能地管理信息在这些层次之间的流动。

3.1.3 RAG就像给AI配了一个研究助理

你可能听说过RAG(检索增强生成)这个术语,让我们用一个简单的类比来理解它:

想象你是一个作家,正在写一本关于某个历史事件的书。你的大脑里有一些关于这个事件的基本知识,但不够详细和准确。你可以:

  1. 自己凭空想象所有细节(这就是没有RAG的生成模型)
  2. 或者,雇一个研究助理,当你需要某个具体信息时,让他去图书馆查资料,然后把相关信息带给你(这就是RAG)

在这个类比中:

  • 你就是AI的生成模型
  • 研究助理就是检索系统
  • 图书馆就是我们的知识库/向量数据库

RAG不是让AI"记住"所有东西,而是给它提供了一个高效的"查阅"机制,让它在需要时能够找到相关信息。

3.2 简化模型与类比

现在让我们把这些类比结合起来,构建一个简化的记忆系统模型。

3.2.1 记忆的三层楼模型

让我们想象一个三层楼的建筑,每一层代表记忆系统的一个层次:

  1. 一楼(前台大厅):上下文窗口

    • 这是访客(当前输入)首先到达的地方
    • 空间有限,只能容纳少数人(少量token)
    • 接待员(模型)可以立即与这里的所有人互动
  2. 二楼(会议室):短期缓存

    • 这是刚刚离开一楼但可能很快需要再次交流的人等待的地方
    • 空间比一楼大,但仍然有限
    • 接待员可以快速召唤这里的人到一楼
  3. 三楼(档案室):长期RAG记忆

    • 这是存储所有过去访客记录和重要文件的地方
    • 空间几乎无限
    • 但需要通过索引和检索系统才能找到需要的信息
    • 找到后,可以把相关信息送到一楼或二楼

这个模型展示了信息如何在不同层次之间流动,以及我们如何根据需要访问不同层次的记忆。

3.2.2 信息流动的水管模型

另一个有用的类比是将记忆系统想象成一个水管系统:

  • 输入是流入系统的水(新信息)
  • 过滤器决定哪些水应该直接送到哪里(记忆分类)
  • 小水箱是短期缓存,存储最近的水(最近的交互)
  • 大水库是长期记忆,存储所有的水(所有历史信息)
  • 水泵是检索系统,根据需要从水库中抽水(检索相关记忆)
  • 混合器将来自不同来源的水混合在一起(整合不同层次的记忆)
  • 输出是最终流出系统的水(生成的响应)

这个模型帮助我们理解记忆系统的动态性和信息的流动路径。

3.3 直观示例与案例

让我们通过一个具体的例子来看看这个系统是如何工作的。

3.3.1 一个编程助手的对话

假设你正在使用一个AI编程助手开发一个电商网站,下面是你们的对话:

:我需要创建一个Python类来管理购物车,帮我设计一下。

AI:好的,我来帮你设计一个购物车类。让我先创建一个基本结构…(提供了一个基本的ShoppingCart类)

:很好,现在我需要添加一个方法来计算总价格,包括税费。假设税率是8%。

AI:没问题,我来添加一个计算总价的方法…(更新了类,添加了calculate_total方法,硬编码了8%的税率)

:实际上,税率应该是可配置的,因为我们的电商网站要服务不同地区的用户。

AI:明白了,让我修改一下,使税率可配置…(进一步更新了类)

:现在我们还需要考虑折扣,用户可以应用折扣码。让我们添加这个功能。

现在,如果没有良好的记忆管理,AI可能会:

  1. 忘记之前我们讨论的购物车类的结构
  2. 忘记我们刚刚决定让税率可配置
  3. 重新发明轮子,而不是在现有代码基础上构建

但有了我们的记忆系统:

  1. 上下文窗口中包含了你最后的请求(添加折扣功能)和AI的上一个响应。
  2. 短期缓存中存储了整个对话历史,特别是关于ShoppingCart类的所有设计决策。
  3. 如果需要,长期RAG还可以检索过去类似项目的代码模式、最佳实践等。

AI可以利用这些信息,在之前的基础上连贯地添加折扣功能,而不会丢失任何上下文。

3.3.2 一个个人助理的例子

让我们再看一个个人助理的例子:

(1月15日):我对坚果过敏,请记住这一点。

AI:好的,我会记住你对坚果过敏。

(1月20日):帮我订一下明天中午的餐厅,要在市中心。

AI:好的,我来帮你找市中心的餐厅。(推荐了几家,但没有考虑过敏信息)

(有点恼火):我告诉过你我对坚果过敏!

没有记忆管理的AI可能真的忘记了过敏信息。但有了我们的系统:

  1. 短期缓存可能还存储着最近几天的交互,但如果1月15日的对话已经过去了几天,可能已经不在缓存中了。
  2. 但是,长期RAG会存储这条重要信息,并在你提到"餐厅"或"食物"时,自动检索出过敏信息。
  3. 记忆系统会将这条信息添加到上下文中,确保AI在推荐餐厅时考虑到你的过敏情况。

这个例子展示了长期记忆的重要性,特别是对于那些不常使用但关键时刻不能忘记的信息。

3.4 常见误解澄清

在我们深入探讨之前,让我们澄清一些关于AI记忆的常见误解:

误解1:“更大的上下文窗口就能解决所有问题”

虽然更大的上下文窗口确实有帮助(比如GPT-4的32K或100K token版本),但它不是万能药:

  • 即使是100K token,也只能容纳大约7-8万字的文本,对于长期交互或大量知识仍然不够。
  • 更大的上下文窗口意味着更高的计算成本和延迟。
  • 模型的"注意力"并不是均匀分布的,它可能更关注最近的token,而忽略较早的token。

这就像仅仅通过增大办公桌来解决存储问题——最终你还是需要一个书房。

误解2:“RAG就是把所有东西都存起来,需要的时候全部拿出来”

有效的RAG不仅仅是存储和检索,它需要:

  • 智能地分块和索引信息,而不是简单地存储整个文档。
  • 评估相关性,只检索最相关的信息,而不是所有相关信息。
  • 组织和呈现检索到的信息,使其对生成模型最有用。

这就像一个好的研究助理,不仅仅是给你一堆书,而是帮你找到最相关的章节,甚至总结关键点。

误解3:“记忆系统会让AI’知道’所有事情”

记忆系统增强了AI的能力,但它并没有赋予AI真正的"理解"或"意识":

  • AI仍然只是模式匹配机器,它不"知道"它在说什么。
  • 记忆系统可能会检索到错误或不相关的信息,导致AI产生错误的响应。
  • 没有完美的检索系统,总会有相关信息被遗漏的情况。

理解这些局限性有助于我们设计更健壮的系统,并合理设定用户期望。

通过这些生活化的类比、简化模型和直观示例,你应该已经对AI Agent的记忆管理有了基本的直观理解。在接下来的章节中,我们将深入探讨技术细节,了解如何实际构建这样一个系统。

4. 层层深入:逐步增加复杂度

在建立了直观理解之后,现在让我们逐步深入,探索记忆系统的技术细节、工作原理和实现机制。我们将从基本原理开始,逐步增加复杂度,直到掌握底层逻辑和高级应用。

4.1 第一层:基本原理与运作机制

让我们从最基本的原理开始,了解短期缓存和长期RAG是如何工作的,以及它们如何协同解决上下文丢失问题。

4.1.1 短期缓存的基本原理

短期缓存是我们记忆系统的第一层,它的主要目标是快速访问最近的交互历史。

什么是短期缓存?

短期缓存可以看作是对话历史的一个滑动窗口,但比模型的原生上下文窗口更大,同时更智能地管理内容。它的核心思想是:

  1. 保留最近的交互,因为它们最可能与当前任务相关。
  2. 提供比原生上下文窗口更大的容量,但仍然保持快速访问。
  3. 可以应用一些智能策略来决定保留什么、丢弃什么。
短期缓存的基本数据结构

最简单的短期缓存可以用一个先进先出(FIFO)队列实现:

class SimpleShortTermCache:
    def __init__(self, max_size=10):
        self.max_size = max_size
        self.cache = []
    
    def add(self, item):
        self.cache.append(item)
        if len(self.cache) > self.max_size:
            self.cache.pop(0)  # 移除最旧的项
    
    def get_all(self):
        return self.cache

这个简单实现有几个问题:

  1. 它只考虑了时间因素,没有考虑重要性。
  2. 它没有对内容进行任何处理或总结。
  3. 它没有与长期记忆交互。

尽管如此,它展示了短期缓存的基本思想。在后面的章节中,我们将改进这个实现。

为什么需要短期缓存?

你可能会问:既然模型已经有了上下文窗口,为什么还需要额外的短期缓存?

  1. 成本效率:将所有内容都放在模型的上下文窗口中可能非常昂贵。短期缓存可以让我们智能地选择最相关的内容放入上下文窗口。
  2. 灵活性:我们可以在短期缓存中应用各种策略(如总结、重要性排序),而这些策略在模型的原生上下文窗口中不容易实现。
  3. 桥接作用:短期缓存可以作为原生上下文窗口和长期记忆之间的桥梁,管理信息在两者之间的流动。
4.1.2 长期RAG的基本原理

长期RAG是我们记忆系统的第二层,它的主要目标是持久化存储和智能检索大量信息。

什么是RAG?

RAG(检索增强生成)是一种结合信息检索和文本生成的技术。它的基本流程是:

  1. 索引阶段:将文档分割成小块,转换为向量表示,存储在向量数据库中。
  2. 检索阶段:当用户提出问题时,将问题也转换为向量,在向量数据库中搜索最相似的文档块。
  3. 生成阶段:将检索到的文档块作为上下文,与用户的问题一起提供给生成模型,生成最终答案。

这个流程可以用以下简化代码表示:

class SimpleRAG:
    def __init__(self, embedder, vector_db, generator):
        self.embedder = embedder
        self.vector_db = vector_db
        self.generator = generator
    
    def index(self, documents):
        # 将文档分割成块
        chunks = self._split_documents(documents)
        # 生成向量表示
        vectors = self.embedder.embed(chunks)
        # 存储到向量数据库
        self.vector_db.store(chunks, vectors)
    
    def query(self, question):
        # 生成问题的向量表示
        question_vector = self.embedder.embed(question)
        # 检索相关文档块
        relevant_chunks = self.vector_db.search(question_vector, top_k=5)
        # 构建提示
        prompt = self._build_prompt(question, relevant_chunks)
        # 生成答案
        answer = self.generator.generate(prompt)
        return answer

这个简化版本省略了很多细节,但展示了RAG的核心流程。

向量表示与相似度搜索

RAG的核心是向量表示和相似度搜索。让我们更详细地了解这两个概念:

向量表示(嵌入)

  • 嵌入模型将文本转换为高维向量(通常是几百或几千维)。
  • 这些向量的神奇之处在于:语义相似的文本在向量空间中也更接近。
  • 例如,"猫"和"猫咪"的向量会很接近,而"猫"和"汽车"的向量会相距较远。

相似度搜索

  • 有了向量表示,我们可以通过计算向量之间的距离来衡量文本的语义相似度。
  • 常用的距离度量包括余弦相似度、欧氏距离等。
  • 向量数据库专门优化了这种相似度搜索,即使在数十亿向量的规模下也能快速返回结果。

用数学公式表示,两个向量 a⃗\vec{a}a b⃗\vec{b}b 之间的余弦相似度为:

cosine similarity(a⃗,b⃗)=a⃗⋅b⃗∥a⃗∥∥b⃗∥ \text{cosine similarity}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{\|\vec{a}\| \|\vec{b}\|} cosine similarity(a ,b )=a ∥∥b a b

其中 a⃗⋅b⃗\vec{a} \cdot \vec{b}a b 是向量点积,∥a⃗∥\|\vec{a}\|a 是向量 a⃗\vec{a}a 的模长。

为什么需要长期RAG?

长期RAG解决了以下几个关键问题:

  1. 知识截止日期:训练模型有知识截止日期,无法获取之后的新信息。RAG可以通过更新索引来添加新知识。
  2. 领域特定知识:通用模型可能缺乏特定领域的专业知识,RAG可以用领域特定文档来增强模型。
  3. 可解释性:RAG可以引用来源,让我们知道模型的答案来自哪里,增加可信度。
  4. 成本效益:与微调相比,RAG通常更便宜、更灵活,不需要重新训练模型。
4.1.3 两者如何协同工作

现在我们了解了短期缓存和长期RAG的基本原理,让我们看看它们如何协同工作,形成一个完整的记忆系统。

记忆系统的基本工作流程

一个完整的记忆系统工作流程如下:

  1. 接收输入:用户的问题或消息到达系统。
  2. 查询短期缓存:首先检查短期缓存,获取最近的交互历史。
  3. 查询长期记忆:同时,使用当前输入和短期缓存中的相关信息作为查询,检索长期记忆。
  4. 整合上下文:将当前输入、短期缓存中的相关内容和长期记忆中的相关信息整合在一起。
  5. 生成响应:将整合后的上下文提供给生成模型,生成响应。
  6. 更新记忆:将新的交互添加到短期缓存,同时考虑是否需要更新长期记忆。

这个流程可以用以下流程图表示:

接收用户输入

查询短期缓存

构建检索查询

检索长期记忆

整合上下文

生成响应

更新短期缓存

是否需要更新长期记忆?

更新长期记忆

完成

简单的协同示例

让我们通过一个简单的例子来看看这个流程如何工作:

假设你正在与一个AI助手讨论旅行计划:

:我正在计划去日本的旅行,你能推荐一些东京的景点吗?

系统处理

  1. 短期缓存是空的,所以没有最近的历史。
  2. 用"日本旅行"、"东京景点"等关键词检索长期记忆,找到关于东京景点的信息。
  3. 生成关于东京景点的推荐。
  4. 将这次交互添加到短期缓存。
  5. 考虑是否需要将你的旅行计划添加到长期记忆(在这个例子中,可能先不添加)。

AI:当然!东京有很多很棒的景点,包括浅草寺、东京塔、涩谷十字路口、上野公园等等。你对什么类型的景点特别感兴趣?

:我对历史文化景点更感兴趣,而且我只有两天时间在东京。

系统处理

  1. 从短期缓存中获取上一次交互(推荐东京景点,询问兴趣)。
  2. 用"东京历史文化景点"、"两天东京行程"等关键词检索长期记忆。
  3. 整合上下文:用户计划日本旅行,对历史文化景点感兴趣,只有两天时间。
  4. 生成一个针对历史文化景点的两天行程建议。
  5. 将这次交互添加到短期缓存。
  6. 现在可能需要将你的旅行偏好(历史文化景点,两天行程)添加到长期记忆。

这个例子展示了短期缓存和长期RAG如何协同工作,提供连贯、上下文感知的响应。

4.2 第二层:细节、例外与特殊情况

现在我们了解了基本原理,让我们深入探讨一些更复杂的细节、例外情况和特殊场景。

4.2.1 短期缓存的高级策略

简单的FIFO队列对于短期缓存来说往往不够,我们需要更智能的策略。

内容总结策略

随着对话变长,即使是短期缓存也可能变得太大,无法全部放入模型的上下文窗口。一个解决方案是对旧的交互进行总结:

class SummarizingCache:
    def __init__(self, max_items=10, summarizer=None):
        self.max_items = max_items
        self.summarizer = summarizer
        self.cache = []
        self.summaries = []
    
    def add(self, item):
        self.cache.append(item)
        # 当缓存超过大小时,总结最早的一部分
        if len(self.cache) > self.max_items and self.summarizer:
            # 总结前半部分
            to_summarize = self.cache[:len(self.cache)//2]
            summary = self.summarizer.summarize(to_summarize)
            self.summaries.append(summary)
            # 保留后半部分
            self.cache = self.cache[len(self.cache)//2:]
    
    def get_context(self):
        # 组合总结和当前缓存
        context = []
        if self.summaries:
            context.append("对话摘要:\n" + "\n".join(self.summaries))
        context.extend(self.cache)
        return context

这种渐进式总结策略允许我们保留更多的历史信息,同时控制上下文窗口的大小。

重要性评分策略

另一个策略是为每个交互分配重要性分数,保留重要的,丢弃不重要的:

class ImportanceScoredCache:
    def __init__(self, max_size=10, scorer=None):
        self.max_size = max_size
        self.scorer = scorer
        self.cache = []  # 存储元组 (item, score, timestamp)
    
    def add(self, item):
        score = self.scorer.score(item) if self.scorer else 1.0
        timestamp = time.time()
        self.cache.append((item, score, timestamp))
        
        # 如果超过最大大小,删除得分最低的
        if len(self.cache) > self.max_size:
            # 综合考虑得分和时间衰减
            def decay_score(item):
                it_score, it_time = item[1], item[2]
                # 时间衰减因子:每小时衰减一半
                hours_passed = (time.time() - it_time) / 3600
                decay_factor = 0.5 ** hours_passed
                return it_score * decay_factor
            
            # 按衰减后的分数排序
            self.cache.sort(key=decay_score, reverse=True)
            # 保留前max_size个
            self.cache = self.cache[:self.max_size]

重要性评分可以考虑多个因素:

  • 用户明确标记为重要的内容
  • 包含实体(如人名、地名、产品名)的内容
  • 用户反复提及的内容
  • 情感强烈的内容
对话线程分离

如果对话涉及多个不同的主题,一个更好的策略是将对话分离成不同的线程:

class ThreadedCache:
    def __init__(self, max_threads=5, max_per_thread=10):
        self.max_threads = max_threads
        self.max_per_thread = max_per_thread
        self.threads = {}  # thread_id -> list of items
        self.current_thread = None
        self.thread_timestamps = {}  # thread_id -> last_access_time
    
    def add(self, item, thread_id=None):
        # 如果没有指定线程,尝试分类到现有线程或创建新线程
        if not thread_id:
            thread_id = self._classify_to_thread(item)
        
        if thread_id not in self.threads:
            # 如果超过最大线程数,删除最久未使用的
            if len(self.threads) >= self.max_threads:
                oldest_thread = min(self.thread_timestamps, key=self.thread_timestamps.get)
                del self.threads[oldest_thread]
                del self.thread_timestamps[oldest_thread]
            
            self.threads[thread_id] = []
        
        # 添加到线程
        self.threads[thread_id].append(item)
        if len(self.threads[thread_id]) > self.max_per_thread:
            self.threads[thread_id].pop(0)
        
        # 更新时间戳
        self.thread_timestamps[thread_id] = time.time()
        self.current_thread = thread_id
    
    def get_context(self):
        # 返回当前线程的内容,以及其他线程的简要摘要
        context = []
        if self.current_thread and self.current_thread in self.threads:
            context.append("当前话题:\n" + "\n".join(self.threads[self.current_thread]))
        
        other_threads = [tid for tid in self.threads if tid != self.current_thread]
        if other_threads:
            context.append("\n其他相关话题:")
            for tid in other_threads[:3]:  # 只显示最多3个其他话题
                if self.threads[tid]:
                    # 只显示每个线程的最后一条消息
                    context.append(f"- {self.threads[tid][-1][:50]}...")
        
        return "\n".join(context)

这种多线程方法对于复杂的多主题对话特别有用,它可以帮助AI保持不同话题的上下文分离,同时仍然能够利用其他话题的相关信息。

4.2.2 长期RAG的高级技术

基本的RAG系统对于简单场景可能足够,但对于更复杂的应用,我们需要一些高级技术。

分块策略

如何将文档分割成块是RAG系统性能的关键因素。不好的分块策略可能会导致:

  • 重要信息被切断
  • 检索到不完整的上下文
  • 噪音过多

让我们看看几种不同的分块策略:

固定大小分块

def fixed_size_chunking(text, chunk_size=1000, overlap=100):
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        start = end - overlap  # 重叠一部分,避免信息被切断
    return chunks

这是最简单的方法,但可能会在句子或段落中间切断。

语义感知分块

import nltk
from nltk.tokenize import sent_tokenize

nltk.download('punkt')

def semantic_chunking(text, max_chunk_size=1000):
    sentences = sent_tokenize(text)
    chunks = []
    current_chunk = []
    current_size = 0
    
    for sentence in sentences:
        sentence_size = len(sentence)
        
        # 如果当前句子超过最大块大小,需要特殊处理
        if sentence_size > max_chunk_size:
            # 如果当前有内容,先保存
            if current_chunk:
                chunks.append(" ".join(current_chunk))
                current_chunk = []
                current_size = 0
            
            # 对长句子进行子分块
            words = sentence.split()
            sub_chunk = []
            sub_size = 0
            for word in words:
                if sub_size + len(word) + 1 > max_chunk_size:  # +1 for space
                    chunks.append(" ".join(sub_chunk))
                    sub_chunk = [word]
                    sub_size = len(word)
                else:
                    sub_chunk.append(word)
                    sub_size += len(word) + 1
            
            if sub_chunk:
                chunks.append(" ".join(sub_chunk))
        
        # 如果添加当前句子会超过最大块大小
        elif current_size + sentence_size + 1 > max_chunk_size:  # +1 for space
            chunks.append(" ".join(current_chunk))
            current_chunk = [sentence]
            current_size = sentence_size
        
        # 否则添加到当前块
        else:
            current_chunk.append(sentence)
            current_size += sentence_size + 1  # +1 for space
    
    # 添加最后一个块
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    
    return chunks

这种方法尝试在语义边界(如句子、段落)处分割,保持语义完整性。

递归分块
更高级的方法是递归地将文档分块,创建一个层次结构:

  • 第一层:整个文档
  • 第二层:章节
  • 第三层:段落
  • 第四层:句子

在检索时,可以在不同层次上搜索,找到最相关的信息粒度。

混合检索策略

仅仅依赖向量相似度搜索有时是不够的,我们可以使用混合检索策略,结合多种搜索方法:

class HybridRetriever:
    def __init__(self, vector_db, keyword_db, reranker=None):
        self.vector_db = vector_db
        self.keyword_db = keyword_db
        self.reranker = reranker
    
    def retrieve(self, query, top_k=10, vector_weight=0.5, keyword_weight=0.5):
        # 向量检索
        vector_results = self.vector_db.search(query, top_k=top_k*2)
        # 关键词检索
        keyword_results = self.keyword_db.search(query, top_k=top_k*2)
        
        # 合并结果,去除重复
        all_results = {}
        for doc, score in vector_results:
            all_results[doc.id] = {'doc': doc, 'vector_score': score, 'keyword_score': 0}
        
        for doc, score in keyword_results:
            if doc.id in all_results:
                all_results[doc.id]['keyword_score'] = score
            else:
                all_results[doc.id] = {'doc': doc, 'vector_score': 0, 'keyword_score': score}
        
        # 计算混合分数
        for doc_id, result in all_results.items():
            # 归一化分数(简化处理)
            norm_vector = result['vector_score'] / max(r[0] for r in vector_results) if vector_results else 0
            norm_keyword = result['keyword_score'] / max(r[0] for r in keyword_results) if keyword_results else 0
            
            result['hybrid_score'] = (vector_weight * norm_vector + 
                                      keyword_weight * norm_keyword)
        
        # 排序
        sorted_results = sorted(all_results.values(), 
                               key=lambda x: x['hybrid_score'], 
                               reverse=True)
        
        # 如果有重排序器,使用它重新排序
        if self.reranker:
            top_candidates = [r['doc'] for r in sorted_results[:top_k*2]]
            reranked = self.reranker.rerank(query, top_candidates)
            return reranked[:top_k]
        
        return [(r['doc'], r['hybrid_score']) for r in sorted_results[:top_k]]

混合检索结合了:

  • 向量搜索:捕获语义相似性
  • 关键词搜索:精确匹配重要术语
  • 重排序:使用更精细的模型对初始搜索结果重新排序
查询扩展与转换

有时用户的查询不够明确或不够优化,我们可以对查询进行扩展和转换:

class QueryProcessor:
    def __init__(self, llm):
        self.llm = llm
    
    def expand_query(self, original_query):
        # 使用LLM生成多个相关查询
        prompt = f"""原始查询: {original_query}

请生成3-5个与原始查询相关的搜索查询,从不同角度表达相同的信息需求。
只返回查询,每行一个,不要编号。"""
        
        response = self.llm.generate(prompt)
        expanded_queries = [line.strip() for line in response.split('\n') if line.strip()]
        expanded_queries.insert(0, original_query)  # 包含原始查询
        
        return expanded_queries
    
    def decompose_complex_query(self, complex_query):
        # 分解复杂查询为多个子查询
        prompt = f"""复杂查询: {complex_query}

这个查询是否可以分解为多个更简单的子查询?如果可以,请将其分解为2-4个子查询。
每个子查询应该独立且能覆盖原查询的一部分。
只返回子查询,每行一个,不要编号。如果不能分解,只返回原查询。"""
        
        response = self.llm.generate(prompt)
        subqueries = [line.strip() for line in response.split('\n') if line.strip()]
        
        return subqueries if subqueries else [complex_query]
    
    def transform_query(self, query, context=None):
        # 根据上下文转换查询,使其更明确
        if not context:
            return query
        
        prompt = f"""对话历史:
{context}

当前用户查询: {query}

请根据对话历史,将用户的当前查询转换为更明确、更完整的查询。
如果用户使用了代词,请替换为具体的实体。如果查询依赖于上下文信息,请将这些信息包含在转换后的查询中。
只返回转换后的查询。"""
        
        return self.llm.generate(prompt)

查询处理可以显著提高检索质量,特别是对于复杂或依赖上下文的查询。

4.2.3 处理特殊情况

让我们探讨一些特殊情况和如何处理它们。

处理冲突信息

当记忆系统中存在冲突信息时,我们需要一种策略来解决这些冲突:

class ConflictResolver:
    def __init__(self, reliability_scorer=None):
        self.reliability_scorer = reliability_scorer
    
    def resolve_conflicts(self, documents, query=None):
        # 首先,识别冲突的信息
        conflicting_groups = self._identify_conflicts(documents)
        
        resolved_docs = []
        for group in conflicting_groups:
            if len(group) == 1:
                # 没有冲突,直接添加
                resolved_docs.append(group[0])
            else:
                # 有冲突,需要解决
                resolved = self._resolve_group(group, query)
                resolved_docs.append(resolved)
        
        return resolved_docs
    
    def _identify_conflicts(self, documents):
        # 简化实现:按主题分组
        # 实际应用中可能需要更复杂的冲突检测
        topics = {}
        for doc in documents:
            topic = doc.metadata.get('topic', 'default')
            if topic not in topics:
                topics[topic] = []
            topics[topic].append(doc)
        return list(topics.values())
    
    def _resolve_group(self, group, query

更多推荐