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 在创建智能体时要求开发者明确划分不同类型的记忆块(如 humanpersona),它们不是整个对话原文,而是经过筛选和结构化后的静态摘要。智能体推理时只引用这些块中的内容,而非完整的对话记录。这种做法直接体现了“智能选择”优于“保留一切”:你必须在一开始就想清楚,什么是这个智能体真正需要记住的,而不是让模型自己在噪音里游泳。

动态检索带来的效果差异可以从两个指标看出来:

场景 全量上下文 动态检索 作者的结论
多轮客服对话(>20 轮) 回答重复率 18%,幻觉率 12% 回答重复率 4%,幻觉率 6% 检索大大降低了模型被历史噪音干扰的风险。
代码评审 Agent(长 diff + 历史评论) 相关评论定位准确率 61% 相关评论定位准确率 89% 长文档场景的受益尤其显著。
个人助手(跨天对话记忆) 无法容纳(被迫截断) 可检索一周以上的关键信息 动态检索让长期记忆成为可能。

数据来自社区公开的 2024 年末 Agent 性能基准测试比较,具体数字因领域而异,但趋势一致:在一个噪声不断增长的环境里,精准拉取优于无脑保留。

核心维度二:重要性加权——保留最可能被引用的信息

检索本身还不够。你还需要决定每段历史“值不值得被检索到”。这就是重要性加权的工作:给每一条历史消息打分,淘汰低分条目,让检索层只在高价值池子里捞鱼。

重要性权重可以通过两种方式生成:

  1. 启发式规则:根据消息的角色、长度、是否包含决策结果、是否包含用户显式反馈等特征打分。例如,用户说“这个方案不错,就用它”显然比“我午饭吃了什么”重要得多。
  2. 模型评分:用一个轻量级分类模型或 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 最常被全量注入上下文但几乎从未被引用的历史消息类别(如重复确认、时间戳、空回复),作为首轮淘汰对象。
  • [ ] 搭建一个最小版本的动态检索原型:用 chromadblancedb 存储最近 50 轮对话的嵌入,将每次 LLM 调用从“全量历史列表”改为“当前查询 + 检索出的 top 5 条相关历史”。
  • [ ] 设计重要性评分函数,先基于角色和关键词,第二轮再引入时间衰减,记录 A/B 测试下的检索命中率变化。
  • [ ] 如果你的系统已经使用 Letta 框架,审查 memory_blocks 的划分是否合理:有没有不应该永久保留的内容被放在了 human 块里?
  • [ ] 在非核心对话流上实验预测预填充:统计预测命中率,设定阈值(建议 >70%)后再推广到主流程。

我们在上一章学习了如何用 LLM 摘要压缩上下文,这一章则把“要保留什么”的控制权从模型手中拿回给工程师。摘要与检索不是二选一,而是一对齿轮:摘要减少单条消息的噪音,检索过滤整个历史消息池的噪音。只有两者配合,上下文窗口才能真正成为 Agent 的“工作记忆”,而不是它陈年的杂物间。

然而,有一个更棘手的问题还没有回答:有些信息我们不但不想保留,而且法律和用户要求我们必须彻底遗忘。下一章《记忆清理与遗忘是合规功能的另一面》将探讨如何实现用户可控的遗忘机制,让 Agent 的记忆治理从工程优化进入隐私合规的深水区。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~