codex + Stitch 实战复盘:用 AI 做一个能上架的移动 App
最近我用 AI 把一个能装到手机上、能真机跑、能打签名包的移动 App 从零做出来了——不是那种 hello world 玩具,是带登录、列表、反馈、统计、历史、离线补传一整套的正式应用。全程代码由 codex 写、界面稿由 Stitch 出、接口来自我提前写好的后端规格。

一、三个人怎么分工:codex、Stitch、接口文档
这套组合里,三个角色各管一段,谁也别越界:
#CNCvBKIRSERX text { white-space: pre; }#CNCvBKIRSERX .fb0 { stroke: var(--N900-FG, 0.15); }#CNCvBKIRSERX .fb0 {stroke: rgba(31,35,41, 0.15)}#CNCvBKIRSERX .fx0 { stroke-miterlimit: 10; }#CNCvBKIRSERX .fx0 {stroke-miterlimit: 10}#CNCvBKIRSERX .fa1 { fill: rgb(187, 191, 196); }#CNCvBKIRSERX .fa1 {fill: rgb(187, 191, 196)}#CNCvBKIRSERX .fx1 { font: 400 12px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#CNCvBKIRSERX .fx1 {font-style: normal}#CNCvBKIRSERX .fx2 { text-anchor: middle; }#CNCvBKIRSERX .fx2 {text-anchor: middle}#CNCvBKIRSERX .fx3 { dominant-baseline: central; }#CNCvBKIRSERX .fx3 {dominant-baseline: central}#CNCvBKIRSERX .fx4 { stroke: none; }#CNCvBKIRSERX .fx4 {stroke: none}#CNCvBKIRSERX .fc0 { fill: var(--text-caption); }#CNCvBKIRSERX .fc0 {fill: #646a73}#CNCvBKIRSERX .fa2 { fill: var(--text-link-disabled); }#CNCvBKIRSERX .fa2 {fill: rgb(187, 191, 196)}#CNCvBKIRSERX .fa3 { fill: rgb(239, 243, 245); }#CNCvBKIRSERX .fa3 {fill: rgb(239, 243, 245)}#CNCvBKIRSERX .fx5 { font: 700 14px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#CNCvBKIRSERX .fx5 {font-style: normal}#CNCvBKIRSERX .fx6 { text-anchor: start; }#CNCvBKIRSERX .fx6 {text-anchor: start}#CNCvBKIRSERX .fx7 { dominant-baseline: alphabetic; }#CNCvBKIRSERX .fx7 {dominant-baseline: alphabetic}#CNCvBKIRSERX .fc1 { fill: rgb(79, 79, 79); }#CNCvBKIRSERX .fc1 {fill: rgb(79, 79, 79)}#CNCvBKIRSERX .fx8 { font: 400 14px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#CNCvBKIRSERX .fx8 {font-style: normal}#CNCvBKIRSERX .fc2 { fill: rgb(0, 0, 0); }#CNCvBKIRSERX .fc2 {fill: rgb(0, 0, 0)}#CNCvBKIRSERX .fc3 { fill: rgb(230, 192, 123); }#CNCvBKIRSERX .fc3 {fill: rgb(230, 192, 123)}#CNCvBKIRSERX .fb1 { fill: var(--N900-FG, 0.15); }#CNCvBKIRSERX .fb1 {fill: rgba(31,35,41, 0.15)}#CNCvBKIRSERX .fb2 { fill: rgb(221, 221, 221); }#CNCvBKIRSERX .fb2 {fill: rgb(221, 221, 221)}#CNCvBKIRSERX .fa4 { fill: var(--bg-body); }#CNCvBKIRSERX .fa4 {fill: #fff}ABC1234角色codexStitch后端接口文档负责什么产出写代码、跑命令、改 bug、提交 git出高保真 UI 设计稿(PNG/HTML)规定所有字段、枚举、错误码(被声明为"唯一真实来源")React + Vite + TypeScript + Capacitor 工程登录页、列表页、反馈弹窗、统计页的视觉稿一份 ,前端不臆造任何接口api.md
关键点在于:UI 和接口都不是 codex 自己拍脑袋生成的。Stitch 负责"长什么样",接口文档负责"数据从哪来、字段叫什么",codex 只做"把这两者翻译成可运行的代码"。这一条是我后面所有约束的地基。

