当前位置: 首页 > news >正文

连接断了怎么办:MCP 稳定性调试

Cloud Agent 开发笔记(4):Skill 与 MCP 集成、项目后记

上一篇讲了 Agent 事件如何推到浏览器、数据如何持久化、多会话和中断如何处理。这一篇讲能力扩展层:Skill 系统和 MCP 集成。

V1 验证的是产品形态:由管理员角色集中创建和维护 Skill、配置 MCP 连接,普通用户在对话中调用这些能力,而不需要自己理解背后的技术细节。这个管理模式在业务场景里是成立的。

但 V1 的 Skill 和 MCP 实现和 V2 完全没有继承关系:V1 用 Python 写、靠 XML 解析、塞在 God 类里,V2 是用 TypeScript 从零设计的,参考的是 Claude Code 的架构而非 V1 的代码。V2 继承的是 V1 验证过的管理模式,不是它的实现。

场景也变了。Claude Code 是单用户 CLI 工具,一个人在本地机器上跑自己的技能和工具。Cloud Agent V2 是多用户 Web 应用,管理员创建技能和 MCP 连接,普通用户使用它们但不能看到内部实现。这和 V1 验证的管理模式一脉相承,但 V2 从架构层面实现了它。

技能不止是文件目录:Web 多租户下的技能管理

Claude Code 的技能管理纯靠文件系统:每个技能是一个目录,里面除了 SKILL.md,还可以放脚本、模板、配置文件等辅助文件。加技能的方式很灵活:可以从别人那里拷贝一个目录过来,也可以让 Claude Code 自己根据会话历史总结出一套操作流程、自动生成 SKILL.md 和配套脚本。不管哪种方式,最终都是落在 skills/ 目录下,启动时扫描一遍,读到什么用什么。CLI 用户在自己的机器上操作,文件管理器就能搞定,不需要什么管理界面。

V2 不行。Web 用户不会去服务器上编辑文件,admin 需要一个管理后台来增删改查技能。直接改文件系统也不是不行,但启用/禁用、显示名称、分类 scope 这些元数据需要一个结构化的载体。总不能每次查"当前启用了哪些技能"都去扫描一遍文件系统。

所以加了一层数据库。aac_skill_registry 表存元数据,aac_user_disabled_global_skills 表管用户级禁用。这两张表在上一篇的 ER 图里已经出现过,这里展开讲它们具体怎么用。

文件系统是技能的源码,数据库是技能的注册表。启动时 syncSkillsDirectory() 扫描 data/skills/ 目录下的每个 SKILL.md,解析 YAML frontmatter,把元数据 upsert 到数据库。已经存在的记录不覆盖用户修改过的字段(scope、is_enabled、default_prompt),只更新名字和描述。

aac_skill_registry 表:

字段含义
id 主键
name 技能名(目录名或 frontmatter 中的 id)
display_name 展示名称(frontmatter 中的 name,无则用目录名)
skill_description 技能描述(frontmatter 中的 description)
source 来源:global(管理员维护)或 user(个人创建)
user_full_name 归属用户(global 技能为 system
scope 分类:general(通用)或 workflow(业务流程)
default_prompt 默认提示词(可选,覆盖系统提示词)
is_enabled 全局启用开关(管理员控制)

aac_user_disabled_global_skills 表:

字段含义
user_full_name 用户名
skill_name 被禁用的技能名

联合主键 (user_full_name, skill_name),有记录表示该用户主动关掉了这个技能。管理员关了全局开关(is_enabled = 0),所有用户都不可见;管理员开着但用户在自己的记录里关了一行。这叫两级开关,后面会展开讲。

为什么不全放数据库?有两个原因。第一,技能文件正文在运行时经常被 LLM 通过 Read 工具读取,从文件系统读比从数据库拼字符串自然得多。第二,文件系统方便做基线管理。skills/ 目录存模板的原始版本,data/skills/ 存运行时副本。升级基线技能时,新增或修改的文件同步过去,admin 的配置不受影响。两套目录的分离让"升级基线"和"保留用户修改"不再冲突。

多租户的Skill安全模型

文件系统加上数据库,技能管理框架搭好了。但很快一个问题就冒了出来:用户能不能偷看技能源码?

Claude Code 的安全模型不需要考虑这个。CLI 用户在自己的机器上运行,读什么文件自己说了算。V2 的前提完全不同:一份财务业务技能的 SKILL.md 里可能包含银行对账逻辑、账户字段映射、特殊处理规则,这些是给 Agent 执行用的,不能让普通用户在对话里通过"帮我读一下那个技能文件"就看到全部内容。

输入端拦截?做不到。LLM 执行技能时需要读取技能目录下的脚本文件和数据模板。如果 Read 工具的路径检查直接拒绝所有技能目录,技能就废了:LLM 读不到技能文件,自然执行不了技能逻辑。

所以设计是: LLM 的读取不限制,在输出端做拦截。具体实现在 chat.ts 的 SSE 事件处理里:当 tool_result 事件准备推送给前端时,检查三个条件:操作者不是 admin、工具是 Read 或 Grep(output_mode 为 content 时)、读取路径在技能目录下。三个条件同时满足,把 content 替换为"处理成功"。

LLM 能读到技能源码,所以能执行技能;用户只能看到"处理成功",看不到技能源码。这个机制不依赖 LLM 的自觉性,权限在输出端卡住。

技能文件的安全是两层防护叠加。HTTP API 层是第一层:读技能文件的路由都挂了 adminMiddleware,普通用户没法通过接口直接访问。SSE 内容过滤是第二层:普通用户通过 LLM 的 Read/Grep 工具间接读取时,返回结果被替换为"处理成功"。两层各守一道门:HTTP 层防直接访问,SSE 层防间接泄露。

Skill管理后台:CRUD 的冰山

安全模型落地后,开始搭管理 UI。最初以为就是几个表单和表格,结果越做越多。

功能清单:

  • 技能列表,带两级开关,全局启用/禁用、用户级启用/禁用

  • 技能详情,实时预览 SKILL.md 正文和 frontmatter、在线编辑

  • 导入导出,单个技能 ZIP、批量导出带注册表元数据

  • 文件管理,技能目录浏览、脚本和模板文件在线编辑、新建/重命名/删除

  • 基线技能管理,模板目录的版本控制

后端路由文件 skill.ts 占了一个 57KB 的单文件,是项目中最大的。不是因为逻辑复杂:大部分是 CRUD,增删改查一个技能记录和它的文件。大是因为 Web 管理涉及的操作维度太多。列表筛选要支持 source、scope、is_enabled、user_enabled 四个维度;导入要兼容 ZIP 和单个文件两种格式;导出要支持单个和批量;文件管理要递归遍历目录、提供每个文件的读写接口。

大部分是 Agent 生成后调整量不大的活。但开关逻辑和同步逻辑需要手动梳理清楚,并告诉Agent。比如两级开关,四种组合:

admin is_enabled用户禁用记录用户能否看到该技能
0 无关 不可见
1 无记录 可见
1 有记录 不可见

这个逻辑用 JOIN 查询能搞定,但 Agent 一开始生成的版本有 N+1 查询问题(先查出技能列表,再逐个查用户的禁用记录),手动让它改成 JOIN 查询才解决。


MCP:只做 stdio

MCP 集成是技能系统之后的下一个能力扩展。Claude Code 支持五种 transport:stdio、SSE、HTTP、WebSocket、SDK。V2 当前只实现了 stdio。

不是能力问题,是需求问题。业务上需要接入的 MCP server本次仅需要支持mineru PDF 做解析、数据分析服务,是命令行程序,通过 uvx 或直接调用可执行文件启动,走 stdio 完全够用。其他 transport 的代码没有删,而是留了占位符:每种 transport 都有完整的 Zod schema 定义和类型守卫函数,但 connectToServerImpl() 里非 stdio 的分支直接 throw new Error('not yet implemented')

4 月 30 号专门做过一次 SSE transport 的预研。对比了 Claude Code 源码和三个开源项目(Open WebUI、Continue、Cline)的进程管理方案,结论是需要做 managed 模式:Node.js 进程管理 MCP server 子进程的生命周期,加上 SSE 持久连接的长连接保活。但当前没需求,先不动。配置 schema 和类型都准备好了,时机到了直接填实现。

这个取舍和前面 02 篇的工具删减是同一个逻辑:实现的边界由业务需求画,不由源码能力画。 Claude Code 支持不代表 V2 需要支持,留好接口比做出用不上的功能更有价值。

连接断了怎么办:MCP 稳定性调试

MCP 连接管理是整个项目调试时间最长的部分。4 月 16 号接入 MCP,4 月 25 号一天交了 5 个稳定性相关的 commit。

一开始我试过直接搬 Claude Code 的连接机制。但它俩的运行时前提完全不同:CC 是单用户本地 CLI,MCP 连接生命周期等于用户会话:启动连上,干完活退出,最长几十分钟。子进程崩了无所谓,下次启动 cc 自动重连。uvx 的依赖缓存是持久的,首次下载后永远在本地。CC 只需要"会话级"的连接管理。

V2 是长驻多用户服务端,MCP 连接要一直活着,几天甚至几周。子进程可能自己崩、可能 idle timeout、可能有多个用户同时调用同一个 MCP server。Docker 每次部署清缓存,uvx 每次都要重新下载 60-90 秒的依赖。V2 需要的是"服务级"的连接生命周期管理:一套 CC 几乎没考虑过的稳定性机制。照搬解决不了,必须自己搭,下面按踩坑顺序讲。

第一个坑:并发重连

场景:用户发了一条消息,LLM 返回了 3 个 tool_use,都需要调用同一个 MCP server。3 个 tool 调用是并行的,几乎同时检测到 server 已断开。如果每个调用各自触发重连,会启动 3 个重复的子进程,后面的要么被系统拒绝,要么互相抢占端口。

翻 Claude Code 源码发现它用 memoize 解决这个问题,不过不是常规的 memoize(缓存函数返回值),而是"缓存 Promise 对象"。核心机制:第一次调用 connectToServer('mineru') 时,创建一个 Promise 执行连接逻辑,立即存入 Map。第二次和第三次并行调用发现缓存里有同一个 key 的 Promise,直接返回它,三者等待同一个连接结果。连接完成后根据状态决定清不清缓存:resolve 为 'failed' 或 'needs-auth' 时清除,让后续可以重试;reject 时也清除。

第二个坑:重连进行中又有新请求

memoize Promise 解决了"同时触发重连"的问题。但重连进行中(Promise 还在 pending),又有新请求在这个窗口期检测到断连。按理说新请求复用同一个 Promise 就行。但如果重连在某个时间点失败了、Promise reject 了、缓存被清除,而新请求恰好在这个空窗期判断"断连了",会触发第二次重连。

加了一层 pendingReconnects Map。startAutoReconnect() 开始时先检查是否有同名的重连正在进行,有就直接返回同一个 Promise,不启动新的。两个防重机制是互补的:memoize Promise 防"并发启动",pendingReconnects 防"串行触发"。

第三个坑:uvx 启动延迟

uvx 命令首次运行时要下载 Python 依赖包,60 到 90 秒。本地运行无所谓,但 Docker 每次重新部署都要重新下载,默认的 30 秒连接超时直接失败。

为什么没把 mineru 的依赖直接打进基础镜像?因为 mineru/magic-pdf 依赖 PyTorch,光这一个就 1GB+。基础镜像里只装了轻量 Python 包(pandas、openpyxl、pdfplumber 等),如果把 PyTorch 和 mineru 全家桶全打进去,镜像体积会翻好几倍,每次 CI 构建和推拉镜像都变成灾难。权衡之后选了按需下载:让 uvx 首次启动时拉依赖,Docker 部署慢就慢一点,镜像体积保持可控。

改成 120 秒超时解决了问题,但设计上不优雅:下载超时和协议握手超时应该是两个独立的值,下载慢不代表连接会失败。改进方案写了 design doc,优先级不高一直没落地。uvx 只在首次使用时下载,后续有缓存,生产环境可以预先跑一次预热。

http://www.jsqmd.com/news/984384/

相关文章:

  • 2026年新消息:聚焦可靠标准,深度解析数控龙门铣床销售厂家的选择之道 - 2026年企业资讯
  • concat graph构造
  • 2026上海虹口区黄金回收+白银回收+铂金回收最新行情 大盘同步报价商家 - 沪上贵金属口碑推荐官
  • Flowframes完整教程:从零开始掌握视频插帧技术,让视频流畅度翻倍!
  • 校园二手交易平台---项目验收
  • 消保委提醒:2026上海普陀区黄金回收+白银回收+铂金回收选择这几家更安全 - 沪上贵金属口碑推荐官
  • 告别‘马赛克’边缘:手把手用DeepLabV3+实现图像分割的精细优化(附TensorFlow/PyTorch配置)
  • 2026资质筑基技术赋能深耕实体:融景科技打造花都GEO优化服务标杆 - 广东科技观察
  • 融景科技:花都 GEO 行业标杆,凭双国标资质与自研技术领跑大湾区 - 广东科技观察
  • 主流7z解压工具怎么选:四款产品深度对比与避坑指南
  • 兰州卫生纸批发市场诚信格局分析:区域供应商服务能力与行业趋势观察(2026年) - 优质品牌商家
  • 保姆级教程:在Win11上搞定MySQL 8.0.28安装与配置(附常见报错排查)
  • Python+Django实战|企业会议室预约管理系统:会议室档案、设备管控、在线预约、多级审批、签到核验、超时提醒、使用数据统计
  • 别再手动建库了!Kettle资源库一键初始化脚本(Oracle版)保姆级分享
  • 2026年西安汽车音响改装市场格局与服务机构能力分析 - 优质品牌商家
  • 2026年新发布承德AI搜索服务机构找哪家?深度解析与本地服务商推荐 - 2026年企业资讯
  • 技术拆解:融景 AI.GEO + 智能体双核系统,重构企业 AI 获客逻辑 - 广东科技观察
  • 即将读博的我,决定开始重新学编程...
  • 从“国际消费中心”到“全球AI认知枢纽”——2026年上海企业GEO选型战略指南 - GEO优化
  • 项目启动之相关方分析
  • 2026广州精品搬家公司深度测评推荐|日式精细打包拆装、同城短途、别墅高端搬家一站式攻略 - gzdjxd
  • 2026年香格里拉民宿行业观察:从草原到雪山的住宿新趋势与多维度评测 - 优质品牌商家
  • 构建安全可靠的后端系统:关键技术与最佳实践
  • Java项目安全管理看这篇就够了!
  • 成都木跳板回收与木方租赁市场格局分析:服务主体与行业趋势研究 - 优质品牌商家
  • 如何用Untrunc拯救损坏的MP4视频文件:完整修复指南
  • 猫抓cat-catch终极指南:如何在3分钟内掌握浏览器视频下载技巧
  • Calibre豆瓣元数据插件:让电子书管理告别信息孤岛
  • 别再纠结选哪个了!手把手教你用Qt和C#快速搭建一个简易SCADA监控界面
  • Adobe软件激活革命:GenP 3.0如何用5分钟解锁创意无限