如何编写技术规格——模板、示例与免费生成器
技术规格(spec)是软件工程中最被低估的工具。这篇文章带你逐一拆解 spec 的每个部分,展示一份完整的真实案例,并给你一个可以直接复制使用的模板——还有一个免费的在线生成器。
技术规格是什么?
技术规格是一份书面文档,描述一段软件应该做什么、不该做什么、以及你怎么判断它做对了。它不是 PRD(产品需求文档),不是 Jira 卡片上的描述字段,更不是某个人在 Slack 里说"对,差不多就这样"。技术规格是定义功能的人、实现功能的人和测试功能的人之间的一份书面契约。
一份好的 spec 的核心特征是可测试性。文档中的每条陈述都应该指向一个可观测的结果——无论是 QA 工程师、自动化测试脚本还是看演示的产品经理,都能不问作者就判断对错。如果 spec 里的某句话无法被测试,那它就是一条观点,不是规格。
Spec 的长度因场景而异。一个简单的配置变更可能半页就够了,一个跨服务的数据迁移功能可能需要五页。长度不是目标,完整性才是。判断标准是:一个从未见过代码库的合格工程师,读完 spec 后能独立实现功能,并通过所有验收标准,期间不需要问任何澄清性问题。这个标准很高,大多数 spec 做不到百分之百——但值得作为目标。
接下来这篇文章会告诉你 spec 里该放什么、带你走一遍完整的真实案例,最后给你一个可以复制到下一个项目里的模板。如果你想更深入了解 Spec-First 的理念,可以看 什么是 Spec-First 开发?。
为什么要在写代码前写 spec?
先写 spec 的理由不是为了流程规范或团队纪律,而是为了成本。具体来说,是一个错误决策在交付的不同阶段被发现时的成本差异。
在文档评审阶段发现的错误决策,代价是一小时。在代码评审阶段,一天。在 QA 阶段,一周。到了生产环境,那就贵得多了。
这条成本曲线不是理论假设,而是每个迭代都在发生的事:开发根据模糊的 ticket 写出一个"合理"的实现,QA 发现行为和产品预期不一致,团队花两三天拆掉重来——不是因为谁犯了错,而是因为没有人在写代码之前写清楚"正确"到底是什么意思。
Spec 还能解决会议解决不了的协调问题。三个工程师坐在规划会里对同一个功能描述点头,离开会议后每个人脑子里的模型都不太一样。这种分歧一直隐藏到代码评审时才暴露——有人认为功能处理已归档记录,有人认为不处理。Spec 能在分歧产生之前把决策写成白纸黑字。
写 spec 确实需要时间。一个中等复杂度的功能大概要花两到三个小时。但问题不是"写 spec 花不花时间"——当然花。问题是"你想在什么时候为模糊性买单?"最便宜的时候是在写代码之前;最贵的时候是代码上线后,客户帮你发现了不一致。
没有 Spec 的情况
- Ticket 写着"加个联系人去重功能"
- 每个工程师各自脑补缺失的细节
- 边界条件在 QA 或生产环境才浮出来
- 实现到一半开始争论范围
- 回滚方案:"到时候再说"
- QA 根据口头描述写测试用例
- 新人上手靠口口相传
有 Spec 的情况
- Spec 定义了匹配规则、合并策略、冲突处理
- 异步评审在写代码前就发现缺漏
- QA 直接从验收标准推导测试用例
- Non-goals 在范围蔓延发生前就挡住了
- 回滚方案:feature flag,已测试,有文档
- 产品在评审时确认行为符合意图
- 新工程师看 spec 而不是到处问人
| 发现阶段 | 通常成本 | 举例 |
|---|---|---|
| Spec 评审阶段 | 1-2 小时 | "我们忘了定义 email 重复时该怎么处理" |
| 代码评审阶段 | 1-2 天 | 实现假设不同,需要大面积重构 |
| QA 阶段 | 3-5 天 | 测试用例和实现对不上,返工加回归 |
| 生产环境 | 1-3 周 | 事故响应、热修复、客户沟通、复盘 |
每份技术规格必须包含的 7 个部分
写过和评审过数百份 spec 之后,我总结出七个每份技术规格都应该包含的部分。对于复杂功能,你可以再加上发布计划、监控要求、安全考量等,但这七个是底线。少了任何一个,实现过程中就会有人用自己的假设去填补空白。
1. 功能名称
一个简短、描述性的名称,团队里任何人提到它时不会产生歧义。"基于 email 的联系人去重"是一个功能名称;"提升数据质量"不是——它是个目标,可能意味着二十件不同的事。功能名称要具体到两个人在不同场合提到它时,一定是在说同一件事。
示例:"CSV 导入——批量创建联系人并去重"
2. 目标(Goal)
一到三句话,描述这个功能解决了什么用户问题或业务目标。目标不是描述系统会做什么——而是说明系统为什么要这么做。一个好的目标是可检验的:功能上线后,有人应该能看着它说"达到了目标"或"没达到"。
示例:"当两条联系人记录拥有相同的 email 地址时,自动将它们合并为一条规范记录,防止重复触达,减少 CRM 中的数据漂移。"
3. 非目标(Non-goals)
明确列出这个功能不会做什么。Non-goals 比 spec 中的其他任何部分都更能预防争论。它们不是限制或道歉——而是有意识的范围决策。没有 non-goals,好心的工程师会把功能扩展到处理相邻场景,项目时间线悄悄漂移,等发现的时候已经晚了。
示例:"本 spec 不涵盖按手机号去重、不构建人工审核 UI、不回溯处理已有记录。"
4. 验收标准(Acceptance Criteria)
一组具体的、可测试的陈述,定义什么行为是正确的。每条标准应该遵循 Given/When/Then 格式:一个前置条件、一个触发动作、一个预期结果。好的验收标准让 QA 完全不需要去猜"正常工作"是什么意思——答案写在 spec 里。
示例:"Given 新联系人使用 email [email protected] 创建,When 同一账户下已存在该 email 的联系人,Then 新联系人不被创建,已有联系人的 updated_at 被刷新。"
5. 边界条件(Edge Cases)
边界条件是主验收标准没有覆盖、但生产环境中一定会出现的输入、状态或场景:空字段、并发写入、大小写差异、权限边界、时区效应。对每个边界条件,写明输入条件和预期行为。别让工程师在实现过程中自己去"发现"——那就是 bug 被发布出去的方式。
示例:"Email 比较不区分大小写。'[email protected]' 和 '[email protected]' 被视为重复。"
6. 产出 / 交付物(Output / Deliverables)
列出实现完成后这份 spec 产出的具体制品。可能是 API 端点、数据库迁移、配置变更、UI 组件、后台任务或 feature flag。目的是给团队一个清单:所有交付物就位、所有验收标准通过,功能就算完成了。
示例:"交付物:dedup_service 模块、findByEmail() 查询方法、dedup_events 审计表、DEDUP_ON_CREATE feature flag、CSV 导入去重处理器。"
7. 依赖(Dependencies)
列出在构建或部署之前必须就位的每个外部服务、团队或系统。没有在实现前明确的依赖会导致迭代中途卡住。尽早列出来,这样团队可以并行推进或调整排期。
示例:"需要 contacts-service v2.3+ 提供 findByEmail 端点。需要 automations-service 暴露 getAutomationsByContactId 用于冲突检测。Feature flag 服务需支持按账户灰度。"
完整示例:CRM 联系人去重
抽象地讲解 spec 各部分固然有用,但大家真正需要的是一份完整、真实的示例来学习和参考。下面是一份 CRM 联系人去重的完整技术规格——和 Spec Coding 首页展示的是同一个功能。每个部分都填入了生产级别的细节。篇幅较长是刻意为之:一份真正的 spec 要详尽到工程师能仅凭它实现功能,不需要追问。
以下是完整的 spec 示例(英文,可直接复制使用):
# Feature: Contact Deduplication by Email
## Goal
When two contact records in the CRM share the same email address,
automatically merge them into a single canonical record. This prevents
duplicate outreach, reduces data drift across sales and marketing
systems, and ensures reporting accuracy on unique contacts.
Applies to contacts created via any channel: web form, API, CSV import,
and manual entry.
## Non-goals (this version)
- We are NOT deduplicating by phone number or name — email only.
- We are NOT merging contacts across different account tenants.
- We are NOT handling contacts with no email (excluded from dedup logic).
- We are NOT building a manual review UI for potential duplicates.
- We are NOT retroactively deduplicating existing records at launch.
This spec covers new-contact creation and import only.
- We are NOT sending email notifications to end users when a merge occurs.
## Acceptance Criteria
Given a new contact is created via the web form with email "[email protected]"
When a contact with that email already exists in the same account
Then the new contact is NOT created;
the existing contact's updated_at is refreshed;
fields that are null on the existing record are populated from the new data;
the response returns HTTP 200 with the existing contact's ID.
Given a new contact has a different name but the same email as an existing contact
When deduplication triggers
Then the existing contact's name is NOT overwritten;
only null fields on the existing record are populated from the new data.
Given a CSV import contains two rows with the same email
When the import job processes them in order
Then the first row creates the contact;
the second row is treated as an update to the first;
a dedup_event is logged with both source row numbers.
Given a CSV import contains 50,000 rows with 2,000 duplicate emails
When the import job completes
Then all dedup merges are logged in dedup_events;
the import summary shows created_count and merged_count separately;
total processing time is under 120 seconds.
Given the existing contact has del_flag = 1 (archived)
When a new contact with the same email is created
Then the archived contact is NOT treated as a duplicate;
the new contact is created normally.
Given the new contact has an empty or null email field
When deduplication logic runs
Then deduplication is skipped entirely;
the contact is created with a null email.
## Edge Cases
- Email comparison is case-insensitive.
"[email protected]" and "[email protected]" are treated as duplicates.
Implementation: normalize to lowercase before comparison.
- Leading/trailing whitespace in email is stripped before comparison.
" [email protected] " matches "[email protected]".
- Two contacts have the same email and the same updated_at timestamp
(e.g., from a bulk import where all rows share the same timestamp).
Resolution: keep the record with the lower internal ID (creation order).
- Two contacts with same email are owned by different users (User A, User B).
Resolution: merge into the record with the earlier created_at;
update ownership to the owner of the surviving record;
log ownership change in the audit trail.
- Dedup target has active automations (sequences, workflows) referencing its ID.
Resolution: do NOT merge. Flag for manual review.
Create a dedup_conflict record. Notify the owning user via in-app notification.
- API caller sends X-No-Dedup: true header with contact creation request.
Resolution: create the contact without dedup.
Requires admin API key. Return 403 if used with a standard key.
Purpose: data migrations and testing only.
- Contact email contains a plus-alias (e.g., "[email protected]").
Resolution: treat as distinct from "[email protected]".
Do NOT normalize plus-aliases. This is a deliberate product decision —
some users intentionally use aliases to segment contacts.
## Output / Deliverables
1. dedup_service module — core matching and merge logic
2. contacts-service: findByEmail(email, accountId) query method
3. contacts-service: mergeContacts(sourceId, targetId) method
4. dedup_events database table — audit log of all merges
Columns: id, source_contact_id, target_contact_id, trigger (api|import|form),
account_id, merged_fields, created_at
5. dedup_conflicts database table — records where merge was blocked
Columns: id, source_contact_id, target_contact_id, reason, resolved, created_at
6. DEDUP_ON_CREATE feature flag (per-account, defaults to false)
7. CSV import handler updated to call dedup_service per row
8. Web form handler updated to call dedup_service on submit
9. Admin API endpoint: GET /admin/dedup-conflicts?account_id=X
10. In-app notification template for dedup conflicts
## Dependencies
- contacts-service v2.3+ must expose findByEmail(email, accountId).
Owner: contacts team. ETA: available now.
- automations-service must provide getAutomationsByContactId(contactId)
to check for active automations before merge.
Owner: automations team. ETA: Sprint 14.
- audit-log-service must accept dedup_event writes with the schema above.
Owner: platform team. ETA: available now.
- Feature flag service must support per-account boolean flags.
Owner: platform team. ETA: available now.
- Notification service must support in-app notification delivery.
Owner: notifications team. ETA: available now.
## Rollout Plan
Stage 1: Internal accounts only (5 test accounts), DEDUP_ON_CREATE = true
Observation: 48 hours
Success: zero unintended merges in audit log; zero support tickets
Stage 2: 10% of paying accounts, randomly sampled
Observation: 7 days
Success: dedup_event rate matches expected duplicate rate (<3% of new contacts)
Stop-loss: any data loss report from a customer → halt immediately
Stage 3: 100% of accounts
Condition: no issues in Stage 2 after 7 days
Rollback:
Step 1: Set DEDUP_ON_CREATE = false (immediate, no deploy)
Step 2: If merges occurred, run restore-merged-contacts.py --since <timestamp>
Data repair owner: data-eng on-call
Point of no return: if >10,000 merges occurred, automated restore is not safe;
escalate to data-eng lead for manual review
这份 spec 大约一页纯文本,花了两个小时写。但这两个小时能帮团队省下好几天的返工、好几轮"等一下,这种情况该怎么处理?"的代码评审讨论,以及至少一次因为静默合并了正在跑自动化流程的联系人而引发的生产事故。
如果你想看这个案例的逐节拆解和决策分析,可以阅读 真实案例:用 Spec-First 构建 CRM 功能。
如何编写验收标准(Given/When/Then)
验收标准是任何技术规格中最重要的部分。它们是"产品应该做什么"和"QA 能验证什么"之间的桥梁。Given/When/Then 格式之所以好用,是因为它强制你写清三件事:前置条件(Given)、触发动作(When)、可观测的结果(Then)。三者缺一,标准就是不完整的。
最常见的失败模式是写出看上去具体、实际上模糊的标准。"系统应正确处理重复数据"听着像一条需求,但 QA 根本不知道该测什么。下面列出五组反面和正面示例。区别永远是一样的:好的版本给出了具体的输入、动作和输出。
反面示例
"重复联系人应该被正确合并。"
正面示例
"Given 新联系人使用 email [email protected] 创建,When 同一账户下已存在该 email 的联系人,Then 新联系人不被创建,已有联系人的 updated_at 时间戳被刷新。"
反面示例
"导入大文件时速度应该足够快。"
正面示例
"Given 一个包含 50,000 行、其中 2,000 个 email 重复的 CSV 文件,When 导入任务完成,Then 总处理时间在 120 秒以内,且导入摘要分别显示 created_count 和 merged_count。"
反面示例
"错误处理应该健壮。"
正面示例
"Given contacts-service 在去重查询时返回 503,When 表单处理器捕获到该错误,Then 新联系人在不执行去重的情况下正常创建,并将原始请求载荷写入 dedup_errors 表作为警告日志。"
反面示例
"用户应该收到冲突通知。"
正面示例
"Given 去重目标关联了活跃的自动化流程,When 尝试执行合并,Then 合并被阻止,创建一条 dedup_conflict 记录,并向负责人发送站内通知,包含冲突详情和管理后台处理页面的链接。"
反面示例
"API 应该返回合适的状态码。"
正面示例
"Given 使用标准 API key 并携带 X-No-Dedup: true 请求头,When 处理联系人创建请求,Then API 返回 HTTP 403,错误码为 ADMIN_KEY_REQUIRED,联系人不被创建。"
注意规律:每条好的标准都给出了具体的前置条件("Given 一个包含 50,000 行的 CSV 导入")、具体的动作("When 导入任务完成")和具体的可度量结果("Then 总处理时间在 120 秒以内")。QA 能直接把每条标准转化为自动化测试,不需要问作者任何问题。这就是你的标杆。
一个实用建议:在讨论实现方案之前写验收标准。如果你在技术设计讨论之后再写,标准容易描述系统内部行为而不是外部可观测的输出。验收标准应该从"站在系统外面测试"的视角来写——除非某些内部细节是公开契约的一部分(比如 API 响应体),否则不要引用内部方法名、数据库列名或实现细节。
功能规格模板——复制即用
下面是一个包含所有七个部分的空白模板。复制到一个 Markdown 文件里,逐部分填写,然后在实现之前发给团队做异步评审。模板中用方括号标出了填写指引——填入真实内容时把它们删掉。
以下是完整的模板(英文,可直接复制使用):
# Feature: [Short, descriptive feature name]
## Goal
[1-3 sentences. What user problem or business outcome does this solve?
Write it so someone can look at the finished feature and say
"yes, this achieves the goal" or "no, it doesn't."]
## Non-goals (this version)
- [What this feature explicitly will NOT do]
- [Scope boundary that prevents drift]
- [Adjacent feature that is out of scope and why]
## Acceptance Criteria
Given [precondition — a specific system state or user context]
When [trigger — a specific action the user or system takes]
Then [outcome — the observable, testable result]
Given [precondition]
When [trigger]
Then [outcome]
Given [precondition]
When [trigger]
Then [outcome]
[Add as many criteria as needed. Each one should be independently
testable by QA without asking the author for clarification.]
## Edge Cases
- [Input condition or system state that the main criteria don't cover]
Resolution: [What the system does in this case]
- [Another edge case]
Resolution: [Expected behavior]
- [Empty input / null value / concurrent write / permission boundary]
Resolution: [Expected behavior]
## Output / Deliverables
1. [Concrete artifact: API endpoint, database migration, module, etc.]
2. [Another deliverable]
3. [Feature flag name and default value]
## Dependencies
- [Service, team, or system that must exist before implementation]
Owner: [team name]. Status: [available / ETA]
- [Another dependency]
Owner: [team name]. Status: [available / ETA]
## Rollout Plan (optional but recommended)
Stage 1: [Limited rollout — internal or percentage-based]
Observation: [Duration]
Success signal: [Metric or absence of failure]
Stage 2: [Broader rollout]
Observation: [Duration]
Success signal: [Metric]
Rollback:
Step 1: [Immediate reversal — e.g., feature flag off]
Step 2: [Data repair if needed]
这个模板适用于任何规模的功能。小改动可能只需三条验收标准、跳过发布计划;复杂的跨服务功能可能每个部分都要扩充。结构保持不变。
如果你不想从空白文件开始,规格生成器工具能根据简短的功能描述生成一份结构化的 spec。输入功能名称和几句话,工具会生成包含全部七个部分的 spec 初稿,你可以在此基础上编辑调整。
技术规格中最常见的 5 个错误
写 spec 是一项技能,和所有技能一样,有些做法看上去没问题但会持续引发问题。以下是我在跨团队评审 spec 时见到最多的五个错误。
1. 验收标准描述的是实现,而不是行为
像"dedup_service 调用 findByEmail 并合并记录"这样的标准,描述的是系统内部如何运作。对 QA 毫无用处,因为他们从系统外部观察不到内部方法调用。验收标准应该描述可观测的输入和输出:用户看到什么、API 返回什么、操作完成后数据库里出现什么。实现细节属于代码,不属于 spec。
修正方法:把每条标准从"通过公开接口测试系统"的视角重写。如果标准里出现了私有方法名、内部队列或实现模式,那它属于设计文档而不是 spec。
2. 完全跳过 Non-goals
当没有 non-goals 部分时,范围只由 spec 写了什么来定义。但功能周围总有合理的人可能会假设属于范围的相邻工作。如果联系人去重的 spec 没有明确写"我们不按手机号去重",实现过程中至少会有一个工程师问到这个问题,而答案会变成一条无人可查的 Slack 消息。Non-goals 写起来成本极低,却能避免迭代中途那些昂贵的范围争论。
修正方法:在定稿之前问自己:"别人最可能认为包含在内、但实际上不在范围内的是什么?"把答案写进 non-goals。
3. 把边界条件当作事后补充
一份有四条验收标准、零条边界条件的 spec 不是完整的 spec。它只覆盖了 happy path,其他一切交给工程师自己判断。边界条件部分是你记录空输入、null 值、并发操作、权限边界和异常系统状态的地方。每一个你没写下来的边界条件,都是一个 QA 不会去测的潜在生产 bug——因为没人告诉他们这个场景相关。
修正方法:逐一审视每条验收标准,问自己:"输入为空怎么办?为 null 怎么办?重复了怎么办?极大值怎么办?两个用户同时提交怎么办?没有权限怎么办?"把每个问题的答案写下来。
4. Spec 写得太长,没人看得完
一份十五页、塞满详尽技术细节、架构图和替代方案分析的 spec 很令人印象深刻,但适得其反。没人会仔细评审它,而评审对话恰恰是 spec 价值所在。这份 spec 变成了一个合规产物,而不是沟通工具。目标是一份评审者在三十分钟以内能读完并给出实质反馈的文档。
修正方法:常规功能瞄准一到三页纯文本。如果 spec 超出了这个范围,考虑是否把多个功能揉在了一份 spec 里。每份 spec 应该覆盖一个可独立构建、测试和发布的交付单元。如果功能确实复杂,拆成多份文档,每份有清晰的边界。
5. 直到实现才去确认依赖
一份写着"接入通知服务"但没有确认通知服务是否支持所需能力的 spec,会在实现阶段卡住。依赖是迭代中途卡住的第一大原因。如果依赖没有在开发开始前确认,你就是在赌它需要时能就位——这个赌注失败的概率高到值得在 spec 里专门处理。
修正方法:对每个依赖,写明所需的具体能力、负责团队和当前状态(已就绪、预计某日就绪、尚未开发)。如果依赖尚未就绪,spec 应说明如果它延期会对时间线产生什么影响。这不是悲观——这是计划。
试用免费规格生成器
如果你想直接跳过"面对空白页"的尴尬,本站的规格生成器能根据简短的功能描述生成一份结构化的技术规格。你只需提供功能名称和几句话的描述,工具就会生成包含全部七个部分的完整 spec:目标、非目标、Given/When/Then 格式的验收标准、边界条件、交付物和依赖。
生成的 spec 是起点,不是终稿。它给你一个合理的结构和默认内容,你可以在此基础上编辑、扩充,然后分享给团队评审。大多数用户发现从生成的草稿开始比从空白模板开始快得多——尤其是当团队刚开始写 spec 的时候。
工具免费、在浏览器里运行、不需要注册。
现场示例:评审真正缺的不是架构图
有些规格看起来很完整,但评审还是拖很久。原因往往不是缺少架构图,而是缺少“什么时候继续、什么时候停止、什么时候回滚”的运行决策。
弱上线说明: - 放在 feature flag 后面发布。 可评审上线说明: - 先对内部账号启用。 - 24 小时内错误率不超过基线 1.2 倍,再扩大到 10% 工作区。 - 如果重复合并任务超过每小时 3 次,停止 rollout。 - 回滚方式:关闭 contact_dedupe_v2;不需要回滚 schema。
这种写法能把“感觉有风险”变成团队可以接受或拒绝的清单。评审因此变短,责任也更清楚。
可复制产物:Spec-First 启动块
团队第一次在真实变更里试 Spec-First 时,可以先用这段。它故意保持短,方便放进工单或 PR。
Spec-First 启动块:如何编写技术规格——模板、示例与免费生成器 本次要做的决策: - 用一个真实变更验证 Spec-First 是否能减少理解偏差和返工。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 范围边界:评审者必须能拒绝不清楚的目标、缺失的非目标,以及没有证据的验收标准。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
旗舰使用路径
这是 Spec Coding 用来承接「技术规格起草」主题的核心参考页之一。建议把它放到真实工单、PR 或发布评审里使用,而不是只当背景文章阅读。
- 适合从这里开始:一个改动超过普通工单,但又不需要笨重设计文档。
- 建议复制:完整规格骨架和决策记录部分。
- 需要附上的证据:编码前填好责任人、依赖、边界情况、发布和测试证据字段。
- 搭配使用:功能规格生成器 与 功能规格模板。
旗舰页使用路径: - 在计划或评审时打开本文。 - 把对应产物复制到工单或 PR。 - 用自己的系统、责任人和失败模式替换示例值。 - 如果证据行仍为空,就不要进入实现。
编辑复核记录
复核日期:2026-04-29。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入Spec-First 开发主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
本文讲解的是面向软件交付团队的技术规格编写方法。示例均为工程场景演示,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:我们如何审核与更新文章
- 纠错反馈:联系编辑