Agent上下文白盒化:从黑盒到可观测的工程实践
很多开发者在调试 Agent 时,都有过这种经历:任务跑到第 4 步突然跑偏,你翻遍 prompt、工具返回值、模型配置,都没发现问题。直到你把每一轮的完整输入抓出来,才看到第 2 步的上下文已经被静默截断了。Agent 没告诉你,它只是“看不到”了。
这不是个例。最近在 GitHub 上,围绕 Agent、上下文、黑盒这几个关键词的讨论热度越来越高。很多人遇到“上下文过大”“自动摘要多次尝试仍超出限制”“Agent terminated due to error”这类报错,第一反应是换模型、调参数,甚至直接重跑一次。但真正的问题往往出在一个更基础的地方:你根本看不到 Agent 每一轮到底带上了哪些上下文。
这篇文章想给出一个明确判断:Agent 的上下文不应该是黑盒。成熟的 Agent 工程,必须让上下文可见、可管理、可排查。下面会从原理讲到实践,并给出一个最小可运行的上下文可观测示例。读完你可以直接对照检查自己的 Agent 项目,也能更清楚为什么 GitHub 上那些热门 Agent 项目都在拼命解决上下文管理问题。
1. Agent 为什么总是「丢上下文」
要理解“丢上下文”这件事,先得弄清楚一个基本结构:Agent 不是一次调用完成的,它是一个循环。每一次循环里,Agent 要读取任务目标、回顾历史消息、拼接工具返回结果、生成下一步动作,然后把新的结果追加到历史里,进入下一轮。这个循环结构决定了上下文不会变少,只会越滚越大。
1.1 上下文就是模型的工作台
可以把上下文比喻成模型的工作台。台子能放的东西是有限的,放不下的部分就只能丢掉,或者被压缩成一个小纸条。问题在于,很多 Agent 框架在处理“放不下”时,不会主动告诉你丢了什么。它可能只保留最近 N 条消息,可能把早期内容粗暴截断,也可能在后台做了一次自动摘要——但开发者看不到这次摘取的完整过程。
当你把 Agent 当成黑盒来用时,它今天的表现就是不可预测的。同一个任务上午能跑通,下午跑不通;换一个用户说法就跑偏;连续执行多步之后,突然忘记了最初的指令。这些现象背后,绝大多数不是模型变笨了,而是上下文在某个环节被悄悄改变或截断了。
1.2 三种最常见的上下文膨胀来源
实际项目中,上下文膨胀的来源主要有三类。
第一类是对话历史的无脑回传。很多人在写 Agent 循环时,习惯把整个 messages 数组原封不动地塞给下一次调用。对话轮数一多,历史消息就会快速占满窗口。
第二类是工具调用的原始结果。尤其是接了 MCP 工具或者外部 API 的 Agent,一个工具可能返回几十 KB 的原始数据,这些数据会被直接拼进下一轮 prompt。外部数据源的输出有多大,你的上下文就被撑得有多大。
第三类是系统指令和中间推理的重复堆积。一些 Agent 框架会把“当前计划”“已完成步骤”“错误信息”等内容反复写入上下文。单看每一轮不多,但十几轮下来,累积量非常可观。
这三种来源叠加之后,上下文窗口再大也会被迅速耗尽。经常有人问“60k 上下文到底能干什么”,单看数字 60k 确实不小,能装下几十页文档,但如果你的 Agent 每一轮都要回传 20 轮对话、5 次工具返回、一段任务说明,60k 很快就不够用了。
1.3 小窗口与大任务之间的矛盾
上下文窗口是模型层面的硬限制,而 Agent 任务是工程层面的真需求。两者之间天然存在矛盾:任务越复杂,需要的信息越多;信息越多,窗口越容易溢出;一旦溢出,Agent 就可能丢掉关键事实。
很多团队的处理方式是用更大的窗口,比如从 60k 换到 200k。这能缓解问题,但不能根治问题。Transformer 架构的注意力机制有一个特点:内容越长,模型对早期信息的关注度会自然下降。也就是说,即使窗口没爆,早期关键信息也可能在注意力分配中被边缘化。这也是为什么单纯加窗口解决不了 Agent“失忆”的根本原因。
所以,上下文不只是 prompt 里的一段文字,它是一项需要主动管理的资源。窗口再大,也要有策略地决定:哪些信息必须保留,哪些可以压缩,哪些可以直接丢弃,以及这些决策如何被开发者看见。
2. 黑盒上下文的三个致命代价
如果你觉得“看不见上下文”只是一个调试体验问题,那就低估了黑盒的代价。从实际项目经验来看,上下文黑盒会在三个层面上拖垮一个 Agent 应用。
2.1 结果不可复现
Agent 应用最怕的不是“效果差”,而是“不稳定”。同一个任务,昨天跑出正确结果,今天跑出错误结果;同一段代码,在 A 环境正常,在 B 环境失败。这类问题一旦出现,你首先会怀疑模型输出有随机性。但当你把上下文追踪打开,往往会发现真正原因是:两次运行中,Agent 实际看到的上下文不一样。
可能是某一次工具返回超时导致内容缺失,可能是某一次历史消息被摘要压缩后丢失了关键数字,也可能是某一段用户输入在截断时被切掉了一半。这些差异不会显示在最终结果里,你只看输出根本找不到原因。上下文一旦变成黑盒,复现问题就变成一场赌博。
2.2 调试成本成倍上升
没有上下文追踪的 Agent 项目,调试基本靠猜。你会反复修改 prompt,加各种约束,甚至换模型,但问题可能根本不在模型,而在上一轮输入给模型的内容就不完整。
在 GitHub 热门 Agent 项目的 issue 区,经常能看到这种问题:“Agent terminated due to error”“已进行多次自动总结但上下文大小仍超出限制”“新开会话丢失上下文记忆”。这些问题表面上五花八门,底层原因高度一致:上下文管理失控,而开发者看不到失控发生在哪一步。
如果能拿到每一步的上下文快照,排查思路会完全不一样。你可以直接看到第 3 步的输入长度是多少,历史被压缩过几次,哪一段摘要丢掉了哪个字段,问题基本一目了然。没有这份快照,就只能靠猜。
2.3 成本、时延与安全全面失控
上下文黑盒还直接影响成本和性能。每一次 API 调用都会按 token 计费,历史消息反复回传,就是在反复为同一批内容付费。更麻烦的是,调用耗时也会随上下文长度增长,用户等待时间变长,体验变差。
安全层面也一样。如果你不知道上下文里实际带了什么,就不可能控制敏感信息的流向。某个工具返回的原始数据里如果包含用户隐私字段,而这些字段又被无差别拼进 prompt 发送给模型,就是一个看不见的数据泄露风险。
3. 从黑盒到白盒:上下文工程的核心思路
聊完问题,再聊解法。既然黑盒的代价这么大,GitHub 上那些热门 Agent 项目到底是怎么处理的?从材料来看,共同趋势可以概括为“上下文工程”,目标就是让上下文从黑盒变成白盒。
3.1 上下文工程与 Prompt 工程的区别
很多人把上下文管理等同于写 prompt,其实两者完全不同。
Prompt 工程关注的是“如何把话说清楚”,核心内容是系统提示词、任务描述、输出格式约束。它的作用对象是模型的语言理解能力。而上下文工程关注的是“模型在每一步到底能看到哪些信息”,核心内容是历史消息的保留策略、压缩策略、注入顺序和可见性追踪。它的作用对象是整个 Agent 循环的数据流。
换句话说,Prompt 工程解决的是“说出去了”的问题,上下文工程解决的是“被看见”的问题。一句话写得再好,如果模型根本没看到,也等于白写。而在 Agent 循环里,影响“看到什么”的,不是你的 prompt,是你背后的上下文管理代码。
3.2 白盒上下文的三个关键词
要让上下文从黑盒变成白盒,核心可以做三件事:可见性、压缩、控制。
可见性,就是每次调用模型前,把当前上下文的构成记录成结构化日志。包括输入长度、消息条数、历史压缩次数、哪些内容被截断、哪些内容被摘要。这份日志是后续所有调试的依据。
压缩,就是设定明确的压缩规则。比如“最早的 10 轮对话压缩成一段任务摘要”“工具结果只保留前 2000 字符”“系统指令永远不被丢弃”。压缩不是目的,目的是在有限窗口内尽可能保住关键信息。
控制,就是把上下文策略从隐式变成显式。不在框架里静默处理上下文,而是由开发者显式配置每一个限制:保留多少条历史、摘要格式是什么、溢出时优先丢弃哪一类内容。控制到位,行为就可预测。
3.3 Harness 与 Agent 的分工
很多初学 Agent 的同学分不清 harness 和 agent 的区别,这里可以简化理解:Agent 是决策主体,负责判断下一步做什么;Harness 是包裹它的运行框架,负责把上下文准备好、把工具接好、把过程记录好。
真正让上下文白盒化的关键,往往在 harness 这一层。你在 GitHub 上看到的很多 Agent 类热门项目,不管名字里带不带 harness,都会内置一套上下文管理机制:有的叫 context manager,有的叫 memory module,有的直接提供压缩上下文的命令。叫法不同,本质一致:在 Agent 循环外增加一个显式的上下文管理层。
这也解释了为什么很多 AI 编程工具在长会话里表现更好。它们不是模型本身变强了,而是在工具层解决了上下文的整理、压缩和记录问题。把模型上下文的管理交给工程层,而不是完全交给模型自己,是当前 Agent 工程的主流思路。
3.4 外部工具引入的上下文风险
还有一个容易被忽略的上下文黑盒来源:外部工具。现在很多 Agent 都通过 MCP 接入数据库、搜索引擎、文件系统等工具。工具返回的数据是外部产生的,长度和格式都不受你控制。
一个很常见的场景是:某个工具返回了 30KB 的原始数据,Agent 不管三七二十一全塞进上下文,下一轮直接超限。更隐蔽的是,工具返回数据里的关键信息可能不在开头,而在第 20KB 的位置,如果被截断,Agent 就看不到真正重要的内容。
所以在白盒化设计里,外部工具返回结果必须单独处理。要给工具结果设定长度预算,要在注入 prompt 之前先抽取关键字段,还要记录工具结果被截断的位置。第三方数据源不能决定你的上下文预算,这个控制权必须收回到工程层。
4. 环境准备与最小工程结构
下面进入可运行的示例部分。我会用一个最小 Python 工程,演示如何实现“上下文可见、可压缩、可追踪”的 Agent 循环。这个示例不依赖具体 Agent 框架,也不绑定任何模型服务商,重点在原理演示,你可以把核心逻辑迁移到自己的项目里。
4.1 运行环境与依赖
示例基于 Python 3.9 以上版本,主要依赖只有一个 PyYAML,用来读取配置文件。如果你暂时没有真实模型 API,示例会用模拟函数代替模型调用;如果你有真实 API,只需要替换 call_llm 函数内部的实现。
这里不写死具体版本,以你本机环境为准。示例的核心逻辑不依赖某个特定版本,迁移成本很低。
4.2 工程文件结构
建议按照下面的结构创建目录:
- config.yaml:集中管理 Agent 的上下文参数,方便调整。
- context_visibility.py:上下文记录器,负责给每一步调用生成快照。
- context_compressor.py:上下文压缩器,负责历史消息的压缩策略。
- visible_agent.py:Agent 主流程,演示如何把记录器和压缩器接入循环。
- run_agent.py:入口,读取配置并启动。
这种分层设计本身就是白盒化的体现:每一层的职责清晰,问题出现时可以快速定位到具体模块。
4.3 配置文件说明
config.yaml 内容如下:
这里的参数含义:
- max_steps:Agent 最多执行多少轮循环。
- max_context_chars:单次 prompt 的最大字符数,超出即认为上下文超限。
- keep_recent:历史消息中保留最近几条,更早的进入压缩摘要。
- trace_dir:上下文追踪日志的输出目录。
- save_trace_json:是否把每一步的上下文快照导出成 JSON 文件。
把这些参数放到配置文件里,是为了让上下文策略可以被显式调整和审查。生产环境中,你还可以把这些配置接入配置中心,实现动态调整。
5. 完整示例:实现一个上下文可观测的 Agent
这一章是核心实操部分。我会把四个文件的完整代码和关键逻辑都讲清楚,你可以直接复制到本地运行。
5.1 上下文记录器
首先实现上下文记录器。它的作用是:在 Agent 的每一步循环中,把“模型看到了什么”保存成结构化快照。快照里包括输入长度、输出长度、是否截断、prompt 预览等重要信息。
文件路径:context_visibility.py
这段代码的核心是 record 方法。每当 Agent 准备调用模型时,调用一次 record,就能保存这一步的上下文快照。关键的字段是 truncated,如果 prompt 长度超过预设的最大值,它会标记为 True,帮你快速发现“这一步开始丢上下文了”。
5.2 上下文压缩器
接下来是压缩器。这里实现两个策略:一是保留最近 N 条历史、把更早的内容压成摘要;二是按字符预算截断,超出的消息直接丢弃并统计丢弃量。
文件路径:context_compressor.py
compress_early_history 适合处理对话轮数较多的问题,它的原则是“最近的消息尽量保留原样,更早的消息变成摘要”。truncate_history_by_chars 适合处理单条消息特别长的情况,它的原则是“超过预算的消息直接丢弃,并统计丢弃量”。
两个函数都没有复杂依赖,你可以根据项目需要替换成更智能的摘要方案,比如用一个小模型做结构化总结,抽取任务目标、已完成事项、未完成事项等字段。核心思想是一样的:压缩过程必须可控、可见。
5.3 Agent 主流程
接下来把记录器和压缩器接入 Agent 循环。这里用一个模拟的 call_llm 函数代替真实模型调用,你不需要 API key 就能跑通整个流程,重点观察上下文管理的节奏。
文件路径:visible_agent.py
这个主流程设计的核心逻辑是:每轮循环开始前,先对 history 做压缩,再按字符预算截断,然后才构造 prompt。压缩和截断的结果都会打印出来,所以你能清楚看到每一步的输入情况和丢弃情况。
这里有一个容易踩坑的地方:压缩和截断的顺序会影响最终效果。如果先截断再压缩,可能会把本来可以压缩进摘要的关键信息直接丢弃;如果先压缩再截断,至少能保证早期信息变成摘要,而不是完全消失。示例采用“先压缩、再截断”的顺序,实际项目中建议保持这个原则。
真实项目中,你只需要把 call_llm 替换成你的模型服务调用,把 build_prompt 中的任务描述替换成真实任务,把输出解析换成你需要的结构,就能把同样的上下文管理逻辑迁移过去。
5.4 运行入口
最后补一个独立的入口文件,方便从命令行启动。
文件路径:run_agent.py
这个文件只做两件事:读取配置、启动 Agent。保持入口精简,把复杂逻辑留在 visible_agent.py 里,便于后续扩展和测试。
6. 运行结果与验证
代码写完之后,重点看运行效果。这个章节教你如何验证上下文是否真正“白盒化”。
6.1 运行方式
在项目根目录执行:
如果你用的是真实模型 API,也可以先安装依赖再运行:
如果你在 config.yaml 中修改了 max_steps 或 keep_recent,重新运行即可看到对应变化。
6.2 预期输出
正常运行时,你会看到类似下面的输出:
这段输出说明 Agent 在每一步运行之前,都清楚地记录了输入大小、是否截断、是否压缩、丢弃了多少内容。你应该能看到随着 step 增加,输入字符数逐渐变大;当历史超过 keep_recent 条时,压缩摘要开始生效。
6.3 如何确认上下文已经“白盒化”
运行结束后,打开 output/context_trace.json,检查里面的字段。每个 step 对应的记录里,有 input_chars、truncated、summary 等字段。如果 truncated 始终为 False,说明当前配置下上下文没有超限;如果某个 step 的 truncated 为 True,则说明从那一刻起,prompt 超过了 max_context_chars,需要调整压缩策略或增大预算。
白盒化的判断标准很简单:你能不能只凭日志文件,还原出 Agent 每一步到底看到了什么?如果能,说明上下文不再是黑盒;如果不能,说明追踪还不完整,需要补充更多字段。
如果你的 Agent 报错“Agent terminated due to error”或者“上下文大小超出限制”,第一步不是重试,而是打开 trace 文件,找到第一个 truncated=True 或者 summary 丢失关键字段的位置。问题往往就藏在那里。
7. 常见问题与排查方法
下面整理 Agent 上下文开发中最高频的几个问题,以及对应的排查思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 新开会话后 Agent 不记得之前的内容 | 会话级记忆没有持久化,历史只存在内存中 | 检查历史存储逻辑是否有跨会话保存 | 引入记忆存储,如向量库或记忆文件 |
| 上下文压缩后效果明显变差 | 摘要丢失了关键事实或数字 | 对比压缩前后 trace 记录,确认摘要字段 | 用结构化摘要模板,保留任务目标和关键数据 |
| MCP 工具返回内容过大,导致上下文超限 | 未对工具结果做长度限制 | 查看工具返回内容的字符数 | 对工具结果做截断或字段抽取后再注入 |
| Agent 报错 terminated due to error | 历史消息中某条异常内容导致循环中断 | 查看该步的 prompt 预览和响应预览 | 增加异常内容过滤,或回退到上一步重试 |
| token 消耗增长异常,成本飙升 | 历史消息重复回传,缺乏压缩 | 对比每次调用的 input_chars | 启用压缩策略,设定历史保留条数 |
| 同一任务多次运行结果不一致 | 上下文在某一步被静默截断 | 查看每个 step 的 truncated 字段 | 增加上下文追踪,确保截断可感知 |
这些问题里,最容易被忽略的是“MCP 工具返回过大”这一类。外部工具是上下文黑盒的重要来源,你无法控制第三方返回的数据量,但你必须控制这些数据进入上下文的方式。
当你遇到“已进行多次自动总结但上下文大小仍超出限制”这类提示时,正确思路是拆解数据流,找到到底是哪一类内容占用了大量空间,然后针对性地压缩,而不是无休止地让模型反复尝试总结。
8. 上下文管理的最佳实践
最后聊几组马上能用的工程建议。这些实践来自 Agent 项目的通用经验,适合绝大多数中大型项目。
8.1 给上下文分级,并制定预算
把上下文内容分成不同优先级,然后给每个级别设定预算。
优先级从高到低大致是:系统指令、任务目标、关键事实、近期对话、工具结果、历史摘要。系统指令永远不丢弃;任务目标必须保留到任务结束;关键事实包括用户 ID、订单号、时间范围等,一旦存在就尽量保留;近期对话保留原始内容;工具结果做长度限制;历史摘要尽可能精简。
预算分配建议给每个级别设定一个比例。比如系统指令占 10%,任务目标占 10%,关键事实占 20%,近期对话占 25%,工具结果占 25%,历史摘要占 10%。这只是初始参考,实际项目中需要根据任务特点调整。关键是预算必须分配,不能“谁先进来谁占位”。
8.2 截断必须显式,压缩必须保留关键字段
如果你的 Agent 确实要丢弃某些内容,不要静默丢弃。在 prompt 中显式写明“以下内容因超出长度已被省略”,让模型知道信息可能有缺失,从而在回答时给出更保守的判断。
压缩时不要只压缩成一句话,要保留结构化关键字段。一个可用的压缩模板是:任务目标 + 已完成步骤 + 未完成事项 + 关键事实列表。这种摘要比自由文本摘要更抗信息丢失,也更容易让模型理解当前状态。
8.3 把上下文追踪纳入日志体系
不要只在调试阶段开启上下文追踪,生产环境也要保留一份精简版的追踪日志。每次调用至少记录:时间戳、输入 token 或字符数、输出 token 或字符数、是否截断、压缩摘要版本。
有了这些日志,出问题后才能做线上排查。很多 Agent 项目的线上事故,最后能定位到的原因,就是某一次调用中被截断了一条关键命令。如果没有日志,你永远不会知道。
8.4 安全与敏感信息处理
上下文记录器中不要把敏感内容完整写入日志。对 trace 中的 prompt_preview 和 summary 做脱敏处理,比如把身份证号、手机号、密钥等替换成掩码。外部工具返回的数据在写入上下文