需求拆解与方案设计
把 AI 功能映射到界面、接口、数据和执行流程,写出可以验收的需求。
建议先读:TypeScript 与接口契约
本页内容
从功能描述到可验收需求#
“做一个 AI 助手”不是实现规格。它没有说明用户是谁、输入是什么、成功是什么、失败时怎么办。作为偏前端的全栈工程师,你需要把交互、数据、接口和执行状态连接起来。目标是 L2:能把一句需求拆成一个可实现、可验证的纵向功能,而不是先列一大串技术名词。
前置是系统边界和接口契约。本章以“上传制度文档后回答员工问题”为例,说明产品决策如何影响代码。即使 AI 协助写实现,这些决策仍必须显式记录,否则它会补出看似合理但彼此矛盾的行为。
先定义用户任务和错误成本#
员工想知道报销需要哪些材料,产品目标是让他获得有出处且适用当前制度的说明。这里的成功不是“生成了流畅文字”,而是引用正确文件、没有遗漏关键条件、权限正确、答复时间可接受。错误成本决定是否需要人工复核或限制自动执行。
先写三类样例:正常问题、无资料的问题、权限不允许的问题。再明确哪些需求使用普通代码解决,例如精确查制度版本;哪些需要模型处理,例如自然语言归纳。模型并不是每条路径的必选组件。
把一条需求映射到四层#
| 层次 | 需要确定的内容 | 示例 |
|---|---|---|
| 用户界面 | 用户能看到和操作什么 | 上传、解析状态、回答、引用、重试 |
| 接口契约 | 请求、响应、身份和错误 | 返回 documentId 与 processing 状态 |
| 数据模型 | 要长期保存哪些事实 | 所有人、版本、解析状态、索引版本 |
| 执行流程 | 谁执行,何时完成,失败怎么办 | 后台解析、发布索引、失败可重试 |
如果 UI 需要显示“上次解析失败原因”,后端就必须保存可展示的失败类别。如果需要查看旧回答的出处,就需要记录当时使用的文档版本。原型中的一个小标签可能意味着真实的数据生命周期,而不是只加一个枚举。
用状态表消除含糊#
下面的状态设计是讨论模板,不是任何框架的内置格式。状态描述的是知识资料是否可使用,不能把上传进度百分比混进业务枚举。
uploaded 原文件已保存,等待安排解析
processing 正在解析、分块或建立索引
ready 当前版本已发布,可用于检索
failed 当前处理失败,可查看原因并重试
deleted 已撤销访问,相关索引进入清理流程
每个转移都要定义触发者与前提。客户端可以申请重试,但不能直接把文档改为 ready;后台任务完成后由服务端更新。删除时应先撤销访问,再清理存储,避免异步清理过程中仍能检索到已删除资料。
版本更新还需要明确:新版本处理中,旧版本是否继续可用?如果继续,应把“文档版本”与“当前发布版本”分开。这个选择影响用户体验、一致性、存储和回滚,不能等代码写完再补。
用验收场景约束实现#
下面是一组可复制到需求文件中的验收文本,不需要运行。它帮助测试覆盖用户行为,而不是覆盖某个实现细节。
Feature: 使用有权限的资料回答问题
Scenario: 当前资料包含答案
Given 用户 Alice 可以阅读 ready 状态的文档 A
When Alice 提问文档 A 中明确解释的问题
Then 回答应附带文档 A 的可打开引用
And 引用应指向回答所使用的文档版本
Scenario: 用户没有资料权限
Given 用户 Bob 无权阅读文档 A
When Bob 提问与文档 A 相关的问题
Then 文档 A 不应进入发送给模型的上下文
And 回答与日志不应泄露文档 A 的私人内容
Scenario: 请求中断
Given 回答已经产生部分文本
When 客户端连接断开
Then 已保存文本应标记为不完整
And 页面应允许发起一次明确的新尝试
Given 定义条件,When 定义触发,Then 定义可观察结果。这里没有指定必须使用哪个向量库或 Agent 框架,因为验收需要约束业务,而不是把技术选择当成功标准。涉及模型时,某些标准需要人工评分规则,不能全部化成精确字符串比较。
拆成可以交付的纵向切片#
第一个切片可以只支持一种文本文件:上传、保存归属、后台处理、状态查询、页面展示。第二个切片增加对单文档提问与引用。第三个切片增加多文档检索和版本更新。每个切片都包含必要的权限、失败状态和日志,形成可演示的闭环。
不要按“先写所有表、再写所有 API、最后写所有页面”把风险延后。纵向切片能尽早发现接口不匹配、状态无法展示、检索证据不够等问题。UI 可以先使用契约一致的模拟数据,但要明确模拟服务与真实服务的切换点。
方案记录与范围控制#
方案至少写清选项、选择理由和代价。例如第一版使用关系数据库和简单关键词检索,是因为资料少且需要先验证交互;以后加入向量检索的触发条件是同义问题无法召回,而不是“AI 项目都应该有向量数据库”。
把没有决定的事情单列出来:账号体系、文档权限、保留期限、模型供应商、允许的自动操作。影响安全或数据一致性的未知项需要确认;按钮间距等可逆实现细节可以由工程师按已有规范决定。不要把所有小问题都抛回用户,也不要擅自决定真正的产品语义。
常见遗漏#
只设计成功页,导致首次进入、无资料、解析失败和模型拒答时不知道显示什么。只定义创建,不定义更新和删除,导致旧知识长期残留。只验收正确答案,不验收“不知道”,导致系统在证据不足时仍自信编造。只考虑一个用户,导致权限和并发在后期大面积返工。
需求可以逐步增加,但每一步的状态与边界必须自洽。个人项目也应该写最小需求记录,它能帮助你和 AI 在数天之后恢复上下文。
从一句模糊需求推导出可以开发的规格#
下面完整推导一个案例。它是用于学习的假设产品,不代表某家公司的真实规定。假设需求方说:“把公司的制度上传,让员工问 AI;制度有更新时替换一下,回答要准确。”先不要问用哪个模型,也不要马上建 documents 表。第一步是找出这句话包含的业务承诺。
“公司的制度”可能包含公开制度和仅人事可见的文件。“员工”可能分正式员工、外包和管理员。“替换”可能意味着立即隐藏旧版,也可能意味着新版处理完才替换。“准确”可能意味着回答符合原文,也可能还要符合生效日期与员工所属地区。每个词都会改变查询条件和界面行为。
第一步:识别真正会改变实现的未知项#
不要把所有未知都当成阻塞问题。问题是否需要确认,取决于答案会不会改变业务结果、权限、数据保存或不可逆操作。主题色深浅通常可按现有规范决定;员工能否阅读历史版本则必须在实现前讲清楚。
| 原始说法 | 隐藏的问题 | 本案例采用的决定 | 对实现的直接影响 |
|---|---|---|---|
| 员工都能问 | 是否包含所有文件 | 普通员工只读公开制度 | 检索前过滤资料权限 |
| 管理员上传 | 谁是管理员 | 角色来自登录后的服务端会话 | 不信任请求体中的角色 |
| 替换制度 | 新版处理时旧版怎么办 | 旧版继续服务,新版就绪后切换 | 分开保存当前发布版本与候选版本 |
| 删除制度 | 已生成的回答怎么办 | 撤销资料访问,历史回答保留但引用不可再打开 | 删除影响检索和引用访问,不假装能收回已读信息 |
| 回答准确 | 什么算有依据 | 结论必须被当前有权访问的片段支持 | 记录检索证据并建立人工评分集 |
| 上传成功 | 文件存好还是可问答 | 分成“已上传”和“可检索”两个结果 | 前端分别展示存储状态与解析状态 |
| 文件较大 | 多大、超限怎么办 | 第一版仅支持不超过明确上限的文本文件 | 前后端限制一致,服务端最终执行限制 |
你不必在第一版设计所有未来情形,但必须让选定情形自洽。例如暂不支持历史版本浏览是可以的,仍要记录回答引用了哪个版本,否则更新后无法解释旧回答。暂不支持多租户也是可以的,但不能声称单用户 demo 已验证企业租户隔离。
第二步:把“好用”转成可观察行为#
“员工可以方便地查询制度”无法直接测试。把它改成:“已登录员工输入一个问题后,能看到处理阶段、回答正文和对应资料的文件名;点击引用可以打开当时使用且当前仍有权访问的原文位置。”这时,界面、数据和权限要求就都有了观察点。
还有一些行为不是功能按钮,却属于产品承诺。例如用户关掉页面后文件解析继续执行;同一次上传重试不会生成两个候选版本;没有可支持答案的资料时显示依据不足;模型故障不能显示为“没有相关制度”。这些区别影响用户采取什么行动,不能用一个通用错误气泡替代。
第三步:为第一版写清不包含什么#
范围排除应该解释边界,而不是逃避必要的失败处理。本案例第一版不支持扫描 PDF 的 OCR,不支持自动创建审批单,不支持跨组织共享;但仍然支持上传失败、解析失败、无权限和重复请求。后一组属于已有功能的完整性,不属于可随意推迟的“高级功能”。
好的范围说明能帮助你拒绝无关扩张。例如有人提出“顺便让 AI 自动发送审批邮件”,你可以指出它新增了外部副作用、收件人校验、确认、幂等和审计,不只是添加一个工具函数。把新增成本写清楚,再决定是否进入下一版本。
逐字段建立前端到数据库的映射#
字段映射的用途是防止前端、接口和存储各自使用相似但不等价的词。下面的表可以作为实际开发前的起点。字段名是本案例的约定,不是行业强制标准。
| 页面字段或行为 | 接口字段 | 持久化事实 | 校验与空值规则 |
|---|---|---|---|
| 文档名称 | title |
documents.title | trim 后非空;不把空字符串变成“未命名”而不告知用户 |
| 原始文件名 | originalFileName |
versions.original_file_name | 用于展示,不作为服务器磁盘路径 |
| 文件类型 | mediaType |
versions.media_type | 由服务端检查,不能只相信扩展名 |
| 文件大小 | byteSize |
versions.byte_size | 非负整数;与实际接收字节数一致 |
| 上传人 | createdBy |
versions.created_by | 来自会话,不由表单填写 |
| 可见范围 | visibility |
documents.visibility | 仅允许约定枚举;修改必须鉴权 |
| 当前可检索版本 | activeVersionId |
documents.active_version_id | 首次处理未完成时可为 null |
| 新版处理状态 | candidate.status |
versions.status | 与当前发布版相互独立 |
| 失败原因 | candidate.errorCode |
versions.error_code | 稳定业务码;不返回内部堆栈 |
| 可否重试 | candidate.retryable |
由状态与错误规则计算 | 不是客户端自行推断的常量 |
| 引用页码或位置 | citations[].location |
chunks.source_location | 格式因文件类型不同,需注明单位 |
| 回答完成状态 | completionState |
messages.completion_state | complete、partial 等枚举,不能由正文是否非空推断 |
| 一次生成尝试 | attemptId |
attempts.id | 每次重新生成都有新标识 |
| 追踪请求 | requestId |
日志关联字段 | 不等同于会话 ID 或业务幂等键 |
注意表里有些字段不需要直接存储。例如 retryable 可以由失败码和当前版本状态计算。冗余保存会引入同步问题:状态已经恢复就绪,旧 retryable 仍为 true。另一些字段必须存下来,例如产生旧回答时使用的版本 ID,不能事后用当前发布版补填。
什么叫“同名但不同义”#
前端的 status 可能指上传控件状态,后端的 status 指解析任务状态,数据库的 status 指文档是否删除。如果直接复用一个字段,上传完成就可能误显示“资料可用”。给不同领域对象各自命名和定义,即使字段名都叫 status,也必须通过对象边界区分。
还要明确时间、金额和计数的单位。duration: 5 是秒还是毫秒?size: 100 是字节还是 KiB?updatedAt 是用户修改时间,还是后台最后一次处理时间?这些细节不会被 TypeScript 自动识别,却会直接造成错误展示和错误排序。
把状态图写成可以执行检查的转移规则#
只有状态列表还不够,还需要事件、前置条件和产生的副作用。下面只描述一个候选版本的处理,不把文档发布指针混进同一个枚举。
| 当前状态 | 事件 | 下一状态 | 必须同时成立的条件 |
|---|---|---|---|
| uploaded | 开始处理 | processing | 已领取任务,版本未撤销 |
| processing | 处理成功 | ready | 解析与索引已经完整保存 |
| processing | 处理失败 | failed | 保存稳定错误码与尝试信息 |
| failed | 请求重试 | uploaded | 错误允许重试,版本仍有效 |
| uploaded / processing / failed / ready | 撤销版本 | deleted | 立即不再允许访问和发布 |
| ready | 发布为当前版 | ready | 另一个事务更新文档的 activeVersionId |
发布事件没有改变版本的 ready 状态,因为“准备好了”和“当前正在服务”是不同事实。一个历史版本仍然可以是 ready,但不再是当前发布版本。模型准确性排查经常需要知道这两个事实,不能只看到一行 status=success。
可运行实验:用状态契约阻止非法转移#
保存为 document-transition.mjs,运行 node document-transition.mjs。这是零依赖 Node 示例,用于验证业务规则;它不包含数据库事务或真正的任务队列。
import assert from 'node:assert/strict';
const rules = {
uploaded: { start: 'processing', remove: 'deleted' },
processing: { succeed: 'ready', fail: 'failed', remove: 'deleted' },
failed: { retry: 'uploaded', remove: 'deleted' },
ready: { remove: 'deleted' },
deleted: {},
};
function transition(version, event) {
// 真实服务端还要先确认调用者身份、资源权限和任务所有权。
const next = rules[version.status]?.[event.type];
if (!next) throw new Error(`ILLEGAL_TRANSITION:${version.status}:${event.type}`);
if (event.type === 'retry' && !version.retryable) {
throw new Error('NOT_RETRYABLE');
}
if (event.type === 'succeed' && !event.indexCommitted) {
throw new Error('INDEX_NOT_READY');
}
if (event.type === 'fail' && !event.errorCode) {
throw new Error('ERROR_CODE_REQUIRED');
}
return {
...version,
status: next,
revision: version.revision + 1,
errorCode: event.type === 'fail' ? event.errorCode : null,
retryable: event.type === 'fail' ? Boolean(event.retryable) : false,
};
}
let version = { id: 'v2', status: 'uploaded', revision: 1,
errorCode: null, retryable: false };
version = transition(version, { type: 'start' });
assert.equal(version.status, 'processing');
assert.throws(() => transition(version, { type: 'succeed' }), /INDEX_NOT_READY/);
version = transition(version, { type: 'fail', errorCode: 'TEMPORARY_STORAGE', retryable: true });
version = transition(version, { type: 'retry' });
version = transition(version, { type: 'remove' });
assert.equal(version.status, 'deleted');
assert.throws(() => transition(version, { type: 'start' }), /ILLEGAL_TRANSITION/);
console.log('通过:索引未提交不能就绪,删除后不能重新启动');
revision 表示状态版本,方便讨论并发更新。这个纯函数里增加数字并不会自动解决数据库竞争。真实数据库更新可以用“旧 revision 必须等于预期值”作为条件,影响行数为 0 时说明状态已被其他操作改变。你需要重新读取或拒绝过期操作,而不是覆盖更新。
这个例子也说明业务函数的测试价值:你可以不启动页面和数据库,先验证规则内部是否矛盾。接着再用集成测试确认数据库条件更新真正实现了这些规则。两个层次都要存在,不能让纯函数测试替代并发数据库实验。
一次版本更新如何跨越多个组件#
假设文档 A 的 v1 正在服务,管理员上传 v2。服务端先把原文件与版本记录可靠保存,返回 v2 的标识和 uploaded 状态;后台任务领取后改为 processing,解析文本、切块并生成索引。只有所有必需部分准备好,v2 才能进入 ready。
切换发布指针时,服务端在事务中检查文档没有被删除、v2 仍属于文档 A、操作者仍有权限、当前版本没有被另一次更新改变。条件满足后,将 activeVersionId 从 v1 改为 v2。旧索引的清理可以异步进行,但新查询必须按发布指针选择版本,不能把所有 ready 版本混在一起检索。
为什么不能“解析一块就立刻对外可见”#
假设新制度前半部分写“适用所有员工”,后半部分写“某地区除外”。如果索引只建立了一半就参与问答,模型可能给出缺少例外条件的结论。原子发布的目的是让一次查询看到一个一致的知识版本,而不只是让数据库字段好看。
删除与解析成功同时发生时怎么办#
管理员撤销文档,后台任务此时刚好完成。如果任务只执行 UPDATE status='ready',它可能把已删除版本重新激活。状态更新必须带上期望的前置条件,例如只有 processing 且未撤销时才能改 ready;发布指针也必须检查文档仍有效。这个竞争条件应写进需求验收,而不是靠一个开发者“记得注意”。
数据库写入成功,但任务没有入队怎么办#
数据库事务和外部队列发送不是同一个原子操作。直接先写库再入队,中间进程崩溃会留下无人处理的记录;先入队再写库,消费者可能读取不到记录。一个常见解决方向是在同一数据库事务中写入待发布任务记录,由独立过程投递队列并标记结果,也就是 outbox 思路。
第一版也可以不用外部队列,直接让后台工作者从数据库领取任务,但仍需租约、重试和幂等。需求只要求可靠处理,不要求某个特定中间件。选型应服务于这个可靠性条件,而不是反过来。
把接口样例写到前端能直接接入的程度#
以下 JSON 是完整合法对象,可以复制到 mock 数据中。它们是案例契约,不代表现有服务器已经实现。上传接口成功表示资源已登记,并不表示可检索。
{
"documentId": "doc-a",
"activeVersionId": "v1",
"candidate": {
"versionId": "v2",
"status": "uploaded",
"errorCode": null,
"retryable": false
},
"requestId": "req-upload-2"
}
前端收到这个对象后显示“新版已上传,正在等待处理;当前问答仍使用旧版”。如果 activeVersionId 为 null,则应显示“文件已上传,处理完成后可以提问”。同一个候选状态,因为是否存在旧版,用户可执行的操作会不同;这就是组合状态,不适合只靠一个字典映射文本。
查询失败时,定义稳定错误对象。例如用户能修正输入的错误可以包含字段名;内部依赖失败不要把原始堆栈传出去。下面的 error code 用于程序分支,message 用于用户说明,两者不要互相替代。
{
"error": {
"code": "DOCUMENT_PROCESSING",
"message": "资料还在处理中,请稍后再试。",
"retryable": true
},
"requestId": "req-question-7"
}
同一个 retryable 在不同接口可能代表不同动作。它可能表示“稍后重新查询”,不表示“重新上传文件”。因此接口文档应写清允许重试的方法、是否复用幂等键,以及服务器是否提供重试时间。一个布尔值没有足够信息替你完成这些产品语义。
为什么接口文档需要说明失败时机#
HTTP 响应头尚未发送时,可以返回正常错误码;开始流式回答后,只能按流协议发送错误事件或结束连接。前端要区分“请求没有开始”“得到部分内容后失败”和“成功完成但保存历史失败”。这些状态影响提示、重试和历史记录展示,不能统称为请求失败。
业务操作尤其需要说明“结果未知”。例如创建任务时网络断开,不能直接说创建失败,因为数据库可能已经提交。应查询 operationId 或用相同幂等键恢复结果。需求中承认“未知”状态,往往比强行二分成功与失败更准确。
把异常矩阵变成可执行验收计划#
验收应包含测试数据、操作、预期结果和观察证据。下面的表不仅给出场景名称,还告诉你去哪里确认结果。它可以直接转成集成测试与端到端测试的任务列表。
| 场景 | 具体操作 | 必须观察到的结果 | 证据位置 |
|---|---|---|---|
| 首次上传 | 上传有效文本 | 有文档 ID,尚不可检索 | API、数据库与页面状态 |
| 新版处理中 | v1 就绪后上传 v2 | 问答仍使用完整 v1 | 本次检索版本记录 |
| 文件超限 | 发送超过上限的请求 | 及时拒绝,不继续消耗解析资源 | HTTP 结果与任务数量 |
| 越权读取 | Bob 请求 Alice 私有文档 | 不返回原文和摘要 | 响应与模型调用日志 |
| 越权检索 | Bob 提问私有资料内容 | 私有片段不进入上下文 | 受控测试追踪 |
| 重复上传请求 | 同一键同一内容提交两次 | 按约定返回同一结果 | 唯一约束与记录数量 |
| 同键不同内容 | 复用键但更改正文 | 明确冲突,不静默覆盖 | HTTP 错误码与原记录 |
| 处理中删除 | 任务未完成时撤销文档 | 后台完成也不能重新发布 | 状态与发布指针 |
| 无依据问题 | 提问资料不包含的内容 | 说明依据不足,不伪造引用 | 固定评测样例 |
| 模型服务失败 | 注入上游 503 | 显示服务失败,不说资料不存在 | 错误分类与请求次数 |
| 连接中断 | 回答一半断开 | 保留 partial 状态 | 页面与消息记录 |
| 新版发布后 | 同一问题重新提问 | 使用 v2,旧回答仍标 v1 | 版本与引用记录 |
表里的“模型调用日志”只应记录受控测试所需的标识、阶段和脱敏信息。生产环境不能为了证明权限而长期记录全部私人文档。如果需要检查上下文是否泄露,可以在测试中用合成的特征字符串和内存 mock 捕获请求,避免真实敏感内容离开边界。
主观质量怎么写验收#
答案质量可以分成有依据、完整性、相关性、表达清晰等维度,并给每个维度明确评分例子。不要只写“正确率达到 95%”,却没有样本分布、判定规则与失败类别。一个只包含十个简单问题的高分,不能代表复杂问题也可靠。
保留无答案、冲突资料、过期制度、权限受限和用户含糊提问等样例。对同一修改,使用同一批问题比较,并记录模型、提示词和索引版本。这个项目的质量结论应限定在已评测的范围内,而不是扩展成“任何制度都能准确回答”。
需求变更时,如何判断影响范围#
假设新增要求:“上传后的新版本必须由另一位管理员审核才能发布。”这不是把按钮文案改成“审核”就够了。版本需要新增等待审核阶段,数据需要保存提交人、审核人、审核时间和意见,权限规则需要禁止本人审核,发布流程需要验证审核仍适用于当前版本。
前端需要显示审核队列与拒绝原因,接口需要提交、批准、拒绝动作,数据库需要约束和审计字段,后台任务不能绕过审核直接更新发布指针。历史数据迁移也要决定已经 ready 的版本如何处理。分析一个变更时沿 UI、接口、数据、执行、权限、测试六条线检查,可以避免只修一个页面。
一份更完整的参考方案#
把解析状态和审核状态分开:processing/ready/failed 描述处理,pending/approved/rejected 描述审批。发布条件是“解析 ready,审批 approved,文档未删除,审批对应版本未变,操作者有发布权限”。拒绝审核不会要求重新解析相同文件;修改文件会创建新版本,并使旧审批不能用于新内容。
练习时先写这条发布谓词,再补状态转移测试。不要先加两个布尔值 isReady 与 isApproved,然后在按钮上判断一次就认为控制完成。真正的发布入口必须在服务端重新执行同样的业务条件,客户端只负责提前解释为什么当前不能操作。
练习:实现发布条件,而不只写方案摘要#
下面给出完整参考实现,保存为 publish-policy.mjs,运行 node publish-policy.mjs。它只检查案例发布策略,不执行数据库写入。实际发布应在事务中读取可信记录并原子更新,以防检查之后状态变化。
import assert from 'node:assert/strict';
function canPublish({ actor, document, version, approval }) {
if (!actor?.permissions?.includes('document:publish')) return { ok: false, reason: 'FORBIDDEN' };
if (document.deleted) return { ok: false, reason: 'DOCUMENT_DELETED' };
if (version.documentId !== document.id) return { ok: false, reason: 'WRONG_DOCUMENT' };
if (version.status !== 'ready') return { ok: false, reason: 'NOT_READY' };
if (!approval || approval.status !== 'approved') return { ok: false, reason: 'NOT_APPROVED' };
if (approval.versionId !== version.id) return { ok: false, reason: 'STALE_APPROVAL' };
if (approval.reviewerId === version.createdBy) return { ok: false, reason: 'SELF_REVIEW' };
return { ok: true, versionId: version.id };
}
const input = {
actor: { id: 'publisher', permissions: ['document:publish'] },
document: { id: 'doc-a', deleted: false },
version: { id: 'v2', documentId: 'doc-a', status: 'ready', createdBy: 'alice' },
approval: { status: 'approved', versionId: 'v2', reviewerId: 'bob' },
};
assert.deepEqual(canPublish(input), { ok: true, versionId: 'v2' });
assert.equal(canPublish({ ...input, document: { ...input.document, deleted: true } }).reason, 'DOCUMENT_DELETED');
assert.equal(canPublish({ ...input, approval: { ...input.approval, versionId: 'v1' } }).reason, 'STALE_APPROVAL');
assert.equal(canPublish({ ...input, approval: { ...input.approval, reviewerId: 'alice' } }).reason, 'SELF_REVIEW');
assert.equal(canPublish({ ...input, actor: { id: 'guest', permissions: [] } }).reason, 'FORBIDDEN');
console.log('通过:发布成功、删除、过期审批、本人审核和无权限五种情况');
你应该先独立写出测试,再看参考实现。尝试添加新的失败条件,例如候选版本已经被后来的上传替代。说明这个条件应作用在版本记录、文档指针还是审批记录,写出一个会失败的测试,再修改实现。这比继续阅读更多术语更能证明你理解了需求对代码的影响。
把需求文档交给 AI 前,最后检查一次#
确认 AI 能从文档中回答:用户是谁,能操作哪些资源,输入允许什么,成功表示什么,失败是否可重试,数据怎样更新与删除,怎么判断实现完成。任何一个答案需要猜测,都可能在生成代码时变成未经确认的产品决定。
你不必一次提供整个系统。可以把本章发布策略、字段映射与五个测试作为一个独立任务,要求先实现纯业务规则;随后再接数据库事务,最后接页面。每一步的输入输出稳定,AI 就能在可审查的范围内帮助你,而你始终掌握最终的业务含义。
练习#
为“让助手帮我创建任务”补齐操作草稿、用户确认、提交中、成功和失败状态。考虑用户点击两次确认、确认后网络断开、任务已创建但响应丢失。
参考答案
先生成可编辑草稿,展示关键字段;确认后由服务端重新检查权限与输入,用幂等键创建任务。网络失败时先查询同一操作状态或使用相同幂等键重试,不能盲目创建新任务。已执行结果应包含任务 ID,页面刷新后可恢复。模型只提出草稿,最终执行由程序和用户授权共同约束。
验收与自测#
- 能把一个 AI 需求映射到 UI、接口、数据和执行流程。
- 至少覆盖正常、无数据、无权限、依赖失败和重复请求。
- 能解释一个技术选择的收益、代价和后续升级条件。
问:验收写“回答智能、准确”足够吗? 答:不够,需要样例、评分规则和可观察的失败标准。
问:原型没有错误页,是否可以忽略? 答:应补充必要状态并明确产品行为。
问:为什么在实现前讨论删除? 答:删除影响文件、索引、缓存和历史引用,属于完整的数据生命周期。