提方案的是模型,按按钮的得是人

让模型学会调工具,其实不算难。

难的是它接进真实业务系统以后。

设备编号传错一位,被改的可能是隔壁那台泵。

眼前这个用户没有重启权限,模型照样会去调那个工具。

它也分不清「重启」和「立刻重启」的区别。

重试跑岔了,同一条指令会执行两遍。

这几个坑都跟模型聪明不聪明没关系。

它们全出在调用链的边界上。

模型只负责提出调用。

执行与否,始终由应用说了算。

这篇不堆名词,直接搭一条最小但能跑通的工具调用链。

服务端把参数再校验一遍。

高风险动作只生成待确认的方案。

最后那一下,由登录过的人亲手点。

人工确认按钮示意图

先说一条底线。

MCP 只管工具怎么被发现,以及怎么被调用。

它不替你做身份认证。

也不替你做权限判断,更不会帮你排人工审批。

Spring AI 的 MCP 服务端,默认同样不带鉴权。

这条线得画在自己手里,或者画在网关那一层。

画漏了,前面全白做。

建议和执行,得走两条路

示例基线摆在这儿:Java 17,Spring Boot 4.0,Spring AI 2.0.1。

模型接口和 MCP 服务端分开配。

拆成两个应用,边界才看得清。

一个叫 device-mcp-server,跑在 8081,只暴露两件事。

查设备状态,预览重启方案。

另一个叫 agent-api,跑在 8080。

它接对话,连 MCP,另外留一个确认接口。

用户在AI对话窗口里说一句「帮我重启 pump-017」,落到这里。

这套骨架其实不难写,AI编程工具现在能生成大半。

难的是每一层该拦什么。

两个模块共用一份 BOM,版本别各写各的。

Spring AI 的模块版本一旦岔开,工具注册会莫名其妙失效。

父工程统一设 Java 17,两个应用都从它继承。

依赖也不多,别堆。

服务端只要两个 starter,一个 webmvc,一个 MCP 服务端。

客户端那边要三个,安全,模型,还有 MCP 客户端。

少一个,启动就会在自动装配那一层报错。

为什么非要拆两个应用。

因为 MCP 服务端是独立的一块能力,谁都能来接。

拆开以后,能连它的只有内网里的几个服务身份。

对外暴露的用户入口只剩一个对话接口。

面收窄了,出事时能查的路径也短。

合并成一个应用当然跑得更快,代价是所有东西挤在一个进程里。

返回结构也要收窄。

查状态就回设备编号,运行状态,最后心跳时间,外加一个温度。

方案就回 actionId,设备编号,原因,影响范围,和一句是否需要人工确认。

别顺手把设备的整条记录返回给模型。

它用不上那么多字段,多给的每一样都可能被写进回答里。

读状态是只读工具,调了也就调了。

重启走的是另一条路。

工具只负责生成方案,它本身不碰设备。

方案里带一个 actionId,还有影响范围和风险提示。

前端把方案摊开给人看。

确认接口从登录态里取操作者。

这一点很关键,后面还要说。

人点了确认,服务才真正动设备。

工具声明里必须把边界写清楚。

名字写清,用途写清,入参写清,会不会产生副作用也写清。

因为它一半给模型看,一半给维护的人看。

描述里那句「不会修改设备」不是修辞。

它是给下一个接手的人看的契约。

参数上的约束也要写进声明。

设备编号限三到四十位,只允许字母和数字还有下划线。

重启原因限一到两百字。

这些约束会变成给模型看的参数 Schema。

模型照着填,填错的概率低很多。

readOnlyHint 这类标记,只是发给客户端的提示。

它不是访问控制机制。

真正的边界在服务端代码和授权策略里。

演示阶段用内存 Map 存待审批记录就够了。

正式环境得落库。

过期时间要有,租户字段要有,审计信息也要有。

否则进程一重启,待审批的方案全没了。

配置里还有一处容易忽略。

服务端和客户端的协议都设成 Streamable HTTP。

本地演示绑 localhost 就行。

一旦部署到能连通的网络里,地址要换,认证也要补上。

对话接口本身只做一件事,把用户那句话转交给模型。

判断别写在 controller 里。

请求体里也只放一句 message。

userId 这种字段压根不该出现在这个请求体里。

系统提示词里也别忘了写一句。

重启请求只生成方案。

不得声称设备已经重启。

不得把工具参数当成用户权限的证明。

这三句省掉,模型很容易在回答里抢跑。

参数在服务端再校验一遍

工具的入参格式,Schema 已经约束过了。

但 Schema 只帮模型把参数写对。

它管不住谁在调。

请求可能压根不是模型发出来的。

也可能是模型被提示注入以后发出来的。

把AI工具直接连到生产库上,就是最常见的冒进。

所以服务端那一遍不能省。

字段在不在,长度和格式对不对,先过一遍。

设备是不是真的存在,是不是属于当前租户,再过一遍。

时间和金额这类边界,也要卡住。

查询要给最大返回量,还要给超时。

文本输入尤其要看。

它会不会被拼进 SQL 语句,会不会被拼进 Shell 命令,会不会被拼进 URL。

拼 SQL 一律走参数化。

调外部设备接口,用明确的设备编号映射,再配一个超时。

别指望在提示词里写一句「不能越权」。

也别觉得模型给的 JSON 长得对,就能跳过业务校验。

这两件事都试过,代价都不小。

工具里抛异常也有讲究。

参数不合法就抛参数异常。

设备不存在就抛不存在。

异常信息别把内部字段全吐出来,给个能定位的短句就够。

模型会把这些话当线索去补参数。

