本文记录一次真实的学习与实践:先理解 Spec-Driven Development(SDD),再用需求、架构和任务文档驱动 AI,做出一个"英文网页一键提取并翻译成中文"的 Chrome 扩展。

一、我为什么开始反思 Vibe Coding
刚接触 AI Coding Agent 时,最容易上手的方式是直接输入一句:"帮我做一个用户认证系统。"很快就能得到一个看起来能跑的项目,但接下来通常会出现另一种体验:框架选型不一致、边界条件没有定义、前几轮对话的决定逐渐丢失,最后只能一边等待模型生成,一边返工。
问题不一定是模型能力不够,而是输入给模型的上下文不够稳定。聊天窗口里的历史会被截断,需求藏在自然语言里,模型只能猜测没有说清楚的部分。我的学习笔记把这种模式概括成三件事:上下文缺失、会话历史丢失、AI 猜测与幻觉。
SDD 的核心并不神秘:先写规范,再写代码。它把开发拆成"两次创造":第一次在文档里明确做什么、为什么做、怎么做、按什么顺序做;第二次才让 AI 按照这些约束生成物理代码。没有蓝图就盖房子,代码越快,返工可能也越快。
二、SDD 里最小但关键的三份文档
在这次项目里,我没有一开始就写 React 组件,而是先建立三层事实来源:
| 文档 | 要回答的问题 | 本项目中的结果 |
|---|---|---|
proposal.md |
做什么、给谁用、边界是什么 | 只做英译中,只处理文章主体,只保留最近一次结果 |
design.md |
用什么技术、模块如何协作 | Readability 提取正文,Turndown 转 Markdown,OpenAI 兼容接口流式翻译 |
tasks.md |
先做什么、哪些能并行、完成怎么算 | 从依赖和 P0/P1/P2 优先级拆成 T0 到 T6 |
这三份文档对应 SDD 的第一次创造。代码不是新的事实来源,而是规范的实现结果;如果实现过程中发现设计不合理,就先更新文档,再继续改代码。
三、先写边界,MVP 才不会膨胀
这个项目最重要的需求不是"翻译能力很强",而是把一次阅读动作压缩成一条稳定链路:
- 点击扩展图标,打开侧边栏。
- 按需注入内容脚本,提取当前页面主要文章。
- 将 HTML 转为 Markdown。
- 调用 OpenAI 兼容的大模型流式翻译。
- 在侧边栏以 Markdown 和打字机效果展示。
- 保存最近一次结果,支持复制和下载。
同样重要的是"不做什么":不处理导航栏、广告、评论、弹窗和 iframe;不做翻译历史、批量翻译、离线缓存或中英对照;第一版只支持英文到中文。
这些边界来自 proposal.md,它们直接减少了 AI 的猜测空间。比如"要不要做历史记录"不再是实现过程中临时讨论的问题,而是明确的非目标。
四、技术架构:把难点放在正确的位置
1. 正文提取:不要先写一堆站点选择器
网页正文提取是第一个真正的难点。纯规则方案需要为不同站点维护选择器,维护成本会随站点增加。设计文档选择了 Mozilla Readability:在浏览器本地运行,通过文本密度和链接密度等启发式规则过滤导航、广告和侧栏,再用 Turndown 转成 Markdown。
实现上还保留了三个关键兜底:先用 isProbablyReaderable 预检;解析时克隆 DOM,避免改坏原页面;Readability 失败后依次尝试 article、main 和 [itemprop="articleBody"]。这不是为了追求"所有网页都能提取",而是把 MVP 的失败方式定义清楚。
ts
const parsed = new Readability(clone, {
classesToPreserve: collectLanguageClasses(clone),
}).parse()
let contentHTML = parsed?.content
if (!contentHTML) contentHTML = findFallbackContent(clone)
if (!contentHTML) throw new Error('该页面无可用文章内容')
图片也被单独写进设计:优先读取懒加载属性,基于页面 URL 补全相对地址,只允许 http、https 和 data 协议,最后输出 。这样做的价值是让"图片不能丢"变成可检查的规则,而不是一句模糊要求。
2. 模型接入:用兼容协议换取可替换性
翻译层没有把某家模型服务写死,而是基于 OpenAI SDK 的兼容模式读取三个配置:baseUrl、apiKey、model。默认值是 DashScope 的 Qwen 兼容端点和 qwen-plus,但用户可以在设置页切换其他兼容服务。
ts
const client = new OpenAI({
apiKey: settings.apiKey,
baseURL: settings.baseUrl,
timeout: 60_000,
maxRetries: 1,
dangerouslyAllowBrowser: true,
})
这里的 dangerouslyAllowBrowser 不是"安全问题消失了",而是明确选择了客户端直连:密钥只存 chrome.storage.local,扩展不提供中间服务器。这个选择适合个人工具和 MVP;如果产品要面向大量用户,密钥托管、额度控制和服务端代理就应该重新设计。
3. 流式翻译和打字机:两个节奏不要混在一起
模型接口负责流式返回 chunk,UI 负责把 chunk 以合适频率渲染。项目里的 useTypewriter 用约 45ms 的节流间隔,把网络增量放入缓冲区,再更新 React 状态;用户点击停止时通过 AbortController 中止请求,并保留已显示内容。
这是一处很容易被"先跑起来"忽略的设计:如果每个网络 chunk 都触发 Markdown 全量重渲染,长文体验会变差;如果等接口全部返回再展示,又失去了流式交互的意义。文档先把两种节奏分开,代码实现就有了明确的验收点。
五、让任务文档成为 AI 的工作队列
tasks.md 没有写成一句"把插件做出来",而是按依赖拆成阶段:
- T0:初始化项目、依赖、Manifest 和页面骨架。
- T1:正文提取、图片处理、HTML 到 Markdown。
- T2:Service Worker 和按需注入。
- T3:模型客户端、提示词、存储和错误处理。
- T4:设置页。
- T5:侧边栏状态机、工具栏、Markdown 渲染、打字机、全流程编排。
- T6:图标、体验打磨和最终验收。
每个任务都有依赖、涉及模块、完成标准和可见效果。例如 T1.1 的完成标准不是"文件写好了",而是"典型英文文章能返回元信息和净化后的 HTML,非文章页返回明确失败原因"。这让 AI 的输出从"看起来合理"变成"能按条件验收"。
项目规则还规定了单一任务原则:一次只执行一个明确任务,完成后等待确认。它牺牲了一点连续生成速度,换来更小的回滚范围和更容易定位的幻觉。
六、规范如何落到真实代码
最终代码里能看到文档留下的结构化痕迹:
text
src/
├── background/ # 打开侧边栏、按需注入、消息路由
├── content/ # 页面内提取与 Markdown 转换
├── panel/ # 状态机、打字机、结果展示
├── options/ # 模型配置表单
└── shared/ # 提取、LLM、存储、类型与常量
从侧边栏点击"翻译"开始,useTranslation 串起完整流程:向 background 请求提取,读取模型配置,先输出标题/作者/链接头部,再消费 streamTranslate 的增量,翻译完成后把 lastResult 覆盖写入本地存储。重新打开侧边栏时,读取这条记录并恢复展示。
这条链路的好处是每一步都能回指规范:
- Manifest 只申请
activeTab、scripting、storage、sidePanel,内容脚本按需注入。 - 失败统一转换为中文错误码,页面不可提取、密钥缺失、超时和服务端错误都有明确状态。
- 输出固定为标题、作者、原文链接和正文,满足复制和下载 Markdown 的需求。
七、Git 和会话管理,也是 SDD 的一部分
AI 生成代码并不等于可以不管版本控制。我的实践记录里把 Git 回退策略和 AI 会话管理单独列出来:一个任务完成就形成可回退的提交;发现模型产生幻觉时,根据改动是否进入暂存区或提交记录选择恢复方式;换一个主题就开启新会话,避免旧上下文污染新任务。
这里的关键不是记住某条命令,而是让"规范、代码、会话"都能被追踪。窗口关闭后,聊天历史可能消失,但 docs/ 和 Git 提交还在。
八、这次实践带来的几个结论
1. SDD 不是多写文档,而是把沟通前移
真正增加的工作,主要是第一次创造:明确边界、做技术选型、拆依赖、写验收标准。后面 AI 生成代码反而更快,因为很多争议已经在生成前解决了。
2. 文档要可执行,不能停留在口号
"要有良好体验"不可验收;"流式 chunk 以 30~60ms 节流更新、支持停止并保留已输出内容"才可以对应代码和测试。SDD 的价值不在文档数量,而在意图是否清晰、可执行、可验证。
3. AI 仍然不能替代架构判断
Readability 还是规则提取,客户端直连是否适合当前安全边界,长文是否需要分段,都是需要人做取舍的问题。AI 擅长把明确的设计变成代码,但不应该替你默认所有重要决策。
4. 最好的规范会持续回写
实现一定会暴露新事实:目录名可能变化,某个库的行为可能和预期不同,某个"预留能力"可能暂时不做。发现偏差后更新 proposal/design/tasks,比只在代码里打补丁更可靠。
结语:把聊天窗口降级为执行工具
SDD 并不是拒绝 AI Coding,而是改变 AI 的位置:聊天窗口不再是唯一的项目记忆,而是执行规范的一种入口。对于这次 Chrome 翻译插件,最有价值的产物不只是一个能工作的扩展,还包括一套可以复盘、验收、回退和继续迭代的开发记录。
当代码生成越来越便宜,稀缺的就不再是"让模型写出更多代码",而是写出清晰、可执行、可验证的意图。先把第一次创造做完整,第二次创造才不会变成无休止的返工。