本文介绍一个基于 LLM 特征提取 + FAISS 向量检索 + 加权平均的故事点自动估算系统的内部实现原理。

一、故事点估算的问题
故事点(Story Point)是敏捷开发中衡量用户故事相对复杂度的单位。一个团队在做 Sprint Planning 时,需要对每个用户故事给出 1、2、3、5、8、13 这样的 Fibonacci 点数。
传统的做法是Planning Poker——团队成员各自出牌,讨论分歧,最终达成共识。这个过程依赖的是人类的经验和直觉:你之所以认为这个故事是"5 个点",是因为你以前做过类似的故事,心中有一个参照系。
但这个参照系有两个问题:
- 不一致:不同成员对"类似"的理解不同,给的点数可能差 2-3 倍。
- 不可传承:老员工的经验装在自己脑子里,新人来了无从参考。
那么,能否让 AI 来替代这个"有经验的团队成员",自动给出故事点的估算呢?
答案是:可以,前提是给 AI 建立一个结构化的参照系。
二、核心思想:从"类比估算"到向量化
人类估算故事点的本质是类比——把新故事和记忆中做过的旧故事比较,找到最相似的,参考它的点数。
我们把这个过程建模为三个步骤:
新故事 → 理解复杂度 → 找到最相似的基准故事 → 综合估算点数
系统的核心架构如下:

下面我们逐步拆解每个环节。
三、第一步:让 AI 理解故事的复杂度
3.1 为什么不直接用文本 Embedding?
最初版的系统尝试过一种简单方案:把用户故事的标题和描述拼接成一段文本,调用 Embedding 模型(如 text-embedding-3-small)生成一个 1536 维的向量,然后在向量空间中做相似度检索。
这个方案的问题是:
- 语义不等于复杂度:"用户登录"和"用户注销"的文本语义完全不同,但开发复杂度其实差不多。
- 高维向量的信噪比低:1536 维里大量维度跟复杂度无关,反而引入了噪音。
- 不可解释:你无法向用户解释"为什么这个故事是 5 点",因为向量只是数字。
3.2 转向 LLM 特征提取
所以我们换了一个思路:不直接对文本做 Embedding,而是让 LLM 先理解故事内容,提取出与开发复杂度相关的结构化特征。
我们设计了 10 个复杂度特征维度:
|
维度 |
类型 |
值域 |
含义 |
|
frontend_pages |
数值 |
0-3 |
涉及的前端页面/弹窗/表单数量 |
|
backend_interfaces |
数值 |
0-3 |
新增或修改的 API 接口数 |
|
db_change |
布尔 |
是/否 |
是否需要 DDL 操作(建表/加字段) |
|
external_dependency |
布尔 |
是/否 |
是否对接外部系统(微信/支付/短信等) |
|
async_processing |
布尔 |
是/否 |
是否涉及 MQ/定时任务/Webhook |
|
transaction_required |
布尔 |
是/否 |
是否需要事务一致性保证 |
|
business_branches |
数值 |
1-5+ |
主流程中的 if-else/状态机分支数 |
|
permission_control |
布尔 |
是/否 |
是否需要 RBAC 权限控制 |
|
data_migration |
布尔 |
是/否 |
是否需要历史数据迁移脚本 |
|
cache_design |
布尔 |
是/否 |
是否需要引入 Redis 缓存 |
LLM 的 Prompt 中包含详细的判断标准参考,并要求只输出结构化 JSON:
{
"frontend_pages": 2,
"backend_interfaces": 3,
"db_change": "是",
"external_dependency": "否",
"async_processing": "否",
"transaction_required": "是",
"business_branches": 4,
"permission_control": "是",
"data_migration": "否",
"cache_design": "否"
}
关键设计决策:这 10 个维度是通过对大量企业级项目的开发任务进行归纳总结得出的。它们覆盖了影响开发工作量的主要因素:界面数量、接口数量、数据变更、外部集成、异步处理、事务要求、业务分支、权限、迁移、缓存。维度数量选择 10 是在"足够表达能力"和"避免维度灾难"之间的平衡。
3.3 容错设计
特征提取是最依赖 LLM 的环节,所以做了多层容错:
- 指数退避重试:LLM 调用失败时自动重试 3 次(间隔 1s / 2s / 4s)
- JSON 解析兼容:支持直接 JSON 和 markdown 代码块包裹的 JSON(```json ... ```)
- 布尔值兼容:接受 7 种布尔表达方式(是/否、true/false、True/False、1/0)
- 温度控制:temperature=0.1,保证输出稳定性
四、第二步:将理解转为数学——特征向量编码
LLM 输出的特征字典仍然是"人类可读"的,需要转为机器可计算的向量。
这个转换由 FeatureEncoder 完成,规则非常直接:
# 连续型特征:除以上限归一化到 [0, 1]
v[0] = frontend_pages / 3.0 # 前端页面数
v[1] = backend_interfaces / 3.0 # 后端接口数
# 业务分支:线性映射 (1-5+) → [0.0, 1.0]
v[6] = (business_branches - 1) / 4.0 # 1→0, 2→0.25, 3→0.5, 4→0.75, 5+→1.0
# 布尔型特征:直接映射
v[2..5,7..9] = "是"→1.0, "否"→0.0
最终得到一个 10 维归一化向量,每个维度都在 [0, 1] 区间。10 维远小于传统 Embedding 的 1536 维,但每个维度都有明确的业务含义:
- 维度越接近 1.0,代表该方面的复杂度越高
- 两个向量的距离可以直接解释为"复杂度特征的差异程度"
五、第三步:找到最相似的基准故事——FAISS 检索
5.1 为什么用 FAISS?
基准故事库(Baseline Stories)是团队历史上已完成的故事,每个故事都有一个确定的故事点数。我们需要在新故事入库时,快速找到与之最相似的几个基准故事。
FAISS(Facebook AI Similarity Search)是 Meta 开源的高性能向量检索库,专为此类场景设计。
5.2 索引类型选择
我们使用 IndexFlatIP(内积索引)而非更复杂的 IndexIVF 等近似索引,原因很简单:
- 基准故事数量少(通常 12-36 条),暴力搜索的耗时可以忽略不计
- IndexFlatIP 是精确搜索,不会引入近似误差
- 简单可靠,没有额外的参数需要调优
5.3 相似度计算
检索前,系统会对所有向量做 L2 归一化(将向量长度缩放到 1)。归一化后,两个向量的内积就等于它们的余弦相似度:
cos_sim(a, b) = (a · b) / (|a| × |b|) = a · b (因为 |a| = |b| = 1)
检索时取 TopK(默认 K=3)个最相似的基准故事,返回它们的索引和相似度分数。
5.4 索引的持久化
FAISS 索引在内存中运行。为了避免每次重启都重建索引,系统在每次基准故事变更后自动将索引序列化到磁盘:
data/faiss_baseline.index
启动时优先加载已有索引文件,加载失败则从 SQLite 重建。
六、第四步:综合估算——加权平均 + LLM 裁决
6.1 加权平均
拿到 TopK 个相似基准故事及其相似度后,先做一个纯数值的加权平均:
w_avg = Σ(similarity_i × points_i) / Σ(similarity_i)
以相似度作为权重的含义很直观:越相似的故事,其点数对新故事的参考价值越大。
举例:找到了 3 个相似故事——
|
基准故事 |
点数 |
相似度 |
|
"修改用户头像" |
3 |
0.92 |
|
"重置密码功能" |
5 |
0.78 |
|
"多角色权限管理" |
8 |
0.45 |
加权平均:(3×0.92 + 5×0.78 + 8×0.45) / (0.92+0.78+0.45) = 10.26 / 2.15 ≈ 4.77
6.2 Fibonacci 舍入
加权平均的结果通常不是整数,需要舍入到 Fibonacci 刻度。舍入策略是"距离优先,平局取大":
FIB_SCALES = [1, 2, 3, 5, 8, 13]
result = min(FIB_SCALES, key=lambda x: (abs(x - value), -x))
- 4.77 → 最近的是 5(距离 0.23 < 距离 3 的 1.77),结果是 5
- 4.5 → 到 3 和 5 的距离相等(都是 1.5),-x 使 5 排在前面,结果是 5
"平局取大"是一个保守策略——宁可略微高估,也不要低估导致 Sprint 承诺无法完成。
6.3 LLM 二次裁决
纯数值计算虽然精准,但缺乏"全局判断"。一个故事可能数值上与某条基准相似,但在业务上下文上有重要差异。
因此,在加权平均计算出结果后,系统会再调用一次 LLM 做综合裁决。Prompt 包含以下信息:
- 新需求的标题、描述、验收准则
- 复杂度特征(翻译为中文标签)
- 加权平均的数值结果
- TopK 匹配详情(名称、点数、相似度)
LLM 被要求输出结构化 JSON:
{
"estimate": 5,
"confidence_min": 3,
"confidence_max": 8,
"reasoning": "1. 复杂度评估:前端2页+后端3接口+4分支+DML变更,复杂度中等偏高\n2. 基准对比:与'用户管理'相似度92%但缺少外部依赖,估5而非8\n3. 参数权衡:w_avg=4.77在3和5边界,因DB变更取5",
"risk_notes": "外部系统对接时间不可控需提前确认;权限改造可能影响现有功能回归测试"
}
注意 reasoning 要求覆盖三个维度:复杂度评估、基准对比、参数权衡——这确保了估算结果的可解释性。
6.4 降级机制
LLM 可能因为网络故障、API 限流、返回格式错误等原因失败。此时系统不会崩溃,而是降级为纯数值报告:
degraded_result = {
"estimate": round_to_fibonacci(weighted_avg),
"reasoning": "基于加权平均的数值估算结果",
"degraded": True,
}
降级报告明确标记 degraded=true,让用户知道这是"兜底估算"。
七、质量保障:基准故事评价
估算的准确性严重依赖基准故事的质量。如果把一个实际上是 8 点的故事标成了 3 点,那么所有和它相似的新故事都会被低估。
系统在上传基准故事时,会自动进行双通道质量评价:
通道一: 统计层评价
不依赖 LLM,纯数值分析,速度快、结果确定:
梯度检测:按点数(1, 2, 3, 5, 8, 13)分组,计算每组的特征向量质心(centroid)的 L2 范数。理论上,点数越大的故事,其质心范数应该越大(因为复杂度更高)。如果出现"8 点组的质心范数反而小于 5 点组"这种倒挂,说明标定可能有问题。
- 1 处倒挂 → warning(提示但不阻断)
- 2 处及以上 → error(阻断入库)
归属检测:对每条故事,计算其特征向量到各组质心的距离。如果一条"3 点故事"的特征向量,距离"5 点组"的质心比距离"3 点组"的质心更近,说明它可能被标低了。
- 归属异常比例 > 20% → error
- 有异常但 <= 20% → warning
综合评分:起评 100 分,每个 error 扣 25 分,每个 warning 扣 10 分。评分 >= 60 且无 error 级问题才算通过。
通道二: LLM 深度分析
当统计层发现异常时,会触发 LLM 做定性分析。LLM 看到的是按点数分组的故事情息,它可以从业务角度判断"为什么这两组故事的特征接近但标了不同的点"。
两通道评价体现了"能用简单方法解决就不用复杂方法"的原则。统计层覆盖了 90% 的质量问题,LLM 只在有异常时才介入做深度分析——既保证了效率,又弥补了纯数值方法在语义理解上的不足。
八、批量估算与拆分建议
8.1 批量估算
除了逐条估算,系统支持通过 Excel 批量导入多个用户故事做一次性估算:
- 下载批量估算模板(含 ID / 标题 / 描述 / 验收标准四列)
- 填入待估算的故事
- 上传后系统逐条处理,生成包含估算结果的 Excel
- 结果 Excel 包含:估算点数、置信区间、估算依据、风险提示
8.2 拆分建议
当估算结果触及上限时,系统会自动建议拆分:
- 硬上限:加权平均值 w_avg > 13 → 必提示拆分
- 软上限:LLM 裁决结果为 13 且 w_avg >= 10.5 → 建议考虑拆分
拆分建议会明确指出当前加权平均值和推荐方向,但不会自动拆分故事——拆分是产品决策,应该由人来完成。
九、关键设计决策总结
回顾整个系统,有以下几个关键的设计选择值得强调:
1) 特征提取 vs 文本 Embedding
放弃通用的文本 Embedding,选择 LLM 提取 10 维业务特征。代价是一次 LLM 调用,换来的是:
- 10 维向量远小于 1536 维,FAISS 检索快得多
- 每个维度有明确的业务含义,结果可解释
- 维度与复杂度直接相关,信噪比高
2) 数值计算 + LLM 裁决的双层架构
没有直接让 LLM 端到端地"看故事给点数"(那样结果不稳定),也没有纯靠数值计算(那样缺乏语义判断)。加权平均提供数值锚点,LLM 在此基础上做语义调整——两者互补。
3) 质量门禁
不比简单的 KNN 分类器,系统对输入数据做了严格的质量把控。梯度检测和归属检测在上传时就能发现标定问题——这源自一个朴素的认识:Garbage In, Garbage Out。
4) 容错降级
每个依赖外部服务的环节都有降级路径:LLM 特征提取失败 → 自动重试;LLM 报告生成失败 → 降级为纯数值报告。系统永远不会因为 AI 服务不可用而"白屏"。
十、总结
回到标题的问题:故事点是否可以用 AI 自动估算?
答案是:在一个有良好基准故事库的团队中,可以。
本系统不是要替代 Planning Poker 中团队的讨论和共识过程,而是提供一种结构化的参考——在新人经验不足、团队对某个故事的复杂度有分歧时,给出一个基于历史数据的、有理有据的估算建议。
核心思路可以概括为一句话:让 LLM 理解故事的复杂度,让 FAISS 找到最相似的参照,让加权平均给出数值锚点,再让 LLM 做最终的语义裁决。
2116

被折叠的 条评论
为什么被折叠?



