从Function Calling到MCP协议:金融Agent工具调用链路完整梳理

从Function Calling到MCP协议:金融Agent工具调用链路完整梳理

金融场景里的Agent,工具调用不是"能不能调通"的问题,而是"敢不敢让它调"的问题。在做一个投研助手Agent的过程中,我经历了从最朴素的Function Calling到引入MCP协议重构工具链路的完整过程。这篇文章把这条链路从头到尾理一遍,包括踩过的坑和当时的判断逻辑。

为什么Function Calling不够用了

最开始我们的投研Agent工具数量不多,就五六个:查行情、读财报摘要、算估值指标、搜公告、查宏观数据。直接用OpenAI风格的Function Calling,在系统提示词里声明函数定义,模型返回要调哪个函数、参数是什么,我们这边执行完把结果塞回对话上下文。这套东西跑通很快,两天就搞了个能演示的版本。

问题出现在工具数量涨到二十多个之后。金融工具的特点是长得像但不一样------比如"查行情"和"查历史行情"就差一个时间参数,"读财报摘要"和"读财报原文"返回的结构完全不同。所有工具平铺在系统提示词里,模型开始频繁选错工具,有时候该调A却调了B,参数还填得有模有样。更麻烦的是,我们同时接了两家不同的模型供应商,Function Calling的声明格式和返回格式有细微差异,每接一家就要写一层适配。

还有个更现实的问题:工具实现分散在各个服务里,有的用Python写的有的用Java写的,函数签名改了之后要手动同步到Agent侧的声明,漏一次就出一次事故。那段时间我经常半夜被叫起来,一查就是"工具声明和实际实现不一致"。这些问题的核心不是某个函数写得不好,而是工具调用缺乏一个统一的、带类型的、可发现的抽象层。

为什么选了MCP而不是继续缝补

坦白说我们一开始没想上MCP,觉得那是给大模型和外部工具之间做标准化的,离我们的业务场景有点远。我们先尝试了两个"轻量"方案:一个是把工具声明集中到一个配置文件里统一生成,一个是给Function Calling加了一层路由逻辑按领域分组。这两个方案都缓解了问题,但都没解决本质------工具的实现和声明仍然是分离的,模型侧的"工具视图"和实际的"工具能力"之间靠人工保证一致性。

后来看了MCP协议的草案,觉得它的几个设计恰好打在痛点上。MCP把工具抽象成客户端-服务端结构,工具提供方注册成一个MCP Server,用统一的schema描述每个工具的输入输出,Agent侧作为MCP Client去发现和调用。这样工具声明和实现天然绑定在一起,Server端工具改了参数,Client端发现时拿到的就是最新定义,不会出现"声明滞后"的问题。传输层支持stdio和SSE,我们内部用HTTP SSE就够,部署上灵活。

选MCP还有一个附带好处:我们团队里做数据平台的同事可以独立开发MCP Server,不用管Agent侧怎么用,他们只负责把数据能力包装成符合规范的MCP工具。这种"工具提供方只管提供、Agent侧只管消费"的分工,让我们从工具调用的泥潭里抽身出来,开始认真考虑工具链路的整体设计。

工具调用链路的实现过程

整个链路分三层:MCP Server层(工具的实际提供方)、MCP Client层(Agent侧的工具发现与调用封装)、编排层(模型决策与执行循环)。我们用Python实现了一套精简的MCP Client,核心逻辑简化后长这样:

python 复制代码
# mcp_client.py - MCP客户端核心逻辑(简化版)
import json
from typing import Any

class MCPToolClient:
    def __init__(self, server_endpoint: str):
        self.endpoint = server_endpoint
        self.tools_cache = None
    
    def list_tools(self) -> list[dict]:
        """从MCP Server发现可用工具,返回标准化的工具schema列表"""
        if self.tools_cache is not None:
            return self.tools_cache
        response = self._http_get(f"{self.endpoint}/tools/list")
        tools = response.get("tools", [])
        # 标准化成模型可理解的函数声明格式
        normalized = []
        for tool in tools:
            normalized.append({
                "name": tool["name"],
                "description": tool.get("description", ""),
                "parameters": tool.get("inputSchema", {})
            })
        self.tools_cache = normalized
        return normalized
    
    def call_tool(self, name: str, arguments: dict) -> Any:
        """调用指定的MCP工具并返回结果"""
        payload = {"name": name, "arguments": arguments}
        response = self._http_post(f"{self.endpoint}/tools/call", payload)
        return response.get("result")

这段代码位于Agent侧的MCP Client层,负责两件事:把MCP Server提供的工具schema转换成模型能理解的函数声明格式,以及把模型的工具调用请求转发给MCP Server。它解决的核心问题是让Agent感知到的工具定义永远和实际实现保持一致,因为发现和调用走的是同一个源头。

在编排层,工具调用循环从"模型返回函数名→我们查表执行"变成了"模型返回函数名→MCP Client执行",表面看变化不大,但工具发现的时机变了。以前是启动时把工具列表塞进系统提示词,现在可以在对话过程中动态发现。这让我们可以按需加载工具,比如用户问到财务数据时才挂载财务领域的MCP Server,而不是一上来把几十个工具全堆给模型。

