news 2026/10/1 19:03:16

不重构老系统,用MCP给旧CRM接入AI:一份实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不重构老系统,用MCP给旧CRM接入AI:一份实战避坑指南

先说个现象。最近这一两年,我在圈子里聊得最多的话题从“要不要上微服务”变成了“能不能给老系统接上AI”。手里捏着跑了好几年的订单系统、CRM、内部ERP,要说推倒重写,老板第一个不同意;但要说继续装作看不见AI这波浪潮,业务部门又天天来问“为什么别人家的系统能自动查单、自动填工单,我们的还得靠人肉点”。

后来我陆续帮几个客户把一套用了将近十年的老CRM接上了大模型,核心系统一行业务代码没动,数据库表结构也没碰,靠的就是在系统旁边加了一层MCP服务。这篇就把我踩过的坑、试出来的路子和可以直接抄的代码整理出来,给同样被旧系统绑住手脚的人一个参考。

1. 先搞清楚MCP到底解决什么问题

1.1 为什么老系统需要AI能力,而不是换一套新系统

很多老系统并不是“不能用了”,而是“太难改了”。业务逻辑埋在一堆存储过程里,表单校验散落在前端JS里,权限模型是十几年前设计的,换一个开发都能琢磨半个月。这种系统的核心资产不是代码,而是里面沉淀的数据和跑了几年的业务规则。

所以业务方说要“引入AI”,真正想要的不是换系统,而是让AI能帮他们处理那些重复、繁琐、需要查多个系统的操作。比如查一个客户的订单状态、根据合同条款生成催款通知、在工单系统里自动建单。这些事情本质上是“读老系统数据、按规则操作老系统”,而不是“重建老系统”。

这就引出一个关键问题:怎么让大模型安全、可控地触达老系统的数据和功能。直接给大模型开数据库权限那是灾难,把老系统的API一个个教会大模型去调也不现实。MCP就是来解决这个连接问题的。

1.2 MCP和以前那堆API网关有什么区别

我最早听到MCP时,第一反应是“这不就是个API网关吗”。等真正用起来才发现,两者解决的问题层面不一样。

API网关解决的是“谁来调、怎么路由、怎么鉴权”,它假设调用方是一个明确的前端应用或者服务,接口的格式、语义是事先约定死的。而MCP解决的是“AI客户端怎么发现工具、怎么理解工具、怎么调用工具”。它多了一层非常关键的东西:机器可读的工具描述。

打个比方,API网关相当于给系统开了一扇门,但门外的人得自己知道按哪个门铃、说什么暗号。MCP则是给门旁边装了一个公告栏,上面写着“这里有什么服务、每个服务需要什么参数、会返回什么结果”,AI来了先看公告栏,然后照着说明按门铃。大模型恰恰是那种“给它一份说明书它就能干活”的东西,所以MCP天然就是给AI用的接口标准。

它也不是某个厂商私有的东西,而是一个开放协议。官方定义里分了三个角色:宿主(Host,就是你运行的AI应用,比如Claude Desktop、Cursor,或者你自己写的Agent)、客户端(Client,负责跟Server通信)、服务器(Server,把具体能力暴露给AI)。它们之间走JSON-RPC 2.0的消息格式,传输方式支持本地子进程的stdio,也支持基于HTTP的SSE或Streamable HTTP。

1.3 一条消息在MCP里是怎么走的

我习惯用一套完整的调用链路来理解它。假设AI想查一个老CRM里的订单:

  • 第一步,AI客户端启动时向MCP Server发一个initialize请求,告诉Server“我是什么客户端、我支持什么协议版本”。
  • 第二步,Server回一个自身的协议版本和它支持的能力列表。
  • 第三步,AI客户端调用tools/list,把Server上所有工具的描述拉下来。这段描述包括工具名字、用途说明、参数用什么样的JSON Schema。
  • 第四步,大模型看完工具列表,决定要用哪个工具,于是发tools/call,带上参数。
  • 第五步,Server收到调用请求,内部去请求老系统的API、查数据库或者读写文件,把结果整理成结构化JSON返回给AI客户端。

这个过程里,老系统完全不知道MCP的存在。它只是被MCP Server以“一个普通API调用方”的身份请求了一下。这就是“旁路接入”的核心:不改变老系统内部,只改变老系统往外暴露能力的路径。

2. 旧系统接MCP的路线怎么选,我的判断标准

2.1 摆在面前的三条路线

