Agent 上下文窗口的动态预算管理与 Token 级配额工程 2026
把上下文窗口当成可观测、可计量、可治理的运行时资源池,用四元组预算 + 滑动回收 + 跨智能体公平队列,让长流程 Agent 在 200K 至 1M 模型下既不因历史膨胀而崩溃,也不因预留不足而输出截断。
约 30 分钟阅读8,893 字4 次阅读博主

把上下文窗口当成可观测、可计量、可治理的运行时资源池,用四元组预算 + 滑动回收 + 跨智能体公平队列,让长流程 Agent 在 200K 至 1M 模型下既不因历史膨胀而崩溃,也不因预留不足而输出截断。

一句话摘要:把上下文窗口当成一个可观测、可计量、可治理的"运行时资源池",用四元组预算 + 滑动回收 + 跨智能体公平队列,让长流程 Agent 在 200K/1M 上下文模型下既不会因历史膨胀而崩溃,也不会因预留不足而输出截断。
2026 年主流大模型上下文窗口从 128K 普遍跃升到 200K(Claude Opus 4.1、Sonnet 4.5、GPT-5、Qwen3-Max),部分闭源旗舰模型甚至支持 1M(Gemini 2.5 Pro 已稳定)。但凡是做过生产级 Agent 系统的人都会发现一件事:窗口越大,工程复杂度不是越低而是越高。这不是悖论——窗口变大掩盖了三个本来该被解决的问题,直到长流程任务把它们一次性放大:
第一,历史对话膨胀。一个 30 步的 ReAct Agent 在每次 tool call 后都要把 tool 结果回填进 messages,几轮下来历史段会占据 60-80% 的窗口。看起来窗口还绰绰有余,但用户在前端的响应延迟(P50 latency)已经因为 prompt 体积爆炸而从 800ms 涨到 3.5s——prefill 阶段的复杂度不是线性的,而是近似 O(n²) 的 attention 计算主导。
第二,工具结果碎片化。一次成功的 shell 调用可能产生 30KB stdout(grep 一个大目录),一次失败的 HTTP 重试可能把 stack trace 也塞回上下文。如果不主动裁剪,单次 tool call 就可能吞掉 5-10% 的窗口;连续几次重试后整个 session 直接打爆。
第三,输出预留的两难。Agent 在执行到第 25 步时突然发现工具返回了一个 60K token 的 JSONL 数据集,它想"再迭代几次分析这个数据集",但系统 prompt + 历史 + 工具结果已经吃掉 160K,剩下 40K 不够生成 8K token 的最终报告——此时模型要么开始截断(结构性丢失),要么在 mid-output 时刻被打断(用户体验崩塌)。
这三个问题在单次调用、单轮对话、单用户场景下都不显著。但一旦进入长流程、多智能体、并发租户的工业级 Agent 运行时,上下文预算管理就从"调优项"升级为"基础设施项"。它跟 request-level 的 quota(id=613 那种 token 配额)是两个完全不同的抽象层——request-level quota 解决"这个用户本月用多少 token",上下文预算解决"这一次 LLM 调用里 200K 窗口怎么切"。两者必须共存但不可互相替代。
本文要回答的就是这个问题:怎么把"上下文窗口"从一个黑盒参数变成一个可观测、可计量、可治理的运行时资源池。核心抓手是三件事——形式化定义预算四元组、设计滑动回收与多层压缩、把跨智能体协作变成可公平调度的资源协商协议。文中所有机制都在生产环境中验证过(截至 2026 年 8 月,覆盖 Anthropic Claude Agent SDK 0.7+、OpenAI Agents SDK 1.4+、LangGraph 0.4+ 三套栈),并把每一步的工程细节、踩坑、监控指标都摊开来。
我们用一个四元组 来描述一次 LLM 调用的上下文预算分配:
约束条件:
其中 是模型的硬上限(200000、1000000 等), 是 256-1024 token 的安全 buffer(给模型的 safety preamble 和潜在的 hidden state padding)。注意 不是"模型输出占用",而是"在 prompt 视角上为模型输出预留的位置"——LLM 推理引擎在打包 prompt 时会按 max_tokens 参数从尾部往前预留位置,所以 必须显式计算而不是估算。
四元组里每一项都有独立的生命周期:
| 字段 | 写入时机 | 回收时机 | 衰减策略 |
|---|---|---|---|
| session 启动 | session 结束 | 不衰减 | |
| 每轮对话/tool 调用后追加 | LRU + 摘要压缩 | 按时间窗口 + 相关性打分 | |
| 每次 tool schema 变更后重写 | tool 不可用时移除 | 动态加载/卸载 | |
| 每次调用前根据任务估算 | 调用结束后释放 | 任务感知(code gen vs Q&A) |
这个四元组在调用前是一个"声明",在调用中是一组"已用/剩余"计量,在调用后是一份"审计日志"。整个 Agent runtime 必须能实时回答"现在 还剩多少"、"能不能再 append 一个 tool result"、"这一轮 够不够生成 2000 token 的报告"。
形式化的好处是它把"上下文窗口"从一个单标量(200000)变成了一个向量。当我们做动态预算调整时,调整的是这个向量的分量配比,而不是整个窗口的开关。这也意味着我们可以独立地为 、、、 设计各自的治理策略,不会因为压缩 而误伤 ,也不会因为预留 而挤压 。
实践中一个常见错误是把四元组合并成单一标量"已用 token 数"。这种做法看起来简单,但在多智能体场景下立刻崩盘——子 Agent 的 可能很短、 很小,但 极长(因为它继承了父 Agent 的对话历史切片)。如果不区分字段,你无法判断"这一调用到底在用什么资源",也无法做精细化的告警与限流。
光有四元组还不够,还需要一个 动态分配器(allocator),它的工作是:
分配器的核心数据结构是一个 token-aware LRU cache。每条历史消息(user / assistant / tool 三类)都有三个属性:
tokens:精确 token 数(用模型 tokenizer 算,不是估算字符数 × 0.75)last_accessed_ts:上次被任何后续消息引用的时间戳relevance_score:与当前任务的语义相关性(用一个小 embedding 模型打分)分配策略的伪代码骨架:
def allocate(budget: Budget, history: list[Message]) -> Budget:
used_s, used_h, used_t, used_r = budget.used()
# 1. 估算 output 预留:基于任务类型 + 最近 5 轮 assistant 平均输出长度
target_output = estimate_output_budget(history, task_meta)
budget.r = clamp(target_output + 512, min=1024, max=16384)
# 2. 如果 H 占用超过 70% 的可用空间,触发滑动回收
available = budget.c - budget.s - budget.t - budget.r - budget.epsilon
if used_h > 0.7 * available:
history = sliding_reclaim(history, target=0.5 * available)
# 3. 如果 S + T + R + 新 H 仍 > C,触发结构性降级
if total(budget) > budget.c:
budget = structural_degrade(budget, history)
return budget, history
sliding_reclaim 不是简单的"删最早的"。它做四层淘汰:
last_accessed_ts 排序,丢弃最久未被引用的;system、tool 定义、被显式标记为 persistent 的消息。这种"分层淘汰 + 结构保留"的设计在 LangGraph 0.4+ 的 StateGraph 里已经被官方采纳为 trim_messages + RemoveMessage 的默认实现思路。OpenAI Agents SDK 的 RunContextWrapper.context 也实现了类似的机制,但它的 API 表面更简洁——通过 @input_guardrail 和 tool_use_behavior 控制 token 预算。
踩坑点 1:不要用字符数估算 token。一个中文技术文档段落(UTF-8)平均 1.6-2.0 字符/token,一个 Bash 输出(英文 + 数字 + 标点)平均 3.5-4.5 字符/token。用 len(text) // 4 估算会把中文段落低估 2-3 倍,结果就是"明明算着没满窗口,结果模型报 context_length_exceeded"。唯一可靠的方式是直接调用模型的 tokenizer(tiktoken、anthropic-tokenizer、或模型 API 自己的 count_tokens 端点)。在生产 Agent runtime 里,tokenizer 通常以 sidecar 进程或本地 WASM 模块的形式部署,避免每次都打远程 API。
踩坑点 2:tool result 的"摘要保留"不是简单 truncate。一个 30KB 的 grep 输出可能前面 80% 都是无信号的 --help 信息或 debug log,中间 10% 是真正匹配的行。简单的 result[:5000] 会把核心结果截掉。正确做法是用模型自己来"摘要 tool result"——但这是个悖论:你调用模型来摘要,又消耗了窗口。工程上有两种解法:
生产系统通常是两者结合:默认走离线摘要(兜底所有未知 tool),对 top-10 高频 tool 写结构化裁剪(性能优化)。
当 sliding_reclaim 仍不够(意味着历史严重冗余),分配器会触发 多层降级。我们设计了五层降级路径,从轻到重:
Level 1:相邻 turn 合并。把连续多轮"user 提问 → assistant 简短回答 → user 再提问"合并成一段摘要:"用户在第 12-15 轮询问了 X 的三个细节,得到以下结论:A, B, C"。这一步对客服、问答类 Agent 特别有效,可以把 4-6 轮历史压缩成 1 段 100-200 token 的摘要。
Level 2:tool call 序列摘要。把连续多步 tool 调用合并成"执行轨迹"。"Agent 在第 5-10 步执行了以下操作:grep 匹配文件列表 → cat 前 3 个文件 → 发现 X → 重新 grep 带 -v flag → 验证"。这段摘要一般 80-300 token,原始 6 步 tool call 可能占 8000+ token。
Level 3:跨段 history 总结。用一个 system-level 的 summarizer agent 对当前 H 做整体总结,类似 Claude 的 compact 功能。这是最重的压缩,会把整个 H 段替换成 1-3 段共 500-1500 token 的"会话摘要 + 关键决策点 + 待办事项"。这一步对长流程任务特别重要,因为很多"工具失败重试"的中间步骤对最终决策没贡献,但占据了大量 token。
Level 4:tool 卸载。当 T 字段接近预算上限时,分配器会卸载"低频 tool"——比如把 30 个可用 tool 中使用率 < 5% 的 tool 临时标记为 disabled,需要时通过 tool_use_behavior 重新加载。OpenAI Agents SDK 的 tool_choice 配合 enabled_tools 列表可以做到这件事;LangGraph 通过 ToolNode 的 tools_condition 也能实现。
Level 5:system prompt 极致压缩。当四元组都到顶、且 H 仍 > 80% 时,进入"应急模式"——把 system prompt 中的 examples(few-shot)全部移除,只保留 instructions 骨架;移除 reasoning instructions;移除 formatting instructions。这一步通常不被采纳,因为损失太多引导能力;只有当用户明确表示"这次任务比提示词更重要"时才会触发。
每层降级都有代价。Level 1-3 的代价是"模型可能失去一些细节信息",Level 4 的代价是"工具可用性下降",Level 5 的代价是"模型行为可能漂移"。生产系统会把这些代价转化为可量化的指标:
task_completion_rate:必须保持在原始的 95% 以上;detail_recall_score:用一个 holdout 问题集测,召回率必须 > 80%;tool_availability_rate:当前任务所需 tool 必须 100% 可用;关键工程纪律:降级是"显式的、可审计的",不是隐式的、不可见的。每次降级都必须在调用日志里打一条结构化事件:{ event: "context_degrade", from_level: 1, to_level: 3, reason: "H exceeded 80% of available", tokens_saved: 4200, history_segments_merged: 6 }。这些事件在事故复盘时是金矿——你能精确知道"哪一次 session 在哪一步降级、为什么降级、降级后效果如何"。
多智能体系统是 2026 年 Agent 工程的主战场,但也是上下文预算最容易翻车的地方。最常见的反模式:
父 Agent 把整个对话历史传给子 Agent,子 Agent 把自己的 tool result 全部回填给父 Agent,循环 5 层后,单个 LLM 调用的 prompt 可能达到 800K token——这在 200K 模型上直接报 context_length_exceeded,在 1M 模型上也只是勉强能跑,但延迟高到不可用。
正确做法是把跨智能体的上下文流转视为资源协商而不是数据传递。每个智能体在调用子智能体时,必须显式声明:
这本质上是一个带配额的消息总线。我们实现过的一个生产级模式叫 scoped history injection:
async def call_subagent(parent_ctx, sub_agent, task, budget_caps):
# 1. 父 Agent 准备一段 "scoped history":只包含与子任务相关的消息
scoped_history = await select_relevant_history(
parent_ctx.history,
task=task,
budget_caps.history
)
# 2. 子 Agent 收到的是一个独立 context,不是父 context 的引用
sub_ctx = AgentContext(
system=sub_agent.system_prompt,
history=scoped_history,
tools=sub_agent.tools,
budget_caps=budget_caps # 子 Agent 总预算上限
)
# 3. 子 Agent 完成后,回传"摘要 + 关键产物",不是完整 history
result = await sub_agent.run(sub_ctx, task)
parent_ctx.append_summary(result.summary)
parent_ctx.append_artifacts(result.artifacts)
return result
这个模式的关键设计点是 "子 Agent 看到的是 isolated context,父 Agent 收到的是 projected summary"。子 Agent 不感知父 Agent 的全貌,只看到自己被授权的那一段;父 Agent 不接收子 Agent 的全部 I/O,只接收摘要和显式产物(artifact)。这样一来,整个多智能体系统的总 token 消耗是有界的、可预测的——你可以在调度器层面给每个智能体分配不同的 budget_caps,并监控它们的实际消耗曲线。
公平调度的实现。当我们有 N 个并发 Agent 在共享一个 LLM API 时,还需要一个跨 Agent 的公平调度器。原理类似 OS 的 fair-share scheduler:
weight(根据租户等级、任务优先级);weight 分配每秒 token 预算;这套机制在 Anthropic 的 Claude Agent SDK 0.7+ 里通过 RateLimiter 和 BudgetEnforcer 暴露给开发者;OpenAI Agents SDK 1.4+ 则通过 RunHooks + 自定义 RateLimitMiddleware 实现。LangGraph 没有内置实现,但社区有 langgraph-enterprise 包提供了参考实现。
实战教训:
reserved output () 是四元组里最被低估但最难搞的一项。三个原因:
第一,模型在生成时是流式的。你不能简单预留一个静态的 R 然后看模型生成。LLM 推理引擎在生成第一个 token 后就已经"消费"了 prompt 中的预留位置,但 prompt 本身不变。预留是一个逻辑概念,不是物理预留。
第二,不同任务需要的 R 差异巨大。一个简单的 Q&A Agent 平均输出 100-300 token,一个 code gen Agent 平均输出 2000-5000 token,一个 long-form report Agent 输出 10000-20000 token。如果用静态 R,要么浪费(Q&A 预留了 8K)、要么不够(report 预留了 1K)。
第三,中断恢复的复杂度。如果 Agent 在第 15 轮调用、输出第 8K token 时被打断(用户取消 / timeout / API 错误),恢复时必须知道"已经生成到哪里"、"prompt 里的 R 应当预留多少"、"已生成内容是否要被纳入历史"。
容量规划方法。我们生产中用的是 自适应 R 估算:
def estimate_r(history, task_meta) -> int:
# 1. 任务类型查询表
base_r = TASK_R_ESTIMATES.get(task_meta.type, 2048)
# 2. 历史增强:最近 5 轮 assistant 平均输出 + 1 stddev
recent_outputs = [m for m in history[-10:] if m.role == "assistant"]
if recent_outputs:
avg = mean(m.tokens for m in recent_outputs)
std = stdev(m.tokens for m in recent_outputs)
base_r = max(base_r, int(avg + 2 * std))
# 3. 显式 hint:如果 task_meta.output_target 设了目标长度(如 "8000 token 报告"),用它
if task_meta.output_target:
base_r = max(base_r, task_meta.output_target + 1024) # buffer for stop tokens
# 4. 上下界
return clamp(base_r, min=512, max=16384)
中断恢复。当 Agent 输出被中断时,不能简单地把"已生成内容 + 新一轮 prompt"塞回去,因为:
正确的恢复协议:
{
"resume_state": {
"session_id": "...",
"call_id": "uuid-of-interrupted-call",
"partial_output": "已生成的 token 序列",
"finish_reason_at_interrupt": "user_cancel | timeout | api_error",
"remaining_r": 4096,
"history_append_position": 15
}
}
下一轮调用时,runtime 检测到 resume_state 就把 partial_output 作为 prefix 传入 LLM(用 prompt_caching 或 prefix continuation 机制),R 只预留剩余需要量。这套机制在 OpenAI 的 stream=true + continuation 端点、Anthropic 的 prompt_caching + 自定义 resume 里都能实现。但截至 2026 年 8 月,没有任何主流 SDK 默认暴露 resume API——你必须自己实现这一层。
一个常见但隐蔽的 bug:如果中断发生在"模型说了一半引用了还没生成的工具调用",恢复后模型的 prompt 已经被改了,新一轮的"接着输出"会失去上下文。我们的解法是:partial_output 在恢复前必须由 runtime 做"语法完整性检查"——如果 partial_output 不是一个有效 message(没有闭合的 JSON、没完成的 tool_use block),runtime 会先用一个补全 prompt 让模型"重写一个完整版本",然后再继续。
把上面所有机制落到代码里,需要在三个 SDK 中分别处理。它们的抽象层次不同:
LangGraph 0.4+ 是最底层的,需要你显式管理 StateGraph:
from langgraph.graph import StateGraph, MessagesState
from langgraph.checkpoint import MemorySaver
class BudgetState(MessagesState):
budget: Budget
degrade_level: int
def budget_allocator_node(state: BudgetState) -> BudgetState:
state["budget"], state["messages"] = allocate(
state["budget"], state["messages"]
)
return state
def llm_call_node(state: BudgetState) -> BudgetState:
response = llm.invoke(
messages=state["messages"],
max_tokens=state["budget"].r,
tools=state["budget"].active_tools,
)
state["budget"].record_call(response.usage)
state["messages"].append(response.message)
return state
graph = StateGraph(BudgetState)
graph.add_node("allocate", budget_allocator_node)
graph.add_node("llm_call", llm_call_node)
graph.add_edge("allocate", "llm_call")
graph.add_conditional_edges("llm_call", should_continue_or_compact)
LangGraph 的优势是完全可控——你能精确决定四元组的每一个字节;劣势是所有事情都要自己写——压缩策略、token 估算、tool 卸载全部要手写。
OpenAI Agents SDK 1.4+ 是中间层抽象,通过 RunContextWrapper + input_guardrail + tool_use_behavior:
from agents import Agent, Runner, function_tool, RunContextWrapper
from agents.guardrail import InputGuardrail
@function_tool
async def my_tool(ctx: RunContextWrapper, query: str) -> str:
# 在 tool 里能访问 ctx.usage,是 OpenAI Agents 的核心抽象
if ctx.usage.total_tokens > ctx.budget.hard_cap * 0.8:
return await summary_tool(query) # 自动降级
return await real_tool(query)
budget_guardrail = InputGuardrail(
check=lambda ctx: ctx.usage.total_tokens < ctx.budget.hard_cap
)
agent = Agent(
name="research-agent",
instructions=dynamic_system_prompt, # 系统 prompt 也是动态生成
tools=[my_tool],
input_guardrails=[budget_guardrail],
)
优势是比 LangGraph 简洁很多,大量 boilerplate 由 SDK 提供;劣势是抽象层次稍高,某些精细控制做不了(比如直接操纵 LRU cache)。
Claude Agent SDK 0.7+ 提供了 BudgetEnforcer + compact 内置机制:
import claude_agent_sdk as cas
agent = cas.Agent(
system=cas.Prompt(
base="You are a research assistant",
dynamic={"current_task": task_description},
),
budget=cas.Budget(
max_input_tokens=180_000,
reserve_output_tokens=8192,
compact_threshold=0.75, # 当 context 占到 75% 时自动 compact
compact_strategy="structured_summary",
),
tools=[cas.Tool.from_function(my_tool)],
)
优势是官方支持最完整的上下文预算管理(毕竟 Anthropic 是最早做大上下文窗口的厂商,他们踩过的坑最多);劣势是生态相对封闭,自定义成本高。
生产系统的真实形态通常是混合栈——LangGraph 做编排层(control flow)、Claude Agent SDK 做单 Agent 核心(LLM + tools)、OpenAI Agents SDK 做某些 specialized sub-agent(如果团队熟悉)。这听起来复杂,但每层抽象都有自己的位置,避免在 LangGraph 里重写 compact 逻辑或在 Claude SDK 里硬塞 multi-agent orchestration。
上下文预算管理 ≠ LLM 网关配额管理。两者经常被混淆,但实际上是两个抽象层:
两者必须共存,但不可互相替代。如果只用网关配额,你会有 quota 但每调用仍可能爆;如果你只用上下文预算,你会有精细控制但仍可能月超额。
类似的边界还有:
一个常被忽视的点:上下文预算管理直接影响 prompt caching 的命中率。如果你动态调整 的内容(比如改 system prompt 的 few-shot examples),会导致 cache miss,整个 prefill 阶段的延迟会重新计费。因此在工程上,system prompt 应当尽量稳定,dynamic 字段应当单独放、不与稳定字段混在一起——这是一个看似无关但实际能让延迟减半的设计选择。
把上下文预算生产化,最关键的是让 SRE 能看见它。以下是经过生产验证的最小可观测性集:
必备指标(Prometheus/Grafana 形式):
agent_context_budget_total_tokens(按 session 标签拆分):当前 200K 窗口里的总占用agent_context_budget_by_field_tokens{field="system|history|tools|output"}:四元组各自的占用agent_context_degrade_total{from_level, to_level}:降级事件计数,按 from/to level 拆分agent_context_compact_duration_seconds:compact 调用的耗时分布agent_context_tool_result_size_bytes:每次 tool result 的字节数分布agent_context_reserved_output_utilization:reserved output 的实际使用率(生成 token / 预留 token)agent_context_cache_hit_rate:prompt caching 的命中率agent_context_subagent_token_share{parent_id}:子 Agent 在父 Agent 预算里的占比必备告警:
必备 dashboard:
事故复盘清单:
当生产事故发生(比如"某个 session 突然 5xx"),SRE 应能在 30 秒内回答:
如果任何一个问题答不上来,可观测性还不够,需要补点。
Conversation
0 条