降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,还是从注册中心拿地址,决定了后面扩容和灰度能不能做。

本篇的完成标准可以验证,一共三条:

  1. 数字人服务启动后,能通过 MCP 发现 CRM Server 暴露的工具,工具清单里出现 crm_query_customer;
  2. 一次真实对话能触发该工具并拿到客户等级,返回内容进入模型上下文;
  3. 人为制造的三类故障——错误端点、服务断连、客户不存在——在日志和接口返回里呈现为三种可区分的错误。

标准要能证
不证即猜
猜则返工
返工更贵

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 自动发现这些工具、怎么在实例扩缩容时保持调用不中断,那是紧随其后的下一个问题。

前提一换
结论全变
显式回答
才叫工程

posted @ 2026-10-10 23:39  李福春  阅读(4)  评论(0)    收藏  举报