技术规格文档怎么写(模板与示例)

技术规格说清楚一段软件要做什么、不做什么,以及团队怎么验证它做对了。这篇文章列出技术规格文档需要的章节,带你看五个示例,从半页纸的配置变更到数据迁移,最后给出一份可以直接复制的模板。

statusreviewedpublished2026-04-08last-review2026-11-10read36 分钟author编辑部policy编辑政策

技术规格是什么?

技术规格(technical specification,也叫 tech spec 或技术规格文档)是一份书面文档,描述一段软件要做什么、不做什么,以及团队怎么验证它做对了。它由负责这项工作的工程师在动手实现之前写好,让负责构建、测试和运维的人对"完成"有同一个定义。

它不是 PRD(产品需求文档),不是 Jira 卡片上的描述字段,更不是某个人在 Slack 里说"对,差不多就这样"。PRD 论证一个功能值不值得做;技术规格确定到底做成什么样。它是定义功能的人、实现功能的人和测试功能的人之间的一份书面契约。

一份好的 spec 的核心特征是可测试性。文档中的每条陈述都应该指向一个可观测的结果——无论是 QA 工程师、自动化测试脚本还是看演示的产品经理,都能不问作者就判断对错。如果 spec 里的某句话无法被测试,那它就是一条观点,不是规格。

长度跟着风险走。配置变更可能半页就够,带数据迁移的跨服务功能可能要五页。判断标准是:一个从未见过代码库的合格工程师,读完 spec 后能独立实现功能,并通过所有验收标准,期间不需要问任何澄清性问题。大多数 spec 离这个标准还差一点,但它值得作为目标。

补充一点术语:在软件之外,"技术规格"也常指产品规格书,比如零件或设备的尺寸、材料、公差和额定参数。本文讲的是软件规格。如果你想了解为什么要先写规格,可以看 什么是 Spec-First 开发?。

技术规格文档应该包含哪些内容

七个核心章节适用于每一份 spec,从半页纸的配置变更到跨团队的大功能都一样。更大的改动再加上设计章节:系统怎么构建、哪些数据和接口会变、怎么发布和观察。用第二张表判断你的改动需要哪些;不适用的章节直接删掉,比填一个"无"更好。

核心章节(每份 spec 都要有)

章节回答的问题
功能名称我们说的是哪一个改动?
目标这次交付什么结果?怎么知道达到了?
非目标这个版本有意不做什么?
验收标准哪些可观测的行为能证明它做对了?
边界条件空值、重复、并发、超大或无权限的输入会怎样?
产出 / 交付物完成时会有哪些具体产物?
依赖哪些东西必须先就位?谁负责?

设计章节(适用时再加)

章节回答的问题什么时候加
背景现在是什么样?为什么要改?团队外的评审者需要上下文
方案设计哪些组件会变?它们怎么交互?改动涉及不止一个组件或服务
API / 接口变更哪些端点、事件或函数签名会变?请求、响应和错误长什么样?其他团队或客户端依赖的接口发生变化
数据模型与迁移哪些表或字段会变?已有数据怎么迁移?有 schema 变更或数据回填
非功能需求延迟、吞吐、可用性、限额和成本,用数字写负载、成本或 SLA 可能受影响
安全与隐私谁能调用或看到它?哪些个人数据会被存储、记录或导出?涉及认证、权限或个人数据
备选方案哪些方案被否了?为什么?评审者很可能会问"为什么不用 X?"
测试计划每条验收标准由哪个测试来证明?光靠单元测试验证不了验收标准
发布、监控与回滚怎么发布?观察什么?怎么撤回?生产风险不是微不足道
待决问题还有什么没定?谁来定?什么时候定?还有未决事项(开工前这一栏应该清空)

如果设计章节的篇幅开始压过其他部分,就把它们拆到单独的设计文档里,在 spec 中链接过去。spec 仍是 QA 据以测试的那份短文档,架构讨论则有自己的空间。两者的区别见下文技术规格、设计文档与功能规格。

如何写好 7 个核心章节

这七个章节是任何技术规格的底线。上面的设计章节是在较大改动时额外加上的,模板里两部分都有。七个里少了任何一个,实现过程中就会有人用自己的假设去填补空白。

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 alice@example.com 创建,When 同一账户下已存在该 email 的联系人,Then 新联系人不被创建,已有联系人的 updated_at 被刷新。"

