公司动态
Agent上下文管理实战:从黑盒到白盒的上下文工程指南
如果你最近在用各种 AI 编程助手、开源 Agent 框架或者自己搭过一个多步任务链路大概率遇到过下面这些让人抓狂的瞬间对话到一半AI 突然“失忆”忘了最开始定义的需求一个长任务的执行链路出现agent terminated due to error或者更直接一点提示context window exceeded/上下文大小已超出限制你新建一个会话然后发现之前沉淀好的业务规则、项目规范全部归零又得从头再贴一遍。很多人第一反应是模型不行或者窗口不够大。于是开始找更长上下文的模型甚至考虑付费升级。但如果你在 GitHub 上翻一翻最近热门的 Agent 项目会发现一个更务实的趋势正在形成Agent 的上下文管理正在从“黑盒”走向“白盒”从“拼命塞窗口”变成“上下文工程”。这篇文章想聊三件事第一为什么 Agent 的上下文问题不是简单的长度问题第二GitHub 上热门项目如何通过技术手段把上下文从黑盒变成可控资源第三如果我们要在真实项目里落地应该怎么做有哪些坑。内容偏工程视角代码和配置都给足。1. 先搞懂Agent 的“上下文”到底是个什么黑盒我们在聊 Agent 上下文问题时经常把几个概念混在一起用先拆开。1.1 上下文窗口不是“内存”很多开发者把模型的上下文窗口理解成机器内存这是最常见的误区。内存是程序可以主动访问、修改、释放的数据存储区而模型的上下文窗口本质上是一次推理请求里模型能看到的 Token 序列。区别在于程序改内存是确定的模型处理上下文是概率性的。你塞进去 200K Token模型并不保证对最早的内容还有足够注意力也不保证它能从中精确提取某一条事实。这就是为什么“上下文越大”不等于“记得越牢”——长上下文模型更多是在缓解问题不是根治问题。1.2 Agent 的上下文是一个多层叠加结构在真实 Agent 项目里上下文不只是用户输入的那段话而是多层信息的叠加系统提示词定义角色、规则、输出格式。用户多轮对话记录包含需求和指令变更。工具调用历史即 Agent 每一步执行的函数、参数、返回值。外部知识检索结果比如 RAG 召回的相关片段。程序产生的中间状态比如临时变量、文件路径、任务队列。这层叠结构意味着Agent 的上下文是运行过程中动态膨胀的。你写一个代码生成 Agent每调用一次工具就会把工具结果完整贴回对话历史跑 20 个工具步骤之后哪怕原始需求只有 500 字上下文可能已经膨胀到几万 Token。这个时候真正的问题不是模型窗口够不够大而是你的 Agent 把太多垃圾信息塞给了模型。这个现象在 GitHub 上有个很形象的讨论Agent 正在“自己在自己产生的上下文里找答案”就像在一个黑盒子里翻找东西——你不知道里面有什么、哪些是重要的、哪些已经过期只知道它越来越大。2. 为什么“上下文过大”会成为 Agent 开发的头号障碍先从真实场景看问题。假设我们用 Agent 自动完成“分析一段代码仓库中的遗留问题并输出整改报告”这样的任务。Agent 的运行链路大致是读取仓库文件列表。逐个读取核心代码文件。调用静态分析工具。汇总问题列表。生成整改建议。问题出现在第 2 步到第 4 步。假设仓库有 20 个核心文件平均每个文件 500 行读取后每个文件可能占 3K 到 5K Token。20 个文件就是 60K 到 100K Token。如果 Agent 的设计比较粗糙把每个文件内容原封不动地放进对话历史再叠加分析结果和工具调用记录很快就会撞上上下文上限。更麻烦的是当你尝试压缩或截断时常常会引入新问题一个文件读到一半被截断函数定义和引用对不上号分析结论和原始代码的对应关系丢失Agent 开始“胡说”之前用户明确说过的约束条件因为截断被丢弃生成的方案完全跑偏。这就是最近的开发者社区里频繁讨论“上下文工程”“上下文压缩”“长对话管理”这些关键词的原因。GitHub 上和 Agent 项目相关的讨论中上下文管理已经被提到了和工具调用、模型选择并列的高度甚至更高Agent 的稳定性一半取决于模型另一半取决于你怎么管理喂给模型的上下文。3. GitHub 热门 Agent 项目的共同转向如果你跟踪 GitHub 上几个代表性 Agent 项目的更新会发现它们不约而同在做同一件事把上下文从黑盒变成可观测、可干预、可压缩的资源。我梳理出三条主线。3.1 主线一显式上下文压缩以前处理长对话大家习惯直接做 Token 截断把最早的对话丢掉。这是最粗暴也最危险的做法因为最早的内容往往是任务定义和核心约束。现在更多项目采用“摘要式压缩”或“分层压缩”早期对话实时总结成结构化摘要每个工具调用记录只保留关键结果不保留完整输出定期触发压缩把已完成的步骤压缩成一条状态描述只保留当前还在进行中的任务细节。这种做法的核心是一个简单判断上下文窗口里应该保留的是“模型下一步做决策所需的信息”而不是“任务执行至今所有的历史痕迹”。如果你用过 Claude Code会发现它的/compact命令就是这种思路的产品化——在不缩减核心诉求的情况下压缩对话历史。从社区反馈看这个命令能显著减少长任务中断的概率。其他开源框架也在跟进类似机制。3.2 主线二结构化上下文替代纯文本上下文很多开源 Agent 框架开始把上下文从纯文本改为结构化对象用户需求、项目事实、工具结果、临时状态分别存储在不同字段里按需加载到提示词中。这个改变看起来不大但效果非常明显。因为模型读一份结构清晰的 JSON 上下文和读一份几百行混杂文本的对话记录理解效率和精确度完全不一样。开发者也可以在每次请求前精确控制哪些字段进上下文哪些字段不进。有项目甚至设计了上下文目录类似 manifests把上下文按模块划分Agent 执行不同拓扑任务时只加载相关模块。这就好比把杂乱无章的桌面整理成了带标签的文件柜——找东西不再需要翻遍整个桌面。3.3 主线三上下文可观测性趋势里另一个关键变化是上下文可视化。开发者现在能看到每次请求向模型发送了多少 Token、哪些模块占比最大、哪些工具结果最占空间、压缩之后节省了多少。在没有可观测性的时代上下文管理全凭感觉上下文爆了就截断截断后模型变笨了也不知道是被截掉的内容导致的。现在主流框架正在给上下文加“仪表盘”从数据层面告诉你上下文在哪一步膨胀、在哪一步失真、在哪一步可以优化。这三条主线叠加在一起就是“上下文工程”这个概念的核心不是买更大的窗口而是把已有的窗口用得更聪明。这也回应了文章标题——别让 Agent 在黑盒里找上下文我们应该打开盒子看清里面有什么再决定留什么、扔什么、压缩什么。4. 如何落地一个最小可用的上下文管理设计理论讲完进入实操。我不会给你一个虚构的完整框架代码而是从工程视角给你一套可以自己实现的最小上下文管理组件。这套设计可以嵌入现有的 Agent 流程不需要推倒重来。4.1 设计目标我们需要实现三个能力对上下文进行分层存储。在请求前决定哪些层进入模型。在上下文过大时自动做摘要压缩。4.2 分层数据结构首先定义上下文的分层结构。实际项目中可以将上下文设计成如下 JSON 结构{ meta: { session_id: sess_001, task_name: code_review_agent, created_at: 2025-01-01T10:00:00Z }, core: { user_requirement: 分析仓库遗留问题并输出整改报告, constraints: [ 只分析 src 目录, 输出格式为 Markdown 表格, 不修改任何源码文件 ] }, history: [ { turn: 1, user: 请查看仓库结构, assistant: 已读取文件列表共 20 个文件 } ], tool_results: [ { tool: read_file, target: src/main/java/Application.java, summary: 读取成功500 行包含 2 个 TODO, raw_excerpt: public class Application { ... } } ], state: { current_step: analyzing, pending_files: [Service.java, Controller.java], collected_findings: 5 } }设计要点core是核心诉求永远不能压缩。即使对话历史被截断模型仍然能看到用户最初的需求和约束。这就是为什么core必须独立于history。history是对话记录可以压缩、可以截断但至少保留结构化摘要。tool_results是工具调用结果这里最容易膨胀。设计时建议只保留summary字段进上下文完整的raw_excerpt按需加载而不是默认塞进模型。state是当前任务状态模型每一步决策都需要它。如果state丢失Agent 会像断线重连一样完全不知道进行到哪一步。4.3 按需加载上下文的实现思路有了分层结构下一步就是写一个“上下文构建器”在请求模型前动态决定哪些信息进入提示词。这里给出一个 Python 实现的最小示例# 文件路径context_builder.py class ContextBuilder: def __init__(self, max_tokens12000): self.max_tokens max_tokens def build_prompt(self, session: dict) - str: # core 信息永远完整保留 core_text self._format_core(session[core]) # history 做摘要式保留 history_text self._format_history(session[history]) # tool_results 只保留 summary 字段 tool_text self._format_tool_summaries(session[tool_results]) # state 永远保留 state_text self._format_state(session[state]) prompt f 任务信息 {core_text} 历史对话摘要 {history_text} 工具执行结果 {tool_text} 当前任务状态 {state_text} return self._trim_to_fit(prompt) def _format_core(self, core: dict) - str: req core[user_requirement] constraints \n.join([f- {c} for c in core[constraints]]) return f需求{req}\n约束条件\n{constraints} def _format_history(self, history: list) - str: # 只保留最近 5 轮更早的压缩为一行摘要 recent history[-5:] lines [] for turn in recent: lines.append(f第{turn[turn]}轮用户说{turn[user]}助手回复{turn[assistant]}) if len(history) 5: lines.insert(0, f更早的 {len(history) - 5} 轮对话已省略) return \n.join(lines) def _format_tool_summaries(self, results: list) - str: lines [] for r in results[-10:]: # 只保留最近 10 条工具结果摘要 lines.append(f[{r[tool]}] {r[target]}{r[summary]}) return \n.join(lines) def _format_state(self, state: dict) - str: return f当前步骤{state[current_step]}待处理文件{, .join(state[pending_files])}已发现问题的数量{state[collected_findings]} def _trim_to_fit(self, prompt: str) - str: # 如果超过窗口上限优先截断 tool_results 部分不截断 core if len(prompt) self.max_tokens * 3: # 粗略按字符估算 return prompt[: self.max_tokens * 3] \n... (上下文已截断) return prompt这段代码的核心逻辑是按优先级分配 Token 预算。core和state永远保留history和tool_results允许裁剪但以摘要形式保留关键信息而不是直接丢弃。执行 Agent 主循环时每轮先构建新的上下文再请求模型# 文件路径agent_loop.py from context_builder import ContextBuilder session load_session(sess_001) builder ContextBuilder(max_tokens12000) for step in range(10): prompt builder.build_prompt(session) result call_llm(prompt) # 更新工具结果和状态 if result.tool_calls: for tool_call in result.tool_calls: tool_result execute_tool(tool_call) session[tool_results].append({ tool: tool_call.name, target: tool_call.target, summary: summarize(tool_result), raw_excerpt: tool_result[:200] }) session[state][current_step] fafter_tool_{tool_call.name} else: break save_session(sess_001, session)这里有一个容易被忽略的细节每次工具执行完一定要更新state。很多 Agent 跑着跑着就乱了不是因为模型笨而是因为state还停留在上一步模型拿着过期状态继续决策自然越跑越偏。4.4 上下文压缩触发器上下文压缩不能每次请求都做开销太大也没必要。建议设置一个触发阈值比如上下文占用达到窗口的 70% 时触发一次压缩。压缩策略可以是“历史摘要化 工具结果清理 状态精简”。# 伪代码压缩逻辑 if context_token_count max_tokens * 0.7: compress_history(session[history]) prune_tool_results(session[tool_results], keep_last5) clean_state(session[state]) notify_user(上下文已自动压缩)剪枝时要注意prune_tool_results可以裁剪掉已完成步骤的工具结果但与当前state相关的工具结果必须保留。否则压缩完上下文Agent 手里没有足够依据继续执行就会开始“发挥”。5. 显式上下文传递框架层怎么做除了自己的上下文管理器之外我们还需要了解主流框架的配置方式。现在多数 Agent 框架支持上下文参数配置。以 API 调用为例显式控制上下文的常见方式如下# 文件路径llm_client.py from openai import OpenAI client OpenAI() response client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: build_system_prompt()}, {role: user, content: current_task_prompt}, ], max_tokens2048, temperature0.2, ) print(response.choices[0].message.content)这里的关键点是 messages 数组内容是我们前面用 ContextBuilder 构建好的 prompt。不要直接把整个 session 的原始数据传给模型那等于把问题原封不动地抛给了窗口管理——你还是在黑盒里挣扎。有些框架支持配置上下文的关联数量限制比如 Spring AI 提供的相关参数。这个配置的本质就是控制“历史消息里最多带多少条进入模型”。如果业务场景中历史消息的价值不大可以把这个数字调小减少 Token 消耗也减少干扰。以配置文件为例spring.ai.client.chat.memory.max-messages10 spring.ai.client.chat.memory.max-tokens4000不同版本配置项名称会有差异重点是理解它的语义限制系统为模型构建历史消息时的数量上限。当你发现模型经常被早期无关对话干扰时应该调小当你发现模型需要完整回顾整个需求链路时再适当调大。6. 效果验证怎么判断上下文管理真的有效写完代码之后不能只看“能跑”。需要建立一套可量化的验证方式。我在实际工作中会从三个维度评估。第一长任务完成率。选 10 个需要多步工具调用的任务对比“无上下文管理”和“有上下文管理”两种模式下任务完整跑通的数量。通常引入分层管理和摘要压缩之后完成率会有明显提升。第二Token 消耗量。记录每次会话的总 Token 消耗。上下文管理做得好的系统Token 消耗会显著下降因为不再反复把历史完整输出发送给模型。第三输出一致性。让 Agent 在长对话后半段复述最初的需求约束看它是否能准确回忆。如果上下文管理有效即使历史已经压缩过模型仍然能说出核心约束如果上下文管理失效模型会含糊其辞或直接编造。验证命令可以简单直接# 记录一次会话的 token 使用情况输出到日志文件 python -c import json; datajson.load(open(session_metrics.json)); print(total_tokens:, data[total_tokens]); print(prompt_tokens:, data[prompt_tokens]); print(completion_tokens:, data[completion_tokens])判断成功的标准是上下文被压缩后核心约束不丢失任务可继续执行不需要人工干预重新说明需求。如果压缩后 Agent 行为明显变差优先检查你的core字段是否被误裁剪或者tool_results中与当前步骤相关的摘要是否被误删。7. 常见问题与排查方法在这里把 Agent 上下文相关的常见问题整理成表格。这些问题如果你遇到可以直接对照排。问题现象可能原因排查方式解决方案提示 context window exceeded发送给模型的 Token 超过模型窗口上限检查请求日志中 messages 的 token 统计增加上下文压缩逻辑限制历史轮数和工具结果摘要长度Agent 中途“失忆”忘了最初需求core 信息没有被独立保留和 history 一起被截断查看压缩逻辑确认 core 字段是否始终进入 prompt将用户需求、约束条件单独存储并在每次构建 prompt 时强制注入新开会话后上下文完全丢失没有持久化会话状态检查 session 存储逻辑将 core、state、history 持久化到本地文件或数据库支持恢复工具调用结果导致上下文膨胀tool_results 完整输出被塞入上下文统计工具结果的 token 占比只保留 summary 和关键 excerpt完整结果按需加载压缩后 Agent 行为变差压缩策略过于激进对比压缩前后同一任务的输出保留最近轮次详情只压缩更早的历史且压缩成摘要而非删除多步骤任务越跑越偏state 没有及时更新打印每个步骤后的 state 内容每次工具调用后强制更新 current_step、pending 信息8. 最佳实践与工程建议上下文管理是一个需要持续打磨的部分这里给出几条踩过坑之后觉得最值得遵循的建议。8.1 把“核心诉求”当作不可压缩的资产用户最初的需求、约束、格式要求是一份会话里最重要、最不可再生的信息。无论上下文多紧张都不要压缩、不要截断、不要改写成摘要时丢细节。一个稳妥的做法是把core字段从对话历史的截断范围中排除单独存储单独注入。8.2 工具结果是最大元凶优先治理Agent 开发中上下文膨胀 80% 来自工具调用结果。每个工具的返回结果都要有两个版本完整版用于展示或调试摘要版用于进入上下文。不要把工具的完整 stdout 或者完整文件内容直接塞给模型永远先摘要。8.3 压缩要保留“当前决策所需”而不是“过往全部信息”好的压缩不是简单截断而是保留对下一步决策有用的信息。比如“已经分析完 5 个文件”这个事实比这 5 个文件的完整内容重要得多。Agent 下一步只需要知道“分析到哪个文件、剩余几个、已发现什么问题”这三个信息足够支持下一轮决策。8.4 可观测性是上下文工程的前提建议在开发阶段为每个请求增加日志输出prompt_tokens、completion_tokens、total_tokens以及context 各模块占比。看不到数据就无法优化。没有可观测性的上下文管理和黑盒管理没有本质区别。8.5 配置项要区分开发环境和生产环境上下文压缩阈值、历史保留轮数开发环境可以保守一点尽量不做激进压缩生产环境要根据成本和质量平衡优先保障稳定性。配置建议放在独立的配置中心或环境变量中不要写死在代码里CONTEXT_MAX_TOKENS12000 CONTEXT_COMPRESS_THRESHOLD0.7 CONTEXT_KEEP_HISTORY_TURNS10 CONTEXT_KEEP_TOOL_RESULTS58.6 使用 Agent 框架时先弄清它的默认上下文策略不同框架对上下文的默认策略差异很大。有些框架默认把完整对话历史放进请求有些则已经做了摘要和压缩。接入框架前先读源码或文档里和 context、prompt、memory 相关的部分确认默认行为而不是猜测。9. 总结与后续学习方向这个时代的 Agent 开发真正拉开差距的不是谁调用的模型更强而是谁拥有更好的上下文管理能力。模型窗口再大也架不住无限制地向里塞数据函数调用设计得再好如果上下文状态混乱Agent 一样会中途“精神分裂”。写这篇文章的核心目的是把一个容易被人忽视的观点摆到台面上Agent 的上下文不是黑盒它应该被当成一种资源、一种需要工程设计和管理的基础设施。分层存储、摘要压缩、按需加载、可观测性这些不是概念而是工程手段。至少把它们用在你的下一个 Agent 项目里你会发现长任务中断率明显下降输出稳定性提升Token 消耗也会更可控。接下来继续深入的话建议按三个方向走熟悉主流框架的上下文策略源码。做一个自己的上下文监控面板。尝试把上下文压缩做成自适应策略按会话内容类型自动选择保留粒度。不管选哪个方向核心原则不变不要让模型在一个乌漆墨黑的盒子里寻找有用的片段而是主动替它把上下文整理成清晰、简洁、可决策的信息。建议收藏这篇下次写 Agent 项目之前翻出来按“分层 摘要 压缩 监控”四步过一遍你会发现很多问题在出现之前就被扼杀了。