第一次起念头把 KataGo 最新那版 Transformer 网络可视化出来,是在翻 cpp/neuralnet 目录的时候。文件数量其实不算夸张,可每读几行就会撞上一个不熟悉的名字:被拆开的 QKV 计算、和卷积主干缠在一起的残差分支、还有一层套一层的自定义算子。单独看每一段都能明白,合起来却拼不出一张完整的图。后来我用 Trae 挂上 GLM 模型,把“读源码、抽取结构、生成可视化”这条链路压缩到了 55 分钟。
这个数字看起来像个标题党,但它真正说明的事情是:当模型结构可视化被拆成几个可以单独验证的小步骤之后,它的耗时是可以被量化、也可以被压缩的。这篇文章想聊的不是“哪个工具更快”,而是这条流水线为什么成立、中间哪些环节最容易骗过你、以及换一个模型之后你能不能把它复用起来。工具会换,模型会升级,能留下来的只有你对“结构”这件事的理解方式。
1. 先搞清楚难点:读得懂每一行,拼不出一张图
很多人以为理解一个大模型的结构,难点在于数学。真动手读 KataGo 这类工程化项目就会发现,数学反而是最规整的部分,真正耗人的是“代码组织方式和人的认知方式不匹配”。一个 Transformer 块在论文里是几个方框加箭头,在工程代码里却散落在构造函数、配置文件、算子实现和推理后端四个地方。你在脑子里要同时维持这四份信息,还要保证它们对得上,这才是那 55 分钟之前、大多数人卡住几个小时的原因。
1.1 代码分层带来的认知断层
KataGo 的工程结构大致可以分成三层:C++ 写的引擎与推理、Python 写的训练侧、以及描述网络的配置文件。三层之间不是一一对应的。同一个概念,在配置里叫一个名字,在 C++ 构造函数里叫另一个名字,到了训练脚本里可能又换了个写法。
这就带来一个很实际的问题:你在配置文件里看到 numBlocks 这类参数,以为已经掌握了主干深度,结果读构造函数才发现,块的内部还按 bottleneck 结构再拆了一层;你顺着 trunkChannels 往下追,会发现通道数在注意力分支和卷积分支里走的不是同一条线。
人的短期记忆容量有限。当你要在三个文件之间来回跳转才能确认一个连接关系时,读到第五个块,前面四个块的结构印象已经开始模糊了。这不是能力问题,是信息组织方式的问题。手工读源码做结构理解,本质上是在用工作记忆去模拟一个本该外化的数据结构。
1.2 Transformer 在围棋引擎里不是标准件
第二个坑更隐蔽。如果你拿一张公开的、标准的 Transformer 结构图去对照 KataGo 的网络,会发现对不上,而且不是细节对不上,是整体拓扑就不一样。
原因在于,围棋引擎里的 Transformer 很少是“原样照搬”。它通常是嵌在卷积主干里的,注意力的输入来自卷积特征,输出又汇回卷积路径;位置信息的处理方式、归一化层的位置、残差连接的作用范围,都可能为了推理性能做过调整。有些实现还会把多头注意力的 Q、K、V 拆成独立的算子,以便在不同后端上分别优化。
这意味着什么?意味着通用结构图在这里只能当参考,不能当依据。你真正需要的那张图,必须从这份代码、这个版本里长出来。这也解释了为什么“可视化”这件事不能靠搜一张图解决,它有很强的版本绑定属性。
1.3 手工画图的隐性成本
假设你耐着性子读完,用绘图工具画了一张图。接下来会发生三件事。
第一,代码一更新,图就作废。网络结构在活跃项目里是会变的,改一个块的内部连接,你那张图可能有一半要重画。第二,图无法被复核。同事看到你的图,没办法快速验证“这个箭头到底存不存在”,只能选择相信或者重新读一遍源码。第三,图里丢失的信息你意识不到。手工画图时,人会不自觉地简化,把“不太确定”的连接画成一根含糊的线,而这些含糊的地方往往正是你理解最薄弱的地方。
注意:手工整理的结构图最大的问题不是不准确,而是“不准确的地方看不出来”。一张图如果没有任何可追溯的来源标注,它就没法被修正。
把这三个难点放在一起看,结论就比较清楚了:需要被自动化的不是“画图”这个动作,而是“从源码到结构化描述”这一段。画图反而是整条链路里最简单的一环。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把“可视化”拆成三段可验证的流水线
我后来把这件事重新定义了一遍:它不是一个“让 AI 画一张图”的任务,而是一条三段式流水线。每一段都有明确的输入、明确的产物、明确的验证方式。这个拆法看起来朴素,但它是 55 分钟能成立的根本原因——因为每一段出问题时,你能立刻知道问题出在哪一段,而不是对着最终那张图猜。
2.1 定位:先确认网络定义的真实入口
第一段只做一件事:确认这个版本的网络结构,究竟由哪几个文件、哪几个函数定义。
这一步千万不要跳。很多人一上来就让工具“分析这个仓库的网络结构”,结果工具读了一堆不相关的文件,输出一份似是而非的东西。AI 工具在仓库里找入口的能力,取决于你给的线索够不够具体。
我的做法是先自己粗略看一遍目录,确认几个候选位置,然后用非常具体的问题去问:
- 这个仓库里,推理侧的神经网络是在哪个目录下定义的?
- 网络主干的构造函数在哪个文件?
- 配置文件里的哪些字段会改变网络拓扑,哪些只影响推理参数?
这三个问题问完,你对“信息边界”就有了概念:哪些是要画进图里的,哪些是不该画进去的。
这里有个容易被忽略的细节:训练侧和推理侧的网络定义可能不是同一份代码。训练框架里可能有更完整的模块划分,而推理侧为了性能做了融合和简化。你要可视化的到底是哪一侧,必须在第一步就说清楚,否则后面全是返工。
2.2 抽取:让模型输出结构化的中间表示
第二段是整条流水线的核心:把源码里的结构,转成一份结构化的中间表示。
这一步之所以关键,是因为它把“理解”和“呈现”解耦了。中间表示是一份数据,可以是 JSON,可以是表格,重点是它有固定字段、可以被检查、可以被 diff、可以被复用。而一张图只是这份数据的一种渲染结果。
我一般会要求中间表示至少包含这几类信息:
| 字段 | 含义 | 为什么必须有 |
|---|---|---|
id |
节点唯一标识 | 后续画边、查错全靠它 |
type |
算子或模块类型 | 区分卷积、注意力、归一化、残差 |
inputs |
上游节点 id 列表 | 决定图的拓扑,最容易出错 |
params |
关键参数(通道数、头数等) | 区分“结构相同但规模不同”的块 |
source |
对应的源码位置或配置字段 | 方便回查,避免图变成孤证 |
source 这个字段经常被省掉,但我建议一定留着。它是这张图唯一的“可追溯性”来源。当你后面发现某个连接画错了,能顺着它回到源码确认,而不是重新读一遍整个文件。
2.3 渲染:把中间表示交给绘图工具
第三段最轻。拿到 JSON 之后,用脚本生成图形即可,Graphviz、Mermaid、甚至自己拼 SVG 都可以。因为输入是结构化数据,渲染逻辑非常短,改起来也快。
这里有个实践上的建议:渲染脚本要和你对结构的理解分开维护。第一版图不好看很正常,你可以反复调布局,但只要中间表示没变,调布局就不会引入新的错误。反过来,如果你直接手写图的描述文件,改布局的同时很容易顺手改错一条边。
2.4 这条流水线真正可复用的地方
把三段拆开之后,你会发现每一段都可以单独替换:
- 定位阶段,可以换成你自己读,也可以换成别的工具;
- 抽取阶段,可以换模型,也可以人工整理;
- 渲染阶段,可以换任何你顺手的图形库。
这带来一个很实际的收益:换一个项目、换一个模型版本时,你不需要重新发明方法,只需要重跑一遍流水线。这次是 KataGo 的 Transformer,下次换成别的网络,定位的对象变了,抽取的字段可能要加几个,渲染脚本基本不用动。
我个人更看重的是第二段的产物。图看完就过去了,但那份 JSON 可以进版本库,可以在下次结构变更时做 diff,可以直接拿去做文档配图。它才是这条流水线沉淀下来的东西。
3. 实操:55 分钟里每一步具体做了什么
说完整体的框架,下面把实际操作摊开讲。需要提前说明的是,KataGo 的目录结构、配置字段名和算子命名会随版本变化,下面出现的文件名和字段仅作为示例结构,落地前请以你本地仓库的实际内容为准。这一点很重要,不要直接照抄字段名去匹配。
3.1 环境与版本确认
先做两件最基础的事:把仓库拉下来,并且记下你当前所在的提交。后面所有的结构描述都以这个提交为准,图上也应该标注它。
bash复制git clone https://github.com/lightvector/KataGo.git
cd KataGo
git log -1 --oneline
然后在 Trae 里把这个目录作为工作区打开。这里有个经验:尽量让工具看到完整的仓库,而不是只丢几个文件给它。结构理解需要跨文件跳转,上下文不完整时,模型很容易用“看起来合理”的东西填补空缺,而这类填补通常就是错误的来源。
如果仓库太大导致上下文被截断,退一步的做法是先把 cpp/neuralnet 这一层单独整理出来,再让工具在这个子集里工作。范围小一点,反而更准。
3.2 第一轮对话:只做定位,不要求产出
第一轮我通常不要求任何代码或图,只问定位问题。比如:
- 这个仓库里定义神经网络主干的构造函数在哪个文件?
- 网络配置里哪些字段会影响块的拓扑结构?
- Transformer 相关的模块分布在哪些文件?
这一轮的目标不是拿到答案,而是拿到“候选范围”。模型的回答需要用你自己扫一眼目录来交叉验证。如果它给出的文件明显和目录结构对不上,说明它没读到正确的位置,这时候继续往下问只会放大错误。
我还习惯在这一轮明确一件事:这个网络的结构是配置驱动的,还是硬编码的。这两者的处理方式完全不同。配置驱动意味着不同 .cfg 文件会得到不同拓扑,你的可视化必须绑定到具体的配置文件,而不是笼统地说“这个网络长这样”。
3.3 第二轮对话:逐块抽取,产出 JSON
第二轮才开始要产物。这里的技巧是“分块要,不要一次性要全图”。
一次性要求输出整个网络的完整结构,模型很容易在中后段开始走样:前面的块描述得很细,后面的块开始用“类似的重复结构”带过。分块抽取则可以逐段校验。
一个可用的中间表示大致长这样:
json复制{
"commit": "示例提交号",
"config": "示例配置文件",
"nodes": [
{
"id": "stem_conv",
"type": "conv",
"inputs": [],
"params": { "channels": 384 },
"source": "cpp/neuralnet/示例文件:示例行号"
},
{
"id": "block0_attn",
"type": "attention",
"inputs": ["stem_conv"],
"params": { "heads": 12 },
"source": "cpp/neuralnet/示例文件:示例行号"
}
]
}
注意 source 字段我写了“示例文件”,就是提醒你:真实路径必须自己填,不要让模型编。你可以让它输出“在哪个函数附近”,然后你手工补准确的路径。宁可慢三十秒,也别留一个假的引用。
抽取阶段的推进顺序建议是:先主干,再分支,最后补残差和归一化。主干理清了,分支的挂载点才有意义;反过来先抠分支细节,很容易在没有整体骨架的情况下陷入局部。
3.4 渲染与第一轮人工校验
拿到 JSON 之后,渲染脚本很短。用 Graphviz 的话,核心逻辑就是把节点和边铺进去:
python复制import json
from graphviz import Digraph
with open("model_graph.json", encoding="utf-8") as f:
data = json.load(f)
dot = Digraph(comment=data.get("config", "model"))
dot.attr(rankdir="TB")
for node in data["nodes"]:
label = f'{node["id"]}\n{node["type"]}'
dot.node(node["id"], label)
for node in data["nodes"]:
for src in node["inputs"]:
dot.edge(src, node["id"])
dot.render("model_graph", format="png", cleanup=True)
第一版图出来之后,不要急着美化。先做三件事:
- 数块数。配置里写的块数,和图上画出来的块数是否一致。
- 查断点。有没有节点没有任何入边(除了输入),或者没有任何出边(除了输出)。
- 抽查三条边。随便挑三条连接,回到源码确认。
注意:这三件事里,第二件最容易发现问题。一张图里如果出现了“孤岛节点”,八成不是网络设计如此,而是抽取时漏了连接。
3.5 把结果落成可交付的东西
最后一步是把图和中间表示一起归档,并且标注清楚:基于哪个提交、哪个配置文件、由谁在什么时间生成。这一步看着琐碎,但它决定了这张图半年后还有没有用。
如果这张图要给团队看,我建议在图旁边直接放上 JSON 的路径和渲染脚本的路径。这样别人质疑某条边的时候,可以自己去改数据、重新渲染,而不是来找你问。可复现的结构图,比好看的结构图有用得多。
4. 时间账本:真正耗时的从来不是生成代码
55 分钟这个数字,如果拆开看,分布其实很不均匀。粗略估计:定位和确认大概占了三成,抽取和逐段校验占了一半以上,渲染脚本和出图加起来不到一成。也就是说,工具真正“替我写代码”的时间非常少,绝大部分时间花在“确认它说的对不对”上。
这不是缺点,恰恰是这类工作流能成立的原因。如果工具的输出无法被验证,那它省下的时间会以返工的形式加倍还回来。
4.1 读与验证占了大头,这很正常
我一开始也期待“一句话生成整张图”,试过之后发现不现实。原因在于,结构理解这件事本身就有验证成本,而这个成本无法被工具消除,只能被工具降低。
降低的方式是让验证变便宜。具体来说:
- 把大问题拆成小问题,每个小问题都能用一次肉眼检查确认;
- 让每个中间产物都有明确的来源标注,方便回查;
- 让错误尽早暴露,比如先画主干再补分支,主干错了立刻能看出来。
这三条本质上都是在“降低单次验证的成本”,而不是“减少验证的次数”。想清楚这一点,你对这类工具的预期就会比较健康。
4.2 AI 工具擅长什么、不擅长什么
从这次的实际体感看,边界大致是这样的:
| 环节 | 工具表现 | 说明 |
|---|---|---|
| 定位文件与函数 | 较好 | 给足上下文时准确率不错,但仍需人工交叉验证 |
| 归纳重复结构 | 很好 | 识别“这是同一类块的重复”很省时间 |
| 逐行精确转写 | 一般 | 长文件容易漏,建议分块进行 |
| 判断配置字段语义 | 较弱 | 字段名相似但含义不同时容易混 |
| 生成渲染脚本 | 很好 | 逻辑简单,几乎不用改 |
| 保证结构完全正确 | 不擅长 | 必须人工校验,这是硬约束 |
这张表的意思是:把它当成一个高效的“第一遍读者”,而不是“最终答案的提供者”。第一遍读得快、能抓大结构,这已经很值钱了;但它读完之后,关键连接还是得你自己确认。
4.3 三个最容易翻车的点
第一个是训练侧与推理侧混淆。前面提过,但值得再强调一次。训练代码里模块划分更细、更贴近论文;推理代码为了性能做了融合。如果你没指定清楚,模型可能两边各取一点,拼出一个现实中不存在的结构。
第二个是忽略配置驱动。网络有多少块、通道多宽、注意力几头,这些在很多实现里是配置决定的,不是写死在代码里的。如果你只看代码不看配置,画出来的图就是一个“可能的实例”,而不是“这个配置下的实例”。这一点必须在图的元信息里写清楚。
第三个是把图当成精确规范。可视化图表达的是拓扑和层级,不是张量形状的完整推导,也不是性能特征。拿它去指导算子级优化,大概率会失望。它的正确用途是:帮助建立整体认知、辅助代码 review、给文档配图。
4.4 出问题时的排查顺序
结构图画歪了,不要对着图硬看。按这个顺序查,通常几分钟能定位到层:
- 先看现象:是块数不对、连接缺失,还是层级混乱?三种现象指向完全不同的原因。
- 再看输入:你告诉工具的仓库范围、提交号、配置文件,是否一致?范围给窄了,它只能猜。
- 再看上下文:工具是否真的读到了关键文件?大仓库被截断是常见情况。
- 再看参数:配置文件里的可变参数是否被纳入了抽取?块数、通道数、头数都属于这一类。
- 最后看工具边界:超长文件的精确转写、相似字段的语义区分,这些是已知的弱项,遇到就人工补。
这个顺序的逻辑是“从近到远”:先排除你自己给错信息,再怀疑上下文,最后才归因于工具能力。实际经验里,前两步能解决掉大部分问题。
5. 从一次可视化到一套可复用的模型理解方法
做完这次之后,我最大的收获不是那张图,而是一个可以反复用的套路。图是消耗品,方法是资产。下次换一个网络、换一个框架,我还是会走同样的三段:定位真实入口、抽取结构化中间表示、渲染并校验。变的只是细节,不变的是“每段都有可验证产物”这个原则。
5.1 中间表示才是可积累的东西
我建议把中间表示当成正式的工程产物来对待,而不是过程中的临时文件。理由有三个。
第一,它可以 diff。网络结构变更时,两份 JSON 一对比,改了什么一目了然,比看代码 diff 直观得多。第二,它可以被多人复核。别人不需要重新读源码,只需要检查数据。第三,它可以喂给不同的渲染器。今天要一张纵向流程图,明天要一张分组视图,数据不用重做。
更进一步,如果你的团队经常需要讨论某个网络的结构,可以考虑把中间表示的字段规范固定下来,形成一个小约定。字段不用多,但 id、type、inputs、params、source 这几个最好别省。
5.2 适用边界:什么情况别这么干
这套方法不是万能的,几种情况下我会选择别的路径。
如果只是想了解一个概念层面的结构,比如“这个模型大概分几段”,那直接看论文或官方文档更快,没必要搭流水线。如果要做的是性能分析,那可视化帮助有限,直接上 profiling 工具更实际。如果代码规模很小,一两个小时能读完,人工整理反而更可控。
它真正适合的场景是:代码规模大到超出短期记忆容量、结构又会随版本变化、并且你需要把这个理解交付给别人。三个条件里满足两个,这套流水线就有价值。
还有一个前提条件容易被忽略:你得对这个领域有基本的判断力。工具给出的结构,你要有能力看出“这里不对劲”。如果完全没有背景知识,验证环节就形同虚设,最后拿到的图看着挺完整,实际上错在哪都不知道。
5.3 给不同阶段读者的下一步
如果你刚开始接触这类项目,我的建议是先别急着自动化。手动读一遍主干构造,把块的数量、通道的变化、残差的连接方式在纸上画个草图。这个过程很慢,但它建立的是你对“这类结构长什么样”的直觉,后面工具输出对不对,你才有判断依据。
如果你已经能读懂代码,只是嫌整理麻烦,那可以直接上三段式流水线。重点放在中间表示的字段设计上,渲染反而是最不重要的部分。字段设计好了,图和文档都是顺手的事。
如果你是要把这个能力沉淀到团队里,那值得多做一步:把中间表示的 schema、渲染脚本、以及一份“如何校验”的检查清单一起放进仓库。这样下次有人要做同样的事,起点就不是零。
回到最开始那个 55 分钟。它不是什么效率奇迹,它只是把一件原本混在一起做的事,拆成了三件可以分别验证的事。真正让时间被压缩的,是“每一段都知道自己在干什么、错了能立刻发现”,而不是某个工具特别聪明。工具会继续迭代,模型会继续升级,但“先把问题拆成可验证的小步”这个习惯,放到哪个技术栈里都不会过时。
