用 AI 做需求澄清:从「找问题」到「辅助决策」的演进

背景:需求澄清是一个被低估的成本中心

做过研发的人都知道,需求评审会结束后,研发真正动手写代码,往往还要经历一轮”追着产品问”的过程。这些问题有时候很小(一个按钮文案),有时候很大(整个业务逻辑是否成立)。问题问得晚,代价是已经写了一半的代码要返工;问题没问到,代价是上线后出 bug 或功能偏差。

我们在 Codelix 中尝试用 AI 来解决这个问题。本文记录了从立项到多轮迭代的全过程,包括遇到的坑、做的取舍,以及最终效果。


第一阶段:打通主链路

最初的设计

最简单的思路:拿到 TAPD 需求单 → 调 AI 分析 → 输出澄清点列表 → 人工确认。

我们接入了内部的 Knot 智能体,它有联网和 RAG 能力,能结合代码库上下文分析需求。技术选型:

层次 方案
后端 Go,集成进 agent-server
Knot 通信 Go SSE 客户端
存储 本地 JSON 文件,按 storyId 分目录
前端 React SPA,新增路由 /clarification

第一个版本跑通了,但踩了几个坑。

SSE 解析的坑

Knot 的响应协议是 SSE,但格式和标准 SSE 不完全一样:

  • 标准格式是 data: {...}(有空格),Knot 是 data:{...}(无空格)
  • 某些单行内容超过 bufio.Scanner 默认的 64 KB,直接截断导致 JSON 解析失败
  • Knot 有时在一次响应里输出多个 JSON 数组,有时还用 ```json ``` 代码块包裹

解决方案:

  1. 扫描缓冲区从 64KB 扩到 4MB
  2. 解析时优先提取代码块,再回退到文本中所有 [...] 候选,取首个可解析的非空数组
  3. 兼容 contentdeltadelta.contentdelta.text 四种字段格式

还有一个问题:LLM 生成的 JSON 字符串里有时含未转义的英文双引号,导致反序列化失败。解决方法是先做一次轻量的 sanitizeJSONQuotes 修复再重试。

TAPD URL 兼容性

TAPD 需求单有两种 URL 格式:

  • 旧版:https://tapd.woa.com/{workspaceId}/prong/stories/view/{storyId}
  • 新版:https://tapd.woa.com/tapd_fe/{workspaceId}/story/detail/{storyId}

返回结构也有四种变体:带 Story 嵌套、直接字段 map、data 数组等。评论拉取失败时不阻断主流程,降级为”无评论”继续处理。

版本管理:patch-based 版本链

澄清点会被多人编辑,需要保留完整历史。我们设计了 patch-based 版本链:

  • versions[0] 是 Knot 分析的全量 baseline,只读
  • 后续每次编辑追加 patch 版本,只记录 delta
  • Resolve(N) 从最近 checkpoint 起重放 patch 链还原到版本 N
  • patch 版本累计 10 个时,自动落一次全量 checkpoint

并发控制:前端提交 baseVersion,后端做 CAS 校验。不同 item 的修改自动 merge,同一 item 冲突返回 409,提示用户刷新重试。

Knot 凭证安全设计

用户首次使用时需要输入 Knot API Token,但不能把 token 直接存到 localStorage——在公用机上会造成泄露。

最终设计是服务端会话:

  • 用户输入 Token + RTX 用户名 → 后端创建 clarification_session,写入 HttpOnly cookie(SameSite=Lax, Secure, Max-Age=15552000
  • 浏览器只保存 cookie,不保存 token 明文
  • 会话有效期 6 个月,到期或 Knot 返回 401/403 时立即失效
  • 检测到 token_invalid 时服务端主动清空 session 并清 cookie,前端自动弹凭证重新输入弹窗

这里有个细节:平台化后产物生成阶段使用 claude-internal CLI 时,需要把 knotApiToken 透传给远程机作为 CLI 鉴权凭证(映射为 CODEBUDDY_API_KEY)。所以 token 必须保留在服务端 session 里直到会话过期,不能在完成分析后就清掉。


第二阶段:平台化——从本地工具到协作平台

核心链路跑通后,几个问题开始暴露:

  1. 数据存在本地 JSON 文件,多人协作时一致性难保证
  2. 完成澄清后缺少产物生成的闭环
  3. 没有历史记录和指标

于是启动平台化改造:所有澄清数据迁入 platform-server 的数据库,本地不再是真源。

数据模型

核心表:

  • clarification_runs:每次分析一条记录,含状态机和 is_primary 标志
  • clarification_items:当前最新澄清点状态
  • clarification_item_versions:完整编辑历史,每次变更记 before_snapshot + after_snapshotfield_diffs 读时计算,避免三者不一致)
  • clarification_item_comments:每条澄清项的讨论回复
  • clarification_artifacts:生成产物(summary、tech_doc、各端技术文档、流程图、协议、tapd_update 等)
  • clarification_knowledge_records:完成澄清后的知识沉淀

