我们在一棵纯 AOSP 17(
android-17.0.0_r1,SDK 37)整机源码树上,围绕 Claude Code 搭建了一套四层 harness 工程,让 coding agent 能够在上千个 git project、千万行量级的 repo 工程里稳定地导航、编译、部署、验证,目标设备是 Cuttlefish 虚拟机(aosp_cf_x86_64_phone)。本文先分析 coding agent 在整机源码树上应用的困难,再梳理 Anthropic 官方博客给出的大库通用要点,接着盘点网络上已有的同类方案,最后介绍我们的四层解决方案——每层解决什么问题、如何协同运转,以及一路探索踩过的坑。
一、问题:为什么 coding agent 在整机源码树上"开箱不可用"
Claude Code 这类 agent 不给代码库建索引(不做 RAG),而是用 agentic search 现场导航——grep、读文件、跟引用,像一个新来的工程师翻代码。这个设计有一个巨大的好处和一个巨大的代价:
好处:永不过期。 它永远读的是 live 代码,不会像向量索引那样"返回两周前已改名的函数、或引用一个已删除的模块,却不告诉你它过期了"。 代价:上下文窗口是唯一稀缺资源。 导航的每一步(grep 结果、读过的文件)都在消耗上下文;导航质量完全取决于你把代码库"布置"得多好。
AOSP 整机源码树把这个矛盾推到极端,有三个放大器:

不搭 harness,直接在整机树上用 coding agent 会反复撞上这些墙(均为实际遇到或验证过的):
核心论断:模型不是瓶颈,环境才是。 社区先行者 utzcoz 用 Claude Code 做成 4 个 AOSP 级项目后的结论也是如此:这类工作 coding agent 开箱做不好,能做成靠的是围绕 agent 搭的 harness engineering——"harness 诚实,产出就诚实"。
二、官方参照:Anthropic《How Claude Code works in large codebases》的要点
上一节的困境不是 AOSP 独有的。Anthropic 官方博客《How Claude Code works in large codebases》归纳了 Claude Code 在大型代码库(百万行 monorepo、几十年遗留系统、几十个仓的分布式架构)落地成功的共性模式——它不是为 AOSP 写的,但几乎每一条都能对上整机树的处境,是我们方案的通用理论底座。这里先把它的要点提炼出来;后文第四节起的四层,本质就是把这些通用原则一条条落到整机树上的具体形态。
2.1 Claude Code 怎么在大库里导航:agentic search,而非 RAG
博客把第一节我们已经点到的机制说得更透:
agentic search:像工程师一样遍历文件系统、读文件、用 grep 精确定位、跟着引用跨库跳转;本地运行、不需要建立/维护/上传任何索引。 RAG 在大规模下会失效:embedding 管线追不上活跃的工程团队——开发者查询时,索引反映的是几周/几天/几小时前的代码,于是返回一个两周前已改名的函数、或引用一个上个 sprint 已删除的模块,却完全不提示它已过期。 代价与甜区:agentic search 反过来要求足够的起始上下文才知道去哪找;导航质量取决于代码库被"布置"得多好(用 CLAUDE.md + skills 分层)。若向一个十亿行的库问一个模糊 pattern,会在开工前就撞上上下文窗口墙。在代码库布置上投入的团队,效果明显更好。
2.2 harness 与模型同等重要:五个扩展点 + 两项能力
博客点名一个最常见的误解——以为 Claude Code 的能力只由所用模型决定。实际上围绕模型的生态(harness)比模型本身更决定表现。harness 由五个扩展点构成,外加两项能力,且叠加有顺序——每一层都建立在前一层之上:
| CLAUDE.md | ||||
| hooks | ||||
| skills | ||||
| plugins | ||||
| LSP | ||||
| MCP servers | ||||
| subagents | 把探索与编辑分离 |
其中几条博客特别强调的:hooks 最有价值的用法不是防错,而是让配置自我改进(stop hook 会话末反思→提议改 CLAUDE.md);skills 可 path-scoped,只在相关目录激活;LSP 是多语言大库里最高杠杆的投资之一(没有它,Claude 对文本 pattern-match,会落到错误的同名符号);subagents 的典型用法是只读子代理测绘子系统、写进文件,主代理再带全貌编辑。
2.3 三个配置模式
博客从成功部署里提炼出三个反复出现的模式:
让大库对 Claude 可读:CLAUDE.md 精简且分层(根只放指针 + 关键坑);在子目录而非仓根初始化(Claude 会自动向上加载沿途每个 CLAUDE.md,根上下文不丢);按子目录 scope test/lint 命令(跑全套会超时、烧上下文);用 .ignore/ 版本化的permissions.deny排除生成物、构建产物、三方码;目录结构不给力时写一份轻量 codebase map;跑 LSP 让按符号而非字符串搜。随模型演进主动维护 CLAUDE.md:为旧模型缺陷写的规则会拖累新模型(如"每次重构拆成单文件改动"会阻止新模型做它本已擅长的协调跨文件编辑);为补模型/工具缺陷写的 skill/hook 一旦缺陷消失就成负担。每 3–6 个月、或模型换代后感觉见顶时做一次配置重审。 指派 owner:技术配置本身不驱动采纳;铺开最快的组织在放开前就有专人/小队把工具接进工作流,出现 agent manager(PM/工程混合角色)或至少一个 DRI,并尽早对齐治理(谁管 skill/plugin、避免重复造轮子、AI 代码走同样的 review)。
2.4 适用边界
博客最后划了适用范围:Claude Code 面向常规软件工程环境——工程师是主要贡献者、用 Git、标准目录结构。非常规设置(游戏引擎的大二进制资产、非常规版本控制、非工程师贡献代码)需要额外的配置工作。
这些都是通用结论;而整机树把每一条的难度都放大了(第一节的三个放大器)。后文第四节起,就是我们把这套通用原则逐条落到 AOSP 整机树上的具体形态——并在两处(子目录初始化、plugin 分发)因 repo 工程的现实做了有意背离(见第四节末)。
三、他山之石:网络上已有的类似方案
动手自建之前,我们先调研了社区在"AOSP + coding agent"这个方向上已有的探索(调研时间 2026-07)。有代表性的四个方案,恰好各占一个生态位:
Lightrion AOSP RAG:托管的 AOSP 语义检索
一个商业 MCP 服务:把 AOSP 13–17 各发布版预先建好索引,暴露 search_code / get_chunk / get_file / list_versions / diff_versions 五个工具,任何 MCP 客户端加一个 bearer token 就能用自然语言检索 AOSP 源码,还能按 minor release 钉住版本、跨版本 diff。
优点:零本地成本——不需要本地源码树、不需要自己建索引;多版本覆盖 + 跨版本 diff 是独有能力("这个函数在 15→17 之间改了什么"一问即得);接入是标准 MCP,五分钟配完。 局限:它索引的是公开 AOSP 发布版,而整机开发的工作对象是自己的本地树——你刚改过的代码、本地 feature 分支、树上的任何 delta 它都不知道。这正是 Anthropic 官方博客点名的 RAG 死穴("返回已改名的函数却不告诉你它过期了")在 fork 场景下的极端形态。此外代码问题出网查询有保密性顾虑;且它只覆盖"读与查"这一层,编译、部署、验证、护栏全不涉及。 我们的取舍:不能作为本地树的主导航(live 树必须 agentic search + LSP),但作为"查上游基线/跨版本差异"的补充通道有真实价值。
utzcoz 十模式:被四个交付项目验证过的方法论
作者用 Claude Code 做成并发布了 4 个 AOSP 级项目(ARM64→x86_64 二进制翻译器、AOSP 14 多窗口补丁集、Chromium WebXR 移植、64 章 AOSP 内核书),沉淀出十条 harness engineering 模式,分三组:跨会话保存状态(CLAUDE.md 行为契约、handoff 交接文档)、验证(模拟器基座、verify 脚本只编码一次、红条 TDD、读截图判对错)、输出可信(结论带源码路径行号、冷启动对抗 review)。
优点:唯一经过"真的交付了东西"检验的完整方法论;对"编过 = 改对"幻觉、"似是而非 ≠ 正确"这两个 agent 根性问题给出了系统解法;多条模式可直接照抄(CLAUDE.md 只写 agent 默认会犯的错、verify 脚本单入口确定性输出)。 局限:它是方法论而非可安装工件,每个项目都要自己重新落地;其项目形态是"单仓 fork + 模拟器",没有处理 repo 千仓工程特有的问题——上下文文件会污染 gerrit project、上下文如何随 feature 分支切换;对代码智能层(LSP/compdb)也没有展开。 我们的取舍:十模式是本文方案在理念层的最大来源——CLAUDE.md 行为契约、verify 确定性脚本、"harness 诚实产出就诚实"都直接进入了设计;repo 工程特有的部分(第六、七节)则是我们补上的。
hyperdroid-skill:插件化的 Android 通用技能包
LineageOS 社区开发者的 Claude Code 插件(MIT):四个按触发词自动激活的 skill(android 设备/adb、android-fastboot 刷机/分区/防砖、android-build Gradle+AOSP 构建、lineageos repo/Gerrit 工作流)加一个受限工具的 crash-analyzer 子代理(自主收集 logcat/tombstone/ANR 后给诊断)。
优点:工程骨架是四家里最值得抄的——单仓分发多 skill + agent(plugin.json/marketplace 一键装)、渐进式披露(SKILL.md 速查 + references/深度页按需加载)、触发词自动激活、受限工具 + 固定工作流的子代理模板、仓库自校验 Makefile(CI 强制每个 skill 结构合规)。局限:内容是参考手册级的通用知识(adb/fastboot/构建命令速查),视角偏三方 ROM 玩机而非整机平台开发;不绑定任何具体源码树——不知道你的 feature、你的编译产物、你的验证脚本;对树内导航、上下文经济、gerrit 污染等核心矛盾不涉及。 我们的取舍:抄骨架、自己填肉——渐进式披露和触发激活的思想直接体现在我们的 path-scoped skill 设计里( pathsglob 替代触发词,粒度更准)。
Android-Software:分层专家路由的知识包
面向 Android Software Owner / BSP 工程师的分层 skill 集(Beta,对 Android 15 验证):所有任务先过 L1 路由器(意图 → 已验证的 AOSP 路径映射),再加载对应的 L2 子系统专家(build/SELinux/HAL/framework/init/内核 GKI/bootloader/ATF/pKVM 等 12 个),每个专家带子系统知识、禁止动作和工具链;另有 hindsight notes 机制沉淀跨会话经验。
优点:直击 agent 在 AOSP 上的三大失败模式(幻觉路径、跨域错配——把 bootloader 问题路由给 init、版本知识漂移);"MMU 式按需加载"与我们的上下文经济诉求同源;子系统覆盖面最广(连 LK/ATF/pKVM 这类 vendor 层都有路由位);没有本地源码也能回答问题。 局限:本质是静态知识包——与"你这棵树"零绑定,答案来自预写的知识而非现场读码,版本演进要人工维护(对 A15 验证,用在 17 上就有漂移窗口,恰是它自己要解决的问题);无 symbol 级导航;无编译→部署→验证闭环;单人 Beta 项目,成熟度有限。 我们的取舍:分层按需加载的思想与我们"索引粒度注入、详情按需 Read"殊途同归;但我们把"知识从哪来"反过来了——不预写知识,让 agent 现场读真代码,harness 只负责把它引到对的地方。
四方案对照与我们的位置
用整机树内开发需要的四类能力——代码智能、上下文、流程、护栏与验证,这也正是下文我们方案的四层框架——给它们做覆盖度体检(● 深度覆盖 ◐ 部分/通用级 ○ 基本不涉及):
| 本文方案 |

