把“帮我写一个首页 Banner 轮播”扔给 AI,十秒就能得到一段看着非常专业的 Kotlin 代码,然后就是熟悉的翻车三连:编译报错、运行闪退、轮播不转。在过去一年里,我每天都在和各种 AI 对话做 Android 需求,直到接触了 Harness Engineering 这套思路,才真正想明白翻车的根因——不完全是 AI 笨,而是我们给 AI 的“工作环境”不对。这篇文章就聊聊:Harness Engineering 到底在讲什么,以及怎么把它落地到 Android 的开发流程里,让 AI 从一个事故制造机变成真正的生产力。
1. AI 写 Android 代码,翻车的深层原因不是“笨”
1.1 “代码看着真”和“代码能跑”之间,隔着一整条流水线
很多人第一次用 AI 写 Android 需求时,都有一个共同错觉:AI 输出的代码格式工整、注释齐全、命名规范,看起来比不少初级开发写的还像样,于是直接复制进项目。结果一等 Gradle 跑起来,问题全出来了。
这里面的核心原因在于大语言模型的本质:它训练时的目标是预测下一个 token,追求的是“上下文里最像样的文字”,而不是“能通过构建、能在真机上运行的代码”。换句话说,它擅长生产的是“看起来合理的代码”,而我们需要的却是“在特定工程里从资源引用、到构建配置、再到运行时都正确的东西”。
Android 恰好是这个差异最明显的领域。一段代码能不能跑,不取决于语法漂不漂亮,取决于一连串外部约束:
R类里到底有没有它引用的那个资源 ID;AndroidManifest.xml里有没有注册它写的那个 Activity;build.gradle里有没有开启 ViewBinding、有没有对应的依赖坐标;targetSdk和运行设备 API 之间的权限差异有没有处理。
AI 写代码时对这些约束只有“语感”,没有“实感”。这就是为什么它经常写出一段在语法上零错误、一编译就报一堆资源找不到的代码。你骂它笨,其实它只是不了解你的工程环境。
1.2 Android 的客观复杂度,天然是幻觉放大器
相比 Web 后端或者脚本类任务,Android 需求的复杂度是系统性的,我归纳成下面几个维度:
| 维度 | 具体表现 | 对 AI 的难度 |
|---|---|---|
| 设备碎片化 | 各家厂商改系统行为、后台限制策略、WebView 内核各不相同 | 高 |
| 构建链咬合 | AGP、Gradle、Kotlin、JDK 版本必须互相匹配 | 高 |
| 生命周期与线程 | 主线程 Looper 规则、Activity 销毁恢复、协程作用域 | 极高 |
| 系统组件交互 | 权限弹窗、Intent 解析、ContentProvider、广播接收器 | 高 |
| 资源与国际化 | 资源名约束、多语言、屏幕适配 | 中 |
举一个我在代码 review 里真实见过的例子:AI 给一个ProgressBar写进度时,直接用了setProgressCompat,表面上看是一个很“现代化”的写法,但普通 View 体系的ProgressBar根本没有这个方法,编译直接挂。另一个更典型的例子是 AI 写组件代码时默认会使用 ViewBinding,却在build.gradle里忘了开启viewBinding开关,导致满屏Unresolved reference: binding。
这些都不是模型能力问题,而是它对你项目上下文的无知。理解了这一点,真正的解法就很清晰了:不要指望模型“变得更懂”,而是要把你的工程约束变成模型工作环境的一部分。这正是 Harness Engineering 的切入点。
2. Harness Engineering 到底讲什么:别押注模型,押注回路
2.1 “缰绳”不是限制 AI,而是让它的力气有方向
Harness 英文原意是马的挽具、缰绳。这套概念在最近关于 AI Agent 和 LLM 应用工程化的讨论里被反复提到,核心主张其实非常朴素:当我们使用大模型或 AI Agent 时,真正决定产出质量的,往往不是模型本身有多聪明,而是模型外面那一整套“工作装具”——你给了它什么上下文、允许它调用什么工具、设置了哪些验证关卡、出错后如何把信息反馈回去。
骑马的人都明白一个道理:马越有力气,越需要一套合身的缰绳。缰绳不是为了限制马跑,而是为了让每一次发力都有方向、可控制。AI 也是一样,它劲大、知识面广、生成速度快,但如果周围没有任何约束和回馈机制,它的“劲”就会乱使。很多人觉得给 AI 提需求越简单越好,放手让它发挥,结果就是收回来一堆需要大改的代码。真正靠谱的做法,是先设计好它工作的“环境”,再让它动笔。
2.2 核心是“回路”:从单向生成,变成生成-验证-反馈-再生成
Harness Engineering 在工程上的落地,本质上就是把过去那种“人给 AI 一个需求,收下一份代码”的单向流程,改成一种带反馈回路的循环。这个循环有四个环节:
- 限定上下文:把环境信息、约束条件、验收标准提前塞给模型,压缩它的自由发挥空间;
- 强制验证:让 AI 的产出必须通过客观关卡——编译、lint、测试、真机启动;
- 收集信号:把验证过程中产生的报错、崩溃栈、运行异常当作反馈数据;
- 回灌修正:把这些信号连同“失败原因”一起给回模型,让它基于事实修正,而不是重新瞎猜。
这个回路每循环一次,AI 的解决方案空间就会被压缩一圈,翻车概率也指数级下降。对开发工作流来说,它真正的价值在于:出错的成本变便宜了。你不指望 AI 一次写对,但你要让写错之后的修正成本降到最低。
2.3 为什么 Android 是实践这套思路最容易出成绩的领域
你会不会觉得奇怪:Android 明明翻车率那么高,为什么反而是最适合实践 Harness Engineering 的领域?
原因很简单:Android 有着几乎是软件工程里最丰富的“客观验证信号”。构建工具会给出具体的编译错误和行号,lint 会告诉你潜在风险,单元测试能验证逻辑,logcat 会在崩溃时输出完整的调用栈。这些信号全部是结构化、可读取、可回灌给 AI 的。
我自己做过一个对比实验:同一个需求,分别让 AI 写“一段 Python 脚本”和“一个 Android 页面功能”,前者的错误往往要到运行到特定输入时才暴露,而后者的错误在./gradlew assembleDebug这一步就能拦下一大半。换句话说,Android 翻车的重灾区,恰恰是反馈信号最丰富的地方。差别只在于:你有没有把这些信号组织成闭环。大多数人翻车,就是因为只把 AI 当成生成器,而没有把它放进一个可验证的工程回路里。
3. Android 需求中,AI 翻车的五个高发区
这一节是我从实际项目里总结的高发区,每一个都真实踩过或者 review 到过。你可以把它当成一份“AI 代码审计清单”来用。
3.1 API 层:一本正经编造不存在的接口
这是 AI 翻车最普遍的一种。它会把不同框架的 API 混搭,或者自创一个“听起来很合理”的方法。常见案例包括:
- 把 Android KTX 的扩展函数当成原生 API 直接调用,比如在没引入
core-ktx的工程里写了view.isVisible = true或者lifecycleScope.launch; - 把 Compose 的 API 写到 View 体系里,在 XML 布局对应的代码里要求
Modifier.clickable; - 混淆不同类的方法,写一个
Color.parse("#FF0000")、Toast.show("...")之类不存在的静态方法。
这类错误的可怕之处在于:语法完全正确,IDE 甚至不会给你划红线,编译时才大面积爆红。要治它,单纯靠肉眼 review 效率太低,最好让编译和依赖管理体系当第一道关卡,把“用了不存在的 API”扼杀在构建阶段。
3.2 版本层:三个 SDK 数字,AI 永远记不住你的底线
minSdk、targetSdk、compileSdk这三个数字,是 Android 项目最重要的“契约”,而 AI 几乎不会主动去看你的build.gradle,它会凭训练数据里的“主流写法”乱猜。
举几个真实的版本差异问题:
- 应用
minSdk是 24,AI 直接使用 API 33 才引入的LocaleManager.setApplicationLocales()来做“应用内切换简体中文”,真机上低版本直接NoSuchMethodError崩溃; targetSdk升到 34 之后,代码里动态注册广播接收器没有给RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED标记,Android 14 设备上直接抛SecurityException;- AI 在 Android 13 及以上设备上申请通知权限的流程不完整,只写了
POST_NOTIFICATIONS权限声明,却没有运行时弹窗请求。
这类问题唯一的解法,就是把手里的 SDK 版本和规则写进给 AI 的上下文里,而不是让它去猜。你的工程底线,必须成为它工作环境的默认前提。
3.3 依赖层:坐标错一个字母,编译崩一个下午
AI 对第三方库的“知识”经常停留在某个历史时期。最经典的坑包括:
- 在新项目里用了老旧的
com.android.support系列依赖,和不存在的androidx混在一起,导致依赖冲突; - 写了一个
jcenter()时代的依赖坐标,而项目仓库已经换成了mavenCentral(); - 为了让“代码能编译”,自作主张锁死一个过老或过新的依赖版本,引发传递依赖冲突;
- 升级完
compileSdk之后没有同步升级 AGP,Gradle 编译直接报“AGP 版本与 Gradle 版本不兼容”。
依赖问题有一个特点:报错信息指向的往往不是真正的原因,AI 如果靠猜来修,很容易陷入“改了 A 又坏 B”的循环。我的建议是:在提示词里明确“除非必要,不新增第三方依赖”,如果 AI 认为必须加,要求它同时给出完整的坐标、版本和引入理由,等你确认后再改。
3.4 生命周期与线程:一上真机就露出马脚
AI 写代码时默认的世界是“程序从 main 开始跑”,但 Android 的世界是“Activity 随时可能被销毁、onPause 和 onResume 会反复触发、UI 只能在主线程操作”。于是这些典型的运行时翻车就出现了:
- 在协程里用了
Dispatchers.IO去更新控件,运行时被CalledFromWrongThreadException砸脸; - 在主线程上做网络请求,
NetworkOnMainThreadException直接闪退; - 用
Handler.postDelayed做轮播自动播放,但没有在onPause里移除回调,Activity 销毁后回调仍然触发,轻则内存泄漏,重则IllegalStateException; - 用
GlobalScope.launch发起异步任务,任务还没结束页面已经销毁,回调里更新 UI 直接崩。
这类问题在编译期完全看不出来,是 AI 代码“能编译但不能上线”的主力原因。要处理它,光靠 review 不够,得靠运行时验证和把生命周期要求直接写进需求约束里。
3.5 构建与资源层:Android 专属的“最后一公里”
最后这一类是最隐蔽的:代码逻辑没问题,编译也没问题,但构建产物就是不对,或者一装到手机上就崩。常见的包括:
- 写了一个
Activity,忘了要求你把它注册进AndroidManifest.xml,一启动就是“Unable to find explicit activity class”; - 资源文件名用了大写字母或者非法的首字母,构建时资源编译失败;
- 中文字符串直接硬编码在 Kotlin 或布局文件里,lint 报 HardcodedText 警告,国际化时全部傻眼;
- 缺少混淆规则,release 包上反射类全线失效。
这些问题单个看都不大,但排在一起会让“AI 写代码”这件事的体验变得非常糟糕。好在它们也全部能被构建工具和 lint 检查出来,属于“缰绳一”就能拦住的那类错误。
4. 我的“五道缰绳”:把 Harness Engineering 落地到 Android 需求
接触 Harness Engineering 之后,我把上面那些翻车原因和这套理论做了一次整理,沉淀出了五个实操原则。我不太喜欢讲虚的,下面每条都对应真实的工程动作。
4.1 缰绳一:需求写成“上下文块”,不给 AI 自由发挥的空间
我见过太多人给 AI 的需求,就一句话:“帮我写个登录页”。然后 AI 自由发挥出一个假设你没用 Retrofit、假设你的后端返回结构是某某格式、假设 UI 风格是某某样式的完整页面,最后十有八九不能用。
我现在的要求是:明确的核心需求 + 严格的约束 + 验收标准。一开始我不要求,只靠举例来给约束,后来干脆整理成了一套模板,效果是有一次让团队成员试用,直接把 AI 生成的可用率从三成提到了七成以上。这个模板我会在第六节贴出来,这里先讲设计逻辑:给 AI 的上下文越明确,它的解决方案空间就越小,你后续返工的成本就越低。
4.2 缰绳二:把编译断言设为最低门槛
无论 AI 写了什么代码,进入项目的第一道关卡必须是机器验证,而不是人眼判断。我的习惯是,让 AI 改完代码后立刻执行:
./gradlew assembleDebug ./gradlew lintDebug前一个命令验证“能不能编译”,后一个验证“有没有明显的代码隐患”。如果 build 失败,直接把报错信息原样贴回给 AI,让它基于真实的报错去修改,而不是自己打开 IDE 一点点排查。
这个小习惯看起来笨,但它解决了 AI 协作里最大的问题:AI 不知道它错了。没有构建系统的反馈,它就只会一错再错。反过来,只要编译错误能第一时间回灌给它,它的自我修正能力会强到让你惊讶。
4.3 缰绳三:用最小可运行样例框住问题范围
AI 特别喜欢一次性给你“完整方案”,动辄改五个文件、跨越三个模块。问题是:如果最终不能运行,你根本不知道是哪个文件的哪一行出了问题,排查成本急剧上升。
我现在的做法是:把需求拆成以“30 分钟能验证完”为颗粒度的小块,要求 AI 在尽量少的文件里交付自包含的代码。拿 Banner 轮播来说,我不会让它改动整个首页布局,而是要求它:交付一个BannerAdapter.kt、一个banner_item.xml、一段在主页面集成的最小示例代码,以及三步以内完成集成的说明。
范围小,失败就能快速定位;定位快,反馈回路就转得快。这是把“出错的代价变便宜”的另一种体现。
4.4 缰绳四:把 logcat 栈当成给 AI 的体检报告
代码能编译、能安装,不代表没有运行时问题。真机或模拟器上闪退时,不要急着自己在代码里翻,第一步是拿到崩溃现场:
adb logcat -c # 清空日志 adb logcat -v time | grep -E "FATAL|AndroidRuntime"然后把堆栈信息原样发给 AI,并附上一句固定句式:“这是运行时崩溃堆栈,请先分析根因,再说修复方案,最后给修改后的完整代码。”你会发现,当 AI 面对具体的堆栈信息时,它“猜”的成分会大幅减少,给出的分析往往能直接命中问题。这其实就是反馈回路里最关键的一环:让 AI 基于证据修正,而不是基于想象发挥。
4.5 缰绳五:业务语义必须人工兜底
前四道缰绳可以靠工具自动化,这一道缰绳必须靠人。AI 永远不知道你的业务里“用户连续点击被判定为重复提交”意味着什么,也不知道“离线状态下首页要展示缓存 Banner”是真需求还是伪需求。
所以不管 AI 生成的代码编译多顺、测试多绿,我最后都会做一遍针对业务语义的 review,重点关注三件事:行为是否符合产品预期、有没有处理异常态和边界态、有没有涉及敏感权限和用户数据。AI 是效率工具,但它不是业务责任人。你可以放权让它写代码,但最终是否上线,责任永远在你自己。
5. 实录:一个 Banner 轮播需求,从翻车到稳定的全过程
理论说多了容易飘,我用一个真实需求把上面的“五道缰绳”串起来。这个需求本身很常见:首页顶部有一个可折叠的标题栏,下面是一个自动轮播的 Banner,要求用CoordinatorLayout + AppBarLayout + ViewPager2实现。
第一轮:没有任何约束的请求,果然翻车
我故意先用最原始的方式提问:“帮我写一个 android 协调布局 banner 轮播的代码”。
AI 在十几秒内返回了一段两百多行的代码:用了老的ViewPager而不是ViewPager2,自动轮播用Handler.postDelayed写了个死循环,图片加载用的是不存在的依赖坐标,页面销毁时回调完全不清理。复制进去,三个编译错误加埋在运行时的一个闪退雷。
这一轮给我的启发是:它的知识停留在“老教程”时代,而且它不知道我项目的依赖和 SDK 版本。翻车的责任一半在它,一半在我——我没有给它任何上下文。
第二轮:套上缰绳,重新组织需求
我这次把需求组织成了标准上下文块:
【环境信息】minSdk 24、targetSdk 34、AGP 8.2、Kotlin 2.0。 现有依赖:androidx.appcompat、material、lifecycle-runtime-ktx、viewpager2、glide 4.16。 【需求】首页顶部是 CoordinatorLayout + AppBarLayout, 内含一个可折叠的搜索栏区域,下方是 ViewPager2 实现 Banner 轮播, 每 3 秒自动切换,支持手动滑动,滑动后重置计时。 【约束】只使用上述依赖,不新增第三方库; 自动轮播必须用 lifecycleScope + repeatOnLifecycle 实现,不允许用 Handler 循环; 中文字符串放进 strings.xml;图片用 Glide 加载。 【验收】assembleDebug 编译通过;在 API 24 模拟器和 API 34 真机各运行一次不崩溃。这一次 AI 返回的代码整体靠谱了很多,ViewPager2、Glide、ConstraintLayout 都选对了,但依然有两个问题:第一,build.gradle里没开启 ViewBinding,代码里的binding.xxx全部报红;第二,轮播虽然用了协程,却把setCurrentItem放到了Dispatchers.IO上,一运行就崩,日志显示CalledFromWrongThreadException。
第三轮:用反馈回路解决问题
我没有自己动手改。第一次编译失败,我把报错信息贴回给 AI,它马上给出了开启 ViewBinding 的修改方案。改完能编译了,装上真机一跑立刻闪退,我又把 logcat 里的堆栈贴给它,它迅速定位到了线程问题,并且主动改成了这样:
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { while (true) { delay(3000) binding?.bannerPager?.setCurrentItem(current, true) } } }这段代码好在哪?repeatOnLifecycle会在页面处于 RESUMED 状态时执行协程体,页面进入 onPause 后自动取消,重新可见后重新启动,既解决了自动播放问题,也解决了生命周期清理问题,完全不需要手动 removeCallbacks。这是 AI 在拿到真实反馈后给出的高质量修复,比它第一轮凭空生成的答案靠谱了一个量级。
第四轮:边界验证
到这里代码能跑了,但我又追加了两个手动测试:快速滑动 Banner 五次,观察是否出现竞态;切到后台再回来,确认轮播是否恢复。这两项其实是对 AI 代码的“业务语义”兜底,它不会主动考虑这种边界行为,但作为开发者,验证这些是我的底线。
整个流程走下来,AI 真正花在“写代码”上的时间其实很少,绝大部分时间花在我们之间的反馈循环上。也正因为如此,最终交付的质量是稳定可控的。这就是 Harness Engineering 在实践中最直白的解释:不是让 AI 一次写对,而是让你和它之间形成一条高效修正的回路。
6. 可以直接抄:提示词模板、验证命令与验收清单
这一节把我现在工作里直接用的一整套东西放出来,你可以根据自己的项目抄作业式调整。
6.1 “AI 需求四段式”提示词模板
【环境信息】 项目架构:单 Activity + Fragment / MVVM / Compose(按实际写) minSdk / targetSdk / compileSdk:24 / 34 / 34 构建配置:AGP 8.2 + Kotlin 2.0 + ViewBinding 已开启 现有依赖:androidx.appcompat、material、lifecycle-runtime-ktx、 viewpager2、glide 4.16、retrofit 2.9 【需求】 (写清楚页面结构、交互流程、数据来源、期望效果) 【约束】 1. 只能使用现有依赖,如需新增依赖必须先说明理由; 2. 所有异步任务必须跟随生命周期取消,禁止用 Thread / GlobalScope; 3. UI 更新必须发生在主线程; 4. 中文字符串统一放进 res/values/strings.xml; 5. 不要修改与本需求无关的文件。 【验收方式】 1. ./gradlew assembleDebug 编译通过; 2. lintDebug 无新增阻断问题; 3. 覆盖 API 24 和 API 34 两种环境运行验证; 4. 补充必要的边界测试点(快速滑动、切后台、低内存等)。这个模板的精髓在【约束】部分。很多人写提示词只写需求不写约束,等于让 AI 在一个没有缰绳的场地上乱跑。有了约束,它才能真正理解“你的项目”和“一个泛化的 Android 项目”的区别。
6.2 一套我自己用的验证命令清单
# 编译 + lint ./gradlew assembleDebug ./gradlew lintDebug # 安装到已连接的设备/模拟器 adb install -r app/build/outputs/apk/debug/app-debug.apk # 启动页面 adb shell am start -n com.example.app/.MainActivity # 抓崩溃日志 adb logcat -c adb logcat -v time | grep -E "FATAL|AndroidRuntime"如果你在用 Android Studio 的 AI 助手或各类 AI 编程插件,建议把这几条命令的执行结果设置为 AI 修改代码之后的“必经流程”。每次 AI 给出代码,你就跑一遍这条命令链,把失败信息回灌给它。坚持一段时间,你会发现它生成的代码质量会显著提升,因为它从反馈里“学会”了你的工程偏好。
6.3 AI 返回代码后的三分钟人工 review 清单
| 检查项 | 为什么重要 |
|---|---|
| Activity / Service / Provider 是否注册 | 漏注册 = 启动即崩 |
| UI 更新是否都在主线程 | 线程错误是运行时崩溃第一原因 |
| 异步任务是否随生命周期取消 | 不取消 = 内存泄漏 + 崩溃风险 |
| 用到的 API 是否在 minSdk 范围内 | 高版本 API 在低版本设备直接崩溃 |
| 字符串是否硬编码 | lint 警告 + 国际化灾难 |
| 新增依赖是否有必要 | 每一行依赖都是潜在冲突源 |
| 业务边界是否有人工确认 | AI 不懂产品语义,最终责任在人 |
这张表不需要全量打印出来对着打勾,我一般看代码时心里过一遍,大概两三分钟。它存在的意义是提醒你:AI 做完了 80% 的机械工作,剩下的 20% 恰恰是决定“上不上线”的那部分。
7. 边界感:哪些 Android 需求,我坚持不让 AI 单独产出完整代码
最后聊一个很少被提及、但我觉得特别重要的话题:不是所有需求都适合交给 AI,哪怕你把它“驯服”得很好。
我现在的判断标准不是“AI 能不能写”,而是“如果 AI 写错了,验证和挽回的成本有多高”。按照这个标准,下面几类需求我基本不让 AI 单独产出完整代码:
第一类是支付、登录鉴权、签名密钥相关。这类代码一旦出错,直接涉及资金安全和用户隐私,验证成本极高,而且一旦上线出问题,后续补救代价远超人力成本。AI 可以辅助 review,但核心逻辑必须人来写、人来主导。
第二类是复杂状态机,比如离线任务队列、埋点去重、断点续传。这类需求依赖大量隐性的业务状态转换,AI 根本不具备那种“对业务上下文的连续性理解”,让它写出来的状态机大概率会在某个边界态上翻车,而且这类 bug 特别难复现、特别难定位。
第三类是某些厂商适配逻辑。不同手机厂商对后台限制、通知行为、保活策略有完全不同的实现,AI 的知识库跟不上厂商的频繁更新,与其让它编造一套“你以为没问题”的适配代码,不如根据真机测试结果老老实实写。
第四类是涉及合规判断的逻辑,比如隐私权限的最小化申请、用户数据的收集范围。这类需求的政策边界经常更新,而且不同地区规则不同,不能靠模型的概率知识来回答。
把这些排除掉之后,AI 仍然有大量发挥空间:普通页面 UI 实现、RecyclerView 列表、网络请求封装、工具类、单元测试、数据库操作、动画效果。它尤其擅长那些“验证信号明确”的工作——因为编译器和测试框架就是最可靠的缰绳。
写到最后想分享一个最近的心得:我把自己和 AI 的协作模式从“上下级”调成了“带教关系”。上级只给命令,出错了只会骂;带教会给上下文、给反馈、给边界,让被带的人越干越稳。Harness Engineering 本质上就是这个朴素道理的工程化版本。如果你现在用 AI 做 Android 需求还经常翻车,别急着换模型、换插件,先把这套缰绳套上去试试。