AI项目代码瘦身实战:静态分析工具揪出3.3万冗余令牌
1. 项目概述一个命令行工具如何帮我揪出3.3万个“膨胀”令牌最近在优化一个AI智能体项目时我遇到了一个典型的“技术债”问题项目启动越来越慢构建体积越来越大但代码功能似乎并没有等比增加。直觉告诉我项目里肯定塞了不少“垃圾”——那些没被用到的依赖、冗余的代码文件、过大的资源。但靠人眼去一个个文件排查无异于大海捞针。于是我决定自己动手写一个命令行工具来干这个脏活累活。这个工具的核心目标很明确像一个精准的“代码审计员”扫描整个项目代码库找出所有未被实际引用的、冗余的、或体积异常大的资源并量化它们的“浪费”程度。最终这个工具交出了一份惊人的“体检报告”它成功捕获了高达33,531个令牌Tokens的项目“膨胀物”。这里的“令牌”是当前衡量AI项目特别是基于大语言模型LLM的智能体项目资源消耗的一个关键指标。无论是OpenAI的API按Token计费还是本地模型的上下文窗口限制冗余的Token都直接意味着更高的成本和更低效的性能。这个工具不仅仅是一个简单的查找-删除脚本。它需要理解项目的依赖结构、模块引用关系、静态资源导入甚至要能解析一些动态加载的模式。在接下来的内容里我会详细拆解这个工具的设计思路、实现的关键技术点、实际操作的步骤以及在这个过程中踩过的坑和总结出的经验。无论你是正在维护一个日益臃肿的代码库还是刚刚启动一个新项目希望建立良好的“身材管理”意识相信这些实战经验都能给你带来直接的参考价值。2. 工具整体设计与核心思路拆解2.1 为什么是“令牌”而不是“行数”或“字节数”在决定量化指标时我首先排除了传统的“代码行数LoC”和“文件大小Bytes”。对于现代AI项目特别是智能体应用这两个指标失真度太高。代码行数一句复杂的正则表达式可能只有一行但包含的Token数量很多而大量空行和格式化的注释虽然增加了行数但对AI模型处理几乎无影响。文件大小一个1MB的JSON配置文件如果全是结构化的数据其Token数可能远低于一个200KB的纯文本文档因为Token化过程会拆分单词和符号。“令牌Token”是大语言模型处理文本的基本单位。在英文中一个Token大约等于0.75个单词在代码中变量名、函数名、操作符、标点都可能被拆分成独立的Token。因此用Token数来衡量“膨胀”是最贴近AI项目运行时真实成本API调用费和性能瓶颈上下文长度的指标。我的工具最终统计出33,531个冗余Token这个数字可以直接翻译成“如果你的智能体每次运行都要加载这些垃圾那么你每次都在为这3.3万个无用的Token付费或消耗宝贵的上下文窗口。”2.2 核心扫描策略静态分析与启发式规则结合工具的设计哲学是“守正出奇”。“守正”是指依靠可靠的静态代码分析Static Code Analysis来建立基准“出奇”则是针对项目特性制定一系列启发式规则Heuristic Rules来捕捉静态分析容易遗漏的“死角”。1. 依赖项分析Dependency Analysis这是清理的第一道大门。工具会读取package.json(Node.js)、requirements.txt/pyproject.toml(Python)、Cargo.toml(Rust) 等依赖声明文件。然后它会遍历项目所有源代码文件构建一个完整的导入import/require关系图。任何在声明文件中列出但在关系图中没有任何节点文件引用的依赖包都会被标记为“未使用依赖”。这一步通常能揪出最大的“块状垃圾”。2. 文件级引用分析File-level Reference Analysis对于项目内部的源代码文件.js,.py,.rs,.ts等和可能被引用的资源文件.json,.yaml,.md等工具会扫描所有文件内容构建一个文件之间的引用网络。一个完全不被任何其他文件导入或引用的文件就是潜在的“孤儿文件”。但这里要小心对于入口文件如main.py,index.js、配置文件、测试文件、构建脚本等需要设置白名单或特殊规则。3. 代码块与死代码探测Dead Code Detection在单个文件内部工具会进行简单的语法分析利用现成的解析库如Python的astJavaScript的babel/parser识别出从未被调用的函数、从未被访问的类或变量。这属于更细粒度的清理。对于动态语言如Python、JavaScript完全准确的死代码检测很难工具会采用保守策略只标记那些明确被导出export却未被其他模块导入的项以及一些明显的、无法通过任何执行路径到达的代码块。4. 资源文件与体积审计Asset Audit这是启发式规则发挥作用的主场。工具会扫描项目中的图片、字体、视频等静态资源。规则一未被引用的大文件。任何在HTML、CSS、JS文件中找不到引用路径且体积超过阈值如100KB的图片/字体文件高风险。规则二重复或近似文件。通过计算文件的哈希值如MD5或感知哈希对于图片找出内容完全一致或视觉上近乎一致的文件它们通常是版本管理或粗心复制留下的。规则三非优化格式。例如发现了未压缩的.png文件而工具目录下存在压缩后的.webp或优化后的.png版本原文件可能就是冗余的。2.3 工具架构与技术选型为了让工具灵活、可扩展我采用了经典的“插件式”架构。核心引擎Core Engine 使用Go语言编写。Go的并发特性goroutine非常适合并行扫描多个目录和文件编译后是单个二进制文件分发和运行极其简单无需环境依赖。解析器插件Parser Plugins 针对不同语言和文件类型编写独立的插件。例如javascript-parser 使用babel/parser和babel/traverse来构建JS/TS的AST并分析导入导出。python-parser 使用Python内置的ast模块工具本身是Go的但此插件会调用一个Python脚本或使用Go的python3绑定。generic-parser 用于JSON、YAML、文本文件等主要做模式匹配和引用查找。输出与报告Reporter 扫描结果会输出为结构化的JSON同时提供一个丰富的命令行报告界面支持颜色高亮、分级警告/错误、以及建议操作删除/移动/压缩。注意 在技术选型上没有选择用Python“一把梭”虽然Python在写解析脚本时更快。主要考虑是工具的性能和部署体验。Go编译后的二进制文件用户下载即用速度也更快更适合集成到CI/CD流水线中作为一道固定的质量关卡。3. 关键模块解析与实现细节3.1 依赖关系图的构建算法这是工具最核心的部分其准确度直接决定了误报和漏报率。我实现了一个基于图论的深度优先搜索DFS算法。步骤拆解节点定义 将每个文件包括入口文件、库文件和每个第三方依赖包都视为图中的一个节点。边定义 如果文件A中通过import B或require(B)导入了文件B或包B则创建一条从A指向B的有向边。入口启动 从项目明确的入口文件如src/main.js开始执行DFS。递归遍历 访问一个节点文件时调用对应的语言解析器插件提取该文件中所有的导入语句。对于每个导入目标如果是相对路径或绝对路径./utils,../components/Button则在文件系统中定位该文件将其作为新节点加入图中并创建边然后递归遍历这个新文件。如果是包名lodash,react则标记该包节点被引用。记录已访问 使用哈希表记录已访问过的文件节点避免因循环引用或重复引用导致无限递归。结果生成 DFS结束后图中所有能从入口文件到达的节点就是“被使用的”部分。那些在依赖声明文件中存在但对应的包节点没有任何入边即没有被任何文件引用的就是“未使用依赖”。一个具体的例子假设package.json声明了依赖lodash和moment。文件src/index.js中import _ from lodash。文件src/utils/date.js中import moment from moment但date.js本身没有被index.js或任何其他入口文件导入。 那么在从src/index.js开始的DFS中lodash会被标记为已使用而moment虽然被date.js导入但date.js这个节点本身无法从入口到达因此moment最终也会被判定为未使用因为使用它的模块是“孤儿模块”。3.2 令牌Token计数的实现为了计算被标记为冗余内容的Token数我需要一个与主流LLM如OpenAI的GPT系列兼容的Tokenizer。直接调用OpenAI的API来计算太慢且昂贵。我选择了Tiktoken这个开源库。集成方式 Tiktoken是Python库我的核心工具是Go。因此我实现了一个轻量级的gRPC或HTTP服务也可以简单封装为子进程调用专门用于Token计数。扫描器将需要计数的文本块发送给这个计数服务服务返回Token数量。计数范围对于未使用的依赖 计算其整个安装目录下所有.py/.js等源代码文件的Token总和。这模拟了如果你将整个库代码内联到项目中所占用的上下文。对于未使用的本地文件 直接计算整个文件的Token数。对于死代码块 从AST中提取出该代码块对应的源代码字符串然后计算其Token数。编码选择 我主要使用了cl100k_base编码这是GPT-4、ChatGPT等模型使用的编码器确保计数标准与当前主流成本核算一致。3.3 启发式规则的具体实现案例以“检测未被引用的大体积资源文件”规则为例收集资源文件 递归扫描项目找出所有.png,.jpg,.jpeg,.gif,.webp,.svg,.woff,.woff2,.mp4,.webm等文件并记录其路径和大小。构建资源引用索引扫描所有.html,.jsx,.tsx,.vue,.css,.scss等文件。使用正则表达式匹配src...“,url(...),require(”...“),import ”...“等模式提取出引用的资源路径。将这些路径解析为相对于项目根目录的绝对路径并存入一个哈希集合Set中记为“被引用的资源”。执行检测遍历所有收集到的资源文件。如果某个文件的路径不在“被引用的资源”集合中并且其文件大小超过预设阈值例如 100KB则将其标记为“疑似未引用大文件”。二次确认 对于标记出的文件工具会输出警告并建议用户手动确认。因为有些资源可能是通过JavaScript动态构建路径加载的静态分析无法捕获。这时报告里会提示“此文件未被静态引用请确认是否为动态加载。”4. 实战操作集成到现有项目进行扫描4.1 安装与快速开始我将工具打包发布到了GitHub并提供了多种安装方式。# 方式一使用Go安装需已安装Go go install github.com/yourusername/code-dietlatest # 方式二直接下载预编译二进制文件适用于Mac/Linux/Windows # 从GitHub Releases页面下载对应系统的压缩包解压后即可运行 # 方式三使用Docker docker run -v $(pwd):/app ghcr.io/yourusername/code-diet:latest scan /app进入你想要分析的项目根目录然后运行code-diet scan .工具会开始递归扫描当前目录并实时在控制台输出进度。扫描完成后会生成一份详细的HTML报告report.html和一个结构化的JSON结果文件code-diet-results.json。4.2 解读扫描报告报告会分为几个主要部分1. 摘要仪表盘以醒目的数字展示总发现冗余Token数本例中就是33,531、受影响文件数、可安全删除的依赖项列表。这是给管理者看的“成绩单”。2. 问题详情列表这是开发人员需要仔细审查的部分。问题会被分类未使用依赖 列出包名、版本、估算的冗余Token数。例如package: “moment”, version: “^2.29.1”, unusedTokens: 12450。未引用文件 列出文件路径、大小、Token数。例如file: “src/assets/old-banner.png”, size: “450KB”, unusedTokens: 850。死代码 列出文件路径、死代码类型未使用函数、未使用变量、代码片段预览、Token数。重复资源 列出重复文件组并建议保留哪一个通常保留第一个被引用的或路径最短的。3. 可执行建议对于每个问题报告会提供一个或多个建议命令。这是最实用的部分。对于未使用依赖npm uninstall moment或pip uninstall -y some-package对于未引用文件rm -rf src/assets/old-banner.png对于死代码直接高亮显示代码位置手动删除。4.3 安全清理策略不要盲目相信工具工具再智能也有误判的可能。因此我强烈建议采用“报告 - 审查 - 分批删除 - 验证”的流程。生成基线报告 在任何修改前先运行工具生成报告并备份当前代码git commit。人工审查 逐条审查报告中的问题。特别是“未引用文件”要确认是否真的被动态加载。对于“死代码”检查是否是公共API的一部分或者被反射Reflection机制使用。分批操作 不要一次性删除所有标记项。建议按类别分批处理第一优先级 删除明确未使用的第三方依赖。风险最低收益最大。第二优先级 删除确认为垃圾的图片、字体等资源文件。第三优先级 清理内部未使用的工具函数、组件文件。删除前确保没有其他分支或项目在引用它们。最后处理 删除单个文件内的死代码。这部分改动可能涉及多个文件需要运行测试来确保功能正常。验证 每完成一批操作运行项目的测试套件npm test,pytest等并手动进行核心功能的冒烟测试确保没有破坏任何功能。5. 常见问题与排查技巧实录在开发和实际使用这个工具的过程中我遇到了不少典型问题。这里记录下最关键的几个及其解决方法。5.1 误报为什么工具说这个依赖/文件没用但我明明在用这是最高频的问题。通常有以下几个原因动态导入Dynamic Import 现代前端项目大量使用import(‘module’)或require([‘module’], callback)。静态分析工具很难确定这些动态字符串在运行时到底会不会执行。解决方案 工具提供配置白名单。在项目根目录创建.codedietignore文件类似.gitignore将确需保留但被误报的模块或文件路径加入其中。例如- dependency: “lodash” # 我们通过动态导入使用它。插件式架构或依赖注入 某些框架如Webpack插件、Babel插件、Express中间件的依赖是在配置文件中以字符串形式声明而不是在代码中直接import。解决方案 工具扩展了配置文件扫描。它会主动读取webpack.config.js,.babelrc,vite.config.ts等文件尝试解析其中的字符串字面量作为依赖名。但这仍然不完美白名单是最可靠的兜底方案。类型声明文件.d.ts 在TypeScript项目中你可能安装了types/package-a只为类型检查但运行时并不需要。工具可能将其标记为未使用。解决方案 工具对types/开头的包有特殊处理。在报告里它们会被归类为“类型依赖”并单独列出。你可以选择保留它们以确保开发体验或者如果确认不需要再移除。5.2 漏报有些明显没用的东西工具为什么没扫出来漏报比误报更危险因为它让你误以为项目很干净。条件式导入Conditional Import 代码可能写在if (false) { import(‘something’); }里静态分析会认为这条导入语句永远执行不到从而忽略。但实际上这个条件可能在构建时被不同的环境变量改变。解决方案 目前的工具对这种情况无能为力这是静态分析的固有局限。需要靠代码审查和良好的开发习惯来避免。依赖的依赖Transitive Dependencies 你的项目依赖了包A包A又依赖了包B和包C。即使你的代码只用了包A的一小部分功能这部分功能可能只依赖包B工具也会把整个包A及其依赖的B、C都算作“被使用”。因为从入口分析包A被引用了。解决方案 这是一个更深层的问题。更高级的工具如Webpack的Bundle Analyzer可以分析最终打包产物知道B和C的哪些代码被打包了。我的CLI工具定位是快速扫描不深入到打包层面。对于深度优化建议结合打包分析工具使用。5.3 性能问题扫描大型项目时速度很慢怎么办当项目有成千上万个文件时尤其是需要解析AST速度可能成为瓶颈。启用缓存 工具实现了基于文件哈希的缓存机制。首次扫描后会将每个文件的解析结果导入列表、导出列表缓存起来。下次扫描时如果文件内容未变通过哈希判断则直接使用缓存结果跳过耗时的解析步骤。并行处理 利用Go的goroutine对不同的目录或文件类型进行并发扫描。I/O操作和CPU密集的解析操作可以一定程度上重叠。限制解析深度 对于node_modules这类第三方依赖目录默认只进行依赖声明分析而不深入解析其内部的所有源代码文件除非需要计算该依赖的Token数。这能极大提升速度。使用.codedietignore 忽略那些你明确知道无需扫描的目录如dist/,build/,.git/, 庞大的日志目录等。5.4 集成到CI/CD流水线要让“代码减脂”成为团队习惯集成到自动化流程是关键。# 一个GitHub Actions工作流的示例片段 name: Code Health Check on: [push, pull_request] jobs: scan-for-bloat: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Run Code Diet Scanner uses: your-org/code-diet-actionv1 # 假设你封装了GitHub Action with: path: ‘.’ fail-on-tokens: 10000 # 如果发现超过10000个冗余Token则标记步骤失败 - name: Upload Report uses: actions/upload-artifactv4 if: always() # 即使失败也上传报告 with: name: code-diet-report path: code-diet-report.html在PR流程中这个检查可以作为一个“门禁”。如果引入了超过一定阈值如5000个Token的冗余代码或未使用依赖CI会失败提醒开发者先清理代码再合并。fail-on-tokens参数让这个阈值可配置团队可以根据项目阶段调整容忍度。6. 从33,531个令牌中获得的经验与反思做完这个工具并成功清理了自己的项目后我得到的远不止一个干净的项目目录。最大的收获是对“项目膨胀”有了更系统化的认知。第一膨胀是静默发生的。很多时候我们引入一个依赖只是为了用其中一个很小的工具函数我们添加一张图片做测试后来换了图却忘了删旧的我们重构代码后留下了许多不再被调用的函数。这些“垃圾”不会主动报错它们只是静静地躺在那里每次构建、每次部署、每次AI智能体加载上下文时都在消耗着资源和时间。没有工具你几乎无法感知它们的存在。第二度量是优化的前提。“33,531个令牌”这个具体的数字比任何“项目有点臃肿”的模糊感觉都有力得多。它让技术债变得可见、可衡量、可追踪。团队可以设定目标比如“每个季度将冗余Token数降低10%”这让代码维护从一个模糊的“好习惯”变成了一个可管理的工程任务。第三工具应该融入流程而非一次性活动。最初我只是想写个脚本清理一下自己的项目。但后来我意识到最有效的用法是把它变成开发流程的一部分。就像写代码时要通过lint检查、要通过单元测试一样提交代码前也应该通过“膨胀度”检查。把它集成到pre-commit钩子或CI/CD中才能防止垃圾代码重新溜回来。最后工具无法完全替代人脑。这个CLI工具再厉害它也是一个基于规则和静态分析的自动化程序。它会有误报需要你通过白名单忽略也会有漏报需要你保持代码简洁的良好意识。它的核心价值是把你从繁琐的、机械的查找工作中解放出来让你能把宝贵的注意力集中在那些需要复杂判断的清理决策上。它是一位不知疲倦的助理帮你列出清单但最终决定哪些该扔进垃圾桶的还是作为架构师的你。清理完那3万多个令牌后我的智能体项目冷启动速度提升了近15%构建产物体积减少了约20%。更重要的是代码库看起来清爽多了新成员阅读代码时的认知负担也显著降低。如果你也在为项目的“肥胖”问题烦恼不妨从构建一个属于你自己的“代码体检仪”开始或者就直接试试那些已经开源了的优秀静态分析工具。