
把“帮我写一个首页 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类里到底有没有它引用的那个资源 IDAndroidManifest.xml里有没有注册它写的那个 Activitybuild.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是 24AI 直接使用 API 33 才引入的LocaleManager.setApplicationLocales()来做“应用内切换简体中文”真机上低版本直接NoSuchMethodError崩溃targetSdk升到 34 之后代码里动态注册广播接收器没有给RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED标记Android 14 设备上直接抛SecurityExceptionAI 在 Android 13 及以上设备上申请通知权限的流程不完整只写了POST_NOTIFICATIONS权限声明却没有运行时弹窗请求。这类问题唯一的解法就是把手里的 SDK 版本和规则写进给 AI 的上下文里而不是让它去猜。你的工程底线必须成为它工作环境的默认前提。3.3 依赖层坐标错一个字母编译崩一个下午AI 对第三方库的“知识”经常停留在某个历史时期。最经典的坑包括在新项目里用了老旧的com.android.support系列依赖和不存在的androidx混在一起导致依赖冲突写了一个jcenter()时代的依赖坐标而项目仓库已经换成了mavenCentral()为了让“代码能编译”自作主张锁死一个过老或过新的依赖版本引发传递依赖冲突升级完compileSdk之后没有同步升级 AGPGradle 编译直接报“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 / compileSdk24 / 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 需求还经常翻车别急着换模型、换插件先把这套缰绳套上去试试。