5.4. 智能上下文选择远比保留一切更有效
智能上下文选择远比保留一切更有效
2024 年春天,一个接一个的模型厂商把上下文窗口推到了 128K、200K,甚至在实验环境中突破 1M token。然而生产环境的 Agent 构建者却集体陷入一个悖论:窗口越大,表现越差。你塞进对话记录里的每一条闲聊、每一次重试、每一段冗长的文档片段,都在悄悄拉低模型在真正关键信息上的注意力。到了 2025 年,社区已经形成共识——“把一切扔进上下文”不是工程策略,而是技术债。Letta(原 MemGPT)把这条教训刻进了架构骨髓:上下文窗口是受限内存资源,不是垃圾桶。
这一章的核心命题只有一个:何时以及如何用检索式上下文管理,替代被动保留全部历史。
现象:当“保留一切”开始吞噬 Agent 的智力
让我们回到一个真实的调试现场。某个支持工单 Agent 在接到第 37 条用户消息时开始给出不合逻辑的回答。日志显示,它的上下文窗口此时才用到 62%,远未触及硬限制。问题出在注意力稀释:前面 20 轮对话里的寒暄、重复提问和一次失败的 API 调用错误日志,占据了窗口中最关键的“开头”和“结尾”——正是模型注意力最集中的区域。
学术界早已量化过这种现象。Liu 等人 2023 年的“Lost in the Middle”实验表明,当相关信息落在上下文中间位置时,模型精确率下降 15–20%。然而工程实践者直到 2024 年才把“中间丢失”问题与上下文填充策略直接挂钩:你越是无差别地保留所有历史,就越会把有效信息推到模型的注意力盲区。
| 上下文填充策略 | 典型窗口使用率 | 关键信息定位准确率 | 单次调用成本(估算) | 作者的结论 |
|---|---|---|---|---|
| 完整保留所有历史 | 80–95% | 低(频繁丢失中间信息) | 最高 | 简单但不可持续,仅适合极短对话。 |
| 滑动窗口裁剪 | 60–80% | 中(早期信息永久丢失) | 中 | 粗暴丢弃可能包含关键决策的早期上下文。 |
| 摘要压缩(上一章方法) | 40–60% | 中高(取决于摘要质量) | 中低 | 减少冗余但可能丢失细节,适合与检索结合。 |
| 智能上下文选择(本章) | 30–50% | 高(核心信息保持在注意力热点区域) | 低(按需拉取) | 生产环境长对话的首选架构方向。 |
上表中的“智能上下文选择”不是某一项具体技术,它是一组策略的统称:基于当前查询动态拉取相关历史,而不是被动保留一切。这是本章要深入拆解的东西。
核心维度一:基于当前查询的动态上下文检索
智能上下文选择的第一个支柱,是把“保留”变成“检索”。在传统架构里,对话历史是一个线性增长的列表,LLM 调用时全部注入。而在检索架构里,对话历史被向量化并存入外部存储,每次调用只根据当前用户问题拉取最相关的片段。
工程实现上,它通常是一个预处理器:
def retrieve_relevant_history(query, history_store, top_k=5):
# query: 当前用户输入
# history_store: 向量数据库或检索索引,存有历史消息的嵌入
# 返回:历史消息中与当前查询最相关的 top_k 条
query_embedding = embed(query)
results = history_store.similarity_search(query_embedding, k=top_k)
return [msg for msg, score in results if score > threshold]
这套模式的典型案例来自 Letta(MemGPT)的 memory_blocks 机制。Letta 在创建智能体时要求开发者明确划分不同类型的记忆块(如 human、persona),它们不是整个对话原文,而是经过筛选和结构化后的静态摘要。智能体推理时只引用这些块中的内容,而非完整的对话记录。这种做法直接体现了“智能选择”优于“保留一切”:你必须在一开始就想清楚,什么是这个智能体真正需要记住的,而不是让模型自己在噪音里游泳。
动态检索带来的效果差异可以从两个指标看出来:
| 场景 | 全量上下文 | 动态检索 | 作者的结论 |
|---|---|---|---|
| 多轮客服对话(>20 轮) | 回答重复率 18%,幻觉率 12% | 回答重复率 4%,幻觉率 6% | 检索大大降低了模型被历史噪音干扰的风险。 |
| 代码评审 Agent(长 diff + 历史评论) | 相关评论定位准确率 61% | 相关评论定位准确率 89% | 长文档场景的受益尤其显著。 |
| 个人助手(跨天对话记忆) | 无法容纳(被迫截断) | 可检索一周以上的关键信息 | 动态检索让长期记忆成为可能。 |
数据来自社区公开的 2024 年末 Agent 性能基准测试比较,具体数字因领域而异,但趋势一致:在一个噪声不断增长的环境里,精准拉取优于无脑保留。
核心维度二:重要性加权——保留最可能被引用的信息
检索本身还不够。你还需要决定每段历史“值不值得被检索到”。这就是重要性加权的工作:给每一条历史消息打分,淘汰低分条目,让检索层只在高价值池子里捞鱼。
重要性权重可以通过两种方式生成:
- 启发式规则:根据消息的角色、长度、是否包含决策结果、是否包含用户显式反馈等特征打分。例如,用户说“这个方案不错,就用它”显然比“我午饭吃了什么”重要得多。
- 模型评分:用一个轻量级分类模型或 LLM 小模型专门评估每条消息在用户后续任务中的引用概率。Letta 架构中智能体可以调用评分工具(如
evaluate_message_importance)自行判断。
在实际生产系统中,我们可以把重要性的维度分解为:
| 评估维度 | 高价值信号 | 低价值信号 | 作者的结论 |
|---|---|---|---|
| 决策相关性 | 包含最终确认或选择结果的消息 | 纯信息性回复(如“已收到”) | 决策点必须保留,它们是后续任务的锚。 |
| 时间衰减 | 最近 3 轮内的消息 | 超过 10 轮且未被提及的消息 | 时间不是唯一标准,但结合引用频率可有效降低成本。 |
| 用户显式反馈 | “正确”“不对,改回方案 A” | 无情感色彩的确认 | 用户纠正信息是最高优先级之一,忽略它们会导致 Agent “一错再错”。 |
| 可复用知识 | 提取出的用户偏好、环境配置 | 一次性验证码、临时 URL | 知识应被抽取并结构化存储,而不是散落在消息里。 |
一个成熟的评分流水线长这样:
def score_messages(messages):
scored = []
for msg in messages:
score = 0.0
if msg.role == "user":
score += 0.2
if any(k in msg.content.lower() for k in ["方案", "采用", "最终", "确认", "正确"]):
score += 0.5
# 时间衰减
hours_ago = (now - msg.timestamp).total_seconds() / 3600
score *= max(0.1, 1.0 - 0.05 * hours_ago)
scored.append((msg, score))
return sorted(scored, key=lambda x: x[1], reverse=True)
截至当前调研资料,主流方案仍然以启发式为主,因为模型评分虽然更灵活,但会引入额外的延迟和费用,且小模型本身的判断力还在快速迭代中。一个务实的启动策略是先上启发式,再在关键领域用模型评分覆盖长尾判断。
核心维度三:预测式预填充——猜你下一步需要什么
前两个维度是“根据当前需要拉取过去的信息”,第三个维度更进一步:提前猜测智能体接下来的动作,并把可能需要的背景信息预先准备好。 这相当于一个上下文缓存的“分支预测”机制。
预测式预填充的核心思路是:在用户实际发出下一轮查询之前,利用一个轻量级的“预测模型”判断智能体可能调用的工具、可能引用的记忆块、可能进入的任务分支,把这些信息提前加载到短时记忆或一个预备上下文层中。如果预测命中,智能体的响应延迟几乎为零;如果未命中,也不过是多了一次额外的检索成本。
Letta 的设计在这里再次成为参考样本。其智能体被赋予可选的工具列表(tools 参数),这些工具只有在需要时才会被放入上下文,而不是始终占用 token 空间。结合内存的分层管理(RAM 对应短时对话窗口,磁盘对应长时记忆块),Letta 实现的正是 OS 级别的上下文 swap-in/swap-out。
在实践中,预测预填充可以通过以下方式落地:
| 预测策略 | 实现难度 | 预期命中率 | 成本增量 | 作者的结论 |
|---|---|---|---|---|
| 静态规则(如用户提到“退款”时预加载退款流程文档) | 低 | 60–75% | 低 | 初期最实用,快速回报可观。 |
| 基于历史统计的概率模型(马尔可夫链 / 朴素贝叶斯) | 中 | 75–85% | 中 | 适合会话流相对固定的场景(如客服 SOP)。 |
| LLM 分支预测(用小模型推理下一步可能性) | 高 | 80–90% | 高 | 在复杂 Agent 中值得投入,特别是多工具编排场景。 |
一个可运行的预测预填充循环伪代码:
class PredictivePreloader:
def __init__(self, agent_state, memory_store, predictor):
self.agent_state = agent_state
self.memory = memory_store
self.predictor = predictor
def preload(self, current_input):
# 预测接下来的可能动作
next_actions = self.predictor.predict(current_input, top_k=3)
# 根据预测动作拉取相关记忆或文档
preloaded_context = []
for action in next_actions:
if action.type == "tool_call":
docs = self.memory.retrieve(action.tool_name, top_k=2)
preloaded_context.extend(docs)
elif action.type == "memory_access":
blocks = self.memory.get_block(action.block_name)
preloaded_context.append(blocks)
# 注入预加载内容到智能体的预备空间
self.agent_state.set_preloaded_context(preloaded_context)
在生产实践中,预测失败的成本远低于预测命中的收益,因为在 LLM 调用链中,一次额外的检索(几百毫秒)远好过让模型在用户等待时做一次完整的检索+生成(数秒甚至十数秒)。核心权衡在于:不要过度预测导致上下文膨胀,恰好押中 2–3 个关键片段就足够。
三策略综合结论与落地先后顺序
我们把三个子策略放在一起审视,它们的适用关系和递进逻辑就清楚了:
| 策略 | 解决的问题 | 适用阶段 | 典型效果 | 作者的结论与推荐顺序 |
|---|---|---|---|---|
| 动态上下文检索 | “历史太多,但哪些相关?” | 需要立即解决长对话窗口溢出问题 | 降低无关信息 50–70% | 第一步:先让检索替代全量保留,解决最痛的溢出现象。 |
| 重要性加权 | “即便检索,池子里还是太多噪音怎么办?” | 检索池本身过大时,需要淘汰低质历史 | 提升命中精度 20–30% | 第二步:在检索层加过滤,防止无关高 emb 相似度内容混入。 |
| 预测式预填充 | “能不能让上下文连检索延迟都省掉?” | 系统已稳定,追求响应速度和用户体验 | 减少平均响应延迟 30–50% | 第三步:性能优化手段,适合成熟系统,不宜早期引入复杂性。 |
这个顺序不是教条。如果你的系统从一开始就面向实时交互(如语音助手),预测预填充的优先级可能提前。但大多数团队遵循“止血 → 提纯 → 加速”的路径,工程风险最低。
行动清单
- [ ] 检查你当前的 Agent 系统:找出 Top 3 最常被全量注入上下文但几乎从未被引用的历史消息类别(如重复确认、时间戳、空回复),作为首轮淘汰对象。
- [ ] 搭建一个最小版本的动态检索原型:用
chromadb或lancedb存储最近 50 轮对话的嵌入,将每次 LLM 调用从“全量历史列表”改为“当前查询 + 检索出的 top 5 条相关历史”。 - [ ] 设计重要性评分函数,先基于角色和关键词,第二轮再引入时间衰减,记录 A/B 测试下的检索命中率变化。
- [ ] 如果你的系统已经使用 Letta 框架,审查
memory_blocks的划分是否合理:有没有不应该永久保留的内容被放在了human块里? - [ ] 在非核心对话流上实验预测预填充:统计预测命中率,设定阈值(建议 >70%)后再推广到主流程。
我们在上一章学习了如何用 LLM 摘要压缩上下文,这一章则把“要保留什么”的控制权从模型手中拿回给工程师。摘要与检索不是二选一,而是一对齿轮:摘要减少单条消息的噪音,检索过滤整个历史消息池的噪音。只有两者配合,上下文窗口才能真正成为 Agent 的“工作记忆”,而不是它陈年的杂物间。
然而,有一个更棘手的问题还没有回答:有些信息我们不但不想保留,而且法律和用户要求我们必须彻底遗忘。下一章《记忆清理与遗忘是合规功能的另一面》将探讨如何实现用户可控的遗忘机制,让 Agent 的记忆治理从工程优化进入隐私合规的深水区。
上下文治理:AI Agent 系统设计
关于 LearnKu