不重构老系统,用MCP给旧CRM接入AI:一份实战避坑指南
发布时间:2026/10/1 19:03:17来源:尧图网络
先说个现象。最近这一两年我在圈子里聊得最多的话题从“要不要上微服务”变成了“能不能给老系统接上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 Server3.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( levellogging.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: 登录老系统换tokenREST接口的老套路 resp requests.post( f{LEGACY_API_BASE}/auth/login, json{username: LEGACY_USER, password: LEGACY_PASS}, timeoutTIMEOUT, ) resp.raise_for_status() return resp.json()[token] def _call_legacy(method: str, path: str, **kwargs) - dict: 统一的请求入口把token塞进header token _get_token() headers {Authorization: fBearer {token}} url f{LEGACY_API_BASE}{path} logger.info(calling legacy API: %s %s, method, path) resp requests.request(method, url, headersheaders, timeoutTIMEOUT, **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, jsonpayload) return data if __name__ __main__: mcp.run(transportstdio)这段代码的核心在于_call_legacy把所有对老系统的HTTP调用统一收口加日志、加异常兜底都方便。实际对接中你们老系统的鉴权可能是Cookie Session、可能是AppKey甚至可能是“先调一个接口拿加密sign”那就把_get_token替换成对应的逻辑就好其余框架不用动。用transportstdio是短跑阶段最稳的选择。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秒的查询降级到异步任务先返回“正在查询”的任务IDAI看到任务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(hostos.getenv(REDIS_HOST, 127.0.0.1), port6379, db0) mcp.tool() def create_collection_reminder(order_no: str, content: str) - dict: 在CRM中创建一条催单提醒。order_no为目标订单号content为提醒内容。返回创建的提醒记录ID和状态。 request_id str(uuid.uuid4()) dedup_key fmcp:dedup:reminder:{request_id} if not r.set(dedup_key, 1, nxTrue, ex600): raise ValueError(重复请求已被拦截) payload {order_no: order_no, content: content, source: ai_assistant, request_id: request_id} data _call_legacy(POST, /reminders, jsonpayload) 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时代。
网站建设高端定制企业官网