在 macOS 上折腾本地大模型的开发者,几乎都会经历同一个场景:前几天下载了一个 7B 模型,昨天从网盘拷了一个 GGUF 文件,今天打开模型目录一看,几十个以哈希命名的 blob 文件躺在那里,你根本分不清哪个对应哪个模型,磁盘又快满了。
模型下载越来越容易,模型管理却非常混乱。What the Model – Local LLM Inventory for macOS这个项目的命名把问题说得很直白——What the model,这到底是个什么模型?它的定位是 Local LLM Inventory,也就是在 macOS 上给本地大模型做资产盘点。本文不急着复述工具界面,而是把这类工具背后的两个核心技术讲透:如何解析 GGUF 元数据、如何统计 Ollama 模型清单,并给出一套可以直接跑通的 Python 脚本。读完之后,你完全可以自己写一份属于你的“本地模型家底清单”。
1. 为什么本地模型需要一份清单
很多人的误区是:本地跑模型 = 下载模型 + 推理。但真实情况是,模型管理很快会成为瓶颈。
先看数据形态。Ollama 在 macOS 上默认把模型存放在~/.ollama/models,其中manifests目录保存模型描述信息,blobs目录保存真正的模型文件,而这些 blob 全部以 SHA256 哈希命名。你运行ollama list可以看到qwen2.5:7b这样可读的名字,但一旦进入磁盘目录,看到的就是一串sha256:xxxxxxxxxx的二进制文件。如果哪天容器清单损坏,你很难把某个 4GB 的 blob 对应回某个模型。
再看手动下载的 GGUF 模型。这类文件通常来自 Hugging Face、ModelScope 或网盘共享,命名五花八门:有的叫qwen2.5-7b-instruct-q4_k_m.gguf,有的直接叫model-q4_k_m.gguf。文件存到不同目录后,很快会出现重复下载、新旧版本并存、不知道哪个该删的情况。
最后是磁盘占用问题。一个 7B 参数的模型,Q4 量化后约 4 到 5GB;一个 14B 模型,Q4 量化后约 9 到 10GB。如果同时下载几个模型的多个量化版本,几十 GB 空间瞬间消失。可当你面对一堆哈希文件时,根本说不清楚这几十 GB 到底是被哪个模型吃掉的,自然也无从下手清理。
所以“What the Model”这一类工具真正的价值在于:把分散在不同目录、不同格式、不同命名规则的模型文件,统一变成一份结构化清单,包含模型名、架构、量化等级、文件路径、磁盘占用等关键信息。有了这份清单,你才能回答三个最基本的问题:我有哪些模型、它们占了多少空间、哪个可以放心删除。
2. 本地 LLM 生态中的模型目录与格式
在动手写脚本之前,需要先理解 macOS 上本地模型的生态布局。
2.1 三种主流管理方式
目前 macOS 用户接触本地大模型,主要有三条路径。
第一类是 Ollama。它自带模型管理、API 服务和 CLI,用户执行ollama run qwen2.5就能自动拉取模型。它的优点是把模型抽象成“名字 + 标签”,缺点是底层文件结构对外部工具不透明。
第二类是 LM Studio。它提供图形界面,模型默认存放在~/.lmstudio/models目录,通常也是 GGUF 格式。它比 Ollama 更适合新手,但同样存在“界面里显示正常、目录里一团乱”的问题。
第三类是 llama.cpp / llama-server 配合手工下载的 GGUF 文件。这是最灵活也最混乱的方式。用户从 Hugging Face 下载任意 GGUF 文件,放在~/models或~/.cache/llama.cpp之类的目录,然后通过命令行指定路径启动。文件命名完全依赖个人习惯,没有统一清单。
2.2 GGUF 格式到底长什么样
GGUF(GPT-Generated Unified Format)是 llama.cpp 社区主推的模型格式。它不是一种压缩容器,而是一个带元数据头的二进制文件格式。
整个 GGUF 文件由两部分组成:文件头区和张量数据区。文件头区描述模型的元信息,比如模型架构、参数数量、上下文长度、量化类型;张量数据区才是真正的权重数据。
文件头的二进制布局是:
- 4 字节:magic,固定为
GGUF四个字符的小端整数,即0x46554747; - 4 字节:格式版本号;
- 8 字节:张量数量;
- 8 字节:元数据键值对数量;
- 然后循环读取每个键值对。
理解这个格式很重要,因为我们可以直接读取文件头来获取模型信息,不需要加载整个模型文件。这也是后续脚本能快速扫描几十个大型 GGUF 文件的基础。
2.3 Ollama 的 Manifest 结构
Ollama 的模型信息并不直接写在 GGUF 文件里,而是维护了一份 JSON 格式的 manifest。每个模型对应~/.ollama/models/manifests/registry.ollama.ai/library/<模型名>/<标签>这样一个路径。manifest 里记录了 config 层和 layers 层的 digest 与 size,而真正的模型数据以 blob 形式存储在blobs目录。
这意味着,统计 Ollama 模型的磁盘占用时,不能简单看单个文件大小,而要把一个 manifest 下所有 layer 的大小合计。同时要注意,不同模型可能共享同一个 blob,统计出的“逻辑大小之和”可能大于“实际磁盘占用”。
3. What the Model 的核心设计思路
从项目名称和定位来看,What the Model 想解决的核心问题是“模型身份识别”。它的思路不是再做一个模型下载工具,而是做一个独立的盘点层:扫描模型目录、解析模型元数据、汇总成清单。
这个设计在工程上很聪明。它不依赖某个特定推理引擎,而是把目光放在文件系统层面和格式解析层面:
- 对于 GGUF 文件,直接解析文件头,读取
general.architecture、general.name、general.file_type等元数据字段; - 对于 Ollama,解析 manifests 目录下的 JSON 文件,把 model:tag 与 blob digest 对应起来,同时计算总大小;
- 对于 LM Studio,按目录扫描 GGUF 文件并补全路径信息。
最终,这三类来源合并成一份统一清单。所以无论你用什么工具管理模型,清单化的思路都是一致的。
我们接下来会完整实现这个流程。先说明环境,再写三个脚本:GGUF 扫描器、Ollama 清单统计器、汇总报告生成器。
4. 环境准备与前置条件
本方案基于 Python 3,不需要安装第三方依赖,标准库即可运行。原因是我们的核心工作是二进制文件解析和 JSON 读取,struct、json、glob、pathlib已经足够。
macOS 版本:建议 macOS 12 及以上 Python 版本:Python 3.9+ 磁盘工具:自带 du、df验证 Python 环境:
python3 --version如果输出 Python 3.9 或更高版本,环境就满足要求。
在动手之前,建议先创建一个专门存放脚本的工作目录:
mkdir -p ~/llm-inventory cd ~/llm-inventory工作目录里会放置三个脚本文件和两个中间 JSON 文件,最终输出一份LLM_INVENTORY.md。如果你还没安装 Python3,可以通过 Xcode Command Line Tools 或 Homebrew 安装,这一步不展开。
5. 核心脚本一:解析 GGUF 元数据
第一个脚本负责扫描手动下载的 GGUF 文件,并解析文件头元数据。
5.1 GGUF 元数据读取原理
在解析代码里,我们需要实现几个基础函数:
read_uint64:读取 8 字节无符号整数;read_string:先读长度,再读 UTF-8 字节;read_value:根据类型编码读取对应的值。
GGUF 元数据值有统一类型编码。0是 uint8,1是 int8,2是 uint16,3是 int16,4是 uint32,5是 int32,6是 float32,7是 bool,8是 string,9是 array,10、11、12分别是 uint64、int64、float64。数组类型会先写一个元素类型字节,再写 8 字节元素个数,最后依次写入元素。解析器需要支持这些类型,才能兼容大多数 GGUF 文件。
5.2 完整代码
#!/usr/bin/env python3 # 文件路径:~/llm-inventory/wtm_scan_gguf.py import os import glob import json import struct GGUF_MAGIC = 0x46554747 # "GGUF" 的小端整数 def read_uint64(f): return struct.unpack("<Q", f.read(8))[0] def read_string(f): length = read_uint64(f) return f.read(length).decode("utf-8", errors="replace") def read_value(f, vtype): # 按 GGUF 类型编码读取值 if vtype == 0: return f.read(1)[0] if vtype == 1: return struct.unpack("<b", f.read(1))[0] if vtype == 2: return struct.unpack("<H", f.read(2))[0] if vtype == 3: return struct.unpack("<h", f.read(2))[0] if vtype == 4: return struct.unpack("<I", f.read(4))[0] if vtype == 5: return struct.unpack("<i", f.read(4))[0] if vtype == 6: return struct.unpack("<f", f.read(4))[0] if vtype == 7: return bool(f.read(1)[0]) if vtype == 8: return read_string(f) if vtype == 10: return struct.unpack("<Q", f.read(8))[0] if vtype == 11: return struct.unpack("<q", f.read(8))[0] if vtype == 12: return struct.unpack("<d", f.read(8))[0] if vtype == 9: elem_type = f.read(1)[0] count = read_uint64(f) arr = [] for _ in range(count): arr.append(read_value(f, elem_type)) return arr raise ValueError(f"unsupported GGUF metadata type: {vtype}") def parse_gguf_meta(filepath): """读取 GGUF 文件头部的元数据。""" with open(filepath, "rb") as f: header = f.read(24) # magic(4) + version(4) + tensor_count(8) + kv_count(8) if len(header) < 24: return {"error": "file too short"} magic, version, tensor_count, kv_count = struct.unpack("<IIQQ", header) if magic != GGUF_MAGIC: return {"error": "not a GGUF file"} meta = { "version": version, "tensor_count": tensor_count, } for _ in range(kv_count): key = read_string(f) vtype = f.read(1)[0] value = read_value(f, vtype) meta[key] = value return meta def scan_gguf_files(search_roots): """扫描指定目录下的所有 .gguf 文件。""" results = [] for root in search_roots: root = os.path.expanduser(root) if not os.path.exists(root): continue pattern = os.path.join(root, "**", "*.gguf") for filepath in glob.glob(pattern, recursive=True): size = os.path.getsize(filepath) meta = parse_gguf_meta(filepath) results.append({ "path": filepath, "file_name": os.path.basename(filepath), "size_bytes": size, "meta": meta, }) return results if __name__ == "__main__": search_roots = [ "~/models", "~/.cache/llama.cpp", "~/.lmstudio/models", ] gguf_inventory = scan_gguf_files(search_roots) with open("gguf_inventory.json", "w", encoding="utf-8") as f: json.dump(gguf_inventory, f, indent=2, ensure_ascii=False) print(f"扫描完成,找到 {len(gguf_inventory)} 个 GGUF 文件")5.3 代码解释
parse_gguf_meta是核心函数。它读取文件头的 24 个字节,依次解出 magic、版本号、张量数、元数据键值对数。如果 magic 不匹配,说明文件不是 GGUF 格式,直接返回错误标记,避免后续误处理。
元数据读取部分,先用read_string读键名,再读一个字节的类型编码,最后按类型读取值。这样得到的meta字典里会包含general.architecture(如llama、qwen2)、general.name(如Qwen2.5-7B-Instruct)这类关键字段。general.architecture对应模型内部架构,general.name是面向使用者展示的名字。
脚本默认扫描三个目录:~/models、~/.cache/llama.cpp、~/.lmstudio/models。你可以按需修改这个列表,比如加入外置硬盘路径,但要注意扫描大目录时 glob 递归会耗时,首次运行建议先试一个小目录。
执行命令:
cd ~/llm-inventory python3 wtm_scan_gguf.py6. 核心脚本二:统计 Ollama 模型占用
第二个脚本处理 Ollama 的场景。Ollama 自己维护了完整清单,我们不需要解析 GGUF,只需要阅读 manifest JSON。
6.1 Manifest 路径解析
Ollama 的 manifest 路径通常是:
~/.ollama/models/manifests/registry.ollama.ai/library/qwen2.5/7b这个文件本身没有.json后缀。它的内容是一个 JSON 对象,包含config字段和layers数组。layers数组中的每个元素有mediaType、digest、size字段,size就是对应 blob 文件的逻辑大小。
要把 path 转成用户可读的model:tag,需要取路径的最后两段。上例中是qwen2.5和7b,组合后就是qwen2.5:7b。
6.2 完整代码
#!/usr/bin/env python3 # 文件路径:~/llm-inventory/wtm_ollama_inventory.py import json import os from pathlib import Path OLLAMA_BASE = Path.home() / ".ollama" / "models" MANIFEST_DIR = OLLAMA_BASE / "manifests" def format_size(num_bytes): size = float(num_bytes) for unit in ["B", "KB", "MB", "GB", "TB"]: if size < 1024 or unit == "TB": return f"{size:.2f} {unit}" size /= 1024 def parse_manifest(manifest_path): with open(manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f) total_size = 0 layers = [] for layer in manifest.get("layers", []): digest = layer.get("digest", "") size = layer.get("size", 0) total_size += size layers.append({ "digest": digest, "size_bytes": size, }) config_size = manifest.get("config", {}).get("size", 0) total_size += config_size rel_path = manifest_path.relative_to(MANIFEST_DIR) parts = rel_path.parts model = parts[-2] if len(parts) >= 2 else "unknown" tag = parts[-1] if len(parts) >= 1 else "unknown" display_name = f"{model}:{tag}" return { "model_display": display_name, "manifest_path": str(manifest_path), "total_size_bytes": total_size, "total_size_human": format_size(total_size), "config_size_bytes": config_size, "layers": layers, } def collect_all_manifests(): inventory = [] if not MANIFEST_DIR.exists(): print("未找到 Ollama manifests 目录,请确认已安装 Ollama 并至少下载过一个模型") return inventory for manifest_path in MANIFEST_DIR.rglob("*"): if not manifest_path.is_file(): continue try: item = parse_manifest(manifest_path) inventory.append(item) except json.JSONDecodeError: print(f"警告:跳过无法解析的文件 {manifest_path}") return inventory if __name__ == "__main__": result = collect_all_manifests() with open("ollama_inventory.json", "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) print(f"Ollama 模型统计完成,共 {len(result)} 个模型") for item in result: print(f"{item['model_display']:<35} {item['total_size_human']:>10}")6.3 设计说明
parse_manifest返回的是一个“模型条目”,它把 manifest 里的 layers 数量、每个 layer 的 digest、大小汇总成了便于对比的结构。collect_all_manifests使用rglob递归遍历 manifests 目录,遇到文件就尝试解析,解析失败就跳过并给出警告。
这个脚本没有处理“删除模型后 blob 残留”的问题,因为 manifest 被删除后,对应的模型信息已经不存在,但 blob 文件可能仍然躺在 blobs 目录里。如果想统计这种残留空间,需要额外对比 blobs 目录中所有文件与所有 manifest 引用的 digest,找出没有被引用的文件。这一步放在第 8 节常见问题中讲解。
执行命令:
cd ~/llm-inventory python3 wtm_ollama_inventory.py7. 核心脚本三:生成模型清单报告
最后一个脚本把前两个脚本产出的 JSON 合并,按磁盘占用降序排列,输出一份 Markdown 报告。
#!/usr/bin/env python3 # 文件路径:~/llm-inventory/wtm_summary.py import json import os from datetime import datetime def format_size(num_bytes): size = float(num_bytes) for unit in ["B", "KB", "MB", "GB", "TB"]: if size < 1024 or unit == "TB": return f"{size:.2f} {unit}" size /= 1024 def main(): entries = [] # 读取 GGUF 扫描结果 if os.path.exists("gguf_inventory.json"): with open("gguf_inventory.json", "r", encoding="utf-8") as f: gguf_items = json.load(f) for item in gguf_items: meta = item.get("meta", {}) entries.append({ "source": "gguf", "name": meta.get("general.name") or item["file_name"], "path": item["path"], "size_bytes": item["size_bytes"], "size_human": format_size(item["size_bytes"]), "architecture": meta.get("general.architecture", "未知"), }) # 读取 Ollama 统计结果 if os.path.exists("ollama_inventory.json"): with open("ollama_inventory.json", "r", encoding="utf-8") as f: ollama_items = json.load(f) for item in ollama_items: entries.append({ "source": "ollama", "name": item["model_display"], "path": item["manifest_path"], "size_bytes": item["total_size_bytes"], "size_human": item["total_size_human"], "architecture": "ollama-managed", }) entries.sort(key=lambda e: e["size_bytes"], reverse=True) total = sum(e["size_bytes"] for e in entries) now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") md_lines = [ "# Local LLM Inventory", "", f"- 生成时间:{now_str}", f"- 模型/条目数:{len(entries)}", f"- 逻辑总占用:{format_size(total)}", "", ] for idx, entry in enumerate(entries, 1): md_lines.append(f"## {idx}. {entry['name']}") md_lines.append("") md_lines.append(f"- 来源:`{entry['source']}`") md_lines.append(f"- 架构:{entry['architecture']}") md_lines.append(f"- 路径:`{entry['path']}`") md_lines.append(f"- 大小:{entry['size_human']}({entry['size_bytes']} bytes)") md_lines.append("") with open("LLM_INVENTORY.md", "w", encoding="utf-8") as f: f.write("\n".join(md_lines)) print(f"已生成 LLM_INVENTORY.md,共 {len(entries)} 个条目,总占用 {format_size(total)}") for entry in entries: print(f"{entry['name']:<40} {entry['size_human']:>10} [{entry['source']}]") if __name__ == "__main__": main()执行命令:
cd ~/llm-inventory python3 wtm_summary.py cat LLM_INVENTORY.md输出的报告结构类似:
# Local LLM Inventory - 生成时间:2025-01-15 10:30:00 - 模型/条目数:4 - 逻辑总占用:18.30 GB ## 1. qwen2.5:14b - 来源:`ollama` - 架构:ollama-managed - 路径:`/Users/xxx/.ollama/models/manifests/registry.ollama.ai/library/qwen2.5/14b` - 大小:9.20 GB ## 2. Qwen2.5-7B-Instruct-GGUF - 来源:`gguf` - 架构:qwen2 - 路径:`/Users/xxx/models/qwen2.5-7b-instruct-q4_k_m.gguf` - 大小:4.70 GB8. 运行结果与效果验证
三个脚本串联起来之后,验证流程可以按以下顺序进行。
第一步,运行wtm_scan_gguf.py,如果输出“扫描完成,找到 N 个 GGUF 文件”,说明 GGUF 扫描链路正常。如果某个 GGUF 文件解析失败,脚本不会中断,只会把 error 信息写进该条目的 meta 字段。你可以打开gguf_inventory.json检查具体错误原因。
第二步,运行wtm_ollama_inventory.py。如果之前从未使用过 Ollama,它会提示“未找到 Ollama manifests 目录”。如果 Ollama 已安装并有过模型,输出会列出每个模型的名称和大小,规模通常和ollama list展示的模型数量一致。
第三步,运行wtm_summary.py,生成LLM_INVENTORY.md。打开这份文件,你应该能看到每个模型的名称、来源、路径和大小。最重要的是,这份文件让“删除哪个模型”这类决策变得有依据:先看大小,再看是否还在使用。
要验证统计的准确性,可以在终端里手动抽查一个条目。比如报告显示某个 GGUF 文件 4.7GB,用ls -l查看文件大小,两者应该一致。对于 Ollama 条目,可以用ollama list对比模型数量,用du -sh ~/.ollama/models/blobs对比总体磁盘占用。需要提醒的是,ollama list展示的模型名和 manifest 路径中解析出的名称可能有细微差异,比如带 namespace 的模型,我们在脚本中已经用最后两段路径作为显示名,基本能覆盖主流场景。
如果报告生成后你发现问题,优先检查 LLM_INVENTORY.md 中条目的路径是否正确存在,然后回到对应 JSON 文件确认数据来源。JSON 是最原始的中间产物,报告只是它的可读化呈现,修改逻辑时改动 JSON 才是根因。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 扫描不到任何 GGUF 文件 | 搜索目录配置不对,或模型文件扩展名不是.gguf | 检查脚本中 search_roots 路径,执行find ~ -name "*.gguf"确认实际位置 | 修改 search_roots,加入手工模型所在目录 |
GGUF 解析返回not a GGUF file | 文件被截断、被改名,或根本不是 GGUF 格式 | 用xxd 文件名 | head -n 1查看文件头是否包含GGUF | 删除损坏文件,或重新下载原始 GGUF |
| Ollama 统计结果为空 | Ollama 未安装,或从未下载过模型 | 检查~/.ollama/models/manifests是否存在 | 先运行一次ollama run <模型>,再重新统计 |
| 统计出的逻辑总占用大于实际磁盘占用 | 不同模型共享同一个 blob,或 APFS 文件克隆导致物理共享 | 执行du -sh ~/.ollama/models/blobs查看实际占用 | 报告中保留逻辑大小用于“删除决策”,实际空间评估以du为准 |
| 删除 Ollama 模型后磁盘空间没有显著释放 | manifest 被删除但 blob 残留 | 对比 blobs 目录 digest 与所有 manifest 引用 digest | 用脚本找出未被引用的 digest 文件,确认后手动删除 |
| 脚本在读取超大 GGUF 文件时变慢 | 文件头虽然很小,但 glob 递归扫描整个大目录非常耗时 | 观察是扫描阶段慢还是解析阶段慢 | 缩小搜索目录范围,或先用first-level扫描一层目录测试 |
10. 最佳实践:macOS 本地模型的工程化管理建议
有了清单脚本之后,更重要的是建立一整套模型管理习惯。
第一,统一目录约定。手工下载的 GGUF 模型,建议固定放在~/models下,并按“模型名/量化版本/文件名”的层级组织。例如~/models/Qwen2.5-7B-Instruct/q4_k_m/model.gguf。这样即使不依赖脚本,靠目录结构也能快速定位模型。
第二,优先使用 Ollama 管理常用模型。Ollama 的 manifest 机制天然适合版本管理,它支持ollama pull、ollama push、ollama rm等命令。清单脚本能统计它的大小,但真正的增删操作应该在 Ollama 层面完成,不要直接去 blobs 目录删文件,否则会破坏 manifest 与 blob 的对应关系。
第三,理解逻辑大小与实际占用的区别。macOS 使用 APFS 文件系统,文件克隆和稀疏文件可能导致逻辑大小与占用空间不一致。清单里展示的是逻辑大小,它更接近“模型本身的资源需求”,适合用来估算模型能否本地运行;而查看磁盘余量时,应该用du -sh看实际物理占用。
第四,清理策略要保守。删除模型前,先执行一次清单生成,把要删的模型名、路径、大小记录下来。如果使用 Ollama,优先用ollama rm删除并定期清理隔离空间。对于手工 GGUF 文件,删除前可以移动到废纸篓或备份目录,运行验证一段时间后再彻底删除。
第五,记录模型来源。模型清单适合记录结构化的技术信息,但模型来自哪个 Hugging Face 仓库、许可证是什么、用于什么任务,这些信息建议单独维护一个model_notes.md文件。很多项目事故都源于“不知道模型是从哪来的、能不能商用”。
第六,定期生成清单。把三个脚本组合成一个 shell 脚本,加入定时任务或者每两周手动执行一次。模型目录变化不大时,一份旧的清单也有参考价值,至少能回答“上次盘点时我有哪些模型”。
11. 面向新模型格式的扩展思路
GGUF 和 Ollama manifest 并不是静态标准。GGUF 格式本身有版本号,新版工具解析旧版文件时可能出现元数据字段缺失。Ollama 的维护策略也会变化,比如 blobs 目录的清理机制、manifest 结构都有可能升级。
所以,你的盘点脚本应该保持“容错优先”。解析 GGUF 时遇到未知类型可以返回 null 而不是抛异常,解析 manifest 时遇到字段缺失就给默认值。最终合并报告时,把未知架构统一标记为“未知”,保证清单仍然可读可用。
从 What the Model 这类工具身上,我们可以学到的不是某个具体的算法,而是一种思维:当文件系统里出现大量“看起来很像但身份不明”的数据时,先做一层识别与索引,再做增删决策。这套思路不仅适用于本地 LLM 模型,也适用于容器镜像、机器学习数据集、构建缓存等场景。
12. 总结与下一步行动
这篇文章从 macOS 本地模型目录混乱的痛点出发,解释了 GGUF 文件格式的文件头结构,介绍了 Ollama manifest 的统计原理,并给出了三个可独立运行的 Python 脚本。现在你已经可以扫描手工下载的 GGUF 文件、统计 Ollama 管理的模型占用、合并生成一份 Markdown 格式的模型清单。
下一步,建议先运行一次完整流程,看看自己的模型目录里到底是什么情况。如果数据超过预期,不要急着删模型,先把清单保存一份,再按照第 10 节的最佳实践逐步清理。如果你想更进一步,可以考虑给脚本加上一个--csv导出参数,或者把清单输出为 JSON 格式供其他工具消费。模型管理是一件需要长期维护的事情,清单就是这一切的基础。