状态机

1
2
3
editing → ready_to_complete → completing → completed

reopened → editing

所有澄清点非 pending 时自动推进到 ready_to_complete,允许点击”完成澄清”。完成后允许 reopen 重新进入编辑。

完成澄清的并发控制

多人协作时,”完成澄清”只能有一个人成功触发一次。用 DB CAS 实现:

1
2
3
UPDATE clarification_runs
SET status = 'completing', completing_by = ?, completing_started_at = NOW()
WHERE run_id = ? AND status = 'ready_to_complete'

RowsAffected = 0 说明已有其他人先点,前端提示”已有其他人正在完成澄清”。

大模型调用边界的划分

这里有一个关键设计决策:两类 LLM 调用必须严格分开

场景 调用来源 原因
首次分析 TAPD 需求 Knot 需要联网、RAG、内部知识召回
完成澄清后的产物生成 内部 LLM(claude-internal) 只基于已确认澄清结论,避免引入未确认内容

协议是唯一例外——它需要结合真实协议仓上下文,允许单独调用 Knot 生成(见后文)。

产物生成

完成澄清后异步生成:澄清总结 + 整体技术文档 + 各端技术文档(iOS/Android/Kuikly/Web/后台,只在该端有已决策条目时生成)+ 业务流程图(Mermaid)+ 后台协议 + TAPD 正文更新内容(tapd_update)。

前端 4s 轮询直到所有产物就绪。单个产物失败不阻断其他产物。

TAPD 回写:状态必须持久化

完成澄清后,支持将澄清结论评论到需求单,并把 tapd_update 产物写回需求正文(展示左右 diff,人工确认后写回)。

这里有个重要设计:回写状态必须持久化,不能只存前端 UI 状态。

1
2
tapd_comment_sync_status:  pending | in_progress | synced | skipped
tapd_content_sync_status: pending | in_progress | synced | skipped

用户中途关页面后重新进入,应能恢复到正确状态:

  • 弹窗未选就关页面 → 入口还在(pending
  • 点了自动更新,后端未完成就关页面 → 后端继续跑;回来看到 syncedpending(失败可重试)

有一个严格约束:diff 右侧只允许来自 tapd_update artifact,禁止用 tech_docsummary 等替代写回。这些产物的生成目标不同,混用会导致需求正文变成技术文档风格。


第三阶段:体验升级——从「找问题」到「辅助决策」

问题的核心

平台化之后,真正使用时发现 AI 输出的澄清点质量有根本性问题。典型的旧格式:

阈值确定责任方和灰度策略未明确。

这种描述能指出风险,但产品拿到这句话之后还是不知道怎么决策——它指出了不确定性,但没有给决策路径。

推荐答案和选项

新设计的澄清点包含:

  1. 推荐答案(结论态表达)
  2. 推荐理由 + 置信度
  3. ABCD 选项,每个选项有描述和推荐度
  4. 每个选项下的快捷动作:回复并澄清(自动写入评论)和 AI 更新澄清点
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"description": "置信度阈值的确定责任方和灰度策略未明确。",
"recommendedAnswer": "推荐由算法团队提供默认阈值,产品确认灰度策略,后台按配置读取并支持动态调整。",
"options": [
{
"key": "A",
"label": "算法给默认阈值,产品确认灰度策略",
"recommended": true,
"reason": "符合能力归属,研发实现风险最低",
"rewriteDescription": "推荐由算法团队提供默认阈值,产品确认灰度策略,后台按配置读取并支持动态调整。"
}
]
}