结论一目了然:四个方案分别解决了检索、方法论、通用领域知识、知识路由,但没有一个解决"这一棵树"的问题——本地 fork 的 symbol 级实时导航、repo/gerrit 布局下不污染上游的上下文组织、随 feature 分支自动切换的工作状态、绑定本树目标设备的确定性验证环。这块空白,就是下文四层 harness 的主体;而各家的长处(utzcoz 的验证纪律、hyperdroid 的渐进披露骨架、Android-Software 的按需分层思想)都被吸收进了对应层的设计。
四、方案总览:四层 Harness
4.1 一个业务前提与五个术语
展开四层之前,先厘清本方案赖以成立的一个业务前提和几个反复出现的术语——它们决定了这套 harness 为什么可行、四层各自靠什么落地。
业务前提:一个专项 = 一个 feature = 一个本地分支 = 3–8 个单仓。 真实的手机厂商以专项的形式推进 OS 特性开发(一个专项就是一次成体系的特性迭代)。我们把一个专项对应成一个 feature,并约定 一个 feature 独占一个 repo 本地分支(repo start <feature> --all),该 feature 的全部改动只在这个分支上进行——这样 harness 才有一个稳定的"当前在做什么"的锚点。一个 feature 通常只触及 3–8 个 git 单仓(例如"新增一个系统服务 + 一个边栏应用",落在 frameworks/base、frameworks/native、新建 app 仓、build/make、system/sepolicy 上)。正是"涉及仓有限、且随分支固定"这个业务事实,让后文所有"随 feature 组织、随分支自动切换、按仓精简"的机制得以成立。
五个术语(四层各自的关键落地物,正文表格里会直接用到):
LSP(Language Server Protocol,语言服务器协议):编辑器/agent 与语言服务器之间的标准协议,把"跳到定义、查找所有引用、按符号导航、类型报错"这类代码智能能力暴露出来。它让 agent 按 symbol(符号) 而非文本字符串检索——同名函数在不同文件/语言里能被精确区分,不会 pattern-match 到错误的符号。C++ 侧我们用 clangd 作为语言服务器,喂给它一份 compdb( compile_commands.json,编译命令数据库)即可(第①层,详见第五节)。SessionStart hook:Claude Code 在会话启动 / 恢复 / 清屏 / 压缩时触发的钩子脚本,它的 stdout 会被 Claude Code 注入为会话上下文。我们用它在每次会话"睁眼"时,按当前分支自动把"这个 feature 在做什么、涉及哪些仓、验证跑哪个脚本"喂给 agent(第②层,详见第六节)。 path-scoped skill:skill 是按需加载的打包指令;带 pathsglob 的 skill 只在 agent 读到匹配路径的代码时才激活。我们用它承载"改到这片代码怎么编译 / push / 验证"这类过程性知识——不读到就零上下文占用(第③层,详见第七节)。permissions.ask硬门禁:Claude Code 的权限机制,把匹配到的危险命令(adb push/reboot、cvd start/stop、repo sync、m clean等)变成执行前的系统级弹窗确认,不依赖模型记性(第④层护栏,详见第八节)。features/<分支>/verify-*.sh确定性脚本:每个 feature 自带的验证脚本,就是这个 feature 的"测试"——输出只有确定性的 PASS/FAIL,用来斩断"编过 = 改对"的幻觉(第④层验证,详见第八节)。
4.2 四层总览
我们把「导航、上下文、流程、护栏与验证」四件事做成工程化的基础设施,让 agent 在这棵树上的每次会话都站在同一套地基上:
.clangd → feature 精简 compdb(C++);Java/Kotlin 明确不配 LSP | ||
CLAUDE.md(固定 bootstrap + 硬约束)+ SessionStart hook 按分支注入 features/<分支>/_index.md | ||
.claude/skills/ 若干 path-scoped skill | ||
permissions.askfeatures/<分支>/verify-*.sh 确定性脚本 |
与官方博客扩展点框架的对应关系(博客要点见第二节):博客把 harness 拆成五个扩展点(CLAUDE.md、hooks、skills、plugins、MCP servers)外加两项能力(LSP、subagents),并强调叠加有顺序——每一层都建立在前一层之上。我们这张"四类能力"表是同一套东西按"解决什么问题"重排的:① 代码智能 = LSP;② 上下文 = CLAUDE.md + hooks(SessionStart 动态注入);③ 流程 = skills(path-scoped);④ 护栏 = hooks(permissions/校验)+ verify 脚本。博客说的叠加顺序在我们的探索历程(第十节)里也如实复现:先用 CLAUDE.md 定行为契约、再用 hook 注入上下文、再用 skill 承载流程、最后补护栏与验证——每一战都踩在上一战的地基上。五个扩展点里我们只暂缓了两个(plugins 与 MCP servers),另有两处对博客通用建议的有意背离,理由见本节末。
四层全部落在树根的游离文件上。这里有一个 repo 工程的关键事实:源码树根不是 git 仓(树根只有 .repo/,没有 .git/),所以放在树根的文件不被任何 gerrit project 跟踪,soong/kati 也不会把纯 markdown 目录当模块编进整机——这是整套方案"不污染上游"的地基。
<AOSP_ROOT>/# repo 工程根(非 git 仓)├── CLAUDE.md# ② 固定 bootstrap + 硬约束(不随 feature 变)├── .clangd# ① 指向 feature 精简 compdb(绝对路径)├── compile_commands.json# ① 根符号链├── gen-compdb-clangd.sh# ① compdb 两段式刷新脚本├── .claude/ │ ├── settings.json# ② hooks 注册 + ④ permissions 门禁│ ├── hooks/load-feature.sh# ② 按分支注入 feature 上下文│ ├── hooks/check-branch-drift.sh# ② 会话中途切分支告警│ ├── rules/compdb-freshness.md# ① compdb 时效提醒(path-scoped rule)│ └── skills/build-*/SKILL.md# ③ path-scoped 编译/验证 skill└── features/# ② 独立 git 仓(不在 manifest,repo/gerrit/soong 全不可见) └── dev-sidebar/# 目录名 = repo 分支名 = feature 名 ├── _index.md# 启动注入的索引(几百 token) ├── frameworks-base.md# 单仓约定(只写 feature 特有内容) ├── check-branch.sh# 涉及仓分支一致性检查 └── verify-sidebar.sh# ④ 确定性验证脚本📦 可跑 Demo:本节这棵目录树的最小可运行复刻见同级
aosp-harness-demo/——四层落地物一应俱全,关键脚本都带--demo,不需要真实 AOSP 树,./run-demo.sh即可一键演示四层如何协同。下文五~八节每节开头的「▶ Demo」标注,就指向它对应的文件。
对博客扩展点的三点取舍(都是被 repo/整机树的现实逼出来的,不是遗漏):
背离一:博客建议"在子目录而非仓根初始化"(让 agent scope 到与任务相关的部分,Claude 会自动向上加载沿途每个 CLAUDE.md,根上下文不丢)。我们反其道必须从树根启动——envsetup / lunch / m / adb 全都要求树根 cwd,repo 树也没有子目录级 git 仓的天然边界可供 scope。子目录上下文不会被"向上加载"自动带进来的代价,我们用 SessionStart hook 的索引注入(第六节)+ path-scoped skill(第七节)显式补回。 背离二:博客把 plugins 当作"分发可复用配置、防止好做法停留在部落知识"的手段(打包 skills/hooks/MCP,marketplace 一键装)。我们刻意不把这套 harness 做成 plugin——它的价值恰恰在于是一组树根游离文件(见上),做成 plugin 会脱离"树根非 git 仓"这个不污染上游的地基。跨机分发改由 features/独立 git 仓推私有 remote 实现(第九节)。暂缓一:MCP servers。博客点名一个常见错误——"基础还没跑通就先建 MCP 连接"。我们目前只在"查上游基线 / 跨版本 diff"这类读侧场景把 Lightrion 这类 MCP 当补充通道(第三节);"把结构化检索暴露成 agent 可直接调用的工具"是后续可演进项,而非当前地基。
下文以一个真实工作中的 feature 为例(记作
dev-sidebar):在 AOSP 17 上新增一个系统服务 + 一个常驻边栏应用,涉及frameworks/base、frameworks/native、新建 app 仓、build/make、system/sepolicy五个仓。
五、第①层 代码智能:让导航按 symbol,而不是按文本
▶ Demo:
aosp-harness-demo里对应.clangd、gen-compdb-clangd.sh(带--demo演示两段式过滤)、.claude/rules/compdb-freshness.md。
这一层要做的事,一句话就是给 agent 配好 LSP——让它像 IDE 里的工程师那样按 symbol 导航,而不是对文本做 pattern-match。LSP 被 Anthropic 官方博客点名为多语言大库里"最高杠杆的投资之一":没有它,agent 在命名相似的代码里会落到错误的 symbol(整机树里 onTransact 这类同名符号成海,是最大的导航陷阱)。下面按"AOSP 提供了什么机制 → 我们怎么用 → Claude Code 侧怎么配 → 配好后 agent 怎么查代码 → 好处"讲清 C++ 侧的落地;Java/Kotlin 侧论证后刻意不配(见 5.2)。
5.1 C++:clangd + 两段式 compdb
① AOSP 为 LSP 准备好的机制。 clangd(C/C++ 的语言服务器)要工作,只需喂它一份准确的 compile_commands.json(compdb,编译命令数据库——记录每个源文件用哪些 flag 编译)。AOSP 内建了生成它的机制:让 soong 在分析阶段顺带吐出全树 compdb(几分钟,不需要真编译);树内还自带一份与构建工具链同版本的 clangd(prebuilts/clang/host/linux-x86/clang-*/bin/clangd)。
SOONG_GEN_COMPDB=1 m nothing# → out/soong/development/ide/compdb/compile_commands.json② 我们怎么用:两段式精简。 全树 compdb 对 clangd 太重(本树实测 113,371 条 / 1.97GB)。脚本 gen-compdb-clangd.sh 把它做成两段——soong 全树库只作过滤源,再按当前 feature 涉及的仓过滤出精简库;soong 版每文件已去重(同一 .cpp 的 shared/static/arch 变体只留一条),过滤后即净(用 ninja -t compdb 从构建图导出也行,但不去重,全树会膨胀到几十万条):

