为什么你的代码总在返工?因为这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_URLhttps://maas-api.lanyun.net/v1 LANYUN_MODEL/maas/zhipuai/GLM-5.1项目本身使用 Node.js因此实际调用代码也直接使用内置fetchconstendpoint${process.env.LANYUN_BASE_URL}/chat/completions;constresponseawaitfetch(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),});constrawBodyawaitresponse.text();if(!response.ok){thrownewError(模型接口返回${response.status}${rawBody.slice(0,500)});}letpayload;try{payloadJSON.parse(rawBody);}catch{thrownewError(模型接口未返回合法JSON响应);}constcontentpayload?.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_couponfalse 且未传 coupon_id 视为不使用优惠券 - 大促期间叠加规则引擎返回 is_stacking_blockedtrue 时拒绝 coupon_id - 当 amount 动态阈值时忽略自动选券仅在传入 coupon_id 时校验并应用。 验收标准 1. 金牌会员非大促期间下单存在多张满足 min_amount 的券时服务端自动应用面额最大者。 2. 大促期间下单叠加规则引擎对所有等级返回 is_stacking_blockedtrue传入 coupon_id 应返回 422。 3. 银牌会员下单传入 coupon_id 与会员等级券叠加时返回 422 并提示不可叠加。 4. 回归券规则在产品确认前不进入计价链路available-coupons 暂不返回回归券。 5. 前端券角标展示可用券数量与最大面额不满足 min_amount 的券不计入。 实现风险 1. 500 阈值写死会再次被产品要求动态化应改为读取配置而非常量。 2. “面额最大”不等于“最优”多券组合可能让用户实际优惠更高需产品确认是否允许组合。 3. 回归券与会员等级券若互斥规则未定上线后极易引发资损建议在契约确认前不实现回归券叠加。 4. 大促与日常逻辑分叉若共用同一计价入口需注意回归测试覆盖两条路径。模型给出的契约草案把“是否叠加”收敛成一个叠加规则引擎的输入输出500 阈值改为读取配置而非常量。它已经抓到了“规则不该写死”这个根因。但我把它拿给产品确认时又发现了一个新问题契约单里把回归券相关任务也拆了出来可回归券的互斥规则原文根本没定义。如果照着契约单直接实现回归券等于模型替产品做了一个未被确认的决定。这次模型找对了矛盾契约草案却没法覆盖所有分支。最后我把回归券相关任务标记为“待产品确认前不实现”available-coupons 接口暂不返回回归券。这样既保留了模型整理出的事实和开放问题又避免把未定义的规则写进代码。// 契约单驱动下的任务分流回归券在产品确认前不进入计价链路constreportparseAlchemistReport(content);consttasksreport.task_breakdown;// 1. 把依赖“回归券发放与校验”的任务单独拎出来暂不进入开发列constblockedtasks.filter(tasktask.depends_on?.includes(回归券发放与校验));constreadytasks.filter(task!blocked.includes(task));// 2. 把开放问题回执给产品逐条确认后才解锁 blocked 任务constopenQuestionsreport.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(ambiguityCount5){// 编码 - 联调暴露第一个歧义 - 返工 - 暴露下一个 - … - 带病上线return{reworkCycles:ambiguityCount,// 5 次返工defectsShipped:1,// 带病上线 1 个deliveredOnTime:false,};}// 澄清后编码前把歧义点逐条和产品确认实现阶段零返工exportasyncfunctionsimulateClarifiedImplementation(ambiguityCount5){// 澄清(逐条 resolved) - 编码 - 联调通过 - 上线return{ambiguitiesResolvedBeforeCoding:ambiguityCount,reworkCycles:0,defectsShipped:0,deliveredOnTime:true,};}模拟没有只检查“函数没有抛错”而是直接比较返工轮次、带病上线和歧义点解决数。例如澄清后开发的核心断言是constresultawaitsimulateClarifiedImplementation(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 把理解偏差缩小、把候选契约摆出来能不能进项目则由产品确认说了算。

相关新闻