如何为AI编写功能规格说明
编程智能体正在改变工程 effort 创造最大价值的环节。现在更多的杠杆作用集中在实现开始之前。结果的质量不再取决于代码输入的速度,而更多地取决于功能定义的清晰度、行为固定的精确度,以及实现开始后留给解释的空间有多小。
这是一个好的转变。它将注意力转向那些本应始终重要的部分:显式契约、确定性行为、精确边界,以及证明功能按预期工作的验证。在这种环境下,功能规格说明不再是一份软性的规划文档。它本身成为工程表面的一部分。
编程智能体需要的不仅仅是一个功能名称和请求格式。它需要一个清晰的命令边界。规格说明必须定义决策、相关的事实上下文、验收条件以及成功时产生的事实。命令上下文一致性将其转化为一个精确的工程表面。边界位于决策本身,而不是模糊的用例描述中。一旦这个边界明确,功能就不会再溶解到共享逻辑中,实现也会变得更加直接。
在本文中,我将讨论是什么让功能规格说明可执行、哪些部分必须在实现开始前明确,以及提示词应该如何指向这些决策而不是试图自己承载它们。这是智能体编程中最实用的方面:不是生成更多代码,而是在代码生成之前做更多工程工作。
1、什么让功能规格说明可执行
如果功能规格说明定义了实现本需要自行发明的行为,那它就是可执行的。它定义了入口点、有效输入、无效输入、需要做出的决策、决策所依赖的上下文、成功决策的结果、失败案例以及验证契约的测试。
定义必须精确。规格说明必须定义什么触发功能以及什么构成成功的结果。它还必须以足够的精度定义请求的形式,以确保验证是确定性的。必填字段、可选字段、修剪、规范化、格式、范围和语义检查必须包含在契约中。如果这些规则没有明确定义,实现就会从猜测开始。
规格说明必须定义功能被允许做出哪些决策、哪些现有事实与该决策相关,以及如果决策成功会产生什么事实或结果。它还必须定义什么构成重复输入。重新创建、拒绝或将其视为幂等重复是不同的行为。只有定义了这种行为,功能才算完全明确。
可执行功能规格说明的核心是一条固定路径:触发器、契约、上下文、决策和定义的结果。
外部失败契约也必须明确。格式错误的请求、无效的业务输入、冲突和内部运行时失败是不同的结果,需要不同的含义。一旦记录下来,实现路径就会变得更小。同样适用于验证。一个强大的功能规格说明已经暗示了它的证明:成功、无效输入、冲突或重试行为,以及边界情况。
2、简单示例:open_account
open_account 规格说明之所以可执行,是因为它在实现通常会开始偏离的点上固定了行为。第一个点是边界。
## 接口形态
- 方法和路径:POST /accounts
- 成功状态:201 Created
- 失败状态:400 Bad Request, 422 Unprocessable Entity, 500 Internal Server Error
这足以阻止功能扩散到多个端点或未记录的结果中。操作从一个 HTTP 边界开始,返回一组定义的状态。实现不再需要决定功能如何暴露。
第二个点是输入契约。
#### date_of_birth
- 必填
- ISO 日期格式:YYYY-MM-DD
- 必须可解析为日历日期
- 必须是过去的日期
- 申请人必须年满 18 岁
#### residential_address.country_code
- 必填
- 修剪后恰好为 2 个字母字符
- 存储为大写
现在功能不再只是一个粗略的需求规格说明。它成为一个确定性契约。这些规则定义了语法分析、语义有效性、规范化和存储格式。编程智能体没有空间去发明关于年龄、日期处理或国家代码规范化的行为。
第三个点是决策和重试行为。
## 业务规则
1. 系统生成 account_id。
2. 账户以 open 状态开启。
3. 账户以 EUR 货币开启。
4. 初始余额为零。
## 幂等性和重复策略
此功能不是幂等的。
请求不会通过申请人姓名、出生日期、地址或 government_id 去重。
在此示例中,重复的合法请求可能会开启多个账户。
本节阐明了通常被隐含的的行为。调用者不指定账户 ID;功能生成它。重复相同的请求不会触发隐藏的重复规则;相反,会创建一个新账户。一旦这些决策被记录,实现就不再需要猜测重复输入的含义。
第四个点是失败契约。
### 400 Bad Request
- INVALID_JSON
- INVALID_FIELD_TYPE
- INVALID_DATE_FORMAT
### 422 Unprocessable Entity
- FIRST_NAME_REQUIRED
- LAST_NAME_REQUIRED
- DATE_OF_BIRTH_INVALID
- APPLICANT_MUST_BE_ADULT
- ADDRESS_REQUIRED
- COUNTRY_CODE_INVALID
- GOVERNMENT_ID_REQUIRED
### 500 Internal Server Error
- INTERNAL_ERROR
这将格式错误的输入、无效的业务输入和内部运行时失败分开。这使得外部行为更小更清晰。实现遵循契约,而不是在过程中发明错误映射。
最后一个点是验证。
## 测试用例
1. 正常路径:有效的申请人数据返回 201 和生成的 account_id。
2. 验证失败:空白的 first_name 返回 422 FIRST_NAME_REQUIRED。
3. 验证失败:格式错误的 date_of_birth 返回 400 INVALID_DATE_FORMAT。
4. 边界情况:第二个相同的请求创建第二个账户,因为此功能不是幂等的。
5. 内部失败:模拟的持久化失败返回 500 INTERNAL_ERROR。
这些测试是契约的一部分。它们定义了实现必须证明的内容。这就是规格说明可执行的原因。它在代码生成之前固定了边界、输入规则、决策行为、失败契约和证明路径。
3、提示词不应承载功能
一旦规格说明固定了行为,提示词就可以保持操作性。它的工作不再是描述功能本身。它的工作是将智能体指向契约、当前事实来源、本地实现边界和所需验证。这是一个更小更可靠的角色。
这里有一个提示词示例:
使用 $feature-slice-rust 和 $factstore-usage 来实现功能切片:<feature-name>。
首先检查 factstore 仓库并使用其当前 API 作为事实来源。不要发明包装器或 CRUD 抽象。
在现有的 Rust 服务中实现一个小型端到端功能。将其保持在 `src/features/<feature-name>/` 下本地,保持 IO 显式,如果功能有多个关注点则使用小的内部分割,避免 HTTP 动词文件名,并避免通用的技术角色。
直接使用 factstore 进行写入。保持启动/配置更改最小化。不要添加投机性抽象。
验证方式:
- cargo check
- cargo test
- cargo fmt --check
- cargo clippy -- -D warnings
这种区别很重要,因为如果提示词试图覆盖整个功能,它就会成为第二个更弱的规格说明,以浓缩形式存在。这创造了两个事实来源。一个在功能规格说明中,另一个在提示词中。它们很快会分道扬镳,实现最终遵循更容易满足的来源。因此提示词应该保持狭窄和程序性。规格说明应该描述行为。
技能定义了实现必须遵守的结构规则。规格说明定义了功能,提示词定义了如何在项目中执行它。当这三部分保持分离时,智能体几乎没有即兴发挥的空间,生成的代码在第一次尝试时就会变得更小更接近预期行为。
4、审计收紧规格说明
功能规格说明可能已经很强,但在边缘仍留有足够的漂移空间。这在 open_account 示例中立即显现出来。主要行为足够清晰可以实现,但第一次通过时仍在契约未完全确定的地方留下了外部行为开放。存在最大长度规则,但这些情况的失败映射不够明确。内部失败行为也必须成为外部契约的一部分,而不是保持为实现细节。
关键不在于第一次就得到完美的规格说明。关键在于将剩余的模糊性转移到一个可见、可审查且易于收紧的地方。一旦实现根据书面契约进行审计,规格说明中的薄弱环节就不再隐藏在工作代码后面。
这也改变了审计的角色。审计不仅检查代码是否编译、端点是否存在或测试是否通过。它检查实现的行为是否与书面契约完全匹配。这包括未记录的状态、未记录的错误代码、缺失的测试、薄弱的边界情况定义,以及代码不得不发明行为的情况,因为规格说明没有首先确定它。
这是智能体编程中最有用的转变之一。实现不再将模糊的工程决策隐藏在代码审查或后续清理中。审计将它们推回契约所属的地方。一旦发生这种情况,下一次实现通过就会变得更小、更直接,并且更少依赖于解释。
因此,功能规格说明通过实现和审计不断改进,直到契约足够紧密,代码几乎没有即兴发挥的空间。这就是工程工作真正向上移动的时刻。代码遵循契约。契约承载艰难的决策。
5、结束语
你可以在这里查看完整示例。
编程智能体将工程 effort 向上转移。更少的价值存在于输入代码中。更多的价值存在于将功能定义得足够紧密,使实现可以遵循契约而不是发明行为。这就是可执行功能规格说明所提供的:固定的边界、确定性规则、明确的结果和证明路径。
一旦这些决策被记录下来,提示词可以保持小型,实现可以保持本地,审计可以保持严格。代码不再需要承载未解决的行为。契约已经完成了这项工作。
功能规格说明越精确,实现需要猜测的就越少。
干杯!
原文链接: How to Write Feature Specs That Coding Agents Can Actually Implement
汇智网翻译整理,转载请标明出处