第1874篇:用户故事地图的AI生成——从用户目标到功能列表的自动化
第1874篇:用户故事地图的AI生成——从用户目标到功能列表的自动化
用户故事地图这个工具,在国内推广得其实不算好。我问过不少团队,大多数的回答是:我们有需求文档、有PRD,用不着这玩意儿。
然后等他们开发到一半,才发现一个很典型的问题:每个功能单独看都没问题,但拼在一起,用户的完整使用流程里有几个关键环节是空白的。
用户故事地图解决的正是这个问题:它不是功能列表,而是从用户视角连续流动的体验路径,功能是附在路径上的。这两者的区别,会直接影响你排期时哪些东西必须MVP一起出,哪些可以后续迭代。
今天说的是:怎么用LLM自动生成用户故事地图,以及这个自动化过程里的坑和取舍。
用户故事地图的结构
先说清楚结构,避免后面代码看不懂:
举一个外卖平台的例子:
- 用户目标(Activities):浏览餐厅 → 下单 → 跟踪配送 → 完成评价
- 用户任务(Tasks):[浏览餐厅下]搜索餐厅、查看菜单、查看评价
- 用户故事(Stories):作为用户,我希望能按菜系筛选餐厅,这样我能快速找到想吃的
- 验收条件:给定在首页,当用户点击"筛选",则显示菜系分类列表,选择后列表刷新
地图的价值在于横向视角——你能看到"下单"这个活动是否完整覆盖了用户需要做的所有任务,有没有漏掉东西。
用LLM生成地图的核心方案
我的方案分三步:生成活动层→展开任务层→生成故事和优先级。
Step 1:从产品描述生成活动层
@Service
public class UserStoryMapGenerationService {
private final ChatClient chatClient;
/**
* 第一步:从产品描述生成用户旅程活动层
* 返回的是按时序排列的活动节点
*/
public List<UserActivity> generateActivities(String productDescription,
String targetUserPersona) {
String prompt = """
你是一位专业的UX研究员和产品设计师,擅长用户旅程分析。
产品描述:%s
目标用户画像:%s
请生成这个产品的用户活动层(Activities Layer)。
要求:
1. 活动层代表用户的主要目标,从用户第一次接触产品到核心价值实现
2. 按照时序排列(用户实际使用的先后顺序)
3. 每个活动用动词短语表达(用户视角)
4. 控制在5-10个活动,覆盖完整用户旅程
5. 不要遗漏"注册/登录"这类基础活动
输出JSON:
{
"activities": [
{
"id": "A001",
"name": "注册与入门",
"description": "用户首次进入产品,了解功能并完成注册",
"order": 1,
"userGoal": "用户希望快速上手产品",
"estimatedFrequency": "once" // once/daily/weekly/occasional
}
]
}
""".formatted(productDescription, targetUserPersona);
String response = chatClient.call(prompt);
return parseActivities(response);
}
/**
* 第二步:为每个活动展开任务层
*/
public List<UserTask> generateTasks(UserActivity activity,
String productContext) {
String prompt = """
产品背景:%s
用户活动:%s(%s)
请为该活动展开用户任务层(Tasks Layer)。
要求:
1. 任务是用户完成该活动需要执行的具体操作
2. 任务应该完整覆盖该活动,不要遗漏关键步骤
3. 特别注意错误处理和异常路径(如:注册时邮箱已存在)
4. 每个任务用动词短语表达
5. 每个活动下3-8个任务
输出JSON:
{
"tasks": [
{
"id": "T001",
"activityId": "%s",
"name": "填写注册信息",
"description": "用户填写用户名、邮箱、密码",
"order": 1,
"pathType": "happy_path", // happy_path/error_path/edge_case
"complexity": "low" // low/medium/high
}
]
}
""".formatted(productContext, activity.getName(),
activity.getDescription(), activity.getId());
String response = chatClient.call(prompt);
return parseTasks(response);
}
}Step 2:生成用户故事并分配优先级
/**
* 第三步:为每个任务生成用户故事,并根据价值/复杂度打优先级
*/
public List<UserStory> generateStories(UserTask task,
String productContext,
String mvpConstraints) {
String prompt = """
产品背景:%s
MVP约束(时间/资源限制):%s
用户任务:%s
任务描述:%s
请为该任务生成用户故事(User Stories)。
要求:
1. 格式:作为[角色],我希望[功能],这样[价值]
2. 每个任务可有1-3个故事(覆盖主流程和重要变体)
3. 为每个故事分配优先级:
- MVP(最小可行产品必须有)
- NEXT(第二迭代)
- FUTURE(后续版本)
4. 给出故事点估算(1/2/3/5/8/13)
5. 给出验收条件(Given-When-Then格式)
优先级判断标准:
- 没有这个功能用户就无法使用产品的 → MVP
- 影响用户留存和满意度的核心体验 → MVP或NEXT
- 增强体验但不影响核心流程的 → NEXT或FUTURE
输出JSON:
{
"stories": [
{
"id": "S001",
"taskId": "%s",
"title": "用户注册",
"statement": "作为新用户,我希望用邮箱注册账号,这样可以保存我的数据",
"priority": "MVP",
"storyPoints": 3,
"acceptanceCriteria": [
{
"given": "用户在注册页面",
"when": "填写有效邮箱和符合规则的密码",
"then": "账号创建成功,跳转到新手引导"
}
]
}
]
}
""".formatted(productContext, mvpConstraints,
task.getName(), task.getDescription(), task.getId());
String response = chatClient.call(prompt);
return parseStories(response);
}Step 3:识别MVP边界
这是整个过程最有价值的一步——自动识别哪些故事必须在MVP中,哪些可以延后:
/**
* 分析用户故事地图,找到MVP的最小完整切片
* 所谓"完整切片":覆盖所有活动的最基础路径,
* 让用户能完成从头到尾的完整体验
*/
public MvpSlice identifyMvpSlice(UserStoryMap fullMap,
String mvpGoal,
int mvpDaysConstraint) {
String prompt = buildMvpAnalysisPrompt(fullMap, mvpGoal, mvpDaysConstraint);
String analysis = chatClient.call(prompt);
return parseMvpSlice(analysis);
}
private String buildMvpAnalysisPrompt(UserStoryMap map,
String goal,
int days) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("MVP目标:%s\n", goal));
sb.append(String.format("时间约束:%d天\n\n", days));
sb.append("完整用户故事地图:\n");
map.getActivities().forEach(activity -> {
sb.append(String.format("\n活动:%s\n", activity.getName()));
activity.getTasks().forEach(task -> {
sb.append(String.format(" 任务:%s\n", task.getName()));
task.getStories().forEach(story -> {
sb.append(String.format(" 故事[%s %d点]:%s\n",
story.getPriority(), story.getStoryPoints(),
story.getTitle()));
});
});
});
sb.append("""
请分析并确定MVP切片:
1. 识别"骨干路径"(Walking Skeleton):
每个活动中,让用户能完成基础操作的最少故事集合
2. 计算MVP工作量:
仅包含P0故事的总故事点
3. 如果超出时间约束,建议削减哪些故事
注意:不能破坏用户完整旅程的连贯性
4. 标注哪些故事之间有强依赖(A必须在B之前完成)
5. 识别"隐藏MVP":
那些没有被标为MVP但去掉后会导致核心流程断裂的故事
输出:
- MVP故事列表(含故事ID和原因)
- MVP总故事点
- 建议切掉的故事(如超出预算)
- 故事依赖关系图
- 风险提示
""");
return sb.toString();
}把地图渲染出来
生成了数据之后,还需要让它可读。我写了一个简单的Mermaid渲染器:
@Component
public class UserStoryMapRenderer {
/**
* 把用户故事地图渲染为Mermaid格式,便于在文档中展示
*/
public String renderToMermaid(UserStoryMap map) {
StringBuilder sb = new StringBuilder();
sb.append("graph LR\n");
// 活动层(顶部横向排列)
sb.append(" subgraph Activities\n");
sb.append(" direction LR\n");
map.getActivities().forEach(activity -> {
String actId = "ACT_" + activity.getId();
sb.append(String.format(" %s[\"%s\"]\n", actId, activity.getName()));
});
sb.append(" end\n\n");
// 连接活动节点(时序)
List<UserActivity> activities = map.getActivities();
for (int i = 0; i < activities.size() - 1; i++) {
sb.append(String.format(" ACT_%s --> ACT_%s\n",
activities.get(i).getId(), activities.get(i + 1).getId()));
}
// 任务层
map.getActivities().forEach(activity -> {
activity.getTasks().forEach(task -> {
String taskId = "TASK_" + task.getId();
String actId = "ACT_" + activity.getId();
sb.append(String.format(" %s[\"%s\"]\n", taskId, task.getName()));
sb.append(String.format(" %s --> %s\n", actId, taskId));
});
});
return sb.toString();
}
/**
* 渲染为Markdown表格格式(更适合文档分享)
*/
public String renderToMarkdownTable(UserStoryMap map) {
StringBuilder sb = new StringBuilder();
// 表头(活动层)
sb.append("| 优先级 |");
map.getActivities().forEach(a ->
sb.append(String.format(" **%s** |", a.getName())));
sb.append("\n");
// 分隔线
sb.append("|--------|");
map.getActivities().forEach(a -> sb.append("--------|"));
sb.append("\n");
// MVP行
sb.append("| MVP |");
map.getActivities().forEach(activity -> {
String mvpStories = activity.getAllStories().stream()
.filter(s -> "MVP".equals(s.getPriority()))
.map(UserStory::getTitle)
.collect(Collectors.joining("<br>"));
sb.append(String.format(" %s |", mvpStories.isEmpty() ? "-" : mvpStories));
});
sb.append("\n");
// NEXT行
sb.append("| NEXT |");
map.getActivities().forEach(activity -> {
String nextStories = activity.getAllStories().stream()
.filter(s -> "NEXT".equals(s.getPriority()))
.map(UserStory::getTitle)
.collect(Collectors.joining("<br>"));
sb.append(String.format(" %s |", nextStories.isEmpty() ? "-" : nextStories));
});
sb.append("\n");
return sb.toString();
}
}踩坑记录:自动生成地图的三个主要问题
问题一:LLM倾向于生成"理想化"的完整地图,而不是MVP地图
LLM知道什么是"完整的产品",所以它生成的故事往往偏向完整功能,不擅长判断哪些是MVP必须的。
解决方案:在提示词里明确加入资源约束("工程师4人、6周时间"),并且在第二遍对话中专门做"MVP裁剪"这个步骤。
问题二:生成的活动层有时候过细或过粗
有时候LLM会把"搜索"和"筛选"分成两个活动,但它们应该是同一个活动下的两个任务。
解决方案:在提示词里加入约束:"活动层代表用户的阶段性目标,每个目标可以用一句话描述用户想要实现什么。任务是达到这个目标的步骤"。同时增加一个校验步骤,让LLM检查自己生成的活动层是否粒度合适。
问题三:验收条件不够具体
LLM生成的Given-When-Then通常比较宽泛,缺少具体的数字和边界条件。
解决方案:在验收条件生成阶段,加入一个提示:"验收条件必须是可以写成自动化测试的,包含具体的输入值、预期的输出状态和可观测的结果"。
跟传统Workshop对比
有人会问:组织一次用户故事地图Workshop,让产品、开发、测试一起来,效果不是更好吗?
是的,Workshop的优势是共同理解和对齐——所有人参与生成过程,对地图有共同所有感。这是LLM无法完全替代的。
但Workshop有几个问题:
- 需要2-4小时的密集会议,时间成本高
- 依赖有经验的引导者,没有经验的团队很容易跑偏
- 输出物质量参差不齐,需要大量整理时间
我的实践是:用LLM生成初版地图(约1小时),然后用30-45分钟的Workshop来修正和对齐,而不是用Workshop从零开始生成。效率提升显著,对齐效果也不差。
完整的工作流
整个自动化部分(LLM生成)大概需要3-5分钟,主要时间花在并发请求和解析上。
用了这个工具之后,我见过最大的价值不是省了时间,而是它每次都会在"错误路径"和"边缘情况"上生成任务,这些东西人工开Workshop时经常被跳过,但落地时必须要做。
