28 KiB
28 KiB
前端意图识别与交互集成 — 实现指南
对应后端系统:
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 枚举
export type SixStepPhase =
| 'idle' // 【初始】未开始任何操作
| 'intent-detecting' // 【过渡】意图正在识别中(通常很短,用户看不到)
| 'expert-match' // 【等待】展示专家卡片,用户可浏览、搜索、重选
| 'expert-confirm' // 【交互】MCP 多选弹窗打开,用户确认选择
| 'generate' // 【处理】6bf 编排执行中,显示进度和卡片
| 'paused' // 【中断】用户手动停止;可继续或重新分析
| 'completed' // 【完成】编排全部完成;可追问或重新分析
| 'clarifying' // 【澄清】后端无法理解,请用户补充
| 'follow-up'; // 【追问】正在基于已有分析回答用户问题
关键点:
expert-matchvsexpert-confirm的区别:前者是显示结果,后者是交互确认(MCP 弹窗)paused必须在handleStop()手动设置,后端的 continue_analysis 分支不会主动发送 pausedfollow-up仅在current_phase === 'completed'且用户输入匹配追问模式时触发
2.2 SixStepMessage 新增字段
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)
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 处理器
export async function runIntentOrchestrateStream(
message: string,
options: {
onUpdate?: (msg: SixStepMessage) => void;
onComplete?: () => void;
abortSignal?: AbortSignal;
},
requestBody?: IntentOrchestrateRequest
): Promise<SixStepStreamController>
内部逻辑概述:
1. 发起 POST /orchestrate-stream(带 requestBody 和 AbortSignal)
2. 遍历 SSE 事件流,解析 JSON
3. 根据事件名分发到不同处理函数
4. 每次修改 message 后调用 onUpdate(message),触发前端 re-render
5. 接收 done 或 error 事件时停止
3.2 关键事件处理函数
intent 事件
// 来自后端:{ 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 事件
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 事件
case 'clarify':
message.phase = 'clarifying';
message.clarifyQuestion = data.message; // "我没有完全理解你的意图,请补充说明..."
break;
resume 事件(continue_analysis 特有)
case 'resume':
// data = { task_id, resume_from, mcp_list: [...] }
message.resumedFrom = data.resume_from; // 后端告诉前端从哪个步骤继续
// UI 用途:在 AssistantMessage 顶部显示 "⏸ 从 [步骤名] 继续"
// 该变量会保留在消息中,整个 continue 过程结束后仍显示
break;
start 事件(开始 6bf 编排)
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 事件
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 专有)
case 'delta':
// data = { text: "..." }(文本块)
// 仅当 flow === 'follow_up' 时处理
if (flow === 'follow_up') {
message.markdownContent = (message.markdownContent || '') + data.text;
onUpdate(message); // 逐块更新前端,流式显示
}
break;
done 事件
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 新增状态变量
// 当前阶段(驱动 UI 和后端意图识别)
const [currentPhase, setCurrentPhase] = useState<SixStepPhase>('idle');
// 已选专家和 MCP(用于请求体和 UI 展示)
const [selectedExpert, setSelectedExpert] = useState<ExpertCard | null>(null);
const [selectedMcps, setSelectedMcps] = useState<SelectableMcpKey[]>([]);
// 已推荐过的专家列表(reselect_expert 时用于过滤)
const [expertCandidates, setExpertCandidates] = useState<ExpertCard[]>([]);
// 临时保存原始用户输入(确认专家后需要)
const [lastUserInput, setLastUserInput] = useState<string>('');
4.2 handleSend() — 请求体构造
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);
}
核心三步:
- 构造请求体:携带所有必要的上下文(current_phase, last_steps, expert_candidates)
- 调用流处理:传入 onUpdate 回调,每个 SSE 事件都更新前端
- 监听事件:特别是 expertMatch.status === 'ready',自动打开 MCP 弹窗
4.3 handleMcpModalConfirm() — 确认专家后立即启动分析
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() — 重选专家
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() — 停止生成
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 的新渲染逻辑
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 (
<div>
{/* 【分支 1】专家匹配阶段 */}
{isExpertPhase && message.expertMatch && (
<ExpertMatchCards
data={message.expertMatch}
onRefetch={onRefetch}
onConfirm={onConfirm}
/>
)}
{/* 【分支 2】6bf 分析阶段(防护条件) */}
{showAnalysisContent && (
<>
{/* 继续分析提示 */}
{message.resumedFrom && (
<div className="resume-hint">
⏸ 从「{message.resumedFrom}」继续
</div>
)}
{/* 各阶段卡片 */}
{message.stages?.info && <InfoStage ... />}
{message.stages?.purpose && <PurposeStage ... />}
{message.stages?.actor && <ActorStage ... />}
{message.stages?.behavior && <BehaviorStage ... />}
{message.stages?.narrative && <NarrativeStage ... />}
{message.finalAnswer && <FinalAnswer ... />}
</>
)}
{/* 【分支 3】追问回答(Markdown) */}
{isFollowUp && message.markdownContent && (
<MarkdownRenderer content={message.markdownContent} />
)}
{/* 【分支 4】Markdown 输出格式 */}
{isMarkdown && !isFollowUp && message.markdownContent && (
<MarkdownRenderer content={message.markdownContent} />
)}
{/* 【分支 5】澄清问题 */}
{isClarifying && message.clarifyQuestion && (
<div className="clarify-block">
<p className="text-sm text-gray-600">我需要你进一步说明:</p>
<p className="font-medium">{message.clarifyQuestion}</p>
<p className="text-xs text-gray-500 mt-2">请重新输入以补充信息</p>
</div>
)}
</div>
);
};
关键防护条件:
const showAnalysisContent = !isExpertPhase && !isClarifying && !isFollowUp;
这一行确保了:
- ✅
expert-match阶段只显示专家卡片,不显示旧分析 - ✅
clarifying阶段只显示澄清问题,不显示其他内容 - ✅
follow-up阶段只显示 Markdown 回答,不显示旧分析
5.2 阶段加载动画
// 当前阶段正在生成时,显示 shimmer(流光)动画
const purposeGenerating = message.phase === 'generate' &&
message.currentSubStep === 'purpose' &&
active;
<StageHeader
loading={purposeGenerating}
icon={<Target ... />}
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 防护条件。
检查:
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 事件处理:
case 'start':
if (!isResume) { // 👈 这个条件必须有
message.stages = {};
}
// ...
break;
Q3: 用户点"继续",但后端返回了"开始新分析"
原因:current_phase 未传递为 'paused',或未手动设置 phase = 'paused'。
检查 SixStepChatPage.handleStop():
setMessages(prev => {
const newMessages = [...prev];
const lastMsg = newMessages[newMessages.length - 1];
if (lastMsg?.role === 'assistant') {
lastMsg.phase = 'paused'; // 👈 必须设置
}
return newMessages;
});
setCurrentPhase('paused'); // 👈 必须同步状态
Q4: 追问回答为空
原因:
current_phase !== 'completed'(后端不识别为 follow_up)last_steps为空或格式错误- LLM 调用失败(查看后端日志)
检查:
- 确保 message.rawSteps 在 step_end 时被正确赋值
- 确保 request 中
last_steps被传递为message.rawSteps - 后端日志是否有
_stream_follow_up的报错
八、开发建议
- 调试技巧:在
onUpdate中console.log(message)观察每个事件的状态变化 - SSE 事件顺序不可打破:前端严格依赖事件顺序做状态机转换
- rawSteps 的完整性:非 info_prepare 的所有 step_end 都必须存档,否则 continue/follow_up 会丢失数据
- 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