5. 边界条件(Edge Cases)

边界条件是主验收标准没有覆盖、但生产环境中一定会出现的输入、状态或场景:空字段、并发写入、大小写差异、权限边界、时区效应。对每个边界条件,写明输入条件和预期行为。别让工程师在实现过程中自己去"发现"——那就是 bug 被发布出去的方式。

示例:"Email 比较不区分大小写。'Alice@example.com' 和 'alice@example.com' 被视为重复。"

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 "alice@example.com"
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.
  "Alice@Example.com" and "alice@example.com" are treated as duplicates.
  Implementation: normalize to lowercase before comparison.

- Leading/trailing whitespace in email is stripped before comparison.
  " alice@example.com " matches "alice@example.com".

- 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., "alice+test@example.com").
  Resolution: treat as distinct from "alice@example.com".
  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 大约一页纯文本,写它是一个下午的事,不是一个迭代的事。这点时间能帮团队省下好几天的返工、好几轮"等一下,这种情况该怎么处理?"的代码评审讨论,以及那种因为静默合并了正在跑自动化流程的联系人而引发的生产事故。

CRM 这个例子是一个中等规模的功能。下一节给出四份更短的 spec,覆盖其他类型的改动。

更多技术规格示例

不同类型的改动,spec 的重心也不同。API spec 的篇幅花在接口契约上,数据迁移 spec 花在顺序和回滚上,UI spec 花在状态和限制上。下面四个示例刻意写得很短,每个后面附一句说明:如果没写那一部分,评审时会漏掉什么。示例正文保留英文,方便直接复制。

示例 1:小型配置变更(管理员会话超时)

小改动也需要目标、非目标和可测试的验收标准,但不需要设计章节。

# Feature: Admin session idle timeout

## Goal
Sign out admin users after 30 minutes without activity, to close
finding SR-14 from the 2026 access-control review.

## Non-goals
- No change to session length for non-admin users.
- No "remember this device" option.

## Acceptance Criteria
Given an admin user who has sent no request for 30 minutes
When they send the next request
Then the response is 401 with error code session_expired
     and the UI redirects to /login?reason=timeout.

Given an admin user who is active at least every 29 minutes
When 8 hours pass
Then the session is still valid (the 12-hour absolute limit is unchanged).

## Edge Cases
- Background polling (GET /api/notifications) does not count as activity.
- Two open tabs share one session; activity in either tab resets the timer.

## Deliverables
- ADMIN_IDLE_TIMEOUT_MINUTES=30 in config, read at startup.
- Idle check in the auth middleware.

## Dependencies
None.
    

每条验收标准对应一个测试。后台轮询那条规则是评审者一定会追问的:没有它,一个开着的仪表盘标签页会让会话永远不过期。

示例 2:API 端点(发票 CSV 导出)

对 API 改动来说,接口契约就是 spec:请求、响应、状态码和限制。"返回发票列表"没法测试,响应的具体结构才可以。

# Feature: Invoice CSV export API

## Goal
Let finance teams export a month of invoices as CSV without opening a
support ticket (about 40 export tickets a month today).

## Non-goals
- No PDF or XLSX export.
- No ranges longer than 31 days per request.
- No scheduled or recurring exports.

## API
POST /v1/invoice-exports
  Body: { "from": "2026-10-01", "to": "2026-10-31", "status": ["paid", "open"] }
  202 { "export_id": "exp_8f2c", "state": "queued" }
  400 invalid_range       to < from, or range > 31 days
  403 forbidden           caller lacks the billing:read scope
  429 too_many_exports    more than 3 queued exports for the account

GET /v1/invoice-exports/{export_id}
  200 { "state": "queued" | "running" | "ready" | "failed",
        "download_url": "...", "expires_at": "..." }   (url only when ready)
  404 not_found           export belongs to another account

## Acceptance Criteria
Given an account with 12,000 invoices issued in October
When the user exports 2026-10-01 to 2026-10-31
Then the export is ready within 60 seconds and the CSV has
     12,000 rows plus a header row, in the column order under Output.

Given an export that is ready
When 24 hours have passed
Then download_url returns 410 and the file is deleted from storage.

## Edge Cases
- Amounts are decimal strings in the invoice currency ("1234.50"),
  never floats; the currency column is always present.
- Customer names starting with =, +, - or @ are prefixed with '
  so spreadsheets do not evaluate them as formulas.
