跳到主要内容《Agent Runtime 工程化》第五章 上下文工程 | 极客日志编程语言Agent Runtime工程化陈堂会
《Agent Runtime 工程化》第五章 上下文工程
长任务跑久了,问题会变成一句话:有限的 context window 里,到底该放什么? Agent Runtime 的上下文不是聊天记录的简单拼接。它更像一份交给模型的案卷:用户当前目标、必须遵守的约束、最近进展、关键文件、工具观察、失败记录、可用工具、当前计划。案卷太薄,模型会失忆;案卷太厚,成本上升,关键事实还可能被噪声挤掉。 上下文工程不是塞更多东西
站点编辑3 浏览

书名:《Agent Runtime 工程化:从工具调用循环到可恢复执行系统》
作者:陈堂会
平台:极客日志(https://zeeklog.com)
X:@Megick_com
联系邮箱:[email protected]
章节:第五章 上下文工程
第五章 上下文工程
长任务跑久了,问题会变成一句话:有限的 context window 里,到底该放什么?
Agent Runtime 的上下文不是聊天记录的简单拼接。它更像一份交给模型的案卷:用户当前目标、必须遵守的约束、最近进展、关键文件、工具观察、失败记录、可用工具、当前计划。案卷太薄,模型会失忆;案卷太厚,成本上升,关键事实还可能被噪声挤掉。
上下文工程不是塞更多东西。它是在决定什么必须在场,什么可以摘要,什么只保留引用,什么应该丢掉,并且把这些决定写成可调试的证据。

图 5-1:上下文窗口像一个有限货箱。Runtime 的责任是按优先级装载信息,而不是把所有东西一股脑塞进去。
本章讨论的不是'prompt 怎样写得更长',而是 Context Builder 怎样成为 runtime 的一个核心模块。它连接用户目标、消息历史、repo map、工具结果、预算策略和 trace。没有它,Agent 很难完成跨文件、跨步骤、可恢复的任务。

图 5-2:Context Builder 的装载流程。输入池中的信息先按优先级进入预算漏斗;完整保留、摘要保留、丢弃和预算使用都要写入 debug report。
5.1 token budget 是产品预算
token budget 不只是技术限制,也是产品预算。一次 run 里,token 影响延迟、成本和可靠性。越长的上下文并不总是越好,因为模型注意力会被稀释,缓存命中可能下降,旧日志还会把模型带回已经过期的路径。
可以把上下文分为六层:
| 层级 | 内容 | 默认策略 |
|---|
| 系统与安全指令 | runtime 规则、权限边界、输出要求 | 必须保留 |
|
| 当前运行状态 | 计划、已完成步骤、未解决问题 | 保留结构化摘要 |
| 近期对话 | 最近几轮用户、模型、工具消息 | 完整保留一部分 |
| 项目事实 | repo map、文件片段、配置、错误栈 | 按相关性装载 |
| 历史与工具结果 | 旧消息、长日志、搜索输出、测试输出 | 摘要、截断或引用 |
| 层级 | 建议占比 | 说明 |
|---|
| 系统、安全与开发者指令 | 8%-12% | 不要被历史挤掉 |
| 当前用户目标和约束 | 8%-12% | 保留原文,少做摘要 |
| 最近对话和运行状态 | 15%-25% | 让模型知道当前进展 |
| 代码片段和 repo map | 30%-45% | coding agent 的主要事实来源 |
| 工具结果 | 10%-20% | 失败结果优先于普通日志 |
| 历史摘要 | 5%-12% | 只保留仍影响决策的内容 |
比例不是硬规则。它的价值是让团队先有默认值,再通过 eval 调整。没有默认比例,Context Builder 很容易退化成'谁先来谁占位置'。
5.2 Context Builder 的输入和输出
Context Builder 应该是一个明确的模块,而不是散落在 controller 里的字符串拼接。
type ContextBuildInput = {
systemPrompt: string;
developerPolicy: string;
userMessage: string;
sessionSummary?: string;
recentMessages: RuntimeMessage[];
runState: RunState;
repoMap?: RepoMap;
pinnedFiles: FileSnippet[];
toolResults: ToolResult[];
tokenBudget: number;
};
输出不应该只有 messages。它还要给 trace 留下一份 debug report:
type ContextBuildResult = {
messages: RuntimeMessage[];
digest: {
messageCount: number;
estimatedTokens: number;
includedFiles: string[];
};
report: {
budget: number;
used: number;
mustKeep: string[];
included: string[];
summarized: string[];
dropped: string[];
reasons: Record<string, string>;
};
};
debug report 很朴素,却能救命。没有它,你很难解释模型为什么没看见某个文件、为什么忘记用户刚才的限制、为什么把旧错误当成新事实。
{
"budget": 64000,
"used": 58420,
"mustKeep": ["user.current", "policy.sandbox", "plan.active"],
"included": ["file.src/runtime/loop.ts", "tool.tsc.error_top"],
"summarized": ["history.run_12.steps_1_8", "tool.npm_test.stdout"],
"dropped": ["tool.rg.large_result", "old_terminal_noise"],
"reasons": {
"tool.rg.large_result": "低相关且超过 12000 chars",
"file.src/runtime/loop.ts": "最近修改且被当前错误栈引用",
"tool.tsc.error_top": "当前失败的直接证据"
}
}
这份报告让上下文策略变成可讨论的工程事实。团队可以调整权重、复现失败、比较策略,而不是争论'感觉模型是不是忘了'。
5.3 装载顺序要先保命,再提效
Context Builder 的装载顺序建议分成四步。
第一步,装载不可牺牲内容。包括系统安全边界、用户当前目标、显式禁止事项、当前 run 的未完成目标。这里不宜过度摘要,尤其是用户的验收标准和限制。
第二步,装载当前任务状态。包括已完成动作、待办步骤、最近失败、用户已批准或拒绝的操作。它让模型知道自己在任务的哪一段,而不是每轮都像刚开始。
第三步,装载项目事实。包括 repo map、入口文件、相关文件片段、错误栈引用、测试结果中的关键行。coding agent 的事实来源主要在这里。
第四步,装载历史摘要和长工具结果。旧历史不是没价值,但它通常应该以摘要形式出现,除非用户明确要求引用原文。
async function buildContext(input: ContextBuildInput): Promise<ContextBuildResult> {
const budget = createBudget(input.tokenBudget);
const builder = new MessageBudgetBuilder(budget);
builder.mustKeep("system.policy", input.systemPrompt);
builder.mustKeep("developer.policy", input.developerPolicy);
builder.mustKeep("user.current", input.userMessage);
builder.add("run.state", renderRunState(input.runState), { priority: "high" });
builder.addMany("recent.messages", input.recentMessages, { priority: "high" });
builder.add("repo.map", renderRepoMap(input.repoMap), { priority: "normal" });
builder.addMany("files", rankFileSnippets(input.pinnedFiles, input.runState));
builder.addMany("tool.results", rankToolResults(input.toolResults));
builder.add("session.summary", input.sessionSummary ?? "", { priority: "low" });
return builder.finish();
}
不要把这段代码理解成唯一写法。它想强调的是:上下文装载应有顺序、优先级和报告,而不是把数组按时间倒序塞满。
5.4 repo map 是导航,不是事实替代品
coding agent 需要理解仓库。直接把所有文件塞进上下文不可行,所以需要 repo map。
- 目录树;
- 关键入口文件;
- 导出的函数、类、类型;
- import/export 关系;
- 最近修改文件;
- 测试文件与源文件对应关系;
- README、配置文件、路由文件摘要。
aider 等 coding agent 重视 repo map,原因就在这里:它让模型在不读取全仓库的情况下判断'可能该看哪里'。但 repo map 只是导航,不是最终事实。要修改某个函数,runtime 必须读取真实文件片段。仅凭摘要改代码,很容易漏掉局部约束。
一个实用的 repo map 数据结构可以这样设计:
type RepoMap = {
root: string;
generatedAt: string;
entries: RepoEntry[];
};
type RepoEntry = {
path: string;
kind: "source" | "test" | "config" | "doc" | "asset" | "generated";
exports?: string[];
imports?: string[];
summary?: string;
lastModified?: string;
tokensEstimate: number;
};
repo map 的生成可以先简单:用 rg --files 找文件,用扩展名分类,读取 package.json、路由、入口文件和测试目录。等需求增加后,再引入 AST 解析、语言服务器或索引服务。
文件摘要要分层。第一次读大文件,可以生成摘要;当任务指向具体函数,再读取精确片段。摘要帮助定位,片段承担证据。
5.5 文件片段选择要有证据链
| 线索 | 示例 | 优先级 |
|---|
| 用户明确点名 | '看看 src/runtime/loop.ts' | 最高 |
| 错误栈引用 | TypeError at src/tools/registry.ts:42 | 高 |
| 搜索命中 | rg "ToolResult" src | 中高 |
| repo map 推断 | 入口文件、相邻测试、配置文件 | 中 |
选择片段时,不要只按文本相似度。最近修改文件、失败栈、测试关联、用户点名,都应该增加权重。
type SnippetCandidate = {
path: string;
startLine: number;
endLine: number;
text: string;
signals: {
userMentioned: boolean;
inErrorStack: boolean;
searchHits: number;
recentlyModified: boolean;
linkedTest: boolean;
};
};
function scoreSnippet(candidate: SnippetCandidate) {
let score = 0;
if (candidate.signals.userMentioned) score += 100;
if (candidate.signals.inErrorStack) score += 80;
score += Math.min(candidate.signals.searchHits, 10) * 5;
if (candidate.signals.recentlyModified) score += 20;
if (candidate.signals.linkedTest) score += 15;
return score;
}
评分不必神秘。它要能被 debug report 解释。未来你可以换成 embedding、BM25、语言服务器索引,但策略仍要能回答'为什么这段进入了上下文'。
5.6 历史滚动摘要要保留决策
- 最近 N 轮完整保留;
- 更早历史压缩成 session summary;
- 用户显式约束永久置顶;
- 已完成任务结果写入 run notes;
- 失败原因和决策记录保留为结构化摘要;
- 用户审批和拒绝记录进入 audit log,不靠自然语言摘要保存。
滚动摘要最大的风险是丢细节,甚至引入轻微偏差。因此摘要应该标注来源范围,例如'摘要覆盖 run_12 step_1 至 step_8',并保留必要原始 trace 引用。
一份适合 Agent Runtime 的摘要不应写成散文。它应该保留决策和未完成事项:
Summary range: run_12 step_1-step_8
User constraints:
- Do not modify database schema.
- Preserve existing CLI flags.
Completed:
- Located runtime loop in src/runtime/loop.ts.
- Added schema validation for read_file.
Open issues:
- run_command timeout still lacks AbortSignal cleanup.
Decisions:
- Keep ToolResult.content short; move full output to artifact.
Evidence:
- trace:run_12/step_6/tool_read_file
这种摘要不漂亮,但好用。模型读完知道任务进展,工程师读完知道证据在哪里。
5.7 工具结果降级
- 这是事实结果,还是日志噪声?
- 后续步骤是否需要逐字引用?
- 是否有结构化字段可提取?
- 是否超过预算?
- 是否包含 secret 或隐私数据?
测试失败日志通常需要失败用例、断言信息、文件路径、行号、错误堆栈顶部,而不是全部构建输出。搜索结果通常需要文件路径和匹配行,而不是每个文件前后几十行。npm install 日志通常只需要失败原因、包名、版本冲突和 lockfile 变化提示。
| 档位 | 进入上下文的形式 | 示例 |
|---|
| 全量 | 原文进入 | 短小配置文件、少量搜索结果 |
| 片段 | 关键行进入 | 测试失败栈、类型错误上下文 |
| 摘要 | 结构化摘要进入 | 长日志、批量搜索结果 |
| 引用 | artifact ref 进入 | 超长构建输出、大型文件 |
降级后要标记 truncated 或 summarized。模型必须知道自己看到的是部分证据。
5.8 上下文不足时的降级顺序
- 删除已确认无关的工具日志;
- 压缩旧历史;
- 长工具结果改摘要;
- 大文件改为结构摘要加关键片段;
- repo map 降级为目录树加入口文件;
- 最后才缩短当前用户意图周边内容。
安全策略、用户明确要求、当前目标、最近失败原因,不应轻易被挤掉。
const DEGRADATION_ORDER = [
"drop_irrelevant_tool_logs",
"summarize_old_history",
"summarize_large_tool_results",
"slice_large_files",
"compact_repo_map",
"compress_recent_messages",
] as const;
如果最后一步仍然放不下,runtime 应明确失败,要求用户缩小范围或允许读取更多内容,而不是默默丢掉关键约束。
5.9 恢复执行需要专门的上下文
resume 不是'把上一轮聊天记录再发一遍'。恢复执行时,第一轮上下文应额外包含:
- 恢复自哪个 checkpoint;
- 上一次完成到哪个 step;
- 哪些工具已经执行;
- 哪些工具不能重放;
- 当前 workspace 校验结果;
- 最近失败观察;
- 用户此前的审批或拒绝记录。
恢复上下文如果写得含糊,模型会把'继续任务'理解成'从头再来'。这正是很多长任务重复写文件、重复跑危险命令的原因。
Resume notice:
- Checkpoint: ckpt_20260814_004
- Last completed step: 7
- Already executed tools: read_file(call_12), apply_patch(call_13)
- Do not replay: apply_patch(call_13), run_command(call_15)
- Last failure: npm test timed out after 120s
- User decisions: denied network install at step 6
- Workspace verification: git diff matches checkpoint ledger
这段文字不是给用户看的仪式感,它是在提醒模型:继续,不要重演。
5.10 Context Builder 的实战路线
实现 Context Builder 时,不建议一开始就上复杂检索系统。先把可解释的规则跑稳。
第一步,实现 token 估算。估算不必完全精确,但要稳定。可以先按字符粗估,再接模型 tokenizer。
type ContextItem = {
id: string;
kind: "policy" | "user" | "state" | "history" | "repo_map" | "file" | "tool";
text: string;
tokensEstimate: number;
priority: number;
mustKeep: boolean;
degrade?: () => ContextItem;
};
第三步,按 mustKeep、优先级和预算装载。超预算时先调用 degrade(),仍然放不下再丢弃,并把原因写入 report。
第四步,把 report 写进 trace。只在开发模式下打印完整 report;生产环境要注意脱敏。
第五步,准备 32K 和 64K 两种窗口跑同一组任务,比较回答质量、成本和失败率。上下文策略必须通过任务结果校验,而不是只看'塞进去了多少 token'。
5.11 本章验收标准
完成本章后,你应该能解释上下文不足时的降级顺序;能说明 repo map 与直接读文件的关系;能通过 debug report 解释模型为什么知道或不知道某个事实。
| 用例 | 期望结果 |
|---|
| 200K token 历史压缩到 32K | 当前用户目标、安全策略、最近失败保留 |
| 大型测试日志进入上下文 | 只保留失败用例、路径、断言和关键栈 |
| 用户点名文件与搜索命中文件冲突 | 用户点名文件优先,搜索命中作为补充 |
| resume 后继续任务 | 上下文包含 checkpoint、已执行工具和不可重放工具 |
| 模型误答'没看到文件' | debug report 能证明文件是否进入上下文 |
做到这里,Agent 才有长任务的记忆力。它不只是'记得很多',而是知道哪些事实该留下,哪些噪声该离场。
系列文章目录
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online