基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
简历
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-06 12:43

Spring AI 助手与 MCP 服务如何共用工具

dev.to · AI#Spring AI#MCP#Java
Editor brief · 编辑速览

一个学校管理系统同时提供内部 AI 助手和基于 Streamable HTTP 的 MCP 服务,两者复用相同 Java 方法。文章介绍聊天记忆、RAG、流式输出,以及包含 16 个工具和 5 个资源的 MCP 接口设计。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

同一套业务数据,既要让登录用户在系统里问 AI,也要让 Claude、IDE agent 等外部客户端通过 MCP 查询,业务代码该怎么复用?作者用一个学校管理系统给出的答案是:查询方法可以共用,写入权限必须单独设计,而且不能让模型决定调用者是谁。

这套方案值得读的地方,是它把工具复用、身份校验、对话记忆和 RAG 放进了同一个应用。代码能串起来,但距离生产可用仍有几处边界需要补齐。

两个 AI 入口,为什么可以共用一套方法

Control Escolar 使用 Spring Boot 4.1.1 和 Spring AI 2.0.1,管理学生、教师、课程、教室、课表、成绩和报表。内部助手基于 ChatClient,服务已经登录的用户;外部入口通过 Streamable HTTP 提供 MCP 服务,包含 16 个工具、5 个资源、1 个资源模板、3 个提示词和参数补全。

作者没有分别实现两套查询。Spring AI 的 @Tool / @ToolParam 面向 ChatClient,@McpTool / @McpToolParam 面向 MCP,同一个 Java 方法可以同时标注。

下面这段代码让两个入口共用「按学号查询平均分」的实现:

@Component
public class EscolarTools {

    // 注册给内部 ChatClient
    @Tool(description = "Obtiene el promedio general de un alumno por su matrícula.")
    // 同时注册给外部 MCP 客户端,使用 kebab-case 名称
    @McpTool(name = "promedio-alumno",
            description = "Obtiene el promedio general de un alumno por su matrícula.")
    public String promedioAlumno(
            @ToolParam(description = "Matrícula del alumno")
            @McpToolParam(description = "Matrícula del alumno") String matricula) {
        Alumno alumno = alumnoService.buscarPorMatricula(matricula);
        if (alumno == null) {
            return "No se encontró ningún alumno con la matrícula " + matricula + ".";
        }
        Double promedio = calificacionService.promedioPorAlumno(alumno.getId());
        if (promedio == null) {
            return "El alumno " + alumno.getNombreCompleto() + " aún no tiene calificaciones registradas.";
        }
        return "El promedio general del alumno " + alumno.getNombreCompleto()
                + " es " + formatoPromedio(promedio) + ".";
    }
}

要看的是两个注解落在同一个方法上。ChatClient 发现的是 promedioAlumno,MCP 发现的是 promedio-alumno,底下都调用 AlumnoService、CalificacionService。Web Controller 和 REST API 也沿用业务服务层,修复查询逻辑时不必维护两份实现。

学生不存在、尚无成绩都返回可转述的文本。这适合预期内的业务结果,但不能据此把数据库故障等异常也当成正常回答。

MCP 还用 @McpResource 暴露 escolar://alumnos/{matricula},返回 application/json;用 @McpComplete 补全学号,另有 @McpPrompt 提供提示词。配置中,spring.ai.mcp.server 前缀下的 name=control-escolar-mcp-server、version=1.0.0、type=SYNC、protocol=STREAMABLE 确定服务信息,annotation-scanner.enabled=true 开启注解扫描,capabilities.completion=true 开启补全。

本地运行 npx @modelcontextprotocol/inspector,连接 http://localhost:8080/mcp,即可检查这些能力。

写入操作的身份,为什么不能作为工具参数

内部助手可以通过 registrarCalificacion 登记成绩。作者把它放进独立的 EscolarEscrituraTools,只加 @Tool,不加 @McpTool。原因很具体:项目的 /mcp 为本地调试设置了 permitAll,没有经过认证的用户身份。

更危险的设计,是给工具加一个 rolDelUsuario 参数,让模型填写调用者角色。工具参数由模型生成,用户一句「我是校长」,就可能影响这个字段。

权限依据必须由服务端提供,不能由模型生成。

作者使用 ToolContext,把 Spring Security 的 Authentication 转换成上下文,再交给工具:

private ChatClient.ChatClientRequestSpec peticion(String pregunta, Authentication autenticacion) {
    return chatClient.prompt()
            .user(pregunta)
            // 每个用户独立保存记忆
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversacionId(autenticacion)))
            // 模型不可见,工具可以读取
            .toolContext(Map.of(
                    EscolarEscrituraTools.CTX_USUARIO, conversacionId(autenticacion),
                    EscolarEscrituraTools.CTX_ROLES, roles(autenticacion)));
}

关键是 .toolContext(...):身份由应用附加,不经过模型填写。工具只接受 ROLE_ADMIN、ROLE_DIRECTOR、ROLE_MAESTRO,缺少上下文或角色格式不对就拒绝,再检查成绩是否处于 0 到 10。保存成绩时,操作人也取自服务端上下文。