我在动手之前列过三个方案,分别评估过成本、风险、见效速度。

第一条路,底层重写或者大重构。听起来最“彻底”,但现实是:老系统的数据模型和业务逻辑长年累月地耦合在一起,重构相当于在心脏上动刀,即便功能测试全过一遍,业务部门也未必敢上线。成本少说几个月,多的按年算。除非系统小到能抄底重来,否则不适合。

第二条路,硬在老系统内部嵌AI模块。直接在老代码里加SDK、加AI调用、加工具函数。问题是老系统通常没有很好的模块化边界,改一处常常牵出好几处编译错误不说,光是给老系统部署环境装上AI SDK依赖,就可能引发版本冲突。而且业务代码一改,整个系统的上线流程、压测、回滚方案全要重走。

第三条路,在老系统旁边部署一层MCP Server适配层。这层Server由新团队独立维护,它通过老系统已有的接口(REST、SOAP、数据库视图甚至直接读表)去访问数据和功能,对AI客户端暴露MCP标准协议。老系统那边最多只加一个只读账号或者开几个内网接口,不碰业务代码、不碰表结构、不用改部署方式。

2.2 我为什么最终选了“旁路适配层”

原因很直白:它对老系统的侵入面最小,又刚好能把现代工程手段都用上。

旁路方案里,MCP Server跑在独立进程、独立环境,依赖随便装,不用担心污染老系统运行时。它跟老系统之间只用最“老土”的方式通信——HTTP调用、SQL查询、文件读取。这些手段是任何老系统都具备的。而对外它又是标准的MCP,任何支持MCP的AI客户端拿过来就能用。

更重要的是,这个适配层的逻辑是“纯增量”的。AI相关的权限控制、数据脱敏、调用限流、日志审计,都可以放在这一层做。出了问题,把MCP Server停掉,老系统该干嘛还干嘛,回滚成本几乎为零。对老板来说,这是“有退路”的方案,决策阻力小了一大截。

2.3 桥接层到底该放哪些能力

不是老系统的一切都要暴露给AI。我建议按这四类来筛:

  • 读操作类:查订单、查客户、查库存、查物流轨迹。这是AI最常用的,安全风险低,优先接。
  • 写操作类:创建工单、更新状态、发送通知。风险中等,要加参数白名单、状态校验。
  • 高风险操作类:改价、退款、删除数据。能不放就不放,非要放的话,必须加人工审批环节,AI只负责起草,最终执行权留给人在界面上点。
  • 分析计算类:汇总统计、导出报表、生成合同。这类通常耗时长,要注意超时和异步任务设计。

放能力的时候记住一个原则:让AI“能读尽读,能写慎写,不改核心”。尤其写操作,能推进“待确认”状态的绝不让AI直接落库,这是我跟业务方博弈后总结出来的安全底线。

3. 不写一句业务代码,把老接口封装成MCP Server

3.1 技术选型:FastMCP帮你省掉一半工作量

MCP官方提供了Python和TypeScript的SDK。我给老系统做桥接,大多用Python,理由很实际:老系统周边往往已经有Python写的运维脚本、数据修正脚本,复用现成的连接池、数据库工具类,比用TypeScript从零搭更顺手。

SDK里我常用的是FastMCP这个封装,它在官方mcp包基础上简化了Server的定义过程,让你不用手写JSON-RPC消息处理逻辑。你只需要定义Python函数,加上@mcp.tool()装饰器,函数名和docstring会自动生成AI能读的工具描述。

这里有一个小知识点:MCP的Tool描述里,docstring写得好不好,直接决定大模型判断“该不该用这个工具、参数怎么填”。所以接口函数的说明要写清楚“这个工具是干什么的、参数含义、返回值结构”,跟给同事写交接文档一个道理。

3.2 核心代码:一个老CRM桥接Server的实例

假设我的老CRM系统提供了一套REST API,格式是老式的那种,前缀是/legacy/api,登录后才给token。我要在MCP Server里封装三个能力:按客户ID查订单列表、按订单号查状态、写一条催单记录。

下面是完整可跑的代码,用Python写:

#!/usr/bin/env python3 # legacy_crm_bridge.py import os import logging import requests from mcp.server.fastmcp import FastMCP logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("legacy-bridge") LEGACY_API_BASE = os.getenv("LEGACY_API_BASE", "http://legacy-crm.internal:8080/legacy/api") LEGACY_USER = os.getenv("LEGACY_USER", "mcp_bridge") LEGACY_PASS = os.getenv("LEGACY_PASS", "change-me") TIMEOUT = int(os.getenv("MCP_HTTP_TIMEOUT", "10")) mcp = FastMCP("legacy-crm-bridge") def _get_token() -> str: """登录老系统换token,REST接口的老套路""" resp = requests.post( f"{LEGACY_API_BASE}/auth/login", json={"username": LEGACY_USER, "password": LEGACY_PASS}, timeout=TIMEOUT, ) resp.raise_for_status() return resp.json()["token"] def _call_legacy(method: str, path: str, **kwargs) -> dict: """统一的请求入口,把token塞进header""" token = _get_token() headers = {"Authorization": f"Bearer {token}"} url = f"{LEGACY_API_BASE}{path}" logger.info("calling legacy API: %s %s", method, path) resp = requests.request(method, url, headers=headers, timeout=TIMEOUT, **kwargs) resp.raise_for_status() return resp.json() @mcp.tool() def query_orders_by_customer(customer_id: str) -> list[dict]: """按客户ID查询订单列表。customer_id是CRM系统中的客户主键,形如C-10023。返回订单列表,每个订单含订单号、金额、创建时间和当前状态。""" data = _call_legacy("GET", f"/customers/{customer_id}/orders") return data.get("orders", []) @mcp.tool() def query_order_status(order_no: str) -> dict: """按订单号查询订单状态。order_no是订单唯一编号,形如SO-2024-000123。返回订单状态、物流状态和最近更新备注。""" data = _call_legacy("GET", f"/orders/{order_no}/status") return data @mcp.tool() def create_collection_reminder(order_no: str, content: str) -> dict: """在CRM中创建一条催单提醒。order_no为目标订单号,content为提醒内容,建议在200字以内。返回创建的提醒记录ID和状态。""" payload = {"order_no": order_no, "content": content, "source": "ai_assistant"} data = _call_legacy("POST", "/reminders", json=payload) return data if __name__ == "__main__": mcp.run(transport="stdio")

这段代码的核心在于,_call_legacy把所有对老系统的HTTP调用统一收口,加日志、加异常兜底都方便。实际对接中,你们老系统的鉴权可能是Cookie Session、可能是AppKey,甚至可能是“先调一个接口拿加密sign”,那就把_get_token替换成对应的逻辑就好,其余框架不用动。

用transport="stdio"是短跑阶段最稳的选择。AI客户端通过标准输入输出跟Server通信,不需要搞端口监听、不需要配防火墙,本地跑起来就通。如果你们的AI应用部署在远程,Server又必须在老系统内网跑,再用SSE或Streamable HTTP。

3.3 配置AI客户端,把Server挂上去

代码写完后,要用支持MCP的客户端验证。我这儿以Claude Desktop和Cursor为例,都是JSON配置就能挂载。

Claude Desktop的配置在claude_desktop_config.json里:

{ "mcpServers": { "legacy-crm-bridge": { "command": "python", "args": ["/data/mcp_servers/legacy_crm_bridge.py"], "env": { "LEGACY_API_BASE": "http://legacy-crm.internal:8080/legacy/api", "LEGACY_USER": "mcp_bridge", "LEGACY_PASS": "change-me", "MCP_HTTP_TIMEOUT": "15" } } } }

重启客户端,在对话窗口里直接说“帮我查一下C-10023这个客户的订单”,如果Server挂载成功,客户端会自动发现query_orders_by_customer这个工具,让AI去调用它。首次跑通这个链路,整个项目的可行性就算验证完成了。

Cursor的话,项目根目录放一个.cursor/mcp.json,结构差不多,只是command要写绝对路径,因为Cursor的进程工作目录不一定是项目目录。

3.4 调通之后AI真的能干活了

我的经验是:第一个工具调通后,别急着接第二个,先让业务方来试用一轮。让真实用户用“自然语言”问AI各种问题,看它能不能正确选对工具、填对参数。这一步暴露出来的问题,比你自己闷头写10个工具有用得多。

第一次调通当天,业务方就试出好几个坑:他们习惯用“上周的订单”这种模糊时间概念,但我们老系统的接口只支持按日期范围查。解决方案不是我改老系统,而是在MCP Server里加一个“自然语言日期转范围”的逻辑,比如用Python的dateparser库,或者干脆把“上周”这类词在工具描述里写清楚让大模型自己转换成日期参数。

4. 老系统接AI必踩的坑和排查实录

