最新n1n v2.0.1 正式上线!企业级大模型接口聚合平台 (LLM API Gateway),为您接入 500+ AI Models,价格低至 1 折,立即尝试

教学 LLM 按需拉取 MCP 资源与提示词以优化上下文

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

在构建面向生产环境的 AI 智能体(Agent)时,管理大语言模型(LLM)的上下文窗口是最关键的挑战之一。随着 Claude 3.5 Sonnet 和 DeepSeek-V3 等先进模型被深度集成到开发者工作流中,它们需要频繁访问外部数据库、API 文档和代码库结构。由 Anthropic 开源的模型上下文协议(Model Context Protocol,简称 MCP)已成为向 LLM 暴露这些能力的标准协议。

然而,标准的 MCP 集成方案通常存在一个根本性的设计缺陷:它们倾向于将海量信息塞满 LLM 的上下文窗口。通过使用领先的 LLM API 聚合器 n1n.ai,开发者能够接入具有极高并发和极低延迟的顶级模型。尽管这些模型支持多达 200k 甚至更长的上下文,但这并不意味着我们应该无节制地消耗它们。将冗余信息直接堆积到上下文窗口中,不仅会显著增加 API 响应延迟,还会大幅提升调用成本,并因为“大海捞针”(Needle in a Haystack)效应而降低模型的推理准确率。

本技术指南将深入探讨如何将 MCP 的“应用控制”(Application-controlled)原语无缝接入模型控制的工具调用循环中,从而将上下文检索的主动权从应用端转移给模型自身。


MCP 的控制权困境:应用控制与模型控制

模型上下文协议(MCP)定义了服务端向客户端提供能力的三种核心原语:

  1. 工具 (Tools):允许模型自主选择调用的可执行函数。
  2. 资源 (Resources):只读的数据源(如文件、API 响应或数据库 Schema)。
  3. 提示词 (Prompts):预定义的模板或斜杠命令,用于辅助用户格式化输入。

这些原语在自主 Agent 的执行流中有着截然不同的控制逻辑:

原语决定使用时机的角色是否天然适合工具调用?
工具 (Tools)大语言模型 (LLM 自身通过 function-calling 触发)✅ 是 — 天然嵌入在推理循环中
资源 (Resources)应用程序或终端用户 (启动时预加载)❌ 否 — 传统做法是直接塞入系统提示词
提示词 (Prompts)终端用户 (通常通过 UI 的斜杠命令触发)❌ 否 — 在执行循环开始前即已加载完毕

由于资源和提示词在模型的自主推理循环中缺乏原生的触发机制,大多数开发者选择走阻力最小的通路:在应用启动时拉取所有资源,并将它们的全部文本内容拼接到系统提示词(System Prompt)中。

这种暴力填充(Context Stuffing)的方式在实际生产中会迅速暴露以下四个瓶颈:

  1. 上下文膨胀 (Context Bloat):每一次 API 请求都必须为所有资源支付 Token 成本,无论模型当前是否需要这些资源。如果挂载了 10 个每个 5,000 Token 的 Markdown 文件,意味着每一次对话交互都会平白无故多出 50,000 Token 的开销。
  2. 内容被动截断 (Truncation):为了防止 Token 费用失控,开发者通常会强制截断资源内容(例如限制 content[:2000])。这会导致大文件末尾的关键信息被无情丢弃,导致模型产生幻觉。
  3. 二进制文件处理失败:PDF、PNG 或 ZIP 等资源没有直接的纯文本表达。简单的文本读取器在处理这些文件时经常抛出 UnicodeDecodeError,或者只输出无用的 [No content available] 占位符。
  4. 模型缺乏自主权:模型无法主动声明“我现在不需要这些数据”或“我需要完整读取那个特定文件”。它只能被动接受被灌输的所有信息。

破局方案:构建客户端合成工具

为了解决上述问题,我们可以改变控制流的方向。我们不再将资源塞入系统提示词,而是将应用控制的资源和提示词转化为模型控制的合成工具(Synthetic Tools)。通过在客户端注册两个合成工具——read_resource(uri)invoke_prompt(name),我们能够让模型在确有需要时自主拉取数据。

在此架构下,系统提示词不再包含任何具体的资源内容,而仅仅包含一个轻量级的资源目录(Resource Catalog),列出所有可用资源的 URI 及其简要描述。

改造后的系统提示词示例如下:

## Available Resources
The following resources can be read on demand. To read one, call the
`read_resource` tool with its exact URI. Do not assume a resource's
contents until you have read it.

### Resource: SUM ABAP Test Matrix
URI: sap-btp://sum-abap-v1
Description: SUM (Software Update Manager) test matrix specification for ABAP products

当模型接收到用户请求并进行分析时,它会首先扫描资源目录。如果它判断解答该问题需要用到 ABAP 测试矩阵,它就会输出一个工具调用请求:read_resource(uri="sap-btp://sum-abap-v1")。我们的编排图(Graph)会拦截该调用,直接从本地缓存中提取对应的内容,并作为工具响应消息返回给模型。这样,资源内容就只会在需要它的那一轮对话中进入上下文。


Python 逐步实现指南

我们将基于 LangGraph 和 langchain-mcp-adapters 的设计模式来演示具体实现。这一模式是框架无关的,你可以轻松将其移植到其他 Agent 框架中。

步骤 1:定义合成工具

我们首先定义 read_resource 作为一个 StructuredTool,其 Schema 接收一个 URI 字符串。其执行函数(func)设为一个占位 Lambda 表达式,因为真正的执行逻辑将在执行图的节点中被拦截和处理。

from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

class ReadResourceArgs(BaseModel):
    uri: str = Field(
        description="The exact URI of the MCP resource to read, e.g. 'sap-btp://sum-abap-v1'."
    )

read_resource_tool = StructuredTool.from_function(
    func=lambda uri: "",  # 占位 Lambda,在图的执行流中被拦截
    name="read_resource",
    description=(
        "Read the full contents of an MCP resource by its URI. "
        "Call this when the user asks to read, open, or summarize a resource, "
        "or when you need a resource's contents to answer. "
        "Only resources listed under 'Available Resources' can be read."
    ),
    args_schema=ReadResourceArgs,
)

# 注册工具并将其加入免审白名单
tools = [read_resource_tool]
allowed_tools_without_review = ["read_resource"]

专家提示:工具的描述(Description)本质上就是模型的路由提示词。务必清晰、严谨地编写描述,以便 LLM 准确判断何时调用该工具,何时使用其自身的参数化知识。

步骤 2:缓存并索引 MCP 资源

为了避免在工具调用循环中产生额外的网络延迟,我们在客户端初始化时一次性拉取资源的元数据,并将其缓存在以 URI 为键的内存字典中。

# 内存资源缓存
resources_content = {}

def initialize_resource_cache(mcp_resources):
    for res in (mcp_resources or []):
        uri = getattr(res, "uri", None)
        name = getattr(res, "name", "")
        description = getattr(res, "description", "")
        content = getattr(res, "content", "")
        
        # 避坑指南:MCP 协议中的 URI 可能会以 Pydantic 的 AnyUrl 对象形式传入。
        # 必须将其显式转换为 plain string,否则在字典查询中会因为类型不匹配而静默失败。
        uri_str = str(uri) if uri is not None else ""
        if uri_str and uri_str != "unknown":
            resources_content[uri_str] = {
                "name": name,
                "description": description,
                "content": content
            }

步骤 3:优雅处理二进制和多媒体资源

如果资源是 PDF、图像或二进制包,直接将原始字节塞进工具的输出消息会使模型崩溃,或产生大量乱码。我们需要解析 MIME 类型,对非文本资源返回结构化的描述符:

def is_text_mime(mime: str) -> bool:
    mime = (mime or "").lower().split(";")[0].strip()
    return (
        mime.startswith("text/")
        or mime.endswith(("+json", "+xml", "+yaml"))
        or mime in {"application/json", "application/xml", "application/yaml"}
    )

def extract_resource_payload(uri_str, resource_data, mime_type):
    if isinstance(resource_data, bytes):
        if is_text_mime(mime_type):
            return resource_data.decode("utf-8")
        else:
            # 返回一个清晰的二进制描述符,而不是传输破坏上下文的原始字节
            size_kb = len(resource_data) / 1024
            return (
                f"[Binary resource: {mime_type or 'application/octet-stream'}, "
                f"{size_kb:.2f} KB. This is not text and cannot be inlined; "
                f"open it with a client that handles its media type.]"
            )
    return str(resource_data)

步骤 4:在执行图中拦截工具调用

在 LangGraph 的 Agent 执行节点中,我们在工具分发逻辑前对 read_resource 进行拦截。这样可以省去网络往返,直接利用本地缓存快速返回结果。