- No matching invoices: the export is ready with only the header row.

## Output
- CSV columns: invoice_id, issued_at, customer_name, currency,
  amount_due, amount_paid, status
- Signed download URL with a 24-hour expiry.

## Dependencies
- Storage bucket with a 24-hour lifecycle rule. Owner: platform team. Available.
    

状态码和 31 天的限制是客户端开发者需要据以开发的信息。公式前缀那条规则,通常要等到有人打开一个以 = 开头的单元格之后,才会由安全评审补上。

示例 3:数据迁移(统一手机号格式)

迁移 spec 的重点是顺序和可回退性。写清楚分几个阶段、阶段之间验证什么,以及从哪一步开始,回滚不再是改个配置那么简单。

# Change: Store customer phone numbers in E.164 format

## Goal
Every customer phone number is stored in E.164 format (+14155550123),
so the SMS provider stops rejecting sends (2.1% failed in October).

## Non-goals
- No phone validation at signup (separate spec).
- No deduplication of customers who share a number.

## Data Model
customers.phone             existing, free text, kept until phase 4
customers.phone_e164        new, nullable varchar(16)
customers.phone_parse_err   new, nullable text

## Phases
1. Expand: add both columns; new writes fill phone and phone_e164.
2. Backfill: parse existing rows in batches of 5,000, using the
   customer's billing country as the default region.
3. Switch reads: the SMS sender reads phone_e164 only.
4. Contract: drop customers.phone after 14 days in phase 3
   with no reads of the old column in the query log.

## Acceptance Criteria
Given the backfill has finished
When a row has phone_e164 = null and a non-empty phone
Then phone_parse_err says why ("no digits", "ambiguous region")
     and the row appears in the weekly data-quality report.

Given phase 3 is live
When the SMS sender sends to a customer
Then it uses phone_e164, skips customers where it is null,
     and logs sms_skipped_no_phone for each skip.

## Verification
- Before phase 3: at least 98.5% of non-empty phones parsed.
- Manual spot check of 200 random rows against the original value.

## Rollback
- Phases 1-3: point reads back at customers.phone (config flag).
- Phase 4 is the point of no return and needs sign-off from the data owner.
    

有了阶段列表,这份 spec 才可评审。没有它,"迁移手机号"往往会变成一次原地改写整列的部署,没有退路。

示例 4:UI 功能(订单列表的保存筛选)

UI 工作的 spec 写状态和限制,而不是像素。布局放在设计稿里链接过去,spec 负责说清楚出错时会怎样。

# Feature: Saved filters on the orders list

## Goal
Support agents reopen their usual order views in one click instead of
re-entering three to five filters each time.

## Non-goals
- No sharing saved views with teammates in v1.
- No saved views on other list pages.

## Acceptance Criteria
Given an agent with filters status = refund_requested and age > 48h applied
When they click "Save view" and enter the name "Old refunds"
Then "Old refunds" appears under Saved views, and clicking it
     reapplies both filters, including after the agent signs out and back in.

Given an agent who already has 20 saved views
When they open the Save view dialog
Then the Save button is disabled with the hint "You can save up to 20 views".

## States
- Empty: "No saved views yet" next to the Save view button.
- Load error: the orders table renders without saved views;
  the error never blocks the table.

## Edge Cases
- Duplicate name: inline error "A view with this name already exists".
- A saved view references a status value that was removed: the view
  opens, the stale value is dropped, and a notice names it.
- Names are 1 to 40 characters after trimming whitespace.

## Design
Figma: Orders / Saved views, frames 12-15.
    

UI 工作的评审讨论大多集中在边界条件上。20 个视图的上限和失效筛选值的处理都是产品决策,写下来才不会在代码评审时临时拍板。

还想看两个按模板填写的示例(Webhook 投递 API 和审计日志表),可以看规格模板示例。

技术规格模板(复制即用)

模板分两部分。分隔线以上是核心章节,每份 spec 都要有;分隔线以下是设计章节,按上面的表格保留适用的部分,其余删掉。方括号里是填写指引,填入真实内容时删除,然后在实现之前发给团队做异步评审。

以下是完整的模板(英文,可直接复制使用):

# 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]

[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]

- [Empty input / null value / concurrent write / permission boundary]
  Resolution: [Expected behavior]

