最近,我利用 GitHub Copilot Chat 背后强大的模型 Codex,对一个名为 Fread 的项目进行了一次大规模重构。这次重构累计改动超过一万行代码,若手动完成可能需要持续一个多月,但在 AI 的辅助下,几天内便完成了核心工作。今天就来回顾一下这次使用 Codex 进行大规模重构的整体感受,并分享一些实践证明有效的使用技巧。

本次重构任务聚焦于两个核心方面:
- 依赖注入框架迁移:将项目中原有的
kotlin-inject 替换为 koin。
- 导航框架升级:使用
navigation3 (AndroidX Navigation Compose) 替换现有的 Voyager 导航库。
选择 Koin 的原因在于我个人更偏好其 DSL 方式来管理依赖注入。这种声明式的代码风格通常更清晰,理解成本也更低。此外,由于依赖注入涉及的类非常多,基于注解的方案不仅对类有侵入性,还会生成大量辅助类,使得整个注入流程更像一个“黑盒”,不够直观。
而将导航框架切换到 Navigation3,则是因为 Voyager 已有一年多未更新,基本处于停滞状态。反观 Navigation3 的设计非常优雅,对预测性返回和 Shared Element 过渡动画的支持都很出色,没有理由不采用它。
可想而知,这是一个浩大的工程,几乎涉及到项目中的每一个页面和每一个依赖注入声明。最终,据不完全统计,约有 70% 的代码变更由 Codex 完成。本文就将详细介绍我是如何组织并指挥 Codex 完成这次大规模重构的。
本次改动的完整 Pull Request 可在此查看:https://github.com/0xZhangKe/Fread/pull/86
充分利用 AI 的模仿能力
目前,我认为 AI 编程最强大的能力之一就是“模仿”。对于一种给定的代码模式或转换规则,Codex 可以模仿得非常出色,即便在过程中遇到一些例外情况,它通常也能随手处理掉。
因此,在这次重构中,我作为“人类工程师”的主要任务,实际上是找出所有不同类型的代码变更模式,并对它们进行分类。也就是说,原项目中 kotlin-inject 的使用方式可以归纳为有限的几种。我需要针对这几种情况,分别手工编写出对应的 koin 替换方案。
一旦建立了这种从“旧模式”到“新模式”的一一对应关系,剩下的事情就简单了:让 AI 分别模仿我写好的这几类重构代码,去批量修改项目中所有类似的代码片段。
这样做极大地降低了 Codex 需要解决问题的复杂度。它只需要专注于模仿我的代码,按部就班地完成剩下的重复性工作。对于越复杂、越开放的问题,Codex 越容易出错。最初,我曾尝试直接让 Codex 去重构整个依赖注入层,结果它运行了1.5小时,不仅耗尽了多轮对话的上下文,也用光了数小时的额度,问题却依然没解决——因为依赖关系太复杂,它自己也“晕”了。

