AI结对编程实战:从零构建单词后台管理系统的完整记录

AI结对编程实战:从零构建单词后台管理系统的完整记录 从今年年初开始我给自己定了一条不成文的规矩凡是能在 AI 结对编程工具辅助下完成的小项目就不再从空目录开始死啃文档。上个月刚收尾的“单词后台管理系统”就是这个习惯下最完整的一次样本技术栈是 Next.js 15App Router、Supabase、Drizzle ORM、shadcn/ui。这个项目本身不算复杂就是一个给外语学习者自己维护单词库、例句和词书的内部后台真正有意思的是从头到尾我都没按传统方式“先看文档再写代码”而是把所有技术问题都变成和 AI 的对话让它负责产出、我负责验收和纠偏。这篇文章不是来宣传“AI 已经能替代程序员”的恰恰相反我想把过程中 AI 一口气写完的部分、翻车之后我不得不返工的部分以及我是怎么建立一套可控协作流程的部分都摊开来讲。适合两类读者一类是刚接触 Next.js 和 Supabase想找一个真实后台项目练手的人另一类是已经在用 AI 写代码但总觉得产出失控、想建立自己协作边界的人。我会把建表、迁移、鉴权、CRUD 页面、排错这些环节的实机操作记录串起来你可以直接照着走一遍。1. 先想清楚再动工我要的到底是“单词后台”还是又一个大而全的 App1.1 从“背单词”需求里切出“管词库”这一层很多人一听到“单词系统”第一反应是做个带每日打卡、遗忘曲线、听力播放的 App。但我在日常学英语时真正的痛点不是没有背单词工具而是那些工具都在帮你管理“它自己的词库”我平时从小说、播客、新闻里摘出来的词散落在备忘录、Excel、笔记软件和手机自带生词本里没有一个统一的地方能让我把这些词按自己的逻辑组织起来。这就是我决定做后台系统而不是 App 的原因。后台系统意味着它只关心数据的录入、编辑、组织和检索不关心用户怎么消费这些数据。它更像一个“仓库管理界面”先把货架和库存理清楚未来无论是做网页版复习、手机端卡片还是导出成 Anki 格式都只是从这个仓库取数据的问题。想清楚这一点之后项目定位就非常清晰构建一个后台词汇管理系统核心功能是为个人词库提供灵活的增删改查与组织能力后续学习应用基于同一数据模型扩展。AI 结对编程在这个阶段的用处很大它能帮我快速把模糊想法变成需求清单但前提是我自己得先知道“要什么”。所以我通常会让 AI 以提问方式帮我澄清需求而不是一上来就让它生成代码。1.2 MVP 词条范围先定字段级清单防止页面做到一半失控我建议在第一次和 AI 对话前先自己把最小闭环内的数据字段列一个草稿。这样 AI 生成的代码才有一个明确依据。我最终的 MVP 字段清单是这样单词原文、音标、词性、中文释义、例句、例句翻译、来源比如书名或播客名、掌握状态、创建时间、更新时间。为什么是这些字段因为它们覆盖了“录入一个生词必须有的信息”和“之后检索时最想过滤的信息”两大类。比如来源字段很多单词软件都没有但对我来说非常重要——我记录一个词的场景直接影响我记不记得住它。状态字段用于标记这个单词处于活用到习惯用法的哪个阶段待复习、已掌握或已归档。这两个字段不算复杂但它们决定了后台列表页必须支持“按来源筛选”和“按状态筛选”数据模型确认得早后面做页面就不会反复返工。1.3 把 owner_id 放进去哪怕单用户也该这么做早期版本里我想反正只是自己用就省了用户 ID 字段所有表都是公共数据。但 AI 提醒了我一个问题如果未来想把这个单词库开放给朋友一起使用或者我在手机上开一个端、在电脑上开一个端两者必须能区分数据归属。更现实的是我本来就准备用 Supabase Auth 做登录那数据模型里没有归属字段RLS行级安全就无从生效。后来所有核心表都加上了 owner_id这个决定让我在“登录后数据隔离”这件事上少走了很多弯路。2. 技术栈的实际取舍四件套选型时AI 说的哪几句话我听进去了2.1 Next.js 15 给后台管理带来的直接好处Server Components 和 Server Actions选 Next.js 做前端不太需要争论。对这个项目来说最重要的原因是整个后台列表页、详情页基本是“读取数据库后渲染 HTML”这是 Server Components 最擅长的场景。Drizzle 查询可以直接在服务端执行不需要像传统前后端分离那样先搭 REST API 再在客户端 fetch。AI 当时给的一句话很关键你们这个阶段Server Actions 还没有大量投入但 Admin 场景下它们非常适合用来替代第一版 REST API。后台管理本质上是内部工具API 的复用性和开放性不是首要约束开发速度和修改成本才是。只要界面和数据库之间有服务端这一层所有数据库操作都在服务端完成前端只负责收集输入、触发 action这样我至少省掉了一整套接口文档和维护成本。2.2 SupabasePostgres 能力、Auth 和 Docker 本地开发一次到位Supabase 相当于给你一个托管好的 Postgres同时附赠了 Auth、Storage 和一套可以随时在浏览器里执行的 SQL 编辑器。对我这种独立开发的小项目来说单独买一台云数据库或者自己维护 Postgres 实例都不划算Supabase 免费层足够支撑个人项目。真正让我推荐它的点是本地开发环境。相关热搜里就有 supabase dockerSupabase CLI 本质上就是通过 Docker 在本地起一整套服务。执行supabase init然后supabase start它会帮你把 Postgres、Auth、Storage 的镜像全部拉起来本地跑的数据库和云端的结构完全一致。这样我可以在本地随意测试迁移、折腾表结构确认无误后再执行与云端数据库的连接和推送不需要担心把生产环境搞坏。AI 在这个环节能帮我写的通常是 Docker 编排脚本和 SQL 诊断语句但 Docker 是否在运行、端口有没有冲突这种环境问题它帮不了你需要自己看日志。2.3 Drizzle 和 Prisma 的选择在“AI 结对”场景下差距比想象中大数据库工具链我一开始想过 Prisma毕竟生态成熟、文档多AI 训练语料里关于 Prisma 的例子也更丰富。但最后选了 Drizzle原因是 Drizzle 更接近 SQL 本身生成的客户端更薄查询都是类型安全的 SQL 构建器不用经过一层重的运行时引擎。对一个只想管理几千个词条的小项目来说Prisma 的很多能力用不上反而每次跑生成都要多一层心智负担。在 AI 结对这个维度Drizzle 还有个隐性优点AI 生成的 Drizzle schema 代码非常贴近“建表 SQL 翻译”我 review 的时候很容易看出每一个字段映射到什么列而 Prisma 的 schema 语言和关系映射规则更多AI 一旦生成多对多关系就容易出现语义偏差。Drizzle 的表定义基本都是函数调用很直白适合在对话里一节一节地确认。2.4 shadcn/ui 的价值不是“组件库”而是代码长在你项目里shadcn/ui 和大多数组件库不一样它不发布 npm 包而是通过 CLI 把组件源码直接复制到你的项目里。这意味着每个按钮、表格、弹窗的样式和逻辑都是你自己的代码想怎么改就怎么改。对 AI 结对编程来说这个特性简直是天然的AI 可以直接读取组件源码来理解现有的样式变量而不是去猜某个 npm 包里我们无法看到的内部实现。后期我给表格加行内编辑时AI 很快定位到components/ui/table.tsx里的样式这和传统“包在 node_modules 里、AI 看不到”的组件库体验完全不同。3. 从 0 到 1 搭基础工程我和 AI 的第一次对话就定好的边界3.1 可以直接抄走的“第一份提示词”框架很多人在 AI 编程工具里第一步就问“帮我搭建一个 Next.js 项目”这种提问方式通常只会得到一段泛泛而谈的步骤因为 AI 既不知道你的环境也不知道你的验收标准。我的经验是第一份提示词必须包含目标、技术约束、上下文、输出格式四个部分。下面这个模板你可以直接改成自己的项目来用目标帮我规划一个“单词后台管理系统”的工程结构技术栈已经确定 Next.js 15App Router TypeScript Tailwind CSS shadcn/ui SupabasePostgres Auth Drizzle ORM。 技术约束 - 所有数据库访问必须封装在独立的>pnpm dlx create-next-applatest word-admin \ --typescript \ --tailwind \ --eslint \ --app \ --src-dir \ --import-alias /*跑完之后的第一件事不是急着写业务代码而是把生成的package.json和tsconfig.json原封不动地贴给 AI告诉它“这是我们当前的版本基线所有依赖判断都以这份 package.json 为准”。这一步能避免后面很多莫名其妙的版本问题。3.3 目录脚手架先约法三章再让 AI 动手初始化完成后我会和 AI 一起确定下面的目录结构src/ app/ (admin)/ words/ page.tsx # 单词列表页 new/page.tsx # 新建单词页 dashboard/page.tsx # 首页概览 layout.tsx login/page.tsx components/ ui/ # shadcn/ui 组件 words/ # 单词模块的业务组件 db/ schema.ts # Drizzle 表定义 index.ts # 数据库连接 lib/ supabase/ client.ts # 浏览器端客户端 server.ts # 服务端客户端 server/ actions/ words.ts # 单词相关的 Server Actions这个结构的核心原则是数据库访问独立页面组件独立Server Actions 独立。AI 在生成代码时如果遵循这个结构后期维护会非常顺如果 AI 想“简化”掉>import { pgTable, uuid, text, timestamp, integer, } from drizzle-orm/pg-core; export const wordStatus [learning, mastered, archived] as const; export const words pgTable(words, { id: uuid(id).defaultRandom().primaryKey(), word: text(word).notNull(), phonetic: text(phonetic), partOfSpeech: text(part_of_speech), definition: text(definition).notNull(), exampleSentence: text(example_sentence), exampleTranslation: text(example_translation), source: text(source), status: text(status, { enum: wordStatus }) .notNull() .default(learning), ownerId: text(owner_id).notNull(), createdAt: timestamp(created_at, { withTimezone: true }) .defaultNow() .notNull(), updatedAt: timestamp(updated_at, { withTimezone: true }) .defaultNow() .notNull(), });这里有一个和 AI 协作时的经典陷阱word语义其实是一个“词条”不是“单词”这个简单概念。如果你只定义字段不管约束AI 可能不设置notNull也可能忘掉给释义设置非空。我让 AI 同时生成一个 zod 校验 schema把表单层和数据库层的约束统一起来省的后面写校验时再翻一遍数据库定义。4.2 词书与词条的多对多让 AI 明确 onDelete 语义光有单词表还不足以体现“管理系统”的价值我还需要能按“主题词书”来组织词条比如“《人类简史》词汇”“口语高频词”“法律英语词”。一个词条可以同时出现在多个词书里所以需要一张连接表。这里我特意让 AI 把删除策略列出来而不是让它自己猜。Drizzle 关联关系里把wordbook_entries里的外部键设置为级联删除相对合理export const wordbookEntries pgTable(wordbook_entries, { id: uuid(id).defaultRandom().primaryKey(), wordId: uuid(word_id) .notNull() .references(() words.id, { onDelete: cascade }), wordbookId: uuid(wordbook_id) .notNull() .references(() wordbooks.id, { onDelete: cascade }), createdAt: timestamp(created_at, { withTimezone: true }) .defaultNow() .notNull(), });为什么需要和 AI 单独确认因为 AI 默认生成的onDelete行为往往是no action也就是删词条的时候如果它挂在某本词书下数据库会直接报错而不是自动清理关联。对于后台管理系统来说用户删除一个词预期就是这个词从所有词书里消失而不是弹出一个外键约束错误。如果你忘了定义级联后面删除接口会变成 bug 高发区。4.3 drizzle-kit 迁移generate 和 push 的顺序别搞反表结构定义好之后接下来要生成迁移文件并应用到本地 Supabase。我的工作流是npx drizzle-kit generate npx drizzle-kit migrategenerate会根据 schema 文件的变化生成带时间戳的 SQL 迁移文件migrate会把这些 SQL 应用到当前连接的数据库。这里容易遇到的问题就是顺序有些教程会让你直接db push它确实更省事但不留历史记录。多人协作或需要回滚时没有迁移文件会非常被动。对于个人项目虽然 push 也够用但我还是倾向于让它生成迁移 SQL我会肉眼 review 一遍drizzle/目录里的 SQL 文件再执行久而久之就养成了检查迁移的习惯。AI 在这一步的参与方式很特别它不直接执行命令而是帮我检查生成的 SQL。我会把迁移文件内容贴给它看问它“这个迁移有没有可能破坏已有数据”AI 通常能指出ALTER COLUMN ... SET NOT NULL在这张表已有数据时可能会失败之类的问题。虽然机器不是万能的但这种“AI 做代码 review 我做最终决策”的方式效率很高。4.4 关于搜索别在一开始就上重量级方案在保留的核心字段之外有一个优化字段我在 MVP 阶段选择不加即全文搜索索引。单词表的搜索场景其实是前缀匹配比如用户输入 “abo” 希望匹配到 “aboard”“abominable”。Postgres 的ILIKE加前缀索引其实就够用了。我自己验证过几千行记录的表在这种场景下查询时间可以忽略不计。如果之后单词量到十万级再考虑 pg_trgm 或者单独接搜索服务也不迟。数据模型设计最重要的原则是别为想象中的未来买单先让它服务好 MVP 功能。5. CRUD 页面怎么从 AI 草稿变成能用的后台表格、弹窗与表单5.1 列表页的做法页面服务端取数表格只做展示后台系统最常见的页面就是“表格 搜索 分页”。我的做法是让列表页本身是一个 Server Component负责接收 URL 参数、调>// src/app/(admin)/words/page.tsx import { findWordsByPage } from /server/data-access/words; import { WordsTable } from /components/words/words-table; export default async function WordsPage({ searchParams, }: { searchParams: Promise{ page?: string; q?: string; status?: string }; }) { const { page 1, q , status } await searchParams; const { items, total } await findWordsByPage({ page: Number(page), q, status, }); return ( div classNamespace-y-4 WordsTable items{items} total{total} q{q} status{status} / /div ); }这个写法有几个好处第一URL 天然是当前状态的序列化刷新页面后仍然停留在当前页和关键词不用额外维护全局状态第二服务端查询不需要额外的 loading 状态首屏 HTML 直接包含数据打开后台的感受非常快。AI 在这个环节非常擅长生成findWordsByPage这类查询函数但你要盯住的是它在>use server; import { createServerSupabaseClient } from /lib/supabase/server; import { deleteWordById } from /server/data-access/words; import { revalidatePath } from next/cache; export async function deleteWordAction(formData: FormData) { const wordId formData.get(id); if (typeof wordId ! string) { throw new Error(invalid id); } const supabase await createServerSupabaseClient(); const { data: { user }, } await supabase.auth.getUser(); if (!user) { throw new Error(请先登录); } await deleteWordById(wordId, user.id); revalidatePath(/words); }为什么权限判断放这里而不是放在按钮上因为按钮隐藏只是前端体验真正要防的是有人直接构造请求调用 Server Action。Server Action 虽然叫“服务端函数”但它本质上是可以从客户端触发的端点所以它内部必须自己校验身份。AI 经常忽略这个要点因为它生成的代码看起来“能点能删”但你一旦把页面做成客户端组件就必须考虑这个问题。5.4 形态控制为什么不让 AI 把整个页面变成一个巨型客户端组件第一次让 AI 生成列表页时它倾向于把整个页面都标记成客户端组件把所有数据通过 fetch 拿到客户端再渲染。这样做的结果就是搜索一次打一次 API页面还要自己管理 loading 和 error 状态代码量凭空多出三分之一。后来我要求它遵循一个默认规则页面级别的数据获取尽量放在 Server Component只有按钮点击、弹窗开合、输入框受控这些交互片段才是客户端范围。shadcn/ui 的表格本身其实没有“逻辑”它只是样式组件可以在服务端组件里直接引用。真正需要use client的是表格内的操作按钮删除确认弹窗、编辑按钮跳转或弹窗组件。这样的组合在 Next.js 里才能发挥 Server Components 的效率。6. Supabase Auth 与 RLS“登录能过但查不到数据”的完整排查链路6.1 从老的 supabase-js 用法换到 supabase/ssr 的必经改造如果你以前搜索过“Next.js Supabase 登录”大概率看到的教程还停留在用supabase/supabase-js的createClient直接初始化然后把 session 存到 localStorage 里。这套方案在 App Router 下已经不推荐了更稳妥的做法是使用supabase/ssr这个包来创建服务端客户端和浏览器端客户端。服务端侧的核心代码大致是这样import { createServerClient } from supabase/ssr; import { cookies } from next/headers; export async function createServerSupabaseClient() { const cookieStore await cookies(); return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) cookieStore.set(name, value, options) ); } catch {} }, }, } ); }所谓“从 localStorage 改为 cookie”核心差异是服务端组件读取 session 时不需要等待浏览器端状态每次请求都能在服务端完成认证判断。AI 最大的问题在于它经常给出新旧两种 API 混用的代码要么用了getSession()这个已被标记过时的接口要么手动处理 cookie 的存取逻辑。后面第 7 节我会专门展开这个坑。6.2 RLS 策略让 AI 写策略但你必须亲自 review 的三种写法Supabase 的 Postgres 默认对数据表打开 RLS也就意味着如果你不写任何 policy任何外部请求都查不到数据哪怕你已经通过 Auth 登录也白搭。AI 写 RLS 策略经常出现三种错误一是策略忘了加to authenticated二是using和with check混用三是把硬编码的用户 ID 写进去。我最终的 policy 非常简单直接create policy words_owner_all on public.words for all to authenticated using (auth.uid()::text owner_id) with check (auth.uid()::text owner_id);注意这里owner_id我在 Drizzle schema 里定义为 text 类型方便和auth.uid()的返回值比较。如果要更严谨可以用 uuid 类型并建立外键到auth.users。但跨 schema 引用 auth 表有时会让开发期迁移变得麻烦这个取舍取决于你想在数据库层做到多严格。6.3 排查“空列表”的完整步骤别总怀疑是你查询写错了实际开发里我遇到过很多次“页面显示空白表格但数据库里明明有数据”的情况。如果你的应用也出现类似现象建议按下面的顺序排查确认当前登录用户能在 Supabase 的 Authentication 页面里看到 session。到 Supabase Dashboard 的 SQL Editor 里执行select auth.uid();确认当前请求上下文能拿到用户。在本地用supabase db dump或select * from words;看一下数据是否存在且owner_id和上面拿到的auth.uid()是否匹配。回到 policy 页面确认 RLS 是开启状态且 policy 的using条件与 SQL 一致。90% 的“登录但无数据”问题最后都出在owner_id没写对或 RLS 没开。这个过程如果让 AI 参与你可以直接把报错信息和表结构一起贴给它让它列出假设但你仍然要自己做 SQL 验证。毕竟 AI 看不到你的真实数据库内容它只能猜。7. 这次结对编程翻车最狠的 4 个坑以及我的纠偏方式7.1 现象一AI 生成了旧版 Auth API一运行就报错我的第一次翻车发生得非常早AI 在生成登录页面时生成了类似下面的代码const supabase createClient(url, key); const { data, error } await supabase.auth.signInWithPassword({ email, password, });光看这段没什么问题但它前面调用createClient的路径和配套的 cookie 管理方式完全基于旧版supabase/supabase-js并没有用supabase/ssr。当我把它集成到服务端布局里时报错明确指出cookies()在服务端组件的调用方式已经改变。这个坑的本质是 AI 的训练数据里含有大量旧教程内容。纠正方式也比较简单我把package.json中相关依赖的版本号贴给 AI并要求它“只用 package.json 中存在的包来引用代码不引入 package.json 之外的任何依赖”。这样它就不会再生成supabase/supabase-js的旧写法了。7.2 现象二shadcn/ui 初始化和 Tailwind 版本互不匹配shadcn/ui 在项目里的安装方式比较特殊它会把组件源码复制到components/ui目录中。如果你用的是 Tailwind v3而 AI 生成的组件样式是基于 v4 的写法默认情况下页面会出现大量样式丢失甚至编译失败。那次的问题出现在 AI 为了“省事”让我在 Tailwind v4 项目里直接执行npx shadcnlatest add button并手工改了一堆配置结果很多类名对不上。这个坑的规避方法是不要手工混着来直接用 shadcn CLI 初始化让 CLI 帮你判断当前 Tailwind 版本。npx shadcnlatest initAI 结对编程里最忌讳的是让 AI“顺手帮你改配置文件”。配置文件之间的依赖关系非常隐晦比如tailwind.config.ts、postcss.config.mjs、app/globals.css和components.json这些文件是耦合的。AI 往往只看到你贴给它的单个文件不了解全套配置。我的经验是涉及配置文件修改时把相关文件全部发给 AI并且让 AI 在一开始列出它将改动哪些文件我确认后再动手绝不让它跨文件“自由发挥”。7.3 现象三AI 在 Server Action 里直接信任客户端传参Server Action 的入参本质上来自客户端请求因此必须有和开放 API 一样的校验意识。AI 在生成编辑单词的 action 时一开始是直接把整个表单对象传进来然后更新数据库完全没有检查这个单词的 ownerId 是不是当前登录用户。这意味着只要我知道某个单词的 id就可以伪造请求改成别人的词条。这个 bug 在 RLS 层面其实会被拦截因为更新时 Drizzle 生成的 SQL 会先执行 policy 检查所以最终数据库层面还算安全但代码里这种信任客户端的坏味道必须在作用层面就杀干净。我给 AI 的补丁指令是在updateWordAction的函数体第一屏加上三件事一是身份校验二是入参的 zod 解析三是确认受影响的词条归属当前用户。只有都通过才继续执行更新更新完再revalidatePath。把这三件事作为所有 action 的标准模板后面再生成其他 action 时我会在 prompt 里带上这个模板让 AI 复用。7.4 现象四AI 自己捏造了不存在的表结构和“假”数据最让我哭笑不得的一次是 AI 在一个页面里引用了word_tags表并自动生成了几个关联查询。但我整个 schema 里根本没有这张表那是它在生成代码时“补全”出来的。为什么会出现这种情况因为 AI 在大段生成时会基于它对“一个单词系统应该有标签”的常识来补全模型而不是严格基于我们前面定义好的 schema。避免这个问题的办法是在生成业务代码前先把完整的 Drizzle schema 文件粘贴给 AI并写一句“所有数据库和字段引用只能从上面这个 schema 文件中取如果不存在请直接说明不要自己补全”。这句话能显著降低虚构表出现的概率。另外我每让 AI 生成完一批文件都会执行一遍类型检查和少数几个针对性查询来验证。在 AI 结对的工作流里持续编译是最重要的“谎言探测器”。7.5 我现在和 AI 结对的最小工作流经历了这些翻车之后我现在已经形成了一套比较固定的工作流分享出来可以参考需求阶段我口述AI 提问澄清产出字段清单和页面清单。工程阶段AI 给命令我执行产出package.json基线。数据阶段AI 按字段清单生成 Drizzle schema我 review 后执行 generate migrate。功能阶段按“查询 → action → 页面 → 组件”的顺序一次只让 AI 改一个文件或一个功能。验证阶段每完成一个小循环都跑 typecheck lint build。回溯阶段如果报错把完整错误信息发给 AI并附上相关文件内容禁止它只猜测。这套流程的核心是AI 负责产生候选方案我负责设边界、验收、回滚。它不是结对编程里“人人平等”的关系更像是高级开发者和实习开发者的关系实习生速度快、体力好但仍然需要一个经验更丰富的人在旁边盯着。8. 跑通核心闭环之后我接下来接的功能与用法扩展8.1 批量导词JSON 导入 按字词去重核心 CRUD 跑通之后后台系统才能真的开始积累词库。下一步我给项目加了“导入词库”功能用户粘贴一段 JSON系统批量解析并写入数据库。const parsed batchWordSchema.parse(JSON.parse(rawJson)); for (const item of parsed) { await db .insert(words) .values({ ...item, ownerId: user.id, }) .onConflictDoNothing(); }onConflictDoNothing需要表上有唯一约束。这里我加上了一个基于(owner_id, word)的联合唯一索引语义是“每个用户同一个单词只存一条”。这个约束让后续无论是导入还是手动输入都不用担心产生重复脏数据。AI 在这个功能上表现得很好因为导入解析、去重逻辑相对独立且容易写单元测试。8.2 从“管理词库”延伸到“按词书学习”当后台单词量积累到一定程度我自然会在前台做一个简单的“按词书练习”页面。这个页面的接口逻辑其实完全是现成的查词书 ID、返回该词书下所有词条、按复习间隔做筛选。因为当初做的就是干净的数据结构分离前台页面甚至不需要改数据库。那时你会感谢自己在 MVP 阶段没有图省事把所有数据塞在一张 blob 表里。8.3 我现在的真实使用习惯和总结这个系统上线后我实际使用中的感受是最初两天热度很高把多年积累的几百个词条都录了进去中期开始变得懒散因为录入一个词需要填的字段确实偏多。于是我又加了一个“快捷录入”模式只需要填单词和释义其他字段后面有需要再补。这个改动让我意识到个人后台类工具最重要的是降低录入摩擦而不是功能多。如果一开始就要求所有字段必填再好的工具也会被闲置。回看这次项目最大的体会是“AI 结对编程”真正提升的不是写码速度而是你敢于从 0 启动一个项目的心理门槛。以前我会因为要查一堆文档而拖延现在只需要把目标拆得足够细然后像布置任务一样安排给 AI。要注意的是AI 永远只会给你一个“看起来合理的答案”它不会为你的数据安全、字段语义和长期维护负责这些还是要靠人来把握。如果你也想做类似的小系统我建议从今天开始选一个你反反复复想做的项目把第一份提示词写出来剩下的就是反复对话、执行和打磨的过程了。