## Output / Deliverables
1. [Concrete artifact: API endpoint, database migration, module, etc.]
2. [Feature flag name and default value]

## Dependencies
- [Service, team, or system that must exist before implementation]
  Owner: [team name]. Status: [available / ETA]

---- Design sections: keep the ones that apply, delete the rest ----

## Background
[What exists today and why it needs to change. Link the PRD or
ticket instead of restating it.]

## Proposed Design
[Components that change and how they interact. One diagram or a
short numbered flow is usually enough.]

## API / Interface Changes
[Endpoint or event, request and response shapes, status and error
codes, backward compatibility for existing clients.]

## Data Model and Migration
[New or changed tables and fields. Migration phases, backfill, and
the point after which rollback is no longer a config change.]

## Non-functional Requirements
[p95 latency, throughput, availability, rate limits, cost. Numbers,
not adjectives.]

## Security and Privacy
[Who can call or see this. Personal data stored, logged, or exported.]

## Alternatives Considered
[Option, and why it was rejected. One or two lines each.]

## Test Plan
[Which test proves each acceptance criterion: unit, integration,
end-to-end, or manual.]

## Rollout Plan
Stage 1: [Limited rollout: internal or percentage-based]
  Observation: [Duration]
  Success signal: [Metric or absence of failure]
  Stop condition: [Threshold that halts the rollout]

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]

## Monitoring
[Metric or log line, alert threshold, and who is paged.]

## Open Questions
[Question, owner, decide-by date. Empty before implementation starts.]
    

下载:technical-specification-template.md

小型配置变更只保留核心章节、删掉分隔线以下的全部内容即可,参见示例 1。API 和数据库相关的工作,可以用 API 规格模板和数据库规格模板,它们在接口契约和迁移上写得更细;架构需要单独成文时,用设计文档模板。

如果你不想从空白文件开始,规格生成器可以根据功能名称和一段简短描述生成核心章节的初稿。它在浏览器里运行,不需要注册;生成结果当作待修改的草稿,而不是定稿。

如何编写验收标准(Given/When/Then)

验收标准是任何技术规格中最重要的部分。它们是"产品应该做什么"和"QA 能验证什么"之间的桥梁。Given/When/Then 格式之所以好用,是因为它强制你写清三件事:前置条件(Given)、触发动作(When)、可观测的结果(Then)。三者缺一,标准就是不完整的。

最常见的失败模式是写出看上去具体、实际上模糊的标准。"系统应正确处理重复数据"听着像一条需求,但 QA 根本不知道该测什么。下面列出五组反面和正面示例。区别永远是一样的:好的版本给出了具体的输入、动作和输出。

反面示例

"重复联系人应该被正确合并。"

正面示例

"Given 新联系人使用 email alice@example.com 创建,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 响应体),否则不要引用内部方法名、数据库列名或实现细节。

怎么写真正挡得住范围的非目标

非目标是一件具体的事:本次工作不会做,哪怕一个合理的人有理由以为会做。后半句才是检验标准。"我们不重做计费系统"如果本来就没人指望,写了也没用;"我们不处理部分退款,即便它和全额退款流程看起来很像"才是真正的非目标,因为两周后一定有人想把它塞进范围。好的非目标清单,会在有人问"要不要顺便把 X 也做了"之前就给出答案。上面的 CRM 示例列了六条,每一条都对应一个本来会在代码评审里冒出来的问题。

大多数非目标不是凭空想出来的,而是从已经发生过的对话里提炼出来的:

非目标的三种类型

按"为什么不在范围内"给每条非目标归类。不同类型需要补充的信息不同,漏掉这部分信息,它迟早会以范围争论的形式回来。

类型含义必须写明示例
延后(Deferred)真实存在、大概率会做,但不在本次发布向前的指针:工单、里程碑或版本号"部分退款:计划在 v2 实现(JIRA-1234)。全额退款流程的实现要保证 v2 扩展时不必重构退款表。"
否决(Rejected)认真考虑过,决定不做理由,避免实现中途被反复翻案"短信重置密码:当前短信服务商的送达可靠性达不到重置密码的要求。除非更换服务商,否则不再讨论。"
不属于本域(Out of domain)归属另一个系统或团队负责团队,让升级路径清楚"Auth token 刷新逻辑:归身份团队负责。本功能原样使用现有刷新行为。"

