MCP 上下文工程与提示词资源管理

上下文窗口是 Agent 最稀缺的资源,MCP 的 Prompts 模板、Resources 与 Tool 结果都在这块窗口里争夺空间。本文详解 Prompt 模板设计、Resource 读取时机与缓存、上下文压缩与 token 预算、Tool result 精简结构化,以及长会话的上下文衰减策略。

1. 上下文是 Agent 的稀缺资源

模型的能力上限由上下文窗口决定,但窗口内塞什么、先看到什么、被裁剪掉什么,才是决定输出质量的关键。MCP 引入的三类能力——Tools、Resources、Prompts——本质上都是在往这块窗口里「灌内容」,如果不管控,Agent 就会在窗口爆掉后开始遗忘关键信息。

一句话:上下文工程不是「省 token」的抠门游戏,而是「把有限注意力分配给高价值信息」的资源配置学。

1.1 上下文中的内容来源

来源类型进入窗口的时机
系统提示固定文本每次请求
对话历史用户/助手消息每次请求
工具 schemaJSON 描述模型决策前
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 窗口为例,建议的分配:

区块预算占比策略
系统提示 + 角色6k5%固定,逐字优化
对话历史(近期)32k25%完整保留最近 10 轮
对话历史(远期)16k12%摘要化
工具 schema(可见区)32k25%动态注入
Tool 结果 + Resource40k31%精简、截断、缓存
预留2k2%余量缓冲

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 的可观测性与调试。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 生态全景:官方服务器、云托管与 A2A 对比
  2. MCP 可观测性与调试:从 mcp-inspector 到生产链路
  3. MCP 与 Agent 框架集成:工具调用编排实战