另一个关键代码在MCP Server端,工具注册的部分:

python 复制代码
# mcp_server.py - 工具注册与schema生成(简化版)
from typing import Callable, get_type_hints
import inspect

class MCPServer:
    def __init__(self):
        self.tools = {}
    
    def register_tool(self, func: Callable):
        """将Python函数注册为MCP工具,自动从类型注解生成inputSchema"""
        hints = get_type_hints(func)
        # 从函数签名和docstring生成工具schema
        schema = {
            "name": func.__name__,
            "description": inspect.getdoc(func) or "",
            "inputSchema": {
                "type": "object",
                "properties": {
                    k: self._type_to_schema(v) 
                    for k, v in hints.items() 
                    if k != "return"
                }
            }
        }
        self.tools[func.__name__] = {"func": func, "schema": schema}
    
    def _type_to_schema(self, type_hint) -> dict:
        """将Python类型映射为JSON Schema类型(简化版)"""
        mapping = {str: {"type": "string"}, int: {"type": "integer"}, 
                   float: {"type": "number"}, bool: {"type": "boolean"}}
        return mapping.get(type_hint, {"type": "string"})

这段代码在MCP Server端,让工具开发者用普通的Python函数加上类型注解就能注册工具,schema自动生成。位置在数据平台同事写的服务里,他们不需要理解Agent侧如何消费这些工具,只需要把函数写清楚、docstring写明白。它解决了工具定义和实现分离的问题------工具的定义不是别处维护的一份配置文件,而是函数签名本身。

这两段代码合起来,构成了工具调用链路的"统一抽象层"。MCP Server负责提供带schema的工具,MCP Client负责发现和执行,编排层只管决策。

过程中遇到的实际问题

第一个问题是工具描述写的质量直接决定模型选工具的准确率。MCP协议本身不解决"工具描述写得好不好",它只是把描述传递过去。我们有几个工具名字起得太像,描述写得太笼统,模型在它们之间反复横跳。后来花了不少时间重写工具描述,把适用场景、参数约束、返回结构都写清楚,体感上工具选择的准确度有明显改善。这个经验很朴素:协议再好,工具的元数据质量才是上限。

第二个问题更隐蔽。MCP协议支持工具返回结构化数据,但模型对超长返回内容的处理会退化。查财报原文那个工具,返回的文本动辄上万字,塞回上下文之后模型开始"忘事",前面问的问题后半段就不管了。我们的处理方式是在MCP Server端加了一层返回压缩,工具内部先做摘要或截断,把核心信息控制在合理长度内再返回给Agent。这严格来说超出了MCP协议的范畴,但在金融场景里不做不行。

还有一个问题是MCP Server的可用性。工具调用从"本地函数调用"变成了"跨进程甚至跨服务的远程调用",网络超时、服务重启这些运维问题全来了。我们在MCP Client里加了超时和重试逻辑,但说实话,比起Function Calling时代的"调用一定成功",MCP的分布式特性带来的不稳定性是真实存在的代价。这些问题的共同点是它们不在协议规范覆盖范围内,但对实际使用的影响远大于协议本身的对错。

总结与下一步

从Function Calling到MCP,本质是把工具调用从"模型和一个函数列表的对话"变成了"模型和一组可发现服务的对话"。对金融Agent来说,这个变化让工具管理从手工作坊变成了有标准的流水线,团队协作的边界也更清晰了。代价是链路多了一层网络开销,以及要自己处理分布式调用带来的各种状况。

下一步想探索的是工具之间的自动组合------让模型根据用户意图自动编排多个MCP工具的调用序列,而不是每次都靠我们预设流程。初步的想法是基于MCP工具schema做依赖分析,看看能不能在编排层做自动规划。这条路不好走,但方向应该是对的。

相关推荐
lunzi_08266 小时前
【无标题】
ai·金融·开源·供应链安全·ai agent·银行开源治理
砚底藏山河8 小时前
行情工程实战 M01|存储选型 CSVSQLiteMySQL
java·python·金融·maven
M哥支付18 小时前
高频交易优选方案:银联快捷支付通道
服务器·网络·其他·微信·金融
M哥支付1 天前
快捷支付 VS 网关支付 安全对比
服务器·网络·其他·微信·金融
砚底藏山河1 天前
【量化纯GET实战 #23】多股票相关性:用收益率看板块联动
java·数据库·python·金融·数据分析
DolphinDB智臾科技2 天前
不懂复杂金融数据,也能让 AI 做投研:DolphinDB 股票分析 MCP 已开源
人工智能·金融·开源
IT毕设实战小研2 天前
基于大数据的DAX40成分股金融新闻情感趋势可视化分析
android·大数据·python·考研·金融·课程设计
砚底藏山河2 天前
【量化纯GET实战 #21】用 pandas 做分析:把接口数据变成 DataFrame
java·python·金融·maven·pandas
2601_960356382 天前
金融工程专业大学期间适合考哪些证?就业方向与考证规划
金融