1. 上下文是 Agent 的稀缺资源
模型的能力上限由上下文窗口决定,但窗口内塞什么、先看到什么、被裁剪掉什么,才是决定输出质量的关键。MCP 引入的三类能力——Tools、Resources、Prompts——本质上都是在往这块窗口里「灌内容」,如果不管控,Agent 就会在窗口爆掉后开始遗忘关键信息。
一句话:上下文工程不是「省 token」的抠门游戏,而是「把有限注意力分配给高价值信息」的资源配置学。
1.1 上下文中的内容来源
| 来源 | 类型 | 进入窗口的时机 |
|---|---|---|
| 系统提示 | 固定文本 | 每次请求 |
| 对话历史 | 用户/助手消息 | 每次请求 |
| 工具 schema | JSON 描述 | 模型决策前 |
| Tool 结果 | 执行返回 | 调用后立即 |
| Resource | 只读数据 | 按需读取 |
| Prompt | 模板展开 | 用户触发时 |
2. Prompts 模板设计
MCP 的 Prompts 是「服务器预制的提示词模板」,用户在对话中按名引用,服务器返回完整的消息序列。
2.1 模板设计原则
- 参数即变量:模板只写骨架,具体内容全部由
arguments注入,保证复用。 - 限定范围:模板应聚焦单一任务,不要试图一篇覆盖所有场景。
- 控制展开体积:模板展开后的 token 也要计入预算,展开前先估算。
{
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": { "pr_id": "42", "language": "typescript" }
}
}
2.2 返回消息模板(带变量注入)
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"description": "PR #42 代码审查",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请以资深 Reviewer 身份审查 PR #42(language: typescript)。\n" +
"审查维度:\n1. 逻辑正确性\n2. 边界条件\n3. 类型安全\n" +
"输出格式:按严重程度(P0/P1/P2)列出问题清单。"
}
}
]
}
}
一句话:Prompt 模板是「给模型的作业纸」,参数是「填进作业纸的题目」,作业纸本身永远不换。
2.3 动态 Prompt 组合
实际项目里,一条用户指令往往要拼接多个模板:角色模板 + 场景模板 + 工具说明模板。动态组合要保证「总预算可控」与「片段可复用」:
// 片段化的 Prompt 组合器
const segments: Record<string, { text: string; cost: number }> = {
role_reviewer: { text: "你是一名资深 Code Reviewer…", cost: 120 },
context_pr: { text: "正在审查 PR {pr_id}({language})…", cost: 60 },
tool_notes: { text: "可用工具:{tools},注意只使用白名单内工具。", cost: 80 },
};
function composePrompt(keys: string[], vars: Record<string, string>, budget = 1000) {
let tokens = 0;
const parts: string[] = [];
for (const key of keys) {
const seg = segments[key];
const rendered = fill(seg.text, vars);
const cost = estimateTokens(rendered);
if (tokens + cost > budget) break; // 超出预算,停止追加
parts.push(rendered);
tokens += cost;
}
return { text: parts.join("\n\n"), tokens };
}
这样每个模板片段是独立单元,可单独优化、单独测试,组合时统一过预算闸门。
3. Resource 的读取时机与缓存
Resource 是服务器暴露的只读数据,但数据不会自动进窗口——模型必须先通过 resources/read 主动拉取。这给了上下文工程一个关键的掌控点:读什么、何时读、读多久。
3.1 三种读取时机
| 时机 | 做法 | 适用场景 |
|---|---|---|
| 懒读取 | 模型需要时才 read | 大数据、按需 |
| 预读取 | 任务开始时批量读 | 确定性依赖 |
| 订阅推送 | resources/subscribe 监听变更 | 高频变化数据 |
3.2 带缓存的 Resource 读取
import time
class ResourceCache:
def __init__(self, session, ttl_seconds=60):
self._session = session
self._ttl = ttl_seconds
self._cache: dict[str, tuple[float, str]] = {}
async def read(self, uri: str, force=False) -> str:
now = time.time()
if not force and uri in self._cache:
ts, text = self._cache[uri]
if now - ts < self._ttl:
return text
# 穿透到服务器
result = await self._session.read_resource(uri)
text = result.contents[0].text
self._cache[uri] = (now, text)
return text
# 用法:60 秒内重复读取直接命中缓存
text = await cache.read("config://production")
一句话:Resource 像图书馆的藏书——「藏书在架上」不等于「书在你手里」,只有
read动作才把内容搬进窗口,缓存则是给借书装上了快递。
4. 上下文压缩与 token 预算
窗口有限,内容无限。压缩策略的核心是:保留结论,丢弃过程。
4.1 Token 预算表
以 128k 窗口为例,建议的分配:
| 区块 | 预算 | 占比 | 策略 |
|---|---|---|---|
| 系统提示 + 角色 | 6k | 5% | 固定,逐字优化 |
| 对话历史(近期) | 32k | 25% | 完整保留最近 10 轮 |
| 对话历史(远期) | 16k | 12% | 摘要化 |
| 工具 schema(可见区) | 32k | 25% | 动态注入 |
| Tool 结果 + Resource | 40k | 31% | 精简、截断、缓存 |
| 预留 | 2k | 2% | 余量缓冲 |
4.2 历史摘要压缩
def compress_history(old_messages, llm) -> list:
"""把旧轮次压缩成一句摘要,替换原文"""
if estimate_tokens(old_messages) < 3000:
return old_messages # 太小不值得压
summary = llm.chat([
{"role": "system", "content":
"用不超过 100 字概括以下对话的关键事实、决定与待办"},
{"role": "user", "content": render(old_messages)},
])
return [{
"role": "system",
"content": f"[历史摘要] {summary}",
}]
一句话:压缩的黄金法则是「能概括就不原文」——模型真正需要的是事实与结论,不是每个中间 token 的搬运过程。
4.3 前缀缓存与 KV Cache 优化
对话历史之外,还有一类不占「逻辑上下文」却决定「成本与速度」的优化:前缀缓存(Prompt Caching)。把不会变化的固定前缀(系统提示、工具说明、长期侧写)放在最前面,模型服务商可以对这块前缀做 KV 缓存,命中后显著降低延迟与费用。
| 内容块 | 是否可缓存 | 说明 |
|---|---|---|
| 系统提示 + 角色 | 是 | 完全固定 |
| 工具 schema 描述 | 是 | 会话内不变 |
| 长期侧写(用户偏好) | 是 | 会话内追加 |
| 对话历史 | 部分 | 追加式变化,越靠前越稳 |
| Tool 结果 | 否 | 每轮不同 |
// 把可缓存前缀稳定放在最前,避免「抖动」破坏缓存
function buildStablePrompt(session): Message[] {
const stable = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "system", content: renderToolSchemas(session.tools) },
{ role: "system", content: session.profile.render() },
];
// 变化部分永远追加在末尾,保持前缀稳定
return [...stable, ...session.history, ...session.pendingToolResults];
}
# Anthropic / 常见供应商的缓存行为:同一前缀超过一定 token 后自动启用
# 实践中把「稳定前缀」控制在 4k+ token,缓存命中收益最明显
一句话:前缀缓存与上下文压缩是一对组合拳——压缩负责「删掉该删的」,缓存负责「保住没变的」,共同压低每次请求的成本。
5. Tool result 的精简与结构化
工具返回结果往往比模型需要的多得多。一个查询可能返回 100 行数据,而决策只需要「总数 42 条,其中 P0 3 条」。在结果进入窗口前,先把它精炼成结论。
5.1 服务端精简示例
// 在 MCP 服务器端精简返回,而不是让模型去读原始数据
async function handleQueryOrders(args) {
const rows = await db.query(args.sql);
const summary = {
count: rows.length,
totalAmount: rows.reduce((s, r) => s + r.amount, 0),
statusBreakdown: groupByStatus(rows),
// 只回传最多 5 条明细示例
samples: rows.slice(0, 5),
};
return {
content: [{ type: "text", text: JSON.stringify(summary) }],
};
}
5.2 客户端截断兜底
MAX_TOOL_RESULT_TOKENS = 2000
def truncate_tool_result(result_text: str) -> str:
if estimate_tokens(result_text) <= MAX_TOOL_RESULT_TOKENS:
return result_text
# 保留头部与尾部,中部省略
head = result_text[:800]
tail = result_text[-800:]
return f"{head}\n...[已截断,原始约 {len(result_text)} 字]...\n{tail}"
6. 长会话的上下文衰减
会话拉长后,早期的信息会逐渐被挤出窗口。衰减策略决定了哪些信息「优先幸存」。
6.1 信息衰减优先级
幸存顺序(从高到低):
1. 用户明确的指令与偏好 (如「回复用中文」)
2. 任务目标与当前状态 (如「正在迁移数据库」)
3. 关键决策与结论 (如「选用 Postgres 而非 MySQL」)
4. 重要中间结果 (如「订单总数 42」)
5. 完整对话原文 (最先被压缩)
6.2 长期记忆侧写
class LongTermProfile {
private facts: string[] = [];
observe(message: string): void {
// 用一次轻量 LLM 调用抽取「可长期保留的事实」
const facts = extractPersistentFacts(message);
for (const f of facts) this.facts.push(f);
}
render(): string {
// 每轮请求都作为「系统侧写」注入,其余历史可被压缩
return [
"用户长期偏好与事实:",
...this.facts.slice(-20), // 只保留最新 20 条
].join("\n");
}
}
一句话:长会话衰减的实质是把记忆分成「随取随用的侧写」和「看完即丢的历史」,前者常驻窗口,后者让位给当下。
7. 实战:一个完整的上下文预算巡检
上线前的上下文健康检查可以固化为脚本,每次发布自动运行:
# 检查线上上下文的 token 构成是否合理
mcp-context-audit \
--session latest \
--max-system 6k \
--max-history 48k \
--max-tools-schema 32k \
--warn-over budget
输出示例:
系统提示 5.2k ✅ 预算内
对话历史(近期) 28.1k ✅ 预算内
对话历史(远期) 9.4k ⚠️ 摘要比例偏低,建议压缩
工具 schema 可见区 12.3k ✅ 预算内
Tool 结果 41.2k ❌ 超预算(40k),有截断风险
------------------------------
总计 96.2k / 128k
8. 总结
上下文工程的本质是把 MCP 的三类能力纳入统一的资源预算:
| 能力 | 进入窗口方式 | 管控手段 | 核心指标 |
|---|---|---|---|
| Prompts | 按名展开 | 模板参数化、控制展开体积 | 展开后 token |
| Resources | 按需读取 | 懒读/预读/订阅 + TTL 缓存 | 缓存命中率 |
| Tool 结果 | 调用后回填 | 服务端精简 + 客户端截断 | 结果 token |
| 对话历史 | 逐轮累积 | 摘要压缩 + 侧写化 | 压缩比 |
上下文管好了,接下来要回答的就不再是「放得下吗」,而是「这个 Agent 在线上到底跑得好不好」——这就引出了 MCP 的可观测性与调试。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。