AI Agent 总在关键时刻“失忆”翻车?一文讲透上下文工程(附可运行 Python 代码)

0 阅读7分钟

导语

你大概率遇到过这种场景:Agent 前几轮还逻辑清晰,十几轮对话后开始答非所问;或者它刚调完一个工具,转脸就忘了工具返回的关键字段;更诡异的是——你明明在 system prompt 里写了规则,它偏偏在长对话尾部违规。

这不是模型"变笨了",而是你的上下文管理失控了。2026 年大家已经形成共识:决定 Agent 上限的不再是 prompt 措辞的雕花,而是上下文工程(Context Engineering)——在有限的上下文窗口里,决定"什么信息该进、什么信息该留、什么信息该扔"的系统性方法。

这篇文章会用三个真实翻车场景拆解根因,然后带你用 60 行可运行的 Python 代码实现一个最小上下文管理器,最后给出生产环境的实践建议。全程不依赖任何第三方库,复制即可运行。

一、三个真实翻车场景,你的 Agent 中了几个?

场景 1:对话越长,回答越差。 模型的注意力并不是均匀分布在整个窗口上的。大量实测表明,关键信息如果埋在几万 token 的中间位置,召回率会明显下降。你的历史消息每轮都在膨胀,而有效信息密度却在稀释。

场景 2:工具结果撑爆窗口。 一个返回 8000 token 的数据库查询结果,往往只有最后几百 token 是有效结论。如果你把原始返回原封不动塞进历史,两三轮工具调用就能吃掉大半个窗口,导致最早的用户需求被截断丢弃。

场景 3:规则写在开头,违规发生在结尾。 你在 system prompt 里写了"必须引用用户 ID",但对话进行到 30 轮后,这条规则的"相对位置"已经离当前生成点十万八千里,模型对它的遵守率随之下降。

这三个场景指向同一个本质问题:上下文是稀缺资源,而大多数人把它当成无限缓冲区在用。

二、上下文工程的核心思路:分层预算制

解决思路其实和内存管理很像——操作系统不会把所有数据都常驻内存,上下文也不该把所有信息都常驻窗口。一个实用的分层模型是:

层级内容策略
常驻层system prompt、核心规则永不淘汰,始终放窗口头部
工作层最近 N 轮对话原样保留滑动窗口,超出即淘汰
压缩层更早的历史压缩为摘要,只留结论
即弃层工具返回的大段原始数据立刻提取关键字段,原始数据丢弃

关键动作有两个:给每层设 token 预算,以及对工具返回做"提取即弃"处理。下面我们直接写代码实现。

三、动手实现:60 行 Python 写一个最小上下文管理器

下面的代码没有任何外部依赖(token 用字符数近似估算,比例约 1 token ≈ 2 个中文字符),直接复制到 context_manager.py 运行即可:

