1. 一次意料之外的管线崩溃那天下午我像往常一样把刚跑通的 AI 审计管线指向了一个真实开源仓库。在此之前它已经在十几个中小型项目上表现稳定单次分析耗时控制在几分钟以内输出报告的质量也让我比较满意。然而这一次任务提交后不到两分钟后台就弹出了内存告警紧接着整个管线进程被系统强制终止。我打开日志一看输入侧的 token 计数赫然写着908K tokens——这个数字远远超出了我预设的 128K 上下文窗口也超出了我此前所有测试用例的总和。这件事让我意识到一个很现实的问题在实验室里跑得通的东西放到真实世界的代码仓库面前可能连第一轮都撑不过去。我做的这套 AI 审计管线核心思路是用大模型对代码仓库做静态审查找出潜在的安全隐患、逻辑缺陷和代码异味。它由几个模块组成仓库拉取与预处理、文件过滤与分块、上下文组装、模型调用、结果聚合与报告生成。在小型项目上这套流程运转良好因为文件数量少、单文件体积小token 总量天然可控。但真实开源仓库完全是另一回事——几千个文件、几十种语言、大量自动生成的代码、测试夹具、第三方依赖副本全部堆在一起token 量呈指数级膨胀。这篇文章不是要讲一个“我如何修好了一个 bug”的简单故事而是想完整复盘这次排查过程908K tokens 到底是怎么来的、管线在哪个环节被撑爆、我做了哪些取舍和改造、最终如何让它在真实仓库上稳定运行。如果你也在做类似的大模型应用尤其是涉及长文本处理、代码分析、RAG 管线这类场景这些踩坑经验应该能帮你少走一些弯路。2. 管线架构与 token 膨胀的根源2.1 原始管线的设计思路我最初的管线设计并不复杂甚至可以说有些“天真”。整体流程是这样的先用 git 把目标仓库克隆到本地然后遍历所有文件跳过二进制文件和明显的依赖目录把剩下的文本文件按扩展名分类。接着对每个文件做简单的行数统计和内容读取拼接成一个巨大的字符串最后交给模型做一次性分析。为了让模型有足够的上下文我选择了支持 128K 上下文的模型并且在 prompt 里明确要求它“逐文件审查输出问题列表”。这个设计在小型项目上没问题因为文件少、代码短拼接后的字符串通常只有几万 token。但问题在于我从来没有对输入规模做过硬性约束。换句话说管线的 token 消耗完全取决于仓库本身的大小而我对此没有任何控制手段。当仓库从几百个文件变成几千个文件时token 量自然就失控了。2.2 908K tokens 的构成拆解为了搞清楚这 908K tokens 到底来自哪里我在管线里加了一段统计代码按目录和文件类型分别汇总 token 数。结果出来后问题一目了然类别文件数估算 token 数占比核心源码约 1200约 310K34%测试文件约 800约 220K24%第三方依赖副本约 400约 180K20%文档与注释约 300约 90K10%自动生成代码约 200约 70K8%其他杂项约 100约 38K4%从这张表可以清楚看到核心源码只占了三分之一左右剩下的大部分 token 都消耗在了测试文件、依赖副本和自动生成代码上。这些内容对于安全审计来说价值密度极低甚至完全是噪声。但在原始管线里它们和核心源码被一视同仁地拼接进了 prompt导致 token 量直接爆炸。这里有一个很关键的认知偏差我一开始以为“代码仓库”就是“源码集合”但实际上一个成熟的开源仓库里源码可能只占很小一部分。测试、文档、依赖、生成物、配置文件、资源文件这些东西加起来往往比源码本身还大。2.3 为什么不能简单粗暴地截断发现问题后我的第一反应是“那就截断呗只取前 N 个 token”。但很快我就否定了这个方案。原因很简单代码审计不是摘要任务截断意味着丢失信息。如果我只取前 128K tokens那很可能只覆盖了仓库的一小部分文件后面的核心逻辑完全没被审查到。这样的审计报告是没有意义的甚至会给人虚假的安全感。另一个思路是“按文件分批调用模型”每次只分析一个文件或一组文件。这个方向是对的但需要解决几个问题如何分组才能保证上下文完整性、如何避免重复分析、如何聚合跨文件的问题。这些都不是简单截断能解决的。所以真正的挑战不是“如何把 908K 压到 128K”而是“如何设计一套分块与调度机制让模型在有限上下文内尽可能覆盖更多有价值的内容同时不丢失关键信息”。3. 分块策略与上下文组装改造3.1 文件过滤先做减法再做加法改造的第一步是过滤。我重新梳理了文件筛选规则把那些对审计价值极低的内容直接排除掉。具体来说以下几类文件被移出了分析范围第三方依赖副本比如 vendor、node_modules、third_party 这类目录里面的代码不是项目自身维护的审计意义不大。自动生成代码比如 protobuf 生成文件、ORM 生成文件、编译产物等这些代码通常有固定模式人工审查价值低。测试夹具与快照比如 testdata、fixtures、snapshots 目录里面的内容大多是测试输入输出不是业务逻辑。二进制与资源文件图片、字体、压缩包等直接跳过。文档与注释这个要分情况。README、CHANGELOG 这类文档可以保留摘要但大段的 API 文档、教程内容可以压缩或跳过。过滤之后token 总量从 908K 降到了大约 420K。虽然还是远超 128K但至少去掉了一半以上的噪声。实操心得过滤规则不要写得太死。我一开始把 tests 目录整个排除了后来发现有些项目的测试文件里包含了重要的边界条件用例对理解业务逻辑很有帮助。所以后来我改成“保留测试文件但降低其优先级”在 token 预算紧张时优先丢弃。3.2 分块策略按模块而非按文件过滤之后下一步是分块。我试过几种方案按文件分块每个文件单独作为一个 chunk。问题是文件之间可能有关联单独分析容易漏掉跨文件的问题。按目录分块每个目录作为一个 chunk。问题是目录大小不一有的目录几十个文件有的只有一两个。按依赖关系分块根据 import/require 关系聚类。这个方案最理想但实现复杂度高需要先做静态分析。最终我选择了一个折中方案按模块分块辅以依赖关系调整。具体做法是先按目录结构做初步分组然后根据文件间的引用关系把强关联的文件合并到同一个 chunk 里。每个 chunk 的大小控制在 8K 到 12K tokens 之间这样既能保证上下文完整性又不会超出单次调用的限制。分块之后我得到了大约 40 个 chunk。每个 chunk 独立调用模型分析最后把结果聚合起来。这样总 token 消耗虽然还是 420K 左右但因为是分批调用单次调用的上下文压力小了很多模型的分析质量也明显提升。3.3 上下文组装给模型足够的“背景信息”分批调用带来一个新问题模型在分析单个 chunk 时看不到全局信息。比如一个函数在 A 文件里定义在 B 文件里调用如果 A 和 B 被分到了不同的 chunk模型就很难发现潜在的问题。为了解决这个问题我在每个 chunk 的 prompt 里加入了一段“全局摘要”。这段摘要包含仓库的整体结构说明有哪些主要模块、各自职责是什么当前 chunk 在整体中的位置和依赖关系关键接口和数据结构的定义从其他 chunk 中提取这样模型在分析局部代码时也能有一个全局视角。实测下来这种方式能显著减少“误报”和“漏报”。注意事项全局摘要本身也会消耗 token所以要控制好长度。我的做法是先用一个小模型对仓库做一次快速摘要生成 500 到 1000 字的概述然后把这个概述注入到每个 chunk 的 prompt 里。这样额外消耗的 token 不多但效果很好。4. 模型调用与结果聚合的工程细节4.1 并发控制与速率限制40 个 chunk 如果串行调用耗时太长如果全部并发又容易触发速率限制。我的做法是控制并发数在 4 到 6 之间并且加入指数退避重试机制。具体参数上我设置了最大重试次数为 5 次初始退避时间为 2 秒每次翻倍。实测下来这个配置在大多数情况下都能稳定跑完。另外我还加了一个令牌桶来做速率控制。桶的容量设为每分钟 60 次请求这样既能充分利用配额又不会因为突发流量被限流。4.2 结果聚合去重与优先级排序每个 chunk 分析完后会输出一个 JSON 格式的问题列表。聚合阶段需要做几件事去重同一个问题可能被多个 chunk 报告需要根据文件路径、行号、问题类型做去重。优先级排序根据问题严重程度严重、警告、提示和置信度排序让最重要的发现排在前面。跨文件关联把涉及多个文件的问题合并成一条附上相关文件列表。聚合后的报告结构大概是这样的{ summary: { total_issues: 47, critical: 3, warning: 18, info: 26 }, issues: [ { id: ISSUE-001, severity: critical, type: security, files: [src/auth/login.py, src/auth/session.py], description: 会话令牌未设置过期时间, recommendation: 在生成令牌时添加过期时间戳 } ] }4.3 成本与耗时的权衡改造后的管线单次完整审计的 token 消耗大约在 450K 到 500K 之间耗时约 8 到 12 分钟。相比最初的“一次性调用”成本确实高了一些但换来的是可运行、可扩展、结果质量可控。我认为这个 trade-off 是值得的。如果你也想控制成本可以考虑几个方向用更小的模型做初筛、对低优先级文件做抽样分析、缓存重复仓库的分析结果。这些策略我在后续迭代中都试过效果不错。5. 常见问题与排查技巧实录5.1 token 计数不准怎么办不同模型对 token 的计数方式不一样同一个字符串在不同 tokenizer 下可能差出 10% 到 20%。我的建议是以目标模型的 tokenizer 为准在管线里统一使用同一个计数工具。如果实在拿不准可以留 10% 的余量。5.2 模型输出格式不稳定让模型输出 JSON 是个常见需求但模型有时候会加一些额外的解释文字导致解析失败。我的做法是在 prompt 里明确要求“只输出 JSON不要任何其他内容”并且在解析时加一层容错先用正则提取 JSON 块再尝试解析。如果失败就重试一次并在 prompt 里强调格式要求。5.3 分块边界导致漏报分块最大的风险就是边界问题。一个完整的逻辑被切到两个 chunk 里模型可能两边都看不出问题。我的应对策略是在分块时尽量保持函数和类的完整性不要在一个函数中间切开。如果实在无法避免就在两个 chunk 的 prompt 里都注明“此代码与另一个 chunk 有关联”提醒模型注意跨块逻辑。5.4 常见问题速查表问题现象可能原因排查方向解决方案管线内存溢出输入 token 过大统计各目录 token 占比过滤低价值文件分块调用模型输出截断超出最大输出长度检查 max_tokens 设置分批输出或要求模型精简分析结果重复分块重叠或聚合去重不彻底检查 chunk 划分和去重逻辑加严去重规则按文件行号去重调用频繁被限流并发过高查看 API 返回的限流错误降低并发加退避重试报告质量差上下文不足或 prompt 不清晰检查 prompt 和全局摘要补充背景信息明确输出要求最后分享一个小技巧在管线里加一个“干跑”模式只做 token 统计和分块不实际调用模型。这样可以在正式分析前快速评估仓库规模避免盲目提交任务导致资源浪费。6. 从这次排查中沉淀下来的经验回过头看908K tokens 这件事表面上是“输入太大”本质上是我对真实场景的复杂性估计不足。实验室里的测试用例都是精心挑选的文件少、结构清晰、噪声低。但真实仓库是混乱的、庞大的、充满意外的。做 AI 应用尤其是涉及长文本和代码分析的应用一定要把“最坏情况”考虑进去。我现在养成了一个习惯任何管线在设计阶段就要问自己如果输入规模扩大 10 倍会发生什么。这个习惯帮我避免了很多后续的返工。另外分块和过滤不是“降级方案”而是工程化的必然选择。与其追求一次性处理所有内容不如设计一套可扩展的分批处理机制让系统在规模增长时依然稳定。这套改造后的管线后来我又在几个不同规模的开源仓库上做了验证最大的一个仓库过滤后仍有 600K tokens但通过分块调度最终也能在 15 分钟内完成审计。虽然离“实时”还有距离但至少不会再被撑爆了。