Context Engineering:上下文工程
2026 年 Agent 工程的核心技能:上下文压缩策略、Token 预算精细管理、长任务状态管理。参考 easy-agent 的 compaction + budget 实现。
2026 年 Agent 工程的核心技能:不是给模型更多上下文,而是给模型对的上下文。
为什么需要上下文工程
你的 Agent 跑了 30 分钟,调了 50 次工具,消息数组已经膨胀到 15 万 token。接下来会发生什么?
- API 报错:超过模型的上下文窗口上限(如 200K token)
- 成本爆炸:每次调用都要付 15 万 token 的输入费用
- 质量下降:lost in the middle——关键信息被淹没在中间,模型找不到
上下文工程解决这三个问题。它不是一门玄学,是一套可量化的工程实践。
Token 预算:先搞清楚你有多少钱
easy-agent Step 11 的实现:
const MODEL_CONTEXT_WINDOW = 200_000;
const AUTOCOMPACT_BUFFER_TOKENS = 13_000;
function buildTokenBudgetSnapshot(messages) {
const estimatedTokens = tokenCountWithEstimation(messages);
const effectiveWindow = MODEL_CONTEXT_WINDOW - 20_000; // 留给回复
return {
estimatedTokens,
autoCompactThreshold: effectiveWindow - AUTOCOMPACT_BUFFER_TOKENS,
};
}
三层扣减:总窗口 → 留回复空间 → 留压缩缓冲。剩下的才是你能「花」的。
Token 估算:不精确但够用
你不可能每次都调 API 来查精确 token 数(那本身就要花钱)。easy-agent 用字符数启发式:
const TEXT_CHARS_PER_TOKEN = 4; // 英文约 4 字符 = 1 token
const JSON_CHARS_PER_TOKEN = 2; // JSON 结构更密,约 2 字符 = 1 token
function roughTokenCount(content) {
return Math.max(1, Math.round(content.length / TEXT_CHARS_PER_TOKEN));
}
然后混合锚定:拿最近一次 API 返回的 usage.input_tokens(精确值)作为锚点,只估算锚点之后新增的消息:
function tokenCountWithEstimation(messages, { usage, usageAnchorIndex }) {
if (usage && usageAnchorIndex !== undefined) {
const knownTokens = usage.input_tokens; // 精确
const suffix = messages.slice(usageAnchorIndex + 1);
return knownTokens + estimateMessagesTokens(suffix); // 估算
}
return estimateMessagesTokens(messages); // 全估算
}
压缩策略:两级火箭
第一级:微压缩(零成本)
把旧的工具结果替换为占位符——不调 API,纯本地操作:
const CLEARED_PLACEHOLDER = "[Old tool result content cleared]";
function microCompactMessage(message) {
return {
...message,
content: message.content.map(block => {
if (block.type === "tool_result" && isOldTool(block)) {
return { ...block, content: CLEARED_PLACEHOLDER };
}
return block;
}),
};
}
只清旧工具结果(Read/Grep/Bash),不清用户消息和模型回答。模型已经看过这些工具结果了,后续对话不需要原始内容。
第二级:全压缩(花一次 API 调用)
微压缩不够时,让模型自己总结历史:
async function compactMessages(messages, callModel) {
// 1. 先微压缩
const microCompacted = microCompactMessages(messages);
// 2. 让 AI 总结前半段
const summary = await callModel(
"请详细总结这段对话:用户请求、技术决策、文件名、错误、待办事项…",
microCompacted
);
// 3. 保留最近 8 条消息(原文不压缩)
const tail = microCompacted.slice(-8);
// 4. 组装新数组:摘要 + 最近对话
return [
{ role: "user", content: "以下是之前对话的摘要:\n" + summary },
{ role: "assistant", content: "[CompactBoundary]" },
...tail,
];
}
碎开来说几个关键细节:
为什么保留最近 8 条? 模型对最近上下文的利用率最高(开头结尾效应)。最近几轮对话是当前任务的直接上下文,压掉会丢失正在进行的工作状态。
为什么用 AI 总结而不是规则截断? 规则截断(只留最近 N 条)会丢失重要信息:用户最初的目标、关键决策、已尝试的方案。AI 总结能提取这些语义信息。
CompactBoundary 标记有什么用? 告诉模型「这里发生过压缩」——模型看到这个标记就知道前面的内容是摘要不是完整对话,不会误以为信息被遗漏。
tool_use / tool_result 配对保护
压缩时最容易犯的错:把 tool_use 和 tool_result 拆散了。API 要求这两个必须成对出现。
function findSafeTailStart(messages, desiredCount) {
let start = messages.length - desiredCount;
while (start > 0) {
const tail = messages.slice(start);
// 检查是否有悬空的 tool_result(没有对应的 tool_use)
const hasDangling = checkForDanglingToolResults(tail);
if (!hasDangling) return start;
start--; // 往前多保留一条,直到配对完整
}
return 0;
}
本章要点
- 上下文工程 = 在有限的 token 窗口里,给模型最相关的信息
- Token 预算三层扣减:总窗口 → 回复预留 → 压缩缓冲
- 混合估算:API 精确值做锚点 + 字符启发式估增量
- 两级压缩:微压缩(免费,清旧工具结果)→ 全压缩(AI 总结 + 保留最近 8 条)
- tool_use / tool_result 必须成对——压缩时不能拆散
→ 下一章:RAG 深水区