被排除的事项还要有去处:下一份 spec、backlog、明确放弃,或者交给产品重新决策。延后和否决两类最好再写一个重新评估的条件——哪个指标、哪类客户请求、哪个发布阶段或哪个依赖完成之后,它可以回到范围内。否则非目标只是把争议往后推。

非目标要写得和它保护的契约一样精确。"不做复杂功能"挡不住任何东西;好的非目标会点名具体的诱惑——不改哪个端点、不迁移哪张表、不支持哪类用户、不处理哪条异常路径,能直接拿来对照 diff 和测试计划:

Weak non-goal:
- We will not redesign billing.

Useful non-goal:
- We will not change subscription renewal dates.
- We will not migrate existing invoices.
- We will not support annual-to-monthly downgrades in this release.
- We will not expose proration preview in the public API.

容易被忽略的写法

  • "X 以后可能会加。"——软绵绵,等于邀请范围蔓延。
  • "X 不是优先级。"——优先级会变,承诺不会。
  • "看看能做到哪儿。"——这不是非目标,也算不上计划。

站得住的写法

  • "本次发布不包含 X。"
  • "X 有意不在本次范围内,单独在 [链接] 跟进。"
  • "我们考虑过 X,因为 Y 决定不做。"

每条非目标都配一条拒绝验收标准

如果调用方仍然可能去尝试被排除的行为,规格里就要有一条验收标准说明系统怎么拒绝。只写"我们不做部分退款"不够:没有对应标准,用户的尝试会落到一个未处理的错误上,最后变成一张客服工单;有了它,用户得到的是一个清楚的、QA 可以测试的拒绝:

Given a user attempts a partial refund via the API
When the request is processed
Then the endpoint returns 400 with error code unsupported_refund_type
     and a link to the refund documentation.

这类拒绝路径很容易被漏掉,AI 生成的实现尤其如此,所以要把它们当作正式的验收标准来写(参见 AI 编程的验收标准)。

写多少条,什么时候可以改

Meta non-goal: This spec does not describe implementation.
Architecture and data structures are engineering decisions. Changing
them does not require a spec change unless observable behavior changes.

PRD 和技术规格有什么区别?

PRD(产品需求文档)和技术规格回答的是两个不同的问题。PRD 回答"值不值得做",读者是决定优先级的人;技术规格回答"既然要做,它到底做成什么样",读者是写代码、写测试、写上线计划的人。把两者合成一份文档的团队,往往得到一份两头不讨好的东西:高管不想看 API 契约,工程师要翻过一堆业务论证才找到验收标准,产品定调和技术细节又以不同速度过时,最后整份文档都没人信。

PRD技术规格
回答的问题值不值得做,排在什么优先级到底做成什么样,怎么判断做对了
通常由谁写产品经理技术负责人或团队里的资深工程师
主要读者产品、工程负责人、设计,有时是高管工程、QA、运维
支撑的决策上不上、排第几写什么代码、跑什么测试
主要内容客户问题、问题真实存在的证据、概念层面的方案、预期影响、大致范围和时间线目标、非目标、验收标准、边界条件与失败模式、API 和数据模型变更、带数字的非功能需求、上线与回滚
上线之后变成历史背景变成运维和后续修改这个功能的参考文档

两份文档是咬合在一起的:规格引用 PRD 来说明动机,但不重抄一遍;PRD 也不去规定架构。下面是同一个结账功能在两个层面上的写法:

PRD 写的是技术规格把它落成
问题:用户在付款环节放弃结账。目标:让付款环节的结账放弃率降低 20%。
方案:增加"已保存的支付方式"。已保存的支付方式在多个会话间持久存在,以带 user_id 和 tokenized method_id 的 DB 记录为准;令牌化走支付服务商(PCI DSS 要求);UI 中默认方式预先选中。
—边界条件:结账页打开期间某个已保存方式被删除 → 刷新列表并内联提示。
影响:结账完成率提升 5%-8%。非功能:结账 API p95 < 400ms;令牌保存成功率 99.9%。
—按用户百分比通过 feature flag 灰度;回滚就是翻转 flag,令牌留在 DB 里但不再使用。

空着的格子正是重点:边界条件和回滚本来就不该出现在 PRD 里,而它们恰恰是规格要拍板的东西。

