降SpringAI阿里第7掌-突如其来-通玄MCP
降SpringAI阿里第7掌-突如其来-通玄MCP
把工具搬出进程之后,错误才开始说话
[读者] 已跑通 ChatClient 与本地 Tool 的 Java 后端
[痛点] 能力锁在一个进程里,多语言多应用无法共享
[现在读] CRM 团队要用 Go 重写,数字人必须远程调工具
[读完] 交付一个可被发现、可区分错误来源的 MCP Server
[旧方案] 本地 @Tool 直接调用 CRM SDK
|
v
[新需求] CRM 能力跨服务、跨语言、多应用共享
|
v
[冲突] 进程内方法调用没有协议,也没有错误语义
|
v
[后果] 每接入一个应用就复制一遍胶水代码
我是老李,接着第 6 掌的仓库继续写。上一掌我们在数字人服务里挂了几个本地 Tool,企业知识查询、场次查询都是进程内的一个方法调用,写完就能用,非常舒服。
舒服的地方往往藏着代价。舒服来自两个前提:调用方和被调用方在同一个 JVM,且同一次发布。这两个前提一旦被打破,第 6 掌那套写法会立刻从资产变成负债。
分支 chapter/07-mcp 就是为打破前提而开的。CRM 团队宣布现有 Java 版客户查询接口要下线,他们用 Go 重写,只保证提供一个 MCP Server;同时公司另外两个产品线也要用同一份 CRM 查询能力。数字人这边不再拥有那段逻辑,只能成为一个调用方。
同进程的方法调用,换成跨进程的协议调用,真的只是换个调用方式吗?
工具从进程内搬到进程外,最先坏掉的到底是功能,还是我们一直以来默认成立的错误处理?
如果这一步做错,数字人会不会在 CRM 服务短暂重启后,连着几天都查不到客户,而我们却只知道一句“工具调用失败”?
01、故事
现场是这样的:数字人 Web 运行页上,运营同学用语音问“张伟现在什么等级”,模型决定调用工具,工具返回客户档案,数字人念出结果。整个链路在第 6 掌已经跑通,延迟和成功率都让人满意。
这一次的任务是,把“CRM 客户查询”从一个进程内方法,变成一个独立部署、可被多语言消费的 MCP Server,并让数字人通过 MCP Client 远程调用它。听起来只是换个连接方式,实际牵动的是三件事:能力的归属、错误的语义、部署的顺序。
第 6 掌我们留下的,是一段极其诚实的代码:在 DigitalHumanChatService 里注入 CrmClient,用 Spring 的 HTTP 客户端去调 CRM 的内部接口。这段代码写得很顺,因为它默认了一件事:CRM 那边永远是一个能说话的 HTTP 服务,它返回 200 就是成功,返回 500 就是失败,网络断了抛异常。
CRM 团队的新接口不是 HTTP 接口,而是一个 MCP Server。这意味着我们不能再假设“返回值是什么形状”,只能假设“工具描述是什么形状”。模型看到的不再是 Java 方法签名,而是一段自然语言描述加一个 JSON Schema。
那么问题来了,我们凭什么相信模型会按我们想的方式调用这个远程工具?
认证加在哪里,是在网关,还是在每一次工具调用上?
如果 MCP Server 重启了两分钟,数字人是应该报错、应该降级,还是应该假装无事发生?
同进程易
跨进程难
难在错误
不在调用
02、问题
旧方案在这个需求下失效得很彻底。原来 CrmClient 是强类型的 Java 接口,编译期就能校验参数。现在对面只承诺一个工具名和一段描述,参数校验退化到运行期,由模型决定传什么。
业务影响是可见的。第 6 掌的数字人回答客户等级时,错一个数字,运营会立刻打电话过来;而远程调用引入的失败面,比进程内调用大一个数量级:进程不在、端口不通、协议不匹配、握手失败、工具内部业务异常,任何一种都会让这一句回答变成“抱歉,我查不到”。
技术表现上,我们真正缺的是三样东西:
第一,能力描述。工具的名字、参数、边界,必须能被非 Java 语言的调用方理解,并且稳定。
第二,错误分层。协议错误、网络错误、业务错误必须能被区分,因为它们的处置动作完全不同:协议错误要改配置,网络错误要重试或降级,业务错误要直接告诉用户。
第三,发现机制。数字人是硬编码一个 URL,还是从注册中心拿地址,决定了后面扩容和灰度能不能做。
本篇的完成标准可以验证,一共三条:
- 数字人服务启动后,能通过 MCP 发现 CRM Server 暴露的工具,工具清单里出现
crm_query_customer; - 一次真实对话能触发该工具并拿到客户等级,返回内容进入模型上下文;
- 人为制造的三类故障——错误端点、服务断连、客户不存在——在日志和接口返回里呈现为三种可区分的错误。
标准要能证
不证即猜
猜则返工
返工更贵
03、原理
MCP 是一个协议,不是一个框架。这句话决定了后面所有的设计判断。协议只规定消息怎么描述、怎么传、怎么回错,不规定你的工具该长什么样、该多细、该不该拆。
先说传输。MCP 的传输方式决定了部署形态:
- stdio:客户端把 Server 当子进程拉起来,通过标准输入输出通信。优点是没有网络、没有端口、没有认证问题;缺点是 Server 必须和客户端在同一台机器上,进程生命周期被客户端绑死。它适合本地开发、CLI 工具、单机桌面应用。
- SSE:早期规范定义的 HTTP 长连接方式,客户端先建立一条 SSE 流接收消息,再向服务端的 message endpoint POST 请求。它能跨网络,但会话状态和连接是两套端点,部署时要处理粘性。
- Streamable HTTP:较新规范引入的方式,单个 HTTP 端点同时承担请求与流式响应,客户端用
Accept: application/json, text/event-stream声明可以接受哪种返回。它更适合放在网关后面、做水平扩容(具体字段与端点路径请按当前官方文档核验)。
这里要给出本篇第一个反直觉判断:MCP 真正标准化的是错误,不是调用。 调用本身,HTTP + JSON 二十年前就标准化了。MCP 的增量在于,它把“工具不存在”“参数不符合 Schema”“工具执行失败”这三种情况,在协议层分开表达,让调用方能写出确定的分支。如果你用 MCP 只是把 HTTP 调用包装一遍,却没有用上这套错误语义,那你付了协议成本,没拿到协议收益。
第二个反直觉判断:远程工具变慢,通常不是网络慢,而是失败模型变复杂。 进程内调用失败只有一种表现——抛异常,而且异常当场就能被调用栈定位。远程调用失败至少有三层来源,每一层都可能被上层吞掉。所谓“变慢”,很多时候是重试在悄悄吃掉时间,而重试策略是被默认配置决定的,没人看过。
第三个判断关于粒度。工具越细,模型选择成本越高。get_customer_name、get_customer_level、get_customer_owner 三个工具,比一个 crm_query_customer 更容易让模型选错。远程工具和本地工具在这一点上规则相同,但远程工具的调用代价更高,粒度错误被放大。
协议管错
不管业务
错分三层
各归其位
04、架构
[输入] 用户问:张伟现在什么等级
|
v
[模块] 数字人服务 ChatClient + MCP Client
|
v
[数据/状态] MCP 会话 / 工具清单缓存 / 连接状态
|
v
[处理] 模型选工具 → JSON-RPC tools/call → CRM MCP Server
|
v
[输出] 客户档案 JSON → 模型 → 自然语言回答 / 分层错误
边界画在这里:数字人服务只负责“决定调不调、怎么解释结果”,CRM MCP Server 只负责“数据准不准、业务规则怎么判”,两者之间只有工具描述和数据契约,没有共享代码、没有共享数据库连接。这个边界是这一掌最贵的资产。
收益有三条。其一,CRM 团队可以用任何语言实现 Server,数字人不需要引入 CRM 的 SDK。其二,另外两个产品线复用同一个 Server,能力只维护一份。其三,错误职责清晰:数据类问题归 Server,编排类问题归数字人。
代价同样有三条。第一,多了一跳网络,端到端延迟必然增加,且增加量不可控。第二,工具的描述质量成了新的故障源,描述不清会让模型传错参数,这种问题在编译期看不见。第三,部署顺序出现了依赖,Server 没起来时数字人启动会有一段时间工具为空,必须设计好这个窗口期的行为。
适用条件是明确的:跨服务、跨语言、多应用共享的能力,走 MCP;同进程内的确定性逻辑,比如把时间格式化、把一段固定文案拼出来,继续用本地 Tool。这条规则会在第 9 章的 Agent 编排里反复用到。
边界一画
责任自明
共享一份
各自演进
05、实战一次
环境与版本
- JDK 21 LTS
- Spring Boot 3.x(与所选 BOM 对齐)
- Spring AI Alibaba 1.1.2.2,通过 BOM 统一版本
- MCP Server 与 MCP Client 的 Starter 均来自 Spring AI 体系
依赖坐标与属性前缀会随版本演进,以下配置请以当前官方文档与 BOM 实际内容核验。
依赖
CRM MCP Server 侧:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
数字人侧:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
配置
CRM MCP Server,application.yml:
server:
port: 8090
spring:
application:
name: crm-mcp-server
ai:
mcp:
server:
name: crm-mcp-server
version: 1.0.0
protocol: STREAMABLE
type: SYNC
数字人侧,application.yml:
spring:
ai:
mcp:
client:
enabled: true
name: digital-human-web
version: 1.0.0
type: SYNC
request-timeout: 20s
streamable-http:
connections:
crm:
url: http://127.0.0.1:8090
endpoint: /mcp
如果只是本机开发、不想开端口,把连接方式换成 stdio 即可,形态大致是给一段 command 与 args 让客户端拉起子进程。stdio 没有认证问题,也没有重连问题,因为它根本没有网络。
核心实现
CRM Server 侧,工具类:
package com.example.crm.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class CrmQueryService {
private final JdbcTemplate jdbcTemplate;
public CrmQueryService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Tool(name = "crm_query_customer",
description = "按手机号查询 CRM 客户档案,返回姓名、客户等级、归属销售、最近成交金额。"
+ "当用户询问某位客户的等级、负责人或成交情况时调用本工具。")
public CrmCustomer queryCustomer(
@ToolParam(description = "客户手机号,11 位数字,例如 13800000000") String phone) {
if (phone == null || !phone.matches("\\d{11}")) {
throw new IllegalArgumentException("phone 必须是 11 位数字");
}
return jdbcTemplate.queryForObject(
"select name, level, owner, last_deal_amount, last_deal_at "
+ "from crm_customer where phone = ?",
(rs, rowNum) -> new CrmCustomer(
rs.getString("name"),
rs.getString("level"),
rs.getString("owner"),
rs.getBigDecimal("last_deal_amount"),
rs.getString("last_deal_at")),
phone);
}
}
package com.example.crm.tool;
import java.math.BigDecimal;
public record CrmCustomer(
String name,
String level,
String owner,
BigDecimal lastDealAmount,
String lastDealAt) {
}
Server 侧注册工具:
package com.example.crm.config;
import com.example.crm.tool.CrmQueryService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider crmToolCallbackProvider(CrmQueryService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
}
数字人侧挂载远程工具,注意 MCP Client 的自动配置已经注册了一个 ToolCallbackProvider,直接注入即可,不需要自己再包一层:
package com.example.digitalhuman.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider tools) {
return builder
.defaultSystem("你是数字人助手。涉及客户档案的问题必须调用 CRM 工具,"
+ "不要凭记忆编造客户信息。")
.defaultToolCallbacks(tools.getToolCallbacks())
.build();
}
}
启动
java -jar crm-mcp-server/target/crm-mcp-server-1.0.0.jar
java -jar digital-human-web/target/digital-human-web-1.0.0.jar
请求与验证
先单独验证 Server 是否真的暴露了工具,这一步能省掉后面大量猜测:
curl -s -X POST http://127.0.0.1:8090/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
示例输出:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "crm_query_customer",
"description": "按手机号查询 CRM 客户档案,返回姓名、客户等级、归属销售、最近成交金额。",
"inputSchema": {
"type": "object",
"properties": { "phone": { "type": "string" } },
"required": ["phone"]
}
}
]
}
}
再走一次数字人的对话接口:
curl -s -X POST http://127.0.0.1:8080/api/chat \
-H 'Content-Type: application/json' \
-d '{"sessionId":"demo-1","message":"张伟现在什么等级"}'
示例输出:
{
"sessionId": "demo-1",
"answer": "张伟目前是黄金等级,归属销售是李娜,最近一次成交金额为 8.6 万元。"
}
未在当前环境实测,以下为预期结果:数字人启动日志里能看到 MCP 客户端完成初始化的记录,工具清单包含一个来自远程 Server 的工具。验收的第 1 条与第 2 条到此完成,第 3 条要留到下一章去挖。
先证服务端
再证调用端
两端都证
才叫跑通
06、排查
第 3 条验收标准不是顺手能过的,它逼着我们把失败当一等公民看。以下三条排查链都来自远程工具落地时的真实形态。
链路一:工具清单是空的
现象:数字人启动成功,但对话时模型说“我没有可用的查询工具”,日志里没有任何异常栈。
怀疑:MCP Server 没起来,或者端口不通。
检查:先不碰数字人,直接对 Server 发 tools/list。
证据:curl 返回 404 Not Found,而 Server 进程明明活着,端口也通。翻配置发现客户端写的是 endpoint: /sse,而 Server 配的是 protocol: STREAMABLE。
根因:客户端传输方式与服务端协议不匹配。SSE 与 Streamable HTTP 是两套端点约定,配错了不会报“协议不匹配”,只会得到 404,然后被客户端静默处理成“没有工具”。
修复:两侧协议对齐,客户端 endpoint 与 Server 实际暴露路径一致。
链路二:工具被调用了,但参数是空的
现象:日志能看到 tools/call 请求,Server 侧直接抛 phone 必须是 11 位数字,用户看到“查询失败”。
怀疑:模型没理解参数含义。
检查:把 Server 收到的 JSON-RPC 请求参数原样打出来。
证据:params.arguments 里 phone 字段缺失,或者传的是 张伟。
根因:工具描述里没有写清“输入是手机号”,@ToolParam 缺少 description,模型只能从方法名和上下文猜;同时参数在 Schema 里没有声明为必填,模型认为可以省略。
修复:把 description 写成人能读懂的一句话,明确格式和示例;Server 侧保持参数校验作为兜底,但校验异常要转成业务可读的错误信息,不要把 Java 异常原文丢回去。
链路三:CRM 重启之后,数字人再也没恢复
现象:CRM Server 滚动重启,两分钟后起来了,但数字人这边后续所有客户查询全部失败,重启数字人才恢复。
怀疑:连接断了没重连。
检查:对比两端日志时间线,看客户端是否在断连后重新发起初始化。
证据:客户端日志出现一次连接被重置,之后没有任何重连记录,会话状态停留在旧连接上。
根因:长连接断开后客户端未自动重建会话,而调用侧的异常处理把“连不上”和“查不到”混成了同一种结果。
修复方向见第 7 章:在客户端加健康探测与重连,并把“连不上”翻译成独立的错误类型,绝不与业务错误共用一条返回路径。
这里必须记录一次错误判断。
错误尝试: 我们把客户端 request-timeout 从 20 秒改到 120 秒,理由是“调不通大概率是超时”。改完之后错误出现得更晚了,问题一次都没少。
为什么错:建连失败和请求超时是两类错误。客户端在启动阶段连不上 Server,属于传输层问题,请求超时参数根本管不到这一段;把超时调长,只是把同一个错误推迟了两分钟暴露,同时让真正的业务超时也失去了报警价值。改配置之前,应该先确认错误发生在哪一层。
先分层
再定位
后改参
不乱猜
07、优化
第 6 章的三条证据指向同一个根因:错误没有被分层,所以调用方无法做出不同动作。V2 只做这一件事,但做彻底。
根因一:端点与协议不匹配只能靠人肉对齐
修改:把端点路径与协议模式收进配置项,并在 Server 启动时打印实际暴露的路径与协议,客户端启动后主动拉一次工具清单,清单为空直接 fail-fast。
原因:静默失败比显式失败贵得多,启动时报错比上线后排查便宜。
新行为:协议配错时应用起不来,错误在本地就被拦住。
验证:故意把 endpoint 改错一位,应用启动失败并给出明确提示,而不是启动成功但无工具。
根因二:业务错误与协议错误共用一条返回路径
修改:在 Server 侧统一业务错误结构,工具内部捕获业务异常后返回带 code 的结果,例如 CUSTOMER_NOT_FOUND;客户端解析 code 得到业务错误,其余异常归入协议或传输错误。
public record ToolError(String code, String message) {
public static ToolError notFound(String phone) {
return new ToolError("CUSTOMER_NOT_FOUND", "未找到手机号为 " + phone + " 的客户");
}
}
原因:调用方要区分的是“我该重试”还是“我该告诉用户”,这个区别必须在数据里表达,不能靠字符串匹配异常消息。
新行为:三类故障在日志与接口返回中呈现为三种形态。
验证:分别制造错误端点、停掉 Server、查询不存在的客户,观察三种结果。
根因三:长连接断开后无恢复路径
修改:客户端加健康探测与重连;对可重试错误设置有限次数重试,重试耗尽后降级为“CRM 服务暂时不可用,请稍后再试”,而不是“查不到该客户”。
原因:用户能接受“系统暂时不可用”,不能接受“这个人不存在”,后者是错误信息,会直接误导决策。
新行为:Server 短暂重启的窗口期内,用户看到的是服务不可用提示,Server 恢复后自动回到正常。
验证:重启 Server,观察客户端重连日志与对话结果。是否真的自动恢复、恢复耗时多少,需要以实际运行日志为准,本节不给出未经实测的数字。
关于认证:远程工具一旦出内网,就必须有认证。做法是在 MCP Server 前置一层鉴权(例如基于 Bearer Token 的过滤器),客户端在传输层注入对应请求头;具体注入入口随客户端传输实现不同,需按当前官方文档核验。认证不是加一个 Header,它重新划定了信任边界——从此工具调用不再默认来自可信内网。
错要分家
才能分治
治有先后
序不可乱
08、演进
[同一句:张伟现在什么等级]
|
+--[V1] 进程内 @Tool 直连 CRM SDK
| 正确性高 / 无法跨语言 / 错误只有一种
|
+--[V2] MCP Client → 远程 MCP Server
| 跨语言共享 / 错误分三层 / 多一跳网络
|
[Trade-off]
得到跨语言复用与统一协议
失去进程内的确定性与零网络成本
边界:同进程确定性逻辑继续留在本地 Tool
正确性上,V1 的参数校验由编译器保证,V2 退化为运行期校验,需要描述、Schema、服务端兜底三重保障才能接近 V1 的水平。这是真实的退步,必须承认。
稳定性上,V2 引入了新的失败面,但也第一次让失败可分类。从“不知道哪里错了”变成“知道哪一层错了”,是稳定性工程里少见的净收益。
复杂度上,V2 多了一个需要独立部署、独立监控、独立发布的服务,运维成本上升。这个成本只能靠“被多个应用共享”来摊薄,如果只有一个调用方,把能力拆成 MCP Server 是不划算的。
成本上,最大的成本不是多一跳网络,而是工具描述成为需要长期维护的接口契约。改了工具描述就是改了对外契约,要像改 API 一样走评审。
适用范围上,跨服务、跨语言、多调用方,选 MCP;同进程、单调用方、确定性逻辑,选本地 Tool;需要被多个 Agent 动态发现的场景,还要在第 13 章叠加注册中心。
遗留问题有三个:一是当前客户端是静态配置地址,扩容与灰度做不了;二是认证方案还没有落地到客户端;三是错误分层已经有了,但没有指标化,运维看不到各层的占比。前两个指向 Nacos 注册,第三个指向第 17 章的观测治理。
得在复用
失在确定
账要算清
再谈演进
09、洞见
9.1 工具边界的判据是调用方数量,不是技术先进性
反直觉判断:一个能力该不该拆成 MCP Server,主要看它有几个调用方,而不是看它复不复杂。
只有一个调用方的复杂能力,拆出去只会让你多维护一个进程、多一条部署依赖、多一类故障,收益是零。只有当调用方变成两个以上,或者调用方使用不同语言时,“拆”才开始有正收益。第 6 章我们坚持本地 Tool,第 7 章必须拆,区别不在能力本身,而在谁要用它。
9.2 远程工具的失败率不是网络决定的,是错误模型决定的
反直觉判断:远程工具让人觉得不可靠,往往不是因为网络抖动,而是因为失败无法归因,导致每次都要从头查。
同样的一次失败,在能分层的情况下是一条明确告警,在不能分层的情况下是一场两小时的排查。前者是可观测的工程系统,后者是靠运气的系统。MCP 提供的分层语义,价值在这里,而不是在“能跨语言”这件表面上。
9.3 工具描述是接口契约,不是注释
反直觉判断:@Tool 的 description 比方法签名更重要,因为模型只读描述,不读签名。
在本地 Tool 时代,描述写差点只是效果差一点;在远程 MCP 时代,描述是双方唯一的沟通契约,描述错了就是接口错了。工具描述应当和 API 文档同等级别管理,改描述要走评审,要能回答“这次改动会不会影响其他调用方”。
9.4 认证是把每个请求都当成不可信
反直觉判断:工具从内网搬到远程之后,调用方不再自动可信,哪怕它昨天还在同一个 JVM 里。
进程内调用天然可信,是因为进程边界本身就是信任边界。一旦跨进程、跨网络,信任边界就被重画了。认证不是额外加的一道门,而是承认“对面可能不是我以为的那个对面”。
判据简单
判断很难
难在克制
不在技术
10、系统落地
原来有什么:数字人服务里有一个进程内的 CRM 查询工具,逻辑和数据访问都在同一个仓库,只能被这一个应用使用。
本篇新增了什么:一个独立部署的 CRM MCP Server,通过 Streamable HTTP 暴露 crm_query_customer;数字人侧通过 MCP Client 自动发现并挂载远程工具;一条贯穿协议层、传输层、业务层的错误分层链路。
现在能做什么:数字人可以远程调用 CRM 能力,工具清单在启动时自动获得,不再硬编码工具方法;CRM 能力可以被其他语言、其他应用以同一份契约接入;三类故障在日志中可区分。
还缺什么:一是地址仍是静态配置,扩容和灰度做不了,需要引入注册中心做动态发现;二是客户端认证尚未落地;三是错误分层没有指标化,无法做容量与质量评估。
下一步如何演进:把 Server 注册到 Nacos,客户端从注册中心发现实例,去掉硬编码 URL,同时叠加认证与健康检查;再往后,工具会从“被一个 Agent 静态挂载”变成“被多个 Agent 按需发现”,这会在多 Agent 章节正式展开。
本篇对应的提交是 feat(ch07): integrate MCP client and tool services,分支 chapter/07-mcp。验收第 1、2 条已在本地闭环,第 3 条依赖第 7 章的分层改造完整落地。
一份能力
多次复用
复用之前
先画边界
11、小结
Q1 → 不只是换调用方式,错误处理的默认前提整个换掉了
Q2 → 坏掉的是错误语义,功能反而最容易保住
Q3 → 短期会,分层 + 重连 + 降级之后不会
状态 → 数字人可发现并调用远程 MCP 工具,失败可分层归因
这一掌交付的不是一段新代码,而是一条边界:哪些能力留在进程内,哪些能力搬到进程外,以及搬出去之后错误由谁负责解释。这条边界一旦画错,后面 Agent 编排、多 Agent 协作、服务化上线都会反复付出代价。
边界画对
后面省事
边界画错
处处返工
12、作业
12.1 理解题:stdio 和 Streamable HTTP 该怎么选?
参考答案:看 Server 与客户端是否必须同机、是否需要被多个调用方访问。stdio 适合本地开发、CLI、单机场景,进程由客户端拉起,没有网络和认证问题;Streamable HTTP 适合跨网络、多调用方、需要放在网关后面的场景,代价是必须处理认证、重连和端点配置。判断标准是部署形态,不是性能偏好。
12.2 实战题:把企业知识查询也做成一个 MCP Server,并让数字人同时挂载两个远程工具。
参考答案:复制 CRM Server 的骨架,替换工具实现与工具描述,改端口和工具名;数字人侧在 streamable-http.connections 下增加一个连接名和对应 URL。注意两个 Server 的工具名不要冲突,描述要能区分场景,否则模型选错工具的概率会明显上升。启动后先用 tools/list 分别验证两个 Server,再看数字人的工具清单是否包含两项。
12.3 排障题:数字人日志显示工具调用成功,但模型回答“查不到这位客户”,而 CRM 数据库里明明有数据。
参考答案:先在 Server 侧打印收到的参数,确认模型传的手机号与用户实际说的号码是否一致;再检查 Server 的查询条件、租户隔离和数据权限;最后确认工具返回内容是否被正确序列化进模型上下文。多数情况下根因在参数层或数据权限层,而不是在模型层。不要先怀疑模型,模型只传它看到的参数。
12.4 架构判断题:某团队把只有数字人一个调用方的“天气查询”也拆成了 MCP Server,理由是“以后可能有人用”。
参考答案:这是把可能性当成需求。单一调用方、同进程、确定性逻辑,本地 Tool 是更优解。拆出去意味着多一个部署单元、多一条依赖链、多一类故障,而收益是零或者负。“以后可能有人用”不是判据,有第二个真实调用方才是判据。等到第二个调用方出现再拆,迁移成本远小于长期维护成本。
题在细节
答在判据
判据清楚
选择不难
13、思考
回到开篇那个冲突:同进程的方法调用,换成跨进程的协议调用,到底变了什么。
变的不是调用语法,而是所有默认前提。进程内调用默认可信、默认同版本、默认同时在线、默认异常只有一个来源;跨进程调用把这四条默认全部取消,换成四条必须显式回答的问题:谁在调用、版本是否匹配、对方是否在线、错误属于哪一层。
MCP 的价值不在于让工具能被远程调用,而在于它把这四个问题变成了协议里的可表达项。用不用得上,取决于你有没有把它们当回事。如果只是把原来的方法包一层扔出去,得到的是一个更慢、更容易挂、更难查的本地调用,而不是一个可复用的共享能力。
所以这一掌真正留下的判断只有一条:拆工具之前,先确认调用方数量;拆工具之后,先确认错误分层。前者决定该不该拆,后者决定拆了之后能不能运维。
至于怎么让多个 Agent 自动发现这些工具、怎么在实例扩缩容时保持调用不中断,那是紧随其后的下一个问题。
前提一换
结论全变
显式回答
才叫工程

浙公网安备 33010602011771号