③ Claude Code 侧要做的配置。 让 agent 用上这套,只需四件事:
树根放一个 .clangd,把CompilationDatabase指向精简库的绝对路径,并开后台索引:
# 树根 .clangdCompileFlags:CompilationDatabase:<AOSP_ROOT>/out/soong/development/ide/compdb-feature/Index:Background:BuildClaude Code 通过 LSP(plugin 层) 把 clangd 挂进来(博客原话:"LSP is accessed through the plugin layer"),agent 便获得符号级能力; 让它拉起的是树内 prebuilt clangd(与生成 compdb 的工具链同版本,避免 flag 兼容噪音)——用 ~/.local/bin/clangd的 shim 按$PWD分树分发,多树共存时防串台(见第十节踩坑二);从树根启动 Claude Code,让 workspace root 与 .clangd、compdb 的绝对路径对齐。
④ 配好之后 agent 怎么查代码。 不再靠全树 grep 猜同名符号,而是走 LSP 的符号级能力——跳到定义(go-to-definition)、查所有引用(find-all-references)、列工程符号、hover 看类型签名。例如追一个 binder 调用,能从接口方法直接跳到具体实现,而不必在几十个同名 onTransact 里逐个排除;clangd 后台已建好索引(Index.Background: Build),preamble 秒级、AST 完整;只改函数体不用重生成 compdb——clangd 实时读源文件内容。
⑤ 好处。
精准:按 symbol 区分同名符号,不落到错误符号——这是整机树最大的导航陷阱; 省上下文:LSP 直接返回定义/引用的确切位置,把"过滤"放在 agent 读文件之前,不用打开一堆文件人工判断哪个才对(正是博客强调的 context 经济性); 快:秒级 preamble + 完整 AST + 后台增量索引; 永不过期:读的是 live 源文件,与 agentic search 同源的好处。
⑥ 两条属于"配置"一部分的实测坑。
一律不要给 AOSP 的 clangd 加 --query-driver。社区常见的这个参数在 AOSP 上是反作用:clangd 会"裸跑"驱动探测系统 include(不带 compdb 里的-nostdlibinc/-target全套参数),把宿主机 glibc 头以-isystem注入,与 bionic 头混装后爆出上百个__GLIBC_USE is not defined之类的错误。AOSP 的 compdb 命令本身已带全套 bionic-isystem,什么都不需要补。compdb 有时效:改了 Android.bp/Android.mk、repo sync、新增源文件后要重跑刷新脚本(只改函数体不用)——这条提醒本身也固化成了一条 path-scoped rule(读到构建文件时自动注入),不靠人记。
一个值得知道的上游坑:AOSP 曾有一个"省内存"的临时 commit(
b790b9cb8,2025-03,soong 核心作者所写),在每个 cc 模块构建动作结束就把compiler字段置 nil;而 compdb 生成器按设计在所有模块之后才遍历——遍历时七万多个 cc 模块的 compiler 全是 nil,收集到 0 条,SOONG_GEN_COMPDB=1 m nothing静默产出一个空[]。更新的 AOSP 基线已移除该 commit,但若你的基线恰好落在这个窗口,标准命令会"成功地什么都不生成"。检查方法:grep 'c.compiler = nil' build/soong/cc/cc.go;绕行方案是ninja -t compdb从 ninja 构建图提取,或给cc.go打三行补丁条件保留 compiler。
5.2 Java/Kotlin:论证之后,刻意不配
Java 侧我们完整走了一遍 aidegen + jdtls(Eclipse JDT 语言服务器)路线,最终结论是不配,且放弃本身是重要的探索成果:
命脉工具已弃用:aidegen 自己打印 "AIDEGen is no longer supported",官方引导到 Android Studio for Platform——那是 GUI IDE,对 agent 毫无帮助。 架构性不匹配:agent 的 LSP 以进程启动目录为 workspace root,而编译/adb 都要求从树根启动;jdtls 是 workspace 模型,会从根 URI 遍历整棵树找工程——实测空转吃约 2GB 内存、 documentSymbol反复 internal error;同一时刻 clangd 对.cpp秒回。无法收窄:官方 jdtls 插件是薄封装,不暴露限制扫描范围的配置。
这一战沉淀出后续所有工具选型的判据:
能靠"精确文件清单"喂的 LSP(clangd 读 compdb,不遍历树)就配;要靠"遍历大树 workspace"、且工具链已弃用的就不配。
Java/Kotlin 导航改用 Grep 搜符号 + Read,跨 Java↔JNI↔native 追踪时用 JNI 注册名(如 android_view_*)作 Grep 锚点。不硬配不是偷懒,是止损——后文"踩坑精选"里还有它写坏源码树的实证。
六、第②层 上下文:每个会话睁眼就知道"在哪、做什么、什么不能碰"
▶ Demo:
aosp-harness-demo里对应CLAUDE.md、.claude/settings.json、.claude/hooks/{load-feature,check-branch-drift}.sh、features/dev-sidebar/{_index,frameworks-base}.md与check-branch.sh(demo 用树根CURRENT_FEATURE模拟"锚定仓当前分支")。
这一层做的事一句话说清:在 Claude Code 的 SessionStart hook 里执行一个 shell 脚本,把「当前 feature 对应的上下文」注入到会话里。 于是 agent 每次启动"睁眼"就知道自己在做哪个 feature、能动哪些仓、什么不能碰——无需人工每次交代。
承接 4.1 的前提(一个 feature = 一个 repo 本地分支 = 3–8 个单仓),本层要解决的是:让 agent 一启动就知道当前 feature 是什么、涉及哪些仓、每个仓的约定在哪。四条需求:按 feature 组织、随分支自动切换、不污染 gerrit、大单仓省上下文。下文先给出方案的具体构成与端到端流程(6.1),再解释为什么非得这么设计——那个 git 语义层面的死结与破局(6.2),最后讲几个关键设计决策(6.3)。
6.1 方案概览:由哪些文件构成、启动时怎么跑
先看具体由哪些东西构成(都放在树根,不进任何 gerrit 仓):
features/—— 一个放在树根的独立 git 仓:每个 feature 一个子目录,目录名就等于该 feature 的 repo 分支名。以dev-sidebar为例:
features/ └── dev-sidebar/# 目录名 = 分支名 = feature 名 ├── _index.md# 启动时注入的"索引"(几百 token) ├── frameworks-base.md# 单仓约定(只在 agent 动到该仓时按需 Read) ├── check-branch.sh# 涉及仓分支一致性检查 └── verify-sidebar.sh# 确定性验证脚本_index.md—— 会话启动时喂给 agent 的"第一屏":内容极简——feature 目标一句话、涉及哪几个仓、每个仓的约定文件在哪、验证脚本入口。例如:
# feature: dev-sidebar目标:新增系统服务 SidebarService + 常驻边栏应用 SidebarApp 涉及仓:frameworks/base、frameworks/native、packages/apps/SidebarApp、build/make、system/sepolicy 单仓约定(动到再 Read):frameworks-base.md 验证入口:./features/dev-sidebar/verify-sidebar.sh.claude/hooks/load-feature.sh—— 注册在 SessionStart 上的脚本:负责"读当前分支 → 选中对应 feature 目录 → 把它的_index.md打印到 stdout"。Claude Code 会把 SessionStart hook 的 stdout 自动当作会话上下文注入。
启动时就跑这么一条链(工程师在树根敲 claude 那一刻):