其次,这种方式让整个重构过程更加可控。毕竟,我对软件整体架构的理解比 AI 更深入,架构演进的方向也由我把控。所以,我应该发挥自己的专长,先把复杂的、非模式化的问题解决掉,再将海量的、模式化的重复工作交给 AI。我们各司其职,效率最高。
现在,当我使用 Codex 处理较大的任务时,基本都会先为它提供一个“最佳实践”示例代码,让它参照这个范本去工作。
特殊情况特殊处理
Fread 是一个基于 Kotlin Multiplatform (KMP) 的跨平台项目。这意味着依赖注入不仅涉及通用代码层 (commonMain),还存在许多平台特定的实现层 (androidMain, iosMain)。多种情况叠加,使得问题变得更加复杂。
很多时候,一些特殊的边界情况我们自己只需要几行代码或很短时间就能理清并解决,但对于 AI 来说,思考这类问题的复杂度会成倍增加,耗时也会很长。Codex 一方面对 KMP 项目的了解可能不深,另一方面对于如何正确处理平台实现层的依赖注入,也可能无法给出最优方案。
对于这类“特殊情况”,我的解决办法依然是“分而治之,人工引导”。我会先手动编写部分关键代码,搭建好框架,然后交给 Codex 去完成剩余的模式化部分。接着,我再进行下一轮的手动编码和 AI 辅助。
具体到本次重构,对于每个包含依赖注入的模块,我都会先手动声明一个如下的 expect 函数,并将其注册到对应的 Koin 模块中。
expect fun Module.createPlatformModule()
val commonModule = module {
createPlatformModule()
}
然后,在 Android 和 iOS 平台创建具体的 actual 实现:
actual fun Module.createPlatformModule() {
}
我先指导 Codex 为所有模块批量添加这个改动。这一步完成后,下一步就是将原本 kotlin-inject 模块中声明的、属于平台层的依赖,注册到刚刚创建的 Koin 平台模块 (createPlatformModule) 中。这时,问题就变得清晰且模式化了,Codex 可以非常出色地完成这种“声明迁移”工作。
如此循环,逐步完成所有模块的平台层依赖注入重构。
任务拆分至关重要
由于 Codex 的上下文长度有限,面对复杂任务时,它很容易“迷失方向”,甚至会对代码做出一些奇怪的、与初衷不符的改动。
任务拆分的逻辑在于:对于复杂的宏观任务,架构设计和关键路径规划这些工作仍然由人类完成。这部分框架搭建好后,就可以继续拆分出许多独立且简单的子任务,再交给 Codex 去执行。这样一来,即使 Codex 在某个子任务上犯错,影响范围也很有限,回滚代码所耗费的 Token 也更少。
此外,对于 AI 生成的代码,我个人习惯一定会进行全面的人工 Review 后才接受。如果不做任务拆分,Review 的工作量将变得极其庞大,大脑也难以承受。
复杂问题善用 SKILL
用 Navigation3 替换 Voyager 这个任务,改动的代码量巨大,且包含大量重复性工作。即使我已经为 Codex 写好了“最佳实践”示例,但由于任务本身过于复杂,Codex 仍可能出错。这时,我们就可以通过为 Codex 创建详细的“SKILL”(或称为“自定义指令”)来规范其行为。
---
name: screen2navkey
description: convert Voyager Screen to navigation 3
---
## 任务背景
目前项目中使用了 Voyager(cafe.adriel.voyager:voyager-navigator) 作为导航框架,现在我希望将 Voyager 替换成 navigation3(androidx.navigation3:navigation3-runtime).
## 任务内容
目前 nav3 我已经集成并且完成了部分代码的重构,现在你需要帮我做一件事情,将一些 Screen 替换成 一个 Composable 函数 + NavKey。
你的目的是把继承自 cafe.adriel.voyager.core.screen.Screen 或者 com.zhangke.fread.common.page.BaseScreen 的类改成 androix.navigaton3 的一个 NavKey + 对应的 Composable 函数。
比如现在有这样的一个 Screen:
class ProfileScreen : BaseScreen() {
@Composable
override fun Content() {
super.Content()
val viewModel = getViewModel<ProfileHomeViewModel>()
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
) {
Text(text = "Profile Screen")
}
}
}
那么你需要改成如下方式,并且新增一个 NavKey:
object ProfileScreenKey: NavKey
@Composable
fun ProfileScreen(viewModel: ProfileHomeViewModel){
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
) {
Text(text = "Profile Screen")
}
}
但如果这个页面有参数,那么 key 也应该带一个参数:
data class DetailScreenKey(val itemId: String) : NavKey
@Composable
fun DetailScreen(viewModel: DetailViewModel){
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
) {
Text(text = "Detail Screen for item: $itemId")
}
}
然后你需要把这个新增的 NavKey 注册到当前模块的 NavEntryProvider 中,比如:
class ProfileNavEntryProvider : NavEntryProvider {
override fun EntryProviderScope<NavKey>.build() {
entry<ProfileScreenKey> {
ProfileScreen(koinViewModel())
}
entry<CreatePlanScreenNavKey> { key ->
// with parameters
CreatePlanScreen(koinViewModel { parametersOf(key.lexicon) })
}
}
override fun PolymorphicModuleBuilder<NavKey>.polymorph() {
subclass(ProfileScreenKey::class)
subclass(CreatePlanScreenNavKey::class)
}
}
## 工作流程
你需要 Follow 以下工作流程:
1. 首先找到给定模块中所有符合如下条件的 Screen:
a. 继承自 cafe.adriel.voyager.core.screen.Screen 或者 com.zhangke.fread.common.page.BaseScreen
b. 不包含任何嵌套 **Navigator**
2. 将这些符合条件的 Screen 列出并输出到控制台
3. 逐个重构这些 Screen
4. 对于每个 Screen,首先创建该 Screen 的 NavKey,比如给 ProfileScreen 创建一个 ProfileScreenNavKey.
5. 将 ProfileScreen 改为 @Composable 函数。
6. 对于使用了 navigationResult 的地方请保持不动,不要试图修改相关的代码,即使有编译报错也不用管,保留原样。
7. 将 ProfileScreenNavKey 以及这个 @Composable 函数 注册到该模块的 NavEntryProvider 中。
8. 找到这个 Screen 的相关引用,并将跳转处改为这个 Screen 的 NavKey
9. 结束这个 Screen 重构并进入下一个 Screen。
10. 直到所有重构完所有满足条件的 Screen。
## 绝对禁止
一下内容为绝对禁止修改的规则:
1. 对于已经修改完成的类请不要再改
2. 你只应该修改 Screen 和 navigation3 相关的代码,其他的代码不要改,即使你觉得有问题也不要改
3. 不要做任何超出我要求的事情
4. 遇到不属于上述情况的页面请直接忽略,不要自己想办法解决
5. 不要求改任何嵌套的 Navigator 页面,遇到嵌套的情况直接跳过
6. 不要修改任何已经使用 navigation3 的页面
7. 不要通过代码引用的方式找某个页面的引用并且试图修改其引用点
8. 不要修改任何超出要求的代码
在这个 SKILL 中,我明确规定了工作流程,并列出了一些“绝对禁止”的行为。这本质上就是在规避各种“特殊情况”,也就是前面提到的“特殊情况特殊处理”原则。通过严格限定 Codex 的行为边界,可以极大降低任务的复杂度和不可预测性。
当我们定义了具体、清晰的工作流程,并要求 Codex 必须遵守时,它出错的概率就会小很多。这种在 开源实战 中常见的“自动化脚本”思维,在指挥 AI 协作时同样有效。
单元测试验证(可选策略)
另一个保证重构质量的策略是:先让 AI 针对重构任务编写足够的单元测试,并确保重构前的代码能通过这些测试。然后进行大规模重构,完成后再次运行单元测试,以验证功能稳定性。不过,由于 Fread 本次重构涉及大量 UI 代码,编写单元测试较为繁琐,因此没有采用此策略。
提交与 Review 策略
根据上述步骤,每完成一个小的、独立的子任务后,都可以立即创建一个代码提交。然后,我们可以开启一个全新的对话线程(Thread),让 Codex 来 Review 这个提交。使用新线程是为了丢弃之前所有的上下文,让 AI 从一个“全新的视角”来审查代码,避免它为自己之前可能犯下的错误“自圆其说”。
通过这样层层递进、人机协同的方式,即便是上万行代码的重构,也能变得井然有序、风险可控。这次经历让我深刻体会到,在 前端 & 移动 乃至更广泛的 Android/iOS 开发领域,将 AI 定位为“高效的执行者”而非“全能的决策者”,往往能收获最佳的效果。如果你也有类似的复杂重构需求,不妨试试这些方法。也欢迎在 云栈社区 分享你的 AI 辅助开发经验与心得。