先说结论Spring AI MCP 这套组合是我最近半年用下来觉得最“稳”的 Java 侧 Agent 落地方式。它解决的核心问题不是“把模型接进来”而是“怎么让模型安全、可控、可扩展地调用真实业务能力”。如果你正在 Java 技术栈里做 AI Agent、智能助手、流程自动化这类项目这篇内容值得你从头到尾看一遍。MCPModel Context Protocol本身是对外开放的协议标准Spring AI 把它原生接进了 Spring Boot 生态意味着你不需要自己解析 function calling、不需要自己维护工具注册表、更不需要为每个模型厂商单独写一套适配层。配合 Spring Boot 的自动配置和 Bean 体系基本可以做到“加一个工具加一个类”这对想要构建标准化、可扩展 Agent 系统的团队来说价值非常直接。这篇我会从架构设计、环境配置、核心代码、踩坑记录四个层面展开全程以真实项目“某内部智能助手”为例。项目不大但麻雀虽小五脏俱全模型接入、工具调用、上下文管理、对话记忆、问题排查都覆盖到了。1. 整体设计与思路拆解1.1 Agent 落地难难的根本不是“大模型”很多团队做 Agent 的第一个误区是把大模型当成核心工作量。真做下来你会发现模型能力再强没有一套规矩的工具调用机制Agent 就是个“能聊天但啥也干不了”的花瓶。实际项目里Agent 要让模型去做事比如查库存、发审批、查工单、算价格必须打通三层模型层决定“什么时候该调用哪个工具”工具层把业务系统的能力暴露给模型可插拔、可复用协调层管理上下文、控制对话历史、记录工具调用结果如果这三层全用自家代码硬撸第一个项目能跑第二个项目就得返工。工具一多模型经常“不知道该调哪个”换了模型供应商原来的 function calling 格式全部作废再想支持第三方工具又得写一套新连接器。这套路我太熟了痛过才知道要提前用标准协议。1.2 MCP 像什么像 AI 世界的万能插头MCP 这个协议核心思路一句话把工具、数据资源、提示词都定义成标准化的“能力端点”模型客户端通过统一协议发现和调用它们。用生活类比最直观你的手机需要一个充电头但市面上的充电协议五花八门。MCP 就是那个“USB-C 接口”——从此以后企业系统不用关心接的是哪家模型模型也不用关心你内部是什么技术栈。只要双方实现了 MCP 协议就能直接协作。协议里最常用的三种能力能力类型作用典型场景Tools可执行的工具模型主动调用查天气、查库存、发送审批Resources可读取的数据资源读取内部知识库、读取配置信息Prompts可复用的提示词模板团队按统一规范复用提示词这套抽象对 Agent 系统的最大意义是工具层和模型层彻底解耦。协议在中间业务系统只管实现服务端模型接入只管做客户端两边互不绑架。1.3 Spring AI 是 Java 生态里最舒服的“接线层”Java 做 AI 应用很早以前是很别扭的。要么调 SDK 裸写要么包一层 HTTP 请求自己封装日志、重试、注入全都要手动处理。Spring AI 出现后最大的贡献是把“模型交互”纳入了 Spring 的编程模型。用过 Spring Boot 的人上手 Spring AI 几乎没有成本Starter 依赖一键引入自动配置帮你把 ChatModel、ChatClient 这些东西全部准备好配置走 application.yml换模型厂商基本只改配置不改代码Tool注解把业务方法和模型工具直接绑定再加上 Spring Boot 对 MCP 的原生支持这一套组合下来构建 Agent 的体验甚至比很多脚本语言框架还要顺畅。2. 架构与标准化设计2.1 分层架构接入层、协调层、工具层我在“某内部智能助手”项目里用到的分层结构经过几轮优化后基本定型成这个形式你可以直接参考接入层负责和模型厂商通信统一封装 ChatModel 和 ChatClient。业务代码不直接碰模型 SDK。协调层Agent 核心逻辑负责工具发现、调用决策、上下文管理、对话记忆。这是整个系统的“大脑”。工具层所有业务能力都以 MCP 工具形式暴露。工具内部再调用底层业务服务或第三方接口。这个分层最大的好处是职责单一。协调层不知道“查库存”具体怎么实现它只知道有一个工具叫query_stock参数是商品 ID返回是库存数据。工具层也不知道模型是什么品牌它只负责把自己的能力注册成标准协议端点。2.2 为什么不用裸 function calling可能有人问我用 OpenAI 的 function calling 或者各家 SDK 的原生工具调用不是挺好吗为什么非要套 MCP直说我的观点小 Demo 用原生 function calling 没问题但生产级 Agent 系统有几个绕不开的坎第一供应商绑定。原生 function calling 的参数结构、返回格式、错误处理都是跟着厂商走的。今天用甲厂商明天换乙厂商代码审计一遍。MCP 是中立协议两边都适配换料不换锅。第二工具治理是硬需求。工具数量超过十个就要面临工具清单从哪里来、权限怎么控、调用日志怎么记、哪个工具耗时过高怎么监控。MCP 把工具生命周期管理收敛到统一层面这套治理逻辑才有地方放。第三生态互通。MCP 出来以后行业里已经有不少现成的 Server 实现数据库、文件系统、开发工具、常用办公套件都能找到协议实现。这意味着你花一次学习成本后续可以接入大量现成能力。当然MCP 也不是银弹。它的学习曲线不算陡但如果你只是临时写一个脚本调一下模型确实没必要上 MCP。什么时候该用当你确定要把它做成一个长期维护、不断加功能的系统时就用。2.3 可扩展性到底从哪里来“可扩展”这个词很容易被说成口号。在我看来可扩展性要落地到三个具体的地方协议扩展新能力通过 MCP Server 暴露Agent 端零代码接入。你给我一个 MCP Server 地址我就能把新工具接到现有系统里。配置扩展模型切换、工具连接、系统提示词都通过配置驱动不写死在代码里。环境隔离、灰度发布、多租户差异化配置都能轻松支持。代码扩展用 Spring 的 Bean 机制管理工具注册新增工具就是新增一个带有Tool方法的Component。依赖注入自动把业务服务送进来工具类内部代码依然遵循 Spring 最佳实践。这个设计带来的感觉是Agent 系统的增长方式从“改代码”变成了“挂载能力”。两者完全不是一回事。3. 环境准备与核心配置3.1 依赖引入能用 Starter 就别手撸项目基于 Spring Boot 3.x构建工具用的 Maven。Spring AI 相关的核心依赖就三个dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId version1.0.0/version /dependency依赖版本这个事多说一句Spring AI 迭代速度很快1.0 之前的 API 变动相当频繁。建议以你实际引入时的最新稳定版本为准尽量不要用 SNAPSHOT 版本跑生产。踩过这种坑的人应该懂我为什么特别强调这一句。Starter 依赖的好处是把底层 SDK、自动配置、默认参数全都打包进来了。你不需要自己构建 MCP Client 实例也不需要手动配置模型提供商连接写个配置就好。3.2 模型接入配置我这里用的是 OpenAI 兼容接口便于切换。如果你的模型厂商支持 OpenAI 兼容协议配置几乎一模一样spring: ai: openai: base-url: https://api.example.com/v1 api-key: ${LLM_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2temperature: 0.2是我在 Agent 场景里比较偏好的值。Agent 需要执行任务输出要尽量稳定、可控、贴近指令不需要太多创造性。如果你做的是文案生成、创意类对话这个值可以再调高。曾经把一个工具调用类的 Agent 温度调到了 0.8结果模型经常“自由发挥”把参数名都改得乱七八糟查了好久才发现是温度惹的祸。3.3 MCP Server 端配置把业务能力变成协议端点我强烈建议你一开始就把工具定义为 MCP Server 模式而不是仅仅依赖内存里的 ToolCallback。原因很简单MCP Server 可以被外部复用。同一个工具既能服务当前 Agent未来也能被其他系统调用。先看配置spring: ai: mcp: server: enabled: true name: internal-assistant-server version: 1.0.0再看一个具体的工具类。项目里第一个接入的是“内部工单查询”能力Component public class TicketTools { private final TicketService ticketService; public TicketTools(TicketService ticketService) { this.ticketService ticketService; } Tool(description 根据工单ID查询内部工单详情返回工单状态、处理人、最后更新时间) public String queryTicketDetail(String ticketId) { TicketDO detail ticketService.queryById(ticketId); if (detail null) { return 工单不存在: ticketId; } return 工单[ ticketId ]状态 detail.getStatus() ,处理人 detail.getOwner() ,最后更新时间 detail.getGmtModified(); } }这里的关键点在Tool注解的description。很多人不重视这个字段觉得就是写给人看的注释。实际不是模型的工具选择决策完全依赖这个描述。描述写得模糊模型就不知道该在什么时候用这个工具。见过团队把描述写成“查询工单”模型就经常在用户询问“我的报销到哪一步了”时错误地调用它。把描述改成“根据工单ID查询内部工单详情返回工单状态、处理人、最后更新时间”之后误调用率显著下降。3.4 MCP Client 端配置Agent 如何发现工具Agent 端要做的就是配置 MCP Client 并连接 Server。我用的是 Stdio 模式也就是本地进程通信一个进程对应一个 Serverspring: ai: mcp: client: enabled: true name: internal-assistant-client version: 1.0.0 connections: - type: stdio name: internal-assistant-server command: java args: - -jar - /opt/mcp-servers/internal-assistant-server.jar这种模式适合和自家服务部署在一起的工具。如果你的工具跑在独立服务上MCP 还支持 HTTP 模式streamable HTTP配置一个 URL 就能远程调用。我当时先跑了 Stdio等工具 Server 多了以后准备切到 HTTP 统一管理但是单机演示和内部验证用 Stdio 更简单直接。MCP Client 启动后会自动发现 Server 暴露的工具清单这些工具会并入 Spring AI 的 ToolCallback 体系。也就是说Agent 调用一个 MCP 工具和调用一个本地函数对模型来说没有区别统一走协议封装。3.5 工具注册成功怎么验证有一个很直接的验证方法启动项目后看日志。Spring AI 的 MCP 自动配置启动时会打印工具列表。如果你配置正确控制台会列出类似以下内容Registered MCP tool: queryTicketDetail(String ticketId)看到这个输出说明 Server 端和 Client 端握手成功、工具已注册。这一步没通过后面 Agent 再怎么调都不会触发工具。4. 实战构建一个标准化、可扩展的 Agent 系统4.1 需求场景设定先交代一下项目背景某公司内部需要一个智能助手能帮助员工查询工单、查看公告、提交简单的审批请求。所有数据都来自内部系统不直接暴露数据库也不允许模型自由发挥去捏造数据。这个场景非常适合 Agent MCP。员工用自然语言提问Agent 决定调用哪个工具工具返回真实数据模型再组织语言回复。4.2 配置 ChatClient 与工具回调Spring AI 1.0 后的标准做法是用ChatClient作为交互入口。先创建配置类Configuration public class AgentConfig { Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是某公司内部智能助手。请根据用户问题判断是否需要调用工具。 如果工具返回的结果不足以回答问题明确告知用户不要编造数据。) .build(); } }注意这里我加了一个明确的系统提示词不要编造数据。模型有时候会为了“完成对话”而强行输出不存在的工单信息这在企业内部场景是不可接受的。系统提示词里写清楚底线比在业务代码里做各种校验都更治本。接下来在调用处把工具带有选择性地注入Service public class AssistantService { private final ChatClient chatClient; private final ListToolCallback toolCallbacks; public AssistantService(ChatClient chatClient, ListToolCallback toolCallbacks) { this.chatClient chatClient; this.toolCallbacks toolCallbacks; } public String ask(String userMessage) { return chatClient.prompt() .user(userMessage) .tools(toolCallbacks) .call() .content(); } }这里把ListToolCallback注入进来Spring 会自动收集所有 MCP 工具。想要控制某个 Agent 只能用某些工具就在构建tools(...)时按名称过滤非常灵活。4.3 Agent 是怎么一步步“思考”和调用工具的CallResponseSpec内部接入 Tool Calling 机制后整个调用流程是这样的用户输入“帮我查一下工单 TK20250201 到哪一步了”模型收到消息判断需要调用工具模型生成一个结构化的工具调用请求包含工具名queryTicketDetail和参数TK20250201Spring AI 框架本地执行queryTicketDetail方法拿到返回结果工具结果作为一条消息回传给模型模型基于工具结果生成最终回答“这个工单当前状态是处理中处理人是张三最后更新时间为今天上午 10:24”这个多轮工具调用循环是 Agent 的核心引擎。你不必自己写循环代码框架帮你做了。但你必须理解它这样才能在出现问题时快速定位是哪一环出了岔子。实战中我发现工具调用失败的常见原因往往不是框架有 Bug而是模型选择的参数不合法。比如用户说“查一下昨天的工单”模型如果把这个请求转换成queryTicketDetail(ticketId昨天的工单)那你得让工具在参数校验不过时返回一个友好的错误信息模型看到错误信息后会尝试修正自己的调用。这也是工具方法内部要做防御的原因。4.4 上下文管理对话记忆与 token 预算单轮问答很容易难的是多轮对话。Agent 必须记住用户之前说过什么。Spring AI 提供了ChatMemory和MessageChatMemoryAdvisor直接解决这个问题。先定义一个内存型记忆组件Bean ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); }然后在构建 ChatClient 时挂上记忆顾问Bean ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultSystem(你是某公司内部智能助手。请根据用户问题判断是否需要调用工具。 如果工具返回的结果不足以回答问题明确告知用户不要编造数据。) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); }.maxMessages(20)的意思是只保留最近 20 条消息。这个“20”不是随便写的。Agent 每轮对话不仅消耗 token还消耗模型的注意力资源。历史太长模型会“记不清”当前这轮问题的重点。我调过多个 Agent 项目内部总结的经验是短任务型 Agent 保持 10~20 条历史最佳长度超过 50 条不仅成本翻倍回答质量也会肉眼可见地下降。MessageWindowChatMemory 是滑动窗口策略实现简单、效果可控。再复杂一点的场景可以考虑摘要压缩把旧消息交给模型生成摘要再塞回上下文。但对多数内部助手来说滑动窗口足够了。4.5 系统提示词设计的几个心得系统提示词对 Agent 行为的影响远超很多人的认知。在我这个项目里经过多轮调优最终稳定下来的一段提示词核心要素是角色、边界、调用策略你是某公司内部智能助手。 - 你的信息来源只有内部系统工具不要编造数据。 - 用户问题涉及工单、公告、审批时先调用对应工具获取真实数据。 - 工具返回数据为空时如实告知用户不猜测结果。 - 回答保持简洁直接给出结论和关键信息。提示词里明确写“先调用对应工具”这一点很重要。模型默认情况下是“懒惰”的能直接回答的问题它不喜欢绕路调工具。除非系统提示词明确要求否则它常常宁愿“猜一个答案”。对于 Agent 应用这就是致命的。5. 常见问题与排查技巧实录5.1 工具一直在但模型就是不调用这是我被问过最多的问题没有之一。排查思路按顺序走第一步看注册日志确认工具确实在 ToolCallback 列表里。有些情况下工具类没有被 Spring 扫描到日志里根本没有这一行。第二步看description。描述模糊、或者没有提到任何触发关键词模型就不知道该用。把用户可能的口语表达写进去是最实用的做法。比如“查一下昨天的工单”“工单”这个词必须出现在描述里。第三步看用户的提问和工具参数是否匹配。模型只有在“当前上下文足够支撑参数填充”时才倾向调用。如果你问的是“我的工单到哪了”但工具要求传ticketId模型不知道 ID 是什么就会放弃调用。这种情况下可以在工具描述里补充说明“如果用户未提供工单ID引导用户提供”。第四步确认是否在同一轮内可以调用多个工具。部分模型对“单轮多工具调用”的支持不好会一次只调一个然后停下来等结果。这不是框架问题是模型策略问题换个对工具调用支持更强的模型即可解决。5.2 工具返回的数据格式影响回答质量工具方法返回的是字符串用大段文本拼接模型确实也能读但不如结构化格式好。建议工具返回值尽量用 JSON 字符串形式。比如天气工具返回{city:北京,temperature:5,condition:晴}模型直接从 JSON 提取关键信息组织自然语言回答比拼接的文本要准确得多。我自己的工具方法统一用一个简单的格式如果数据正常返回 JSON 字符串如果异常返回{error:查询失败: ...}。模型能识别 error 字段在最终回答里会主动传达失败原因。这对用户体验是很大的提升。5.3 上下文越来越长成本飙升滑动窗口能限制“对话历史”长度但工具调用产生的 token 一样要花钱。如果你的 Agent 频繁调用工具一次工具调用循环可能消耗几百 token多轮就是几千。经验做法是工具结果尽量精简只回传关键字段。不需要把整个数据库记录都塞给模型。对工具返回结果做截断超过 500 字符的只保留前半部分和后半部分中间用省略号代替。对话之间及时清理无用的历史比如用户已经完成了一次咨询就不需要把上一轮的完整工具调用过程一直留在上下文里。5.4 Spring AI 自动配置冲突这个坑比较隐蔽。Maven 里同时引入了多个 Spring AI 模块时偶尔会出现自动配置冲突表现是 MCP Client 连接失败、工具注册不生效。排查手段检查依赖树mvn dependency:tree查看是否有重复版本的 Spring AI 模块检查META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里的自动配置类是否被手动排除确认没有引入两个不同版本号的spring-ai-core由于 Spring AI 是较新的框架社区资料不像老牌框架那么多。遇到诡异问题第一反应应该是“查版本、查依赖树”而不是怀疑代码写错了。5.5 问题排查速查表现象可能原因解决方向日志无工具注册Server/Client 配置未生效检查 MCP 自动配置、依赖引入模型不调用工具description 不明确重写描述加入触发同义词调用工具但参数错误上下文缺关键信息引导用户补全参数或工具内做容错回答质量波动大temperature 过高Agent 任务调到 0.2 以下老问题经常复现上下文被污染检查 ChatMemory 窗口设置成本上涨明显工具结果太长截断返回 JSON只保留关键字段6. 扩展方向与实用建议6.1 把外部公共工具接进来内测稳定以后我们开始接更多现成的 MCP Server。GitHub 上能找到不少开源实现比如数据库查询、时间日期、文件读取、HTTP 请求等通用工具。接任何一个工具本质上都是加一个依赖、加一段连接配置Agent 核心代码不需要改动。这就是 MCP 标准的实际红利。当然接外部工具要注意权限问题。MCP Server 暴露的是真实能力权限控制必须做在 Server 端。不要让一个“查公网 IP”的工具在有内网权限的 Agent 里随意使用。6.2 多 Agent 拆分的思路工具数量到了 30 以后单 Agent 的决策压力会变大。模型面对三十个工具的时候调用准确率明显低于面对五个工具。此时可以考虑纵向拆分一个“路由 Agent”先判断用户意图属于哪个领域再把请求转发给对应领域的子 Agent。子 Agent 只挂自己领域的少量工具。这个架构能显著提升工具调用的准确率。Spring AI 目前提供了 ChatClient 级别的工具过滤机制你可以轻松实现“按 Agent 过滤工具”的逻辑。我用下来觉得判断的临界点大概在 20 个工具附近。工具少于这个数单 Agent 更简单超过这个数拆多 Agent 收益更大。6.3 别忽略可观测性Agent 系统最让人头疼的就是“黑盒”。用户问一个问题触发了几次工具调用每次结果是什么最终回答基于哪些数据——这些过程全透明地记录下来才方便复盘和排查。建议至少记录三类日志用户原始输入和最终回答每一次工具调用的工具名、入参、出参、耗时模型返回的中间推理内容如果厂商 API 支持的话Spring AI 有 Advisor 机制可以在调用链路上做切面和日志。我建议从第一天就加上这个能力不要等出了问题再补。等到上线后再补日志你连当时发生了什么都不知道。6.4 最后一个小技巧没有兜底规则Agent 系统永远不够格上生产。我经历过一次印象特别深的 incident。当时用户问了一个不在任何工具能力范围内的问题模型牛头不对马嘴地调用了一个工具把耗时的接口命中了一次结果返回的数据完全无关。用户看到的回答是“已为您处理”实际上什么都没发生。从那以后我在系统提示词和工具返回两个层面都做了兜底一是明确要求“没有合适工具时如实告诉用户目前无法处理”二是工具内对无效请求直接返回{error:参数异常}。两层兜底叠起来再也没有出现过“假装处理成功”的尴尬场面。这个经验是那些漂亮架构图里不会写出来的东西但每一套能稳定运行的生产级 Agent 系统背后都离不开这种对失败场景的执着。做 Agent 不是做 demo能稳住的系统才是真功夫。
阅读完成 · 觉得有帮助?