在本文中,您将了解为什么大型上下文窗口与代理记忆不同,以及检索、压缩和摘要等技术如何在代理的认知堆栈中组合在一起。
我们将讨论的主题包括:
- 为什么上下文窗口的行为就像无状态暂存器而不是持久内存。
- 检索增强生成、压缩和摘要如何在管理进入暂存器的内容方面发挥独特的作用。
- 代理如何通过充当数据库管理员而不是数据库本身来实现真正的内存持久性。
介绍
上下文窗口是现代人工智能模型(尤其是语言模型)的一个关键方面,这些模型可以在产生响应时立即处理和利用有限数量的输入和先前的对话(通常以标记数量来衡量)。
当人工智能实验室发布具有 200 万个令牌上下文窗口的模型时,一些开发人员本能地这样想也就不足为奇了:“让我们将整个代码库推入提示符中!内存问题已解决!”但是,有一个警告。从建筑学角度来看,将巨大的上下文窗口视为“内存”类似于因为不愿意购买文件柜而购买 25 英尺宽的办公桌。当然,您可以将所有文件放在您面前,但是一旦工作结束,整个办公桌的文件就会被清除(被清洁人员!)。
为了澄清这种区别并揭开其他相关概念的神秘面纱,本文对人工智能代理的认知堆栈中的多个层次进行了概念分解。我们将使用几个主要与办公室相关的隐喻来帮助更好地理解这些概念。
上下文窗口
人工智能模型中的上下文窗口,特别是具有底层语言模型的基于代理的模型,就像桌面或无状态便签本。值得注意的是,模型本质上是完全无状态的。无论如何,对模型的每个 API 调用都从“第零步”开始。
当向代理传递超过 200K 令牌(大上下文窗口)的对话历史记录时,它不会及时记住上一步发生的情况。相反,它会在几毫秒内从头开始快速重新读取“它的宇宙”。从长远来看,在基于代理的环境中依赖此策略可能会引入一些危险的(如果不是致命的)陷阱:
- 人工智能模型就像一个懒惰的学生,他密切关注大量提示(文本)的开头和结尾部分,但完全掩盖了深埋在中间部分的想法和事实。
- 存在滚雪球效应:随着对话的增长,代理必须在每一步中重新发送并重新读取整个历史记录,包括最早的、通常不相关的回合。
- 在延迟方面,存在“大脑冻结”效应,因此面对巨大的文本墙,模型需要一些时间才能开始生成响应中的第一个单词。
为了使这一点具体化,请考虑单个 API 调用在幕后的实际情况。由于模型在调用之间没有记忆,因此之前的每一轮都必须完全重新发送,只是为了提出一个新问题:
模型.生成(消息=[
{“role”: “user”, “content”: “Step 1: Let’s call this variable `session_id`.”},
{“role”: “assistant”, “content”: “Got it, I’ll use `session_id` going forward.”},
# … every intervening turn must be resent, every single time …
{“role”: “user”, “content”: “Step 47: What variable name did we agree on back in step 1?”}
])
|
模型。产生( 消息=[ {“role”: “user”, “content”: “Step 1: Let’s call this variable `session_id`.”}, {“role”: “assistant”, “content”: “Got it, I’ll use `session_id` going forward.”}, # … every intervening turn must be resent, every single time … {“role”: “user”, “content”: “Step 47: What variable name did we agree on back in step 1?”} ] ) |
仅步骤 47 就迫使整个桌子(之前的所有 46 个回合)回到桌子上,只是为了回答有关步骤 1 的问题。这就是上面描述的具体的滚雪球效应。
检索
检索增强生成(RAG)系统就像整个办公室的一个大书架,有助于以“即时”方式获取与当前步骤相关的静态现有数据。 RAG系统 当用户提出某个问题时,将前 K 个相关文档块拉入暂存器(上下文窗口):当然,检索到的文档是被确定为与用户的问题或提示在语义上最相关的文档。
然而,当智能体处于循环中时,事情就没那么容易了,因为向量相似性(RAG 系统中使用的相似性度量和数据表示的类型)在某些情况下不一定等同于语义真实。例如,假设用户告诉其调度代理将会议移至星期五,然后说“取消星期四,爱丽丝生病了”。矢量搜索引擎可以从文档库中检索这两个语句,即使它们彼此矛盾。代理及其相关的语言模型必须能够充当会计师,能够确定哪种陈述更好地反映当前的现实。
简单的 RAG 管道只是连接它检索到的任何内容,并让模型猜测哪条指令仍然有效。更可靠的模式可以在生成发生之前解决冲突,例如通过支持最近记录的语句:
检索到的块= [
{“text”: “Move meeting to Friday”, “timestamp”: “2025-01-10T09:00:00”},
{“text”: “Cancel Thursday, Alice is sick”, “timestamp”: “2025-01-12T14:30:00”}
]# 在矛盾的块到达提示之前调和它们latest_relevant = max(retrieved_chunks, key=lambda chunk: chunk[“timestamp”])
|
检索到的块 = [ {“text”: “Move meeting to Friday”, “timestamp”: “2025-01-10T09:00:00”}, {“text”: “Cancel Thursday, Alice is sick”, “timestamp”: “2025-01-12T14:30:00”} ] # 在出现提示之前调和矛盾的块 最新相关 = 最大限度(检索到的块, 钥匙=拉姆达 块: 块[“timestamp”]) |
这一条协调逻辑是自信地重述过时指令的代理与正确知道会议已取消的代理之间的区别。
压缩
如果您熟悉压缩 ZIP 文件,这很容易理解。在代理和语言模型的上下文中,这需要减少一些算法标记:保持关键基础数据完整,同时缩小其在某个步骤的提示内的物理足迹。有一些技术可以做到这一点,例如剥离停用词、将原始文本传递到特定的压缩模型(如 LLMLingua 或提示缓存)。从本质上讲,这是一种带宽优化方法,可用于将 15K 令牌 JSON 有效负载压缩到 5K 等情况,从而在模型中留下足够的暂存器空间来完成其主要工作。
实际上,这可能看起来很简单,就像在到达主提示符之前通过压缩模型路由大型有效负载一样:
raw_payload = json.dumps(large_api_response) # 大约 15,000 个令牌compressed_payload = compress_with_llmlingua( raw_payload, target_token_count=5000 )prompt = f”鉴于此数据:{compressed_payload}\n\n回答用户的问题。”
|
原始有效负载 = json。转储(大_api_响应) # 大约 15,000 个代币 压缩有效负载 = compress_with_llmlingua( 原始有效负载, 目标令牌计数=5000 ) 迅速的 = f“鉴于此数据:{compressed_payload}\n\n回答用户的问题。” |
基本事实在旅途中完好无损;只是他们在桌子上的足迹缩小了。
总结
与压缩不同,摘要会删除原始数据并用抽象代替。必须按其本来面目对待它:本质上是不可逆转的单程旅行。因此,在应用上下文摘要时,一个几乎势在必行的好做法是使用分叉存储:将原始记录转储到 S3 存储桶或基本 SQL 表等廉价存储中,然后仅将合成的摘要传递到活动提示中。
这种分叉存储模式可以简单地表示为两步写入,一步写入冷存储,一步写入活动提示:
defsummary_turn(raw_transcript, session_id,turn_id): # 1. 将原始的、未删节的转录本保存到冷存储中 s3_client.put_object( Bucket=”agent-transcripts”, Key=f”{session_id}/turn_{turn_id}.json”, Body=raw_transcript ) # 2. 为活动提示生成紧凑摘要 摘要 = summarizer_model.generate(raw_transcript) # 3.只有摘要重新进入上下文窗口返回摘要
|
定义 总结转(原始记录, 会话ID, 回合编号): # 1. 将原始的、未删节的转录本保存到冷库中 s3_客户端。放置对象( 桶=“代理记录”, 钥匙=f“{session_id}/turn_{turn_id}.json”, 身体=生的_成绩单 ) # 2. 为活动提示生成紧凑的摘要 概括 = 总结模型。产生(原始记录) # 3.只有摘要重新进入上下文窗口 返回 概括 |
如果后续步骤需要原始详细信息,则始终可以从 S3 检索。与压缩不同,摘要不需要从活动提示本身内部重建。
作为状态机的内存持久化
代理中的内存持久性通常被认为是理所当然的,尤其是初级开发人员。但要给代理真正的记忆,它不能充当数据库,而应该充当数据库管理员。假设用户说:“我的狗的名字是 Goofy,但我们可以将他重命名为 Pluto”。然后代理应该能够显式触发工具调用,如下所示:
{ “tool”: “update_entity_graph”, “params”: { “subject”: “User_Dog”, “attribute”: “Name”, “value”: “Goofy”, “notes”: “考虑冥王星” } }
|
{ “工具”: “更新实体图”, “参数”: { “主题”: “用户_狗”, “属性”: “姓名”, “价值”: “高飞”, “笔记”: “考虑冥王星” } } |
它是否由标准 SQL 表、知识图或 Redis 支持都无关紧要:无论哪种方式,都应该教会代理在每个回合开始时查询状态机,并在该回合结束时提交它。作为一个循环,这个“查询然后提交”规则如下所示:
def agent_turn(user_message,Entity_graph): # 查询每回合开始时的现有状态 current_state =entity_graph.query(subject=”User_Dog”) response = model.generate(messages=[{“role”: “user”, “content”: user_message}]context=current_state ) # 在每轮结束时提交任何更新,以便在 response.tool_calls 中进行调用:entity_graph.update(**call.params) return response
|
定义 代理轮转(用户消息, 实体图): # 查询每回合开始时的现有状态 当前状态 = 实体图。询问(主题=“用户_狗”) 回复 = 模型。产生( 消息=[{“role”: “user”, “content”: user_message}], 语境=当前的_状态 ) # 在每回合结束时提交任何更新 为了 称呼 在 回复。工具调用: 实体图。更新(**称呼。参数) 返回 回复 |
总结
通过这些概念,您现在应该更清楚地了解在基于语言模型构建的代理的上下文管理中发挥作用的元素。这个教训很简单:停止尝试购买一张价值 1000 万代币的巨大办公桌。相反,只需找一张普通的桌子,给你的特工一支锋利的铅笔,并教它如何打开文件柜并最佳地利用其中的内容来完成工作。

