LangChain系列之MCP 与 Skills 接入:工具怎么接,章法怎么给

2026-09-16 16:250 条评论87 次阅读

MCP 与 Skills 接入:工具怎么接,章法怎么给

上一篇我们把记忆装好了。跨会话记得住人,也记得住事。

可 Agent 手里还是那几个硬编码的函数,能做的事就那几件。

这一篇解决「能做什么」。

一、5 个 MCP Server,6 万 token 的入场费

先看一组公开数据,出自 Anthropic 自己的工程团队。

GitHub 的 MCP server 带 35 个工具,工具定义约 26000 token。Slack 只有 11 个工具,却占了约 21000 token。Jira 再加约 17000。

三个加起来,6 万 4 千 token。Anthropic 内部环境优化之前,光是工具定义就到过 134000 token。

这些 token 在用户说第一句话之前就已经付掉了,而且每一次请求都要重付一遍。

比账单更贵的是准确率。Claude API 的文档里写得很直白:工具数量超过 30 到 50 个之后,模型选错的概率显著上升。

Anthropic 给出一组对照。58 个工具全量摆在模型面前时,Opus 4 选对的概率是 49%。

改成按需检索之后,同样的模型,准确率到了 74%。窗口里少掉的是选项,涨上来的是判断力。

网上有一类工具名,放进同一个上下文里就是在为难模型。

notification-send-user 和 notification-send-channel。两个都是发通知,只有目的地不同。模型靠文字匹配做决定,名字像到一定程度,它就开始抓瞎。

更麻烦的是抓瞎的时候不报错。模型自信地从错的地方拿了数据,自信地总结给你。

我也拿自己这套工程算了一遍账。业务风格的工具,带中文描述与参数说明,单工具均值 220 token 上下。

按这个均值外推,200K 窗口下:

工具数 token 总量 占 200K 窗口
10 约 2.2K 1.1%
50 约 11.0K 5.5%
200 约 43.9K 22.0%
1000 约 219.6K 109.8%
7000 约 154 万 768.6%

7000 个工具,超窗口 7 倍多。

这里有个容易忽略的事实:上一篇文章的三招救不了这笔账。

裁剪、删除、摘要,全都在 messages 里做文章。工具定义走的是另一个字段,压根不在消息历史里。你裁得再干净,请求开头那几十 K 的工具说明一个 token 都不会少。

MCP 官方在客户端实践指南里给了一条判据:工具定义占到窗口的 1% 到 5%,就该换成渐进式发现。

于是两个问题浮上来:工具怎么接进来?接进来之后,怎么让模型选得动?

还有个更少被提起的问题。工具再多,也只是「能做什么」。至于「这件事该怎么做」,工具本身不会告诉你。

这一篇把两件事一起说清。MCP 管能力,Skills 管章法。

二、MCP 是什么:一个协议,替掉 N 套适配

一句话说清:MCP 把「工具」从代码里的一个函数,变成一个可以被发现的协议对象。

客户端连上 server,问一句你能干什么,server 回一份清单。清单里的每个工具都带名字、说明和参数结构。

协议本身给了三样东西:

原语 是什么 谁发起
tools 能调用的动作 模型决定何时调用
resources 能读的数据 客户端按需拉取
prompts 预设好的指令模板 用户或客户端选用

这一篇主要用 tools,resources 会在合体那一章顺带看一眼。

它为什么能在两三年里变成事实标准?因为省掉的适配工作是可量化的。

server 那边加一个工具,所有客户端零改动。Java 写的服务、Python 写的脚本、Node 写的网关,只要包一层 MCP,模型侧看到的形态完全一样。

那什么时候不用它,直接写 @tool 更省事?

场景 选择 原因
同进程内的几个纯函数 直接 @tool 少一层进程通信,调试也简单
跨团队、跨语言的服务 MCP 对方不用装你的 Python 环境
已经有 REST 接口的服务 MCP 把接口包一层,比让每个客户端各接一遍划算
本地开发调试用的小工具 @tool 起 server 的开销不值当
要给多个 Agent 共用 MCP 一份 server,多个客户端