4.1 我在实战中踩过的具体坑

坑一:老接口响应太慢,AI客户端先超时

MCP客户端调用工具有默认超时,Claude Desktop默认大概几十秒。我接的那个订单系统,有个查询接口要关联七八张表,冷查询要跑40秒。AI调用后干等,然后报超时,反复试几次之后AI就会跟用户说“这个工具不可用”。

解决思路:在MCP Server里做“缓存+超时分级”。高频查询加Redis缓存,缓存过期时间设短一点,比如5分钟。超过5秒的查询降级到异步任务:先返回“正在查询”的任务ID,AI看到任务ID后再轮询结果。这一套做完,AI的体验顺畅多了。

坑二:老系统鉴权方式奇葩,MCP Server频繁登录被限流

有个老系统的登录接口没有做频率限制,但登录一次拿到的token有效期只有30分钟。我一开始每个请求都重新登录,跑了半天,老系统那边的安全团队找过来说账号被风控了。

后来改成:MCP Server内存里维护一个token缓存,带过期时间,只在token过期前1分钟才重新登录。代码很简单,但能避免把老系统的账号搞出问题。

坑三:字段语义不一致,AI给出的参数和老系统对不上

老系统里的“客户名称”可能是“customer_name”,也可能是“cust_nm”,还有可能是“客户全称”这种中文键。大模型从对话里提取“张三”去调用工具,如果工具描述里没写清楚,它很可能填错字段。

我的办法是:在工具函数的docstring里写清字段别名和示例值,并且在MCP Server里做一层“参数归一化”。比如客户名,不管AI传的是customer_name还是cust_nm还是中文“客户名称”,都在Server内映射到老系统真正认的那个字段。

4.2 一条排查路径

MCP Server报错,优先看日志,不要瞎猜。我给Server加了统一的日志出口,本地跑的时候直接看标准输出,部署到远程后打到文件。日志要重点打三件事:收到什么工具调用、带什么参数、老系统返回什么(或者报什么错)。

我见过很多同行卡在“AI说工具不可用”,其实是MCP Server进程启动时环境变量没配全,初始化就挂了。这种问题看启动日志一眼就明白。所以客户端配置里的env,务必跟Server代码里读的环境变量一一对齐。

4.3 再补几个设计层面的经验

  • 审计日志单独建表:AI对老系统的每次读写,都要能追溯到是哪次对话、哪个用户触发的。这个可以直接在MCP Server里加一个装饰器,用AOP的思路统一记录。
  • 测试工具用“影子模式”:写操作的API,先在测试环境完全模拟一遍。真实环境只先开放读工具,等业务方对AI行为有信任了,再逐步放开写工具。
  • 回滚按钮要物理存在:MCP Server做成独立服务,出问题就停进程。老系统的防火墙出站规则里,可以随时断开MCP Server到老系统的访问,这样就算Server被攻破,影响面也限制在“断了桥”。

5. 从“接口能通”到“AI真好用”,还要补哪些功夫

5.1 日志管理和可观测性

MCP Server的日志不能只靠print。建议用标准logging模块,分模块、分级别地打。我给实际项目定的规范是:

  • INFO级别:记录每次工具调用、调用来源、耗时。
  • WARNING级别:老系统接口返回非200、参数接近白名单边界。
  • ERROR级别:调用失败、鉴权失败、数据解析异常。

如果MCP Server跑在容器里,日志直接打到stdout,交给日志采集系统统一收。这样出了问题,能从“AI客户端请求”一直追到“老系统SQL执行”,整条链路看得清。

5.2 权限控制,尤其是召回和数据边界

很多老系统的账号体系没有细粒度权限,一个接口可能返回客户全字段,包括手机号、身份证号、价格成本。AI调用这个接口后,如果它把这些信息原样回复给用户,就是数据泄露。

我建议在MCP Server上加一层“返回字段白名单”。老系统返回什么我不管,MCP Server往外吐数据之前,按工具级别把敏感字段剥掉。比如查订单接口,我默认只保留订单号、商品名、金额、状态,手机号、身份证这种除非专门的高级权限工具,否则一律过滤。

5.3 打通回写链路,让AI不只是“只读助手”

业务方最满意的功能其实是“回写”。他们不满足于AI只查数据,还希望AI能直接帮他们把结果写回系统。比如AI分析了客户的订单习惯,生成一条跟进任务,自动写到CRM的日程表里。

