.. Kenneth Lee 版权所有 2026 :Authors: Kenneth Lee :Version: 0.1 :Date: 2026-07-25 :Status: Draft 一个MCP服务程序的架构分析 ************************* 介绍 ==== 最近和一个同事讨论AI/敏捷编程的架构控制问题。原始问题是我提出设计文档是没法持 续保持和代码一致的,所以每个文档保证在某个起点有效,对之后的设计有指导作用就够 了,他不同意,他认为代码和文档应该时刻(至少在一定的时间窗之内)保持一致,给了 我他的一个程序,说明他的维护策略。 我的程序例子是这个: `redis-gqm `_\ ,这个项目传输 层设计的三个版本升级: * `gqm传输层设计 `_ * `gqm传输层设计v2 `_ * `gqm传输层设计v3 `_ 我都是保留在仓库中的,原因是虽然前面的版本已经旧了,但决策历史本身也是代码形成 现在这个样子的原因,如果忽略这些历史原因,重头开始考虑这个代码,这个代码也不是 现在这个样子。所以,保留三个版本,反而能让读者(无论这个读者是AI还是碳基人), 更容易看懂设计和代码。 同事提供的代码是一个MCP服务器,也是开源项目,但他不希望我公开他的工作,所以我 这里隐藏这个代码仓的位置,仅仅讨论我的分析,未来也许他正式对外发布了,可能我会 补上代码仓的位置。 本文主要探讨我对他说的这种策略的认识和对他的代码架构的分析,读者对象主要是这位 同事,但也是我自己记录一个案例,也供对这个主题有兴趣的其他读者参考。 功能介绍 ======== 这位同事的项目(以下简称A项目)代码树中大概有13万行rustc代码(包括测试代码), 核心是一个多语言(当前实现了14种)的语法解释器分析器,它通过tree_sitter(一个 开源的语法分析库)把这些代码(这是代码的大头)解释成CST(Concrete Syntax Tree),然后对CST进行数据建模,写到sqlite数据库中,然后提供cli和MCP接口去访问 这个数据库中的信息。 代码架构分析 ============ 项目的工作量主体是解释语言相关的CST数据,让它们统一成一种内部的数据库表达,这 种表达作者自身肯定是有一定的思路的,但没有文档定义出来,所以,这个思路有没有经 过完整的穷举分析,我判断不了。其实这也是做架构分析的有趣之处:在架构控制初期, 因为事实的特征很少,你不做推理穷举,说“我心里已经推过了”,每人知道这是真的还是 假的,因为这种信息只能Pass Over In Silence,你只能凭人来选择“是否信任”。 而且代码已经在那里了,在代码量有限的情况下,你说你“总体有规律”,这也是无法反驳 的。这个文档的核心定义文档(叫architecture.md),一眼能看出是AI写的,具有明显 的“发明新词”的习惯,这些新词是什么意思,见仁见智,它们会引导AI编程的方向,但它 引导到什么程度,完全看运气。 我能理解为什么同事说这个文档能与代码“同步”,因为这个文档对代码并没有强约束。这 一点他很可能不同意,因为每个人给别人做出了要求,而最终这个要求符合了使用上的期 望,他都会认为自己的意志得到贯彻。但架构意志和功能能满足的意志,是完全不同的。 而且,我认为他触雷的机会也不大,因为他这个代码的构架太简单了:代码的语法逻辑是 被tree_sitter控制的,得到结果后,用这个分析建立一张图,这种图怎么解释,也是有 很大的自由度的,对又如何,不对又如何?只是个参考而已。只要心里有个方向,怎么组 织一张关联图,这怎么都错不到哪里去。最后是把这张图的数据图提供一组查询接口暴露 出来,这能触什么雷? 这种代码量再大,也触不了什么雷的。这个我前面提到的redis-gqm面对的情况完全不同, redis-gqm面对的问题是:用redis调用libgqm,要通讯,总是可以做到的,但我未来要不 同的应用,比如gRPC,基于gRPC的上层业务,广告推广数据库……可以从qm(封装队列, qm_u(qm的upper层,封装RPC),qm_uu(qm的upper upper层,封装会话)分别选择调用。 这个东西就不仅仅有“功能意志”要贯彻了,我需要有“架构意志”要贯彻进去的,这些层, 每层未来遇到具体的生产环境都是要加功能的。到那个时候才发现功能放错层,就无法挽 回了。这种情况下,我就不但要控制redis-gqm功能能不能实现了,我还要控制它的代码 怎么写。 而A项目,其实很小——尽管代码量不小,但有清晰边界的东西很少,从SCP转换到具体的数 据库的逻辑很多,但这个是就事论事的,没有要控制的边界,这就不用控制了。其他控制 是:用tree-sitter控制语法分析,结果用一种主体是图的数据库保存,数据库可以查到 类型,caller,callee的关联关系,最终用MCP标准接口暴露……就这几个控制点了。从这 个角度上说,它就很小了,大部分复杂性,都被标准控制了。 关于代码关系和架构关系 ---------------------- 关于自动生成代码关系这种功能,我个人是一直有很高的期望的。刚工作的时候,Source Insight带来的体验太好了。但开始维护商用的交换机项目,还有社区的Qemu, Linux, Gdb这些大型项目后。很多工具表现也就那样,慢慢我也放弃了想它了。 这次看到这个工具,又让我有了一些期望。但老实说,试用了A项目后,我基本打消了我 对这类项目的最后期望了。 我不是说这个工具做得不好,它做得完全符合我的期望,就是因为它完全符合我的期望, 所以我才不再有幻想。而这一点,我觉得很可能是我和这个同事的核心分歧。 我尝试在我的binutils-gdb代码上用这个A项目分析代码结构,然后我尝试去看看gdb有什 么依赖,使用了什么外部模块,如何组合逻辑…… 没用,这个工具提供了非常详细的引用关系给我了,完全满足我的想象,但没用,我看着 这个东西我都不想看下去。gdb这个分析我以前写过,但由于设计到我们一个内部的CPU, 没有公开,我们可以用这个qemu的分析做类比: :doc:`qemu概念空间分析 <../概念空间分析/qemu/README>` 我们很多人,觉得“代码包含我的当前意志的全部信息”,你会发现,架构分析需要的概念 空间的关联关系,不是代码的关联关系。两个socket程序A, B进行通讯,我们要的是A和B 的通讯协议,我们不要A调用了socket(), send_msg(),B调用了accept(),recv_msg()…… 这个关联不是代码调用关系可以看到的(虽然有参考价值)。 就算是A和B当前的协议,你看见A发了个hello到B,但这是当前的实现,不是我们对它们 的“定性”,比如我要的是A是一个请求者,按需启动,而B是一个服务者,持续后台。 而前面提到的redis-gqm的三次升级,有一次就是对定性的修改,redis-server最初的定 性是一个服务器,但最终新需求决定了它不仅仅是个服务器,还是一个请求者。它具有双 重身份,所有对它的封装就必须修改了。 这些都不是代码关系,而是概念关系。代码关系有用,但非常有限。所以,你总想代码即 代码,或者让文档总代表代码最后一刻的代码的设想,在你的逻辑变大的时候,你就罩不 住了。就算功能简单如gdb,它又是插件,又是脚本,又是换binutils底层,每个其实在 某个时刻可能目的和逻辑都是冲突的,这些你不允许多个冲突文档的存在,你都没法维护。 附录 ==== 我的分析方法 ------------ 我是用AI(具体是copilot pro@auto)分析这个代码的,下面是我的提问列表,你可以看 到我的分析方向: * 我用cloc看了一下代码量,发现有十几万行rust代码,这个代码量主要消耗在什么逻辑 上了?也就是说,这个项目的工作量到底落在什么工作上? * 它这个代码分析的工作主要是在做什么?支持什么语言?用什么算法来把代码翻译成 AST的? * 我关心的是:它靠什么来解释这个语法?我理解每种语言都需要一个至少是编译器的前 端(比如很多人会用LLVM生成IR),有一个统一的入口了,然后统一翻译成CST,这是 一种工作量,如果这个工程自己对每种语言都做一种编译器前端,这又是另一个工作量 了,这里是哪种情况? * 既然语法分析主要是tree-sitter做的,那本项目的代码量消耗在哪里?是因为集成了 tree-sitter的grammar,还是它自己还要做什么特殊的适配? * 所以代码量不是语法解释,而是把不跨语言的CST/AST转化成“可查询语义图谱”,那这 个统一的“可查询语义图谱”的抽象是什么,为什么可以用一种统一的逻辑描述所有代码 的关联关系的? * 项目中有定义这个统一的语法建模吗?我举个例子,如果一个模块import了另一个模块, 但其实没有调用它的任何函数,那我能认为两者产生了关联吗?如果定义了这种原则, 使用这个数据库的人就会知道,数据库中说两者产生了关联,具体表示什么。项目中定 义了这个标准吗?