class ContextManager:
    """最小可用的分层上下文管理器:常驻层 + 摘要层 + 工作层"""

    def __init__(self, budget=8000, work_window=6, digest_chars=40):
        self.budget = budget              # 总 token 预算(近似值)
        self.work_window = work_window    # 原样保留的最近消息条数
        self.digest_chars = digest_chars  # 摘要中每条消息保留的字符数
        self.system = []                  # 常驻层:system prompt
        self.history = []                 # 消息池

    @staticmethod
    def _len(msg):
        # 粗略估算:1 token ≈ 2 个中文字符 / 4 个英文字符
        text = msg.get("content", "")
        return max(len(text) // 2, len(text) // 4)

    def set_system(self, text):
        self.system = [{"role": "system", "content": text}]

    def add(self, role, content):
        self.history.append({"role": role, "content": content})

    def add_tool_result(self, key_facts):
        """工具返回只保留提取出的关键事实,原始数据不进历史"""
        self.history.append({"role": "tool",
                             "content": "[工具结论] " + ";".join(key_facts)})
        # 原始返回在这里被丢弃——它只服务于"提取"这一步

    def _total(self, msgs):
        return sum(self._len(m) for m in msgs)

    def build(self):
        """组装最终上下文:更早的历史压成摘要,超预算时继续淘汰工作层"""
        window = self.history[-self.work_window:]
        overflow = self.history[:-self.work_window]
        head = self.system[:]
        if overflow:  # 更早的历史压缩为摘要,随 system 常驻窗口头部
            digest = ";".join(m["content"][:self.digest_chars] for m in overflow)
            head.append({"role": "system", "content": "[历史摘要] " + digest})
        while len(window) > 1 and self._total(head) + self._total(window) > self.budget:
            window = window[1:]  # 预算不足时从工作层头部继续淘汰
        return head + window


# ---- 运行演示 ----
cm = ContextManager(budget=240, work_window=4)
cm.set_system("你是订单客服,回答必须引用用户ID")
cm.add("user", "查询订单 20261115001 的物流状态")

# 模拟一个 3000 字的工具原始返回——实际只提取 3 条关键事实
big_json = '{"logs": ["' + "包装扫描".join(["站点"] * 1500) + '"], "status": "TRANSPORTING"}'
_ = big_json  # 原始数据仅演示"存在但不入库",真正进入历史的只有 key_facts
cm.add_tool_result(key_facts=[
    "用户ID=U8821", "订单20261115001状态=运输中", "预计48小时内送达",
])

for i in range(8):  # 再模拟 8 轮普通对话,制造上下文膨胀
    cm.add("user", f"第{i}轮问题:帮我看看还能不能改地址?")
    cm.add("assistant", f"第{i}轮回复:可以,发货前均可修改。")

final = cm.build()
print(f"消息总数:{len(cm.history)} 条 -> 实际送入模型:{len(final)} 条")
print(f"预算占用:{cm._total(final)}/{cm.budget}")
print("首条消息:", final[0]["content"])
print("摘要层:", final[1]["content"][:60], "……")
print("末条消息:", final[-1]["content"])

真实运行输出如下:

消息总数:18 条 -> 实际送入模型:6 条
预算占用:188/240
首条消息: 你是订单客服,回答必须引用用户ID
摘要层: [历史摘要] 查询订单 20261115001 的物流状态;[工具结论] 用户ID=U8821;订单2026111500 ……
末条消息: 第7轮回复:可以,发货前均可修改。

三个关键现象一目了然:18 条消息被收敛成 6 条送入模型;预算占用 188/240,始终不超限;最早的用户需求("查询订单物流")没有消失——它被压进了摘要层,随 system 一起常驻窗口头部。

这就是上下文工程的最小骨架。LangChain 的 ConversationSummaryBufferMemory、各家 Agent 框架的 memory 模块,核心逻辑都是它的工业级放大版。

四、生产环境的三条实践建议

建议 1:工具返回永远走"提取即弃"。 在工具调用层统一加一个后处理器:原始返回只允许存活在当轮,进历史前必须收敛为结构化结论(字段、状态、数值)。这一条能直接把上下文消耗降低一个数量级。

建议 2:把关键规则写到离生成点最近的位置。 与其把所有规则堆在 system prompt 开头,不如把"与当前任务相关"的规则在每轮末尾以简短 reminder 重复一次。窗口越长,这一招的收益越明显。

建议 3:为摘要设预算,而不是为摘要设字数。 摘要层的价值密度应该高于原文,实践中建议给它分配总预算的 10%~20%,并优先保留:用户的核心诉求、未完成的待办、已确认的参数。闲聊性内容压缩时可以无脑丢弃。

总结

Agent 的"失忆"和"翻车",多数时候不是模型能力问题,而是上下文管理问题。本文给出的分层预算模型(常驻层 / 摘要层 / 工作层 / 即弃层)和 60 行最小实现,可以直接嵌进你现有的 Agent 项目做起步验证。当你的 Agent 需要长会话、多工具、连续工作时,先检查上下文,再怀疑模型——这个排查顺序,能帮你省下大把无效的 prompt 调优时间。

如果这篇文章对你有帮助,欢迎点赞收藏;你在 Agent 上下文管理上踩过什么坑,评论区聊聊。