补不上的时候,它应该学会停下来问人。

还有一层常被忽略,工具不止模型会调。

团队里的脚本也能直接打这个 MCP 端点。

所以端点上要限来源。

内网地址是一层,服务身份是另一层,白名单是第三层。

只靠模型自觉,等于没设防。

身份从登录态来,不从模型来

用户发来的文字,工具参数,模型生成的 JSON,全是不可信输入。

权限判断只能发生在服务端。

依据是认证过的当事人,加上资源归属关系。

审批接口上加一个权限点。

只有拿到设备重启审批权限的人,才进得来。

还得把方法级安全的开关打开。

只配一个权限点,方法级安全没开,注解等于装饰。

审批服务还要确认四件事。

这个 action 属于当前租户。

状态还是待确认。

没有过期。

目标设备归这个人管。

任何一项不成立,直接拒掉。

独立部署的 MCP 服务,还有个容易踩的地方。

MCP 请求带的那个服务凭据,不等于最终用户。

要么让可信的 API 层先做完用户级授权。

要么用一份有明确受众和有效期的用户委托凭证。

最省事的错法,是把 userId 塞进工具参数让模型传过来。

这条路不能走。

模型转述一个别人的编号,太容易了。

服务身份和用户身份这两层,最好在链路图上分开画。

画在一起,出事的时候查不清是谁批的。

确认和幂等是一件事

高风险工具,最好拆成准备和执行两段。

第一段是准备。

模型调准备工具,回来的是 actionId,还带设备名和原因。

第二段是人确认。

前端把卡片推给用户,用户拿自己的登录凭证点确认。

服务端这时候才校验权限,状态,有效期和一次性幂等键。

都过了,才去执行设备动作。

顺手记下操作人,时间,结果,还有关联的请求号。

有一种设计看着很像,其实是错的。

让模型先调 restart_device(),再在回答里问一句要不要确认。

副作用已经发生了。

这时候的确认只剩文字。

审批记录别只放单机内存。

落到数据库里,做成状态机。

待确认往前走一步,是已执行。

也能走到已过期。

执行失败就停在失败态,按策略决定要不要人工重试。

状态更新用条件更新,别用先查后改。

把状态从待确认改成审批中,条件里带上原始状态和有效期。

只有更新行数为 1,才继续往下走。

这就是幂等的第一道锁。

两个人同时点确认,只会有一个人的更新成功。

另一个人拿到的是零行,直接告诉他这单已经被处理了。

确认成功后返回一张回执。

回执里写清谁批的,什么时候执行的,结果是什么。

这张回执就是事后追责的凭据。

和设备网关打交道时,把 actionId 当幂等键传下去。

网络超时以后客户端重试,也不会把设备重启两遍。

要更稳一点,可以在同一个数据库事务里写一条 Outbox。

再由一个工作进程去执行命令,记成功或失败。

事务提交了但设备没动,这种事不能假装它成功。

反过来,设备动了但事务没提交,也得有对账的出口。

这两头的账,靠的就是 actionId 这一条线串起来。

本地先跑通再说。

开两个终端。

一个起 device-mcp-server,一个起 agent-api。

模型密钥放环境变量,别提交到 Git。

第一个请求问「pump-017 现在的状态和温度」。

模型会挑只读工具,拿回结果再组织回答。

那一轮的工具调用在日志里应该看得到工具名和耗时。

第二个请求说「温度异常,帮我重启它」。

正常的结果是回来一份方案,加一句需要人工确认。

它不该去碰设备控制接口。

这一步是整套设计的分水岭。

返回方案,说明边界立住了。

返回「已重启」,说明工具写错了。

真正确认得有人带审批权限登录,再调确认接口。

拿同一个 actionId 再点一次,第二次应该被拦下。

这条重复确认的路,最好自己先走一遍。

上线前还要把观测补齐。

每次工具调用至少留这几样:traceId,会话 ID,工具名,调用人。

还要记参数校验结果,耗时,actionId,以及最后的状态。

日志里默认不要写 API Key,也不要写访问令牌,更不要存完整的敏感参数。

也不建议把整轮对话原文原样落库。

留摘要和工具调用记录,已经够复盘了。

脱敏别只脱一层。

参数里的手机号和地址,往往就藏在所谓业务字段里。

几条指标值得接进监控。

MCP 连接的成功率,工具数量和调用延迟。

参数被校验拦下来的次数,权限拒绝的次数,还有超时次数。

待审批有多少,过期多少,重复确认拦了多少。

设备控制的成功率,失败重试数,Outbox 有没有堆积。

这几项不需要做得多漂亮。

参数校验被拒的次数突然翻倍,往往不是模型变笨了。

更可能是有人在拿这套接口试边界。

重复确认被拦下的数字同理。

能在一张图上看见异常就够了。

出事那天,你会庆幸当初多打了这几个点。

公网能访问的 MCP 服务,先把认证和授权做完。

再做网络隔离,然后才上线。

只把当前这次请求真需要的工具交给模型。

读写分开,危险操作默认关掉,或者一律走审批。

工具描述写得再清楚,参数 Schema 定得再细,提示词写得再硬,都替代不了代码里的那一句权限判断。

模型可以负责理解意图。

也可以负责提出下一步。

但谁能做,做什么,什么时候真的执行,这三个决定权还在系统手里。

顺序不能反,先问再动。

反过来做,代价往往是一次事故。

这套划法不只适用于重启设备。

发邮件是这样,下单退款是这样,删数据更是这样。

只要动作有副作用,就先出方案再等人点头。

链路这么划开,Agent 才敢从演示走到业务里去。