日期: 2026-08-28 状态: 待审查 优先级: P1
现有健康方案生成链路存在三处脱节:
AI 返回侧:cfc-langgraph/app/api/adapter.py 的 PLAN_SYSTEM_PROMPT 已要求 LLM 输出结构化 JSON(overview + sections[].content/items + abnormal_indicators),但 JSON 解析靠 find("{")/rfind("}") 手动截取,无 Pydantic 校验,失败时静默降级为 raw 文本——前端需同时兼容两种形态。
呈现/编辑侧:cfc-frontend/pages/health/health-plan-summary.vue 已支持分段确认 + 反馈式重生成,但 submitPlan 组装 planJson 时漏写 sections[].tasks(当前 schema 根本没有该字段),导致保存时丢失任务意图。规划师端 HealthPlanReview.vue 仅提供 planContent 纯文本编辑,无结构化任务条目 UI。
任务转化侧:HealthPlanServiceImpl.generateDailyTasksFromPlan 用正则(TASK_PATTERN + 动作词分类)从 plan_content 纯文本猜测任务行,可靠性建立在"LLM 输出恰好符合正则预期"上。结构化的 plan_json 在此链路完全未被消费。前后端各维护一份重复正则(TASK_PATTERN 与前端 parseTasksFromPlan)。
目标: 让 plan_json.sections[].tasks 成为任务生成的唯一事实源,AI 有标准格式返回、用户看到标准格式可编辑、结果确定性地转化为任务。
采用方案 A:
tasks 数组 + Pydantic 校验tasksplanJson.sections[].tasks,缺失才回落正则不做方案 B(完全移除正则,容错低)或方案 C(仅前端展示,未解决可靠性)。
在现有 sections[].items 基础上,为每个 section 增加 tasks 数组(机器可读任务意图),与 content(人类可读 Markdown)分离:
{
"overview": "总体概述",
"sections": [
{
"key": "nutrition",
"title": "营养补充建议",
"content": "## 建议 1...(Markdown 展示文本)",
"items": [{"name","dosage","timing","reason"}],
"tasks": [
{
"action_type": "buy",
"title": "购买维生素D3补充剂",
"dimension": "wealth",
"frequency": "once",
"notes": "每日一粒,随餐服用"
}
]
}
],
"abnormal_indicators": []
}
| 字段 | 类型 | 说明 |
|---|---|---|
action_type |
string 枚举 | buy/read/exercise/checkin/diet/activity,决定任务类别与后处理 |
title |
string | 任务标题(最终写入 Task.title) |
dimension |
string 枚举 | 五维维度 body/mind/wisdom/action/wealth |
frequency |
string | once=一次性 / daily=每日重复 |
notes |
string? | 补充说明(可选) |
| action_type | 现有 TaskDraft 分类 | 维度 | 特殊后处理 |
|---|---|---|---|
buy |
购买任务 | wealth | 派生"每日使用/服用"子任务(parentTaskId) |
read |
阅读任务 | wisdom | — |
exercise |
运动任务 | body | — |
checkin |
打卡任务 | mind | — |
diet |
饮食任务 | body | — |
activity |
活动任务 | action | — |
文件: cfc-langgraph/app/api/adapter.py、cfc-langgraph/app/schemas.py
PLAN_SYSTEM_PROMPT 追加 tasks 输出契约:在每个 section 内强制输出 tasks 数组,明确枚举取值与 frequency 语义。
新增 Pydantic 模型(app/schemas.py):
from typing import List, Optional, Literal
from pydantic import BaseModel
class PlanTask(BaseModel):
action_type: Literal["buy","read","exercise","checkin","diet","activity"]
title: str
dimension: Literal["body","mind","wisdom","action","wealth"]
frequency: Literal["once","daily"] = "daily"
notes: Optional[str] = None
class PlanSection(BaseModel):
key: str
title: str = ""
content: str = ""
items: List[dict] = []
tasks: List[PlanTask] = []
class PlanResponse(BaseModel):
overview: str = ""
sections: List[PlanSection] = []
abnormal_indicators: List[dict] = []
生成逻辑:LLM 调用用结构化输出(response_format={"type":"json_object"}),随后 PlanResponse.model_validate_json() 校验;校验失败 → 触发一次正则兜底重新提取 + 二次校验,仍失败则降级(raw 文本 + 前端兼容)。
regenerate-section:反馈重生成改为返回 {content, tasks},前端同步更新,不再只回传纯字符串。
文件: HealthPlanServiceImpl.java、DatabaseInitializer.java、schema.sql
generateDailyTasksFromPlan 新逻辑:
plan_json.sections[].tasks[],按 action_type 直接映射现有 TaskDraft 分类。tasks 数组(老数据/LLM 漏输出)→ 回落现有 TASK_PATTERN 正则从 content 提取。TaskDraft 的 dimension/frequency 直接取自 tasks[].dimension/tasks[].frequency,不再依赖正则猜维度。在 generateDailyTasksFromPlan 开头:
plan_json 中该 section 无 tasks 数组 → 用现有正则(TASK_PATTERN + 动作分类)从 content 提取任务,写入 plan_json.sections[].tasks,落库(updateById)。无需预跑全量脚本,零运维成本;存量方案在首次发布/确认时自动统一。
文件: cfc-frontend/pages/health/health-plan-summary.vue、cfc-web/src/views/teacher/HealthPlanReview.vue
submitPlan 组装 planJson 时补上 sections[].tasks(当前只写 overview+sections,漏 tasks)。HealthPlanReview.vue 当前只有 planContent 纯文本编辑,无 tasks UI → 新增任务条目级增删改(action_type/title/dimension/frequency/notes)。plan_json.sections[].tasks,保存走现有 pending-review/update 接口(已是透传 planJson)。PlanResponse 校验(合法/非法 action_type、frequency 越界)、正则兜底回填。generateDailyTasksFromPlan 覆盖"有 tasks(优先走 tasks)"与"无 tasks(回落正则)"两分支;TaskDraft 映射正确性(buy→wealth+派生使用子任务等);懒加载回填幂等性。| 模块 | 命令 |
|---|---|
| 后端 | cd cfc-backend && mvn clean compile + 针对性单测 |
| LangGraph | cd cfc-langgraph && pytest tests/ -v |
| cfc-web | cd cfc-web && npm run build |
TASK_PATTERN 短期保留作兜底,不退役;等所有活跃方案走新格式后再评估退役。getPlanList 中残留 SortUtil.applySort 调用(之前 revert 掉的分页+排序残留)——不在本设计范围内,仅提示后续清理。npm run build:mp-weixin)。