def handle_tool_calls(state):
    messages = state["messages"]
    last_message = messages[-1]
    new_messages = []
    
    if not last_message.tool_calls:
        return {"messages": []}
        
    for tool_call in last_message.tool_calls:
        if tool_call["name"] == "read_resource":
            uri = str(tool_call["args"].get("uri", ""))
            resource = resources_content.get(uri)
            
            if resource:
                # 根据 MIME 类型安全地解析资源内容
                raw_content = resource["content"]
                mime = resource.get("mime_type", "text/plain")
                resolved_text = extract_resource_payload(uri, raw_content, mime)
                
                new_messages.append({
                    "role": "tool",
                    "name": "read_resource",
                    "content": resolved_text,
                    "tool_call_id": tool_call["id"],
                })
            else: # 优雅处理未找到资源的情况
                available_uris = list(resources_content.keys())
                new_messages.append({
                    "role": "tool",
                    "name": "read_resource",
                    "content": f"Resource '{uri}' not found. Available URIs: {available_uris}",
                    "tool_call_id": tool_call["id"],
                })
    
    return {"messages": new_messages}

架构执行流程图

以下是当用户发起请求时,该架构在内部的完整数据流向:

   ┌─────────────────┐
   │   用户输入消息   │
   └────────┬────────┘
   ┌──────────────────────────────────────────┐
   │ 系统提示词 = 仅包含资源目录 (Catalog)    (仅包含 URI 与描述,不包含内容)   └────────┬─────────────────────────────────┘
      ┌───────────────┐
LLM 自主决策 │
      └──┬─────────┬──┘
         │         │
 需要读取│         │ 不需要读取
 资源内容│         └──────────────► 直接输出回答
 ┌──────────────────────────┐
 │ tool_use: read_resource  │
         (uri) └────────────┬─────────────┘
 ┌──────────────────────────────┐
运行图 (Graph) 拦截该工具调用 │
  (自动批准,无需人工介入审核) └────────────┬─────────────────┘
 ┌──────────────────────────────┐
 │ 在本地缓存 Map 中检索 URI └───────┬───────────────┬──────┘
         │ 文本类型      │ 二进制类型
         ▼               ▼
 ┌────────────────┐  ┌──────────────────────┐
 │ 填充完整文本   │  │ 填充描述符:          │
 │ 作为工具消息返回│  │ MIME 类型与文件大小  │
 └───────┬────────┘  └───────────┬──────────┘
         │                       │
         └───────────┬───────────┘
                     
              (返回给 LLM ──► 生成最终回答)

生产环境避坑指南与最佳实践

在通过 n1n.ai 部署大规模生产级 Agent 时,建议严格遵循以下五个工程最佳实践:

  1. 类型标准化 (Type Normalization):确保将缓存 Key 统一强制转换为 str 类型。某些 MCP 客户端库会将 URI 解析为 Pydantic 的 AnyUrl 对象,若直接用作字典的 Key,在进行字符串匹配查询时会静默失败。
  2. 免审自动批准 (Auto-Approval):如果你的 Agent 架构中引入了人工审核(Human-in-the-Loop)机制,务必将 read_resource 等只读操作加入免审白名单。要求用户手动确认每一次“读取文件”的操作会极大地损害交互体验。
  3. MIME 字段定位:在 langchain-mcp-adapters 中,媒体类型信息是直接挂在 Blob 对象的 .mimetype 属性上的,而不是在 metadata 字典里。读取错误的字段会导致所有文件都被识别为通用的二进制流。
  4. 提示词缓存复用:当使用 n1n.ai 提供的模型 API 时,尽量保持系统提示词中的资源目录结构稳定。静态的资源列表能够极大提高大模型的 Prompt Cache 命中率,从而大幅削减首字延迟(TTFT)与调用成本。
  5. 动态目录剪枝:如果你的 MCP 服务端暴露了成百上千个资源,切忌将它们全部塞入目录。应当在生成系统提示词前加入一步轻量级的向量检索(RAG),仅将与当前问题最相关的 5-10 个资源的描述和 URI 写入系统提示词中。

总结

通过将模型上下文协议(MCP)的资源转化为由模型控制的合成工具,我们成功地将上下文管理模式从粗暴的“推送(Push)”转变为精准的“拉取(Pull)”。LLM 获得了自主检索数据的能力,Token 消耗显著降低,大文件被迫截断导致的幻觉问题也迎刃而解。

n1n.ai 获取免费 API 密钥