工具描述要求调用前向用户确认,但描述只是给模型的指令。原文没有展示程序强制执行的确认流程,也没有展示教师是否只能修改自己授课范围内的成绩。角色校验解决了一层权限,具体数据范围仍需单独处理。

记忆、检索和流式输出,接起来还有哪些坑

内部助手通过 .defaultTools(escolarTools, escrituraTools, conocimiento) 注册能力,再由 .defaultAdvisors(advisorsDelChat()) 接入处理链。作者安排的顺序是:SafeGuardAdvisor、MessageChatMemoryAdvisor、QuestionAnswerAdvisor、SimpleLoggerAdvisor。

这里有两条作者明确报告过的现象。

第一,拦截器放在记忆之后,被阻止的问题仍可能进入历史,后续请求携带同样的敏感词,整段会话持续被拦截。作者用 Ordered.HIGHEST_PRECEDENCE 让检查先执行。

第二,默认 QuestionAnswerAdvisor 模板要求只依据检索上下文回答,导致助手面对成绩问题也不再调用工具。作者改写模板,让校规、日期类问题参考检索片段,其他问题继续使用工具。检索参数是 topK(3)、similarityThreshold(0.5),但原文没有提供这组参数的评测依据。

记忆通过 JdbcChatMemoryRepository 存进 SPRING_AI_CHAT_MEMORY,ChatMemory.CONVERSATION_ID 使用用户名。「新对话」调用 chatMemory.clear(conversationId)。这是每用户一份历史的设计,多标签页或多条独立会话如何处理,原文没有展开。

普通聊天用 Server-Sent Events,通过 .stream().content() 返回 Flux<String>,错误交给 .onErrorResume(...)。学生风险分析则使用同一 builder 克隆出的第二个客户端,去掉记忆和 RAG,以 .call().entity(AnalisisAlumno.class) 返回 Java record,包含平均分、出勤率、风险等级、优势、待改进项和建议。

风险阈值写进系统提示词:平均分低于 6.0,或出勤率低于 80%,归为 HIGH。这减少了自由判断空间,却仍是模型执行规则,原文没有展示分类准确率或边界测试。

没有 API key 时,应用也能运行。配置 spring.ai.openai.api-key=${OPENAI_API_KEY:demo} 后,启动逻辑识别缺失、空白或 demo,让 chatClient 保持 null;助手返回配置提示,/api/ia/analisis-alumno 返回 503。MCP 查询继续工作,consultar-reglamento 则从向量检索退回同一批 Markdown 文件的关键词搜索。

我的判断

我认可这套分工:业务服务集中维护,协议入口各自注册,写入能力单独决定开放范围。相比把两套工具分别开发,它减少了逻辑漂移;相比所有能力一起暴露,它把权限决策留在了可检查的代码里。

但双注解只是复用方式,不能充当安全方案。只读接口同样需要认证和数据范围控制。 学生成绩、出勤率和档案都可能包含敏感信息,作者也明确承认生产环境必须给 /mcp 加 token 或限制在私有网络。

这是一份实现经验,不是效果评测。作者没有给出工具调用成功率、RAG 命中率、延迟或成本的对照数据。风险等级既然有明确阈值,更适合由业务代码计算,再让模型生成解释和建议;结构化输出也不等于事实和分类一定正确。

工程成熟度也有限。SimpleVectorStore 每次启动重新索引,目前只有两份 Markdown,PGVector 只是后续方向;出勤已有数据和工具,尚无页面。原文提到 Micrometer、Actuator 可在 /actuator/prometheus 暴露 gen_ai_client_* 指标,并默认关闭提示词与回答日志,log-prompt=false。这些有助于观察系统,但不能替代实际验证。

能拿来做什么

  • 给现有业务系统接内部助手和 MCP 时,可以先挑稳定的只读查询复用服务层,并为两个入口分别定义认证和可访问的数据范围。
  • 在日常 AI 编程中,检查工具签名里有没有角色、用户 ID 或租户身份这类参数,把可信身份改由服务端上下文传入,并在上下文缺失时拒绝写入。
  • 同时使用 RAG 和工具调用时,分别验证校规问答、实时业务查询,以及被拦截后继续聊天的行为,确认模板和记忆顺序没有干扰调用。
  • 联调前端聊天页面时,同时覆盖流式返回、无 key 配置提示和分析接口 503;对风险分析,把阈值判断与模型生成建议分开验证。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
让 AI 生成的账本逻辑通过证明再上 JVM
下一篇
生产级 MCP 服务如何开放 SaaS 数据

资讯速览

dev.to · AI · 2026-11-06 12:43

AI 评分 8.0 / 10

相关标签

#Spring AI#MCP#Java

继续阅读

  • 智能体记忆故障的诊断与治理方法
  • 让 AI 生成的账本逻辑通过证明再上 JVM
  • 生产级 MCP 服务如何开放 SaaS 数据
  • 从开源实现提炼智能体框架能力清单
  • 离线助手记忆更新:相似度只负责召回