# 研发工单 v1.3.0 - 03-03 AI 任务拆分服务
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
>
> 依赖:03-01(API 契约)、工单 02(`LlmClientService`)
---
## 一、目标
实现基于 LLM 的会议内容分析服务:接收会议纪要文字,通过 LLM prompt 工程提取行动项,生成结构化的待办项建议列表(含标题、优先级、理由)。
## 二、范围
**包含**:
- `MeetingAiBreakdownService`:会议文本 → LLM → 结构化建议列表
- 会议拆分专用 prompt 设计与迭代
- AI 拆分 API 端点:`POST /api/meeting/{taskId}/breakdown`
- 批量创建确认端点:`POST /api/meeting/{taskId}/breakdown/confirm`
- 在线可用性检测(离线时拒绝 AI 拆分请求)
- 复用工单 02 的 `LlmClientService`
**不包含**:
- 单任务拆分(属于工单 02 `AI_BREAKDOWN` 意图)
- LLM 基础设施搭建(由工单 02 提供)
- 前端审阅 UI(由 03-04 完成)
## 三、前置条件
| 条件 | 说明 |
|---|---|
| 03-01 完成 | `MeetingController` 骨架就绪 |
| 工单 02 `LlmClientService` | LLM 调用封装可用 |
## 四、详细规格
### 4.1 MeetingAiBreakdownService
```csharp
/// 会议 AI 拆分服务,将会议文字内容分析为待办项建议
public class MeetingAiBreakdownService
{
private readonly ILlmClientService _llmClient;
private readonly MeetingService _meetingService;
private readonly ITaskService _taskService;
public MeetingAiBreakdownService(
ILlmClientService llmClient,
MeetingService meetingService,
ITaskService taskService) { ... }
/// 分析会议内容,返回待办项建议列表
public async Task> AnalyzeAsync(
int taskId, string? notes = null, CancellationToken ct = default)
{
// 1. 验证 taskId 为 Meeting 类型
var meeting = await _meetingService.GetMeetingTaskOrThrow(taskId);
// 2. 获取会议文字(参数优先,否则取数据库中的 meetingNotes)
var meetingText = notes ?? meeting.MeetingNotes;
if (string.IsNullOrWhiteSpace(meetingText))
throw new BusinessException("会议内容为空,请先输入纪要或上传录音");
// 3. 构建 prompt,调用 LLM
var prompt = BuildBreakdownPrompt(meetingText, meeting.Title);
var response = await _llmClient.SendAsync(prompt, ct);
// 4. 解析 LLM 返回的 JSON 为建议列表
return ParseSuggestions(response);
}
/// 确认建议并批量创建子任务
public async Task ConfirmAndCreateAsync(
int taskId, List subTasks, CancellationToken ct = default)
{
// 1. 验证 taskId 为 Meeting 类型
await _meetingService.GetMeetingTaskOrThrow(taskId);
// 2. 批量创建子任务
var created = new List();
foreach (var item in subTasks)
{
var dto = new CreateTaskDto
{
Title = item.Title,
Priority = item.Priority,
ParentTaskId = taskId
};
created.Add(await _taskService.CreateTaskAsync(dto));
}
return new BatchCreateResult { CreatedCount = created.Count, SubTasks = created };
}
}
```
### 4.2 LLM Prompt 设计
```
System Prompt:
你是专业的项目管理和会议纪要分析助手。根据用户提供的会议内容,提取所有需要
后续行动的事项,生成待办项建议列表。
要求:
1. 每条建议包含标题(title)、优先级(priority)、原因(reason)
2. 标题应简洁明确(15 字以内),如"整理需求文档"、"安排评审会议"
3. 优先级:High(2)=紧急重要,Medium(1)=一般,Low(0)=可延迟
4. 原因应解释为什么需要这个待办项,引用会议中的具体决策或讨论
5. 只提取明确需要执行的事项,不要生成模糊或无来源的建议
6. 建议数量根据会议内容合理判断,通常 3-10 条
7. 如果会议内容无法提取出明确的行动项,返回空列表
输出格式(只输出 JSON,不要解释):
{
"suggestions": [
{
"title": "待办项标题",
"priority": 1,
"reason": "原因说明"
}
]
}
```
### 4.3 DTO 定义
```csharp
/// 会议任务拆分建议
public class MeetingTaskSuggestion
{
/// 建议的待办项标题
public string Title { get; set; } = string.Empty;
/// 建议优先级
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
/// 拆分原因(LLM 输出,用于 UI 展示)
public string Reason { get; set; } = string.Empty;
}
/// 拆分请求
public class BreakdownRequest
{
/// 会议纪要文字(可选,不传则使用已保存的 meetingNotes)
public string? Notes { get; set; }
}
/// 拆分响应
public class BreakdownResponse
{
public int TaskId { get; set; }
public List Suggestions { get; set; } = new();
}
/// 批量创建请求中的单个子任务
public class SubTaskCreateItem
{
public string Title { get; set; } = string.Empty;
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
}
/// 批量确认请求
public class ConfirmBreakdownRequest
{
public List SubTasks { get; set; } = new();
}
/// 批量创建结果
public class BatchCreateResult
{
public int CreatedCount { get; set; }
public List SubTasks { get; set; } = new();
}
```
### 4.4 API 端点
#### POST /api/meeting/{taskId}/breakdown
- 接收:`BreakdownRequest`
- 返回:`ApiResponse`
- 处理:
1. 获取会议文字(参数优先于数据库)
2. 调用 `MeetingAiBreakdownService.AnalyzeAsync()`
3. 返回建议列表
- 错误:
- 内容为空 → 400 "会议内容为空"
- 离线 → 503 "AI 拆分仅在线可用"
- taskId 非 Meeting 类型 → 400
#### POST /api/meeting/{taskId}/breakdown/confirm
- 接收:`ConfirmBreakdownRequest`
- 返回:`ApiResponse`
- 处理:
1. 验证 taskId 为 Meeting 类型
2. 逐条创建子任务(通过 `TaskService.CreateTaskAsync`)
3. 返回创建结果
### 4.5 LLM 响应解析
```csharp
/// 解析 LLM 返回的 JSON 为建议列表
private List ParseSuggestions(string llmResponse)
{
// 1. 清理 LLM 响应(移除可能的 markdown 代码块包裹、前后空白)
var json = CleanJsonResponse(llmResponse);
// 2. 反序列化
var result = JsonSerializer.Deserialize(json);
// 3. 验证
if (result?.Suggestions == null || result.Suggestions.Count == 0)
return new List();
// 4. 过滤无效条目(标题为空)
return result.Suggestions
.Where(s => !string.IsNullOrWhiteSpace(s.Title))
.Select(s => new MeetingTaskSuggestion
{
Title = s.Title.Trim(),
Priority = s.Priority,
Reason = s.Reason?.Trim() ?? string.Empty
})
.ToList();
}
/// LLM 原始响应结构
private class LlBreakdownResponse
{
public List Suggestions { get; set; } = new();
}
```
### 4.6 错误处理
| 场景 | HTTP 状态码 | message |
|---|---|---|
| 会议文字内容为空 | 400 | 会议内容为空,请先输入纪要或上传录音 |
| taskId 不存在 | 404 | 待办项不存在 |
| taskId 不是 Meeting 类型 | 400 | 该待办项不是会议类型 |
| LLM API 调用失败 | 502 | AI 拆分服务暂时不可用,请稍后重试 |
| LLM 返回格式异常 | 500 | AI 拆分结果解析失败 |
| 离线 | 503 | AI 拆分仅在线可用 |
| 确认时子任务列表为空 | 400 | 请至少选择一个待办项 |
## 五、验收标准
| 验收项 | 验证方法 | 预期结果 |
|---|---|---|
| 会议内容分析 | 提交一段会议纪要文字 | 返回 3-10 条结构化建议 |
| 建议质量 | 检查建议的标题、优先级、原因 | 标题明确、优先级合理、原因引用会议内容 |
| 空内容拒绝 | 不传 notes 且 meetingNotes 为空 | 返回 400 |
| 非 Meeting 类型拒绝 | 传普通待办项 ID | 返回 400 |
| 在线依赖 | 离线时请求拆分 | 返回 503 |
| 批量创建 | 确认勾选的建议 | 子任务批量创建成功 |
| LLM 异常降级 | 模拟 LLM 调用失败 | 返回 502,不影响其他功能 |
| 空结果处理 | 会议内容无可提取的待办项 | 返回空建议列表 + 提示信息 |
## 六、风险
| 风险 | 影响 | 应对 |
|---|---|---|
| LLM 返回格式不符 | 建议解析失败 | 增加 JSON 清理逻辑(移除 markdown 包裹、前后文本) |
| LLM Token 限制 | 长会议纪要超出上下文窗口 | 限制文本长度(如 4000 字);超长时截断并告知用户 |
| Prompt 需要迭代 | 建议质量不达预期 | prompt 作为配置项,支持热更新 |
## 七、Touch List
| 文件路径 | 修改类型 |
|---|---|
| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 |
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 修改 |
---
**工单编号**:03-03
**标题**:AI 任务拆分服务
**版本**:v1.3.0
**创建日期**:2026-06-16