2026-08-14-ai-dynamic-questionnaire-design.md 19 KB

AI 动态问卷引擎(通用引擎 + 菌群健康第一场景)设计

日期:2026-08-14 分支:cfclub 状态:待评审 技术选型:LangGraph(cfc-langgraph 服务,用户指定)

1. 背景与目标

现有问卷功能均为静态形态

现状 路径 局限
亲子关系问卷 relationship-questionnaire.vue/api/family/questionnaire/* → LangGraph /api/v1/questionnaire/generate 一次性生成 8-12 题静态问卷,无动态交互、无画像、无知识库
定期调研 survey-questionnaire.vueSurveyService.aiGenerateQuestions/api/v1/analysis/run 按模板/一次性生成 5 题,非对话式
健康现状档案 health-status-form.vuehealth_status 固定字段表单(身高/血压/血糖…),非 AI 出题

用户需求(已确认):

  1. 问卷不事先编好:从第一题开始,AI 根据用户回答动态生成下一题(逐题对话式)。
  2. AI 对问卷结果处理,生成用户画像 + 需求画像
  3. 问卷内容结合菌群知识库(出题时 RAG 检索 + 画像分析时引用 + 降级兼容)。
  4. 做成通用动态问卷引擎:话题可配(菌群是第一个场景,未来可加睡眠/运动等)。
  5. AI 能力统一走 LangGraph(cfc-langgraph Python 服务),并写入开发规范。

设计目标

  • 引擎与场景解耦:引擎负责"动态出题 + 画像生成"通用机制,场景通过配置驱动(管理端可视化配置)。
  • 画像灵活 JSON 维度:场景定义维度,引擎不硬编码。
  • 画像三用途:小程序展示 + 入库(member 维度)+ 驱动推荐。
  • 复用现有 LangGraph 服务、ChromaDB 知识库(cfc_knowledge collection 已含 microbiome_ / danknowledge / article 三源)、AiGateway 熔断模式。

2. 需求决策(头脑风暴确认)

# 问题 决策
1 问卷定位 通用动态问卷引擎,话题可配,菌群健康为第一场景
2 交互形态 逐题对话式:答一题 → AI 根据回答生成下一题 → …
3 画像结构 灵活 JSON 维度user_profile[] + need_profile[],维度由场景配置定义,引擎不硬编码
4 画像用途 三者都要:展示给用户 + 存入数据库(member 维度)+ 驱动推荐
5 结束条件 题数上限 + AI 判定 + 用户主动结束 三者并存
6 知识库结合 出题时 RAG 检索 + 画像分析时引用 + 检索失败降级纯 LLM
7 场景配置方式 cfc-web 管理端可视化配置(CRUD:场景名/开场引导/维度定义/知识库范围/题数上限)
8 填写者与关联 家庭成员(memberId),家长可代孩子填,画像存 member 维度
9 AI 技术栈 LangGraph(cfc-langgraph),写入开发规范(AGENTS.md)

3. 目标架构

方案 1(已选):Java 会话管理 + LangGraph 无状态推理引擎

小程序 (ai-questionnaire.vue 逐题对话)
   │  POST /api/ai-questionnaire/*(JWT)
   ▼
Java 后端 (cfc-backend)
   ├── AiQuestionnaireController ──→ AiQuestionnaireService
   ├── ai_q_scene / ai_q_session / ai_q_profile(MySQL)
   └── AiGateway.advanceQuestionnaire / generateProfile(熔断+超时)
            │  HTTP(ai.etotem.com.cn / localhost:9000)
            ▼
LangGraph (cfc-langgraph, Python)
   └── POST /api/v1/qna/advance(出题,快)
   └── POST /api/v1/qna/profile(画像,慢 30-60s)
       └── qna_graph: retrieve_knowledge → decide_next → generate_profile
            └── ChromaDB cfc_knowledge(microbiome_/dan_knowledge_/article 三源)

选型理由

  • 历史持久化可靠(DB 天然支持中断恢复、审计、回显历史)。
  • 引擎纯函数可测:(场景配置, 历史) → 下一题/画像,输入输出干净。
  • LangGraph 发挥价值:知识检索→推理→画像的图结构在服务端;Java 只做编排。
  • 会话/画像入库,复用 AiGateway 熔断模式;后端治理一致。
  • 避免"LangGraph 有状态会话"方案的内存态(MemorySaver 重启丢失)与额外持久化运维。
  • 避免"复用 analysis/run"方案的无法扩展与 RAG 集成残缺。

LangGraph 有状态方案(弃):MemorySaver 内存态重启丢失;持久化需引入 checkpoint store;Java 审计历史需同步。 复用 analysis/run 方案(弃):无状态机、RAG 集成残缺、与 LangGraph 技术路线冲突。

4. 组件设计

4.1 LangGraph 侧(cfc-langgraph,核心引擎)

新增 src/qna/ 模块(在既有 src/ 问卷模块基础上演进),无状态纯函数:

qna_graph.py
  START → load_scene → retrieve_knowledge → decide_next ──ask──→ END
                                              │
                                              └──finish──→ generate_profile → END

qna_profile_graph.py(独立,供 /api/v1/qna/profile)
  START → load_scene → retrieve_knowledge → generate_profile → END
节点 职责
load_scene 透传 Java 传入的 scene_config,组装 system prompt(含 opening_prompt、dimensions_json、kb_scope)
retrieve_knowledge 从 history 提取关键词(复用 _extract_health_keywords 思路)→ RagRetriever.retrieve(按 kb_scope 过滤 source 前缀)→ 注入知识上下文;检索失败/空 → 置空降级
decide_next LLM 基于 (场景配置 + 知识 + 历史) 输出决策 JSON:`{"action":"ask"
generate_profile LLM 基于完整 history + 知识库 → 画像 JSON {user_profile:[...], need_profile:[...]}

问题结构(Question)

{
  "id": "q3",
  "type": "single | multi | scale | text",
  "text": "您通常一周吃几次富含膳食纤维的食物(如全谷物、蔬菜、豆类)?",
  "options": [{"id": "a", "label": "几乎不吃"}],
  "scale": {"min": 0, "max": 10, "minLabel": "从不", "maxLabel": "每天"}
}
  • single/multi 必须带 optionsscale 必须带 scaletext 不带两者。
  • 引擎校验 question 结构,非法 → 自动重试 1 次 → 兜底题。

画像结构(灵活 JSON 维度)

{
  "user_profile": [
    {"dimension": "肠道菌群状态", "score": 72, "description": "……", "evidence": ["答1: ……"]}
  ],
  "need_profile": [
    {"dimension": "营养需求", "description": "……", "evidence": ["……"], "suggestion": "……"}
  ]
}
  • 维度由场景 dimensions_json 定义,引擎只校验"数组 + 必填字段",不约束维度名。

端点契约

POST /api/v1/qna/advance

// 请求
{
  "scene": {
    "scene_key": "microbiome",
    "opening_prompt": "我将了解您的肠道健康状况……",
    "dimensions_json": {"user": [...], "need": [...]},
    "kb_scope": ["microbiome", "dan_knowledge"],
    "max_questions": 12,
    "system_prompt": "可选:场景自定义 system prompt"
  },
  "history": [{"question": {"type": "single", "text": "…"}, "answer": "…"}]
}
// 响应
{
  "action": "ask",
  "question": {"id": "q4", "type": "single", "text": "…", "options": [...]},
  "finished": false
}

POST /api/v1/qna/profile(请求同 advance,响应画像)

{
  "profile": {"user_profile": [...], "need_profile": [...]},
  "kb_used": true
}

前提(风险项):cfc-langgraph 大部分源码文件当前为 0 字节(commit f18dd86e 清空),但 git 历史有完整版本(如 2f685d22app/api/adapter.py 10356B、app/main.pyapp/rag/*src/* 问卷模块)。实现第一步:从 git 历史恢复最小可运行集(app/config.pyapp/rag/retriever.py+loader.py+splitter.py+embeddings.pyapp/tools/java_client.pyapp/tasks/knowledge_sync.pyapp/middleware.pyapp/main.pysrc/llm/client.py),验证 uvicorn src.app:app 可启动后,再新增 src/qna/

4.2 Java 侧(cfc-backend,编排与存储)

新 Entity/Mapper/Service/Controller

  • entity/AiQScene.javaentity/AiQSession.javaentity/AiQProfile.java
  • mapper/AiQSceneMapper.javaAiQSessionMapper.javaAiQProfileMapper.java
  • service/AiQuestionnaireService.java + impl/AiQuestionnaireServiceImpl.java
  • controller/AiQuestionnaireController.java(路由前缀 /api/ai-questionnaire

AiGateway 扩展(复用熔断/连接池):

  • Map<String,Object> advanceQuestionnaire(Map scene, List history)POST /api/v1/qna/advance,超时 python.timeout-ms(15s)
  • Map<String,Object> generateProfile(Map scene, List history)POST /api/v1/qna/profile,独立超时 90s(画像推理 30-60s),失败返回 null 由 Service 降级

4.3 cfc-web 管理端

  • 新页面 src/views/admin/AiQuestionnaireScenes.vue:场景 CRUD
    • 字段:scene_key、scene_name、description、opening_prompt、dimensions_json(JSON 编辑器)、kb_scope(多选:microbiome/dan_knowledge/article)、max_questions、system_prompt、enabled
  • 路由 + 菜单:/admin/ai-questionnaire/scenes(admin 权限)
  • src/api/aiQuestionnaire.js 接口封装

4.4 小程序前端

  • 新页面 pages/health/ai-questionnaire.vue(逐题对话式):
    • 场景列表选择(调用 /api/ai-questionnaire/scene/list)→ 选择成员 → start
    • 展示当前题(单选/多选/量表/文本)→ 作答 → answer → 下一题(loading 态"AI 思考中…")
    • 进度提示(已答 n/max)、主动结束按钮(确认弹窗)
    • 若该成员有 running 会话 → 提示续答或重新开始
  • 新页面 pages/health/ai-questionnaire-result.vue(画像展示):
    • 用户画像:维度条/雷达(score 0-100)+ description + evidence
    • 需求画像:维度列表 + suggestion + 推荐入口按钮
  • 入口:pages/health-main/index.vuepages/body-detail/ 健康相关页加「AI 健康问卷」入口
  • utils/api.js 增加:aiQStartaiQAnsweraiQFinishaiQSceneListaiQHistoryaiQProfileDetail
  • 小程序限制:禁止可选链 ?.、禁止 :key 表达式、日期用 parseDate()、Options API

5. 数据库设计

5.1 新表 ai_q_scene(场景配置)

CREATE TABLE IF NOT EXISTS ai_q_scene (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  scene_key VARCHAR(50) NOT NULL COMMENT '场景唯一标识(如 microbiome)',
  scene_name VARCHAR(100) NOT NULL COMMENT '场景名称(如 菌群健康评估)',
  description VARCHAR(500) DEFAULT NULL COMMENT '场景描述',
  opening_prompt TEXT COMMENT '开场引导(AI 出第一题时的引导语)',
  dimensions_json JSON COMMENT '画像维度定义 {user:[...], need:[...]}',
  kb_scope VARCHAR(200) DEFAULT 'microbiome' COMMENT '知识库范围(逗号分隔 source 前缀)',
  max_questions INT DEFAULT 12 COMMENT '题数上限',
  system_prompt TEXT COMMENT '可选:场景自定义 system prompt',
  enabled TINYINT DEFAULT 1 COMMENT '1=启用 0=禁用',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_scene_key (scene_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 动态问卷-场景配置';

5.2 新表 ai_q_session(问卷会话)

CREATE TABLE IF NOT EXISTS ai_q_session (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  scene_id BIGINT NOT NULL COMMENT '场景ID',
  user_id BIGINT NOT NULL COMMENT '填写者用户ID',
  member_id BIGINT NOT NULL COMMENT '画像关联成员ID',
  family_id BIGINT DEFAULT NULL COMMENT '家庭ID',
  status VARCHAR(16) DEFAULT 'running' COMMENT 'running/finished/aborted',
  history_json JSON COMMENT '已回答历史 [{question:{...}, answer:"..."}]',
  current_question_json JSON COMMENT '当前待答题(LangGraph advance 返回、尚未回答)',
  question_count INT DEFAULT 0 COMMENT '已答题数',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  finished_at DATETIME DEFAULT NULL COMMENT '完成时间',
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_member_status (member_id, status),
  INDEX idx_scene (scene_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 动态问卷-会话';

5.3 新表 ai_q_profile(画像)

CREATE TABLE IF NOT EXISTS ai_q_profile (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  session_id BIGINT NOT NULL COMMENT '会话ID',
  scene_id BIGINT NOT NULL COMMENT '场景ID',
  member_id BIGINT NOT NULL COMMENT '成员ID',
  user_profile_json JSON COMMENT '用户画像 [{dimension,score,description,evidence}]',
  need_profile_json JSON COMMENT '需求画像 [{dimension,description,evidence,suggestion}]',
  raw_result TEXT COMMENT 'LLM 原始输出(审计)',
  kb_used TINYINT DEFAULT 0 COMMENT '是否使用了知识库',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_session (session_id),
  INDEX idx_member (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 动态问卷-画像结果';

5.4 迁移

  • DatabaseInitializer.runMigrations() 新增迁移(CREATE TABLE IF NOT EXISTS ×3,幂等 try-catch),迁移编号按现有最大编号递增。
  • 同步 schema.sql(完整快照)。
  • 种子数据:插入一个 microbiome 场景(scene_key=microbiome,菌群健康评估)便于演示。

6. API 契约(Java 侧)

统一 @PostMappingResult<T> 响应,JWT 认证:

接口 说明 请求 返回
POST /api/ai-questionnaire/scene/list 启用场景列表(登录可见) {} List<AiQScene>
POST /api/ai-questionnaire/scene/save 场景新增/更新(admin) AiQScene 保存后场景
POST /api/ai-questionnaire/scene/delete 场景删除(admin) {id} void

scene/delete:若该场景已有 ai_q_session 引用则拒绝物理删除(返回 Result.error("该场景已有问卷记录,请改用禁用")),无引用时可物理删;日常禁用推荐 enabled=0(scene/save 置 enabled 字段)。 | POST /api/ai-questionnaire/start | 开始问卷 | {sceneId, memberId} | {sessionId, question, answeredCount} | | POST /api/ai-questionnaire/answer | 提交答案 → 下一题或结束 | {sessionId, answer} | {action:"ask"\|"finish", question?, profile?, finished} | | POST /api/ai-questionnaire/finish | 用户主动结束 → 生成画像 | {sessionId} | {profile} | | POST /api/ai-questionnaire/history | 成员问卷会话列表 | {memberId, sceneId?} | List<AiQSession 摘要> | | POST /api/ai-questionnaire/profile/detail | 画像详情 | {sessionId} | AiQProfile | | POST /api/ai-questionnaire/abort | 放弃会话(可选) | {sessionId} | void |

answer 内部流程

  1. 校验 session 存在且 status=running;校验 memberId 归属(家长可代填家庭成员)。
  2. 并发防护:更新时校验 status='running',后到者返回"问卷已完成"。
  3. current_question_json 为待答题,与本次 answer 组装 {question, answer} 追加到 history_jsonquestion_count+1answer 统一为字符串(单选=选中 label,多选=逗号分隔 labels,量表=数字字符串,文本=文本,前端提交前规整)。
  4. question_count >= max_questions → 直接走画像生成(调 /api/v1/qna/profile)→ 存 ai_q_profile + session 置 finished → 返回 {action:"finish", profile}
  5. 否则调 /api/v1/qna/advance
    • action=ask → 返回的新题存入 current_question_json → 返回 {action:"ask", question}
    • action=finish → 调 /api/v1/qna/profile → 存画像 + 置 finished → 返回 {action:"finish", profile}
    • 调用失败(熔断/超时/解析失败)→ 返回兜底题(保存为 current_question_json)+ 前端 toast 提示;画像失败 → Result.error("画像生成失败,请稍后重试"),session 保持 running,用户可重新 finish。
  6. 知识库检索失败由 LangGraph 内部降级(不阻塞)。

start 内部流程:查 scene → 建 session(status=running)→ 调 /api/v1/qna/advance(history 为空)→ 返回题存入 current_question_json → 返回 {sessionId, question}

7. 错误处理与边界

场景 处理
LangGraph 出题超时/熔断 返回兜底题(模板池轮换),toast 提示"AI 暂时繁忙,已使用备选题目"
LangGraph 画像超时/失败 Result.error 提示重试;session 保持 running 可重试 finish
LLM 输出 JSON 解析失败 引擎内自动重试 1 次(换 temperature=0 确定性重试)→ 兜底题
question 结构非法 同上
知识库检索失败/空 降级纯 LLM(kb_used=false
并发 answer 同 session 更新时校验 status,后到者返回"问卷已完成"
会话中断恢复 进入页面查询 member running 会话 → 从 DB history 续答
成员权限 只能对本人或本人家庭内成员填写(复用 FamilyMemberService 校验)
memberId 缺失 Result.error("请先完善家庭成员信息")(复用 SurveyServiceImpl 的 resolveMemberId 思路)
画像维度/score 越界 引擎 validate 钳制(score 0-100);Java 存库前二次校验 JSON 结构

8. 测试策略

  • LangGraph(pytest):fake LLM 返回固定 JSON,验证:
    • advance:正常 ask / 达上限强制 finish / JSON 非法重试 / 知识库注入(mock RagRetriever)
    • profile:画像结构校验(user_profile/need_profile 必填字段)、kb_used 标记
  • Java
    • mvn clean compile 通过(唯一编译验证)
    • Service 单测(可选):answer 流程(mock AiGateway)、并发防护、成员校验
    • Controller 集成测试(mock LangGraph)
  • 前端node --check 语法校验(不打包,HBuilderX 打包)
  • 手工验证清单
    1. 管理端建场景 → 小程序可见
    2. start → 首题出现 → 逐题回答 → 下一题与历史回答相关
    3. 达题数上限 → 自动出画像
    4. 主动结束 → 画像生成 → 展示
    5. 中断会话 → 续答
    6. 停掉 LangGraph → 兜底题可继续答题
    7. 画像入库 → 历史查询可见 → 推荐入口可跳转

9. 非目标(本次不做)

  • 不做流式输出(SSE)——逐题交互已是异步形态,画像生成可等待。
  • 不做画像驱动的自动推荐引擎——本期画像入库 + 展示,推荐入口跳转复用现有推荐接口(AiGateway.recommend / 商城搜索),画像标签对接推荐留待后续迭代。
  • 不做会话过期定时清理(DB 保留即可,后续可加)。
  • 不做 LangGraph 服务的全量源码恢复——只恢复 qna 所需最小集 + 验证可运行;其余模块(chat/report_parse/tongue 等)按生产需要单独处理。
  • 不做场景配置的版本历史/草稿。
  • 不做多语言。

10. 开发规范更新(用户指定)

将以下约定写入 AGENTS.md(根 + cfc-backend/AGENTS.md):

AI 服务统一走 LangGraph:所有 AI 能力(对话/问卷/画像/推荐/解析)基于 cfc-langgraph Python 服务(FastAPI + LangGraph + ChromaDB RAG,端口 9000,生产 ai.etotem.com.cn),Java 侧通过 AiGateway 调用(熔断 + 超时),禁止直接对接 Dify 之外的第三方 LLM 服务;新增 AI 功能优先以 LangGraph graph 形式实现。