# 前端意图识别与交互集成 — 实现指南 > 对应后端系统:`backend/app/intent/` + `backend/app/orchestrator.py` > 前端主文件:`SixStepChatPage.tsx`, `api.ts`, `types.ts`, `SixStepConversation.tsx` > 完成度:**全部 6 步 + follow_up ✅** --- ## 一、核心概念回顾 ### 意图体系(Intent System) 前端负责**传递上下文** + **处理 SSE 事件流** + **更新 UI 状态**,后端负责**识别意图** + **分支路由** + **执行业务**。 ``` 前端按需收集当前状态信息 ↓ 打包到 IntentOrchestrateRequest ↓ POST /orchestrate-stream (SSE) ↓ 后端 detect_intent() 识别意图 ↓ 根据意图分支,emit SSE 事件 ↓ 前端处理事件,同步更新 message 和 phase ``` ### 七种意图与前端对应 | 意图 | 触发词 | 后端处理 | 前端转换 | 最终状态 | |------|-------|---------|---------|---------| | **find_expert** | "找专家" | find_expert MCP | phase: idle → expert-match | 显示专家卡片 + MCP 选择弹窗 | | **reselect_expert** | "换专家" | find_expert MCP + 过滤 | phase: expert-confirm → expert-match | 显示新的专家卡片 | | **start_analysis** | "分析" "开始" | 全新 6bf | phase: expert-confirm → generate | 显示 6bf 阶段卡片和流程图 | | **restart_analysis** | "重新分析" | 清空+重新执行 | phase: completed → generate | 同上 | | **continue_analysis** | "继续" (在 paused 时) | 从中断点恢复 | phase: paused → generate | 显示 resume 提示 + 继续的卡片 | | **follow_up** | "为什么" "解释" (在 completed 时) | LLM 回答 | phase: completed → follow-up | Markdown 渲染追问回答 | | **unknown** | 含糊/歧义 | 请求澄清 | phase: * → clarifying | 显示澄清问题 | --- ## 二、数据模型层(types.ts) ### 2.1 SixStepPhase 枚举 ```typescript export type SixStepPhase = | 'idle' // 【初始】未开始任何操作 | 'intent-detecting' // 【过渡】意图正在识别中(通常很短,用户看不到) | 'expert-match' // 【等待】展示专家卡片,用户可浏览、搜索、重选 | 'expert-confirm' // 【交互】MCP 多选弹窗打开,用户确认选择 | 'generate' // 【处理】6bf 编排执行中,显示进度和卡片 | 'paused' // 【中断】用户手动停止;可继续或重新分析 | 'completed' // 【完成】编排全部完成;可追问或重新分析 | 'clarifying' // 【澄清】后端无法理解,请用户补充 | 'follow-up'; // 【追问】正在基于已有分析回答用户问题 ``` **关键点**: - `expert-match` vs `expert-confirm` 的区别:前者是**显示结果**,后者是**交互确认**(MCP 弹窗) - `paused` **必须**在 `handleStop()` 手动设置,后端的 continue_analysis 分支不会主动发送 paused - `follow-up` 仅在 `current_phase === 'completed'` 且用户输入匹配追问模式时触发 ### 2.2 SixStepMessage 新增字段 ```typescript export interface SixStepMessage { // ... 原有字段 (id, role, content, timestamp, stages, finalAnswer, ...) // ========== 意图识别相关 ========== intent?: { name: string; // 识别的意图名(find_expert / follow_up / unknown 等) confidence?: number; // 置信度 [0.0, 1.0] reason?: string; // 识别理由(用于 debug/日志) }; // ========== 澄清问题 ========== clarifyQuestion?: string; // 后端提出的澄清问题(当 intent.name === "unknown") // ========== 继续分析上下文 ========== /** * 原始步骤数据(来自后端 step_end 事件)。 * 结构:BackendStepResult[] = { key, name, status, thinking, structured_outputs, markdown, references } * 用途: * - 当用户暂停(handleStop)时,保存这些数据 * - 当用户"继续分析"时,传回后端作为 last_steps,后端可恢复上下文 * - 当用户"追问"时,后端用这些数据构造 context,生成更准确的回答 */ rawSteps?: BackendStepResult[]; /** * 本轮是从哪个步骤继续的(来自后端 resume 事件)。 * 值:step key(如 "behavior_factor")或 step name(如 "预期行为与驱动因素") * UI 用途:在 AssistantMessage 上方显示"⏸ 从 [步骤名] 继续"提示 */ resumedFrom?: string; } ``` ### 2.3 IntentOrchestrateRequest(api.ts) ```typescript export interface IntentOrchestrateRequest { // 意图识别相关 intent_mode?: 'auto' | 'force_orchestrate' | 'force_find_expert'; // 会话状态(前端告知后端) current_phase: string; selected_expert?: ExpertCard | null; selected_mcps?: SelectableMcpKey[]; // 继续分析相关 last_steps?: BackendStepResult[]; resume_from_step?: string; expert_candidates?: ExpertCard[]; // 用于 reselect_expert 时过滤 } ``` --- ## 三、流处理层(api.ts) ### 3.1 runIntentOrchestrateStream() — 统一 SSE 处理器 ```typescript export async function runIntentOrchestrateStream( message: string, options: { onUpdate?: (msg: SixStepMessage) => void; onComplete?: () => void; abortSignal?: AbortSignal; }, requestBody?: IntentOrchestrateRequest ): Promise ``` **内部逻辑概述**: ``` 1. 发起 POST /orchestrate-stream(带 requestBody 和 AbortSignal) 2. 遍历 SSE 事件流,解析 JSON 3. 根据事件名分发到不同处理函数 4. 每次修改 message 后调用 onUpdate(message),触发前端 re-render 5. 接收 done 或 error 事件时停止 ``` ### 3.2 关键事件处理函数 #### **intent 事件** ```typescript // 来自后端:{ intent: "follow_up", confidence: 0.88, reason: "..." } case 'intent': const intentName = data.intent; message.intent = { name: intentName, ...data }; // 👇 根据意图确定流程分支 if (intentName === 'find_expert' || intentName === 'reselect_expert') { flow = 'expert'; // 接下来期待 expert_start/expert_end } else if (intentName === 'follow_up') { flow = 'follow_up'; // 接下来期待 delta(文本块) message.phase = 'follow-up'; message.outputFormat = 'markdown'; } else if (intentName === 'unknown') { flow = 'clarify'; // 接下来期待 clarify 事件 } else { // start_analysis / restart_analysis / continue_analysis flow = 'generate'; // 接下来期待 start / step_start / step_end } break; ``` **前端的 flow 变量**(内部状态,不持久化):用于判断预期的**下一个事件类型**和**数据处理方式**。 #### **expert_start / expert_end 事件** ```typescript case 'expert_start': message.phase = 'expert-match'; message.expertMatch = { status: 'searching', thinking: '', experts: [], selectedExpertId: undefined, }; break; case 'expert_end': // data = { experts: [...], thinking: str, is_reselect: bool, ... } message.expertMatch!.status = 'ready'; message.expertMatch!.thinking = data.thinking; // 逐张卡片追加,配合 pop 动画 for (const expert of data.experts) { message.expertMatch!.experts.push({ id: expert.id, name: expert.name, detail: expert.detail, ... }); // 每张卡片之间延迟 100ms,触发 pop 动画 } // 卡片全部加载后,更新 phase 并打开 MCP 选择弹窗 message.phase = 'expert-confirm'; // → SixStepChatPage 监听 expertMatch.status === 'ready',打开 mcpModalOpen break; ``` #### **clarify 事件** ```typescript case 'clarify': message.phase = 'clarifying'; message.clarifyQuestion = data.message; // "我没有完全理解你的意图,请补充说明..." break; ``` #### **resume 事件**(continue_analysis 特有) ```typescript case 'resume': // data = { task_id, resume_from, mcp_list: [...] } message.resumedFrom = data.resume_from; // 后端告诉前端从哪个步骤继续 // UI 用途:在 AssistantMessage 顶部显示 "⏸ 从 [步骤名] 继续" // 该变量会保留在消息中,整个 continue 过程结束后仍显示 break; ``` #### **start 事件**(开始 6bf 编排) ```typescript case 'start': message.phase = 'generate'; // data = { task_id, mcp_list: [...], ts: number } // 👇 防止 restart_analysis 时保留旧卡片 if (!isResume) { // isResume 由 resume 事件设置,continue_analysis 时为 true message.stages = {}; // 清空 message.subStepStatus = {}; message.mainStepStatus = { info: 'done', sixstep: 'pending', final: 'pending' }; } // 若 isResume === true(continue_analysis),则保留已有的 stages,只追加新步骤 message.currentMainStep = 'sixstep'; break; ``` **关键**:`!isResume` 的条件确保 **restart_analysis 时清空旧卡片**,而 **continue_analysis 时保留已有卡片**。 #### **step_start / step_end 事件** ```typescript case 'step_start': // data = { name: "目的分析", key: "purpose" } const stepKey = data.key; if (stepKey === 'purpose') { message.subStepStatus['purpose'] = 'running'; message.currentSubStep = 'purpose'; message.stages!.purpose = { thinking: '', cards: [], complete: false }; } // ... 类似处理 actor / behavior / narrative break; case 'step_end': // data = { key, status, thinking, structured_outputs, markdown, references, ... } const stepKey = data.key; // 👇 原始数据存档,用于后续 continue_analysis / follow_up if (stepKey !== 'info_prepare') { message.rawSteps = message.rawSteps || []; message.rawSteps.push(data); // 保存完整的 BackendStepResult } // 更新当前消息的对应阶段 if (stepKey === 'purpose') { message.stages!.purpose!.thinking = data.thinking; message.stages!.purpose!.cards = buildPurposeCards(data.structured_outputs); message.stages!.purpose!.complete = true; message.subStepStatus['purpose'] = 'done'; } // ... 类似处理 actor / behavior / narrative break; ``` #### **delta 事件**(follow_up 专有) ```typescript case 'delta': // data = { text: "..." }(文本块) // 仅当 flow === 'follow_up' 时处理 if (flow === 'follow_up') { message.markdownContent = (message.markdownContent || '') + data.text; onUpdate(message); // 逐块更新前端,流式显示 } break; ``` #### **done 事件** ```typescript case 'done': // 各分支的终止信号 if (flow === 'follow_up') { message.phase = 'completed'; // ✅ follow_up 回答完成 } else if (flow === 'expert') { // 专家查询完成,等待前端用户在弹窗中确认选择 // phase 保持 'expert-confirm' } else if (flow === 'generate') { message.phase = 'completed'; // ✅ 6bf 推演完成 } else if (flow === 'clarify') { // 保持 'clarifying',等待用户重新输入 } onComplete?.(); // 回调:请求完成 break; ``` --- ## 四、主页面逻辑(SixStepChatPage.tsx) ### 4.1 新增状态变量 ```typescript // 当前阶段(驱动 UI 和后端意图识别) const [currentPhase, setCurrentPhase] = useState('idle'); // 已选专家和 MCP(用于请求体和 UI 展示) const [selectedExpert, setSelectedExpert] = useState(null); const [selectedMcps, setSelectedMcps] = useState([]); // 已推荐过的专家列表(reselect_expert 时用于过滤) const [expertCandidates, setExpertCandidates] = useState([]); // 临时保存原始用户输入(确认专家后需要) const [lastUserInput, setLastUserInput] = useState(''); ``` ### 4.2 handleSend() — 请求体构造 ```typescript async function handleSend(inputText: string) { setLastUserInput(inputText); // 保存原始输入 // 【1】获取上次的助手消息(用于获取 rawSteps) const lastAssistant = messages.findLast(m => m.role === 'assistant'); // 【2】构造请求体(关键!传递上下文给后端) const requestBody: IntentOrchestrateRequest = { intent_mode: 'auto', // 自动意图识别 current_phase: currentPhase, selected_expert: selectedExpert, selected_mcps: selectedMcps, last_steps: lastAssistant?.rawSteps || [], // 👈 用于 continue / follow_up resume_from_step: undefined, // 让后端自动推断 expert_candidates: expertCandidates, // 👈 用于 reselect_expert 过滤 }; // 【3】调用流处理 const controller = await runIntentOrchestrateStream( inputText, { onUpdate: (msg) => { // 替换消息列表中的末尾消息(正在生成的消息) setMessages(prev => { const newMessages = [...prev]; newMessages[newMessages.length - 1] = msg; return newMessages; }); // 同步更新当前阶段 setCurrentPhase(msg.phase || 'idle'); // 【关键】监听专家卡片加载完成,打开 MCP 选择弹窗 if (msg.expertMatch?.status === 'ready') { setMcpModalOpen(true); } }, onComplete: () => { setIsGenerating(false); }, }, requestBody ); controllerRef.current = controller; setIsGenerating(true); } ``` **核心三步**: 1. **构造请求体**:携带所有必要的上下文(current_phase, last_steps, expert_candidates) 2. **调用流处理**:传入 onUpdate 回调,每个 SSE 事件都更新前端 3. **监听事件**:特别是 expertMatch.status === 'ready',自动打开 MCP 弹窗 ### 4.3 handleMcpModalConfirm() — 确认专家后立即启动分析 ```typescript function handleMcpModalConfirm(selectedExpert: ExpertCard, selectedMcpList: SelectableMcpKey[]) { // 【1】保存用户选择 setSelectedExpert(selectedExpert); setSelectedMcps(selectedMcpList); setExpertCandidates(prev => [...prev, selectedExpert]); // 添加到候选列表 // 【2】立即用 force_orchestrate 启动分析(不需要等用户再次输入) const requestBody: IntentOrchestrateRequest = { intent_mode: 'force_orchestrate', // 👈 强制执行编排,不识别意图 current_phase: 'expert-confirm', selected_expert: selectedExpert, selected_mcps: selectedMcpList, last_steps: [], }; // 【3】使用保存的原始输入(用户之前在输入框里输入的文本) runIntentOrchestrateStream( lastUserInput, // 👈 原始输入 { onUpdate: (msg) => { setMessages(prev => [...prev.slice(0, -1), msg]); setCurrentPhase(msg.phase || 'idle'); }, onComplete: () => setIsGenerating(false), }, requestBody ); } ``` **关键点**: - `intent_mode: 'force_orchestrate'` — 跳过意图识别,直接执行 6bf - 使用 `lastUserInput`(保存的原始输入)而非动态输入 - 添加 selectedExpert 到 expertCandidates,以便后续 reselect 时过滤 ### 4.4 handleRefetchExpert() — 重选专家 ```typescript function handleRefetchExpert() { const requestBody: IntentOrchestrateRequest = { intent_mode: 'force_find_expert', // 👈 强制查询专家 current_phase: 'expert-confirm', expert_candidates: expertCandidates, // 👈 告诉后端已有候选,避免重复推荐 last_steps: [], }; runIntentOrchestrateStream( lastUserInput, { onUpdate: (msg) => { setMessages(prev => [...prev.slice(0, -1), msg]); setCurrentPhase(msg.phase || 'idle'); // MCP 弹窗保持打开,等待用户重新选择 }, }, requestBody ); } ``` ### 4.5 handleStop() — 停止生成 ```typescript function handleStop() { controllerRef.current?.stop(); // 👇 关键:标记当前消息为 paused,供后续 continue_analysis 识别 setMessages(prev => { const newMessages = [...prev]; const lastMsg = newMessages[newMessages.length - 1]; if (lastMsg?.role === 'assistant') { lastMsg.phase = 'paused'; } return newMessages; }); setCurrentPhase('paused'); setIsGenerating(false); } ``` **为什么需要标记 paused?** - 后端的 intent 识别器会检查 `current_phase === 'paused'`,优先返回 `continue_analysis` 意图 - 前端显示 "⏸ 从 [步骤] 继续" 提示,让用户清楚地知道可以继续 --- ## 五、对话区渲染(SixStepConversation.tsx) ### 5.1 AssistantMessage 的新渲染逻辑 ```typescript const AssistantMessage = ({ message, active }: Props) => { const isExpertPhase = message.phase === 'expert-match' || message.phase === 'expert-confirm'; const isFollowUp = message.phase === 'follow-up'; const isClarifying = message.phase === 'clarifying'; const isMarkdown = message.outputFormat === 'markdown'; // 👇 【防护条件】防止旧分析内容在专家/澄清/追问阶段出现 const showAnalysisContent = !isExpertPhase && !isClarifying && !isFollowUp; return (
{/* 【分支 1】专家匹配阶段 */} {isExpertPhase && message.expertMatch && ( )} {/* 【分支 2】6bf 分析阶段(防护条件) */} {showAnalysisContent && ( <> {/* 继续分析提示 */} {message.resumedFrom && (
⏸ 从「{message.resumedFrom}」继续
)} {/* 各阶段卡片 */} {message.stages?.info && } {message.stages?.purpose && } {message.stages?.actor && } {message.stages?.behavior && } {message.stages?.narrative && } {message.finalAnswer && } )} {/* 【分支 3】追问回答(Markdown) */} {isFollowUp && message.markdownContent && ( )} {/* 【分支 4】Markdown 输出格式 */} {isMarkdown && !isFollowUp && message.markdownContent && ( )} {/* 【分支 5】澄清问题 */} {isClarifying && message.clarifyQuestion && (

我需要你进一步说明:

{message.clarifyQuestion}

请重新输入以补充信息

)}
); }; ``` **关键防护条件**: ```typescript const showAnalysisContent = !isExpertPhase && !isClarifying && !isFollowUp; ``` 这一行确保了: - ✅ `expert-match` 阶段只显示专家卡片,不显示旧分析 - ✅ `clarifying` 阶段只显示澄清问题,不显示其他内容 - ✅ `follow-up` 阶段只显示 Markdown 回答,不显示旧分析 ### 5.2 阶段加载动画 ```typescript // 当前阶段正在生成时,显示 shimmer(流光)动画 const purposeGenerating = message.phase === 'generate' && message.currentSubStep === 'purpose' && active; } title="目的分析" count={message.stages?.purpose?.cards.length || 0} /> ``` --- ## 六、生命周期示例 ### 6.1 用户"找专家"的流程 ``` 【用户输入】"帮我找个专家" ↓ 【前端】handleSend("帮我找个专家") - requestBody: { intent_mode: 'auto', current_phase: 'idle', last_steps: [] } - 调用 runIntentOrchestrateStream ↓ 【后端】detect_intent("帮我找个专家", current_phase='idle', ...) - 规则识别:匹配 _FIND_PATTERNS → confidence=0.88 - 返回:IntentResult(intent='find_expert', ...) ↓ 【后端】根据 intent='find_expert',调用 _stream_find_expert() - emit: expert_start - 查询 find_expert MCP agent - emit: expert_end { experts: [...] } - emit: done ↓ 【前端】处理 SSE 事件 - intent: { name: 'find_expert', ... } → flow = 'expert' - expert_start → message.phase = 'expert-match' → message.expertMatch.status = 'searching' - expert_end → message.expertMatch.status = 'ready' → message.expertMatch.experts = [...] → message.phase = 'expert-confirm' → 检测到 expertMatch.status === 'ready' → setMcpModalOpen(true) 打开 MCP 选择弹窗 - done → onComplete() ↓ 【用户】在 MCP 选择弹窗中选择专家和 MCP,点击"确认" ↓ 【前端】handleMcpModalConfirm(selectedExpert, selectedMcps) - setSelectedExpert(selectedExpert) - setSelectedMcps(selectedMcps) - requestBody: { intent_mode: 'force_orchestrate', ... } - 调用 runIntentOrchestrateStream(lastUserInput, ...) ↓ 【后端】orchestrate_stream() with intent_mode='force_orchestrate' - 跳过意图识别 - 调用 _stream_orchestrate(..., intent_name='start_analysis') - emit: start - emit: step_start / step_end × 4 (purpose / actor / behavior / narrative) - emit: flow { nodes, edges } - emit: done ↓ 【前端】处理 SSE 事件 - start → message.phase = 'generate' → 初始化 stages 和状态 - step_start × 4 → 每个步骤的 status = 'running' → 对应阶段初始化为空 - step_end × 4 → 填充 stages[stepKey].cards → message.rawSteps.push(data) → status = 'done' - flow → 流程图数据更新(供沙箱显示) - done → message.phase = 'completed' ↓ 【前端 UI】 - ✅ 显示 4 个阶段的卡片 - ✅ 显示流程图沙箱 - ✅ 阶段完成时有 pop 动画和叙事卡片 ``` ### 6.2 用户"继续分析"的流程 ``` 【前次】完成了目的、行为体阶段,用户点击"停止" - 后端返回 step_end(预期行为/驱动因素)但用户取消 - message.phase = 'paused' - message.rawSteps = [ { key: 'purpose', status: 'completed', ... }, { key: 'actor', status: 'completed', ... }, { key: 'behavior_factor', status: 'running', ... } ] ↓ 【用户输入】"继续" ↓ 【前端】handleSend("继续") - requestBody: { intent_mode: 'auto', current_phase: 'paused', // 👈 关键 last_steps: message.rawSteps, // 👈 关键 } ↓ 【后端】detect_intent("继续", current_phase='paused', last_steps=[...]) - 规则识别:匹配 _CONTINUE_PATTERNS AND current_phase='paused' - 返回:IntentResult(intent='continue_analysis', confidence=0.90) ↓ 【后端】_stream_orchestrate(..., intent_name='continue_analysis') - _resume_context_from_steps(req.last_steps, None) → 检查已完成的步骤:['purpose', 'actor'] → resume_key = 'behavior_factor'(下一个未完成的) - emit: resume { resume_from: 'behavior_factor', mcp_list: ['behavior_factor', 'narrative'] } - emit: start { mcp_list: ['behavior_factor', 'narrative'] } - emit: step_start / step_end × 2 (behavior_factor, narrative) - emit: done ↓ 【前端】处理 SSE 事件 - intent: { name: 'continue_analysis', ... } - resume: { resume_from: 'behavior_factor' } → message.resumedFrom = 'behavior_factor' → isResume = true // 标记为继续,不清空旧 stages - start → 因为 isResume=true,不清空 message.stages → 保留已有的 purpose/actor 卡片 - step_start / step_end × 2 → 追加新的 behavior/narrative 卡片 → message.rawSteps.push(behavior_data) → message.rawSteps.push(narrative_data) - done → message.phase = 'completed' ↓ 【前端 UI】 - ✅ 显示"⏸ 从「预期行为与驱动因素」继续"提示 - ✅ 保留旧的目的/行为体卡片 - ✅ 追加新的预期行为/叙事卡片 ``` ### 6.3 用户"追问"的流程 ``` 【前次】完成了完整的 6bf,message.phase = 'completed' - message.rawSteps = [ { key: 'purpose', ... }, { key: 'actor', ... }, { key: 'behavior_factor', ... }, { key: 'narrative', ... } ] ↓ 【用户输入】"第一条是什么意思" ↓ 【前端】handleSend("第一条是什么意思") - requestBody: { intent_mode: 'auto', current_phase: 'completed', // 👈 关键 last_steps: message.rawSteps, } ↓ 【后端】detect_intent("第一条是什么意思", current_phase='completed', last_steps=[...]) - 规则识别:匹配 _FOLLOW_UP_PATTERNS(r"第[一二三四五六七八九十\d]+[点条个项]") - 返回:IntentResult(intent='follow_up', confidence=0.88) ↓ 【后端】_stream_follow_up(req, ...) - 提取 req.last_steps 中已完成步骤的数据 - 构造 context_str(包含目的、行为体、预期行为、叙事) - 调用 LLM 流式回答 - emit: delta { text: "..." } × N - emit: done ↓ 【前端】处理 SSE 事件 - intent: { name: 'follow_up', ... } → flow = 'follow_up' → message.phase = 'follow-up' → message.outputFormat = 'markdown' - delta × N → message.markdownContent += text → 每块更新都触发 onUpdate,前端流式显示 - done → message.phase = 'completed' ↓ 【前端 UI】 - ✅ 显示 Markdown 格式的追问回答 - ✅ 不显示旧的 6bf 卡片(因为 showAnalysisContent = false) - ✅ 流式效果:用户看到实时生成的文本 ``` --- ## 七、常见问题解决 ### Q1: 完成 6bf 后再选专家,为什么显示了旧的分析内容? **原因**:AssistantMessage 中没有正确应用 `showAnalysisContent` 防护条件。 **检查**: ```typescript const showAnalysisContent = !isExpertPhase && !isClarifying && !isFollowUp; ``` 确保: - `isExpertPhase = message.phase === 'expert-match' || message.phase === 'expert-confirm'` ✓ - 所有 `stages` / `finalAnswer` 的渲染都被 `{showAnalysisContent && (...)}` 包裹 ✓ ### Q2: 继续分析后,旧卡片消失了 **原因**:SSE start 事件中清空了 stages(应该只在 !isResume 时清空)。 **检查** `api.ts` 中的 start 事件处理: ```typescript case 'start': if (!isResume) { // 👈 这个条件必须有 message.stages = {}; } // ... break; ``` ### Q3: 用户点"继续",但后端返回了"开始新分析" **原因**:`current_phase` 未传递为 'paused',或未手动设置 `phase = 'paused'`。 **检查** `SixStepChatPage.handleStop()`: ```typescript setMessages(prev => { const newMessages = [...prev]; const lastMsg = newMessages[newMessages.length - 1]; if (lastMsg?.role === 'assistant') { lastMsg.phase = 'paused'; // 👈 必须设置 } return newMessages; }); setCurrentPhase('paused'); // 👈 必须同步状态 ``` ### Q4: 追问回答为空 **原因**: 1. `current_phase !== 'completed'`(后端不识别为 follow_up) 2. `last_steps` 为空或格式错误 3. LLM 调用失败(查看后端日志) **检查**: - 确保 message.rawSteps 在 step_end 时被正确赋值 - 确保 request 中 `last_steps` 被传递为 `message.rawSteps` - 后端日志是否有 `_stream_follow_up` 的报错 --- ## 八、开发建议 1. **调试技巧**:在 `onUpdate` 中 `console.log(message)` 观察每个事件的状态变化 2. **SSE 事件顺序不可打破**:前端严格依赖事件顺序做状态机转换 3. **rawSteps 的完整性**:非 info_prepare 的所有 step_end 都必须存档,否则 continue/follow_up 会丢失数据 4. **phase 与 outputFormat 的对应**: - follow-up → outputFormat = 'markdown' - generate → outputFormat = 'structured'(卡片渲染) - expert-match → 无 outputFormat(专家卡片独立) --- **相关文件导航**: - 后端意图系统:`backend/app/intent/` - 后端编排系统:`backend/app/orchestrator.py` - 前端核心:`SixStepChatPage.tsx` (state hub) → `api.ts` (SSE handler) → `SixStepConversation.tsx` (rendering) - 前端类型:`types.ts` - 前端原始文档:`doc.md`