两份文档要共用一张词表。PRD 用用户语言,规格用系统语言,中间最好写一份映射。比如用户看到的"退款处理中",在 API 里是 pending_provider_confirmation,数据库字段是 refund.status,客服后台显示 Waiting for provider。命名不一致会一路传导到 UI 文案、客服话术和测试用例;映射写清楚,这些地方的歧义会少很多。

一句话自检。对任何一份文档都可以问:谁会根据它做决策,做什么决策?如果是产品 leadership 决定要不要排期,它是 PRD;如果是工程决定写什么代码、QA 决定跑什么测试,它是技术规格;如果答案是"没人,只是记一下",那两种文档都没在干自己的活。

只需要一份的时候。小功能不必两份都写。"为什么做"有争议,就写 PRD;"到底做成什么样"有争议,就写规格。一句话的需求如果动机显而易见,可以直接写规格。如果你手上已经有一份 PRD,后文的PRD 转规格步骤会讲怎么转。

技术规格、设计文档与功能规格

这三个名字有重叠,不同团队的用法也不一样。下面这种划分在实践中最好用:

文档回答的问题通常由谁负责
功能规格(functional specification)用户能做什么:页面、流程和业务规则,从外部视角描述产品经理或业务分析师
技术规格(technical specification)系统做什么、怎么验证:行为、接口、数据、限制、发布负责这项工作的工程师
设计文档(technical design spec)怎么构建、为什么选这个方案:架构、权衡、备选方案工程师,由资深工程师评审

在小团队里,一份文档常常同时承担这三种角色,这也是上面的模板把设计章节设为可选的原因。只要验收标准始终描述行为,这样做没有问题。提到内部类名或队列的标准应该放进设计部分,而不是放在 QA 用来测试的清单里。

把 PRD 转成技术规格

大多数规格都是从一份 PRD 开始的。转换的原则是保留用户结果、去掉歧义:把"提升""支持""优化"这类方向性的词改写成可观测的结果,把隐含的范围边界改写成明确的非目标。顺序是先 PRD、后规格,不要并行。如果规格评审发现 PRD 的范围估计有误,就回去更新 PRD、让产品重新批准,而不是在规格里悄悄消化掉差异。

先确认 PRD 能不能转

强行转换一份不完整的 PRD,会得到结构正确、内容错误的规格——验收标准在形式上可测试,实质上却不对,因为底层意图从来没被定下来。出现下面任何一种情况,就先停下来,把一份简短的缺口清单退回给 PM:

转换的七个步骤

  1. 提炼最重要的用户结果——要改善的是用户的处境,而不是系统本身。这些写进目标。
  2. 把本次发布不解决的问题写成非目标。
  3. 把每条需求改写成 Given/When/Then 验收标准。
  4. 补上 API、数据库和 UI 的影响说明,标出向后兼容的假设。API 规格模板和数据库规格模板里有对应字段。
  5. 列出边界条件:非法输入、重复、并发、权限路径。常见类别可以对照边界条件清单。
  6. 给每条验收标准对应一个自动化或人工测试。
  7. 定义灰度和回滚:feature flag、监控指标、失败阈值。

开工前再过一遍门槛:每条验收标准都能明确判定通过或不通过;没有任何需求依赖未写明的行为;每个外部依赖都有负责人和降级行为;有风险的改动都有监控和回滚触发条件。还有未知项,就先改规格,别急着写代码。

常见 PRD 句式与规格转写对照

PRD 句式规格目标非目标验收标准
"支持用户管理通知偏好"用户可以单独开关各类通知,不影响其他账户设置。本次不做批量重置;移动端推送推迟到 v2。Given 已登录用户,When 把 email_marketing 切换为关闭,Then preferences.email_marketing = false 被持久化,且下次营销活动不给该用户发邮件。
"提升搜索相关性"在现有 Elasticsearch 索引内,包含全部查询词的结果排在部分匹配之前。本次不引入 ML 排序模型;不做同义词扩展。Given 查询"年度报告 2024",When 返回结果,Then 包含全部三个词的文档排在只包含一两个词的文档之前。
"支持基于团队的访问控制"工作区管理员可以把用户分配到命名团队,并按团队限制资源可见性。本次不做跨团队共享;不支持团队层级或子团队。Given 属于 A 团队的访客用户,When 请求 B 团队的资源,Then 返回 403,且响应中不包含任何资源数据。

交接中的角色分工

