[Project] 8. NL 胶水层 — Vibe-Coding 时代的需求工程实践

我的Vibe-Coding实践方向——NL胶水层
问题:AI写代码太快了
Vibe-Coding正在改变软件开发的节奏。AI编码工具让代码生成速度提升了10倍甚至100倍。但这种速度带来了一个显而易见的问题:代码失控。
Vibe-Coding的甜蜜陷阱
Vibe-Coding的体验是这样的:你告诉AI——“帮我搭一个订单系统,要支持下单、支付、退款,支付用webhook回调,退款要走审批流,所有操作要有审计日志”。
十分钟后,你面前出现了:
order_service.py— 完整的订单状态机(pending → paid → shipped → completed / refunded)payment_gateway.py— 支付网关集成,带签名验证、幂等性检查、超时重试refund_workflow.py— 退款审批流,三级审批,自动通知audit_logger.py— 审计日志,异步写入,支持查询models.py— 数据库模型,带索引、约束、软删除tests/— 一整套单元测试,覆盖率看着有90%+
你跑一下测试,全绿。启动服务,调几个API,都通。代码质量看着也不错——类型标注完整、错误处理到位、日志分级清晰。
但这只是开始。你不可能一句话就搞定整个系统——你得一轮一轮地和AI对话,逐步细化:
- 第一轮:“搭一个订单系统的基本框架”→ 生成了订单模型和基础CRUD
- 第二轮:“加上支付集成,用webhook回调”→ 生成了支付网关和签名验证
- 第三轮:“退款要走审批流,三级审批”→ 生成了退款工作流和审批链
- 第四轮:“所有操作要有审计日志”→ 生成了审计日志模块
- 第五轮、第六轮……每一轮都在补充细节、修复边界情况、调整业务规则
每一轮对话,AI都根据你的描述生成了高质量的代码。你跑测试,全绿。你提交了。感觉自己一天干了别人一周的活。
然后问题开始出现了。
第二周,你让AI加一个"部分退款"功能。AI生成了partial_refund.py,逻辑完整,测试通过。但你没注意到:
- 原来的
refund_workflow.py里,退款金额校验是refund_amount == order_amount(全额退款) - 新的
partial_refund.py里,校验是refund_amount <= order_amount - 两个模块各自独立运行,没有共享校验逻辑
- 如果有人同时调用全额退款和部分退款接口,订单金额可能被退两次
第三周,支付网关那边改了webhook的签名算法。你让AI更新签名验证。AI改了payment_gateway.py里的verify_signature()函数。但你没注意到:
refund_workflow.py里也有一个签名验证——是AI当初生成退款模块时独立写的,用的是旧算法- 支付验证通过了,退款验证失败了,但失败被
try/except吞掉了,只打了一行warning日志
第四周,你回头看这个系统——12个文件,3000行代码,每个文件都写得很专业,但没有人(包括你自己)真正理解它们之间的所有交互。你不知道哪些模块共享了逻辑,哪些模块各自为政,哪些隐含的假设在某个角落被违反了。
这就是Vibe-Coding的甜蜜陷阱:每一次生成都是完美的,但累积起来是混乱的。AI每次只看到一个文件,它不知道整个系统的全貌。而你,因为代码生成得太快,也来不及建立这个全貌。
代码失控的三个信号
这种混乱不是突然爆发的,它有三个渐进的信号。
信号一:重复实现
订单系统里,退款金额校验这个逻辑,AI在三个地方各写了一次:
每个实现都能跑,每个实现都有自己的"风格"。但当你需要修改退款规则——比如加上"退款金额不能超过实付金额"——你需要找到所有三个地方,分别修改,还要确保修改后的行为一致。
AI不会帮你做这件事。它每次只看到一个文件。
信号二:隐性依赖
payment_gateway.py的webhook处理器依赖audit_logger.py的一个特定行为——“当日志写入失败时,主流程不应该中断”。但这个依赖没有写在任何地方,它只存在于AI生成代码时的那次对话里。
三周后,你让AI优化审计日志的性能,AI把异步写入改成了批量写入。批量写入失败时会抛异常。支付webhook处理器没有捕获这个异常,导致支付成功但订单状态没更新。
没有人知道这两个模块之间有依赖关系。代码里没有注释,没有文档,没有契约。
信号三:知识流失
代码写完后,连作者自己都记不清"为什么这么写"。你看着refund_workflow.py里的三级审批逻辑,完全不记得为什么是三级而不是两级。
是因为金额阈值?是因为合规要求?还是AI当时就这么生成的,你觉得能跑就接受了?
三个月后的你,和一个新加入团队的成员,面对的是同样的困境:只有代码,没有上下文。
为什么需要在代码上抽象一层
问题的根源是:代码是细节层,不是理解层。
当你直接让AI从意图生成代码,中间没有任何缓冲层:
没有抽象层时,每次AI生成都是一次独立的"翻译"——从自然语言到代码。每次翻译都可能产生不同的结果,因为AI没有记忆,没有上下文,没有标准。
有了抽象层,情况完全不同:
- 你先把意图写成结构化的自然语言(比如User Story)
- AI从这个结构化的描述生成代码
- 每次生成都有同一个"真相源"作为参照
- 代码可以变,但意图不变
这个抽象层的目的不是减慢速度,而是:
- 提供一个稳定的理解锚点——不管代码怎么变,意图是固定的
- 让AI生成的代码有一个可追溯的源头——每段代码都能追溯到哪条需求
- 在代码变化时,保持意图的连续性——重构代码时,你知道"应该做什么"不变
这个抽象层可以是API文档、接口契约、或者需求文档。我选择的是User Story。
选择:为什么是User Story
抽象层的选择决定了整个工作流的形态。不同的抽象层有不同的特性和局限。
抽象层的候选方案
常见的抽象层包括:
- API文档:描述接口签名、参数、返回值
- 接口契约:描述模块间的协议、数据格式、行为约束
- 需求文档:描述功能目标、用户场景、验收标准
- User Story:以用户视角描述需求,包含角色、目标、价值
每种方案都试图在代码之上建立一个"真相源",但效果不同。
API文档的局限
API文档的优势是精确:
- 函数签名、参数类型、返回值都是确定的
- 可以直接从代码生成,保持同步
但API文档的局限是缺乏上下文:
- 知道"这个函数做什么",但不知道"为什么要做"
- 知道"参数是什么",但不知道"什么场景下会用到"
- 无法回答"这个功能对用户有什么价值"
一个具体的例子:你看到exportCSV(filters: FilterOptions): Promise<Buffer>这个签名,你知道它导出CSV。但你不知道:
- 谁在用这个功能?(运营?数据分析师?)
- 为什么要导出?(做报表?做审计?)
- 导出的数据量级是多少?(100条?100万条?)
- 导出失败时应该怎么处理?(重试?通知用户?)
这些信息对AI生成正确的代码至关重要,但API文档里完全没有。
接口契约的局限
接口契约的优势是约束明确:
- 定义模块间的边界和协议
- 可以验证是否符合契约
但接口契约的局限是过于技术化:
- 描述的是"系统内部如何协作",而不是"用户需要什么"
- 对于非技术角色(产品、运营)难以理解
- 无法作为"需求的真相源"
接口契约告诉你"模块A通过gRPC调用模块B的ProcessOrder方法,超时时间5秒,重试3次"。但它不告诉你"用户下单后,系统应该在3秒内确认订单,如果支付网关超时,应该保留订单并通知用户稍后重试"。
前者是实现细节,后者是业务需求。接口契约只能粘合代码和代码,粘不住人和代码。
User Story的优势
User Story的结构是:
| |
这个结构的优势:
- 以用户为中心:从用户的视角描述需求,而不是从系统的视角
- 包含上下文:角色、目标、价值、场景都有明确的描述
- 可验证:Acceptance Criteria提供了具体的验收标准
- 易于理解:非技术角色也能读懂
回到订单系统的例子,User Story会这样写:
| |
AI读到这个描述,它知道:
- 用户是谁(买家)
- 要做什么(部分退款)
- 为什么(部分商品有问题)
- 边界情况怎么处理(超额退款、重复退款、审批流程)
这些信息足以生成正确的代码,也足以生成正确的测试。更重要的是——当AI生成partial_refund.py时,它不会再和refund_workflow.py里的全额退款逻辑冲突,因为User Story已经明确了金额校验规则。
但User Story的价值不止于此。它提供了两个关键能力:
全局视野:当你有20个User Story描述整个订单系统时,你可以一眼看到系统的全貌——哪些功能已经定义了,哪些还缺失,哪些之间有依赖关系。这是代码做不到的,因为代码分散在几十个文件里,没有人能一次性读完所有代码并理解它们的关系。
从故事到代码的清晰路径:User Story不是写完就扔在一边的文档,它是开发的起点。工作流变成了:
- 写User Story → 2. 从Story生成代码 → 3. 从Story生成测试 → 4. 验证代码和Story一致
这条路径是明确的、可追溯的。每一段代码都能追溯到哪条User Story,每一条User Story都能检查是否有对应的代码实现。
User Story作为胶水的定位
User Story不是需求的终点,而是胶水层:
- 向上粘合用户意图(产品需求、业务目标)
- 向下粘合代码实现(函数、模块、接口)
- 向外粘合测试用例(Acceptance Criteria → Test Case)
作为胶水层,User Story的核心职责是保持一致性:
- 代码实现是否符合User Story的描述
- 测试用例是否覆盖了User Story的验收标准
- 文档是否反映了User Story的意图
这就是为什么我选择User Story而不是API文档或接口契约——User Story是唯一能同时粘合人、代码和测试的抽象层。
核心原则:NL是中心,代码是细节
一旦确立了User Story作为胶水层的定位,整个工作流的视角就发生了转变:NL(自然语言)是中心,代码是实现细节。
NL不需要"完整",需要"可靠"
传统的文档思维追求"完整性":
- 每个功能都要有文档
- 每个细节都要描述
- 文档要和代码100%同步
但作为胶水层,NL不需要完整:
- 可以只描述核心功能,忽略边缘情况
- 可以只描述"应该是什么",不描述"现在是什么"
- 可以有重复、有冗余、有粗略的描述
唯一的要求是:NL不能错。
NL可以重复,不能错误
重复的NL不是问题:
- 同一个功能在多个User Story中描述,不会造成混乱
- 重复反而提供了多个视角,增强理解
错误的NL是致命的——这里说的"错误"不是指代码和NL不一致(那是代码的问题),而是NL本身就是错的、自相矛盾的:
- US-05说"退款金额不能超过实付金额",US-08说"退款金额可以包含补偿金,允许超过实付金额"——两条Story自相矛盾,开发者不知道该听哪个
- US-12说"退款审批只需主管一级",US-15说"所有退款必须经过财务审批"——两条Story对审批流程的描述冲突
- US-03说"支付成功后立即发货",但业务规则其实是"T+1结算后才能发货"——NL本身就写错了,和真实业务规则不符
这些NL错误比代码bug更危险——因为NL是胶水,是真相源。如果NL本身是错的,那基于它生成的代码、测试、文档全都是错的,而且你很难发现,因为"文档里就是这么写的"。
NL可以粗略,不能模糊
粗略的NL是可以接受的:
- “订单支持退款”——虽然没有细节,但方向是对的
- “支付回调要安全”——虽然没有量化,但意图是清楚的
模糊的NL是无法作为胶水的:
- “系统应该表现良好”——什么是"良好"?无法验证
- “处理用户的请求”——什么请求?怎么处理?无法实现
- “更好的体验”——什么是"更好"?无法衡量
从"代码覆盖率"到"NL落地率"
传统的覆盖率思维是:代码被测试覆盖了多少。
NL胶水层的覆盖率思维是双向的:
- NL落地率:NL在代码中落地了多少(NL → Code)
- 代码NL率:代码被NL覆盖了多少(Code → NL)
这两个指标回答不同的问题:
- NL落地率:你写的需求有多少变成了代码?(未实现的NL是backlog)
- 代码NL率:你写的代码有多少被需求描述了?(未覆盖的代码是"野生代码",没人知道它为什么存在)
代码NL率低意味着系统中有大量"野生代码"——它们存在,但没有NL解释它们为什么存在。这些代码在重构时最危险,因为没有人知道它们的业务背景,改了可能破坏某些隐含的业务规则。
这个转变的意义:
- 不再关心"代码有没有被测试覆盖"(那是测试覆盖率的事)
- 关心"NL描述的功能有没有在代码中实现"(NL落地率)
- 关心"代码有没有被NL描述"(代码NL率)
- 未实现的NL是backlog,不是问题
- 未覆盖的代码是风险,需要补充NL或确认是否可以删除
- 已实现但与NL矛盾的代码是问题,必须修复
NL覆盖率是这个思维转变的度量工具——它回答的核心问题不是"代码好不好",而是"NL落地了多少、落地得对不对、代码被NL覆盖了多少"。
三个核心指标
基于"NL是中心"的原则,我定义了三个核心指标来度量NL的健康度。
NL实现率:这条NL落地了吗
定义:已实现的NL数量 / 总NL数量(排除模糊的NL)
含义:有多少User Story的Acceptance Criteria在代码中找到了对应的实现。
示例:
| |
未实现的NL是backlog。在敏捷开发中,backlog指的是"已识别但尚未实现的需求列表"——简单说就是"待办事项"。未实现的NL就是待办事项,让你知道哪些功能还没做。这不是问题,而是正常的开发节奏。
NL准确率:NL和代码一致吗
定义:已实现且一致的NL数量 / 已实现的NL数量
含义:在已实现的NL中,有多少是和代码实际行为一致的。
示例:
| |
冲突是最严重的问题:NL作为胶水断了,它会误导所有人。
代码NL率:代码被NL描述了吗
定义:被NL覆盖的代码行为数量 / 总代码行为数量
含义:有多少代码行为有对应的NL描述。与NL实现率(NL→Code)相反,这是Code→NL方向的度量。
示例:
| |
未覆盖的代码是"野生代码":它们存在,但没有NL解释它们为什么存在。这些代码在重构时最危险,因为没有人知道它们的业务背景,改了可能破坏某些隐含的业务规则。
指标之间的关系
三个指标形成了一个双向度量体系:
- NL实现率(NL→Code):你写的需求有多少变成了代码?未实现的NL是backlog
- NL准确率(NL↔Code一致性):已实现的NL中,有多少和代码行为一致?不一致的是冲突
- 代码NL率(Code→NL):你写的代码有多少被NL描述了?未覆盖的代码是"野生代码",存在风险
NL覆盖率不是单一数字,而是这个双向度量的完整画像——它告诉你NL落地了多少、落地得对不对、代码被NL覆盖了多少。
为什么不打分
传统的做法是给NL打分:
- 精确度80分、完整度70分、一致性90分
- 综合得分80分,“良好”
但打分有问题:
- 阈值怎么定:80分算好还是70分算好?不同项目标准不一样
- 难以行动:知道"70分",但不知道具体哪里有问题
- 容易作弊:为了分数而优化,而不是为了质量而优化
我的做法是直接报问题:
- 不告诉你"这条NL得70分"
- 告诉你"这条NL和代码有冲突:NL说X,代码做Y"
- 告诉你"这条NL模糊:‘快速’没有定义"
问题比分数更有行动价值。
三个核心检测
基于三个指标,我设计了三个检测机制来发现问题。
冲突检测:NL说X,代码做Y
目标:发现NL和代码之间的矛盾。
检测方法:
- 提取NL中的关键值(数字、条件、行为)
- 在代码中找到对应的实现
- 比较NL描述和代码实际行为
- 如果不一致,标记为冲突
示例:
| |
冲突的优先级最高:它直接破坏了NL作为胶水的价值。
模糊检测:粘不住下游的NL
目标:发现无法作为下游输入的NL。
检测方法:
- 检查NL是否包含具体的值(数字、条件、状态)
- 检查NL是否使用了主观语言(“快速”、“良好”、“更好”)
- 检查NL是否可以转化为测试用例
- 如果无法转化,标记为模糊
示例:
| |
模糊不是错误:但它需要改进,否则无法粘住下游。
Backlog检测:待办事项可视化
目标:发现还没有在代码中落地的NL,形成待办事项清单。
检测方法:
- 提取NL的Acceptance Criteria
- 在代码中搜索对应的实现
- 如果找不到,加入backlog
示例:
| |
Backlog不是问题:它是待办事项的可视化,让你知道哪些功能还没做。
检测的优先级
三个检测的优先级:
- 冲突:NL和代码矛盾,胶水断了,必须修复
- 模糊:NL无法粘住下游,建议改进
- 未实现:NL还没落地,是backlog,不是问题
实践:从处理代码到处理User Story
理论的最终目的是指导实践。NL胶水层的建立,意味着工作范式的转变。
工作流的转变
传统的工作流:
| |
NL胶水层的工作流:
| |
关键的变化:
- 起点变了:从"写代码"变成"写User Story"
- 终点变了:从"代码跑通"变成"NL和代码一致"
- 验证变了:从"测试通过"变成"NL落地且准确"
NL覆盖率工具的设计思路
基于上述理论,一个理想的NL覆盖率工具应该具备以下特征:
核心功能:
- 提取所有User Story的Acceptance Criteria
- 在代码中搜索对应的实现
- 检测冲突、模糊、未实现
- 生成NL覆盖率报告
设计原则:
- 不打分:直接报问题,不给分数
- 可行动:每个问题都有具体的描述和建议
- 优先级清晰:冲突 > 模糊 > 未实现
输出示例:
| |
与现有工具链的集成
NL覆盖率工具不是孤立的工具,它与整个req工具链集成:
与req-refresh集成:
- 在刷新需求文档后,自动运行NL覆盖率分析
- 发现冲突时,提示用户修复
与req-catalog集成:
- 在生成需求目录时,嵌入NL覆盖率指标
- 提供全局的NL健康度视图
与req pipeline集成:
- 在justify阶段,检查NL覆盖率
- 如果冲突数量过多,标记为"NL gap"
实际使用场景
场景一:新功能开发
- 写User Story和Acceptance Criteria
- 用AI生成代码
- 运行NL覆盖率工具检查NL落地情况
- 修复冲突,改进模糊的NL
- 确认NL和代码一致后,提交
场景二:代码重构
- 运行NL覆盖率工具检查现有NL覆盖率
- 发现冲突:NL说X,代码做Y
- 决定:是改NL还是改代码
- 重构代码
- 再次运行NL覆盖率工具确认一致性
场景三:需求评审
- 运行NL覆盖率工具生成报告
- 查看backlog:这是待办事项
- 查看模糊的NL:这是需要细化的需求
- 查看冲突的NL:这是需要立即修复的问题
- 基于报告进行需求评审
测试的真相:AI写测试的问题
NL胶水层不仅改变了开发流程,也暴露了AI写单元测试的一个根本性错误。
AI写测试的陷阱
当前AI写单元测试的方式是这样的:
- 读取代码实现
- 根据代码逻辑生成测试用例
- 测试100%通过
这看起来很完美,但实际上是循环论证:
- 代码说X,测试验证X → 通过
- 代码说Y,测试验证Y → 通过
- 代码错了,测试也跟着错 → 还是通过
这种测试的价值是零:它只能证明"代码做了代码做的事",不能证明"代码做了应该做的事"。
测试应该从User Story来
正确的测试生成路径:
- 从User Story的Acceptance Criteria生成测试
- 测试描述的是"应该是什么",不是"现在是什么"
- 如果代码错了,测试会失败 → 发现冲突
这个区别是本质性的:
- 基于代码的测试:验证代码的自洽性(代码和自己一致吗?)
- 基于NL的测试:验证代码的正确性(代码和需求一致吗?)
一个具体的例子
假设User Story说:
| |
AI基于代码生成的测试(代码中阈值写死为500元):
| |
基于User Story生成的测试:
| |
第二种测试才能发现问题。
NL作为测试的真相源
这进一步强化了NL胶水层的定位:
NL胶水层的完整价值链:
- NL → 代码:指导实现
- NL → 测试:生成验证标准
- 测试 → 代码:验证一致性
- 冲突检测:发现NL和代码的矛盾
当测试从NL来,而不是从代码来,测试才真正有了价值——它验证的是"代码做了应该做的事",而不是"代码做了代码做的事"。
更大的图景:NL作为万能胶水层
当我们把NL胶水层的思维从"代码和代码之间"推广出去,会发现一个更大的图景:AI本身就是胶水层,而NL是它的粘合剂。
代码与代码之间的胶水
这是最直接的层面,也是本文前面讨论的核心:
- 模块A和模块B之间的依赖关系,用NL描述比用代码注释更清晰
- 函数之间的契约,用User Story表达比用类型签名更完整
- 重构时的意图保持,用NL锚定比用代码推断更可靠
AI在这里的角色是翻译器:把NL翻译成代码,把代码翻译成测试,把测试翻译成报告。NL是粘合剂,AI是执行者。
程序与程序之间的胶水
当你有多个系统需要集成时,NL的价值更加明显:
- 系统A的API说"返回用户信息",系统B期望"返回客户档案"——这是同一个东西吗?
- 微服务之间的数据流,用代码定义只能看到格式,用NL定义能看到语义
- 第三方集成时,对方文档说"支持批量操作"——批量是多少?100?10000?
传统的做法是用IDL(Interface Definition Language)或OpenAPI Spec来定义接口。这些是精确的,但缺乏语义。NL补充了语义层——不仅定义"格式是什么",还定义"这意味着什么"。
人与人之间的胶水
这是最容易被忽视的层面。在软件开发中,人与人之间的沟通成本往往高于代码编写成本:
- 产品经理说"用户体验要好",开发理解为"响应要快",测试理解为"不能有bug"
- 前端说"这个组件要可复用",后端理解为"要抽象",设计理解为"要一致"
- 新成员问"这个功能为什么这么做",老成员说"当时就这么定的"
NL作为胶水层的价值:
- 消除歧义:把"好"定义成"< 200ms响应时间"
- 对齐理解:把"可复用"定义成"支持3种以上场景"
- 传递上下文:把"为什么"写进User Story的value子句
AI在这里的角色是对齐器:把不同角色的NL表述统一成同一个真相源,把模糊的共识转化为精确的验收标准。
团队与团队之间的胶水
当组织规模扩大,团队之间的协作成为瓶颈:
- 平台团队说"我们提供了用户服务",业务团队说"我需要的是客户管理"
- 数据团队说"数据仓库已就绪",分析团队说"我找不到我需要的指标"
- 安全团队说"符合合规要求",产品团队说"用户注册流程太复杂了"
传统的解决方案是架构评审会、跨团队文档、API网关。这些都是有效的,但它们有一个共同的问题:更新成本高,容易过时。
NL胶水层的优势:
- 低成本更新:改一条User Story比改一份架构文档快10倍
- 高保真传递:NL比代码更容易被非技术人员理解
- 可验证性:NL可以通过AI自动检查一致性
AI作为胶水层的本质
回到最根本的问题:AI在软件开发中的角色是什么?
传统观点:AI是加速器——让代码写得更快。
我的观点:AI是胶水层——让不同的事物粘合在一起。
而NL(自然语言)是这个胶水层的粘合剂:
- 没有NL,AI只是快速生成代码的工具
- 有了NL,AI成为连接意图、代码、测试、团队的枢纽
这就是为什么NL覆盖率如此重要——它度量的不是"代码写得好不好",而是**“胶水粘得牢不牢”**。
当NL覆盖率高且准确时:
- 代码改动有迹可循
- 测试失败有因可查
- 团队协作有据可依
- 系统演化有向可追
当NL覆盖率低或不准确时:
- 代码成为黑盒
- 测试成为装饰
- 团队协作靠猜
- 系统演化靠运气
总结:可靠的胶水
Vibe-Coding让代码生成变得极其快速,但这种速度需要被约束和引导。
NL胶水层的建立,本质上是工作范式的转变:
- 从"处理代码"变成"处理User Story"
- 从"代码覆盖率"变成"NL落地率"
- 从"测试通过"变成"NL和代码一致"
这个转变的意义:
- 代码是细节:AI可以快速生成,但NL是真相源
- NL是中心:它粘合了用户意图、代码实现、测试用例
- 一致性是目标:NL和代码必须一致,否则胶水就断了
三个核心指标(实现率、准确率、代码NL率)和三个核心检测(冲突、模糊、未实现)提供了度量和改进的框架。
最终的目标不是"完美的文档",而是可靠的胶水——让NL能够有效地粘合上下游,让Vibe-Coding在速度的同时保持可控。