大家好,我是苏晚。前两天我干了一件很多开发者在脑子里想过、但没真下手的事——把整个微服务仓库,没错,整整 5 万多个文件,一次性扔进 Gemini 1.5 Pro 的上下文窗口里,然后让它给我做架构洞察、找出反模式,再自动输出重构成 diff。
结果怎么着?它还真画出了一张挺像样的全局依赖图,甚至标出了我过去两年都没发现的循环依赖。只是中间过程太刺激了:Google Cloud 的配额卡死、模型引用函数名字时胡言乱语、还有一次差点因为我少传一个字段把实验环境的 CI 全炸掉。这篇文章就是我在这个项目里从“这玩意儿真能用”到“妈的再也不搞了”再回到“好像还挺香”的心路全记录。
别把它当成工具测评,我没兴趣给谁带盐。我就是想跟你聊聊,当你把“百万 token 上下文”从宣传语变成真·工程工具的时候,到底要踩多少坑、怎么让输出可信、以及钱到底烧到了什么程度。
30秒速览
- - 要往 Gemini 1.5 Pro 里塞整个仓库,先得自己写格式化器瘦身,并提前申请 Vertex AI 的大上下文配额,否则会被 429 玩死。
- - 分三阶段构建心智地图、模块边界识别、反模式挖掘,比发一条超长 prompt 幻觉少得多。
- - 自动重构 diff 必须接静态分析、编译检查和单元测试流水线,模型不懂框架隐式约定和 goroutine 泄漏这类魔法。
- - 模型给出的代码引用必须交叉验证:要求路径+行号、自动化 grep 抽查、换角度复述问题,不然你会被它一本正经的假引用骗过去。
一、把五万文件塞进一个请求,听着很神,但光准备数据就差点把我送走
1.1 5 万文件不是直接往里灌的,我写了个“流式格式化器”
我这次分析的仓库是一个运行了三年多的微服务体系,Java、Go、Python 混着来,proto、yaml、SQL 脚本一应俱全。最早的一版我偷懒,直接用 find + cat 把所有内容拼成一个超长字符串,想直接糊给 API。结果 Vertex AI SDK 在本地 OOM 了——32GB 的 Mac 直接卡死。
冷静下来我才意识到,Gemini 1.5 Pro 虽然支持百万 token,但没说你可以在客户端随便构造这么巨大的 payload。我得先做文件过滤和内容瘦身:排除 node_modules、.git、target 等目录,对二进制文件直接跳过,对超过 50KB 的大文件只保留头部注释和前 200 行代码。然后我写了一个 Python 脚本,把每个文件包装成带有路径和语言标识的标准化 text chunk:(延伸阅读:多模态Agent的评测,我们一直在用错尺子——从轨迹对齐到目标达成的严格考试)
def format_repo_for_gemini(root_dir: str, max_file_kb: int = 50) -> str:
chunks = []
for fpath in sorted(Path(root_dir).rglob('*')):
if any(part.startswith('.') or part in BLACKLIST for part in fpath.parts):
continue
if not fpath.is_file() or fpath.stat().st_size > max_file_kb * 1024:
continue
try:
content = fpath.read_text(errors='ignore')
except UnicodeDecodeError:
continue
header = f'--- FILE: {fpath.relative_to(root_dir)} ---n'
header += f'LANGUAGE: {guess_lang(fpath)}n'
chunks.append(header + content)
return 'nn'.join(chunks)
处理完后,整个仓库的纯文本大小降到了 22MB 左右,估算 token 数约 60 万,稳稳落在 Gemini 的 1M 窗口内。但光是这个“瘦身”脚本,我就调了大概十几次,因为各种编码问题、换行符不一致导致文件拼接后总 token 数预估不准。后面我干脆在脚本尾部加上一个基于 tiktoken 的计数,超过 90 万就强制截断。
1.2 第一次请求就撞了 Vertex AI 的配额墙,差点以为项目还没开始就结束了
数据准备好,我兴冲冲地在 Vertex AI 上建了个端点,发了第一个请求。结果报错 429:Quota exceeded for 'GenerateContent'。我当时心态就崩了——因为按照默认配额的每分钟 60 个请求、每个请求最多 10 万 token 算,我的 60 万 token 请求根本连发都发不出去。
后来看文档才发现,长上下文模型的配额和普通模型是分开的,而且对于超过 128k 输入 token 的请求,需要特别申请“在线预测大上下文配额”。我提了一个工单,跟 Google Cloud 支持扯了三封邮件,等了大概 18 个小时终于把 OnlineLargeInputQuota 从 0 调成了 2 个并发。就为了发一个请求,费了这么大劲。
更离谱的是,调整后的配额仍然按每分钟 token 数限流,我的请求 token 数太大,一个请求就吃掉了那一分钟的绝大部分额度,所以后面如果连续发第二个大请求就会立刻 429。我最后只能搞了个手动延时重试的装饰器,每个请求之间睡 70 秒,这才稳住。所以如果你也想搞这种一次性塞整个仓库的操作,提前把配额调整到至少每分钟 300 万输入 token 级别,不然根本别想顺畅跑起来。(延伸阅读:我用三个框架跑了同一批模型,结果只有一个活得过生产环境)
二、我设计了一套“心智地图”提示链,长上下文分析终于从玩具变成了真武器
2.1 别一上来就问“这项目有啥问题”,先让模型画地图才是正解
很多长上下文用例翻车,就是因为 prompt 写得太笼统。你如果直接问:“这个仓库有什么架构违规?”,模型很容易在几十万 token 里迷失,给你一些不痛不痒的回答,或者干脆编造出根本不存在的依赖关系。我吃过这种亏,所以这次我设计了三阶段提示链:
第一阶段:建立全局心智地图
不要求分析,只让模型输出每个顶级目录的作用、包含的关键模块、以及对外暴露的接口摘要。这个阶段的输出大概 3000 行,像一份粗粒度的“代码黄页”。Gemini 1.5 Pro 对这个任务完成度极高,几乎没有幻觉,因为它的注意力只需要集中在目录级别的聚合特征上。(延伸阅读:我让Codestral Mamba在256k上下文中跑补全,速度是GPT-4的3倍,但上下文管理差点让我翻车)
第二阶段:识别模块边界和跨域依赖
我把第一阶段输出的目录索引再喂回模型,同时附上一份简化版的模块间 import 关系(我用 jq + rg 手动提取的)。然后让 Gemini 画一张“依赖关系示意图”,用 mermaid 语法描述,并标出哪些模块违反了分层原则。这一步开始出现有意思的现象:模型把两个通过共享 Kafka topic 通信的模块标注为“硬依赖”,而实际上这种依赖是异步解耦的。这说明它虽然理解了代码,但对基础设施层抽象还不够敏感,需要我来纠偏。
第三阶段:发现反模式
有了全局图之后,我再问具体问题:“找出所有跨领域循环依赖;找出单文件超过 800 行的上帝类;找出没有接口定义、直接用具体实现的调用链。”这一次的输出质量明显上了一个台阶,因为模型已经知道了整个项目的骨架,不再凭空捏造。
2.2 我踩了一个巨坑:提示链顺序一旦搞反,幻觉就会爆炸
一开始我图省事,跳过前两个阶段,直接把整个仓库文本和一条超长 prompt 发过去:“分析架构坏味并输出循环依赖”。结果模型给我列了 12 个“循环依赖”,我一个个去验证,发现其中 5 个根本不存在,1 个把接口和实现搞反了,还有 2 个循环实际上是通过共享库间接形成的,不能算真正的循环。我当时真想抽自己——百万 token 窗口不是让你一次发完就躺赢的,注意力机制在超大上下文中需要锚点,如果没有先建立全局索引,模型就会在相似函数名、相似包名之间跳来跳去,产生所谓的“上下文误关联”。(延伸阅读:我把代码重构的AI赌注押在JetBrains AI Assistant上:一个后端架构师的三个月实战复盘)
后来我把流程强制改成上面那三阶段链之后,幻觉率从将近一半降到了大概 10%,剩下的我再用后面要讲的静态分析交叉验证机制过滤掉。
三、让模型直接写 diff 听起来很酷,但第一次跑直接炸掉了 CI 流水线
3.1 自动重构提案的生成与自动验证流水线
在得到反模式清单之后,我挑了 3 个最典型的循环依赖重构任务,让 Gemini 1.5 Pro 直接输出可应用的 unified diff。模型输出的格式相当干净:(延伸阅读:Google ADK这把轻量级快刀,正在切开LangGraph没啃下的审批流骨头)
diff --git a/service-order/src/main/java/com/example/order/OrderHandler.java b/service-order/src/main/java/com/example/order/OrderHandler.java
index 83ad9fa..c0de5a7 100644
--- a/service-order/src/main/java/com/example/order/OrderHandler.java
+++ b/service-order/src/main/java/com/example/order/OrderHandler.java
@@ -34,7 +34,7 @@ public class OrderHandler {
private final InventoryRestClient inventory;
public OrderResult process(Order order) {
- InventoryStatus status = inventory.checkStock(order.getSku());
+ InventoryStatus status = inventoryGateway.checkLocalFirst(order.getSku());
if (status == InventoryStatus.AVAILABLE) {
return doProcess(order);
}
为了不让模型瞎改,我设计了一套自动验证流水线:
- 静态分析过滤:用 SonarScanner 对每个 diff 涉及的文件跑一遍,如果有新增的阻断级 issue,直接标记为无效。
- 依赖编译检查:在容器里
apply diff然后执行受影响模块的gradle compileJava,编译不过的直接淘汰。 - 单元测试回归:跑对应模块的单元测试集,如果测试失败,记录失败原因,把 diff 打回给模型重生成。
最终从 7 个初始重构提案里,存活下来的只有 3 个,另外 4 个因为引入了新的方法调用但没补充 import 被编译检查挡掉了,还有 1 个虽然编译通过但单元测试挂了,因为模型改了方法签名却没改调用方。这一套流水线跑下来,我才敢把 AI 生成的 diff 合并入主干。
3.2 那次 CI 爆炸让我知道,长上下文模型对“隐式约定”完全没概念
最让我炸毛的一次,是 Gemini 在重构一个 Go 服务的时候,把一个用来处理 context 截止时间的 defer cancel() 语句删掉了,理由是“该上下文未在后续代码中被使用”。实际上那个 cancel 是配合 context.WithTimeout 用的,如果不调用,goroutine 会泄漏。模型只看了函数内部的文本,没有理解函数间隐式契约。类似的情况还发生在它把 Java 里的 @Transactional 注解挪了位置,导致事务传播行为改变。
这些坑不是长上下文模型特有的,但当你给它一整仓库的上下文时,你会误以为它“懂”得更多,反而更容易忽略这类隐式语义。我的教训是:长上下文能帮它看到更多调用关系,但帮不了它理解框架魔法。这类魔法必须由静态分析工具来捕捉,或者写进 prompt 里的强约束里。
四、怎么证明它的引用不是编的?我花了一周才捣鼓出一套交叉验证方案
4.1 幻觉验证:从“它说的看着很真”到“我能证明它不对”
长上下文模型最容易让你掉进的陷阱就是“听起来特别顺理成章”。我举个例子:Gemini 在报告里写:“PaymentClient 依赖了 UserService 的 getInternalId 方法,从而形成了跨域循环”。我根据报告去代码里搜这个方法,压根不存在。真实的方法叫 getInternalUserId,而且 PaymentClient 根本没直接调用它,而是通过一个 Facade 层间接使用的。
这就是典型的上下文混叠。我后面强制自己遵守三条验证原则:
- 引用必须含文件路径和行号:在 prompt 里要求模型输出每个依赖关系时必须附带文件路径和行号范围。如果没有,就不采信。
- 自动化抽查脚本:写了个 Python 脚本,根据模型输出的路径和行号去仓库里 grep 对应的函数名或调用模式,如果匹配不到,就标红。
- 双重交叉检查:对每个关键结论,我会换一种提问方式再问一次,比如从“请列出循环依赖”变成“这个模块的所有出向依赖有哪些”,看两次回答是否自洽。
这套方法实施之后,我手动核验了 26 条架构发现,模型给出的错误引用从最初的大概 40% 降到了 15% 以下,而且剩下那些错误基本都被抽查脚本抓住了。
4.2 意外发现:当上下文太大时,模型会“选择性失明”
有一个特别有意思的现象:我在分析一个老 Go 服务的时候,Gemini 识别出了所有 proto 文件定义的服务,却忽略了一个在目录深处、文件名完全没体现其作用的 legacy_adapter.go。这个文件是连接两套认证系统的关键,但因为它的内容和其他新模块的调用方式格格不入,模型可能将其当成了噪声。我猜这是因为长上下文中的注意力集中在高频模式上,对那些“异常点”反而会弱化。
所以我现在会让模型专门执行一次“异常检测”步骤——要求它找出功能上与其他模块差异最大的 5 个文件。那次它准确地挑出了 legacy_adapter.go。这也是为什么分阶段提示如此重要。