rewriteDescription 是 AI 给出的”可以直接替换澄清点原文”的结论态文案。点击 AI 更新澄清点不是一键直替,而是弹出确认框,提供三个选项:

  • 取消:关闭,不做改动
  • 直接使用:用 rewriteDescription 替换原文 + 自动标记为已澄清 + 写系统评论留下来由(🤖 采用 AI 推荐方案【A】:...
  • 编辑再替换:把 rewriteDescription 预填进编辑框,用户确认后再提交,不自动设为已澄清

这个交互设计的考量是:把”一键直替”变成”可控的二次确认”,避免用户因为误点进入无法回退的状态。

归属体系扩展

原始归属只有”后台开发”,实际上算法团队(推荐排序、模型策略、置信度评估)与后台开发差异很大。本轮新增:

  • 算法:独立于后台开发,用于 AI 能力、推荐系统相关澄清点
  • 数产:数据产品团队,负责上报方案相关确认

同时 Knot prompt 新增了上报专项检查,6 类触发条件:

  1. 关键新增页面未提及曝光上报
  2. 重要点击行为未提及点击上报
  3. 描述了上报点但未给出上报 ID
  4. 上报触发时机含糊(”上报播放”未区分开始/结束/心跳)
  5. 上报字段不完整
  6. 已有类似点位,需确认复用或新增

命中任一条件,自动生成 module=数据上报, attribution=["产品","数产"] 的澄清项。

协议生成:从推测到 Knot 生成

原来协议产物是用当前上下文推测的,容易出现编造接口形态、字段结构不符合真实协议仓、忽略历史约束等问题。

本轮把 api_contract 从普通产物链路拆出,单独调用 Knot 生成,传入:需求原文 + 已确认澄清结论 + 相关后台代码 + 协议仓上下文 + 历史修改记录。协议内容中明确标注”待确认项”,不允许把不确定内容写成确定结论。

Knot 模型一致性

一个容易被忽略的问题:发起分析时选的 Knot 模型,在重新分析和协议生成时应该复用。否则用户会看到同一个 run 里不同阶段的产物风格迥异。

实现:clarification_runs 增加 knot_model 字段,分析时写入,重新分析和协议生成时从 run 读取。老数据没有该字段时,fallback 到系统默认 Knot 模型。

前端详情页显示”本轮分析模型:deepseek-v4-flash”,便于追溯。

推荐确认人的坑

让 Knot 基于代码提交历史输出推荐开发人员,一开始踩了个明显的坑:

Knot 把需求单处理人(产品经理 morsonxie)归到了 Kuikly 客户端开发。

根因:Knot 在代码线索不足时会回退到需求单的处理人/关注人,而这些经常是产品经理。

修复:prompt 明确约束——只推荐开发角色(后台/算法/iOS/Android/Kuikly/Web)、推荐依据必须是代码提交历史和模块归属、证据不足时输出”暂无可靠推荐”而非编造。

澄清项评论

每条澄清点下方有独立的讨论回复区(clarification_item_comments):

  • 按新到旧排序,展示作者 · 时间 · 内容
  • 卡片底部提供回复输入框,回车或点「回复」提交
  • 生成澄清总结(summary artifact)时,会读取每条澄清点的回复评论,将用户在回复里补充的最终决策、原因或约束吸收进结论,避免”只复述原始问题”

工作量变化与 ROI 提醒

每条澄清点可以填写:

  • effort_delta_d:工作量变化(天数,可为负)
  • effort_reason:原因说明
  • roi_review_required:是否需要产品评估 ROI

当本次澄清累计开发工作量变化 > 0.5D 时,页面顶部显示显著警告横幅:

本次澄清带来超过 0.5D 的开发工作量变化,请提醒产品评估 ROI

这个信息同时出现在”完成澄清”按钮附近、TAPD 评论摘要里、以及数据看板的 ROI 风险报表中。

参考文档来源

每条澄清项下方支持展示 AI 分析时参考的来源(sourceRefs):

1
2
3
4
5
6
{
"type": "iwiki",
"title": "xxx 系统设计文档",
"url": "https://...",
"snippet": "相关段落摘录"
}

支持 iwiki / gongfeng / tapd / doc / code 五种类型。Knot prompt 要求输出 sourceRefs 可选字段,解析后落库,前端在澄清点卡片中展示”参考来源”折叠区块。

产物重试

产物生成失败后,不需要重新完成整轮澄清,支持:

  • 单个失败产物”重试”按钮
  • 产物区”重试全部失败产物”
  • 重试不新建同类型 artifact 记录,直接更新 status,保留 artifactId 不变,避免前端轮询引用失效
  • retry_count / last_retry_at 记录历史

并发保护:同一产物 pending 时不允许重复触发重试。

技术文档改为手动触发

完成澄清后,原来整体技术文档和各端技术文档(iOS/Android/Kuikly/Web/后台)会自动批量生成,token 成本较高,且模型质量口径和协议不一致(协议走 Knot,技术文档走内部 LLM)。

本轮调整:

  • tech_doc 及各端 tech_doc_* 从自动批量生成中剔除,改为 idle 状态预插入产物列表
  • 产物区显示”未生成(手动触发)”+ 「生成」按钮,用户按需单独触发
  • 技术文档同样改走 Knot 生成(和协议对齐),并追加”去过程化”约束(不输出 Knot 的检索/思考过程,直接出正文)
  • 自动批量仍保留:summary / tapd_update / flowchart / api_contract

这个决策的核心是:技术文档的质量需要代码库上下文,Knot 更合适;但每次完成澄清后全自动生成 5-6 份文档成本太高,改为按需手动触发可以控制成本。


第四阶段:统计口径和 AI 效果度量

采纳率的坑

早期采纳率直接按条目算,同一个需求单重新分析几次、每次都上报,会重复计数。

修正口径:

  • story_id 去重,只取最近一次上报的结果
  • 公式:采纳率 = (采纳 + 搁置) / (采纳 + 拒绝 + 搁置),pending 不进分母
  • 搁置视为”AI 建议未被否决”,计入分子

技术实现遇到了 MySQL 优化器 bug:用 IN (SELECT COALESCE(correlated_sub1, correlated_sub2)) 嵌套相关子查询时,MySQL 会静默返回空结果集,不报错不警告。改为非相关派生表 INNER JOIN 写法后解决。

漏报率

“漏报”定义为用户在 AI 初始分析之外手动新增的澄清点。

clarification_items 新增 origin 字段(ai / manual),漏报率 = 人工新增澄清点数 / 所有澄清点数,可以持续评估 AI 分析质量的变化趋势。

完成引导弹窗

典型用户行为问题:全部澄清点都确认完了,但忘记点”完成澄清”,导致产物没有触发生成。

解决:当最后一个 pending 澄清点被确认时,立即弹窗引导。触发条件严格限定为:本次更新前 pending > 0,更新后 pending = 0,且 run 未完成。刷新页面、在已完成的 run 上改判某项都不会误弹。

主按钮点击记 complete_now 埋点,进入 AI 效果漏斗统计。

各环节耗时统计

clarification_runs 表新增时间戳字段:

  • analysis_ms:Knot 分析耗时(毫秒)
  • first_confirmed_at:第一个澄清点被确认的时间
  • artifacts_generated_at:自动批量产物全部生成完成的时间
  • tapd_comment_synced_at / tapd_content_synced_at:TAPD 回写完成时间

这些字段在各状态变更点写入,支持看板展示:平均分析耗时、分析完成→首个确认间隔、完成→自动产物、完成→TAPD 评论等。

数据看板

最终包含三个维度(以纵向 Section 排列,不是 Tab 切换):

质量:采纳率(按需求去重,口径 tooltip 说明)、按归属拆分采纳率、漏报率趋势、工作量 ROI 风险(触发次数 + 累计工作量变化)

效率:各环节耗时均值(分析时长、人工确认时长、产物生成时长、TAPD 同步时长)、完成趋势、流程漏斗(发起→完成→评论→写回)

AI 效果:曝光 → 操作 → 完成漏斗,推荐项 vs 非推荐项点击率,rewriteDescription 直接使用率(区分”直接使用”和”编辑再替换”)

埋点事件:suggestion_shown(曝光,按 session+澄清点 sessionStorage 去重)、reply_and_confirmrewrite_and_confirm(直接使用)、rewrite_fallback(编辑再替换/无 rewriteDescription 的兜底)、complete_now(完成引导弹窗点击)。取消等未提交动作不记。


第五阶段:体验细节和 UX 打磨

主版本(Primary Run)

多次重新分析后,用户可能有多个 run,但之前确认过的那轮不应该因为”重新分析了一下”就从默认视图消失。

引入 is_primary 字段(clarification_runs.is_primary):

  • 首次分析自动设为主版本
  • 后续重新分析产生的新 run 默认 is_primary=0,不抢占主版本
  • 用户可手动”设为主版本”或”取消主版本”
  • 进入详情页时,优先打开主版本;没有主版本时回退到最新 run

顶部”重新分析”按钮增加确认弹窗,明确说明:若当前正在查看主版本,分析完成后不会自动切走当前页面,新 run 只出现在下拉选择器里。

首页筛选:我的需求澄清 / 全部澄清

随着使用增多,首页全量列表变得难以查找。新增双选切换:

  • 我的需求澄清clarification_runs.created_by = 当前用户,默认选中
  • 全部澄清:保持原有全量列表

接口扩展:GET /api/story-clarifications?scope=mine&username=xxx

UI 从工具页升级为 Dashboard

原来详情页是一个功能完整但视觉朴素的”工具页”。本轮参照设计稿做了改版,核心变化:

  • Sticky Header:需求标题、当前状态、澄清进度(已确认/总项)、右侧主操作区
  • Quick Nav:页内锚点导航(需求总览 → AI 分析摘要 → 澄清点矩阵 → 生成产物 → 需求更新)
  • Section 分组:白色卡片 + 浅灰背景 + 统一圆角阴影
  • 采纳率指标卡:已采纳 / 不采纳 / 搁置 / 待确认 的计数和简化采纳率
  • 澄清点分组:按归属或状态分组折叠,高优先级标签醒目

改版策略是”套设计稿的页面叙事和视觉层级,保留我们现有更强的真实能力”——run/version 双层浏览、item 历史记录、sourceRefs 引用来源、完成澄清后的产物区都保留,不为了贴近设计稿而删掉。

知识沉淀

完成澄清后,异步将已决策条目写入 clarification_knowledge_records,沉淀:

  • 澄清点描述、归属、最终决策(adopted/rejected/shelved)
  • 决策原因、谁确认的、什么时间
  • 工作量变化、ROI 是否触发
  • 参考来源 refs

后续可基于这些沉淀做相似需求召回、高频模糊点归类、采纳模式分析。


几个关键设计决策的反思

JSON 是唯一真源,MD 是展示格式:MD 导入导出存在格式损失,不能成为系统真源。版本链、并发控制、历史重建都依赖 JSON 结构。

不允许物理删除澄清项:所有”不处理”的澄清点通过 confirmResult=rejected 表达,在列表中显示删除线。多人协作时如果有人删了一条其他人正在讨论的澄清点,会造成上下文丢失,很难恢复。所有确认状态都应可撤销。

Knot 只做分析,协议是例外:产物生成只用内部 LLM,避免引入未确认内容。唯一例外是接口协议,它需要结合真实协议仓上下文,允许单独调用 Knot 生成。技术文档后来也改走 Knot,但改为手动触发。

完成澄清不是终点,是第二阶段入口:所有澄清点非 pending → 可以完成澄清 → 触发产物生成 → 评论回写 TAPD → 更新需求正文。这条链路里每个节点都需要持久化状态和可恢复设计,不能只靠前端 UI 状态。


小结

这个方向从最初的”AI 找问题列表”演进到”AI 辅助决策工具”,核心变化:

  • 澄清点从问题态变为结论态,推荐答案 + ABCD 选项 + rewriteDescription,产品可以直接选择采纳
  • 每端输出推荐开发确认人(基于代码提交历史,不是需求单处理人)
  • 上报专项检查自动发现需求里缺失的埋点说明
  • 协议和技术文档改由 Knot 生成,质量更接近真实
  • 澄清项下有独立评论区,总结生成时吸收评论内容
  • 工作量变化 + ROI 提醒给产品早预警
  • 数据看板让采纳率、漏报率、各环节耗时可量化追踪

最大的教训:功能上线不等于体验完成。第一版打通了主链路,但产品真正拿到 AI 输出后,”这句话告诉我了什么,但我还是不知道怎么决策”的反馈,才是后续所有体验升级的起点。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道