
几个版本连着出了事。
先是首页新增了一个挂件,需要支持多语言。需求本身不大,流程也和之前一样:AI 分析需求、生成方案文档、我确认后执行、功能验证通过、AI 自检最小改动和影响范围、提交上线。结果上线后收到反馈:首页多语言切换失效了。排查发现,AI 用 Category 扩展了一个基础类来做便利方法,而这个 Category 的行为影响了首页已有的语言刷新机制。
没过多久又出事。一个网络请求模块,我让 AI 修了一个"同时请求会 stop 第二个"的逻辑问题,改完后那个场景好了,但另一个完全不相干的页面里,回调不再执行了——因为那个页面依赖的是"即使被 cancel 也会收到 completion"的旧行为,而新逻辑悄悄改了这条隐性契约。
两件事放到一起看,我开始回过味来:问题不是 AI 写错了一行代码,而是整个流程在设计阶段就没让 AI 看到这些风险。
我之前的 AI 工作流是这样的:
需求 → AI 分析文档 → 我确认方案 → AI 执行 → 功能验证 → AI 检查最小改动和影响范围 → 提交看起来每一步都有,比"直接让 AI 写代码"已经成熟不少。但两件事暴露了同一个结构性问题:"影响范围检查"发生得太晚。代码已经改完了,你再让 AI 看 diff 做解释,它只会基于当前改动做正向推理,不会站在全项目调用关系、运行时行为、历史 bug、边界场景的角度做反证。
而 Objective-C 老项目恰恰是最容易被这种流程漏掉问题的场景。OC 的很多影响不体现在显式调用上——Category 覆盖方法、runtime swizzling、KVO 回调、block 持有关系、单例状态污染、多语言资源的隐式刷新链路、请求 cancel 后的回调契约——这些东西都不容易通过常规代码搜索发现,但一旦改动,影响就是全局的。
我那两个线上 bug 本质上是同一个问题:AI 在"实现者"的位置上做得很好,但缺乏"变更审计员"的视角。它能完成当前需求,但做不到主动发现"这段代码在历史上被哪些地方依赖、哪些副作用没有写在需求文档里、哪些旧逻辑靠隐式行为活着"。
多语言切换在 iOS 里不是靠改一个配置就能辐射全局那么简单。它依赖通知刷新、页面重建、资源重新读取这一整套链路。但如果某个 UI 组件在初始化时把翻译好的字符串缓存下来了,它就不会在语言切换时自动刷新。
AI 接到"新增挂件多语言"这个需求时,它看到的上下文是挂件本身——它不知道首页原有的多语言刷新依赖什么链路,不知道 Category 新增的方法会不会覆盖已有行为,不知道 associated object 存的状态会不会和宿主对象的生命周期不一致。
在 OC 老项目里,Category 的坑是系统性的:新增同名方法会在运行时产生不可预测的覆盖行为、localizedString 或 setText: 这类基础方法的全局修改会影响所有页面、便利方法被多个模块共用后改动面远超预期。
所以根因不是那几行 Category 代码写错了,而是流程里没有"涉及 Category 的需求必须先做影响面审计"这个硬门槛。
这个问题的本质是"请求生命周期契约不清楚"。
stop 到底表示取消当前请求、取消队列里的请求、取消同类请求,还是取消某个 owner 下的全部请求?取消后是否必须回调?回调是 success、failure、cancel,还是 completion?多个请求并发时,如何保证不误伤?
我原来的请求封装里,"同时请求会 stop 第二个"这个行为本身就很粗糙——它用一个共享状态来控制多个请求的生命周期。AI 改这个逻辑时,它看到的是"让第二个不被 stop",但它没看到"某些旧调用方依赖 cancel 也会走 completion 来关闭 loading 和恢复按钮状态"。改完后,第二个请求确实不被 stop 了,但某个旧调用方在 cancel 路径上的状态恢复逻辑断了。
根因同样是流程问题:改请求封装这种公共能力时,没有先定义契约,没有扫描所有调用方,没有确认每个调用方在 success/failure/cancel 三条路径上的行为。
复盘完这两件事,我发现一个更深层的问题:我之前让 AI 检查"是否是最小、最优改动",这个问题本身就问错了。
"最小改动"是用代码行数衡量的——三行改完,看起来很美。但在 OC 老项目里,三行改动一个公共 Category,可能比三十行业务代码的风险大一百倍。改一个 request manager 的 stop 逻辑,代码可能很少,但所有调用方的行为都可能被改变。
我把这个问题换成了:
请从"最小风险改动"的角度审查本次 diff,而不是只看代码行数。
具体来说,AI 必须回答:
这个转变很小,但效果很大。它迫使 AI 不再满足于"我改了三行,看起来没问题",而是要去追溯"这三行改了之后,谁的行为会变"。
我不需要推翻原来的 AI 工作流,但必须在关键节点加硬门槛。新的流程是:
需求 → 风险分类 → 代码地图 → 方案+禁止项 → 测试点 → 实现 → 证据审查每一道门都有明确的输入和输出。
在 AI 写任何代码之前,先让它判断这个需求属于什么风险等级。我定义了几类高风险标签:
只要命中一个,就必须走高风险流程——不允许只做 happy path 验证,必须输出影响面和回归点。
这一步的输出格式是:
AI 漏问题的很大一部分原因是上下文不够。你不能只把当前文件扔给它,要让它主动告诉你还需要看什么。
比如多语言需求,我要求 AI 先搜索哪些关键词、找到哪些文件,然后输出一份"代码地图":
网络请求同理:请求从创建到完成的完整生命周期、stop/cancel 的语义、completion 是否保证一定执行、多个并发请求如何区分、哪些调用方依赖 completion 做 UI 状态恢复——这些必须在写代码之前摸清楚。
以前我只要 AI 给一个方案。现在我要求它必须先列出什么不能做、为什么。
比如 Category 场景,AI 必须明确说明:
另外我也定了一条硬规则:业务需求默认不允许通过 Category 改变已有类的核心行为。Category 只能新增带前缀的工具方法,不能覆盖原方法,不能隐式修改全局行为,不能承担复杂状态管理。需要全局拦截的情况必须单独评审。
方案确认后,AI 必须先输出测试点或手工验证清单,然后再写实现。这样做的目的是把验证思路和实现思路剥离开——AI 在"实现模式"下会倾向于为当前方案找证据,但在"测试设计模式"下会更客观地找边界。
实施代码、跑测试和核心回归。这一步和之前一样,但它前后都被质量门包围。
这是替代原来"检查是否最小改动"的那一步。现在 AI 必须给出证据,不能泛泛而谈:
关键要求是"列出所有调用方"和"给出证据"。AI 如果不能列出调用方,就不能声称影响范围可控。
另一个重要改变是:每个线上遗漏都不只是"记住了下次注意",而是变成了一条固定的检查项。
我已经积累了两套 Checklist,每次让 AI 改代码时直接贴给它:
Objective-C 和 Swift 不一样,很多能力是动态的,AI 在没有约束的情况下很容易低估影响面。我现在把下面这几条作为每次对话的前置约束:
OC 项目测试基础薄弱是常态,但这不意味着什么都不做。我的策略是不追求覆盖率,只追出过事故的路径。
三个最优先的测试方向:
不是为了测每一个文案,而是测"语言切换后会重新取值"这个规则本身:
- (void)testHomeWidgetTitleUpdatesAfterLanguageChanged {
[LanguageManager setLanguage:@"en"];
HomeWidgetView *view = [[HomeWidgetView alloc] init];
[view refreshLocalizedText];
NSString *englishTitle = view.titleLabel.text;
[LanguageManager setLanguage:@"zh-Hans"];
[[NSNotificationCenter defaultCenter] postNotificationName:LanguageDidChangeNotification object:nil];
[view refreshLocalizedText];
NSString *chineseTitle = view.titleLabel.text;
XCTAssertNotEqualObjects(englishTitle, chineseTitle);
}确认 cancel 后会回调且只回调一次:
- (void)testCancelRequestStillCallsCompletionOnce {
XCTestExpectation *expectation = [self expectationWithDescription:@"completion"];
__block NSInteger callbackCount = 0;
RequestToken *token = [client startRequestWithCompletion:^(Response *r, NSError *e, RequestState state) {
callbackCount += 1;
XCTAssertEqual(state, RequestStateCancelled);
[expectation fulfill];
}];
[token cancel];
[self waitForExpectationsWithTimeout:1 handler:nil];
XCTAssertEqual(callbackCount, 1);
}覆盖"取消第一个不能误伤第二个":
- (void)testSecondRequestNotStopped {
XCTestExpectation *expectation = [self expectationWithDescription:@"second"];
RequestToken *first = [client startWithCompletion:^(...) {}];
RequestToken *second = [client startWithCompletion:^(Response *r, NSError *e, RequestState state) {
XCTAssertNotEqual(state, RequestStateCancelled);
[expectation fulfill];
}];
[first cancel];
[self waitForExpectationsWithTimeout:2 handler:nil];
}三组测试加起来不到一百行代码,但它们锁定了所有线上出过事故的路径。每次改涉及多语言或请求的代码时,先跑这三组——能过不一定说明没问题,但过不了就一定有问题。
这套流程跑了几个迭代之后,最直观的变化不是"再没出过 bug"——那不可能——而是每次出问题后,我有了一套标准动作:不是在心里记一笔"下次注意",而是把它拆成一条 checklist 检查项、一组测试用例、或者一条 AI 约束规则。
AI 没有变聪明,但流程变了。以前它是实现者,现在它是风险审计员。以前我在上线前问它"这个改动最小吗",现在我问它"这个改动的风险最小吗"。以前线上出了事我只能复盘加内耗,现在出了事我能把它变成一条规则锁进系统里。
如果你也用 AI 做 iOS 开发,尤其是 OC 老项目,我建议你把下面三件事排在所有优化前面:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。