2.5 一次工具调用的端到端旅程


2.5 一次工具调用的端到端旅程

本节摘要:本章前面四节分别讲了三角边界、三大原语、两层模型、模块分层,现在用一个具体的场景把这些概念全部串起来——一次「模型决定调用 add 工具」的完整旅程。我们会从模型在宿主里推理开始,跟着请求穿过客户端、跨过传输、到达服务端、被能力声明匹配、被工具管理器分发、执行函数、返回结果,最终喂回模型。每一步都标注它对应本章哪个概念,让抽象的分层变成可见的过程。读完本节,本章所有概念将形成一个连贯的整体心智模型。

一、场景设定

设定一个具体场景:用户在 Claude Desktop(宿主)里问「1 加 2 等于几」。宿主连着一个跑着第 1.2 节 add 工具的服务端。我们追踪从「模型决定调用」到「结果回喂模型」的全过程。

涉及的各方:

┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 宿主 │ │ 客户端 │ │ 服务端 │ │ (Claude │ ──► │ (说 MCP 的 │ ──► │ (你的 add │ │ Desktop) │ │ 那一半) │ │ 工具) │ │ │ ◄── │ │ ◄── │ │ └──────────────┘ └──────────────┘ └──────────────┘ 跑模型 转发请求 执行业务

二、第一步:模型决定调用工具

旅程的起点在宿主内部。宿主把用户问题「1 加 2 等于几」连同工具清单(从服务端能力声明拿到的)一起喂给模型:

宿主 → 模型: 用户问:"1 加 2 等于几" 可用工具:[{name: "add", description: "Add two numbers.", schema: {a: integer, b: integer}}] 模型推理: "用户问加法,我有一个 add 工具正好能算, 我决定调用 add(a=1, b=2)"

关键点:模型看到的工具清单,是宿主从服务端能力声明里拿的(对应第 1.3 节的 server_capabilities、第 3 章的能力声明)。模型据此判断「该不该调、调哪个、传什么参数」。

💡 技巧:这一步解释了为什么工具的文档字符串和 Schema 这么重要——它们是模型在这个决策点的唯一依据。模糊的描述会导致模型乱调用,这就是第 4 章「类型即契约」的核心动机。

三、第二步:宿主把请求交给客户端

模型决定调用后,宿主把这次工具调用请求交给它的客户端实例:

模型 → 宿主:"请调用 add(a=1, b=2)" 宿主 → 客户端 A:转发这次调用

注意是宿主(不是模型)在驱动客户端——客户端是宿主的一部分,模型从不直接和客户端对话。这就是第 2.1 节讲的「三角边界」:模型只跟宿主对话,宿主跟客户端对话,客户端跟服务端对话。

四、第三步:客户端发出 JSON-RPC 请求

客户端把这次调用包装成一个 JSON-RPC 请求(MCP 协议的消息格式),通过传输层发出去:

客户端构造的 JSON-RPC 请求(概念形态): { "method": "tools/call", "params": { "name": "add", "arguments": {"a": 1, "b": 2} } }

这一步对应第 2.4 节讲的「两端通过协议类型对话」——客户端用 mcp-types 里定义的 tools/call 请求类型构造消息。

五、第四步:传输层搬运消息

请求通过传输层从客户端搬到服务端。具体怎么搬,取决于用哪种传输(第 8 章详讲):

传输 怎么搬
内存(开发用) 直接函数调用,无序列化
stdio(本地) 写到服务端子进程的 stdin
流式 HTTP(生产) 发 HTTP POST 请求到服务端

关键洞见:无论哪种传输,服务端业务代码完全不变——它收到的都是同样的协议对象。这就是第 8 章会讲的「传输与业务解耦」。

六、第五步:服务端能力声明匹配

请求到达服务端后,服务端先做能力声明匹配——确认自己声明了 tools 能力,愿意回应 tools/call:

服务端检查: 客户端发来 tools/call 请求 我声明了 tools 能力吗? → 是(因为注册了 @mcp.tool) → 接受请求,继续分发

如果服务端没声明 tools 能力(没注册任何工具),它会在连接初期就告诉客户端「我不支持 tools」,客户端根本不会发这种请求过来。这就是第 1.3 节看到的「能力是自动声明的,没注册就不声明」。

七、第六步:工具管理器分发

通过能力检查后,请求进入工具管理器(第 3 章详讲),由它分发到对应的工具:

工具管理器收到 tools/call add: 在注册表里查 "add" → 找到对应的函数 从 params 取 arguments {a: 1, b: 2} 把 arguments 按 Schema 校验 → 通过 调用 add(a=1, b=2)

这一步是「两层模型」的核心现场——你用 @mcp.tool() 装饰器注册的 add 函数,在低层被工具管理器包装成一个 on_call_tool 处理器。装饰器糖在这一步被「拆开」,变成实际的函数调用。这就是第 2.3 节说的「MCPServer 是写法,Server 是事实」。

八、第七步:执行函数,返回结果

add(1, 2) 执行,返回 3。SDK 把这个返回值包装成协议响应:

服务端构造的响应(概念形态): { "content": [{"type": "text", "text": "3"}], ← 给模型的文本 "structuredContent": {"result": 3} ← 给应用的结构化数据 }

注意返回里有两块内容——这是第 1.3 节看到的 contentstructured_content,第 4 章会详讲为什么标量被包成 {"result": ...}

九、第八步:结果回程

响应沿着原路返回:服务端 → 传输层 → 客户端 → 宿主:

服务端 → 传输 → 客户端:tools/call 响应 客户端 → 宿主:工具调用的结果 宿主 → 模型:把结果作为「工具结果消息」喂回

十、第九步:模型继续推理

最后一步,模型拿到工具结果,继续推理:

模型收到: 工具结果:{"content": [{"text": "3"}]} 模型推理: "工具返回 3,所以 1 加 2 等于 3, 我可以回答用户了" 模型 → 宿主:"1 加 2 等于 3" 宿主 → 用户:显示回复

旅程结束。从用户问问题到拿到答案,请求在三角之间走了一个完整的来回。

十一、把全程压成一张图

把这九步压成一张时序图,你会看到整条链路的全貌:

用户 宿主 客户端 传输 服务端 工具管理器 add函数 │ │ │ │ │ │ │ │──问──────►│ │ │ │ │ │ │ │──喂问题+工具清单───►│ │ │ │ │ │ (模型推理:决定调 add) │ │ │ │ │──转发调用─►│ │ │ │ │ │ │ │──JSON-RPC►│ │ │ │ │ │ │ │──搬运───►│ │ │ │ │ │ │ │──能力检查─►│ │ │ │ │ │ │ │──分发────►│ │ │ │ │ │ │ │─执行 │ │ │ │ │ │◄──结果───│ │ │ │ │ │◄──────────│ │ │ │ │ │◄──响应───│ │ │ │ │ │◄──响应──│ │ │ │ │ │◄──结果────│ │ │ │ │ │ │ (模型推理:用结果回答) │ │ │ │◄──答──────│ │ │ │ │ │

十二、本章概念与旅程步骤的对应

最后用一张表,把旅程每一步对应到本章的概念,巩固整体理解:

旅程步骤 对应本章概念
模型看工具清单 第 2.1 节 三角边界(模型只在宿主里)
模型决定调用 add 第 2.2 节 工具是「模型决定」的原语
宿主转发给客户端 第 2.1 节 一宿主多客户端
客户端构造 JSON-RPC 第 2.4 节 协议类型独立成包
传输搬运消息 第 8 章(预告)传输与业务解耦
服务端能力声明匹配 第 3 章 能力声明机制
工具管理器分发 第 2.3 节 两层模型(装饰器拆开)
返回 content + structured_content 第 4 章 结构化输出
结果回喂模型继续推理 第 2.1 节 服务端从不直接对话模型

本节要点回顾

  1. 旅程九步:模型决策 → 宿主转发 → 客户端构造 JSON-RPC → 传输搬运 → 能力匹配 → 工具管理器分发 → 执行 → 包装响应 → 回喂模型。
  2. 模型看到的工具清单来自服务端能力声明,这是它决策的唯一依据。
  3. 模型从不直接和客户端/服务端对话,所有交互经宿主中转。
  4. 传输怎么搬不影响业务,服务端收到的都是同样的协议对象。
  5. 装饰器糖在「分发」这一步被拆开,变成实际的函数调用——两层模型的现场。
  6. 返回值含两块(content + structured_content),分别给模型与应用。
  7. 本章所有概念在这条旅程里各就各位,形成连贯心智模型。

第 2 章结束。你已经有了完整的架构地图。从第 3 章开始,我们深入服务端主线,讲透三大原语的注册与能力声明机制。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U