1. 百万行代码库上的 coding agent 实测:为什么这件事值得认真聊
百万行代码是什么概念?一个中等规模的商业产品,核心仓库通常在 20 万到 50 万行之间;百万行基本意味着这个仓库已经经历了五年以上的持续迭代,几十个模块互相咬合,历史包袱和活跃开发并存。Databricks 拿自己的主力代码库去测 coding agent,这个动作本身就说明了一件事:单文件、几百行的 demo 已经不能说明任何问题了。
我自己带团队做代码智能化工具落地也有几年了,踩过的坑基本都集中在同一个地方——小仓库里表现惊艳的 agent,一进真实仓库就原形毕露。原因不复杂:真实仓库的难点从来不是"写出一段能跑的代码",而是"在几十个约束条件下写出不破坏任何东西的代码"。Databricks 这次实测的价值,就在于它把评测场景拉到了真实工程复杂度上。
这篇文章适合三类人看:正在评估 coding agent 能不能进自己团队的技术负责人、已经在用但效果不稳定的开发者、以及想搞清楚"百万行代码"这个量级到底难在哪的工程师。我会把这次实测背后的核心问题拆开讲——上下文怎么组织、检索怎么做、验证环节怎么设计、失败模式长什么样,以及如果你要在自己的仓库里复现类似评测,具体该怎么动手。
先给一个结论性的判断:coding agent 在百万行代码上的瓶颈,八成不在模型能力,而在上下文工程和验证闭环。这个判断后面会用大量细节来支撑。
2. 百万行代码库到底难在哪:四个绕不开的核心挑战
2.1 上下文窗口装不下,也不该全装
很多人第一反应是"现在上下文窗口都上百万 token 了,直接把仓库塞进去不就行了"。这个想法在实操中会立刻撞墙。百万行代码按平均每行 10 个 token 粗算,就是 1000 万 token 量级,远超任何主流模型的窗口。就算窗口够大,成本和延迟也会让这个方案完全不可用——每次改一行代码都要处理千万级 token,响应时间会从秒级掉到分钟级。
更关键的是,全量上下文会稀释信号。模型在无关代码上分配的注意力,会直接降低它对真正相关代码的敏感度。我实测过一个对比:同一个 bug 修复任务,给模型 5 万 token 的精准上下文,成功率明显高于给 50 万 token 的"全量+精准混合"上下文。这跟人读代码是一个道理,你给一个新人扔整个仓库让他找 bug,他大概率会迷失。
所以百万行代码的第一个核心挑战是:如何从千万级 token 中,精准捞出那几千个真正相关的 token。这是检索问题,不是模型问题。
2.2 依赖关系是网状的,不是树状的
小项目里,模块依赖基本是一棵树,A 调用 B,B 调用 C,顺着走就行。百万行代码里,依赖是网状的:你改一个工具函数的签名,可能有 200 个调用点散落在 30 个模块里,其中还有十几个是通过反射或动态派发间接调用的,静态分析都抓不全。
Databricks 这类数据平台代码库尤其典型——SQL 解析、执行计划、存储层、API 层之间耦合很深,一个看似局部的改动经常引发连锁反应。coding agent 如果只看到"当前文件",它给出的修改在局部是完美的,一编译就炸一片。这就是为什么跨文件的依赖追踪能力,是区分玩具和工具的分水岭。
2.3 隐式约定写不进类型系统
真实仓库里大量规则是"约定"而非"类型约束"。比如某个模块的所有日志必须走统一的 logger 封装、某个目录下的函数必须幂等、某类异常必须带特定的错误码。这些规则编译器不检查,但 code review 会拦。coding agent 如果不知道这些约定,产出的代码"能跑但不合规",最后还是得人工返工。
百万行代码库积累的这类隐式约定,可能有几百条。怎么把它们喂给 agent,是个非常实际的问题。常见做法是维护一份CONTRIBUTING或AGENTS.md之类的约定文档,让 agent 在生成前先读。这个做法简单但有效,后面会详细讲怎么写。
2.4 验证成本高,反馈闭环慢
小项目改完跑个单元测试几秒钟出结果。百万行代码库,全量构建动辄几十分钟,全量测试可能几小时。如果 agent 每改一步都触发全量验证,整个流程会被拖垮。所以验证策略必须分层:快速静态检查、增量编译、相关模块的定向测试、最后才是全量回归。这个分层设计做得好不好,直接决定 agent 的迭代速度能不能接受。
3. 上下文工程:把千万行代码喂给 agent 的正确姿势
3.1 检索增强是主力,不是可选项
在百万行代码场景下,检索增强生成(RAG)不是锦上添花,而是基础设施。核心思路是:把代码库切块、建索引,任务来了先检索出最相关的片段,再拼进上下文。
代码检索和文档检索有个关键区别:代码的相关性不只是语义相似,还有结构相关。一个函数和它调用的函数、它实现的接口、调用它的地方,语义上可能八竿子打不着,但结构上强相关。所以纯向量检索不够,得混合多种信号:
- 语义检索:用代码 embedding 找功能相似的片段
- 符号检索:按函数名、类名、变量名精确匹配
- 图检索:沿调用图、继承图、导入图扩展邻居节点
- 历史检索:找最近改动过相关文件的 commit 和 PR
我实测下来,符号检索 + 图检索的组合,在代码任务上召回质量明显优于纯语义检索。原因是代码里的标识符命名往往很规范,精确匹配的命中率很高,而语义检索容易被"看起来像但实际无关"的代码干扰。
3.2 分块策略:按语法结构切,别按行数切
代码分块最忌讳的就是"每 500 行切一块"。这样切出来的块经常从函数中间断开,语义完全破碎。正确做法是按语法结构切:以函数、类、方法为最小单位,超大的函数再按逻辑段落细分。
具体操作上,可以用 tree-sitter 这类解析器把代码解析成语法树,然后按节点类型切块。每个块保留完整的函数签名和文档字符串,这样即使块被单独检索出来,模型也能理解它的用途。
一个实操细节:块要带足够的元信息。除了代码本身,还要附上文件路径、所属类、被谁调用、调用了谁。这些元信息在拼上下文时非常有用,能让模型快速建立结构认知。我一般会给每个块加上这样的头部:
# file: src/parser/sql_parser.py # class: SQLParser # calls: Tokenizer.tokenize, ASTBuilder.build # called_by: QueryEngine.parse, CLI.run def parse(self, sql: str) -> AST: ...别小看这几行注释,它能让模型在没看到完整调用链的情况下,也大致知道这段代码在系统里的位置。
3.3 上下文预算分配:给谁多给谁少
检索出来的内容往往还是超预算,这时候要做二次筛选和排序。我的经验是按这个优先级分配上下文预算:
| 优先级 | 内容类型 | 建议占比 |
|---|---|---|
| 最高 | 任务直接相关的文件全文 | 40% |
| 高 | 直接依赖和被依赖的接口签名 | 25% |
| 中 | 相关测试用例 | 15% |
| 中 | 项目约定文档 | 10% |
| 低 | 相似历史改动 | 10% |
注意"接口签名"这一项——很多时候你不需要依赖模块的完整实现,只需要它的公开接口。这样能省下大量 token。我在实操中会把依赖模块压缩成"签名 + 文档字符串"的形式,实测对生成质量影响很小,但 token 消耗能降一半以上。
3.4 约定文档怎么写才管用
前面提到隐式约定的问题,解决方案是显式化。我建议在仓库根目录放一个专门给 agent 看的约定文件,内容要具体、可执行,别写成空泛的价值观。
反例(没用):
代码应该保持整洁,遵循最佳实践。
正例(有用):
- 所有日志必须用
from core.logging import get_logger,禁止直接用- 数据库操作必须在
with transaction():块内,禁止裸连接- 新增公共函数必须带类型注解和 docstring,格式参考
src/utils/strings.py- 异常必须继承
AppError,并带error_code属性
这种写法 agent 能直接照做。我实测过,加了这份文档之后,agent 产出代码的"合规返工率"能降一大截。这份文档本身也是团队资产,新人来了同样受益。
4. 验证闭环:让 agent 自己知道自己错了
4.1 分层验证,别一上来就全量
前面说过全量验证太慢,实操中我会把验证分成四层,agent 每改一步按顺序过:
- 语法与静态检查(秒级):格式化、lint、类型检查。这一层能拦掉大量低级错误,成本极低,必须每次都跑。
- 增量编译(十秒到分钟级):只编译受影响的模块。很多构建系统支持增量编译,用好了能把反馈压到可接受范围。
- 定向测试(分钟级):跑与被改文件相关的测试用例。怎么找相关测试?可以按文件路径匹配,也可以按测试覆盖率数据反查。
- 全量回归(小时级):只在 agent 认为任务完成、准备提交时跑一次。
这个分层的核心思想是:用最便宜的手段拦掉最多的错误。语法错误不该等到跑完两小时回归才发现。
4.2 让 agent 读得懂报错
验证的价值取决于 agent 能不能从报错里学到东西。这里有个常见坑:原始报错信息往往又长又乱,agent 抓不住重点。比如一个编译错误可能刷屏几百行,真正的根因藏在中间某一行。
我的做法是给报错做预处理:提取错误类型、文件位置、关键信息,压缩成结构化格式再喂给 agent。比如:
ERROR: type_mismatch FILE: src/parser/sql_parser.py:142 EXPECTED: Optional[AST] GOT: AST HINT: return value may be None when input is empty这样 agent 一眼就能定位问题。实测下来,报错预处理能让 agent 的自我修复成功率提升不少,因为它不用在噪音里找信号了。
4.3 测试用例是 agent 的"外部记忆"
百万行代码库的测试套件本身就是宝贵资产。我强烈建议让 agent 在动手前先读相关测试,原因有两个:一是测试用例精确描述了函数的预期行为,比文档还准;二是测试能暴露边界条件,这些边界条件往往就是 bug 的高发区。
更进一步,可以让 agent 在修改代码后主动补充或更新测试。这一步很关键——如果 agent 改了行为但没更新测试,测试就会失败,形成负反馈;如果它同时更新了测试,你就能通过 diff 审查它是否理解了改动的影响范围。测试 diff 是审查 agent 改动的一个极佳切入点。
4.4 人工审查卡在哪个环节
完全自动化的 agent 在百万行代码上目前还不现实,人工审查必须保留。但审查卡在哪很讲究。我的经验是卡在"提交前"而不是"每一步":让 agent 自由迭代,只在它认为任务完成、准备产出最终 diff 时人工介入。这样既保证了效率,又守住了质量底线。
审查时重点看三样东西:改动的文件范围是否合理(有没有误伤无关文件)、测试 diff 是否与代码 diff 一致、有没有触碰约定文档里的红线。这三样过一遍,大部分问题都能拦住。
5. 实操复现:在自己的仓库里搭一套评测
5.1 评测集怎么构造
想复现 Databricks 这类评测,第一步是构造评测集。别用网上现成的,那些题目和你的仓库没关系。正确做法是从自己的历史提交里挖:
- 找最近半年内已合并的 bug 修复 PR,把修复前的代码作为起点,修复后的代码作为标准答案
- 找重构类 PR,同样取前后对比
- 找新增功能的 PR,取功能实现作为任务
每个任务记录:任务描述、起始 commit、标准答案 diff、相关测试。这样构造出来的评测集,难度和分布都贴合你的真实仓库。我一般会攒 50 到 100 个任务,太少统计不显著,太多跑起来太慢。
5.2 评测指标怎么定
别只看"通过率"一个数,太粗。我建议至少看四个维度:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 任务完成率 | 最终 diff 通过全部测试的比例 | 整体能力 |
| 首次通过率 | 第一次产出就通过的比例 | 迭代效率 |
| 平均迭代轮数 | 从开始到通过的平均修改次数 | 反馈闭环质量 |
| 误伤率 | 改动波及无关文件的比例 | 安全性 |
误伤率这个指标特别值得关注。很多 agent 为了通过测试,会顺手改一堆无关代码,短期看任务完成了,长期看是技术债。误伤率高的 agent,实际不可用。
5.3 跑评测的工程细节
跑评测有几个容易忽略的工程点。第一是环境隔离:每个任务在独立的容器或 worktree 里跑,避免互相污染。第二是超时控制:给每个任务设上限,防止 agent 陷入死循环。第三是日志留存:完整记录 agent 的每一步操作、每次报错、每次修改,事后分析全靠这些日志。
我踩过的一个坑是没做环境隔离,两个任务同时跑,一个改了共享的配置文件,另一个直接失败,排查了半天才发现是互相干扰。从那以后我坚持每个任务一个独立 worktree,虽然占点磁盘,但省心太多。
5.4 一个最小可用的评测脚本骨架
import subprocess from pathlib import Path def run_task(task, agent, worktree): # 1. 准备环境 subprocess.run(["git", "checkout", task.base_commit], cwd=worktree, check=True) # 2. 给 agent 任务描述和上下文 context = build_context(worktree, task.description) # 3. agent 迭代 for round in range(MAX_ROUNDS): diff = agent.propose(context) apply_diff(worktree, diff) result = run_verification(worktree, task.related_tests) if result.passed: return {"status": "success", "rounds": round + 1} context = update_context(context, result.errors) return {"status": "failed", "rounds": MAX_ROUNDS}这个骨架很粗,但把核心流程说清楚了:准备环境、给上下文、迭代、验证、反馈。你可以在此基础上加日志、加指标统计、加并发。
6. 常见问题与排查技巧实录
6.1 agent 反复改同一个地方改不对
这是最常见的失败模式。原因通常是上下文里缺少关键信息,agent 在信息不足的情况下只能瞎猜,猜错了再猜,陷入循环。排查思路:看 agent 每次迭代的上下文,是不是每次都一样?如果一样,说明反馈没生效,检查报错预处理是不是把关键信息丢了。如果上下文在变但方向不对,说明检索出来的内容不相关,检查检索策略。
我的处理办法是设一个"重复检测":如果 agent 连续两轮改了同一个文件同一区域还没通过,就强制中断,把当前状态和报错完整打出来人工看。十有八九是上下文缺了某个关键依赖。
6.2 任务完成了但引入了新 bug
这是误伤问题的典型表现。agent 为了让目标测试通过,改了某个共享函数的行为,结果其他调用点挂了。排查方法:跑全量测试,看哪些原本通过的测试变红了。根治办法是在上下文里明确列出"这个函数的其他调用点",让 agent 知道改动的波及范围。
我一般会在检索阶段做一次"反向依赖查询":找出所有调用目标函数的地方,把它们的调用方式(传什么参数、期望什么返回)一并放进上下文。这样 agent 改之前就知道有哪些约束。
6.3 大文件处理不动
百万行代码库里总有几个几千行的巨型文件,检索和生成都吃力。我的做法是对巨型文件做二级切分:先按类或大函数切,再对超大函数按逻辑段落切。切的时候保留函数签名和关键注释,确保每块都能独立理解。
另一个技巧是对巨型文件做摘要:生成一份"这个文件里有哪些类、哪些关键函数、各自干什么"的摘要,检索时先给摘要,需要细节再展开。这样能大幅降低上下文压力。
6.4 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 反复改不对 | 上下文缺关键信息 | 检查检索召回、报错预处理 |
| 引入新 bug | 未考虑反向依赖 | 补充调用点上下文 |
| 大文件处理慢 | 分块粒度过粗 | 二级切分 + 摘要 |
| 迭代轮数过多 | 验证反馈太慢或太粗 | 优化分层验证、报错结构化 |
| 合规返工多 | 约定未显式化 | 补充约定文档并纳入上下文 |
| 误伤无关文件 | 任务边界不清 | 明确任务范围,限制改动文件集 |
6.5 几条踩坑换来的经验
第一条:别迷信模型能力,先把上下文工程做好。我见过太多团队一上来就换更强的模型,结果提升有限,因为瓶颈根本不在模型。把检索、分块、约定文档这些基础打好,收益比换模型大得多。
第二条:验证反馈的质量比速度更重要。快但模糊的反馈,会让 agent 在错误方向上快速迭代,越跑越偏。宁可慢一点,也要把报错信息整理清楚。
第三条:评测集要持续更新。仓库在演进,半年前的评测任务可能已经过时。我一般每季度补充一批新任务,淘汰一批老任务,保持评测集和当前代码库的相关性。
第四条:人工审查的精力要花在 diff 上,不是过程上。盯着 agent 每一步操作既累又低效,不如让它跑完,集中审查最终 diff。审查 diff 时重点看改动范围和测试一致性,这两点能拦住大部分问题。
7. 这套方法能扩展到什么场景
百万行代码的 coding agent 评测,本质上是"在复杂约束下做自动化决策"的一个实例。这套方法论——检索增强、分层验证、约定显式化、评测集从历史挖——可以迁移到很多类似场景。
比如大型配置仓库的自动化变更、跨多个微服务的接口重构、遗留系统的渐进式迁移,核心挑战都是一样的:上下文装不下、依赖是网状的、隐式约定多、验证成本高。把上面这套思路套过去,基本都能用。
我个人在实际操作中的体会是,这类工作的成败,八成取决于你有没有把"上下文"和"验证"这两件事做扎实。模型是引擎,但引擎再好,油路和刹车不行,车也跑不起来。Databricks 这次实测最大的启发,不是某个模型多强,而是它把工程复杂度摆到了台面上,逼着大家去解决真问题。如果你正准备在自己的仓库里试 coding agent,建议先从构造一个贴合自己仓库的小评测集开始,跑通了再谈规模化。