
1. 为什么我选择用对话式编程从零起一个宠物生命周期管理App宠物生命周期管理这个需求乍一听像是那种做出来没人用的伪需求但我实际调研了一圈养宠人群之后发现痛点比想象中密集得多。养一只猫或狗从接回家那天算起疫苗、驱虫、绝育、体检、体重曲线、换粮记录、就诊病历、用药提醒、临终关怀这一整套信息散落在微信聊天记录、纸质病历本、手机备忘录和各个宠物医院的系统里几乎没有哪个工具能把它们串成一条完整的时间线。我身边养了五年猫的朋友到现在都说不清自家猫上一次打加强针是哪个月。这就是我决定动手做一个宠物生命周期管理App的起点。而这次我刻意没有像以前那样先画UML图、再搭脚手架、再一个个文件手写而是全程用对话式编程的方式让AI编码助手下面统一叫它编码助手从零把项目骨架、数据模型、核心页面、状态管理、本地持久化一路搭起来。整个过程我更像一个技术负责人负责提需求、审代码、纠偏、补边界条件而不是一个逐行敲键盘的执行者。这篇文章要讲的就是这套流程的完整复盘。适合谁看如果你是有一定前端或全栈基础、想尝试用AI把项目从0推到能跑的开发者或者你是产品出身想自己验证一个想法但不想被工程细节卡死再或者你只是好奇对话式编程到底能落地到什么程度那这篇内容应该能给你不少可直接抄的作业。我会把每一步的提示词思路、AI容易翻车的地方、我怎么验收、以及那些文档里不会写的坑全部摊开讲。需要先说明一点下面涉及的所有项目名、目录名、示例数据都是虚构代称你照着做的时候换成自己的即可。技术栈我选的是React Native Expo TypeScript Zustand SQLite理由后面会专门讲。2. 动手之前把宠物生命周期拆成AI能听懂的数据模型2.1 先想清楚生命周期到底包含哪些实体很多人一上来就跟AI说帮我做一个宠物管理App然后AI给你吐出来一堆页面结果数据模型是散的后面越改越乱。我的经验是在让AI写第一行代码之前先把领域模型用自然语言描述清楚这一步的投入回报比极高。宠物生命周期管理核心实体其实就几个Pet宠物名字、品种、性别、生日、绝育状态、芯片号、头像、到家日期。Event事件这是整个App的灵魂。疫苗、驱虫、体检、洗澡、换粮、就诊、用药、体重记录全部抽象成事件用type字段区分。Reminder提醒基于事件周期生成的待办比如每21天体外驱虫。MedicalRecord病历就诊的详细记录关联到某次 Event。WeightLog体重单独拎出来是因为要做曲线图查询频率高。我把这套模型直接写成一段结构化描述丢给编码助手而不是让它自己猜。提示词大概是这样我要做一个宠物生命周期管理App技术栈 React Native Expo TypeScript。 请先不要写代码先帮我设计数据模型。核心实体包括 Pet、Event、Reminder、 MedicalRecord、WeightLog。Event 用 type 字段区分疫苗/驱虫/体检/就诊/体重等。 请给出每个实体的字段、类型、以及实体之间的关系用 TypeScript interface 表达。提示让AI先设计再编码这个动作非常关键。直接让它写代码它会边写边改模型最后你得到一堆自相矛盾的类型定义。2.2 为什么 Event 要统一抽象而不是每种类型建一张表这是我在设计阶段跟编码助手来回拉扯最久的一个点。AI 一开始倾向于给疫苗、驱虫、体检各建独立的表理由是字段不一样。听起来合理但实际用起来是灾难时间线页面要 union 五张表提醒逻辑要写五套统计要写五套。我坚持用单表 type 字段 可空扩展字段的方案理由是时间线是核心视图单表查询天然有序性能好写起来也简单。提醒引擎只需要针对 Event 写一套周期计算逻辑。不同事件类型的差异化字段用metadataJSON 字段兜底TypeScript 侧用 discriminated union 保证类型安全。最终 Event 的接口大概长这样type EventType vaccine | deworm | checkup | visit | weight | grooming; interface PetEvent { id: string; petId: string; type: EventType; title: string; occurredAt: number; // 时间戳 note?: string; metadata?: Recordstring, unknown; // 类型差异化字段 createdAt: number; }这个决策后面省了我大量返工。经验就是凡是同一类东西的不同变体优先考虑单表抽象而不是物理分表。除非字段差异极大且查询模式完全不同否则抽象收益远大于那点存储冗余。2.3 提醒引擎的周期计算别让AI自由发挥提醒是这个App最容易被低估的部分。疫苗有年周期体外驱虫有月周期体内驱虫有季度周期而且还有上次实际执行时间 周期这种滚动计算。我一开始让AI自由发挥它给我写了个固定 cron 表达式完全没考虑上次执行时间这个变量。正确的逻辑应该是下一次提醒时间 上一次同类事件的实际发生时间 该类型的标准周期。这个规则我明确写进提示词AI 才写对。周期配置我做成一张常量表事件类型默认周期是否可自定义疫苗加强365天是体外驱虫30天是体内驱虫90天是体检180天是体重记录30天是注意周期一定要允许用户自定义。不同品牌驱虫药周期不一样硬编码会被用户骂。3. 项目骨架搭建让编码助手一次生成可运行的 Expo 工程3.1 初始化命令与目录结构约定骨架这一步我基本是命令 约定一起给。先让编码助手给出初始化命令我手动执行确认没问题再让它按约定生成目录。初始化用的是 Expo 的 TypeScript 模板npx create-expo-app pet-lifecycle --template blank-typescript cd pet-lifecycle npx expo install expo-sqlite zustand react-navigation/native react-navigation/native-stack目录结构我提前定死避免AI乱放文件src/ db/ # SQLite 封装、迁移 models/ # 类型定义 store/ # Zustand 状态 screens/ # 页面 components/ # 通用组件 services/ # 提醒引擎、业务逻辑 utils/ # 工具函数为什么目录要先定因为AI在生成多个文件时如果目录约定不清晰它会在screens里写数据库逻辑在store里写UI最后你根本没法维护。把目录约定当成架构约束喂给AI它的产出质量会稳定很多。3.2 数据库层SQLite 封装与迁移策略本地持久化我选 SQLite 而不是 AsyncStorage原因是这个App的数据是结构化、需要关联查询、需要聚合统计的。体重曲线要按时间排序取点时间线要按 petId 过滤再排序提醒要跨表 join这些用 key-value 存储做起来非常别扭。编码助手生成的数据库封装我重点审了三个地方迁移机制用PRAGMA user_version做版本号每次启动检查版本低于当前版本就执行迁移脚本。AI 一开始没写迁移我要求补上否则后面加字段用户数据就废了。事务批量插入事件时必须包在事务里否则几百条记录会慢到卡界面。索引petId和occurredAt上建索引时间线查询直接从全表扫变成索引扫。// db/migrations.ts 的核心思路 const MIGRATIONS: string[] [ // v1 CREATE TABLE IF NOT EXISTS pets (...); CREATE TABLE IF NOT EXISTS events (...); CREATE INDEX IF NOT EXISTS idx_events_pet ON events(petId, occurredAt DESC);, // v2 后续加字段 ]; export async function migrate(db: SQLiteDatabase) { const { user_version } await db.getFirstAsync{ user_version: number }(PRAGMA user_version); for (let i user_version; i MIGRATIONS.length; i) { await db.execAsync(MIGRATIONS[i]); await db.execAsync(PRAGMA user_version ${i 1}); } }提示PRAGMA user_version是 SQLite 自带的轻量版本号不用额外建表非常适合移动端本地库的迁移管理。3.3 状态管理为什么选 Zustand 而不是 Redux这个选择我思考得比较久。Redux 生态成熟但对这个体量的App来说样板代码太多一个简单的添加宠物要写 action、reducer、dispatch 三处。Zustand 的 store 就是一个 hook写起来极简而且和 SQLite 的异步操作配合很自然。我给编码助手的约束是store 只存内存状态和派生状态所有持久化操作走 service 层store 通过调用 service 来读写数据库。这样职责清晰AI 也不会把 SQL 语句塞进 store 里。// store/petStore.ts interface PetState { pets: Pet[]; loadPets: () Promisevoid; addPet: (input: NewPet) Promisevoid; } export const usePetStore createPetState((set, get) ({ pets: [], loadPets: async () { const pets await petService.listAll(); set({ pets }); }, addPet: async (input) { await petService.create(input); await get().loadPets(); }, }));实测下来这套结构让AI后续加功能时非常听话因为它知道该往哪一层写代码。4. 核心页面逐个击破时间线、提醒、体重曲线4.1 时间线页面性能与可读性的平衡时间线是打开App第一眼看到的东西也是数据量最大的页面。一只养了五年的猫事件记录轻松上百条。我让编码助手用FlatList而不是ScrollView并且明确要求用keyExtractor指定稳定 key。用getItemLayout固定行高跳过测量。按月份分组组头吸顶。AI 第一次生成的版本用了ScrollViewmap我直接打回重写。列表页超过50条数据就必须用虚拟化列表这是移动端的铁律别让AI偷懒。分组逻辑我单独抽了个纯函数方便测试function groupByMonth(events: PetEvent[]) { const map new Mapstring, PetEvent[](); for (const e of events) { const key formatMonth(e.occurredAt); // 2024-06 if (!map.has(key)) map.set(key, []); map.get(key)!.push(e); } return Array.from(map, ([month, items]) ({ month, items })); }4.2 提醒页面把该做什么变成一眼看懂提醒页面的设计目标是用户打开就知道今天/本周/本月要做什么。我让AI按紧急程度分三档逾期红色、今日橙色、即将到来灰色。这里踩过一个坑AI 一开始把提醒的完成操作直接删除了提醒记录。但实际业务里用户点完成应该是生成一条对应的 Event 记录然后基于新时间重算下一次提醒而不是简单删除。这个逻辑我纠正了两轮才让AI理解。async function completeReminder(reminder: Reminder) { await eventService.create({ petId: reminder.petId, type: reminder.eventType, title: reminder.title, occurredAt: Date.now(), }); await reminderService.reschedule(reminder); // 基于新时间重算 }注意提醒的完成和删除是两个完全不同的语义UI 上一定要分开否则用户会误操作丢失记录。4.3 体重曲线图表库选型与数据降采样体重曲线我纠结过用哪个图表库。react-native-svg手绘灵活但工作量大victory-native开箱即用但包体积大。最后选了react-native-svg自己画折线因为体重曲线的交互需求很简单就是看趋势没必要引入重型依赖。数据降采样是另一个点。如果用户记录了两年、每周一次就是100多个点全画上去在小屏上挤成一团。我让AI实现了简单的按周聚合同一周内取平均值点数直接降到原来的七分之一。function downsampleByWeek(logs: WeightLog[]) { const buckets new Mapstring, number[](); for (const l of logs) { const week getWeekKey(l.recordedAt); if (!buckets.has(week)) buckets.set(week, []); buckets.get(week)!.push(l.weight); } return Array.from(buckets, ([week, values]) ({ week, weight: values.reduce((a, b) a b, 0) / values.length, })); }这个降采样逻辑我特意让AI写成纯函数因为它是典型的边界条件多、容易出错的代码必须能单独测。5. 对话式编程的实战心得AI会在哪里翻车5.1 类型定义漂移最常见的返工来源用AI写TypeScript项目最大的坑是类型定义漂移。你在文件A定义了PetAI在文件B又定义了一个字段略有不同的Pet编译能过因为结构兼容但运行时数据对不上。我的应对方法是所有核心类型集中在models/目录任何文件引用类型必须从那里 import禁止就地定义。这条规则我写进了给AI的系统提示里返工率明显下降。5.2 异步边界AI 特别容易忘记 awaitSQLite 操作全是异步的AI 生成的代码经常漏await导致数据还没写完就读取的竞态。这类 bug 特别隐蔽因为大部分时候能跑通偶尔才出问题。我的做法是在 service 层所有函数返回 Promisestore 层调用时强制 await并且用 ESLint 的no-floating-promises规则兜底。让工具帮你抓比人眼靠谱。5.3 边界条件空状态、超长文本、时区AI 生成的UI默认假设数据总是存在且正常。但真实场景里新用户没有任何宠物时间线是空的得给引导。宠物名字可能很长会撑破布局得numberOfLines。时间戳跨时区显示会错得统一用本地时区格式化。这些我都是逐个页面手动补的。经验是每让AI生成一个页面就立刻问自己空数据会怎样、超长会怎样、跨时区会怎样然后让它补上。5.4 一个具体的排查链路提醒不触发有一次测试发现添加了驱虫事件后提醒没有自动生成。我按这个顺序排查先看数据库events表里记录写进去了吗——写进去了。再看reminders表有没有对应记录——没有。定位到eventService.create里创建事件后应该调用reminderService.schedule但AI生成的代码里这个调用被放在了if (type vaccine)分支里其他类型漏了。修复把提醒调度逻辑从分支里提出来所有类型统一处理。这个坑的教训是AI 写分支逻辑时容易只处理它想到的那一种情况。凡是 if/else都要检查是否覆盖了所有枚举值。6. 从能跑到好用我补的那些AI想不到的细节6.1 数据导出与备份宠物数据是用户长期积累的丢了会很崩溃。我加了一个导出为JSON的功能把 pets、events、reminders 全量导出成一个文件。这个需求AI不会主动提但对真实用户极其重要。async function exportAll(): Promisestring { const [pets, events, reminders] await Promise.all([ petService.listAll(), eventService.listAll(), reminderService.listAll(), ]); return JSON.stringify({ version: 1, pets, events, reminders }, null, 2); }6.2 多宠物切换的上下文管理养多只宠物的人不少切换宠物时所有页面数据都要跟着变。我用一个全局的currentPetId放在 Zustand 里所有列表查询都带上这个 id。AI 一开始把currentPetId存在每个页面自己的 state 里切换后其他页面不刷新我改成全局状态才解决。6.3 深色模式与无障碍深色模式我用useColorScheme配合一套主题常量实现成本不高但体验提升明显。无障碍方面至少保证按钮有accessibilityLabel字体缩放不崩布局。这些AI默认不管得你主动要求。补充项优先级AI是否主动生成数据导出高否多宠物上下文高否深色模式中否无障碍标签中否空状态引导高否这张表基本说明了AI 能帮你把主流程跑通但产品化的细节几乎全靠人补。这也是为什么对话式编程里人的角色不是消失而是从写代码变成定义什么是好产品。7. 我对这套流程的真实体会用对话式编程从零搭这个宠物生命周期管理App前后大概花了几个周末的碎片时间。如果纯手写同样的完成度我估计要三到四倍的时间。但这不是说AI能替代思考——恰恰相反它把写的成本压到极低之后想清楚的价值反而被放大了。数据模型怎么抽象、提醒逻辑怎么设计、哪些边界要覆盖这些决策AI给不了你只能你自己扛。我最大的收获是学会了一种新的协作节奏先让AI快速产出可运行的粗糙版本然后我像做代码评审一样逐层挑问题把架构约束、类型规范、边界条件一条条喂回去。这个循环跑顺了之后效率提升是实打实的。踩过的坑也都在这篇文章里了你照着走能少绕不少弯路。