ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Agent构建AI芯片全栈软件地图:从UMD/KMD到知识图谱

用Agent构建AI芯片全栈软件地图:从UMD/KMD到知识图谱 1. 从一颗芯片的诞生说起为什么全栈软件地图比芯片本身更难画一颗 AI 芯片从设计到量产硬件团队交付的是 RTL、网表、GDS而软件团队要交付的是一整套让这颗硅片真正“跑起来”的软件栈。我做过几个芯片项目的软件适配最深的体会是芯片流片回来能点亮只是起点真正决定这颗芯片能不能被客户用起来、能不能上量靠的是软件栈的完整度和成熟度。而“全栈软件地图”这件事本质上就是把这套软件栈的每一层、每个模块、每个接口、每个依赖关系都梳理清楚让团队知道“我们有什么、缺什么、先做什么、后做什么”。这次我想聊的是用 Agent 来做这件事——不是让 Agent 去写驱动代码而是让 Agent 帮我们把一颗 AI 芯片的全栈软件地图给“画”出来。听起来有点抽象我换个说法你手里有一堆零散的文档、代码仓库、接口定义、测试用例、团队聊天记录甚至是一些口口相传的“坑”你要把这些东西整合成一张结构化的、可查询、可追溯、可演进的软件地图。传统做法是拉一个架构师花几周时间画 PPT但问题是芯片软件栈太复杂了从 UMD 到 KMD 到 Runtime 到 Compiler 到 Framework每一层都在快速迭代PPT 画完就过时了。Agent 在这里的价值不是替代架构师而是做一个“持续在线的地图维护者”。它可以帮你从代码仓库里自动提取模块依赖从文档里抽取接口定义从 issue 里归纳已知问题从测试报告里统计覆盖率然后把这些信息组织成一张活的、可查询的地图。你问它“UMD 层的内存管理模块依赖哪些 KMD 接口”它能直接给你答案而不是让你去翻三个仓库和五个文档。这篇文章适合几类人看一是正在做 AI 芯片软件适配的工程师尤其是负责架构梳理和模块拆解的二是对 Agent 应用开发感兴趣、想找一个真实复杂场景练手的开发者三是技术管理者想了解怎么用 Agent 提升团队的知识管理效率。我会从整体设计思路讲起然后拆解核心细节再给出一套可复现的实操方案最后分享我在这个过程中踩过的坑和总结的技巧。2. 整体设计思路为什么用 Agent 而不是传统文档工具2.1 传统软件地图维护的三大痛点在讲 Agent 方案之前先说说传统做法为什么不行。我经历过三种典型的软件地图维护方式每一种都有硬伤。第一种是静态文档比如 Confluence 上的架构图、Word 里的接口说明。问题是更新滞后芯片软件栈每周都在变文档三个月不更新就没人看了。而且静态文档是“死”的你没法查询、没法追溯、没法做影响分析。比如你想知道“如果 KMD 的调度接口改了哪些 UMD 模块会受影响”静态文档给不了你答案。第二种是代码注释加 Doxygen靠代码里的注释自动生成文档。这个方式比静态文档好一点至少跟代码同步。但问题是它只能覆盖代码层面覆盖不了跨模块的依赖关系、设计决策、已知问题、测试覆盖情况。而且 AI 芯片软件栈里有很多“非代码”的知识比如硬件寄存器的行为、固件的约定、编译器的 pass 顺序这些都不在代码注释里。第三种是人肉知识库靠架构师和资深工程师脑子里的经验。这个最靠谱但也最脆弱人一走知识就断了。而且人脑的容量有限一颗 AI 芯片的全栈软件栈涉及几千个模块、上万个接口没人能全记住。Agent 方案的核心思路是把软件地图从“静态文档”变成“可查询的知识图谱”从“人维护”变成“Agent 持续维护人审核”。Agent 不替代架构师的判断但替代架构师的“信息收集和整理”工作。2.2 Agent 在全栈软件地图中的角色定位我给 Agent 在这个场景里的角色定了三个边界这三个边界决定了整个方案的设计。第一个边界Agent 是信息聚合器不是决策者。它负责从各种来源收集信息、去重、归类、建立关联但不负责判断“这个模块该不该拆”“这个接口该不该改”。这些决策还是人来做。Agent 的输出是“事实和关系”不是“建议和方案”。第二个边界Agent 是持续运行的不是一次性任务。软件地图不是画一次就完了它需要随着代码提交、文档更新、issue 关闭而持续演进。所以 Agent 要能定期扫描仓库、监听变更、增量更新地图。这跟传统的“跑一次脚本生成文档”有本质区别。第三个边界Agent 的输出要可追溯、可验证。地图上的每一条信息都要能追溯到来源比如“UMD 的内存分配模块依赖 KMD 的kmd_alloc接口”这条信息要能点进去看到是哪个代码文件、哪一行、哪个 commit。这样人才敢信这张地图。基于这三个边界Agent 的架构就清晰了它需要一个信息采集层从代码、文档、issue、测试报告里抽信息一个知识组织层把信息组织成图谱一个查询接口层让人能问问题一个增量更新层持续维护。这四层缺一不可。2.3 为什么选 UMD 和 KMD 作为地图的核心骨架AI 芯片的全栈软件栈通常分这么几层最底下是硬件和固件往上依次是 KMD、UMD、Runtime、Compiler、Framework、应用层。其中 UMD 和 KMD 是软件栈的“腰”承上启下也是最复杂、最容易出问题的地方。KMD 是内核态驱动跑在操作系统内核里负责硬件资源管理、内存映射、中断处理、任务调度。UMD 是用户态驱动跑在用户空间负责把上层 Runtime 的请求翻译成 KMD 能理解的命令同时管理用户态的内存、队列、同步对象。这两层之间的接口是整颗芯片软件栈里最关键的接口也是最容易出兼容性问题的地方。我选 UMD 和 KMD 作为地图的核心骨架有三个原因。一是接口最密集UMD 和 KMD 之间的 ioctl、共享内存、队列对、事件通知这些接口的数量和复杂度远超其他层。二是变更最频繁芯片 bring-up 阶段UMD 和 KMD 几乎每周都在改地图必须能跟上。三是影响面最大UMD 和 KMD 的接口一变上面的 Runtime、Compiler、Framework 全要跟着改所以地图要能快速做影响分析。把 UMD 和 KMD 作为骨架其他层作为“挂载点”整个地图就有了主心骨。Agent 在采集信息时优先采集 UMD 和 KMD 的模块、接口、依赖然后再往上挂 Runtime、Compiler、Framework 的信息。这样地图的层次感就出来了。3. 核心细节解析Agent 怎么把零散信息变成结构化地图3.1 信息采集从代码仓库里抽模块和依赖Agent 要做的第一件事是从代码仓库里抽模块和依赖。这里的关键是“抽什么”和“怎么抽”。抽什么我定义了四类核心信息模块定义这个模块叫什么、在哪个目录、负责什么、接口定义这个模块暴露了哪些函数、ioctl、数据结构、依赖关系这个模块依赖了哪些其他模块的接口、变更历史这个模块最近改了什么、谁改的、为什么改。怎么抽对于 C/C 代码Agent 可以用 tree-sitter 做语法分析提取函数定义、结构体定义、宏定义。对于 ioctl 这种特殊的接口Agent 要能识别_IOW、_IOR、_IOWR这些宏提取出 ioctl 号、参数类型、方向。对于依赖关系Agent 要能分析#include、函数调用、符号引用建立模块之间的依赖图。这里有个坑AI 芯片的代码仓库往往很大几十万行代码Agent 不可能每次全量扫描。我的做法是让 Agent 先做一次全量扫描建立基线然后后续只扫描变更的文件。变更检测可以用 git diff也可以用文件哈希。增量扫描能把每次更新的时间从几十分钟压到几分钟。还有一个坑代码里的依赖关系有很多是“隐式”的比如通过全局变量、通过回调函数、通过共享内存。这些依赖光靠静态分析抽不出来需要 Agent 结合运行时日志和测试用例来推断。我的做法是让 Agent 先抽显式依赖然后标记出“疑似隐式依赖”的地方让人来确认。3.2 信息采集从文档和 issue 里抽设计决策和已知问题代码仓库能告诉你“是什么”但告诉不了你“为什么”和“有什么坑”。这两类信息在文档和 issue 里。文档包括设计文档、接口文档、测试文档、用户手册。Agent 要从这些文档里抽三类信息设计决策为什么这么设计、当时考虑了哪些方案、为什么选了这个、约束条件这个模块有什么限制、什么情况下不能用、使用示例怎么调用这个接口、参数怎么填、返回值怎么处理。issue 包括 bug 报告、功能请求、讨论记录。Agent 要从 issue 里抽已知问题这个模块有什么已知 bug、什么条件下会触发、有没有 workaround、变更原因这个接口为什么改、改之前是什么样、改之后兼容性如何、经验教训这个坑是怎么踩的、怎么避免。这里最大的挑战是文档和 issue 往往是非结构化的自然语言Agent 需要用 NLP 技术来抽信息。我的做法是让 Agent 先做实体识别识别出模块名、接口名、函数名然后做关系抽取识别出“A 依赖 B”“A 导致 B”“A 替代 B”这些关系最后做摘要生成把一段讨论压缩成一句话的结论。实测下来Agent 从文档和 issue 里抽信息的准确率大概在 70% 到 80% 之间剩下的 20% 到 30% 需要人工审核。这个准确率已经比人肉整理高很多了因为人肉整理往往会漏掉很多细节而 Agent 至少能做到“全量扫描不漏”。3.3 知识组织用图结构表达模块、接口和依赖采集到的信息怎么组织我用的是图结构。图里的节点有三类模块节点UMD 的内存管理模块、KMD 的调度模块、接口节点kmd_alloc、umd_submit、问题节点已知 bug、待办事项。图里的边有四类依赖边A 依赖 B、调用边A 调用 B、影响边A 变更影响 B、关联边A 和 B 相关。为什么用图而不是树因为软件栈的依赖关系不是树状的是网状的。一个 UMD 模块可能依赖多个 KMD 接口一个 KMD 接口可能被多个 UMD 模块调用一个已知问题可能涉及多个模块。树结构表达不了这种多对多的关系图结构可以。图建好之后Agent 就能回答很多传统文档回答不了的问题。比如“UMD 的内存管理模块直接和间接依赖了哪些 KMD 接口”这是一个图遍历问题Agent 从 UMD 内存管理节点出发沿着依赖边走就能找到所有直接和间接依赖的 KMD 接口。再比如“如果 KMD 的调度接口改了哪些 UMD 模块会受影响”这是一个反向图遍历问题Agent 从 KMD 调度接口出发沿着影响边反向走就能找到所有受影响的 UMD 模块。图结构还有一个好处是可视化。你可以把图渲染成一张网络图节点是模块和接口边是依赖和调用一眼就能看出哪些模块是“枢纽”连接很多其他模块哪些模块是“孤岛”没什么连接。这对架构师判断模块划分是否合理很有帮助。3.4 查询接口让人能用自然语言问地图问题地图建好了怎么让人用最自然的方式是自然语言查询。你问“UMD 的内存管理模块依赖哪些 KMD 接口”Agent 把这个问题翻译成图查询然后返回结果。这里的关键是意图识别和查询生成。Agent 要先识别出你问的是哪类问题是“依赖查询”A 依赖什么、是“影响查询”A 变更影响什么、是“路径查询”A 到 B 的依赖路径是什么、还是“属性查询”A 模块的负责人是谁。识别出意图后Agent 再生成对应的图查询语句。我实测下来自然语言查询的准确率取决于两个因素一是地图本身的质量如果地图里的模块名、接口名不规范Agent 就识别不准二是问题的表述方式如果问题里用了地图里没有的别名Agent 也识别不准。所以我在设计时加了一个“别名表”让人可以把常用的别名映射到地图里的标准名。除了自然语言查询Agent 还支持结构化查询比如直接写图查询语句。这对高级用户很有用因为自然语言查询有时候不够精确结构化查询可以精确控制查询逻辑。4. 实操过程从零搭建一个 AI 芯片全栈软件地图 Agent4.1 环境准备与工具选型先说环境。我用的是一台 32 核 128G 内存的 Linux 服务器跑 Ubuntu 22.04。为什么需要这么大内存因为代码仓库全量扫描时Agent 要把整个仓库的 AST 加载到内存里几十万行代码的 AST 大概要占几十 G 内存。如果仓库更大可能还需要分布式扫描。工具选型上我用了这几个核心组件tree-sitter做代码语法分析支持 C/C、Python、Rust 等多种语言。选它是因为它解析速度快、增量解析支持好、API 简单。Neo4j做图数据库存模块、接口、依赖关系。选它是因为它查询语言 Cypher 表达力强、可视化工具成熟、社区活跃。LangChain做 Agent 的编排框架把信息采集、知识组织、查询接口串起来。选它是因为它生态丰富、文档齐全、上手快。OpenAI API做自然语言理解和生成。选它是因为效果稳定、接口简单、成本可控。这里要说明一下工具选型不是唯一的你可以用别的图数据库比如 NebulaGraph、别的 Agent 框架比如 AutoGen、别的 LLM比如 Claude。我选这套是因为我用得最熟而且实测下来稳定。环境准备好之后先建一个 Neo4j 数据库建好节点和边的 schema。节点标签有Module、Interface、Issue边类型有DEPENDS_ON、CALLS、AFFECTS、RELATES_TO。schema 建好之后Agent 采集信息时就直接往图里写。4.2 代码仓库扫描与模块抽取第一步是扫描代码仓库抽取模块和接口。我写了一个 Python 脚本用 tree-sitter 解析每个源文件提取函数定义、结构体定义、宏定义。import tree_sitter from tree_sitter import Language, Parser # 加载 C 语言语法 LANGUAGE Language(build/my-languages.so, c) parser Parser() parser.set_language(LANGUAGE) def extract_functions(file_path): with open(file_path, rb) as f: code f.read() tree parser.parse(code) functions [] # 遍历 AST找函数定义节点 query LANGUAGE.query((function_definition declarator: (function_declarator declarator: (identifier) name))) captures query.captures(tree.root_node) for node, name in captures: functions.append({ name: name.text.decode(utf-8), start_line: node.start_point[0], end_line: node.end_point[0] }) return functions这个脚本能抽出每个文件里的函数定义。对于 ioctl我加了一个专门的提取逻辑识别_IOW、_IOR、_IOWR宏提取出 ioctl 号、参数类型、方向。import re def extract_ioctls(file_path): with open(file_path, r) as f: content f.read() ioctls [] # 匹配 _IOW/_IOR/_IOWR 宏 pattern r#define\s(\w)\s_IO([WR]?)\s*\(\s*(\w)\s*,\s*(\d)\s*,\s*(\w)\s*\) for match in re.finditer(pattern, content): ioctls.append({ name: match.group(1), direction: match.group(2), type: match.group(3), nr: match.group(4), arg_type: match.group(5) }) return ioctls抽出来的模块和接口信息直接写到 Neo4j 里。每个模块建一个Module节点每个接口建一个Interface节点模块和接口之间建EXPOSES边。这里有个实操心得代码仓库里往往有大量自动生成的代码和第三方代码这些要排除掉。我的做法是维护一个排除列表把third_party/、build/、generated/这些目录排除掉。如果不排除地图里会混入大量无关信息查询时噪音很大。4.3 依赖关系抽取与图构建模块和接口抽出来之后下一步是抽依赖关系。依赖关系分两种显式依赖和隐式依赖。显式依赖靠静态分析抽。对于 C/C 代码显式依赖包括#include、函数调用、符号引用。我写了一个脚本用 tree-sitter 遍历 AST找call_expression节点提取出被调用的函数名然后查这个函数属于哪个模块建立模块之间的CALLS边。def extract_calls(file_path, module_name): with open(file_path, rb) as f: code f.read() tree parser.parse(code) calls [] query LANGUAGE.query((call_expression function: (identifier) callee)) captures query.captures(tree.root_node) for node, callee in captures: calls.append({ caller_module: module_name, callee_name: callee.text.decode(utf-8), line: node.start_point[0] }) return calls隐式依赖靠运行时日志和测试用例推断。比如两个模块通过共享内存通信静态分析抽不出来但运行时日志里能看到两个模块都在读写同一块内存。我的做法是让 Agent 分析运行时日志找出“同一时间段内访问同一内存地址”的模块对标记为“疑似隐式依赖”然后让人来确认。依赖关系抽出来之后写到 Neo4j 里建DEPENDS_ON边和CALLS边。边的属性里记录依赖的类型显式/隐式、强度强/弱、来源代码/日志/文档。这里有个坑依赖关系会有环。比如 UMD 模块 A 依赖 KMD 模块 BKMD 模块 B 又回调 UMD 模块 A。这种环在软件栈里很常见但图查询时如果不处理会陷入死循环。我的做法是在图查询时加一个“最大深度”限制比如最多遍历 5 层超过就停。4.4 文档和 issue 的信息抽取代码仓库扫完之后下一步是扫文档和 issue。文档我支持 Markdown、Word、PDF 三种格式issue 我从 Jira、GitHub Issues、GitLab Issues 三个来源拉。文档抽取用 LangChain 的文档加载器和文本分割器把长文档切成小段然后让 LLM 从每段里抽信息。抽取的 prompt 我调了很多次最后定下来的是这个你是一个芯片软件栈的文档分析助手。请从以下文档片段中抽取 1. 设计决策为什么这么设计考虑了哪些方案为什么选了这个 2. 约束条件这个模块有什么限制什么情况下不能用 3. 使用示例怎么调用这个接口参数怎么填返回值怎么处理 输出格式为 JSON每个信息点包含 type、content、source 三个字段。issue 抽取类似但 prompt 不一样你是一个芯片软件栈的 issue 分析助手。请从以下 issue 中抽取 1. 已知问题这个模块有什么已知 bug什么条件下会触发有没有 workaround 2. 变更原因这个接口为什么改改之前是什么样改之后兼容性如何 3. 经验教训这个坑是怎么踩的怎么避免 输出格式为 JSON每个信息点包含 type、content、source 三个字段。抽出来的信息写到 Neo4j 里建Issue节点和RELATES_TO边。Issue节点关联到相关的Module节点和Interface节点。这里有个实操心得LLM 抽取的信息一定要人工审核。我实测下来LLM 抽取的准确率大概 70% 到 80%剩下的 20% 到 30% 有各种问题比如把“不推荐”抽成“推荐”把“已废弃”抽成“可用”。我的做法是让 Agent 把抽取结果标记为“待审核”然后让人在 Neo4j 的界面上逐条审核审核通过的标记为“已确认”审核不通过的标记为“已拒绝”。4.5 查询接口实现与自然语言问答地图建好之后最后一步是实现查询接口。我实现了两种查询方式自然语言查询和结构化查询。自然语言查询用 LangChain 的 Agent 实现。用户输入一个问题Agent 先识别意图然后生成 Cypher 查询执行查询最后把结果用自然语言返回。from langchain.agents import initialize_agent, Tool from langchain.llms import OpenAI def query_graph(cypher): # 执行 Cypher 查询 with driver.session() as session: result session.run(cypher) return [record.data() for record in result] tools [ Tool( nameQueryGraph, funcquery_graph, description执行 Cypher 查询输入是 Cypher 语句输出是查询结果 ) ] agent initialize_agent(tools, OpenAI(temperature0), agentzero-shot-react-description) response agent.run(UMD 的内存管理模块依赖哪些 KMD 接口)结构化查询就是直接写 Cypher。比如“UMD 的内存管理模块依赖哪些 KMD 接口”对应的 Cypher 是MATCH (umd:Module {name: UMD_MEM})-[:DEPENDS_ON*1..5]-(kmd:Module) WHERE kmd.layer KMD RETURN kmd.name, kmd.description这里有个实操心得自然语言查询的准确率取决于地图的质量和问题的表述。如果地图里的模块名不规范或者问题里用了别名Agent 就识别不准。我的做法是维护一个“别名表”把常用的别名映射到标准名。比如“内存管理”映射到“UMD_MEM”“调度”映射到“KMD_SCHED”。别名表让自然语言查询的准确率从 60% 提升到了 85%。5. 常见问题与排查技巧实录5.1 代码扫描太慢怎么办这是最常见的问题。全量扫描一个几十万行的代码仓库tree-sitter 解析加图写入可能要几十分钟甚至几个小时。我的优化方案分三步。第一步是增量扫描。用 git diff 找出变更的文件只扫描变更的文件。增量扫描能把每次更新的时间从几十分钟压到几分钟。但增量扫描有个问题如果变更涉及接口定义依赖关系可能变了需要重新计算依赖。我的做法是让 Agent 在增量扫描时把变更文件相关的依赖边先删掉重新扫描后再建。第二步是并行扫描。把仓库按目录切成多个分片每个分片用一个进程扫描最后合并结果。并行扫描能把时间再压一半。但并行扫描有个坑如果两个分片之间有依赖关系合并时可能会漏掉跨分片的依赖。我的做法是在合并时对跨分片的依赖做一次补充扫描。第三步是缓存 AST。tree-sitter 的 AST 可以序列化缓存下次扫描时如果文件没变直接读缓存不用重新解析。缓存能把重复扫描的时间压到几乎为零。5.2 依赖关系抽不准怎么办依赖关系抽不准有两个原因一是静态分析抽不出隐式依赖二是 LLM 抽取有误差。对于隐式依赖我的做法是多源验证。静态分析抽一遍运行时日志抽一遍测试用例抽一遍三个来源都指向同一个依赖才标记为“确认依赖”。只有一个来源指向的标记为“疑似依赖”让人来确认。对于 LLM 抽取误差我的做法是交叉验证。用两个不同的 LLM 抽同一段文档如果两个 LLM 抽出的结果一致标记为“高置信度”如果不一致标记为“低置信度”让人来审核。交叉验证能把 LLM 抽取的准确率从 70% 提升到 85%。还有一个技巧是用结构化信息辅助 LLM。比如抽依赖关系时先把代码里的#include和函数调用抽出来作为“提示”给 LLM让 LLM 在这些提示的基础上抽。这样 LLM 不容易漏掉显式依赖只需要专注于隐式依赖。5.3 地图更新后怎么保证一致性地图是持续更新的每次更新都可能引入不一致。比如一个模块被删了但依赖它的边还在一个接口改了名但引用它的地方没改。我的做法是每次更新后跑一致性检查。检查分三类一是孤儿检查找出没有入边或出边的节点这些节点可能是被删了但没清理干净的二是悬空引用检查找出引用了不存在节点的边这些边可能是接口改名后没更新的三是环检查找出依赖关系里的环这些环可能是设计问题也可能是数据错误。一致性检查发现问题后Agent 会生成一个“修复建议”让人来确认。确认后Agent 自动修复。实测下来每次更新后跑一致性检查能把地图的错误率控制在 5% 以内。5.4 常见问题速查表问题可能原因排查方法解决方案代码扫描太慢全量扫描、单进程、无缓存看扫描日志统计每个阶段耗时增量扫描、并行扫描、缓存 AST依赖关系抽不准隐式依赖、LLM 误差对比多源抽取结果多源验证、交叉验证、结构化提示地图更新后不一致孤儿节点、悬空引用、环跑一致性检查自动修复、人工确认自然语言查询不准别名、地图质量差看查询日志统计失败率维护别名表、规范模块名图查询太慢图太大、查询太复杂看查询计划统计遍历节点数加索引、限制遍历深度、分片查询5.5 独家避坑技巧技巧一先建小地图再扩大地图。不要一上来就扫全仓库先选一个核心模块比如 UMD 的内存管理把它的模块、接口、依赖抽出来建一个小地图。小地图跑通了再逐步扩展到其他模块。这样能快速验证方案也能快速发现问题。技巧二地图的 schema 要稳定。节点标签和边类型一旦定了就不要轻易改。因为 schema 一改所有查询和可视化都要跟着改。我的做法是 schema 定下来后写一个 schema 文档所有团队成员都按这个 schema 来。技巧三地图要有人工审核环节。不要指望 Agent 全自动建图一定要有人工审核。我的做法是让 Agent 把抽取结果标记为“待审核”然后让人在 Neo4j 的界面上逐条审核。审核通过的标记为“已确认”审核不通过的标记为“已拒绝”。人工审核能把地图的准确率从 70% 提升到 95%。技巧四地图要能导出。地图不能只存在 Neo4j 里要能导出成各种格式比如 Markdown、JSON、GraphML。这样团队成员不用装 Neo4j 也能看地图也能把地图集成到其他工具里。技巧五地图要能追溯。地图上的每一条信息都要能追溯到来源。比如“UMD 的内存管理模块依赖 KMD 的kmd_alloc接口”这条信息要能点进去看到是哪个代码文件、哪一行、哪个 commit。这样人才敢信这张地图。6. 地图的演进与扩展从 UMD/KMD 到全栈6.1 从 UMD/KMD 扩展到 Runtime 和 CompilerUMD 和 KMD 的地图建好之后下一步是往上扩展到 Runtime 和 Compiler。Runtime 是芯片软件栈的“调度中心”负责把上层 Framework 的请求翻译成 UMD 能理解的命令同时管理任务队列、内存池、同步对象。Compiler 是芯片软件栈的“翻译官”负责把上层 Framework 的算子翻译成芯片能执行的指令。扩展的方法跟 UMD/KMD 类似先扫代码仓库抽模块和接口再扫文档和 issue抽设计决策和已知问题然后建依赖关系写到图里。但 Runtime 和 Compiler 有自己的特点需要特殊处理。Runtime 的特点是状态机复杂。Runtime 里有很多状态机比如任务状态机、内存状态机、同步状态机。这些状态机在代码里往往是一堆switch-case和if-else静态分析抽不出状态转移图。我的做法是让 Agent 分析运行时日志从日志里抽状态转移重建状态机。Compiler 的特点是pass 依赖复杂。Compiler 里有很多 pass每个 pass 做一种优化pass 之间有依赖关系。这些依赖关系在代码里往往是一堆注册和调度逻辑静态分析抽不出完整的 pass 依赖图。我的做法是让 Agent 分析编译日志从日志里抽 pass 执行顺序重建 pass 依赖图。6.2 从静态地图到动态地图静态地图是“代码里有什么”动态地图是“运行时发生了什么”。静态地图能告诉你模块和接口的定义但告诉不了你运行时的行为。比如“UMD 的内存管理模块在什么情况下会调用 KMD 的kmd_alloc接口”静态地图给不了你答案动态地图可以。动态地图的构建方法是分析运行时日志。Agent 从日志里抽事件比如“UMD 内存管理模块调用了kmd_alloc”然后建事件之间的时序关系比如“A 事件在 B 事件之前发生”最后把事件和静态地图里的模块、接口关联起来。动态地图的价值在于行为分析。比如你想知道“UMD 的内存管理模块在什么情况下会触发 KMD 的调度”静态地图只能告诉你“UMD 内存管理模块依赖 KMD 调度接口”动态地图能告诉你“当内存分配超过阈值时UMD 内存管理模块会触发 KMD 调度”。这对排查性能问题和稳定性问题很有帮助。6.3 地图的自动化维护与持续集成地图建好之后最大的挑战是持续维护。芯片软件栈每周都在变地图如果跟不上很快就没人用了。我的做法是把地图维护集成到 CI/CD 流程里。每次代码提交CI 自动跑增量扫描更新地图。每次文档更新CI 自动跑文档抽取更新地图。每次 issue 关闭CI 自动跑 issue 抽取更新地图。更新后跑一致性检查发现问题自动生成修复建议发到团队群里让人确认。这样地图就能跟上软件栈的演进不会变成“死文档”。实测下来集成到 CI/CD 后地图的更新延迟从“几周”压到了“几小时”团队用地图的频率也高了很多。6.4 地图的查询接口扩展从问答到 API自然语言问答适合人用但不适合工具集成。比如你想把地图集成到 IDE 里让开发者在写代码时就能看到依赖关系自然语言问答就不合适了。这时候需要 API。我实现了一套 REST API支持几种查询/modules查模块列表/interfaces查接口列表/dependencies查依赖关系/impact查影响分析。API 返回 JSON方便工具集成。API 的价值在于生态扩展。有了 API你可以把地图集成到 IDE、集成到代码审查工具、集成到监控系统。比如在代码审查时如果提交的代码改了某个接口代码审查工具可以调地图的/impactAPI自动列出受影响的模块提醒审查者注意。7. 我个人在实际操作中的体会做这个项目的过程中我最大的体会是Agent 的价值不在于“替代人”而在于“放大人”。Agent 不能替代架构师判断模块该怎么拆、接口该怎么设计但 Agent 能把架构师从“信息收集和整理”的苦力活里解放出来让架构师专注于判断和决策。第二个体会是地图的质量取决于信息的质量信息的质量取决于采集的覆盖度。如果只扫代码不扫文档地图就只有“是什么”没有“为什么”如果只扫文档不扫 issue地图就只有“设计”没有“坑”。只有多源采集地图才完整。第三个体会是人工审核环节不能省。我试过全自动建图结果地图里混入了大量错误信息团队用了几次就不敢用了。后来加了人工审核地图的准确率上去了团队才敢信、才敢用。第四个体会是地图要能追溯。地图上的每一条信息都要能追溯到来源这样人才敢信。我试过不追溯来源结果团队用地图时总是问“这个信息哪来的”用了几次就不用了。后来加了追溯团队用地图时能直接点进去看来源信任度就上去了。最后分享一个小技巧地图的可视化很重要。人都是视觉动物一张网络图比一堆文字更容易理解。我用了 Neo4j 的可视化工具把地图渲染成网络图节点是模块和接口边是依赖和调用。团队一看图就能看出哪些模块是“枢纽”哪些模块是“孤岛”对架构判断很有帮助。这个项目后续还可以这样扩展一是把地图和芯片的硬件设计关联起来从软件地图追溯到硬件模块二是把地图和性能数据关联起来从软件地图追溯到性能瓶颈三是把地图和测试用例关联起来从软件地图追溯到测试覆盖。这些扩展能让地图的价值从“知识管理”扩展到“研发效能”。
返回列表