多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零构建AI工程:MCP插件开发与实战复盘

从零构建AI工程:MCP插件开发与实战复盘 不少朋友问我AI工程到底该怎么入门。市面上聊AI工程的课程和帖子动不动就是RAG、Agent、微调、大规模分布式推理看起来高深真正上手却发现离自己的日常工作很远。我自己的体会是与其从那些宏大概念开始不如找一个能塞进真实工作流的微型项目把AI能力真正用起来。我最近做了一个代号为ai-engineering-from-scratch的小工程——给Claude Code写一个MCP反馈插件用来做代码库安全风险扫描和重构影响评估。整个过程从设计、实现到部署、评测覆盖了AI工程的全链路。这篇内容不是理论科普就是一次完整实操的复盘希望对想系统接触AI工程、却又不知道从哪里下手的开发者有点帮助。1. 为什么“从零开始做AI工程”值得认真对待1.1 AI工程不是“调大模型接口”而是“让模型变可靠”很多开发者以为接上大模型API就算做了AI工程。真不是。你调通一个OpenAI或Anthropic的接口那只是拿到了一个“会说话的引擎”而AI工程解决的是把引擎装进真实系统后的一系列麻烦输入不可控、输出不稳定、失败成本高、响应时延飘忽不定、安全和隐私怎么兜底、效果怎么评估。这些才是日常开发中真正消耗精力的地方。传统软件工程里你写的函数只要逻辑正确、资源不泄漏基本就能交付AI工程里模型给出的答案可能部分正确、可能完全自信地说错还可能因为一段措辞不同的Prompt产生完全不同的行为。你没法像断言返回值那样约束它。所以做AI工程本质上是在技术和不确定性之间搭桥把“模型输出”变成可以纳入工程体系的“结构化资源”。这也是为什么我特别认“from scratch”这个思路。它不是说要你从反向传播开始手写Transformer而是说把AI能力从想法到落地跑通一遍完整路径亲身体会各个环节的取舍。一次完整的血泪实操比刷十篇架构文章有用得多。1.2 一个迷你但完整的案例给Claude Code写MCP反馈插件我选择的切入点非常小给Claude Code写一个MCP扩展插件。MCP全称是Model Context Protocol简单理解就是给AI模型外接工具的标准协议。Claude Code本身是Anthropic推出的命令行编程助手它能读懂代码库、改代码、运行命令。但它默认并不知道你们公司内部API的调用规范也不知道某个依赖在你们生产环境里的真实风险。MCP插件就是用来补这些信息差的。我写的这个插件叫yio它做了三件事扫描依赖清单文件对比内部漏洞库输出高风险、中风险的依赖建议分析代码调用链估算一次重构会影响的文件范围和测试集合把工具调用频率、失败率、耗时时长等匿名指标通过事件上报到自己的分析服务为后续迭代UI和Prompt提供数据依据。范围很小但它精准命中了AI工程的核心环节协议接入、工具设计、上下文管理、评测闭环、可观测性。做完这个项目你会对“AI工程”这四个字有一个很扎实的体感而不是停留在名词层面。1.3 这篇分享适合谁读、你能带走什么如果你是有几年经验的后端、前端或测试开发想转AI应用方向那这篇内容很适合你。你不需要懂模型训练只要会写TypeScript或者Python能理解基本的进程通信就能跟上。如果你已经在写一些AI辅助脚本但总觉得散不成体系这篇内容也会帮你梳理出一条完整的工程路径。跟着走一遍之后你会带走三样东西一套可以直接复用的MCP插件工程模板包括配置管理、工具注册、协议调试一套针对AI工具效果的评估思路知道怎么从“能跑”到“靠谱”一份踩坑清单都是我实际开发中会耽误一整天的类型问题你可以直接绕过去。2. 动手前的系统性设计模块划分与工具链选型2.1 模块边界四个组件少一个都不行接到这个任务我的第一反应不是打开编辑器写代码而是先花半小时把模块边界画清楚。任何工程只要注入了“模型能力”就不太可能像写小脚本那样一个文件搞定因为你要同时面对协议解析、工具逻辑、外部API、数据上报四类问题混在一起后期会非常痛苦。我最终拆成了四个组件config负责读取环境变量、配置文件、合并默认值所有工具的鉴权信息、超时控制、开关策略都在这里统一管理protocol负责MCP协议层的消息解析、请求校验、错误码转换让上层工具完全不用关心JSON-RPC细节tools实现具体业务工具比如依赖安全扫描、重构影响评估它们是纯粹的输入处理逻辑计算结果返回telemetry负责事件上报包括工具调用次数、失败原因分类、耗时分布并且支持开关和脱敏。这样划分的逻辑很简单协议层会跟着SDK升级变工具层会跟着业务需求变上报逻辑会跟着指标口径变。如果三者耦合在一个文件里任何一方变动都会引发连锁故障。而分开之后每个组件都可以独立测试、独立替换甚至独立上版本。这个设计原则和你在传统后端写service、repository、controller的拆分思路是一致的。2.2 技术选型Node.js TypeScript 为什么比 Python 更顺手技术选型上我几乎没有犹豫就选了Node.js TypeScript而不是很多人默认的Python。原因很实际。MCP官方SDK对TypeScript的支持非常完善Claude Code本身通过npx就能拉起Node进程标准输入输出通信在Node里处理起来很顺。而Python生态在这一块也不差但如果你用的是一个集成开发工具最终总会遇到“要给Claude Code临时装一个Python环境”的尴尬——版本冲突、依赖缺失、环境变量不一致这些破事会拉低开发体验。TypeScript带来的类型安全不是“锦上添花”在MCP插件开发里它是刚需。每个工具要做输入参数描述这些描述会经过序列化、传输、再解析最后落到一个JSON对象里。没有类型定义你根本不知道模型给你传进来的参数到底是字符串还是数字是一个路径还是直接塞了一段文件内容。有了类型和运行时校验协议层后面才有机会做更细的容错。另外MCP插件的本质是一个长驻进程Node在处理长驻I/O任务上的表现非常稳定资源占用也可控。配合tsx做开发时的热运行迭代起来很舒服。2.3 关键参数设计超时、置信度与评分阈值不能拍脑袋写这类插件最忌讳的就是参数全凭感觉。我在动手前就把几个核心参数定下来了每个参数都经过了推导。第一个是外部API的超时时间。我的安全扫描需要调用内部漏洞库接口这个接口的P99延迟实测大概是2.8秒。超时如果设在1秒线上有1%的请求必然失败如果设在10秒用户等一个扫描结果要卡半天。我取的是5秒——在P99基础上预留了接近两倍余量保证了绝大多数请求能成功又不会让交互显得拖沓。第二个是置信度阈值。安全评分逻辑会综合依赖版本落后情况、已知漏洞数量、维护状态给出0到1的置信度。我用一个包含40个真实案例的标注集分别跑了一遍阈值设为0.7、0.8、0.9时的准确率和召回率最后选了0.8。这个数值的含义是宁可少报两条边缘风险也不允许把安全结果报错。第三个是返回内容大小限制。工具返回给模型的结果不是越多越好上下文窗口有限塞太多信息反而会干扰后续推理。我把每条建议的token上限控制在200以内一次扫描总时长不超过几十秒整体返回控制在1500 token以内。这个参数直接影响了后面评测集的上下文膨胀率指标后面会详细说。3. 核心实现一个MCP插件从0到13.1 初始化工程先搭一个能跑的MCP服务骨架我习惯把项目初始化这一步当成“冒烟测试”来做确保最小骨架能跑通再往里填业务逻辑。工程结构很简单src目录下放了四个子目录对应前面说的四个模块根目录放package.json和tsconfig.json。下面是MCP服务骨架的代码这个骨架是后续所有工具的基础// src/index.ts import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { loadConfig } from ./config/index.js; import { registerSecurityTools } from ./tools/security.js; import { registerRefactorTools } from ./tools/refactor.js; const config loadConfig(); const server new McpServer({ name: yio, version: 0.1.0, }); registerSecurityTools(server, config); registerRefactorTools(server, config); const transport new StdioServerTransport(); await server.connect(transport);这段代码的核心是McpServer实例加StdioServerTransport。前者负责维护工具注册表和处理JSON-RPC消息后者把进程的标准输入输出变成通信管道。Claude Code启动插件时会和这个进程建立双向通信模型发送请求插件返回结果。整个过程对用户是透明的你只要保证这个进程能正常被拉起、正常响应请求就行。骨架搭好之后立刻验证启动进程往里塞一个JSON-RPC初始化请求看它是否返回标准响应。只有这一步通了后面写工具才有意义。3.2 配置加载环境变量、默认值与密钥处理配置模块是整个插件的“入口命门”处理不好后面所有工具都会跟着遭殃。我做配置加载时遵循了一个分层合并的原则默认值优先其次是环境变量最后是显式配置文件后面的覆盖前面的。安全扫描工具需要访问内部漏洞库接口必须带上一个API Key。这个Key是团队内的共享凭证不能写死在代码里也不能提交到Git仓库。我的处理方式是默认从环境变量读取变量名叫YIO_API_KEY如果环境变量里没有再尝试读取~/.yio/config.json两份都找不到工具直接返回错误而不是带着空Key去请求外部API。这个设计看起来简单实际运行中救了很多次命。团队里新同学拉下代码只要配置好环境变量就能跑不用改任何代码。密钥信息只存在于运行环境的上下文中代码仓库里永远干干净净。此外配置里还放了一个“开关”telemetry.enabled。如果用户不想上报任何使用数据一键关掉。从工程伦理角度讲任何数据采集都必须给用户明确的选择权。我这个插件默认开启但首次运行时会打印提示告知数据用途和关闭方法。3.3 实现第一个工具依赖安全风险扫描安全扫描工具的目标是给定一个依赖清单文件输出每个依赖的风险等级和修复建议。我用TypeScript写了一个简化版的分析逻辑// src/tools/security.ts import { z } from zod; import { checkVulnerabilityDB } from ../services/vuln-db.js; export function registerSecurityTools(server, config) { server.tool( scan_dependency_risks, { manifestPath: z.string().describe(依赖清单文件的绝对路径), }, async ({ manifestPath }) { const entries await parseManifest(manifestPath); const checks entries.map(entry checkVulnerabilityDB(entry.name, entry.version, config)); const results await Promise.all(checks); const high results.filter(r r.level high); const medium results.filter(r r.level medium); const suggestions buildSuggestions(results, config.threshold); return { content: [ { type: text, text: formatReport(high, medium, suggestions), }, ], }; } ); }这里有几个值得展开的细节。manifestPath的类型描述不是随便写的。我把“这是一个文件路径”这个语义写得很清楚模型才能把用户口中的“检查一下package.json”映射成对这个参数的正确赋值。如果描述写成任意字符串模型就会把整个文件内容当作参数传进来不仅浪费token还会破坏后续解析逻辑。Promise.all并行请求确保了扫描效率。真实场景下一个大型项目的依赖可能有四五百个串行请求会把整个插件拖到超时。并行之后几十毫秒就能拿到绝大多数结果。最后返回给模型的是格式化后的文本报告。我刻意不让工具直接返回原始JSON因为模型擅长读自然语言而不是解析嵌套结构。格式化的重点是风险等级放最前受影响函数和修复建议紧随其后优先级一目了然。3.4 实现第二个工具重构影响面评估第二个工具解决的是重构场景。模型在代码库里找到一段需要重构的函数但不确定改完会影响哪些调用方。我的工具会做一个静态调用链分析粗略估算影响范围。// src/tools/refactor.ts export function registerRefactorTools(server, config) { server.tool( estimate_refactor_impact, { sourcePath: z.string().describe(待重构源文件路径), functionName: z.string().describe(需要重构的函数或方法名), }, async ({ sourcePath, functionName }) { const graph await buildCallGraph(sourcePath); const impacted graph.findImpactedFiles(functionName); const tests findRelatedTests(impacted); return { content: [ { type: text, text: describeImpact(sourcePath, functionName, impacted, tests), }, ], }; } ); }这个工具的实现难点在于调用图构建不能太重。如果用上完整的AST分析库处理一个中型项目可能要花好几秒体验很差。我的方案是走“正则启发式”的轻量路径先提取文件间的导入关系再通过函数名匹配找出直接和间接引用点。这样牺牲了一部分准确性但换来的是几十毫秒的响应速度。权衡是可以接受的。这个工具的目的是给模型一个“初步判断”让它在改代码之前心里有数。真正精确的影响面评估完全可以交给CI流水线里的静态检查工具去做。在AI辅助编程的场景里速度带来的交互价值远大于一点点的精度提升。3.5 接入Claude CodemcpServers配置与本地联调工具写完最后一步是让Claude Code认识这个插件。Claude Code的MCP配置放在项目根目录的.mcp.json里我是这样配置的{ mcpServers: { yio: { command: node, args: [dist/index.js], env: { YIO_API_BASE_URL: https://api.internal.example.com, YIO_API_KEY: xxxxxxxx } } } }这段配置的意思是Claude Code会在需要调用yio工具时用node dist/index.js拉起一个子进程通过标准输入输出通信。env字段会把密钥和API地址注入到插件的进程环境变量里。联调时有个特别坑的细节改完配置后必须重启Claude Code会话它不会自动重新加载MCP服务。我一开始改完配置直接在会话里继续聊结果模型始终报“工具不存在”浪费了大半天。后来养成习惯每次改完代码npm run build然后重启会话再验证。本地联调还有一个神器叫MCP Inspector用npx modelcontextprotocol/inspector就能启动。它提供一个可视化界面可以手动给插件发各种请求、看原始协议消息。我在开发阶段几乎每个工具都先用Inspector过一遍确认请求响应完整了再回Claude Code里做端到端验证。这一步能帮你把“插件逻辑问题”和“模型调用问题”快速区分开。4. 工程化闭环部署、评测与迭代4.1 用MCP Inspector做协议级调试很多开发者写完工具插件只会在Claude Code里随口问一句“你能不能扫描一下我的package.json”看到结果就跑根本不检查底层的协议通信。这样出了Bug完全不知道是模型不会调用还是工具逻辑有错。MCP Inspector的价值在于跳过模型直接和工具对话。你可以手动输入一个工具名和参数看它返回什么也可以查看原始JSON-RPC消息的每一帧确认参数解析是否命中了预期结构。我在调试scan_dependency_risks时就发现过一个问题Inspector里传入的manifestPath是带引号的字符串而模型有时候会传入不带引号的路径。如果没有这一层调试这种问题只有在真实对话里随机出现排查成本极高。使用Inspector还有一个技巧把environment参数设置成和Claude Code完全一样的配置包括环境变量和启动命令。这样能保证你在Inspector里验证通过的行为在Claude Code里也是可复现的。我见过有人因为两边配置不一致导致调试半天没问题、一上真实环境就崩。4.2 自建回归评测集从“能跑”到“靠谱”工程化最容易被忽略、但价值极高的一步是给AI工具建一个回归评测集。模型不像普通程序改一行代码可能让它在20%的场景下表现突变。没有评测集你根本无法判断一次改动是优化还是退化。我花了点时间建了一个小评测集包含20个典型请求分布在四个维度常规请求比如“扫描我的package.json”“估算修改logger.js里init函数的影响”边界请求比如依赖清单文件不存在、函数名拼写错误、版本号缺失恶意/异常请求比如路径指向/etc/passwd、参数包含超长字符串多轮上下文请求在前面对话基础上追加请求看工具能否正确复用上下文。每次代码改动后我会跑一遍评测集记录三个指标工具调用成功率、平均响应时延、返回文本的token膨胀率。成功率不用解释时延直接影响交互体验token膨胀率则代表这条工具输出占用了多少模型上下文。膨胀率过高会压缩后续对话的可用窗口导致模型“忘记”早期指令。我最终给自己定的及格线是成功率不低于85%P95时延不超过5秒单条工具返回不超过1500 token。这套指标不一定适合所有项目但“用可量化的指标来约束AI行为”这个思路应该成为AI工程的默认动作。它把“我感觉变好了”变成了“评测集上成功率从82%提到了91%”这两者的可信度完全不同。4.3 发布策略与版本管理别急着打1.0插件开发完要上线给团队用版本策略就得跟上。我采用的是语义化版本号规则很简单修复一个Bug比如API地址拼错、某个边界场景抛异常涨patch比如0.1.0到0.1.1新增一个不影响已有工具行为的工具或参数涨minor比如0.1.1到0.2.0改动工具的参数结构或返回格式导致旧版Prompt无法正常调用涨major哪怕只是0.x到0.y也要遵守这个约定。这个策略的核心逻辑是MCP插件的“接口契约”是模型通过Prompt和工具schema建立的。一旦工具的参数或返回结构变了模型中缓存的对这个工具的理解就失效了表现为“之前用得好好的突然不会调了”。所以任何破坏契约的改动都必须是显式的、大版本的、需要人工确认的。发布方面我打包成了npm包团队同学在项目里通过.mcp.json直接指向安装后的dist/index.js。这样升级时只需要换依赖版本号不需要大家手动改路径。早期版本我尝试过把源码直接放在共享盘让大家clone后来发现版本混乱问题频出果断放弃。发布工具插件也是一等工程事物该走流程就走流程。4.4 避坑清单五条真金白银的经验这部分是我最想写的内容。以下是这个项目过程中真实踩过的坑每个都耽误了至少半天希望你看完能直接绕过去。坑一工具参数的描述写得太宽泛。最初我把manifestPath描述成“manifest文件相关信息”模型就会有时传路径、有时传文件内容、有时传目录名。改成“依赖清单文件的绝对路径例如/src/app/package.json”之后几乎不再出错。模型对参数的理解完全取决于你在schema描述里给出多少约束和示例。描述越具体模型行为越稳定。这是AI工程里投入产出比极高的一件小事。坑二stdio传输时日志污染协议。插件进程的标准输出是用来传MCP消息的如果你在代码里顺手console.log一条调试信息这串字符会混进协议流直接把通信干崩。我之前就因为一个调试日志没清理导致Claude Code隔几分钟就报协议错误排查了很久。正确的做法是所有日志写入独立文件或者走标准错误输出stderr。这个教训对任何基于stdio的插件都适用。坑三密钥写进配置文件被提交到Git。有一次我把API Key写进了.mcp.json的env字段忘了加忽略规则差点推到公共仓库。以后我的做法是.mcp.json只作为模板提交真正的密钥放在~/.yio/config.json并用gitignore排除.mcp.json里通过env字段显式指向本地配置文件。这样既方便团队协作又不会泄密。坑四改完代码忘了重启会话。Claude Code对MCP插件的加载是“启动时快照”修改插件代码不会热更新。我至少碰到五次改完代码以为没用后来发现是没重启。最稳妥的做法写一个小脚本一键完成build、杀掉旧进程、重启会话把操作成本降到最低你才更容易每次都做对。坑五返回给模型的结果塞了太多噪声。早期版本我把漏洞库返回的原始JSON几乎原封不动地塞给模型结果上下文窗口被大量无关字段占满模型反而抓不住重点。后来我加了一个格式化层只保留“风险等级、影响版本、修复版本、一句话描述”效果立竿见影。AI工程的上下文管理不只是“给多了会超限”的问题更是“给对了模型才能答对”的问题。5. 做完这个项目之后再看AI工程的全貌5.1 输入端与输出端可控性是一等公民做完这个插件之后我再去看各种AI系统第一反应就是看它的输入和输出两端是否可控。输入端模型能拿到哪些上下文、能调用哪些工具、参数结构是什么必须在协议层面用schema和权限锁死。就像你不会让一个实习生直接访问生产数据库并执行任意SQL一样你也不该让模型在没有任何约束的情况下去调用内部工具。MCP这类协议本质上就是给模型画了一个“可以做/不可以做”的操作边界。输出端模型返回的内容要尽量结构化、可验证、可降级。我的插件每次返回前都会做一次格式校验发现结果字段异常就降级成普通文本提示而不是把一个坏结构抛给上层。这样即使模型调用出错用户得到的也是一个可理解的错误信息而不是一坨解析不了的乱码。可控性不是一个锦上添花的设计而是AI系统能不能放心交给用户的关键。5.2 可观测性给AI系统装上仪表盘这个项目里我花了不少精力在事件上报上每次工具调用的耗时、成功失败、耗时分布、模型传参是否命中预期结构。这些数据看起来不起眼但它直接决定了后续优化方向。举一个具体例子。通过上报数据我发现estimate_refactor_impact这个工具的平均耗时是scan_dependency_risks的三倍而调用成功率只有78%。进一步排查发现大部分失败都来自函数名匹配不到调用点。于是我把匹配逻辑从“精确匹配”改成了“精确匹配模糊匹配双通道”成功率一下提升到了91%。如果没有可观测的数据这种优化完全靠猜效率极低。可观测性的核心原则是每次请求都留痕每条错误都可归类每个指标都可对比。只要做到这三点AI系统就不再是不可捉摸的黑盒而是可以持续打磨的工程产品。我见过很多团队把AI应用上线后就不管了出了问题时只能靠用户反馈去猜这其实是工程意识的缺失。5.3 AI工程师的能力成长路径从小工具到平台回顾从设想到落地的整个过程我更坚定了“AI工程师能力是在小项目里长出来”的看法。你不需要先精通分布式系统、再掌握模型原理才能碰AI工程。从一个给IDE装“眼睛”和“手”的小插件开始你就能接触到AI工程最核心的命题如何理解模型的行为边界如何设计工具接口如何评估效果如何用数据驱动迭代。随着经验积累你自然会往更多方向扩展接入更多工具类型、优化上下文策略、引入更复杂的评测体系甚至把手上的MCP插件扩展成支撑团队的平台服务。但起步阶段成本最低、反馈最快的方式永远是做一个你每天都在用的小工具。这个工具会让你对AI工程的真实复杂度产生敬畏也会让你在谈论Agent、RAG这些概念时不再是纸上谈兵。按照我个人的经验做AI工程最忌讳的是一开始就想搭一个面面俱到的平台。找个小场景做深做透比画一张宏大的架构图有用得多。你需要的不是更多的理论而是把一个真实问题从发现到解决完整地走一遍。这个过程会逼你把协议、配置、评测、发布、可观测性这些环节全部过一遍而恰恰是这些环节构成了AI工程区别于普通脚本开发的全部厚度。如果你也想试试不要犹豫从你最常用的AI工具里找一个痛点给它写一个MCP插件。这个周末就可以开始。
返回列表