为什么你的代码总在返工?因为这120字需求里,藏着5个连产品都没想清楚的坑
一个怎么都做不对的优惠券规则
事情最早看起来很简单。
产品在群里丢了一段话:“会员等级越高优惠越大。金牌会员买东西可以叠加优惠券,银牌不行。下单时如果有可用优惠券就自动选面额最大的那张。但是金牌会员大促期间不能和优惠券叠加,怕亏本。下单金额超过 500 自动用券,不超过的话看用户心情。另外老用户回归也送一张券,回归券能不能叠加再说。前端要有一个券的角标显示。”
不到 120 字。我先后实现了三版。
第一版:所有会员都允许叠加券,上线后金牌会员大促期间叠加,被财务驳回。第二版:禁用大促期间所有叠加,结果普通日金牌会员也无法叠加,被产品打回。第三版:500 阈值写死在后端,产品又说这个阈值要按活动动态调整。
这类需求最麻烦的地方不是技术难,而是每次实现都像已经抓到了规则。过两天,它在另一个活动、另一个会员等级下再次翻车,你才发现之前理解的只是需求的某一面。
我不想再做第四次“凭感觉实现,等产品打回”,于是准备换一种方式:把需求原文、涉及的表结构、现有接口和已经返工的记录全部交给模型,让它先把需求里能确定的事实、互相矛盾的地方和必须由产品确认的问题拆清楚,再讨论怎么写。
这就是“需求炼金炉”的起点。
先认识蓝耘元生代 MaaS
我这次使用的是蓝耘元生代 MaaS 平台。MaaS 是 Model as a Service,也就是把大模型能力封装成可以调用的服务。开发者不需要在自己的电脑上准备 GPU、下载模型权重或维护推理环境,应用通过 API 提交请求,再接收模型返回的结果。
对于这个项目,我需要的不是一个只会把需求复述一遍的聊天页面,而是一个可以嵌入程序、按照固定格式返回澄清契约单的模型接口。需求炼金炉要连续完成事实提取、矛盾标记、开放问题列举、任务拆解和接口契约起草,输出还要能够被前端拆成不同卡片,因此使用 MaaS API 比手动复制对话更合适。
注册并进入 MaaS 平台
注册过程并不复杂:进入蓝耘元生代官网,完成账号注册和登录后,从顶部导航进入“MaaS平台”。控制台左侧可以看到模型广场、文本模型、智能路由、批量推理、用量统计等入口。
我进入“模型广场”,选择文本模型,然后找到 GLM-5.1。模型卡片给出了模型类型、上下文长度、调用名称和 API 示例入口。这几项信息后面都会直接用到。
为什么选 GLM-5.1
需求澄清并不是“把需求翻译成开发能看懂的话”这么简单。为了判断一段模糊需求里到底藏了多少歧义,模型至少需要同时看到以下材料:
- 需求原文(产品口述或 IM 转录);
- 涉及的表结构、现有接口和约束;
- 之前返工过几次、每次卡在哪;
- 项目对叠加、阈值和回归券的处理边界。
这些内容放在一起,比一段需求长得多。蓝耘模型广场给 GLM-5.1 标注了 198k 上下文,同时将它定位为面向智能体工程、长程任务和代码工作的模型。对“需求炼金”这种需要同时阅读需求文档、表结构和历史返工记录、再分阶段输出结构化契约单的项目来说,这些特性与任务比较匹配。
这里需要说明:本文不测试延迟、吞吐量和价格,也不据此给出性能排名。项目只验证一件事——GLM-5.1 能否根据完整需求材料形成可检查的澄清契约单,并帮助我把返工挡在编码之前。
接入蓝耘 GLM-5.1
在 GLM-5.1 模型卡片中点击“API示例”,可以查看当前平台提供的请求格式。项目没有把密钥写进源码,而是通过环境变量读取:
LANYUN_API_KEY=请填写自己的API密钥 LANYUN_BASE_URL=https://maas-api.lanyun.net/v1 LANYUN_MODEL=/maas/zhipuai/GLM-5.1项目本身使用 Node.js,因此实际调用代码也直接使用内置fetch:
constendpoint=`${process.env.LANYUN_BASE_URL}/chat/completions`;constresponse=awaitfetch(endpoint,{method:"POST",headers:{Authorization:`Bearer${process.env.LANYUN_API_KEY}`,"Content-Type":"application/json",},body:JSON.stringify({model:process.env.LANYUN_MODEL,messages:[{role:"system",content:"你是蓝耘元生代 GLM-5.1 需求分析工程师。必须依据用户提供的需求原文与上下文回答,不得编造未给出的业务规则。",},{role:"user",content:alchemistPrompt},],temperature:0.1,stream:false,}),signal:AbortSignal.timeout(120_000),});constrawBody=awaitresponse.text();if(!response.ok){thrownewError(`模型接口返回${response.status}:${rawBody.slice(0,500)}`);}letpayload;try{payload=JSON.parse(rawBody);}catch{thrownewError("模型接口未返回合法JSON响应");}constcontent=payload?.choices?.[0]?.message?.content;if(!content){thrownewError("模型响应中缺少 choices[0].message.content");}除了请求本身,项目还在两个地方做了兜底。一是接口地址:基础地址末尾有没有/v1、有没有已经带/chat/completions,都交给resolveChatCompletionsUrl统一处理,避免拼出…/v1/v1/chat/completions这种重复路径。二是响应体先按文本读出再解析,这样在接口返回非 JSON 错误(比如网关 502 的 HTML 页面)时,能把前 500 字符带进报错,而不是直接抛一个看不懂的SyntaxError。契约单解析阶段还会校验 7 个必填字段是否齐全,缺字段就返回"澄清契约单缺少字段:xxx",避免一段残缺输出被当成完整契约显示到页面上。
真实 API Key 只保存在本地环境变量中。截图、代码仓库和文章都不应该出现完整密钥。
我没有让模型一上来就写代码
最初版本的提示词很直接:“根据下面的需求实现优惠券叠加。”结果通常也很直接:一段看起来能跑的服务端代码,阈值写死、回归券默认可叠加、大促判断靠一个布尔值。问题是,这些正是我之前已经做过且翻车的事。
后来我把流程拆成四步:
- 只从需求原文与上下文中提取已经能确定的事实;
- 标记互相矛盾、含糊或多义之处;
- 列出必须由产品确认的开放问题;
- 在事实足够的前提下再起草接口契约和验收标准。
核心系统提示词如下:
你是一名需求分析工程师。 请把下面材料视为一份待澄清的需求案卷,不要直接假定产品意图,也不要只盯着最后一句结论。 工作顺序: 1. 只从需求原文与上下文中提取已经能确定的事实; 2. 标记互相矛盾、含糊或多义之处; 3. 列出必须由产品确认的开放问题; 4. 在事实足够的前提下拆解任务,标注依赖; 5. 起草接口契约草案,字段类型与约束尽量明确; 6. 给出可验证的验收标准; 7. 提示实现风险。 证据不足时必须明确写出“待确认”,不要补造业务规则。这个限制很重要。模型很擅长给出“看起来完整”的规则,但需求炼金首先要做的是暴露哪里没说清楚。如果它无法把原文里的矛盾单独列出来,再漂亮的契约单也不值得直接采用。
案发现场:120 字需求里的三处矛盾
为了稳定复现问题,我把产品那段 120 字的需求原样保留,再附上涉及的表结构和现有接口:
members(id, name, level ENUM('bronze','silver','gold'), last_active_at) coupons(id, code, face_value, min_amount, scope, expires_at) orders(id, member_id, amount, coupon_id, created_at) POST /api/orders 创建订单,当前不处理优惠券 GET /api/members/:id/available-coupons 返回该会员可用券列表只看需求原文,这段话读起来顺:等级越高优惠越大、金牌可叠加、自动选最大面额。但把原文和表结构放在一起,矛盾就藏不住了。
模型需要判断的核心问题是:这段需求里究竟是规则写漏了,还是多处规则互相打架?
我把以下材料一起提交给需求炼金炉:
- 需求原文;
- 涉及的表结构、现有接口和约束;
- 大促活动的控制方式;
- 前三次返工分别卡在哪。
GLM-5.1 给出的“澄清契约单”
实际调用后,GLM-5.1 先给出了一个很明确的判断:问题不在某一条规则写没写,而在原文里多处规则互相矛盾,且至少 5 个关键点完全没定义。
已确认事实: 1. 会员分为 bronze、silver、gold 三个等级,level 字段存于 members 表。 2. 默认仅金牌会员允许优惠券叠加,银牌与铜牌不可叠加。 3. 下单存在可用优惠券时自动选取面额最大者。 4. 大促期间由 ops_activities.is_active 控制,活动订单走独立计价逻辑。 5. 现有 POST /api/orders 不处理优惠券,GET /api/members/:id/available-coupons 已返回可用券列表。 6. 回归券面向流失后回归的老用户发放。 矛盾 / 多义点: 1. 金牌会员大促能否叠加——原文前半句说金牌可叠加, 后半句又说大促不能叠加,未说明该限制是否仅作用于金牌。 2. 500 阈值的适用范围——“超过 500 自动用券,不超过看用户心情” 与“自动选面额最大”冲突:不超过 500 时到底是自动还是手动未定义。 3. 回归券的叠加规则——原文以“再说”收尾, 回归券能否与会员等级券、大促活动叠加完全未定义。 开放问题(需产品确认): 1. 大促期间是否对所有等级统一禁用叠加,还是仅限制金牌? 2. 500 阈值是否随活动动态调整?由哪个字段或配置决定? 3. 不超过 500 时,前端“看用户心情”对应哪种交互:手动勾选还是默认不选? 4. 回归券是否计入 available-coupons 返回?它与会员等级券是否互斥? 5. “面额最大”是否需要同时满足 min_amount 约束?多张券同时满足时是否需做最优组合而非单张最大? 任务拆解: - 叠加规则引擎:输入会员等级、活动状态、券类型,输出是否允许叠加及可叠加券集合 - 大促状态读取:读取 ops_activities.is_active,注入到下单计价链路(依赖叠加规则引擎) - 券选取策略:在可用券中按 min_amount 过滤后选面额最大者,支持手动覆盖(依赖叠加规则引擎) - 回归券发放与校验:入库与互斥规则,待产品确认后实现 - 前端券角标:展示可用券数量与最大面额,区分自动/手动(依赖券选取策略) 接口契约草案: POST /api/orders 请求体:{ member_id: string, items: [{sku, qty}], coupon_id?: string, use_auto_coupon?: boolean } 响应:{ order_id, amount, applied_coupon_id, applied_coupon_face_value, breakdown } 约束: - 服务端按叠加规则引擎计算最终可用券; - use_auto_coupon=false 且未传 coupon_id 视为不使用优惠券; - 大促期间叠加规则引擎返回 is_stacking_blocked=true 时拒绝 coupon_id; - 当 amount < 动态阈值时,忽略自动选券,仅在传入 coupon_id 时校验并应用。 验收标准: 1. 金牌会员非大促期间下单,存在多张满足 min_amount 的券时,服务端自动应用面额最大者。 2. 大促期间下单,叠加规则引擎对所有等级返回 is_stacking_blocked=true,传入 coupon_id 应返回 422。 3. 银牌会员下单传入 coupon_id 与会员等级券叠加时,返回 422 并提示不可叠加。 4. 回归券规则在产品确认前不进入计价链路,available-coupons 暂不返回回归券。 5. 前端券角标展示可用券数量与最大面额,不满足 min_amount 的券不计入。 实现风险: 1. 500 阈值写死会再次被产品要求动态化,应改为读取配置而非常量。 2. “面额最大”不等于“最优”,多券组合可能让用户实际优惠更高,需产品确认是否允许组合。 3. 回归券与会员等级券若互斥规则未定,上线后极易引发资损,建议在契约确认前不实现回归券叠加。 4. 大促与日常逻辑分叉,若共用同一计价入口需注意回归测试覆盖两条路径。模型给出的契约草案把“是否叠加”收敛成一个叠加规则引擎的输入输出,500 阈值改为读取配置而非常量。它已经抓到了“规则不该写死”这个根因。
但我把它拿给产品确认时,又发现了一个新问题:契约单里把回归券相关任务也拆了出来,可回归券的互斥规则原文根本没定义。如果照着契约单直接实现回归券,等于模型替产品做了一个未被确认的决定。
这次模型找对了矛盾,契约草案却没法覆盖所有分支。最后我把回归券相关任务标记为“待产品确认前不实现”,available-coupons 接口暂不返回回归券。这样既保留了模型整理出的事实和开放问题,又避免把未定义的规则写进代码。
// 契约单驱动下的任务分流:回归券在产品确认前不进入计价链路constreport=parseAlchemistReport(content);consttasks=report.task_breakdown;// 1. 把依赖“回归券发放与校验”的任务单独拎出来,暂不进入开发列constblocked=tasks.filter(task=>task.depends_on?.includes("回归券发放与校验"));constready=tasks.filter(task=>!blocked.includes(task));// 2. 把开放问题回执给产品,逐条确认后才解锁 blocked 任务constopenQuestions=report.open_questions;// sendToProduct(openQuestions) -> 等回执 -> unblock(blocked)// 3. 在此期间 available-coupons 不返回回归券,避免前端误用constavailableCouponFilter=(coupon)=>coupon.scope!=="returning-user";契约单不能代替产品确认
模型暴露矛盾、起草契约,只完成了一半工作。真正决定这份契约单是否可信的,是产品对开放问题的逐条确认。
我最后保留了四个最关键的验证场景。在app目录执行npm test即可跑全部 12 项测试,用的是 Node.js 内置测试框架,不需要额外装依赖:
cd app npm test三种模拟对应三种开发策略,差别只在“歧义点是在编码前解决,还是拖到联调阶段爆炸”:
// 盲开发:5 个歧义点一个不澄清,每个都变成一次返工exportasyncfunctionsimulateBlindImplementation(ambiguityCount=5){// 编码 -> 联调暴露第一个歧义 -> 返工 -> 暴露下一个 -> … -> 带病上线return{reworkCycles:ambiguityCount,// 5 次返工defectsShipped:1,// 带病上线 1 个deliveredOnTime:false,};}// 澄清后:编码前把歧义点逐条和产品确认,实现阶段零返工exportasyncfunctionsimulateClarifiedImplementation(ambiguityCount=5){// 澄清(逐条 resolved) -> 编码 -> 联调通过 -> 上线return{ambiguitiesResolvedBeforeCoding:ambiguityCount,reworkCycles:0,defectsShipped:0,deliveredOnTime:true,};}模拟没有只检查“函数没有抛错”,而是直接比较返工轮次、带病上线和歧义点解决数。例如澄清后开发的核心断言是:
constresult=awaitsimulateClarifiedImplementation(5);assert.equal(result.ambiguityCount,5);assert.equal(result.ambiguitiesResolvedBeforeCoding,5);assert.equal(result.reworkCycles,0);assert.equal(result.defectsShipped,0);| 模拟场景 | 实际结果 | 结论 |
|---|---|---|
| 盲开发:5 个歧义点不澄清直接编码 | 返工 5 次,带病上线 1 个 | 成功复现需求返工 |
| 只澄清一半:5 个歧义点确认 2 个 | 返工 3 次,带病上线 1 个 | 剩余歧义点照样返工 |
| 全部澄清后开发:5 个歧义点逐条确认 | 返工 0 次,带病上线 0 个 | 编码阶段零返工 |
| 契约单驱动:回归券任务暂缓 | 回归券不进入计价链路 | 未定义规则不写进代码 |
本地完整运行流程:配好环境变量后在app目录执行npm start,浏览器访问http://127.0.0.1:4174。页面左栏是需求案卷,可切换"需求原文 / 已有上下文 / 返工记录"三个标签并直接编辑;中栏点"盲开发模拟"看 5 个歧义点全部变成返工的时间线,点"澄清后模拟"看返工归零的对比;右栏点"送入 GLM-5.1 炼丹炉"会实时调用模型生成契约单,点"载入实测报告"则直接展示已保存的真实结果。未配置密钥时模拟和载入报告均可正常使用,只有实时调用才需要 API Key。
这一步也给“需求炼金炉”划了一条边界:它负责提取事实、暴露矛盾和起草契约,但不能替产品回答开放问题。产品没确认之前,契约单只是一份分析意见。
用完整案卷代替一句话需求
以前接到需求,我经常只看产品最后发的那段话,再凭经验补全细节。模型当然可以给出规则,但它看到的只是需求最表面的几句话。
这次把材料按“案卷”组织后,体验发生了变化。真正有用的信息往往不在最后一句结论里,而在更早的位置:哪些字段已经存在、哪些接口已经定义、之前返工过几次、哪些规则只在某个活动下才成立。
蓝耘 GLM-5.1 在这个项目里做的事比起草一段接口契约多。模型广场给了清晰的调用名称和 API 示例,MaaS 接口让我能把事实提取、矛盾标记、开放问题列举和契约起草串进同一个应用,模型输出也就从聊天窗口走进了需求工作流。
当然,一次案例不能证明它能处理所有需求。会员、优惠券和叠加只是业务规则中的一类。涉及计费、权限、风控和跨部门流程时,需求材料会更复杂,模型给出的契约也更需要人工和产品核对。
最后:这次挡住的不是某一次返工
这段需求前三次实现都围绕某一层规则展开:全部允许叠加、全部禁用叠加、阈值写死。它们没有完全错,只是没有碰到真正的盲区。返工只在规则互相打架时发生,根因自然也藏在规则的交叉处。
“需求炼金炉”没有替我承担最终决策。它做的是另一件事:逼我把一段口语需求整理成完整案卷,再按照事实、矛盾和开放问题去看需求。GLM-5.1 给出的契约最终是否进入编码,仍然由产品对开放问题的确认决定。
这套流程比“把需求贴进聊天框,复制第一段代码”慢一点,却更像真正的需求澄清。
模型找出了 3 处矛盾和 5 个开放问题,产品确认又替它补上了回归券的未定义分支。比起直接生成一段“标准实现”,我更放心这种分工:GLM-5.1 把理解偏差缩小、把候选契约摆出来,能不能进项目则由产品确认说了算。