角色负责评审并签字
产品经理PRD、用户问题、成功指标规格中的目标和非目标,确认与用户意图一致
技术负责人 / 工程师规格本身:Given/When/Then 验收标准、PRD 里看不到的技术边界条件、依赖和契约影响PRD 的可实现性
QA可测试性每条验收标准在当前测试环境里都能验证,错误路径和权限路径都已覆盖

三个角色都签字后,规格才能进入实现。这套分工最常见的两种走样:PM 为了省时间替工程写规格,结果通篇散文、没有验收标准,行为靠工程师在实现时临时编;或者工程师因为"PM 太忙"替 PM 写 PRD,结果只有功能描述、没有用户问题。在规格顶部留一份简短的交接记录,写清谁决定了什么,后来的评审者才分得清哪些是产品决策、哪些是工程约束:

PRD 来源:支持联系人 CSV 导入
规格负责人:Growth engineering
仍未解决的问题:无

产品决策:
- v1 只支持 1,000 行以内导入。
- 同一个文件内重复 email 时,返回行级错误,不让整份文件失败。

工程决策:
- 上传校验通过后进入异步导入。
- v1 不覆盖已有联系人,只记录冲突行。

QA 证据:
- fixture: valid_1000_rows.csv
- fixture: duplicate_email_rows.csv
- 手工检查:进度状态、失败行下载、导入完成通知

此后任何范围变化都更新规格本身,而不是只改聊天记录或会议纪要。PRD 之后如果新增目标,先判断它是否属于当前版本:属于,就补进验收标准和测试计划;不属于,就写进非目标或下一阶段。

为什么要在写代码前写 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 周事故响应、热修复、客户沟通、复盘

技术规格中最常见的 5 个错误

写 spec 是一项技能,和所有技能一样,有些做法看上去没问题但会持续引发问题。以下是 spec 评审中最常见的五个错误。

1. 验收标准描述的是实现,而不是行为

像"dedup_service 调用 findByEmail 并合并记录"这样的标准,描述的是系统内部如何运作。对 QA 毫无用处,因为他们从系统外部观察不到内部方法调用。验收标准应该描述可观测的输入和输出:用户看到什么、API 返回什么、操作完成后数据库里出现什么。实现细节属于代码,不属于 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 应说明如果它延期会对时间线产生什么影响。这不是悲观——这是计划。

技术规格常见问题

技术规格应该写多长?

常规功能一到三页纯文本。配置变更半页就够;带数据迁移的跨服务改动可能要五页。再长的话,通常说明它包含了不止一个交付单元,应该拆分,或者把架构讨论移到单独链接的设计文档里。

技术规格由谁来写?

由负责这项工作的工程师来写。产品评审目标和非目标是否贴合用户问题,QA 评审验收标准是否可测。三方都签字后,spec 才算可以进入实现。

技术规格和功能规格有什么区别?

功能规格从外部描述用户能做什么:页面、流程和业务规则。技术规格描述系统做什么以及怎么验证:行为、接口、数据变更、限制和发布方式。小团队常把两者合在一份文档里。

技术规格和设计文档是一回事吗?

不完全是。技术规格定义行为和验证方式;设计文档解释系统怎么构建、为什么选这个方案。很多团队把两者放在同一个文件里,设计章节设为可选,等架构讨论变长时再拆开。

这个模板能在 Google Docs 或 Word 里用吗?

可以。在 Google Docs 里,先在“工具 → 偏好设置”中开启 Markdown,再用“编辑 → 从 Markdown 粘贴”,标题层级会保留。Word 不支持直接粘贴 Markdown,可以用 Pandoc 转换(pandoc spec.md -o spec.docx),或者按纯文本粘贴后手动套用标题样式。

什么时候写技术规格?

在问题和优先级达成一致之后(通常体现在 PRD 或工单里),在开始实现之前。异步评审,解决待决问题,并保持更新:实现过程中的范围变化要写回 spec,而不是留在聊天记录里。

中文工单里的写法建议

如果团队已经用 Jira、飞书项目、禅道或 TAPD,不必新建一套很重的文档系统。把目标、非目标、验收标准、联调接口、灰度策略和回滚信号放在工单顶部,再把详细 spec 链接到同一个位置,就足够让评审和排期少很多来回确认。

交互式生成规格
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
试用规格生成器

编辑说明

本文讲解的是面向软件交付团队的技术规格编写方法。示例均为工程场景演示,不构成法律、税务或投资建议。