二、最值钱的不是 prompt,是约束清单
很多人用 AI 写项目翻车,不是因为不会写 prompt,而是没给 AI 划红线。我这个仓库的 README 里直接写死了一组"禁止事项"和"开发原则",相当于给 codex 立了规矩。几个真正救命的:
•
不允许臆造接口:所有字段必须来自接口文档,缺字段就问,不能自己编;
•
不允许保存明文密码:密码必须先哈希再出站;
•
不允许绕过业务结果回填:每一项操作必须落库、可追踪;
•
不允许跳过状态更新:数据状态流转一步不能漏;
•
不允许一次性生成完整 App:必须分阶段开发、逐阶段验收。
👍
这五条不是装饰。AI 写代码最大的毛病就是"看起来跑通了但其实抄了近路"——比如为了省事把密码明文存了、把没定义的接口字段自己造一个、为了快速出活把整个 App 一次性糊出来结果状态满天飞。把禁止项写进项目根文档,比在聊天里反复叮嘱十条都管用,因为 codex 每次开工都会先读它。
#mmWeHzgTSkbc text { white-space: pre; }#mmWeHzgTSkbc .fb0 { stroke: var(--N900-FG, 0.15); }#mmWeHzgTSkbc .fb0 {stroke: rgba(31,35,41, 0.15)}#mmWeHzgTSkbc .fx0 { stroke-miterlimit: 10; }#mmWeHzgTSkbc .fx0 {stroke-miterlimit: 10}#mmWeHzgTSkbc .fa1 { fill: rgb(187, 191, 196); }#mmWeHzgTSkbc .fa1 {fill: rgb(187, 191, 196)}#mmWeHzgTSkbc .fx1 { font: 400 12px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#mmWeHzgTSkbc .fx1 {font-style: normal}#mmWeHzgTSkbc .fx2 { text-anchor: middle; }#mmWeHzgTSkbc .fx2 {text-anchor: middle}#mmWeHzgTSkbc .fx3 { dominant-baseline: central; }#mmWeHzgTSkbc .fx3 {dominant-baseline: central}#mmWeHzgTSkbc .fx4 { stroke: none; }#mmWeHzgTSkbc .fx4 {stroke: none}#mmWeHzgTSkbc .fc0 { fill: var(--text-caption); }#mmWeHzgTSkbc .fc0 {fill: #646a73}#mmWeHzgTSkbc .fa2 { fill: var(--text-link-disabled); }#mmWeHzgTSkbc .fa2 {fill: rgb(187, 191, 196)}#mmWeHzgTSkbc .fa3 { fill: rgb(239, 243, 245); }#mmWeHzgTSkbc .fa3 {fill: rgb(239, 243, 245)}#mmWeHzgTSkbc .fx5 { font: 700 14px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#mmWeHzgTSkbc .fx5 {font-style: normal}#mmWeHzgTSkbc .fx6 { text-anchor: start; }#mmWeHzgTSkbc .fx6 {text-anchor: start}#mmWeHzgTSkbc .fx7 { dominant-baseline: alphabetic; }#mmWeHzgTSkbc .fx7 {dominant-baseline: alphabetic}#mmWeHzgTSkbc .fc1 { fill: rgb(79, 79, 79); }#mmWeHzgTSkbc .fc1 {fill: rgb(79, 79, 79)}#mmWeHzgTSkbc .fx8 { font: 400 14px MonospacedNumber, LarkHackSafariFont, LarkEmojiFont, LarkChineseQuote, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Tahoma, "PingFang SC", "Microsoft Yahei", Arial, "Hiragino Sans GB", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }#mmWeHzgTSkbc .fx8 {font-style: normal}#mmWeHzgTSkbc .fc2 { fill: rgb(0, 0, 255); }#mmWeHzgTSkbc .fc2 {fill: rgb(0, 0, 255)}#mmWeHzgTSkbc .fb1 { fill: var(--N900-FG, 0.15); }#mmWeHzgTSkbc .fb1 {fill: rgba(31,35,41, 0.15)}#mmWeHzgTSkbc .fb2 { fill: rgb(221, 221, 221); }#mmWeHzgTSkbc .fb2 {fill: rgb(221, 221, 221)}#mmWeHzgTSkbc .fa4 { fill: var(--bg-body); }#mmWeHzgTSkbc .fa4 {fill: #fff}ABC12345约束手段解决什么坑生效方式接口文档声明为"唯一真实来源"README 禁止项清单分阶段开发原则codex 命令白名单AI 臆造端点/字段明文密码、漏状态、抄近路一次性生成导致结构失控防止 AI 乱跑危险命令根文档 + 提示词双重强调项目根文件,每次必读拆 commit、逐阶段验收 里放行 / 等rulespnpmcurl
三、分阶段开发:git 历史证明"一次性生成"必翻车

