资讯动态

Spring AI + MCP 实战:构建实时基金估值助手与全链路观测

发布时间:2026/10/14 20:17:58 来源:尧图企业网站定制
如果你也做指数基金定投大概率有过这样的体验想判断今天某个指数是贵还是便宜得先打开行情软件翻到估值页面再对比历史百分位。要是盘中着急看这一套操作相当折腾。所以我做了一个小工具用 Spring AI 搭一个能对话的基金估值助手再通过 MCPModel Context Protocol把实时估值数据接进来让大模型直接回答“沪深300现在贵不贵”“中证500的PE百分位是多少”。这个项目跑通之后我又把从用户提问、工具调用、数据返回再到模型回复的全链路观测一起做了文章后面会有很大一部分在讲这件事。如果你正在研究 Spring AI 怎么接 MCP或者想给自己的 AI 应用加一层可观测能力这篇实战记录应该能给你省不少时间。1. 为什么估值数据必须走 MCP而不是让大模型硬记1.1 大模型再大也背不住今天的沪深300市盈率刚开始做这个项目时我第一反应是直接把指数估值写进 system prompt让模型记住“沪深300当前PE是XX倍”。这个方案被我很快毙掉了原因很实际估值数据是典型的高频变化数据今天和明天可能差不少盘中甚至每分钟都在动。大模型的训练数据有截止时间它无法知道“此刻”的估值是多少硬塞进 prompt 的结果就是它一本正经地胡编一个数字。你可能会说那让 AI 联网搜一下不就行了实测效果也很一般。搜索引擎拿回来的是一堆页面摘要模型要从里面猜哪个数是“PE”哪个是“PB”还得自己判断数据新不新鲜错误率很高。而且搜索接口不稳定有时候模型会为了凑答案编出一个“看起来合理”的数。对于基金定投这种对数据准确性有要求的场景这种回答是不能用的。所以我的结论是估值数字必须来自一个程序化的、可验证的数据源而不是靠模型“记住”或者“猜”。这正好是 MCP 擅长的领域。1.2 MCP到底解决了什么问题把“查数据”变成标准动作MCP 的全称是 Model Context Protocol翻译过来叫模型上下文协议。它做的事情一句话总结把外部数据源和工具统一成一种“插头”格式AI 按需发现、按需调用。我习惯用一个类比来解释它MCP 就是 AI 应用里的 USB-C 接口。以前要给 AI 接一个数据源你得单独写一套插件或者 API 对接换一个客户端就废了现在只要数据源实现了 MCP Server任何支持 MCP 的客户端都能连上。对开发者来说好处有两个。第一工具的定义是标准化的大模型能看到工具名称、参数说明、返回值格式第二数据源的接入逻辑和 AI 应用逻辑解耦了我可以单独维护估值数据服务再让多个应用共用。在 Spring AI 里这个好处体现得很直接。它提供了现成的 MCP 客户端支持可以自动拉取远程 MCP Server 的工具列表并把它们注册成大模型可调用的 function calling 工具。我不用自己拼 JSON Schema也不用自己实现工具调用的循环Spring AI 把这一层的样板代码都消化掉了。2. 落地第一步把指数估值数据封装成MCP Server工具2.1 数据源接入与百分位计算工具接到 MCP 之前先得有数据。我调研了一圈行情数据服务最后选用了一个带鉴权的 HTTP 接口返回的是 JSON 格式的指数估值快照。这个接口可以按指数代码查询返回字段包括当前 PE、PB、股息率、指数名称以及更新时间和数据质量等级。这里有个技术问题必须在服务端解决估值百分位。单纯把 PE 拿给大模型意义不大因为不同行业的 PE 中枢不一样沪深300的 PE 和创业板指的 PE 不能直接比。业界通用的方法是算历史百分位把某个指数过去8年的每日 PE 收集起来找出当前 PE 落在历史区间的哪个位置。低于30%属于低估区高于70%属于高估区中间是正常区间。计算逻辑并不复杂核心步骤是这三段拉取历史估值序列按天排序剔除停牌或者数据异常的日期。取出今天的 PE统计历史序列中“小于等于今天 PE”的天数。用这个天数除以总的有效天数乘以100得到百分位。2.2 工具参数与返回结构设计MCP 工具的核心是“让大模型看得懂、调得对”。我把工具设计成一个同步查询接口工具名称叫get_index_fund_valuation入参只有一个code用来接收指数代码。为了让模型不传错参数我在工具描述里明确注明了支持的指数代码列表比如沪深300对应000300中证500对应000905中证红利对应000922。返回结构我踩过一个小坑后面会细说这里先说最终的形态。为了让模型容易“读懂”我把返回结构尽量拉平避免多层嵌套{ code: 000300, name: 沪深300, pe: 11.68, pb: 1.27, dividendYield: 3.15, valuationPercentile: 41.2, valuationLevel: 正常估值, updatedAt: 2025-06-15 14:30:00, source: valuation-api }字段名全部用英文小驼峰因为大模型对常见英文 token 的理解更稳定数值字段不带单位单位在字段说明里定义清楚比如 PE 是倍数dividendYield 是百分数。valuationLevel字段由服务端直接算好模型不需要自己判断“41%算不算低估”减少一层推理出错的风险。2.3 启动MCP服务并进行本地联调服务端我直接用 MCP Java SDK 起了个轻量服务进程独立于 Web 应用端口监听在本地 8081。下面这段代码是核心流程的示意不同版本的 SDK 在包名上会有些变化但整体思路一致McpServer server McpServer.sync(transport) .tool(get_index_fund_valuation, 获取指定指数基金的实时估值数据返回PE、PB、股息率、估值百分位和估值结论, args - { String code args.get(code).toString(); return valuationService.fetchValuation(code); }) .build(); server.start();启动后我先用一个 MCP 调试客户端做了联调确认工具能返回正确数据也确认了鉴权头能正确传递。这一步很重要因为很多问题不是出在 AI 侧而是数据服务本身在该死的边界条件上翻车比如传了个不存在的指数代码。我的策略是在服务端做白名单验证如果指数代码不在支持列表里直接返回一条结构化的错误信息而不是抛空指针异常。3. 落地第二步Spring AI客户端接入与工具自动发现3.1 依赖配置与MCP客户端连接Spring AI 侧的事情分成两块一块是模型接入另一块是 MCP 客户端。我用的模型 API 是兼容 OpenAI 接口的服务所以直接引入了 Spring AI 的 OpenAI StarterMCP 部分则引入了官方的 MCP 客户端 Starter。核心依赖大概长这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency在配置文件里指定 MCP Server 地址。因为我是本地调试所以只配了本机端口spring: ai: mcp: client: connections: - name: valuation-server url: http://localhost:8081这里我想强调一个容易忽略的点客户端连接配置好后Spring AI 默认会在启动时向 MCP Server 拉取工具列表。如果你的 MCP Server 挂掉了应用启动会失败。我一开始不知道这个细节测试时先停了服务端再启动应用结果直接报连接拒绝。解决方案是给连接配置设长一点的超时时间并在业务代码里做好降级。3.2 模型如何决定“该调用工具”Spring AI 内部把从 MCP Server 拉到的工具转换成ToolCallback统一交给模型侧的 function calling 机制。也就是说当用户提问“沪深300估值怎么样”时模型不是直接回答而是先判断自己缺少实时数据然后生成一个工具调用指令请求里的参数是{code: 000300}。Spring AI 收到工具返回结果后会把它作为上下文再喂回给模型模型基于这段真实数据生成最终回答。这一整个循环对开发者来说相当于自动完成你只需要在构建 ChatClient 时把 MCP 工具注册进去Bean McpToolCallbackProvider mcpToolCallbackProvider(ListMcpClient mcpClients) { return new McpToolCallbackProvider(mcpClients); }然后构建 ChatClient 时挂上ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(mcpToolCallbackProvider) .build();这部分是 Spring AI 做得比较舒服的地方。以前我自己实现 function calling 时光是维护参数 JSON Schema 就够头疼的了现在 MCP 工具定义直接描述了入参和返回结构模型可以自动适配。3.3 跑通一次完整的基金估值提问全部接通后我在真实对话里做了一个验证。用户输入是“今天沪深300的估值处于什么水平”我观察到的完整链路是模型判断需要估值数据生成工具调用get_index_fund_valuation参数为000300。MCP 客户端把请求转发给本地 8081 端口的 MCP Server。服务端拉取估值接口计算百分位返回 JSON 给 Spring AI。模型拿到 JSON 后生成回答“沪深300当前PE为11.68处于近8年正常估值区间百分位41.2%。”从用户提问到最终回复整个过程快的时候不到3秒。如果不走 MCP这个回答的准确性没有任何保障。这也验证了我最初的判断MCP 的价值不只是“能接数据”更是“让模型拿到它能信任的、结构化的实时数据”。4. 全链路观测提问、取数、生成每一步都留痕4.1 接通之前先想清楚要观测哪些指标AI 应用和新接入的数据链路打通之后会遇到一个实际的问题用户说“回答变慢了”你很难判断是模型 API 慢还是 MCP Server 慢还是用户网络问题。传统的单体接口监控只能看到总耗时看不到 AI 应用内部的工具调用环节。所以我在项目里专门加了一套全链路观测目标不是做复杂的大数据平台而是先把几个最关键的指标量出来。根据这个项目的实际环节我选了下面这些指标指标说明埋点位置整轮问答耗时用户提问到模型回复的总时间ChatClient 外层MCP 工具调用耗时工具请求发出到拿到返回值的时间MCP 客户端或服务端数据源接口耗时估值 HTTP 接口的实际响应时间MCP Server 内部缓存命中率有多少次请求走缓存而不是真实接口缓存工具类Token 消耗每次问答的输入和输出 token 数模型应答结果4.2 用拦截器和指标注册表完成埋点埋点的具体实现我用的是两层方案。第一层是日志贯穿。我在项目里加了一个简单的 TraceId 生成器在收到用户请求时生成一个唯一 ID塞进 MDC之后所有相关日志都会带上这个 traceId。这样一次完整提问的所有日志都能被串起来哪怕分布在多个模块里。第二层是指标采集。我用 Micrometer 注册了耗时计量器和计数器具体是在数据源 Service 层做了一次包装public Valuation fetchWithObservation(String code) { Timer.Sample sample Timer.start(registry); try { return fetchFromSourceOrCache(code); } finally { sample.stop(VALUATION_TIMER); } }Token 消耗指标我放在模型返回之后读取。Spring AI 的 ChatClient 返回结果里带Usage信息我把它取出后记录到计数器顺便算了一笔账一次完整的估值问答大约消耗的 token 量这样能估算出整个系统的运行成本。4.3 从一次异常反推链路日志如何定位讲一个实测中遇到的例子。某天我发现 MCP 工具调用耗时的 P95 从 300ms 涨到了 1800ms但数据源接口耗时没有明显变化。通过链路日志串查发现问题发生在 MCP Server 的线程池等待上服务端只有一个默认连接器同时多个指数查询进来时排队严重。我当时在日志里打的每个环节耗时是这样的[traceIdabc123] 接收请求开始时间14:02:01.100 [abc123] MCP客户端发送工具调用耗时结束14:02:01.450 [abc123] MCP Server收到请求标记开始14:02:01.452 [abc123] 数据源HTTP调用开始14:02:01.500结束14:02:01.560 [abc123] 服务端线程池排队耗时 0.9s一眼就能看出来瓶颈不在数据源而在 MCP Server 的连接器配置。我把服务端改成支持并发请求的 NIO 传输并给线程池设置了合理的大小之后P95 回到了 500ms 以内。这就是全链路观测的直接收益不是靠猜而是看数据定位。5. 我踩过的四个坑和对应的优化方案5.1 数据源限流把AI“卡死”只能上缓存和预热第一个坑发生在真实流量进来之后。第三方估值接口只允许每秒请求1次但我的应用为了让模型回答准确每次提问都要调用工具。两个人同时问问题第二个就超时了。解决方案是加了一层带 TTL 的本地缓存。我用 Caffeine 实现了缓存TTL 设成 5 分钟主流指数的数据在启动时做一次预热加载。这样即便是同一个指数连续被问也只有第一次真正打第三方接口。缓存命中率我给到了 90% 以上接口压力大幅度下降。CacheString, Valuation cache Caffeine.newBuilder() .maximumSize(200) .expireAfterWrite(Duration.ofMinutes(5)) .build();这里要说明一个点缓存会损失一点“实时性”但对指数估值这个场景是完全可以接受的。估值数据五分钟内的变化不会影响低估/高估判断实在要更准就把 TTL 调到 1 分钟按自己的业务容忍度来定。5.2 工具返回嵌套太深模型把字段读岔了第二次调整来自一个看起来不是问题的设计问题。我最初把返回结构设计成了嵌套标签比如{index: {code: 000300, name: 沪深300}}。模型在回答时偶尔会把index对象里的name字段当成用户问的“指数名称”或者干脆说“该指数名为 index”明显是对 JSON 结构产生了误读。后来我把返回结构拉平所有字段放在同一层并且在工具描述里加了一句“所有字段均为指数自身的估值属性没有子对象。”问题立刻减少了很多。这条经验让我意识到MCP 工具返回的数据结构要面向“大模型的理解习惯”来设计而不是面向 Java 对象设计。5.3 MCP服务单点故障用户对话直接瘫痪第三个坑是某天我部署新版本时MCP Server 重启了几十秒期间所有带了估值工具的应用都回答不了基金相关的问题。原因是 Spring AI 客户端在模型生成工具调用后如果拿不到服务整个对话流程就中断了。我的处理分两级。第一级是客户端配置虚拟机参数给连接设置合理的连接超时和请求超时第二级是兜底逻辑如果工具调用失败就返回缓存中的旧数据并明确告知用户“本次数据来源于缓存可能不是最新”。这样至少保证了对话不崩溃用户也能知道数据的可信度边界。5.4 盲目追求“实时”反而拖垮了回答速度最后一个坑是我自己的理解偏差。一开始我看到标题里写着“实时估值”下意识认为每次问答都必须打到第三方接口拿最新数。结果实测下来整个服务既慢又不稳。后来我想明白了一个道理这里的“实时”是相对于“大模型训练数据滞后几个月”说的。指数估值这个指标天然是慢变量核心看区间判断而不是看分秒变化。把缓存设成 5 分钟既回答了“是不是低估”又兼顾了响应速度。真正需要秒级数据的场景不是估值查询那应该走行情快照而不是走 AI 问答链路。做完这个小助手我最大的体感是MCP 把 AI 应用的数据接入门槛拆开了。以前做工具调用要自己搭工具协议、做鉴权、写描述文件现在一套标准协议能走到底。而全链路观测这件事千万不要等项目上线后再补一开始就把链路日志打全后面排查问题会轻松很多。接下来我打算把估值助手扩展到更多指数和更多数据源再把观测指标接到统一的监控面板上。这一期先分享到这里如果你也在折腾 Spring AI 和 MCP可以按这个思路动手试试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