
接手一个陌生代码仓库时最耗时间的往往不是让程序跑起来而是弄清楚这个项目的模块边界、依赖方向和数据入口。人工读代码画架构图仓库小的时候还能维护文件超过几十个后图很快过时团队每个人手里的版本都不一样。Agent 开发正在改变这件事用 Agent 自动解析代码仓库抽取文件与模块之间的依赖再输出可视化架构图。下面从零搭建一个最小可用的仓库解析 Agent把仓库扫描、依赖提取、模型理解、Graphviz 渲染串成一条流水线并给出可复现的命令、常见报错和排查路径。1. 先拆开“仓库解析 Agent”这条链路1.1 为什么人工画架构图跟不上仓库变化代码仓库是持续变化的事实集合。每次重构、新增模块、调整目录架构图都要跟着改而人工维护的图往往落后于代码。更麻烦的是人看代码时会漏掉边界情况一个看似不重要的工具模块被几十个模块引用一个 service 层文件偷偷 import 了仓储层实现。这些信息靠肉眼很难在大仓库里快速汇总。自动化解析能解决“事实”部分也就是模块有哪些、依赖谁、谁被谁依赖。但自动化工具很难回答“这个模块在业务上承担什么角色”“层间调用是否符合规范”。这正是 Agent 能补上的部分把代码片段和依赖上下文交给大模型让它输出分层、职责和风险描述。1.2 一条完整流水线扫描、解析、建模、理解、渲染这个工具可以拆成五段每一段解决一个问题扫描遍历仓库目录按语言后缀收集源码文件。解析用语法分析器抽取每个文件里的 import、引用和调用关系。建模把文件和依赖关系建成有向图节点是文件或目录边是依赖方向。理解调用 Agent 对关键文件做语义分析补充分层、职责和风险。渲染把图数据输出成 Graphviz 的 DOT 文件再渲染成 PNG、SVG。流水线的关键点在于顺序。静态解析必须走在 Agent 之前因为依赖关系是确定事实不能交给可能产生幻觉的模型去编。Agent 只处理“这张图里每个节点该怎么解释”的问题。1.3 静态分析与 Agent 的分工边界用表格来说明分工能力静态分析ast / tree-sitterAgent大模型import 依赖关系准确稳定可能幻觉不能兜底模块职责理解基本没有强能写人话架构分层判断只能按目录猜测可按代码语义判断成本与速度低毫秒级高按 token 计费适合输出节点、边、入度出度layer、职责、风险、建议结论是静态分析负责喂事实Agent 负责解释语义两者各管一段。这个边界一旦反过来架构图就会变成“看起来漂亮但不可信”。2. 环境准备把最小链路先跑起来2.1 Python 依赖与 Graphviz 安装示例代码用 Python 实现依赖只有两个networkx 负责建图requests 负责调用 Agent 接口。先创建虚拟环境并安装python -m venv venv source venv/bin/activate pip install networkx requests架构图渲染依赖 Graphviz 的外部命令dot安装方式按系统区分# Ubuntu / Debian sudo apt-get install graphviz # macOS brew install graphviz # Windows管理员 shell choco install graphviz安装后确认两个命令都能执行dot -V python -c import networkx, requests; print(networkx, networkx.__version__); print(requests, requests.__version__)环境要求如下软件建议版本用途Python3.10 及以上运行主程序networkx3.x有向图建模requests2.x 及以上调用 Agent HTTP 接口Graphviz8.x 或 10.x渲染 DOT 文件这里要先确认自己的版本。Python 3.8 也能运行但代码里用了Path.read_text、集合语法和类型标注老版本需要微调。2.2 项目目录结构按职责拆四个模块后续扩展只需要替换其中一段repo-arch-agent/ ├── main.py # 入口组装流水线 ├── scan.py # 仓库扫描 ├── graph_builder.py # 依赖提取和建图 ├── agent_client.py # Agent 调用和输出解析 ├── render.py # DOT 生成与目录聚合 ├── requirements.txt └── output/ # 生成结果目录main.py 只负责调度scan.py 不知道 Agent 的存在agent_client.py 不知道 Graphviz 的存在。这样测试时可以用--skip-agent只验证静态链路。2.3 准备一个待分析的示例仓库用于验证的数据不要太小也不要太大。20 到 50 个 Python 文件的仓库最合适既有足够的依赖关系又能在几分钟内看出结果。可以直接拿本地 Python 项目做实验也可以从 GitHub、Gitee、GitLab 克隆一个中小型 Flask 或 FastAPI 项目。下面的命令统一把仓库放在sample_repo目录git clone 你的仓库地址 sample_repo注意仓库里的代码会以文本片段方式发送给 Agent 接口。涉及密钥、内部域名、客户数据时先做脱敏或改用私有化部署模型否则不要把整段代码直接外发。3. 仓库扫描与依赖提取先拿到确定的事实3.1 递归收集源码文件过滤无关目录第一步是遍历目录收集指定后缀的源码文件。真正要处理的文件往往只占仓库的一部分.git、node_modules、venv、__pycache__这些目录必须跳过。# scan.py import os from pathlib import Path SKIP_DIRS {.git, node_modules, venv, .venv, __pycache__, dist, build, .idea, .vscode} def collect_source_files(repo_path: Path, extensions: set[str]) - list[Path]: files [] for root, dirs, filenames in os.walk(repo_path): dirs[:] [d for d in dirs if d not in SKIP_DIRS] for name in filenames: if Path(name).suffix in extensions: files.append(Path(root) / name) return sorted(files)dirs[:] [...]这一行是关键它直接修改os.walk下一次遍历的目录列表。如果写成普通赋值os.walk仍然会进入被跳过的目录过滤就不生效。3.2 用标准库 ast 提取 import 关系Python 项目可以直接用标准库ast做语法解析不需要安装 C 扩展。脚本文件、动态导入可以靠正则粗筛但正则很难处理重名变量和复杂的 import 写法ast是更可靠的起点。# graph_builder.py import ast from pathlib import Path def extract_python_imports(file_path: Path) - set[str]: imports set() try: source file_path.read_text(encodingutf-8) tree ast.parse(source) except (SyntaxError, UnicodeDecodeError) as exc: print(f[warn] 解析失败: {file_path} ({exc})) return imports for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: imports.add(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: imports.add(node.module) return imports返回结果同时包含项目内部模块和第三方包。内部模块能匹配到仓库里的文件第三方包匹配不到自然会被过滤这也符合画架构图的诉求只展示仓库内部结构不展示外部依赖细节。如果要支持 Java、Go、JavaScript、TypeScript 仓库把ast换成 tree-sitter 对应的 grammar 即可每个语言对应一个解析器包接法完全相同。3.3 把模块依赖建成有向图用 networkx 的DiGraph保存文件级依赖。节点是相对路径边从“当前文件”指向“被依赖文件”。# graph_builder.py import networkx as nx def build_dependency_graph(repo_path: Path, files: list[Path]) - nx.DiGraph: G nx.DiGraph() module_to_file {} for f in files: rel f.relative_to(repo_path) module str(rel.parent / rel.stem) # services/order_service module_to_file[module] rel.as_posix() G.add_node(rel.as_posix()) for f in files: rel f.relative_to(repo_path) source rel.as_posix() for mod in extract_python_imports(f): target module_to_file.get(mod.replace(., /)) if target and target ! source: G.add_edge(source, target) return G这里mod.replace(., /)是为了把services.order_service这种模块名还原成相对路径形式才能和module_to_file的键对齐。若仓库里存在跨目录的相对导入还需要按当前文件所在目录做一次展开这个可以作为后续优化点。依赖图建好后可以立刻检查一些全局指标import networkx as nx print(节点数:, G.number_of_nodes(), 边数:, G.number_of_edges()) print(前 5 个高入度节点被依赖最多:) for node, degree in sorted(G.in_degree(), keylambda x: -x[1])[:5]: print( , node, degree)这里显示的“高被依赖节点”往往就是核心模块也是下面应该优先交给 Agent 分析的对象。4. 让 Agent 负责语义理解而不是负责编造事实4.1 给 Agent 划定职责边界Agent 在这条流水线里只做三件事判断文件属于哪一层。用一句话概括文件职责。标记最明显的架构风险。它不需要生成依赖列表。依赖列表由静态解析产出Agent 一旦参与生成就可能出现它没见过某个 import、却凭训练记忆补充出“可疑依赖”的情况。这种幻觉在单文件上不明显叠加到整张图上就会误导排查。实际项目里如果用的是 LangGraph、AutoGen 这类 Agent 框架可以把“扫描”“解析”“渲染”封装成工具节点把“语义理解”封装成 LLM 节点。本文保持纯 Python 函数实现便于看清每一步发生了什么。4.2 用结构化 Prompt 约束输出Agent 的输出必须是机器可解析的 JSON。除了在 prompt 里说明字段还要在请求里带上response_format并要求低温度减少随机波动。# agent_client.py import json import os import re import requests SYSTEM_PROMPT 你是资深架构师正在审查 Python 代码仓库。 请分析指定文件在系统架构中的角色严格输出 JSON不要输出其他内容。 字段如下 - layer: web|service|data|utils|other - responsibility: 不超过 40 字的一句话职责 - risk: 没有风险写 null有风险写一句话描述 def call_agent(path: str, code: str, model: str None) - str: api_base os.getenv(LLM_API_BASE, https://api.example.com/v1) api_key os.getenv(LLM_API_KEY, ) model model or os.getenv(LLM_MODEL, gpt-4o-mini) resp requests.post( f{api_base}/chat/completions, headers{Authorization: fBearer {api_key}}, json{ model: model, messages: [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f文件: {path}\n代码预览:\n{code[:4000]}}, ], temperature: 0.2, response_format: {type: json_object}, }, timeout60, ) resp.raise_for_status() return resp.json()[choices][0][message][content]代码里调用的是 OpenAI 兼容的/chat/completions接口因此 OpenAI、Azure以及大量提供兼容网关的开源模型服务都可以直接替换 base URL 和模型名。四个参数决定了 Agent 效果参数示例值作用调整建议temperature0.2控制输出的随机性语义标注类任务保持 0.2 以下response_formatjson_object强制输出 JSON接口不支持时可去掉但要加解析兜底代码预览长度4000 字符控制 token 成本文件过长时截断保留头部和类定义timeout60 秒防止接口阻塞大文件先截断再设置合理超时很多模型虽然声明支持 JSON 模式实际输出仍可能混入解释文字。做一个解析兜底从响应里直接抽取最外层大括号def parse_json_safe(content: str) - dict: try: return json.loads(content) except json.JSONDecodeError: match re.search(r\{.*\}, content, re.S) if match: return json.loads(match.group(0)) raise ValueError(无法从 Agent 输出中解析 JSON)4.3 把 Agent 结果合并到依赖图对每个文件调用call_agent把返回的 layer 和 responsibility 记录下来供渲染阶段给节点上色和加标签。# main.py 中的语义分析函数 def analyze_with_agent(files, repo_path, max_files80): layers, responsibilities {}, {} for f in files[:max_files]: rel f.relative_to(repo_path).as_posix() code f.read_text(encodingutf-8) for attempt in range(2): try: result parse_json_safe(call_agent(rel, code)) layers[rel] result.get(layer, other) responsibilities[rel] result.get(responsibility, ) print(f[agent] {rel} - layer{layers[rel]}) break except Exception as exc: print(f[agent] 第 {attempt 1} 次失败 {rel}: {exc}) return layers, responsibilities这里做了最多 2 次重试。Agent 请求可能因为超时、限流或返回格式异常而失败重试是最基本的韧性手段。生产环境还应该加指数退避避免多文件同时失败把接口打爆。注意max_files80是学习环境的阈值跑通即可。真实仓库上千个文件时不要全量分析优先分析高入度、高出度的核心文件或者按目录抽样。5. 渲染架构图从依赖图到可视化5.1 生成 DOT 描述文件Graphviz 使用 DOT 语言描述图结构。渲染时用不同填充色区分层级用节点标签同时显示文件路径和职责。# render.py from pathlib import Path import networkx as nx LAYER_COLORS { web: #dbeafe, service: #dcfce7, data: #fef3c7, utils: #f3e8ff, other: #f8fafc, } def write_dot(G, layers, responsibilities, output_path: Path): lines [ digraph architecture {, rankdirLR;, node [shapebox, stylerounded,filled, fontnameMicrosoft YaHei];, ] for node in G.nodes: color LAYER_COLORS.get(layers.get(node, other), #f8fafc) label node desc responsibilities.get(node, ) if desc: label f{node}\n{desc} label label.replace(\\, \\\\).replace(, \\) lines.append(f {node} [label{label}, fillcolor{color}];) for src, dst in G.edges: lines.append(f {src} - {dst};) lines.append(}) output_path.write_text(\n.join(lines), encodingutf-8)DOT 文件里节点名和 label 都在双引号内文件名本身就带或\时必须先转义否则 Graphviz 会报语法错误。中文字体名按操作系统调整Windows 用 Microsoft YaHeimacOS 用 PingFang SCLinux 需要先安装中文字体再用 fontconfig 查询名称。5.2 用 Graphviz 渲染 PNG/SVGDOT 文件生成后用dot命令渲染dot -Tpng output/arch.dot -o output/arch.png dot -Tsvg output/arch.dot -o output/arch.svgSVG 适合放进文档和 Web 页面可以缩放不模糊PNG 适合直接发给同事或贴到设计文档里。渲染失败时先检查 DOT 文件本身dot -v output/arch.dot -o /dev/null-v会输出详细的布局过程语法错误会在最开始报出来。5.3 目录级聚合解决节点爆炸文件级图在 20 到 50 个文件时很清晰到 500 个文件就会变成一团线球。目录级聚合是解决节点爆炸最简单的手段把同一目录下的文件合并成一个包节点边在目录之间连接。def aggregate_by_directory(G: nx.DiGraph, level: int 1) - nx.DiGraph: A nx.DiGraph() for node in G.nodes: parts Path(node).parts group /.join(parts[:level]) or node A.add_node(group) for src, dst in G.edges: s /.join(Path(src).parts[:level]) d /.join(Path(dst).parts[:level]) if s ! d: A.add_edge(s, d) return A命令行参数化后可以通过--dir-level控制粒度。一级目录适合看整体分层三级目录适合看具体业务模块文件级只适合小仓库。参数默认值作用应该怎么调--dir-level00 表示文件级1 表示按一级目录聚合仓库超过 200 文件时至少设 1--max-files80限制 Agent 分析文件数成本敏感时调小分析核心文件时手动指定--skip-agentfalse跳过 LLM只出依赖图快速验证静态链路时打开--outputoutput/arch.dotDOT 输出路径配合 CI 流水线时改成固定 artifact 路径6. 运行与验证架构图到底可不可信6.1 完整命令与预期输出把主流程组装到 main.py 中# main.py import argparse from pathlib import Path from scan import collect_source_files from graph_builder import build_dependency_graph from agent_client import call_agent, parse_json_safe from render import write_dot, aggregate_by_directory def analyze_with_agent(files, repo_path, max_files80): # 完整版带重试见 4.3这里为了可读性省略重试逻辑 layers, responsibilities {}, {} for f in files[:max_files]: rel f.relative_to(repo_path).as_posix() code f.read_text(encodingutf-8) try: result parse_json_safe(call_agent(rel, code)) layers[rel] result.get(layer, other) responsibilities[rel] result.get(responsibility, ) except Exception as exc: print(f[agent] 失败 {rel}: {exc}) return layers, responsibilities def main(): parser argparse.ArgumentParser(description代码仓库架构图生成 Agent) parser.add_argument(--repo, typePath, defaultPath(sample_repo)) parser.add_argument(--max-files, typeint, default80) parser.add_argument(--dir-level, typeint, default0) parser.add_argument(--skip-agent, actionstore_true) parser.add_argument(--output, typePath, defaultPath(output/arch.dot)) args parser.parse_args() files collect_source_files(args.repo, {.py}) print(f[scan] 发现 {len(files)} 个 Python 文件) G build_dependency_graph(args.repo, files) print(f[graph] 节点 {G.number_of_nodes()}, 边 {G.number_of_edges()}) layers, responsibilities {}, {} if not args.skip_agent: layers, responsibilities analyze_with_agent(files, args.repo, args.max_files) if args.dir_level 0: G aggregate_by_directory(G, args.dir_level) print(f[graph] 聚合后节点 {G.number_of_nodes()}) write_dot(G, layers, responsibilities, args.output) print(f[render] DOT 已写入 {args.output}) if __name__ __main__: main()先配好 Agent 环境变量再执行export LLM_API_BASEhttps://你的模型网关地址/v1 export LLM_API_KEY你的密钥 export LLM_MODELgpt-4o-mini python main.py --repo ./sample_repo --output output/arch.dot dot -Tpng output/arch.dot -o output/arch.png正常输出大致如下[scan] 发现 36 个 Python 文件 [graph] 节点 36, 边 47 [agent] app.py - layerweb [agent] services/order_service.py - layerservice [agent] repositories/order_repo.py - layerdata ... [render] DOT 已写入 output/arch.dot到这里就拿到了第一版架构图。但“能生成图”和“图是可信的”是两回事必须验证。6.2 人工抽检 Agent 判别结果抽检建议按固定比例执行从所有被 Agent 分析的文件里随机挑 10 个人工判断 layer 和 responsibility 是否正确统计一致率。一致率在 80% 以上说明 prompt 和上下文足够可以继续扩展。一致率在 60% 到 80%优先检查代码预览是否截断了关键逻辑或文件职责本身不清晰。一致率低于 60%不要硬调 prompt先看静态依赖图是否正确因为 Agent 拿到的上下文来自这张图图错则语义必错。如果目标是做成可回归的东西就把抽检文件做成 golden 集合固定 10 个文件的期望输出每次修改 prompt 后跑一遍对比防止优化一个模块同时破坏另一个模块。6.3 从架构图上发现真实问题架构图的价值不只是把依赖画出来而是让问题浮出来高被依赖节点。入度最大的几个节点往往是核心服务也可能是被过度复用的“上帝模块”。层间逆向调用。web 层越过 service 层直接依赖 data 层说明接口未收敛。循环依赖。networkx 可以直接找环cycles list(nx.simple_cycles(G)) if cycles: print(发现循环依赖:, cycles[:5]) else: print(无循环依赖)这些检查点应该固化成脚本交给 CI 在每次合并请求时自动跑而不是等人工看图。7. 常见问题与排查链路7.1 排错顺序从输入到输出逐层检查遇到问题不要先怀疑模型。按依赖顺序排查仓库目录是否扫描到文件[scan]行数字是否合理。import 匹配逻辑是否正确[graph]的边数是否少得异常。Agent 接口是否可用返回是否 JSON是否超时。DOT 文件是否合法用dot -v校验。渲染结果是否有乱码是否缺字体。架构图本身是否可信回到 6.2 做抽检。前两层是静态逻辑出错误差是确定的后四层是外部依赖和语义判断需要观察日志。7.2 常见现象、原因和处理方案速查表问题现象常见原因检查方式处理方案全部文件数正常但边数几乎为零import 匹配逻辑没对齐模块名打印未匹配的 import 列表检查相对导入和__init__.py导出补充路径映射Agent 调用报agent execution terminated due to error.上下文过长、输出非 JSON、接口超时看调用日志、检查代码预览长度、重试截断代码到固定长度启用response_format加重试和指数退避Agent 返回内容解析失败模型在 JSON 外输出了解释文字打印原始 content用parse_json_safe抽取大括号内容必要时换更稳的模型图中节点爆炸无法阅读文件级粒度对仓库过大统计节点数开启--dir-level 1或更高循环依赖导致布局混乱项目存在真实环或模块映射过粗用nx.simple_cycles找环修代码或把环内节点聚合显示渲染后中文变成方块或乱码DOT 未指定字体或系统缺中文字体fc-list :langzh查字体在 node 配置中写明字体名或改用英文标签整个流程能跑但图不可信Agent 把职责判断和依赖判断混在一起对比静态依赖和 Agent 描述严格分工依赖只来自静态解析Agent 只做语义解释7.3 两个高频报错的现场还原第一个高频报错是agent execution terminated due to error.这种提示通常出现在 Agent 执行中途被框架终止时。常见于把整个文件甚至整个仓库一次性塞进上下文导致请求超时或 token 超限。处理方式是代码预览固定截断例如只保留前 4000 字符和所有 import 行同时把单文件分析封装成可重试的独立任务失败不拖垮整批。第二个高频报错是 Graphviz 的Error: syntax error in line X near 。DOT 里 label 使用 HTML 风格标签时和有特殊含义。如果文件路径里出现泛型写法或尖括号必须先替换或转义。更稳妥的做法是始终保持label...字符串格式并把文件名里的引号、反斜杠一并处理。8. 落地最佳实践与扩展方向8.1 学习环境和生产环境的差异这个工具在个人电脑上跑通和在公司内部稳定运行差别很大。维度学习环境生产环境代码仓库本地 demo 项目Git 平台受控仓库CI 触发Agent 模型低配模型手动设置密钥企业网关、限流、预算、审计渲染本地 dotCI artifactWeb 服务托管安全检查本机代码密钥脱敏、私有化部署、权限审批结果保存本地 PNG对象存储、版本化、增量刷新生产环境下最容易被忽视的是代码安全。仓库中的代码可能包含密钥、内部域名、客户信息和未公开逻辑调用外部 Agent 前必须确认脱敏策略和审批流程。私有化部署模型可以显著降低外发风险但如果企业内部有大量仓库还要做好预算控制和批次限流。8.2 落地前检查清单发布到团队内部前逐项确认[ ] 目标仓库的语言是否在解析器支持范围内。[ ] 已配置模型网关、密钥和模型名且密钥不写入代码仓库。[ ] 已对长文件做截断并对 Agent 调用加重试和超时。[ ] 已随机抽检至少 10 个文件的判别结果一致率达标。[ ] 渲染端已安装 Graphviz中文字体可正常显示。[ ] 大仓库已配置目录级聚合阈值和最大分析文件数。[ ] 输出目录和运行日志有固定位置失败可重跑。[ ] 涉及敏感代码的仓库不会把原始代码外发到外部模型。8.3 可以继续做的五个方向多语言支持。Python 示例只覆盖ast接入 tree-sitter 后可以解析 Java、Go、JavaScript、TypeScript图结构和渲染部分不需要改。增量分析。在 CI 里只对git diff涉及的文件重算依赖和 Agent 标注避免每次全量跑。Agent 记忆。对已分析过的文件保存结果文件内容未变化时直接复用降低 token 成本也提高结果稳定性。架构规则检查。把“web 不能直连 data 层”“不允许循环依赖”等规则写到引擎里图中每出现一次违规就产生一条 issue而不是只画出来让人肉眼看。文档联动。把架构图、模块职责和风险描述一起交给 Agent生成 README 的架构章节让文档和代码保持同步。最后说一句实践判断这类工具最容易犯的错误是让 Agent 在缺少静态事实的情况下“自由发挥”。先把扫描、解析、建图这三段做扎实再把 Agent 的语义分析加进来每一步都可抽检、可回滚、可离线运行这样的架构图工具才有长期使用价值。