回写比读取麻烦的地方在于幂等。AI可能因为网络不明原因把同一个工具调用发了两次,如果Server没做幂等,老系统里就会多出两条一样的工单。解决办法是:写操作的工具,接收一个由AI生成的request_id,老系统那边如果支持,就在业务表里建唯一索引;不支持的话,MCP Server里用Redis的SETNX做一个幂等标记。

import redis import uuid r = redis.Redis(host=os.getenv("REDIS_HOST", "127.0.0.1"), port=6379, db=0) @mcp.tool() def create_collection_reminder(order_no: str, content: str) -> dict: """在CRM中创建一条催单提醒。order_no为目标订单号,content为提醒内容。返回创建的提醒记录ID和状态。""" request_id = str(uuid.uuid4()) dedup_key = f"mcp:dedup:reminder:{request_id}" if not r.set(dedup_key, "1", nx=True, ex=600): raise ValueError("重复请求已被拦截") payload = {"order_no": order_no, "content": content, "source": "ai_assistant", "request_id": request_id} data = _call_legacy("POST", "/reminders", json=payload) return data

这里request_id也可以由AI客户端主动传,但让Server自己生成更省心。Redis的SETNX保证同一把钥匙只能成功一次,10分钟内重复提交自动拦截。

5.4 多个AI客户端共用同一个Server

我最初是给Claude Desktop配好就结束了,后来Cursor、自研Agent也都要用同一套老系统能力。不要让每个客户端都去装一遍桥接代码,而是把MCP Server单独部署成一个常驻服务,用SSE或Streamable HTTP暴露给不同客户端。

这样一来,工具代码只维护一份,权限、日志、幂等都在这一层统一做。客户端那边只需各自配置一行Server地址。这套架构的演进方向,其实就是企业内部慢慢会长出一个“AI能力网关”,老系统的各种能力围绕它逐步开放。

我自己在这几轮落地里最大的感受是:接AI这件事,本质不是技术突破,而是“系统对外开放能力的标准化”。老系统之所以让人望而生畏,是因为它太封闭、太特别、太依赖于懂它的人。MCP给了我们一个契机,用一套现代AI完全听得懂的语言,把老系统的能力重新讲了一遍。只要抓住“旁路适配、最小侵入、按需开放”这三个原则,哪怕系统再老,都能一步一步把它带进AI时代。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:03:08

深入理解 Redis 分布式锁:从原理到生产实战(2 万字详解)

摘要:Redis 是互联网后端中使用最广泛的中间件之一,除了作为缓存和消息队列,它还被大量用于实现分布式锁。本文从分布式锁需要解决的核心问题出发,系统讲解 Redis 实现分布式锁的常见方案、加锁与解锁的正确姿势、锁超时与自动续期…

作者头像 李华
网站建设 2026/10/1 19:02:25

SQL调优实战:从慢查询定位到索引优化,实现10倍提速

数据库工程做了这么多年,SQL调优的目标无非就是让查询更快、让数据库更扛压。但“提升10倍查询速度”这件事,很多人一听就觉得夸张,觉得是不是要上什么高端硬件、搞什么分布式架构。实际上,在我经手的绝大多数项目里,S…

作者头像 李华
网站建设 2026/10/1 19:02:12

运行时错误RE四大根源:数组越界、空指针、除以零与死递归

1. RE问题到底是什么?别再被缩写搞晕了 RE,全称是Runtime Error,中文叫运行时错误——这个词在编程圈里天天见,但很多人直到报错弹窗跳出“Segmentation fault”“NullPointerException”或者“java.lang.ArithmeticException: / …

作者头像 李华
网站建设 2026/10/1 19:00:57

从摔车到SOP:游戏开发者的skill蒸馏实战指南

1. 从“摔车”说起:为什么我把自己的开发方式推倒重来“摔车”这个词,是我对自己早期做游戏开发状态的一个比喻。不是真的骑摩托摔了,而是那种一路猛冲、突然翻车、爬起来发现车也坏了、路也走错了的感觉。我最早做游戏,靠的是一股…

作者头像 李华
网站建设 2026/10/1 19:00:43

MCP实战:用AI构建Excel自动化处理服务

每天跟Excel打交道的朋友应该都有这种体会:处理报表本身不是最费时间的,费时间的是那些重复性的操作——打开表格、定位列、写公式、复制粘贴、再生成新表。尤其是当数据源有变动、格式不统一的时候,整个人都会烦躁起来。 我最近用MCP&#…

作者头像 李华