欢迎光临
我们一直在努力

第25章:Spring AI 2.0 MCP Server——把能力暴露成工具

上章思考题答案

  • 库存进程使用 spring-ai-starter-mcp-server-webmvc。方法标 @McpTool(name = "query_stock", description = "按 SKU 查询邻里仓库存"),参数标 @McpToolParam。server.port 为 8081,协议用 STREAMABLE,端点保持默认 /mcp。健康检查是 GET /actuator/health,需要另外引入 actuator。客户端 URL 仍是 http://localhost:8081/mcp。
  • 未知 SKU 抛出 IllegalStateException("未找到商品")。回到模型的工具文本含「未找到商品」,不含 at org.springframework。模型转述时必须带上这六个字。堆栈只留在服务端日志。

  • 1. 项目背景

    1.1 业务场景

    客服要连 http://localhost:8081/mcp 查库存。若对面只是一个返回固定句子的桩,改库存规则仍然要改客服。本节把库存服务真正跑起来。进程只负责 SKU:SKU-RICE-5KG 的库存是 12,SKU-NONE 不存在。任何兼容 MCP 的客户端都能发现 query_stock,不只是邻里仓客服。客服侧用 DeepSeekChatModel 决定是否调用,调用路径是 SyncMcpToolCallbackProvider 和 ToolCallingAdvisor。

    注解在 mcp/mcp-annotations 的 McpTool、McpToolParam。扫描开关是 spring.ai.mcp.server.annotation-scanner.enabled,默认 true。服务端 starter 选用 WebMVC 这一条,因为客户端走的是 HTTP,而不是 STDIO。spring-ai-starter-mcp-server-webflux 留给响应式进程,本章的库存服务是同步的。进程内 @Tool 仍适合订单和物流这种跟客服一起发布的方法。库存要独立扩容、独立发布,才用 MCP Server。

    1.2 痛点

    有人在客服进程里再写一遍 @Tool 查询库存,MCP 服务白起。两边数字不一致时,用户会看到两个库存。有人把未知 SKU 的异常 printStackTrace 到返回值,模型把堆栈念出来,也把内部包名送进了提示词。有人只测服务自己的方法,不经过客户端和模型,于是端点路径错了、客户端仍连 /sse 时,单测仍是绿的。健康检查若只看 MCP 有没有注册工具,HTTP 端口没监听时运维会认为服务已好。端口、健康检查、工具列表、模型转述要分成四步。

    McpServerProperties 的 requestTimeout 默认 20 秒。库存方法若去扫全库,会在超时后断开,客户端看到的是传输失败,不是「未找到商品」。本章的方法只查内存里的两行夹具,不访问数据库。generateOutputSchema 默认 false,返回字符串即可,不额外生成输出 schema。健康检查返回 UP 只说明端口开着,工具没扫到时列表仍是空的,运维放行前还要看 listTools。扫描开关默认开启,测试仍显式写成 true,避免有人以为默认是关的。手工端口 8081 和测试随机端口同时存在时,以测试打印的端口为准。

    #mermaid-svg-gPEUe28b9wlsTb4B{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gPEUe28b9wlsTb4B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gPEUe28b9wlsTb4B .error-icon{fill:#552222;}#mermaid-svg-gPEUe28b9wlsTb4B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gPEUe28b9wlsTb4B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gPEUe28b9wlsTb4B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gPEUe28b9wlsTb4B .marker.cross{stroke:#333333;}#mermaid-svg-gPEUe28b9wlsTb4B svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gPEUe28b9wlsTb4B p{margin:0;}#mermaid-svg-gPEUe28b9wlsTb4B .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-gPEUe28b9wlsTb4B .cluster-label text{fill:#333;}#mermaid-svg-gPEUe28b9wlsTb4B .cluster-label span{color:#333;}#mermaid-svg-gPEUe28b9wlsTb4B .cluster-label span p{background-color:transparent;}#mermaid-svg-gPEUe28b9wlsTb4B .label text,#mermaid-svg-gPEUe28b9wlsTb4B span{fill:#333;color:#333;}#mermaid-svg-gPEUe28b9wlsTb4B .node rect,#mermaid-svg-gPEUe28b9wlsTb4B .node circle,#mermaid-svg-gPEUe28b9wlsTb4B .node ellipse,#mermaid-svg-gPEUe28b9wlsTb4B .node polygon,#mermaid-svg-gPEUe28b9wlsTb4B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gPEUe28b9wlsTb4B .rough-node .label text,#mermaid-svg-gPEUe28b9wlsTb4B .node .label text,#mermaid-svg-gPEUe28b9wlsTb4B .image-shape .label,#mermaid-svg-gPEUe28b9wlsTb4B .icon-shape .label{text-anchor:middle;}#mermaid-svg-gPEUe28b9wlsTb4B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gPEUe28b9wlsTb4B .rough-node .label,#mermaid-svg-gPEUe28b9wlsTb4B .node .label,#mermaid-svg-gPEUe28b9wlsTb4B .image-shape .label,#mermaid-svg-gPEUe28b9wlsTb4B .icon-shape .label{text-align:center;}#mermaid-svg-gPEUe28b9wlsTb4B .node.clickable{cursor:pointer;}#mermaid-svg-gPEUe28b9wlsTb4B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gPEUe28b9wlsTb4B .arrowheadPath{fill:#333333;}#mermaid-svg-gPEUe28b9wlsTb4B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gPEUe28b9wlsTb4B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gPEUe28b9wlsTb4B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gPEUe28b9wlsTb4B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gPEUe28b9wlsTb4B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gPEUe28b9wlsTb4B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gPEUe28b9wlsTb4B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gPEUe28b9wlsTb4B .cluster text{fill:#333;}#mermaid-svg-gPEUe28b9wlsTb4B .cluster span{color:#333;}#mermaid-svg-gPEUe28b9wlsTb4B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gPEUe28b9wlsTb4B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gPEUe28b9wlsTb4B rect.text{fill:none;stroke-width:0;}#mermaid-svg-gPEUe28b9wlsTb4B .icon-shape,#mermaid-svg-gPEUe28b9wlsTb4B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gPEUe28b9wlsTb4B .icon-shape p,#mermaid-svg-gPEUe28b9wlsTb4B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gPEUe28b9wlsTb4B .icon-shape .label rect,#mermaid-svg-gPEUe28b9wlsTb4B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gPEUe28b9wlsTb4B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gPEUe28b9wlsTb4B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gPEUe28b9wlsTb4B :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    SKU-RICE-5KG 或 SKU-NONE

    MCP Server :8081 /mcp

    @McpTool query_stock

    库存 12

    未找到商品

    MCP 客户端

    DeepSeekChatModel 转述

    回答含夹具原文且无堆栈


    2. 项目设计

    小胖说:「这不就是把库房的电话登记到黄页吗?谁都能打,不只有咱们柜台。黄页上写清楚分机,没货就说没货,别把仓库的监控录像讲给客人听。」

    小白问:「黄页用注解还是用一份手写 JSON?没货时把异常抛出去,客人听到的是『未找到商品』还是一串类名?柜台进程里再留一个同样的查询,两边库存不一样怎么办?」

    大师说:「工具方法放在库存进程的 @Service 上,@McpTool 写出名字和描述,@McpToolParam 写出 SKU。扫描默认开启。协议 spring.ai.mcp.server.protocol 设为 STREAMABLE,端点默认 /mcp,和客户端 URL 一致。spring.ai.mcp.server.stdio 保持 false,避免服务去占标准输入。未知 SKU 抛 IllegalStateException,消息只有「未找到商品」。全局处理把这段消息写进 MCP 的错误文本,不把 getStackTrace 拼进去。客服进程删除自己的库存 @Tool,只留客户端。订单和物流仍是第 22 章的进程内工具,因为它们和售后一起发布,没有独立端口。」

    技术映射: 独立发布的库存用 @McpTool。跟客服一起发布的订单查询仍用进程内 @Tool。错误文本只有「未找到商品」。

    小胖问:「门开了没有,怎么让运维一眼看见?」

    小白追问:「只 curl 一下 /mcp 算健康吗?模型没有密钥时,怎样证明错误文本能被转述,而不是只证明方法返回了六个字?」

    大师说:「运维看 GET http://localhost:8081/actuator/health,需要 spring-boot-starter-actuator。这只证明进程活着。工具是否注册,用客户端 listTools 断言名字以 query_stock 结尾。转述用脚本模型:工具结果含「未找到商品」时,回答必须含这六个字,且不含 at org.springframework。有 DEEPSEEK_API_KEY 才打真模型,真模型同样断言这六个字,不断言整段客套话。服务端日志可以有堆栈,返回给客户端的内容里没有。超时保持默认 20 秒,方法里不睡觉、不扫表。」

    技术映射: 健康检查看端口,工具列表看名字,转述看脚本模型。三步各自有断言。

    小白再问:「客户端和第 25 章的服务分两个进程,测试怎么启动?」

    大师说:「库存用 @SpringBootTest(webEnvironment = RANDOM_PORT) 时,客户端 URL 改成那个随机端口加 /mcp。固定 8081 留给运维的手工启动。测试不要假定本机 8081 空闲。随机端口的用例仍走真的 MCP 客户端和脚本模型,这样端点、注解、转述在同一次测试里相遇。手工启动时把端口设回 8081,用第 24 章的 yml 直连。」

    技术映射: 自动化测试用随机端口。运维约定的 8081 用于手工和健康检查。

    小白问:「异常在服务端日志里可以有堆栈,客户端拿到的工具文本里没有。这两份内容怎样在测试里同时看见?」

    大师说:「返回给 MCP 的错误只用 IllegalStateException.getMessage(),也就是「未找到商品」。日志用服务端自己的 logger 打异常,测试不抓控制台来做断言。客户端断言回答含这六个字,且不含 at org.springframework。若回答里出现 InventoryTools.queryStock,说明有人把 printStackTrace 或 toString 拼进了错误文本。堆栈进了工具结果,下一跳的提示词就会变长,第 26 章记 token 时会把包名也算进去。」

    小胖问两套库存:「柜台里的 @Tool 还留着一份十二袋,库房注解又一份,客人听谁的?」

    大师说:「客服模块删掉库存方法,只留 spring-ai-starter-mcp-client。订单 query_order_status 和物流 query_shipment 仍在客服进程,它们没有独立端口。库存数字的唯一来源是 InventoryTools。测试若在客服工程里再扫到名为 query_stock 的进程内工具,这条用例失败。随机端口写进客户端 URL,形如 http://localhost: 加上 @LocalServerPort 再加上 /mcp。写死 8081 会打到昨天没关的手工进程,注解改了测试仍绿。」

    小白再问超时:「默认 20 秒够不够?方法里要不要做一次数据库查询来显得真实?」

    大师说:「不够真实也不要加查询。本章的夹具是内存映射,SKU-RICE-5KG 对 12,其他 SKU 抛六个字。requestTimeout 保持 Duration.ofSeconds(20)。方法里不 sleep。一旦睡眠超过超时,客户端看到的是传输失败,断言会去找「未找到商品」从而红在错误的原因上。」

    小白问 WebFlux:「库存如果改成 spring-ai-starter-mcp-server-webflux,客户端还用现在的 URL 吗?」

    大师说:「端点仍是 /mcp 时 URL 不用改。本章不换 starter。WebMVC 和客服侧的同步客户端同一套调用习惯。换 WebFlux 要单独验证传输,不放进这次验收。stdio=true 会让服务去读标准输入,和 Web 端口抢运行方式。yml 里保持 false,测试可以断言绑定后的 isStdio() 为 false。协议枚举使用 STREAMABLE,不要写成小写后指望宽松绑定。写错时端点对不上,客户端列表为空。」


    3. 项目实战

    3.1 环境准备

    库存模块:

    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    application.yml:

    server:
    port: 8081
    spring:
    ai:
    mcp:
    server:
    name: linlicang–inventory
    stdio: false
    protocol: STREAMABLE
    annotation-scanner:
    enabled: true
    management:
    endpoint:
    health:
    probes:
    enabled: true

    端点不写时就是 /mcp。不要改成 /sse,除非客户端一起改。客服模块不再依赖库存的业务类,只保留第 24 章的客户端 starter。

    3.2 分步实现

    步骤 1:用注解暴露查询。 目标是 listTools 能看见 query_stock。

    @Service
    public class InventoryTools {
    @McpTool(name = "query_stock", description = "按 SKU 查询邻里仓库存")
    public String queryStock(@McpToolParam(description = "SKU 编码") String sku) {
    if ("SKU-RICE-5KG".equals(sku)) {
    return "库存 12";
    }
    throw new IllegalStateException("未找到商品");
    }
    }

    运行结果: 客户端列出的工具名以 query_stock 结尾,描述含「邻里仓」。

    可能的坑: 类没有变成 Spring Bean,扫描不到方法。处理:加上 @Service,并确认 annotation-scanner.enabled 没有被改成 false。

    步骤 2:随机端口上完成一次真调用。 目标是模型回答含「库存 12」,工具确实被打到。

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class InventoryMcpServerTest {
    @LocalServerPort int port;
    @Test
    void deepSeekPathUsesServer() {
    String url = "http://localhost:" + port + "/mcp";
    // 用 url 建造和第 24 章相同的 streamable-http 客户端
    String answer = ask(scriptedModel, "SKU-RICE-5KG 还有多少");
    assertThat(answer).contains("库存 12");
    assertThat(stockHits.get()).isEqualTo(1);
    }
    }

    脚本模型第一跳调用 query_stock,第二跳回显工具文本。stockHits 在 queryStock 里递增。

    运行结果: 回答含「库存 12」,次数为 1。次数为 0 则失败。

    可能的坑: 测试仍连接 8081,随机端口上的服务没人访问,打到了昨天手工启动的旧进程。处理:URL 使用 port 变量。

    步骤 3:未知 SKU 的转述。 目标是六个字在,堆栈不在。

    服务端把 IllegalStateException 的 getMessage() 写进工具错误。不要拼接 printStackTrace。

    String answer = ask(scriptedModel, "SKU-NONE 还有多少");
    assertThat(answer).contains("未找到商品");
    assertThat(answer).doesNotContain("at org.springframework");
    assertThat(stockHits.get()).isEqualTo(1);

    运行结果: 模型文本含「未找到商品」。日志里可以有异常,返回体里没有堆栈行。

    可能的坑: 异常被容器吃掉,工具结果变成空串,模型回答「库存充足」。处理:错误映射显式使用 getMessage(),并断言空串不是成功。

    步骤 4:健康检查与手工端口。 目标是运维能看进程,开发能看工具。

    GET http://localhost:8081/actuator/health

    手工启动使用 server.port=8081。返回 JSON 里的状态为 UP,只说明 Web 进程在。再让客户端 listTools,确认 query_stock。两步都做。只做健康检查,注解没扫到时运维仍会放行。

    可能的坑: 生产把 actuator 暴露到公网。处理:健康检查只在内网,MCP 的 /mcp 也不要对着公网裸奔。密钥仍只放在客服进程的 DEEPSEEK_API_KEY。

    3.3 完整代码清单

    库存应用:InventoryTools、application.yml、异常消息映射、InventoryMcpServerTest。客服侧复用第 24 章的询问方法,把 URL 指到随机端口。清单不包含数据库和真实库存中心。

    3.4 测试验证

    @Test
    void unknownSkuIsPhraseNotStack() {
    assertThat(answer).contains("未找到商品");
    assertThat(answer).doesNotContain("at org.springframework");
    }

    这条不需要密钥。冒烟在密钥和随机端口都就绪时,把脚本模型换成 DeepSeekChatModel,仍断言「未找到商品」或「库存 12」,并断言 stockHits 大于 0。回答像客服但次数为 0,失败。

    失败时分开看:列表里没有工具,先看 Bean 和扫描开关;有工具但次数为 0,先看客户端 URL 的端口;次数为 1 但回答没有六个字,先看错误映射有没有把 getMessage() 送出去。不要合成「MCP 不通」。

    3.5 端口、列表、转述分开记

    随机端口用例做三件事。listTools 的名字有一个以 query_stock 结尾,描述含「邻里仓」。SKU-RICE-5KG 的回答含「库存 12」,stockHits 为 1。SKU-NONE 的回答含「未找到商品」,不含 at org.springframework,stockHits 仍增加。次数为 0 而回答像客服,是模型没走 MCP。回答含堆栈类名,是错误文本拼接了异常 toString。

    客户端 URL 使用注入的端口。用例结束后打印实际 URL,方便和手工的 http://localhost:8081/mcp 区分。手工进程的健康检查是 GET /actuator/health,状态 UP 之后再列一次工具。只看 UP 就放行,扫描开关被关掉时工具列表是空的,客服会走第 24 章的「库存服务不可用」。那一句是客户端的短路,不是本章的未知 SKU。

    客服测试里的进程内工具名字集合不含 query_stock。订单和物流仍在。两套库存并存时,先删进程内方法,再跑随机端口。冒烟把脚本模型换成 DeepSeekChatModel 时仍断言夹具原文和 stockHits 大于 0,不断言模型的客套整段。没有密钥时,随机端口上的脚本模型测试仍要执行。

    3.6 夹具就两行,错误文本就一句

    内存映射只放 SKU-RICE-5KG 到「库存 12」。不要再放第三个 SKU 让断言变成「有库存」。未知编号统一走 IllegalStateException,消息固定「未找到商品」。映射用不可变 Map,测试之间不会互相改数量。若某条测试把 12 改成 0,下一条仍断言 12 就会红在夹具被污染,而不是红在 MCP。

    错误映射的单元测试可以直接调用映射函数,输入该异常,输出字符串等于 getMessage(),长度就是六个字。这条不启动服务器,用来防止以后有人拼接堆栈。启动服务器的那条仍走随机端口和客户端,证明这六个字能穿过 MCP 到达脚本模型。两条都要有。只有映射单测时,端点配错仍是绿的。只有端到端时,堆栈是在映射里拼上的还是在客户端拼上的,会看不出来。

    手工检查表给运维三行:进程端口 8081,健康检查 UP,listTools 含 query_stock。少一行都不算发布完成。客服侧的固定句「库存服务不可用」只在第三行失败时出现。第三行成功而用户仍听到这句,回到第 24 章看短路条件,不要在库存服务里再返回这句。

    3.7 随机端口上的三次断言

    @LocalServerPort 的值打印在用例名后面。客户端只连这个端口。listTools 之后立刻断言长度至少为 1,名字以 query_stock 结尾。长度为 0 时不要继续问模型。继续问会落到编造或短路,把「注解没扫到」显示成「模型没调用」。

    大米用例的 stockHits 从 0 变到 1,回答含「库存 12」。未知 SKU 再加 1,回答含「未找到商品」,全文不含换行后的 at 堆栈样式。错误映射单测不占端口,输出等于异常消息。端到端单测占随机端口,输出经过客户端和脚本模型。手工 8081 不在自动化里启动。开发者要看健康检查时,自己起进程,再执行三行检查表。自动化绿了而 8081 上是旧进程,以自动化的随机端口结果为准,然后重启手工进程。

    客服工程的组件扫描范围不包含 InventoryTools。扫到了,进程内会再出现一个 query_stock。断言客服上下文里的工具名没有这个字符串。库存数字只在库存进程的映射里。


    4. 项目总结

    4.1 优点与缺点

    优点缺点
    库存可以单独发布、单独扩容 多一个进程和端口
    @McpTool 让方法直接变成工具 扫不到 Bean 时列表是空的
    任何 MCP 客户端都能发现 query_stock 客户端端点必须和 /mcp 一致
    错误消息可以短到六个字 把堆栈写进返回值会进入模型提示词
    进程内 @Tool 仍适合订单查询 两套库存实现会给出两个数字

    和只在客服里写 @Tool 相比,服务能独立重启。和把库存 SQL 放进客服相比,客服不再持有库存表的账号。

    4.2 适用场景

  • 库存由独立团队发布。
  • 多个客户端都要查同一份 SKU。
  • 未知 SKU 要让模型转述固定原因。
  • 运维要用健康检查判断进程,用工具列表判断能力。
  • 订单查询仍留在客服进程,避免无谓的网络跳。
  • 不适合:一个方法只给一个进程用,而且必须和客服同一事务;没有人维护第二个进程。

    4.3 注意事项

    • 协议 STREAMABLE,端点 /mcp,手工端口 8081。
    • stdio 保持 false。
    • 返回给模型的错误只用「未找到商品」。
    • actuator 只在内网。
    • 测试用随机端口,避免打到旧进程。

    4.4 常见踩坑

  • 客户端工具列表为空。 根因是路径 /sse 或方法不是 Bean。处理:URL 带 /mcp,类加 @Service。
  • 用户看到一屏堆栈。 根因是错误文本拼接了 printStackTrace。处理:只送 getMessage()。
  • 测试绿了,手工 8081 仍是旧库存。 根因是用例连的是随机端口,手工进程没重启。处理:改完注解后重启 8081,再 curl 健康检查和列工具。
  • 4.5 思考题

  • 工具调用成功和失败,只看最终回答文本会漏掉什么?指标上怎样把「未找到商品」和「库存 12」分开?
  • 未知 SKU 的错误文本进了模型之后,若有人把堆栈也送进工具结果,prompt token 里会多出哪一类内容?本章要求送进模型的只有哪六个字?
  • 答案在第 26 章开头。

    延伸阅读与资源

    10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用 后端工程师转型AI第一课-Ollama 与私有化大模型实战 大型语言模型(LLM) vLLM 高性能推理落地实战 Agent开发之LlamaIndex 实战修炼与源码进阶 大语言模型Transformers 实战修炼与源码剖析

    赞(0)
    未经允许不得转载:171主机测评 » 第25章:Spring AI 2.0 MCP Server——把能力暴露成工具
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址