我不信"一句 prompt 出一个完整 App"。这个项目真实的提交节奏是这样的(泛化描述):
先初始化 React + TypeScript + Capacitor 空壳;
再实现核心业务流程(登录 → 列表 → 反馈 → 统计);
然后把 Stitch 出的视觉稿逐页替换进去、调样式;
接着补移动端专属能力——把"读取系统通话记录"重构为独立的 Capacitor 原生插件;
再加离线优先与离线补传;
最后才是安卓签名、真机调试、发版。
每一阶段都是独立 commit、独立验收。一次性让 AI 吐出全部代码,结果一定是:状态管理混乱、边界漏处理、移动端原生能力缺席。分阶段的核心价值不是"慢",是让每个阶段都有清晰的验收标准,AI 跑偏了能立刻发现、立刻回滚,而不是等到十几个文件全乱了再抢救。
四、三个真坑,全是 AI 协作的专属坑
坑 1:AI 最爱臆造接口。 你让它"做个提交",它顺手就编了个文档里根本不存在的字段。解法是把接口文档钉死成唯一真实来源,并在提示词里反复强调"缺字段就问、不许造"。配合后端返回的真实错误码(比如参数非法直接 400),让 AI 的每一次"自由发挥"都会在联调时撞墙。
坑 2:Web 标准 ≠ 移动端能力。 这是这个项目和纯前端工程最大的分水岭。codex 默认只会 Web 那套——fetch、、浏览器 API。但 App 要调系统拨号盘、读系统通话记录,这些 Web 根本没有。我的做法是:业务逻辑仍交给 codex,但系统能力亲手补一个 Capacitor 原生插件,再用 registerPlugin 把它桥接成前端能 import 的模块。AI 负责 90% 的普通代码,剩下 10% 真正吃移动端底层经验的活,必须人自己上。
// 自己写的原生桥接层,AI 不碰这块系统能力import { registerPlugin } from '@capacitor/core';
export const CallLog = registerPlugin<CallLogPlugin>('CallLog');
export async function getLatestCallForNumber(phone: string) {const result = await CallLog.getLatestForNumber({ phone });return result.entry; // 系统通话记录,Web 拿不到
}
坑 3:移动端发布是另一门手艺。 代码写完只是开始。安卓要自己生成并保管 keystore 签名密钥(丢了就再也没法更新已上架的包)、要开 ProGuard 混淆、要管 versionCode 版本号、要真机跑 release 包验证。这些 codex 能帮你写配置,但密钥保管和发版决策必须人拍板——我专门写了发布指南文档,把命令、坑、检查清单都固化下来,避免每次发版现查。
五、结论:能,但前提是"人定架构 + 约束 + 分阶段验收"
回到开头的问题:一个人用 AI 做完一个能上架的移动 App,靠谱吗? 我的答案是靠谱,但前提是别把 AI 当甩手掌柜。
真正决定成败的,是开工前那几件事:把接口文档写清楚当唯一事实源、把禁止项写进根文档立规矩、把"分阶段验收"当铁律、把移动端系统能力这种硬骨头留给自己。codex + Stitch 这套组合,把"写代码"和"出设计"的成本压到了极低,但它省不掉的是人的架构判断和验收责任。
如果你也正打算用 AI 从零撸一个 App,记住一句话:AI 负责把你想清楚的东西做出来,而"想清楚"这件事,永远得你自己来。