一步步拆开:
Claude Code 启动,先加载树根 CLAUDE.md(固定 bootstrap + 硬约束,见 6.3)。触发 SessionStart hook load-feature.sh:它从 stdin 的 JSON 里拿到 cwd,再到锚定仓读当前 git 分支名(比如读出dev-sidebar)。脚本把 features/dev-sidebar/_index.md打印到 stdout,这几百 token 被注入为会话上下文。同一时刻, permissions.ask护栏生效、clangd + feature 精简 compdb 就绪。于是 agent "睁眼"第一屏就知道:在做 dev-sidebar、只该动那几个仓、验证跑哪个脚本——全程无需人工交代。
hook 脚本的核心不过十几行(拿 cwd → 读分支 → cat 索引)。几个容易踩的细节都已加固:
SessionStart 不写 matcher = 四种触发源全覆盖(startup / resume / clear / compact)。初版只覆盖 startup+compact, /clear之后 feature 上下文会静默消失。每条 prompt 跑一次分支漂移检测(UserPromptSubmit hook check-branch-drift.sh):比对当前分支与注入时的快照,会话中途repo checkout切了分支会收到一次告警,提示先读新分支索引再动手;没切则零输出、零打扰。锚定仓做成链:feature 不一定碰 frameworks/base,按 base → native → 下一候选的顺序找到第一个能读出分支的仓。
博客视角的一个补白:官方博客认为 hooks 最有价值的用法并不是"拦住 agent 做错事",而是让配置自我改进——用 stop hook 在会话结束、上下文还热的时候反思本次教训并提议更新 CLAUDE.md;用 start hook 动态加载团队/模块上下文,让每个人无需手动配置就拿到对的地基。我们当前的两个 hook(SessionStart 索引注入、UserPromptSubmit 分支漂移检测)已经落地了"动态加载"这一半,但还都停在"注入 + 防错"这一侧;"会话末反思 hook"已列入演进方向(第十一节),是把这套 harness 从"静态地基"推向"自我改进闭环"的下一步。
6.2 命门矛盾与破局:为什么 features/ 是"树根上的独立 git 仓"
上面这套结构里最不显然的一步,是"为什么 features/ 要单独做成一个放在树根的 git 仓,而不是就放进 frameworks/base 里、让它跟着分支走"。因为这里藏着一个 git 语义层面的死结:

即:「随分支变」与「不进 gerrit」在同一个 project 仓内不可兼得,必须把"随分支"这件事从 git 跟踪机制里拿出来,改由 hook 在会话启动时按当前分支动态注入(这正是 6.1 那条链)。方案推演走了五版:
features/ | ||
| SessionStart hook 按分支注入 | ||
features/,索引粒度注入 |
最终形态 = v4 + v5。
6.3 三个设计决策
为什么注入用"索引粒度"? 先说清什么叫索引粒度:像一本书的目录——只列"讲了什么、在第几页"这类指针,而不是把每一章正文都抄给你。落到本方案,就是启动时只注入 _index.md 里那几行指针(feature 目标 + 涉及仓清单 + 各约定文件的路径 + 验证入口),而不把每个仓的完整约定全文灌进会话。为什么这么设计:frameworks/base 这类单仓的约定若全量注入会每个会话常驻、挤占上下文;改成只注入索引、agent 真动到某仓时再顺着路径 Read 对应的 frameworks-base.md——用"一个 feature 涉及的单仓有限、且不会全被碰到"这个业务事实,换来上下文经济性。
为什么硬约束放根 CLAUDE.md 而不是全靠 hook? 因为子代理不吃 hook 注入的内容,但会加载 CLAUDE.md。"改 public API 必须跑 m update-api,否则 checkapi 挂构建"这种不知道会出事故的规则,必须让派出去的子代理也看见。同时遵守一条纪律:CLAUDE.md 只写 agent 默认会犯的错,不写文档。本树的硬约束一共六条:
m update-api | |
system/sepolicy | |
out/ 下任何生成物 |
为什么编译约定也要写进 CLAUDE.md? 有两个 agent 默认必错的点:envsetup 必须用 bash(工具默认 shell 可能是 zsh),且 source 后不能接 pipe(函数会进子 shell);一切编译必须后台跑 + 轮询日志(agent 的前台命令有超时上限,而单编模块十几分钟起步)。
bash -c'source build/envsetup.sh >/dev/null 2>&1 \ && lunch aosp_cf_x86_64_phone-trunk_staging-userdebug >/dev/null 2>&1 \ && m services' > /tmp/build.log 2>&1 &# 后台 + 日志轮询,# 看到 build completed successfully 才算完(顺带一个 Android 17 的新变化:lunch 目标是三段式 product-release-variant,release 段如 trunk_staging,可从 out/soong.log 的 TARGET_RELEASE= 反查。)
七、第③层 流程:动到哪片代码,就知道怎么编译验证
▶ Demo:
aosp-harness-demo里对应.claude/skills/build-services-jar/SKILL.md(paths: frameworks/base/services/**)与build-sepolicy/SKILL.md(paths: system/sepolicy/**),配合features/dev-sidebar/frameworks-base.md里"一句话指回 skill"的单一事实源写法。
这一层做什么。 一句话:让 agent 一动某片代码,就自动知道这片代码"怎么编译、产物在哪、要 push 哪些文件、怎么验证"——不用人每次交代,平时也不占上下文。这类"改了这片代码该怎么走完编译到验证"的知识是过程性知识(区别于第②层"在哪、做什么"那种背景知识)。
怎么做到的。 难点在于:把它全塞进根 CLAUDE.md,会每个会话常驻、白白挤占上下文。解法是 skill 按需加载 + paths glob 按路径激活——把每一类代码的编译验证流程各写成一个 skill,并在 frontmatter 里用 paths 标注它作用的代码路径;只有当 agent 真的 Read 到匹配路径的文件时,对应 skill 才被拉进上下文,其余时间零占用。

# .claude/skills/build-services-jar/SKILL.md(frontmatter 示意)---name:build-services-jardescription:编译/部署services.jar——改frameworks/base/services下代码时用paths:-"frameworks/base/services/**"---这里的 glob 就是文件路径的通配符匹配(和 shell 里 ls *.c 的 * 同源):* 匹配单层路径内的任意字符,** 跨目录递归匹配任意层级。所以 frameworks/base/services/** 的意思是"frameworks/base/services/ 目录下任意深度的任意文件"。skill frontmatter 里的 paths 字段就是给这个 skill 挂一组这样的路径模式;agent 每 Read 一个文件,Claude Code 就拿该文件路径逐条比对这些模式,一旦命中就把对应 skill 载入上下文,不命中就当它不存在。这就是它按"你正在动的代码落在哪个目录"来决定加载哪个 skill——比 hyperdroid 那种按关键词触发的粒度更准(路径是确定的,关键词会误触),也正是官方博客说的 skill "可 path-scoped、只在相关目录激活"。
repo 工程有个特殊决策点:skill 放哪。嵌套进 frameworks/base/.claude/skills/ 会被该 gerrit project 跟踪——又是污染问题。结论与 feature 工作流同构:集中放树根 .claude/skills/(树根不属于任何 project),用 paths glob 做作用域。前提是从树根启动 agent,而编译/adb 本来就要求树根 cwd,天然满足。
另一条重要纪律是单一事实源:初版曾在 skill 和 feature 单仓约定里把编译/push 流程写了两遍,评审时判定必然漂移。最终分工——skill 承载不随 feature 变的通用流程(编译命令、产物、push 清单、已知坑);features/ 单仓约定只写 feature 特有内容,流程一句话指回 skill。本树目前有五个这样的 skill:framework.jar、services.jar、inputflinger、边栏 app、sepolicy,每个都写明"后台编译 + 日志轮询、产物与 push 清单、快环稳环、编过≠改对指向 verify 脚本"。
八、第④层 护栏与验证:斩断"编过 = 改对"
▶ Demo:
aosp-harness-demo里对应.claude/settings.json的permissions.ask门禁与features/dev-sidebar/verify-sidebar.sh --demo(四步确定性断言、FAIL>0非零退出)。
这一层做什么。 一句话:在 agent 干活的两个危险时刻各设一道拦截——动设备/动源码树前别让它误操作,收工宣布完成前别让它把"编译通过"当成"改动正确"。前三层帮 agent 把事做顺,这一层专门防它把事做砸或自我误判。
怎么做到的。 两道护栏,都不依赖模型的记性:一是 permissions 硬门禁(permissions.ask),把危险命令变成系统级弹窗,模型上下文再长也绕不过(见 8.1);二是 确定性验证脚本(verify 脚本),把"改对了没有"编码成只输出 PASS/FAIL 的机器判定,堵死 agent 拿模糊输出自我安慰的路(见 8.2)。
8.1 permissions 硬门禁
"动设备先确认"写在文档里是软约束,模型上下文一长就可能被忽略。permissions.ask 把危险操作变成系统级弹窗,不依赖模型记性:
adb pushadb reboot / adb remount … | |
cvd startcvd stop / cvd reset、launch_cvd / stop_cvd | |
repo sync | |
m cleanm installclean |
8.2 确定性验证脚本
AOSP 没有"改完跑一下"的现成测试套,verify 脚本就是这个 feature 的测试。utzcoz 的原话:"没有这个,会话结尾 agent 会因为 build 成功就确信改动生效了。" 验证环编码成脚本、只编码一次,输出只有确定性的 PASS/FAIL——中间态会诱导 agent 把模糊输出读成成功。

以 dev-sidebar 的 verify 脚本为例,它做四步确定性断言:
sys.boot_completed=1(设备真的起来了);system_server存活;crash buffer 扫描(无新增崩溃); 新增系统服务与边栏应用存在性( service list/pm list packages命中)。
feature 早期允许部分断言标 SKIP,随开发推进逐项转为硬断言——这本身就是一种可执行的进度表。Cuttlefish 作为目标设备在这里显出独特价值:虚拟机的"稳环"(整机镜像 + cvd stop/start)是真机没有的兜底手段,push 出诡异状态时可以低成本回到干净基线。
九、串起来:一个会话的完整生命周期
四层不是四个孤立的配置,而是按时间协同的一条流水线:

换 feature 的成本约等于零:repo start dev-next --all + 建 features/dev-next/ 目录 + 重启会话,hook 自动跟随新分支;skills 完全不用动(paths 是按仓的,不随 feature 变)。features/ 是独立 git 仓,feature 上下文、上游调研、verify 脚本随开发演进提交,还可以推私有 remote 跨机同步。
一句话总结这套工作流:
启动时 hook 告诉 agent"在哪、做什么",干活时路径激活 skill 告诉它"怎么编怎么验",危险动作被 permissions 拦一道,收工前 verify 脚本判定"真的改对了"——四层各管一段,缺一层闭环就断。
十、这条路是怎么探索出来的
前面九节呈现的是结果——四层各就各位、彼此咬合,读起来像是一开始就照着蓝图搭的。真实过程要曲折得多:这套 harness 不是自顶向下设计出来的架构,而是被一个个具体的失败逼出来的。每一层的定型几乎都走同一条轨迹——先撞上一个具体故障 → 挖到根因 → 才沉淀出对应的设计决策,顺序恰恰和成品的呈现顺序相反。
回头看,整个探索大致是四场"战役"加一轮自审:前两场在代码智能层,分别啃下 C++(compdb 空数组的上游坑)和 Java(论证后刻意不配 LSP)两块硬骨头;第三场在上下文层,解开"随分支 ⇔ 被跟踪 ⇔ 污染 gerrit"这个 git 语义死结;第四场把流程知识从"全塞 CLAUDE.md"改造成 path-scoped skill。四层搭齐后我们没有就此收工,而是专门做了一轮自审——"配上了"不等于"能用",回头把六个会在真实会话里咬人的缺陷逐个补掉。这段历程里最有价值的往往不是最终方案,而是为什么放弃了另一些看似更直接的方案(比如 Java LSP、把 harness 做成 plugin)。
下面这张时间线先给全局,随后的「踩坑精选」再挑几个最有代表性、且都有实证的坑展开细节。

踩坑精选(每个都有实证)
坑一:Eclipse/jdtls 残留会写坏源码树。 一次 m 在 aconfig 阶段失败——flag already declared,而 frameworks/base 零本地提交。根因:此前 Java LSP 实验中 Eclipse 把资源文件连同 .class 拷进了 services/*/bin/,其中的 .aconfig 副本被 soong 的 **/*.aconfig glob 收进同一 aconfig_declarations,导致重复声明。更隐蔽的是 frameworks/base 自带 .gitignore 忽略 .project/.classpath,git status 完全看不见这批残留;甄别要用 git ls-files --error-unmatch 逐个判断是否 tracked(AOSP 一些老仓的 Eclipse 文件是 checked-in 的,不能一刀切删)。这让"禁配 Java LSP"从性能取舍升级成了硬约束——它不只吃内存,还会污染构建。
坑二:机器级 clangd 包装脚本劫持(多树共存必踩)。clangd --check 日志里出现了另一棵树的 compdb 路径 + "Compile command inferred" + 错误的目标架构——树根 .clangd 明明配对了。根因:~/.local/bin/ 里给旧树写的 clangd 包装脚本硬编码了 --compile-commands-dir,而 CLI flag 优先级压过一切 .clangd 配置。修复:包装脚本按 $PWD 分树分发,每棵树用各自的 in-tree clangd + 各自 compdb 绝对路径;树根 .clangd 一律写绝对路径双保险。速查信号:--check 日志里 ①DB 路径不是本树 ②"inferred"字样 ③目标架构/API 级不对。
坑三:--query-driver 在 AOSP 上是反作用。(详见第五节——宿主 glibc 头污染 bionic,删掉即清零。)
元教训:harness 自身也要用工程标准对待——设计完要评审、加固、实测,而不是"配上了"就算完。三大件搭好后我们专门做了一轮"这套方案还有哪些缺陷"的自审,修掉的六个缺陷里最典型的三个:长编译撞工具超时上限(改为强制后台+轮询)、/clear 后上下文静默丢失(matcher 全覆盖 + 漂移检测)、"动设备先确认"是软约束(升级为 permissions 硬门禁)。
十一、边界与下一步
已知边界(都是明确认下的取舍,不是遗漏):
何时重审:每 3–6 个月、或新一代模型发布后感觉规则见顶时,删过期/矛盾的规则——"为迁就某代模型缺陷写的规则,下一代模型上来就变成束缚"。另加一条我们自己的信号:依赖的工具链出现弃用声明时立即重审(aidegen 之鉴)。
可演进方向(按杠杆排序):
五段式 handoff 交接文档(What Was Done / How Verified / Files Modified / Blocker / Next)——跨会话 bug hunt 的最高杠杆,新会话读最新 handoff 即可冷启动续上; 会话末反思 hook——提议更新 CLAUDE.md,形成持续改进闭环; 冷启动 review 子代理对抗审查——作者 agent 偏 "ship it",无历史包袱的 reviewer 偏 "explain this"。 只读子代理测绘、主代理编辑——博客点名的 subagent 核心模式:整机树上"探索"极烧上下文,可先派一个只读 subagent 去测绘某个子系统(跟调用链、读 dumpsys、定位改动点),把发现写进一个文件,主代理再带着全貌下手编辑,避免探索的中间产物挤占主上下文。要与本树"hook 注入不进子代理"的边界(见上表)配套——派发时必须把 feature 上下文转述进子代理的任务描述。
结语
这套 harness 工程没有任何一处依赖"更聪明的模型":它做的全部事情,是把一位 AOSP 老工程师带新人时会做的四件事——给他一张地图(上下文)、教他怎么编译(流程)、告诉他哪些红线不能碰(护栏)、要求他改完必须验证(闭环)——翻译成了 agent 基础设施。模型每半年换代一次,而这棵树上的地基,每个会话都在复用。
参考资料
本文配套可跑 Demo:yuandaimaahao/aosp-harness-demo(四层方案的最小可运行复刻,关键脚本带 --demo,无需真实 AOSP 树,./run-demo.sh一键演示)Anthropic 官方博客:How Claude Code works in large codebases(agentic search 的代价与甜区、"上下文是唯一稀缺资源"、五扩展点 CLAUDE.md/hooks/skills/plugins/MCP + LSP + subagents 及其叠加顺序、hooks 的"自我改进"用法、subagents"探索与编辑分离"、"子目录初始化"建议、plugins 分发反部落化、LSP 最高杠杆、每 3–6 个月重审) utzcoz:Using Claude Code on AOSP-scale projects(harness engineering 十模式,https://utzcoz.github.io/2026/04/26/using-claude-code-on-aosp-scale-projects.html) Claude Code 官方文档:hooks / skills / memory / large-codebases(SessionStart 注入机制、 pathsfrontmatter 语义、CLAUDE.md 加载规则)AOSP 树内文档: build/soong/docs/compdb.md;相关源码build/soong/cc/compdb.go、build/soong/cc/cc.go社区同类方案(见第三节):Lightrion AOSP RAG(https://lightrion.com/docs)、hyperb1iss/hyperdroid-skill(https://github.com/hyperb1iss/hyperdroid-skill)、jonaschen/Android-Software(https://github.com/jonaschen/Android-Software)
夜雨聆风