# AI 动态问卷引擎(通用引擎 + 菌群健康第一场景)设计 > 日期:2026-08-14 > 分支:cfclub > 状态:待评审 > 技术选型:LangGraph(cfc-langgraph 服务,用户指定) ## 1. 背景与目标 现有问卷功能均为**静态形态**: | 现状 | 路径 | 局限 | |------|------|------| | 亲子关系问卷 | `relationship-questionnaire.vue` → `/api/family/questionnaire/*` → LangGraph `/api/v1/questionnaire/generate` | 一次性生成 8-12 题静态问卷,无动态交互、无画像、无知识库 | | 定期调研 | `survey-questionnaire.vue` → `SurveyService.aiGenerateQuestions` → `/api/v1/analysis/run` | 按模板/一次性生成 5 题,非对话式 | | 健康现状档案 | `health-status-form.vue` → `health_status` 表 | 固定字段表单(身高/血压/血糖…),非 AI 出题 | **用户需求**(已确认): 1. 问卷不事先编好:从第一题开始,AI 根据用户回答**动态生成下一题**(逐题对话式)。 2. AI 对问卷结果处理,生成**用户画像 + 需求画像**。 3. 问卷内容**结合菌群知识库**(出题时 RAG 检索 + 画像分析时引用 + 降级兼容)。 4. 做成**通用动态问卷引擎**:话题可配(菌群是第一个场景,未来可加睡眠/运动等)。 5. AI 能力统一走 **LangGraph**(cfc-langgraph Python 服务),并写入开发规范。 **设计目标**: - 引擎与场景解耦:引擎负责"动态出题 + 画像生成"通用机制,场景通过配置驱动(管理端可视化配置)。 - 画像灵活 JSON 维度:场景定义维度,引擎不硬编码。 - 画像三用途:小程序展示 + 入库(member 维度)+ 驱动推荐。 - 复用现有 LangGraph 服务、ChromaDB 知识库(`cfc_knowledge` collection 已含 microbiome_ / dan_knowledge_ / 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"|"finish", "question":{...}, "reason":"..."}`;已达 max_questions → 强制 finish | | `generate_profile` | LLM 基于完整 history + 知识库 → 画像 JSON `{user_profile:[...], need_profile:[...]}` | **问题结构(Question)**: ```json { "id": "q3", "type": "single | multi | scale | text", "text": "您通常一周吃几次富含膳食纤维的食物(如全谷物、蔬菜、豆类)?", "options": [{"id": "a", "label": "几乎不吃"}], "scale": {"min": 0, "max": 10, "minLabel": "从不", "maxLabel": "每天"} } ``` - `single`/`multi` 必须带 `options`;`scale` 必须带 `scale`;`text` 不带两者。 - 引擎校验 question 结构,非法 → 自动重试 1 次 → 兜底题。 **画像结构(灵活 JSON 维度)**: ```json { "user_profile": [ {"dimension": "肠道菌群状态", "score": 72, "description": "……", "evidence": ["答1: ……"]} ], "need_profile": [ {"dimension": "营养需求", "description": "……", "evidence": ["……"], "suggestion": "……"} ] } ``` - 维度由场景 `dimensions_json` 定义,引擎只校验"数组 + 必填字段",不约束维度名。 **端点契约**: `POST /api/v1/qna/advance` ```json // 请求 { "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,响应画像) ```json { "profile": {"user_profile": [...], "need_profile": [...]}, "kb_used": true } ``` **前提(风险项)**:cfc-langgraph 大部分源码文件当前为 0 字节(commit `f18dd86e` 清空),但 git 历史有完整版本(如 `2f685d22` 的 `app/api/adapter.py` 10356B、`app/main.py`、`app/rag/*`、`src/*` 问卷模块)。实现第一步:从 git 历史恢复最小可运行集(`app/config.py`、`app/rag/retriever.py`+`loader.py`+`splitter.py`+`embeddings.py`、`app/tools/java_client.py`、`app/tasks/knowledge_sync.py`、`app/middleware.py`、`app/main.py`、`src/llm/client.py`),验证 `uvicorn src.app:app` 可启动后,再新增 `src/qna/`。 ### 4.2 Java 侧(cfc-backend,编排与存储) **新 Entity/Mapper/Service/Controller**: - `entity/AiQScene.java`、`entity/AiQSession.java`、`entity/AiQProfile.java` - `mapper/AiQSceneMapper.java`、`AiQSessionMapper.java`、`AiQProfileMapper.java` - `service/AiQuestionnaireService.java` + `impl/AiQuestionnaireServiceImpl.java` - `controller/AiQuestionnaireController.java`(路由前缀 `/api/ai-questionnaire`) **AiGateway 扩展**(复用熔断/连接池): - `Map advanceQuestionnaire(Map scene, List history)` → `POST /api/v1/qna/advance`,超时 `python.timeout-ms`(15s) - `Map 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.vue` 或 `pages/body-detail/` 健康相关页加「AI 健康问卷」入口 - `utils/api.js` 增加:`aiQStart`、`aiQAnswer`、`aiQFinish`、`aiQSceneList`、`aiQHistory`、`aiQProfileDetail` - 小程序限制:禁止可选链 `?.`、禁止 `:key` 表达式、日期用 `parseDate()`、Options API ## 5. 数据库设计 ### 5.1 新表 `ai_q_scene`(场景配置) ```sql 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`(问卷会话) ```sql 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`(画像) ```sql 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 侧) 统一 `@PostMapping`,`Result` 响应,JWT 认证: | 接口 | 说明 | 请求 | 返回 | |------|------|------|------| | `POST /api/ai-questionnaire/scene/list` | 启用场景列表(登录可见) | `{}` | `List` | | `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` | | `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_json`;`question_count+1`;`answer` 统一为**字符串**(单选=选中 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 形式实现。