背景
Codelix 最初是为后台服务设计的 AI 编码平台:给一个 TAPD 需求单,AI 自动完成需求分析、方案设计、代码生成、编译校验、Code Review 全流水线。后台场景相对规整,踩坑踩了几个月后,我们开始把这套流水线推广到 iOS、Android、Kuikly 三个客户端平台。
这篇文章记录了整个过程的技术细节,包括 iOS pipeline 从零到可用,Kuikly 接入时踩的低级 bug,Android 的广撒网问题,以及跨三端做统一优化的五轮演进。
一、iOS 流水线的设计:7 个 Agent 的协作链路
iOS 编码流水线在一个已有 AI 工具(iOSBugAutoFix)上迁移过来,核心思路是:复用 Codelix 的编排基建,只迁移 Agent 提示词。不重新实现 pipeline 引擎,不重新写 ACP 协议,只把 iOSBugAutoFix 的 agent 逻辑搬进 Codelix 的框架里。
最终链路是 7 个 Agent:
| Agent | 阶段 | 职责 |
|---|---|---|
ios-requirement-analyst |
planning | 需求分析 + 代码搜索 + 影响范围定位 |
ios-design-interpret |
planning | 解释设计稿 / D2C 信息 |
ios-feature-impact |
planning | 评估影响范围和风险 |
ios-task-planner |
planning → coding | 方案设计 + 任务拆分 + 调度 coder |
ios-coder |
coding | 编码实现 |
ios-validate |
test | 编译验证 + 语义评估 |
ios-code-reviewer |
git | 代码审查 |
阶段之间靠结构化 artifact 传递上下文,而不是把所有历史对话塞给下一个 agent。ios-task-planner 完成后调用 add_coder_dispatch MCP 工具,前端监听后自动拉起 ios-coder。
二、iOS coder 的性能灾难
第一个版本跑起来后,效果非常糟糕。以「歌手主页视觉优化」需求为例:
- 需求分析耗时:**~25 分钟**
- 代码生成耗时:**~1 小时**
- 代码生成费用:**~$30**
- 实际完成进度:5 个 TODO 中只完成 1 个
对比来源系统 iOSBugAutoFix:20 分钟 / $10 / 全部完成。
根因一:路径搜索空转
优化前 ios-coder 的实际执行顺序:
get_artifact(tech_design)读取技术方案(4000+ token)- 从方案中推断文件路径(不可靠)
Bash(grep)/Bash(find)搜索确认路径Read文件内容- 最终才
Edit
两次相邻 Edit 之间间隔 10 分钟(17:31 → 17:41)。Session 重启 4 次,每次都重读技术方案,累计浪费约 16,000 token。
根因二:工具白名单过宽
ios-coder 的 AllowedTools 原来是 Bash(*),模型可以自由使用 find/grep/ls 等所有搜索命令。提示词里写了”Read 已知路径直读,不要用 find/grep 搜索”,但这是软约束,在高噪声场景下模型根本不遵守。
根因三:没有轮次收敛机制
虽然有 MaxTurns=40,但没有剩余轮次催促,没有 Edit 失败预校验,model 只有硬上限,不知道”快用完了”。
解决方案
P0-1:路径硬注入
ios-task-planner 在给 coder 派发任务时,coder_prompt 里必须包含已确认文件路径表格(绝对路径 + 方法名/selector + 行号辅助)以及每个文件的具体变更意图。coder 收到后路径已知,第一步直接 Read 目标文件,无需推断。
P0-2:路径守卫
ios-coder/system-prompt.md 中增加路径守卫:
若 prompt 中包含”已确认文件路径”表格,严禁对表格中的文件使用
Bash(find)/Bash(grep)/Bash(ls)进行路径搜索。直接使用表格中的绝对路径调用 Read。
与 planner 侧形成双侧约束。
P1-1:工具白名单收紧
ios-coder:Bash(*)→Bash(git *),从能力层物理封口搜索类命令ios-requirement-analyst:Bash(grep/find)→ 原生Grep+Glob(无 fork 开销)
P1-2:轮次收敛提示注入
在 ws_channel.go 里,coder 启动时注入催促:剩 5 轮停止搜索,剩 3 轮强制收敛。
实测效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 两次 Edit 间隔 | ~10 分钟 | 首轮直接 Edit |
| Session 重建 token 浪费 | ~16,000 token | 路径已注入,无需重推断 |
| 需求分析耗时 | ~25 分钟 | ~5 分钟 |
三、iOS 编译验证:5 轮假设才找到真正根因
ios-validate agent 在大型 iOS 工程(QQMusic)上反复出现漏报编译错误:本地 Xcode 能看到的错误,agent 报告里显示”目标文件 0 errors”。
整个排查经历了 5 个假设:
| 假设 | 结论 |
|---|---|
tail -80 截断了关键错误 |
部分成立,但改掉后仍漏报 |
changePlan 漏列改动文件 |
部分成立,但改掉后仍漏报 |
git diff HEAD~1 HEAD 看不到未提交改动 |
部分成立,三路 git 后目标文件正确了,但仍漏报 |
| DerivedData 增量缓存跳过改动文件 | 部分成立,touch 目标文件后仍漏报 |
| scheme manual target order 串行阻断 | 加 -parallelizeTargets 后仍有 database is locked |
最终真正的根因(第 6 轮):
用户的 Xcode GUI 在运行,DerivedData 的 build.db 被 GUI 占用。
具体机制:
- 用户配置了
IDECustomDerivedDataLocation = DerivedData(工程内相对路径) - Xcode GUI 编译时占用
DerivedData/build.db - 命令行 xcodebuild 启动后,43 秒内就因
database is locked退出 - 这 43 秒里依赖图都没建完,主 target 一行代码都没编译
- build log 里只有 xcframework 警告,没有任何 CompileC 记录
- agent 之前报告的”12 个 QMMoveView.swift 错误”是残留的旧 build log 内容
实测对比:
| 场景 | 结果 |
|---|---|
| Xcode GUI 在跑 + 命令行 xcodebuild | 43 秒后 database is locked,0 个 CompileC,主 target 未编译 |
| Xcode GUI 关闭 + 命令行 xcodebuild | 11 分钟完整编译,9000+ CompileC,捕获到真实错误 ✅ |
核心教训
当 agent 报告的错误”很奇怪”(总是同一组无关错误)时,第一步应该是直接看 build log 里有什么(grep -c CompileC、grep "error:"),而不是改 prompt。
最终可靠的编译命令关键参数:
- 不带
-quiet(会吞掉关键错误信息) - 加
-parallelizeTargets(防止 manual order 串行阻断) -destination 'generic/platform=iOS'(真机架构,避免模拟器差异漏报)CODE_SIGNING_ALLOWED=NO(跳过签名)- 检测到
database is locked立即报错,而不是继续给出误导性结论
四、Fix Mode 的设计:编译、语义、CR 三类修复
编译验证之后,需要把”发现编译错误 → 修复 → 再验证”这条链路做顺。原来用户需要:看 validate 报告 → 复制错误信息 → 切回 coder tab → 自己拼修复 prompt → 修完再回 validate 重跑。5 个手动步骤。
Fix Mode 独立 Agent
ios-coder-fix 从 ios-coder 完全拆分出来,独立 system-prompt + 独立工具白名单:
1 | AllowedTools: []string{ |
物理移除 Bash,模型无法调用 find/ls/git log/git show。
优化前的实测数据(22:30 那一轮)
| 时刻 | 工具调用 | 累计耗时 |
|---|---|---|
| 22:30:09 | prompt 启动 | 0s |
| 22:31:01 | find 探文件 |
52s |
| 22:32:44 | 又一次 find 同样文件 |
+103s |
| 22:33:03 | ls -la /Users/xxx/worktrees/ |
+19s |
| 22:33:25 | get_workspace_context |
+22s |
| 22:34:08 | git checkout -- 撤销改动 |
+43s |
| 22:38:22 | 4 次连续 Edit 同一文件 | +234s |
| 22:39:02 | end_turn | 8m53s |
三个根因:①白名单仍有 Bash(git *),模型把 find/ls 当宽松 git 子命令提交,被 auto-approve;②prompt 体积 15KB,模型忽略注入的文件内容,主动跑 git show 读历史;③同一文件多 error 连发 4 次 Edit。
优化后效果
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 总耗时 | 8m53s | 50.1s | ↓ 90.6% |
| 首次工具调用 | find 探文件 |
Edit 改代码 |
✅ |
| Bash 调用次数 | 11 次 | 0 次 | 物理切断 |
| 同文件 Edit 次数 | 4 次 | 1 次 | 合并 |
三类 Fix 统一设计(Unified Fix Mode)
后来把 Fix Mode 扩展成三类,统一放在验证 tab 底部:
| fixType | 触发条件 | 问题来源 | 用户控制 |
|---|---|---|---|
compile |
有 targetFileErrors |
test_report |
自动注入,无需输入 |
semantic |
编译通过但有 medium/high semanticRisks |
test_report |
自动注入,无需输入 |
cr |
CR 完成,有 criticalIssues |
commit_reviews.critical_issues |
必须经用户输入框确认 |
CR fix 必须经用户确认的原因:编译错误和语义缺失影响功能,必须解决。CR 问题不一定——可能是误报,可能推后处理。
CR Fix 引入了新编译错误
Unified Fix Mode 上线后,第一次真实使用 CR 快修就踩坑了:
agent 把 insertOrReplaceObjects 改为了 WCDB 属性级 UPDATE,但用错了调用对象:
- ❌ Agent 的修复:
[self.database updateTable:onProperties:withObject:where:](不存在于此版本 WCTDatabase) - ✅ 正确写法:先
[self.database getTableOfName:withClass:]拿到WCTTable *t,再[t updateRowsOnProperties:withObject:where:]
根本原因:ios-coder-fix 的 AllowedTools 没有 Grep,在 CR/semantic fix 模式下,agent 无法查询头文件验证 API 是否存在。
修复:AllowedTools 加 Grep + Glob,同时 prompt 加规则”使用第三方框架 API 之前,先 Grep 对应头文件确认 selector 存在”。
五、Kuikly 接入:一个低级 bug 导致所有 Agent 全部被拦截
Kuikly 接入后,出现了一个诡异的现象:
kuikly-task-planner多次启动,每次很快结束,没有产出 tech_design artifact,没有拆 TODO,也没有触发 coder- 手动点「进入代码实现」后页面无响应
查看 agent 日志,发现大量:
1 | BLOCKED tool="mcp__codelix__mcp__codelix__get_workspace_context" kind="other" agentRole=kuikly-task-planner (not in AllowedTools) |
双重前缀。
根因:codelix-mcp 的 handleToolsList 在 manifest 里把工具名注册为 mcp__codelix__<tool>(已带前缀)。Regular claude 和 tme-claude 收到这个 manifest 后,按 MCP 协议惯例再加一次 mcp__codelix__ 前缀,最终工具名变成 mcp__codelix__mcp__codelix__<tool>。
ACP 服务端的 IsToolAllowedByACPPermission 拿双重前缀名去匹配 AllowedTools(单前缀),完全匹配不上,所有 codelix 工具被全部拦截。
影响范围:所有平台(backend / kuikly / iOS / Android)所有使用 codelix MCP 工具的 agent。
修复:
acp/manager.go—IsToolAllowedByACPPermission:进入白名单检查前先归一化,检测mcp__X__mcp__X__foo模式并折叠为mcp__X__foomcp-server/main.go—handleToolsCall:循环剥前缀,兼容双重前缀调用
另外还有一个配套 bug:startCodingFromPlanning 切换到 coding tab 时,没有清除 activeRun(仍指向 task-planner 的旧 run)。coder 启动后,WS 事件被旧的 planning 视图抢走,coding tab 收不到任何输出,UI 上表现为”进不去”。修复:切 tab 之前先执行 setActiveRun(null); setActiveRunTab(null); clearSingleAgentMemory()。
六、Android 需求分析:21 分钟 4 次 Compacting
Android 的「韶音二期体验优化」需求(12 条子需求):
- 需求分析耗时:21 分钟(13:38:28 → 13:59:34)
- 累计约 250 次工具调用
时间线:
| 时间 | 事件 |
|---|---|
| 13:38:28 | 开始 |
| 13:39:44 | 第 1 次 Compacting(开始后仅 1.5 分钟) |
| 13:43:19 | 第 2 次 Compacting |
| 13:50:00 | 第 3 次 Compacting |
| 13:54:45 | 第 4 次 Compacting |
| 13:59:34 | 输出完成 |
4 次 Compacting 合计消耗 10–14 分钟,占总耗时 50–67%。第 1 次 Compacting 在开始后 仅 1.5 分钟就触发,说明文件读入速度极快地撑满了上下文。
最严重的一个批次:单轮读取 18 个文件 + 12 次搜索。
方案设计阶段更夸张:22 分钟,两轮探索合计读取 129 个文件、107 次搜索、614 次 Tool 调用——task-planner 把”方案核验”做成了全仓重新分析。
七、五轮跨三端统一优化
以上这些问题的解法,最终在 platform/05-multi-platform-analysis-design-optimization.md 做了三端统一规范,经历了五轮演进:
第一轮:收敛广撒网
核心原则:把”全面覆盖”和”全面阅读”解耦。覆盖率靠”逐条需求都处理过”保证,阅读量靠”证据分级”收敛。
硬约束:先 Grep 后 Read,单轮 Read ≤ 5 个文件,大文件只精确匹配符号,同一条需求连续 3 轮无命中则停止扩散。
收益:iOS 需求分析从 ~20 分钟降到 ~5 分钟。
副作用:约束过强时,部分需求定位不准或漏掉边缘影响面。
第二轮:为了补覆盖率,耗时再次劣化
有人为了提高定位准确性,重新强化了”每个候选文件都要读内容验证”、”发现路径立即读取”、”无法判断时扩大搜索范围”。iOS 需求分析从 ~5 分钟退回到 ~20 分钟。
教训:提示词优化有回归风险。没有工程层面的约束,只靠 prompt 软规则,容易被后来的”优化”盖过。
第三轮:三端统一 + 方案设计约束
把第一轮 iOS 经验统一推广到 Android / Kuikly,同时限制方案设计阶段:
- 最多 2 轮补充搜索;单轮 Read ≤ 3 个文件;整个方案设计阶段累计 Read ≤ 8 个文件
- 默认信任并消费上游 artifact,不重新做全仓目标文件定位
时间收住了,但规则没有区分”普通不确定点”和”主链路断点”。Kuikly 的 ASR 搜索需求:commonMain → native bridge → plugin/service → callback → UI 展示 这条链有没有接通才是关键,搜不到就写 risk 的规则导致主链路断点被当成普通风险,coder 继续局部实现,代码看似覆盖不少文件,但关键能力没有真正连上。
第四轮:Contract Gate
主链路必须 blocker 化,而不是 risk 化。
规则:涉及跨层能力时(bridge/native/plugin/callback/route/jump/状态链路),必须输出 contracts[],记录 entry / transport / consumer / result / ui 五个关键节点。任一节点缺失或未知 → 进入 blockingGaps[] + 阻塞 TODO,不得降级为普通 risk。
各端触发词(命中任一词即启动 Contract Gate 检查):
- Android:Activity / Fragment / ViewModel / Repository / UseCase / Flow / StateFlow / module interface / broadcast / intent / callback / SDK / native
- iOS:delegate / notification / block / protocol / Manager / Service / native / SDK / callback / jump / router
- Kuikly:bridge / native / plugin / callback / route / page param / jump / locate / ASR / WVS / FC
副作用:缺口落在跨仓时(宿主 App / 独立仓 / 预编译库,当前工作区无源码),被判 blockingGaps 会连带阻塞本仓本可编码的部分;Contract Gate 也只管到”规划”,编码阶段仍有大撒网。
第五轮:把闭环延伸到编码 + 校验
问题一:编码阶段大撒网
实测某次 Kuikly 代码实现,27 分钟里前 ~14 分钟,coder 并行读了 60+ 文件、Grep 64 次。coder 提示词写了”禁止搜索”,但不起作用。
根因:ACP 模式下工具是通过运行时 request_permission + AutoApprovePermission 放行的,AllowedTools 白名单只在 CLI --print 生效,不是真正的物理门禁。
修复:HardBlockByAllowedTools(acp/manager.go)对 claude 形态 agent 拦下白名单外的工具。但原来的匹配器只认 Bash(git:*) 冒号写法,不认三端 coder 用的 Bash(git *) 空格写法,导致硬门会误杀 coder 的 git 命令。修掉这个 bug 后,硬门才真正对三端 coder 可用。
问题二:跨仓缺口过度阻塞
宿主 App / 预编译 xcframework / AAR / SDK 等,当前工作区无源码,修不了。改为 human_action TODO(coder 自动跳过、留人工),**不进 blockingGaps[]**。
问题三:校验→修复闭环断开
覆盖缺口只写进 designCoverage.missing[],但”语义修复”按钮只认 semanticRisks[]。编译通过、只差覆盖时,用户没有可点的修复入口,只能盲目”重做”,coder 拿不到缺口信息,必然复现同样结果。
实测 Kuikly ASR 需求重做两次仍缺同样 2 处 [语音] 前缀 Span。
修法:校验 prompt 统一要求,designCoverage.missing[] 中本仓可改的缺口,同时在 semanticRisks[] 镜像一条(fixable=true, severity=medium),让现成的”语义修复”按钮能识别并驱动 coder 修复。仅在本轮编译通过时镜像(有编译错误优先走”编译修复”),逻辑天然隔离。
八、工程保障:为什么不能只靠提示词
最重要的教训:提示词软约束在 ACP 模式下不稳定。
iOS 需求分析速度从 5 分钟回归到 20 分钟,就是因为有人在提示词里加了覆盖率相关约束,把广撒网逻辑写了回去。解决方案:
- 预算片段放 prompt 第一屏:约束必须在 system prompt 顶部,模型读到约束前就已决定”先探索”
- 运行时催促:
ws_channel.go在 requirement-analyst(MaxTurns 20)、task-planner(MaxTurns 15)剩余轮次不足时,注入”剩 5 轮停止搜索 / 剩 3 轮强制收敛” - AllowedTools 物理切断:必须禁止的行为先从工具白名单切断,再用 prompt 双侧重复约束。Fix Mode 无 Bash 白名单是物理约束,比 prompt 写”禁止 find”强得多
前两条治分析/规划阶段,第三条治编码/修复阶段。改 prompt 治”标”,工程约束治”本”。
九、首轮试点数据
首次在 Kuikly / iOS / Android 三端完整跑通 Codelix 流水线,4 个真实需求:
| 需求 | 端 | 估算工作量 | 总耗时 | 输入 Token | 总费用 | 完成度 |
|---|---|---|---|---|---|---|
| Kuikly A | Kuikly | 1D | 69 min | 21,451,917 | $11.69 | 95% |
| Kuikly B | Kuikly | 7D | 97 min | 16,532,406 | $15.77 | 92% |
| iOS | iOS | 2D | ~85 min | 16,801,822 | $11.00 | 100% |
| Android | Android | 2.5D | ~99 min | 23,118,214 | $17.10 | 89% |
数据为首轮试点、样本量小,仅作量纲参考,不代表稳定均值。
Android 阶段明细(最复杂的一单)
| 阶段 | 耗时 | 费用 |
|---|---|---|
| 需求分析 | 21 min | $3.47 |
| 方案设计 | 22 min | $4.54 |
| 代码实现 | 17 min | $3.40 |
| 校验 + CR + 多轮修复 | ~38 min | $4.69 |
这里有个典型问题:CR 快修顺手新增了 2 个 override,但没先验证编译,导致引入了 4 个编译错误,额外多出两轮”编译错误→快修”循环。后来通过在 coder prompt 中明确禁止”未经验证的顺手新增改动”来规避。
关键观察
- 代码生成是 Token / 费用大头:四个样本中代码生成阶段占总费用 30%–65%,是最大优化空间
- 需求单质量直接影响分析成本:Kuikly A 需求单含大量代码名/模糊路径,分析阶段确认点偏多;iOS/Android 需求分析都在 20 min 量级,输入 token 很高
- 多轮校验/快修常态化:几个样本都经历 2–4 轮”校验→快修”,单轮成本不高但累加可观
十、几个核心设计决策
Fix Mode 必须和首次实现分开:修复轮次是机械修补,不是重新分析。两者混在同一个 agent 里会浪费 token、混淆模型决策,且无法针对 fix 场景单独收紧工具白名单。
上下文传递要主动,不要靠 Agent 重新推断:ios-task-planner → ios-coder 的调度路径上,路径信息从技术方案(自然语言)变成硬注入(结构化表格),消除了”推断 → 搜索确认”这一环。这个原则后来推广到三端。
不引入 orchestrator:iOS validate/fix 闭环不使用后端 orchestrator / 状态机。当前 codelix 是”前端 tab + artifact 串联”模式,引入 orchestrator 会出现两套并行编排模型,维护成本不成比例。编译修复闭环完全可以靠结构化 artifact + 前端按钮 + prompt 模板实现。
编译结果提取交给确定性程序:LLM agent 自由执行 shell + 解析文本本质不可靠。长期方案是新增 mcp__codelix__ios_build_verify MCP tool,由 Go 后端确定性实现——LLM 只做语义判断,不做 xcodebuild 输出解析。
小结
客户端三端流水线从无到有,踩了大量坑:
- iOS coder 路径搜索空转导致 1 小时 / $30 只完成 1/5 的任务
- iOS validate 漏报编译错误,排查了 5 个假设才找到真正根因(Xcode GUI 占用 DerivedData)
- Fix Mode 优化把单轮耗时从 8 分 53 秒降到 50 秒
- Kuikly 接入时 MCP 双重前缀 bug 导致所有工具全部被拦截
- Android 需求分析触发 4 次 Compacting,耗时 21 分钟
- 五轮跨三端统一优化,从广撒网约束到 Contract Gate,再到编码阶段物理硬门
目前的方案在 4 个真实需求样本上跑通了端到端闭环,成本在 $11–$17 / 需求、耗时在 70–100 分钟 / 需求的量级。对于一个 1–7D 的真实客户端需求,这个量级在可接受范围内。
后续主要优化方向:首轮 coder 的 token 消耗(目前占 30%–65%)、需求分析输入 token(iOS/Android 单次 600–700 万输入)、以及把编译验证从”LLM 解析 build log”升级为 MCP tool 确定性实现。