链路图生成:跨边界调用链追踪与 mermaid 语义学
一次点击,四种语言,两个进程
台账列表页有个查询按钮。点一下,pageList.vue 里的 handleQuery 被触发;它调用 api/money/pageList.js 里封装的 getList,往后台发一个 HTTP 请求;请求落到另一个 Java 工程的 PageController,再钻进 PageServiceImpl 和 PageMapper 接口;真正的 SQL 躺在 resources/mapper/PageMapper.xml 的动态标签里,最后打到 PostgreSQL 的一张视图上。
一条链,穿过 JavaScript、Java、XML、SQL 四种语言、一次网络传输、两个进程。人追这条链靠 IDE 的跳转键,还得祈祷两个工程恰好都打开了。大模型追这条链更艰难:前端工程里只有一个 url 字符串,后端工程里只有一个 @RequestMapping 注解,两边各说各话,中间隔着一次运行时才发生的网络传输。静态分析断在边界上,是逆向工程里最常见的信息塌方点。
链路图生成(skill2,link-diagram)接的就是这个活。输入是上一步功能点提取产出的 function-list JSON,里面登记了每个功能点的 源文件、技术栈 和 相关接口;输出是一份链路图文档,落在 doc/link_diagram/ 目录,命名形如 pbk-pageList-台账列表-链路图.md。文档里每个功能点单独成节:一段业务描述、一张 mermaid 调用链路图、一份按节点提取的代码。
下游的业务需求文档,原材料全部来自这些链路图——skill3 只读链路图,不再回头读代码。这决定了 skill2 的质量标准不是「画得好看」,而是够不够格当唯一事实来源:图上每个节点都要有出处,每个功能点都不能缺席。
三种接缝信号:链路如何跨过边界
跨边界追踪的通用规则一句话就能说完:从入口函数或前端事件出发,找到它调用的 API 或接口,由接口一路追到 Controller、Service、DAO/Mapper、SQL、表,直到可选的外部系统。
真正的问题出在每个边界上。模型怎么知道 getList 这个 JS 函数对应后端的哪个方法?规则给出三种接缝信号:前端 API 函数里的 url 字段、Java 侧的 @RequestMapping 注解、脚本里 request(...) 调用的目标地址。只要在链路的任何一段看到这三种信号之一,就把两边的「文件-方法」对上,链路继续向前延伸。三种信号覆盖了六种技术栈里绝大多数接缝:Vue 工程靠 url 对齐,传统 MVC 靠注解对齐,Python 脚本靠 requests 的目标地址对齐。
主干长这样:
flowchart TD
A[1. 前端入口文件 - 事件函数] --> B[2. api 封装 - url /money/pageList]
B --> C[3. Controller - @RequestMapping]
C --> D[4. Service/ServiceImpl - 业务方法]
D --> E[5. Mapper/DAO - 数据访问接口]
E --> F[6. XML/注解/原生 SQL - 查询语句]
F --> G[7. 表/视图]
G --> H[8. External: SAP GUI 或 OSS 或 第三方 API]
两个工程细节值得单说。第一,每个节点必须记录「文件-方法」二元组和序数,格式是 序号 文件名 - 方法名。序数让链路有了确定的读法,评审时可以说「第 5 步有问题」,而不是「中间某层可能有问题」。
第二,分析范围被硬性收窄:只允许追踪 JSON 里登记的 源文件 与 相关接口 能到达的前后端文件。没有这条限制,模型顺着 import 就能漫游整个仓库,上下文窗口迅速被无关代码填满,还会把兄弟模块的调用混进当前链路。范围收窄是防御性设计——上游 skill1 已经做过一次功能点粒度的裁剪,skill2 只在裁剪过的世界里工作。
还有一个分支情况:纯本地功能和批处理没有后端接口。这时不硬造链路,改为绘制功能自身的内部步骤调用链,节点从「文件-方法」退化为步骤名描述。图的形式不变,语义从「跨进程调用」换成「处理步骤」。下文的示例 B 会看到这种退化形态。
六张地图:模板不是穷举是路由
skill2 没有让模型在空白里自由发挥。它为六种技术栈各准备了一条链路模板:
| 技术栈 | 链路追踪路径 |
|---|---|
| RuoYi + Vue2 + MyBatis | .vue -> api/.js -> Controller -> Service -> ServiceImpl -> Mapper 接口 -> mapper/.xml -> PostgreSQL 表/视图 |
| RuoYi + Vue2 + Hibernate | .vue -> api/*.js -> Controller -> Service -> DAO(extends SimpleHibernateDao) -> 原生 SQL -> 表/视图 |
| Spring Boot + MyBatis 注解 | 前端/脚本 -> Controller -> Service -> Mapper(注解 SQL) -> 表 |
| Spring MVC + Hibernate + ExtJS/JSP | jsp -> Action/Controller -> BP/Service -> DAO(Hibernate/JPA) -> 表,或 Quartz/@Scheduled 任务入口 |
| Python + Selenium/Requests | 脚本函数 -> requests 调用的 HTTP 接口 -> 后端 Controller;可含 Selenium 页面操作、图像识别、文件系统、psycopg2 直连 SQL |
| 定时任务/批处理 | @Scheduled/main/主循环 -> Service -> DAO -> 表;或调用外部服务(SAP GUI/VBS、OSS、第三方 API) |
模板怎么选?不靠现场猜。JSON 里有一个 技术栈 字段,是 skill1 提取功能点时写进去的,skill2 读到它就直接路由到对应模板。技术栈识别的复杂度被前置到上游,每一步只消费上一步的结论,不做重复判断——这是流水线拆步带来的又一层好处。
模板的价值在于给模型一张「链路应该长什么样」的地图。没有地图时,模型画链路图靠对框架的一般印象,深度参差不齐:有的链追到 Controller 就停,有的把 Service 接口和实现类混成一个节点。有了模板,每条链该有几层、每层是什么角色,都有了基准。模板同时承担语义压缩,DAO(extends SimpleHibernateDao) 这一个括号注记,就能区分 Hibernate 和 MyBatis 两种完全不同的数据访问风格。
让图不说谎:五种视觉语义
链路图用 mermaid flowchart 画,但技能为它规定了一套严格的视觉语义,每种图形元素对应一种确定的事实陈述:
| 图形元素 | 语义 | 对应的技术场景 |
|---|---|---|
| 实线边框 | 静态分析确认的调用 | 代码里直接可见的调用关系 |
| 虚线边框 | 反射、动态、运行时推测的调用 | 注解驱动、动态代理,旁注「推测: 方法名」 |
| 实线箭头 —> | 同步调用 | 常规方法调用、HTTP 请求 |
| 虚线箭头 -.-> | 异步/Promise 调用 | Vue 的 await/then、Python 的 Thread/asyncio、Java 的 CompletableFuture/@Async |
| 蓝色边框节点 | SQL 语句 | XML、注解或原生 SQL |
| 灰色背景节点 | 外部系统 | 节点文本以 External: 开头,如 External: SAP GUI |
| 红色虚线框 | 循环依赖 | 标注循环依赖,编号不重复展开 |
这套语义解决的是静态分析的诚实问题。逆向工程里最危险的不是不知道,而是把推测当成确认写进文档。下游的读者——无论是写业务需求的大模型还是评审的人——分不清「这个调用我在代码里看到了」和「这个调用我猜运行时会发生」。边框虚实把两者在视觉上永久分开:实线是证据,虚线是推测,图上每个像素都有确定含义。
异步箭头则是免费的性能线索。-.-> 出现的位置就是链路上可能不阻塞主流程的位置,评审时一眼能看出哪个请求是等待结果的、哪个批处理步骤跑在子线程里。这些信息在纯文字里要读很多行代码才能还原,在图上是一眼的事。
两条真实链路:同一格式装下两个世界
技能文档里给了两个完整示例,一条是典型的跨工程同步链,一条是纯脚本批处理链。
示例 A,Vue 页面查询:
graph TD
A[1. pageList.vue - handleQuery] -.-> B[2. api/money/pageList.js - getList]
B --> C[3. PageController.java - list]
C --> D[4. PageServiceImpl.java - list]
D --> E[5. PageMapper.java - getList]
E --> F[6. resources/mapper/PageMapper.xml - getList SQL]
F --> G[7. pbk.PBK_PAGE 表/视图]
注意首段箭头是虚线:handleQuery 里对 getList 的调用带着 await,属于 Promise 调用;之后从 API 封装到 Controller 是 HTTP 请求,从 Controller 到 XML 是进程内方法调用,全部实线;末端的 pbk.PBK_PAGE 是表/视图节点,链路在数据落点收口。
示例 B,Python 脚本批处理:
graph TD
A[1. 202506TestPg_api.py - 目录监视主线程] -.-> B[2. 新增文件分发]
B --> C[3. wait_for_download_complete 锁等待]
C --> D[4. 解压zip/识别二维码]
D --> E[5. requests 调用 updateWriteoffStatus 接口]
E --> F[6. 后端 Controller/Service/DAO]
F --> G[7. 数据库表/状态更新]
这条链没有前端,也没有 Controller 起点。主线程监视目录,文件到齐后分发给处理步骤:锁等待、解压、二维码识别、requests 调接口、更新状态。节点不再是「文件-方法」,而是批处理步骤名——这正是前文说的退化情形,无后端接口时画内部步骤链。
两个示例共用同一套节点格式、同一套箭头语义、同一个从 1 开始编号的读法。格式统一的价值在下游兑现:skill3 逐节消费这些链路图时,不需要为不同技术栈写不同的解析逻辑,子代理拿到任何一节都知道从哪里找输入参数、到哪里看输出字段。
图下面的证据链:按节点提取代码
链路图不是孤立的画。每个功能点节里跟着一份按节点组织的代码提取:入口函数、API 封装、Controller、Service、DAO/Mapper、SQL 片段、表结构、外部调用片段,小节标题与图里的节点一一对应。以示例 A 的第 6 步为例:
##### 6 resources/mapper/PageMapper.xml - getList SQL
<select id="getList" resultType="map">
select * from pbk.PBK_PAGE t where 1=1
<if test="name != null and name != ''">and t.name like concat('%', #{name}, '%')</if>
</select>
这份代码提取让链路图具备了可下钻的证据链。图上的每个节点都能在下文找到对应实现,评审者可以抽查任意一步:图说第 6 步是 getList SQL,往下翻就是这段 XML,动态标签、模糊查询、参数占位一目了然。反过来,任何对图的质疑都能落到具体代码上对质。没有这一层,链路图只是又一份「看起来可信」的文档;有了这一层,它是带证据的审计记录。
对下游的 skill3 来说,这份代码提取是字段级翻译的直接原料——输入参数表的方法签名、SQL where 参数,输出字段的 SELECT 别名,全都从这些片段里来。图的骨架加代码的血肉,合起来才是完整的中间表示。
N=M:最朴素也最有效的门禁
skill2 的输出要求近乎偏执:JSON 里每个功能点都必须单独生成一节,禁止「等功能点」、禁止「类似流程」、禁止「参照上述」。怎么确认模型没有偷懒?靠一道数量校验门禁。
校验分三步。先数出 JSON 里功能点的总数 N——数的是 "功能点": 后面的字符串字面量,"功能点": [ 这种数组声明不计入,这个细节不写清楚,模型连数数都会数错。再数生成文件里 ### 功能点X 标题的数量 M。最后比对,必须 M = N,不一致就补齐,直到相等。文件末尾输出校验结果:
✓ 功能点数量校验通过:JSON文件功能点数(N) = 生成链路图功能点数(M)
这道门禁的检测成本几乎为零——两个计数、一次比较——却能拦住大模型最典型的失败模式:静默丢任务。长输出里模型悄悄跳过两三个功能点,不报错、不提示,文档看起来依然完整。没有数量校验,这种丢失要等到下游 skill3 发现链路图缺节才会暴露,排查成本高得多。
门禁通过后,控制台打印完成标志:
输出文件:pbk-pageList-台账列表-链路图.md
功能点数量校验:JSON文件功能点数(N) = 生成链路图功能点数(M) ✓
编排协议在这里收口:中文冒号、完整文件名、不含路径。调度器捕获这两行输出,才允许流水线进入下一步。
踩坑清单:每条规则背后都有事故
这一步的技能文档不算长,踩坑记录的密度却不低,四条最典型:
节点内禁止换行。mermaid 节点里写 \n 或 <br/> 追求视觉换行,渲染直接崩掉或节点错位。规则要求压成单行、空格分隔:A[1. pageList.vue - handleQuery] 合法,A["1. pageList.vue<br/>handleQuery"] 非法。本文这几幅示例图遵守的也是同一条规则。
禁止「参照上述」式简化。第十个功能点和第三个长得像,模型很想写一句「流程同功能点3」。禁令写得很死:每个功能点必须带完整的业务描述、完整的链路图、完整的代码提取。理由在下游——skill3 的子代理按功能点拆任务、独立消费每一节,任何「参见」都会让那个子代理拿不到上下文。
超长链路全部展开。一个批处理功能跑三十步,画出来纵向很长。规则明确「全部展开,不简化」:压缩图表的视觉成本,远低于丢失步骤的信息成本,何况这些图的主要读者是程序和评审者,不是随手划过的浏览者。
循环依赖不重复展开。A 调 B、B 又调回 A,展开就是无限递归。规则是红色虚线框标注循环依赖,编号停在第一次出现的位置,不再进入第二次展开。
回看这一步,它做的是给「读代码」立证据标准:追踪规则保证链路不断在边界上,视觉语义保证确认与推测不混淆,代码提取保证每个节点可下钻,数量门禁保证没有功能点被丢弃。四层约束叠完,一张 mermaid 图才配得上当整条流水线的中间表示。
链路图画好之后,下一步是把它翻译成纯业务语言:业务需求生成:把代码翻译成人话的质量工程。而这一步里反复露面的数量门禁、禁令清单,会在LLM 输出质量控制的三层防线里收拢成一套完整的质控体系。