分界线在「复用范围」。只服务一个 Agent 的一个场景,函数就够了。要被两个以上的消费方用到,包 MCP 才开始划算。

三、接入姿势一:三行拿到工具

LangChain 侧走 langchain-mcp-adapters。最省事的写法是这样:

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "order": {
        "command": "python",
        "args": ["servers/order_server.py"],
        "transport": "stdio",
    },
})

tools = await client.get_tools()

get_tools() 一回来,server 那边的 3 个函数就变成了 3 个 StructuredTool,可以直接交给 create_agent。

网上教程里的第一句话,现在大概已经跑不起来了。

with MultiServerMCPClient({...}) as client:   # 这条路径已经不通
    ...

实测会直接抛:

TypeError: 'MultiServerMCPClient' object does not support the context manager protocol

大量 2025 年的教程还停在 with 写法上。遇到这个报错不用怀疑自己,是写法变了。

拿到工具之后,有三处细节值得先看一眼,后面处理错误时会用到。

第一,args_schema 是 dict 形式的 JSON Schema,不是 Pydantic 模型。

schema 类型 : dict
schema 内容 : {"properties": {"order_no": {"title": "Order No", "type": "string"}, ...

写 @tool 的时候参数结构是 Pydantic 模型,从 MCP 拿回来的是一份 JSON Schema 字典。要做参数校验或改写,得按 dict 处理。

第二,description 就是 server 端那个函数的 docstring 原文。

我那个查单工具的 docstring 第一行是「按订单号查询订单详情。」,客户端侧读到的就是这一句,一个字都没变。

写 docstring 就是在写提示词。 这句提醒看着朴素,实际是把 MCP server 做好的关键。文档写得含糊,模型就选不准。

第三,工具调用返回的是 content blocks 列表,不是字符串。

raw type : list
raw value: [{'type': 'text', 'text': '{\n  "found": true, ...', 'id': 'lc_...'}]

想拿文本得自己拼一遍,从每个 block 里取 text 字段。

传输方式三选一,判断口径也简单:

transport 连接方式 适合场景 鉴权
stdio 客户端把 server 当子进程拉起 本地工具、单机脚本、开发期 没有 header 概念,走环境变量
streamable-http 标准 HTTP 请求 生产环境、跨网络、多客户端共享 支持 headers
sse 长连接服务端推送 早期远程方案,正被前一个替代 支持 headers

本地起步,stdio 最省事。要上生产、要给多个客户端共用,换成 streamable-http。

四、接入姿势二:会话复用,与上线前要处理的四件事

姿势一有个隐含代价,官方文档写得很清楚:每一次工具调用都会新建一个会话。

查一次订单,起一个进程。再查一次,再起一个。

无状态的服务这么干没问题。碰到有状态的 server 就麻烦了。浏览器会话、数据库连接、登录态,这些都得活在同一个会话里。

这时候换姿势二:

async with client.session("order") as session:
    tools = await load_mcp_tools(session)
    result = await tools[0].ainvoke({"order_no": "SO20260912001", "tenant_id": "t-acme"})

进到会话内部,工具在这个会话里加载,调用也在这个会话里发生。

代价是生命周期得自己管。什么时候开、什么时候关、断了怎么重连,全归你。

姿势定了,接下来四件事是上线前绕不过去的。

第一件,鉴权。

headers 只对 http 和 sse 有效。stdio 走的是一条管道,没有 header 这个概念。

本地 stdio server 要拿凭据,走环境变量:

{
    "command": "python",
    "args": ["servers/order_server.py"],
    "transport": "stdio",
    "env": {"ORDER_API_TOKEN": os.environ["ORDER_API_TOKEN"]},
}

第二件,工具重名。

接了 5 个 server,其中 3 个都有个叫 search 的工具。后加载的会覆盖先加载的,而且不留任何提示。

官方参数叫 tool_name_prefix,给每个 server 的工具加前缀。这件事建议默认打开,别等出事再补。

第三件,错误。

MCP 把两类错误分得很清楚。工具执行失败是一种,会以 isError=True 的结果返回,作为 ToolMessage 回到模型手里,模型看到错误可以自己调整再来一次。协议层失败是另一种,连接断了、初始化失败,这类该崩就崩,硬扛没有意义。

客户端上有 handle_tool_errors 开关,用来决定执行错误怎么落地。

第四件,超时与重试。

第三方 server 的响应时间不归你管。LangChain 侧给了两个中间件兜底,ToolRetryMiddleware 管重试,ToolErrorMiddleware 管错误转译。

有时候还要防模型自己打转,ToolCallLimitMiddleware 可以直接卡住调用次数。

接入是十行代码,上线是这十件事。这句话我建议贴在工位上。

五、工具规模化:10 个靠人管,7000 个靠检索

前面那张账单表到这里要变成真问题了。

10 个工具靠人管,谁都能说清每个工具是干什么的。100 个工具就得上规范。几千个工具的时候,靠人管这件事本身就不成立了。

官方给了三条路。

第一条,把发现这件事交给 provider。

ProviderToolSearchMiddleware 会把指定的工具标记成延迟加载,由 provider 那边按需把 schema 取回来。

from langchain.agents.middleware import ProviderToolSearchMiddleware

ProviderToolSearchMiddleware(searchable_tools=["query_order", "calc_refund"])

这条路成本最低,但有一条硬约束:它只支持 Anthropic 与 OpenAI。

被标记为延迟加载、而当前 provider 不认识这套约定时,会直接抛 ValueError。用国产模型或本地模型的朋友,这条路走不通,得看后两条。

第二条,用一个便宜模型先挑工具。

LLMToolSelectorMiddleware 的逻辑是:每次调用之前,先把用户的问题和工具清单交给一个便宜模型,让它挑出相关的那几个,再把挑好的清单交给主模型。

from langchain.agents.middleware import LLMToolSelectorMiddleware

LLMToolSelectorMiddleware(model="gpt-4o-mini", max_tools=8, always_include=["read_skill"])

要留意它的解析失败策略。默认是 'error',挑工具的模型如果吐不出合法结构,整条请求就失败了。可以改成 'all' 兜底成「全给」,宁可多花点 token 也别把请求打断。

第三条,自己搭一个工具网关。

这是我在产线上用的路子,也是本篇 demo 里实现的那条。

思路一句话:货架全量注册,模型只看清单。 工具注册进一张表,每轮按当前的检索意图捞出 top-N,用 request.override(tools=...) 注入。

class ToolGatewayMiddleware(AgentMiddleware):
    def wrap_model_call(self, request, handler):
        query = last_human_text(request.messages)
        on_shelf = self.shelf.names()
        # 只接管货架上的工具,别的中间件注入的工具原样保留
        keep = [t for t in (request.tools or []) if t.name not in on_shelf]
        visible = keep + self.shelf.search(query, top_k=self.top_k)
        return handler(request.override(tools=visible))

整条链路是这样一个形状:

货架:全量注册
几千个工具都在,但只在库里

检索:按当前意图打分
先滤掉危险档,再取 top-N

清单:注入给模型
只有命中的几个 + 中间件自带的工具

模型决定调用哪一个

ToolNode 执行
货架上的工具始终可执行

这段代码里有一行特别容易踩空,我单独拎出来说。

request.override(tools=...) 是整份替换,不是增量合并。你只挑了 8 个工具注入,另外那些中间件自己加的工具也会被一起抹掉。

我自己写 Skills 中间件时撞上过这个。技能清单进上下文了,模型却调不到读技能正文的工具,报 Error invoking tool,查了半天才发现是网关把工具吃掉了。

正确做法就是上面那样,先把不在货架上的工具捞出来,原样带着走。

网关真正难的地方不在代码,在三个工程约定。

命名规范。 工具名按 {域}_{对象}_{动作} 组织,例如 order_refund_submit。前缀天然就是分组,检索和权限都能直接按前缀切。

危险分级。 只读、写、危险三档分开。危险操作不进自动检索结果,就算关键词命中也得显式放行。删库这个动作,不该因为用户说了句「清理一下」就出现在模型面前。

货架与清单分离。 注册是全量注册,可见是按需可见。这两件事分开之后,你能同时得到完整的能力覆盖和可控的上下文成本。

跑一遍这套网关,账是这样的:

货架共 63 个工具|分组:dom00 3,dom01 3,dom02 3|分级:read 33,write 22,danger 8

全量 63 个工具:13,799 token = 200K 窗口的 6.9%
命中 8 个(上限 8),模型侧账单从 13,799 降到 1,669 token

Agent 注册的工具总数 : 63
模型这一轮实际看到   : 9 个

账单降到原来的 1/8。第 9 个是 read_skill,来自 Skills 中间件,被网关原样保留了下来。

把货架换成 7000 个工具,模型侧依然是 9 个。这就是规模化之后还能管住的原因。

六、Skills 是什么:把工序写成一个文件夹

工具解决的是「能做什么」,接下来是「该怎么做」。

拿退款这件事举例。你有查订单的工具、有试算的工具、有提交的工具。模型手里攥着这三个工具,它会怎么做?

它可能先提交再试算。可能不问原因就自己挑一个退款理由编码。可能会凭印象告诉你「7 天内可以无理由退」,而这句话不在任何工具返回值里。

工具给了它能做的事,没给做事的方法。

Skills 的思路是:把方法写成一个文件夹。

refund-flow/
├── SKILL.md                    # 主文件,写工序
├── references/policy.md        # 细则,需要时再读
└── scripts/                    # 脚本,需要时再跑

SKILL.md 的结构很轻。开头一段 frontmatter,必填的只有两个字段:

---
name: refund-flow
description: 处理客户退款申请的标准工序。当用户提出退款、退货、退钱、订单取消、售后补偿等请求时使用,覆盖查单、试算、审批判断、提交与回执全流程。
---

下面是正文,用大白话把步骤写清楚。我那份写的是 5 步:查订单、查政策、试算金额、判断是否需要审批、提交并出回执。

关键在于它怎么进上下文,这一层叫做渐进披露:

第一层:元数据常驻
name + description,约 100 token

第二层:任务命中时才读
SKILL.md 正文

第三层:正文里引用的资源
references / scripts,按需再读

启动的时候,模型只看到 skill 的名字和那句 description。任务命中哪条,它自己去读那条的正文。正文里提到的细则文件,要用了再读。

成本对照很直观。

同样一件事,用 MCP 的方式给 GitHub 能力,工具定义是 23K 到 50K token。写成一个 skill,名字加描述 200 到 500 token 就够。

有个观念得掰一下:Skills 不是「更弱的 MCP」。

流程、判据、输出格式这几类东西,本来就没法用函数签名表达。你没法定义一个函数叫「按公司规定判断这笔退款该不该批」,因为它的判断依据是一段话。

还有一条实操提醒,我认为是 Skills 最容易翻车的地方。

description 是唯一的触发入口。 模型只凭这一句话决定要不要用你这条技能。

写得含糊,例如「帮助处理售后相关事项」,条件相近的场景全都命中不了,这条技能等于白写。写得具体,把触发场景一个个列出来,命中率立刻不一样。

七、MCP 与 Skills,到底什么关系

讲到这儿,两个东西容易混。它们确实容易混,因为都在解决「模型能力不够」这件事。

先把分层讲清楚。

维度 MCP(能力层) Skills(过程层)
给什么 能调用的动作 事情该怎么做
单元形态 函数签名加描述 文件夹加段落文字
谁定义 server 作者,跨团队跨语言 业务方,跟着流程变
怎么用上 模型直接调用 模型先读正文,再照正文办事
触发方式 工具名与描述匹配意图 description 匹配意图,再读正文
上下文成本 全量常驻,单工具约 220 token 清单常驻约 100 token,正文按需
失败的样子 调错工具、参数填错 压根不触发,或触发了不照做
典型场景 查订单、发通知、提工单 退款流程、周报格式、评审清单

右边这一列有个共同点:它们的答案都是一段话。左边这一列也有个共同点:它们的答案都能写成一行签名。

于是判断口径就出来了:能用函数签名说清的事交给 MCP,必须用一段话才能说清的事交给 Skills。

遇到具体需求时,按这张表归位:

需求 落在哪一层 理由
让 Agent 能查订单 MCP 输入输出明确,两个参数
让 Agent 按退款流程办事 Skills 步骤顺序与审批判据要说清
让 Agent 出周报 Skills 四段结构、统计口径、字数限制
让 Agent 调内部 API MCP 接口定义现成
让 Agent 处理一笔退款 两层都要 工序来自 Skills,动作来自 MCP

最后一行是常态。真实任务几乎都要两层叠起来。

有两个常见误判,两头都有人踩。

一种是把 Skills 当轻量版 MCP 用,把接口文档整段抄进 SKILL.md。结果最该省的正文长度没省下来,还得自己实现读正文的工具,成本优势一点没剩。

另一种是反过来,把 20 步的业务流程硬写成一个工具函数。参数会多到模型填不准,中间任何一步的判断逻辑也没地方放。

八、框架自带的 Skills,与它的一条边界

先看现成的怎么用。deepagents 里一行接入:

from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend

agent = create_deep_agent(
    model=model,
    skills=["./skills"],
    backend=FilesystemBackend(),
)

我把它的 SkillsMiddleware 拆开看过,实现很干净,值得照着写。

before_agent 阶段扫各来源目录,解析 frontmatter,把元数据写进 state["skills_metadata"]。state 里已经有了就直接返回 None,所以一次会话只扫一遍。

wrap_model_call 阶段把技能清单追加进 system message,走 request.override 写回。

多来源按顺序加载,同名后者覆盖前者,提示词里会标出哪条优先级更高。base → user → project 这种分层就是这么实现的。

加载失败的情况处理得也讲究。它会以 <skill_load_warnings> 的形式注入,并明确标注这是诊断信息、不得当作指令执行。这条写法的价值在防提示注入,值得抄。

但这里有一条边界,是我读源码时最想写进文章的一条。

SkillsMiddleware 自己不注册任何工具。它的 tools 是空的。

技能正文靠谁读?靠 read_file。而 read_file 这个工具来自 deepagents 的另一个中间件,文件系统那一个。

把 SkillsMiddleware 单独拿到纯 create_agent 里用,会发生什么?

模型看得到技能清单,知道有这么两条技能,却没有工具去读它们的正文。

清单上写着路径,模型照着路径伸手,抓了个空。

这不是抱怨官方实现。它在 deepagents 那套组合里工作得很好。

我想说的是:「读正文」这件事得有工具来承担,而这件事没人替你兜底。

顺手把几个校验规则列一下,都是容易踩的:

规则 触发时的行为
name 必须与目录名一致 只警告,照样加载
name 只允许小写字母、数字、连字符 不规范会警告
name 不超过 64 字符 超了会警告
description 不超过 1024 字符 静默截断,只保留前 1024
SKILL.md 不超过 10MB 超了跳过,防的是 DoS
allowed_tools 里列的工具 只渲染进提示词,不做任何强制检查

最后一条要特别说。allowed_tools 写的是「这条技能允许用哪些工具」,但它在实现里只是被拼进提示词里给模型看,没有任何运行时检查。

它是一句建议,不是一道闸门。想要真正的隔离,得上子 Agent 或者自己拦。

description 那条静默截断也很隐蔽。前端展示的时候看着很长很详细,实际进上下文的只有前面 1024 个字符。触发词要是写在第 1025 个字符上,这条技能就永远不会被触发。

九、自己写一个 Skills 中间件

知道边界之后,自己写一个就不难了。核心逻辑 4 步,代码 100 行上下。

class SkillLibraryMiddleware(AgentMiddleware):
    state_schema = SkillsState

    def __init__(self, skill_dirs, tool_name="read_skill"):
        super().__init__()
        self.library = SkillLibrary(skill_dirs)
        self.tools = [self._build_read_tool()]     # 关键:自己提供读正文的工具

    def before_agent(self, state, runtime):        # 扫目录、解析元数据
        if state.get("skills_metadata"):
            return None
        metas, warnings = self.library.scan()
        return {"skills_metadata": [m.to_state() for m in metas],
                "skills_warnings": warnings}

    def wrap_model_call(self, request, handler):   # 清单进 system message
        return handler(self._inject(request))

四个设计要点,前三个是常识,第四个是我实测踩出来的。

第一,路径白名单。

技能正文是磁盘上的文件,路径是模型给的。模型的路径来自它读到的清单,但你不能假设它一定守规矩。

../ 一个目录穿越,就能把服务器上任意一个文件读进上下文。这一条是安全底线,不是可选项。

root = Path(meta.root).resolve()
target = (root / file).resolve()
try:
    target.relative_to(root)
except ValueError:
    return f"[read_skill] 路径越界,已拒绝:{file}"

先 resolve 再比对,别用字符串前缀判断。.. 和各种软链接的变体能绕过简单的字符串检查。

第二,清单进 system message,别塞成一条消息。

技能清单是每轮都要用的内容,放进 system message 才能吃到 prompt 缓存。塞成一条普通消息,每轮都在变,缓存直接失效。

第三,正文绝不预加载。

wrap_model_call 里只注入 name 和 description。正文等模型自己调 read_skill 去读。这一条守住了,Skills 的成本优势才成立。

第四,同步与异步钩子都要实现。

这条我撞得比较实在。写完 wrap_model_call 用 ainvoke 跑,直接报错:

NotImplementedError: Asynchronous implementation of awrap_model_call is not available.
You are likely encountering this error because you defined only the sync version
(wrap_model_call) and invoked your agent in an asynchronous context.

官方报错信息写得很清楚,两套都要给。写成 abc 层面强制可能更好,现在这个设计得靠报错提醒。

自己写和用官方的差异,一张表说清:

对比项 官方 SkillsMiddleware 自己实现
读正文的工具谁提供 依赖文件系统中间件 自己注册 read_skill
能否脱离 deepagents 不能,缺了文件系统就没法用 能,核心逻辑自包含
allowed_tools 是否强制 只进提示词 可按需做成运行时校验
多源优先级 支持,后者覆盖前者 需要自己加
路径白名单 由文件系统中间件负责 自己负责,必须做

选哪边,用的还是上一篇那套判据:能力能不能自己说了算。

要接进 deepagents 那套现成体系,用它自带的。要嵌进自己的平台、要控制读文件这条路径、要做工具级强制,自己写。

十、合体:一条完整工序串起两层

两层都齐了,走一遍真实场景。

用户说:客户说键盘有质量问题,要退款,订单号 SO20260912002,已经开票了。

这个任务里,工序来自 Skills,动作来自 MCP。两个 server 各走一种传输,正好一起验。

connections = {
    "order": stdio_conn("order_server.py"),    # 本地进程,stdio
    "kb": http_conn(port),                     # 常驻服务,streamable-http
}
tools = await pose_get_tools(connections)

built = build_agent(model, tools=tools)        # 注册 2 个中间件:技能库 + 工具
result = await built.agent.ainvoke({"messages": [{"role": "user", "content": QUESTION}]})

跑起来是这样:

用户提问:处理一笔退款

Agent 启动

技能清单进 system message
只有 name + description

模型读清单,判断命中 refund-flow

调 read_skill 读正文
拿到 5 步工序

按工序调 MCP 工具
查政策 → 查订单 → 试算 → 提交

出回执:工单号 / 金额 / 到账时间 / 依据条款

实际输出:

stdio : order_server -> ['query_order', 'calc_refund', 'submit_refund']
http  : kb_server    -> ['search_policy']

system message 里出现技能清单 : True
system message 里出现技能正文 : False
system message 长度 : 600 字符

  第 1 轮|模型看到  5 个工具|动作:read_skill
  第 2 轮|模型看到  5 个工具|动作:search_policy
  第 3 轮|模型看到  5 个工具|动作:query_order
  第 4 轮|模型看到  5 个工具|动作:calc_refund
  第 5 轮|模型看到  5 个工具|动作:submit_refund
  第 6 轮|模型看到  5 个工具|动作:最终回答

第 1 轮先读工序,第 2 到第 5 轮照着工序办事,第 6 轮出结果。

看看第 1 轮读到的正文,这是我那份 skill 的开头:

# 退款处理工序

按下面 5 步走,不要跳步。每一步都要拿到上一步的返回值再继续。

## 第 1 步:查订单

调用 query_order(order_no, tenant_id)。
拿不到订单时直接回复客户「未查到该订单」,不要继续往下走。

正文 674 个字符,只在命中时才进上下文。system message 里的清单是 600 个字符,常驻。

两件事同时发生了。技能清单不预加载正文,工具定义也不全量上桌。

最后出结果,跑通的输出是这样:

已受理。工单号 RF912002,退款金额 458.00 元,走质量问题通道,运费与税点全免。
到账时间 1 到 3 个工作日。您需要把发票原件寄回,地址我发到您手机上。
依据:refund.window 质量问题 30 天内可退;refund.invoice 已开票需财务先冲红。

回执里四个要素都齐了。金额来自试算工具的返回值,两条依据来自政策检索工具,我那份 skill 里明确写了「金额、时间、政策条款三项全部来自工具返回值,不要自己编」。

这就是工序的价值。 它把「该拿哪些数据」「该按什么顺序」「哪几个字段不能自己编」这三件事,从模型的自由发挥变成了明确要求。

顺手把货架模式叠上去,同一个 Agent 多挂一个中间件:

货架 404 个工具,模型看到 9 个
['read_skill', 'calc_refund', 'query_order', 'search_policy',
 'dom00_order_query_0000', 'dom00_order_query_0080', ...]

工序、动作、成本控制,三样都在位了。

十一、把这两层合起来看

MCP 让 Agent 有能力,Skills 让 Agent 有章法。

只给能力,它会乱试。手里攥着退款工具,却不问原因直接提交,把该走的审批跳过去。

只给章法,它无从下手。工序写得再清楚,没有工具去查订单,它只能干看着那 5 个步骤。

两张落地清单给你。

MCP 接入五步:

先探协议,确认 server 支持哪几种传输,本地还是远程。

再拿工具,get_tools 起手,有状态的场景换成会话复用。

然后处理鉴权、重名、错误、重试这四件事。

接着算一遍工具定义的账,看占到窗口的百分之几。

最后决定要不要上货架。

Skills 接入四步:

先把 description 写具体,把触发场景一条条列出来。

再控制正文长度,细则推到 references 里去。

然后决定读正文的工具由谁提供,是借现成的还是自己注册一个。

最后给路径白名单兜底。

回到这篇开头那笔账。

工具定义那 6 万 token,MCP 自己解决不了,它只负责把工具送进来。真正把它按住的,是「别一次全给」这个思路。货架解决工具那一半,任务命中才加载解决章法那一半。

两半加起来,一个 Agent 才可能在几百上千个工具面前保持清醒。

工序和工具都齐了,下一个问题自然会冒出来:这些能力该不该塞进同一个 Agent。

查订单的、写文案的、审合同的,全堆在一个 system prompt 里,上下文又开始膨胀,权责也开始模糊。下一步该聊的,是把一个 Agent 拆成几个。


本篇完整工程化 demo(2 个 MCP server 分别走 stdio 与 streamable-http、货架式工具网关、自实现的 Skills 中间件、16 项冒烟测试,核心零依赖、无需 API key 即可跑通)已打包。在公众号回复 mcp 领取。