在本文中,您将学习如何为长期运行的 AI 代理实现上下文修剪管道,使它们能够通过语义相似性有效地管理会话记忆。
我们将讨论的主题包括:
- 为什么无界对话历史对于构建在大型语言模型之上的代理来说是一个问题,以及上下文修剪策略是什么样的。
- 如何使用句子转换器嵌入模型来计算当前提示和存档对话轮次之间的语义相似度。
- 如何根据最近的回合、过去 K 个语义相关的回合以及当前的提示来组装修剪后的上下文窗口。
为长时间运行的代理构建上下文修剪管道
介绍
建立在大型语言模型 (LLM) 之上的现代人工智能代理旨在连续运行。因此,他们的对话历史会无限增长。将这样一个完整的历史记录作为 LLM 的上下文窗口传递是解决过高的令牌成本、延迟瓶颈和最终推理退化的完美方法。
构建上下文修剪管道可以通过动态管理最近的会话内存来解决这个问题。本文概述了为长时间运行的代理实现上下文修剪管道的基本原则。
我们使用基于开源嵌入模型的完全可访问且免费运行的本地解决方案,而不是付费 API,但如果您想要更高效的解决方案,您可以将其替换为付费 API。
建议的内存策略
代理中的经典记忆策略依赖于滑动窗口,该窗口会在旧信息落后时忘记它,包括潜在的关键细节。超越这种方法,可以建立一个有选择性的、更智能的管道,为法学硕士提供准确的背景信息。
本质上,上下文可以精简为以下基本元素:
- 这 当前提示,包含用户的请求或问题。
- 这 最近回合,即前一个输入-响应交换,这是保持会话连续性的关键。
- 这 前 K 个语义相关匹配,根据相似度分数计算。这些是与当前提示密切相关的过去回合,通过向量嵌入检索。
对话历史记录中超出这三个元素范围的所有内容都会从活动提示的上下文中丢弃,从而节省计算和内存。
基于仿真的实施
我们的示例实现模拟了上述策略的应用,逐步构建上下文修剪窗口。句子转换器模型用于模拟长期运行的管道以及模拟的对话历史记录。
我们首先进行必要的导入:
将numpy导入为np从sentence_transformers导入SentenceTransformer从scipy.spatial.distance导入cosine
|
进口 麻木 作为 NP 从 句子转换器 进口 句子转换器 从 scipy。空间的。距离 进口 余弦 |
接下来,我们加载并初始化一个预训练的嵌入模型——具体来说 all-MiniLM-L6-v2 从 sentence_transformers 图书馆。该模型经过训练,可以将原始文本转换为捕获语义特征的嵌入向量。我们还创建一个简单的模拟代理历史记录,其中包含用户代理交互(在真实设置中,这将从数据库中获取):
# 初始化一个轻量级开源嵌入模型 model = SentenceTransformer(‘all-MiniLM-L6-v2’) # 1. 模拟代理历史记录(通常从数据库中获取) chat_history = [
{“role”: “user”, “content”: “My name is Alice and I work in logistics.”},
{“role”: “agent”, “content”: “Nice to meet you, Alice. How can I help with logistics?”},
{“role”: “user”, “content”: “What’s the weather like today?”},
{“role”: “agent”, “content”: “It’s sunny and 75 degrees.”},
{“role”: “user”, “content”: “I need help calculating route efficiency for my fleet.”},
{“role”: “agent”, “content”: “Route efficiency involves analyzing distance, traffic, and load weight.”},
{“role”: “user”, “content”: “Thanks, that makes sense.”},
{“role”: “agent”, “content”: “You’re welcome! Let me know if you need anything else.”}
]
|
# 初始化一个轻量级开源嵌入模型 模型 = 句子转换器(“全 MiniLM-L6-v2”) # 1. 模拟代理历史记录(通常从数据库获取) 聊天记录 = [ {“role”: “user”, “content”: “My name is Alice and I work in logistics.”}, {“role”: “agent”, “content”: “Nice to meet you, Alice. How can I help with logistics?”}, {“role”: “user”, “content”: “What’s the weather like today?”}, {“role”: “agent”, “content”: “It’s sunny and 75 degrees.”}, {“role”: “user”, “content”: “I need help calculating route efficiency for my fleet.”}, {“role”: “agent”, “content”: “Route efficiency involves analyzing distance, traffic, and load weight.”}, {“role”: “user”, “content”: “Thanks, that makes sense.”}, {“role”: “agent”, “content”: “You’re welcome! Let me know if you need anything else.”} ] |
接下来是上下文修剪管道的核心逻辑。它被封装在一个 prune_context() 接收当前提示、完整交互历史记录以及要检索的语义相关的过去轮数的函数, k:
def prune_context(current_prompt, History, top_k=2): # 如果对话历史记录太短,我们只需返回它 if len(history)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 号 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
定义 剪枝上下文(当前提示符, 历史, 前k个=2): # 如果对话历史记录太短,我们只需返回它 如果 伦(历史) 2: 返回 历史 + [{“role”: “user”, “content”: current_prompt}] # 提取最近的回合(最后一个用户/代理对) 最近的回合 = 历史[–2:]
# 其余的历史记录将有资格进行语义修剪 已存档的回合数 = 历史[:–2]
# 2. 嵌入当前提示符 提示_嵌入 = 模型。编码(当前提示符)
# 3. 嵌入存档回合并计算相似度 得分回合数 = [] 为了 转动 在 已存档的回合数: 转嵌入 = 模型。编码(转动[“content”]) # 我们想要相似性,所以我们用 1 减去余弦距离 相似 = 1 – 余弦(提示_嵌入, 转嵌入) 得分回合数。附加((相似, 转动))
# 4. 按最高相似度排序并切片 Top-K 回合 得分回合数。种类(钥匙=拉姆达 x: x[0], 撤销=真的) 顶部语义转弯 = [turn for score, turn in scored_turns[:top_k]]
# 按时间顺序对语义轮次进行排序(可选,但建议法学硕士使用) 顶部语义转弯。种类(钥匙=拉姆达 x: 已存档的回合数。指数(x)) # 5. 组装最终修剪后的上下文 剪枝上下文 = 顶部语义转弯 + 最近的回合 + [{“role”: “user”, “content”: current_prompt}]
返回 剪枝上下文 |
上面的代码很大程度上是不言自明的。它将逻辑分为基本情况(当对话历史记录仍然太短时,在这种情况下,整个历史记录作为上下文传递)和一般情况,其中实际的语义修剪管道通过几个步骤进行:嵌入过去的回合,计算与当前提示嵌入的余弦相似度,从最高到最低相似度对它们进行排序,并选择前 K 个过去的回合。当前提示、最近的回合和过去 K 个语义相似的回合最终被组装成经过修剪的上下文。
以下示例说明如何获取新提示的上下文,在该提示中用户返回到与车队路线效率相关的方面:
# 模拟执行 current_request = “我们可以回到舰队数学吗?” Optimized_context = prune_context(current_request, chat_history) # 输出结果 print(“— PRUNED CONTEXT WINDOW —“) for msg in optimization_context: print(f”{msg[‘role’].upper()}: {消息[‘content’]}”)
|
# 模拟执行 当前请求 = “我们可以回到舰队数学吗?” 优化上下文 = 剪枝上下文(当前请求, 聊天记录) # 输出结果 打印(“— 修剪上下文窗口 —”) 为了 味精 在 优化上下文: 打印(f“{消息[‘role’].upper()}: {消息[‘content’]}”) |
我们的修剪策略产生的上下文窗口如下所示:
— 修剪的上下文窗口 — 用户:我需要帮助计算我的车队的路线效率。 代理:路线效率涉及分析距离、流量和负载重量。 用户:谢谢,这是有道理的。 代理人: 不客气!如果您还需要什么,请告诉我。 用户:我们可以回到舰队数学吗?
|
—– 修剪 语境 窗户 —– 用户: 我 需要 帮助 计算 路线 效率 为了 我的 舰队。 代理人: 路线 效率 涉及 分析 距离, 交通, 和 加载 重量。 用户: 谢谢, 那 使 感觉。 代理人: 你‘关于 欢迎! 让 我 知道 如果 你 需要 任何事物 别的。 用户: 能 我们 去 后退 到 这 舰队 数学? |
请注意,我们使用了默认值 k, IE top_k=2。最后一轮始终包含在我们定义的管道中,由消息对组成:
用户:谢谢,这是有道理的。 代理人: 不客气!如果您还需要什么,请告诉我。
|
用户: 谢谢, 那 使 感觉。 代理人: 你‘关于 欢迎! 让 我 知道 如果 你 需要 任何事物 别的。 |
那么为什么在这一轮之前只出现一次额外的用户代理交互,而不是两次呢?原因是top-k策略不是在全回合级别(即一对消息)运行,而是在单个消息级别运行。在这种情况下,基于相似性检索到的两条消息恰好形成同一交互的两半,但是两条最相关的消息同样有可能都是用户消息、都是代理消息或者只是聊天历史记录的不连续部分。
总结
本文演示了如何基于模拟代理对话历史来实现上下文修剪管道,该管道依赖于语义相似性来选择对话中最相关的部分作为当前提示的上下文。对于长时间运行的代理来说,这是一项重要技术,有助于减少内存使用和计算成本,同时提高整体效率。

