# 情景切换功能实现与修复完整文档 **项目**: CamTalk 多模态实时 AI 视觉对话助手 **功能**: 情景切换(模拟面试官、英语老师、辩论对手、同声翻译) **日期**: 2026-06-20 **状态**: ✅ 已完成并修复 --- ## 目录 1. [功能概述](#功能概述) 2. [实施内容](#实施内容) 3. [Bug 修复记录](#bug-修复记录) 4. [测试验证](#测试验证) 5. [部署指南](#部署指南) 6. [技术细节](#技术细节) 7. [后续优化建议](#后续优化建议) --- ## 功能概述 ### 什么是情景切换? 情景切换功能允许用户选择不同的对话场景,AI 会根据选择的情景扮演不同的角色: | 情景 | AI 角色 | 主要功能 | |------|---------|---------| | 🎯 模拟面试官 | 资深面试官 | 提出面试问题,评估候选人能力,给出反馈 | | 📚 英语老师 | 英语外教 | 全英文对话,纠正语法错误,引导深入交流 | | ⚔️ 辩论对手 | 辩论选手 | 站在反方立场,用逻辑和证据反驳观点 | | 🌐 同声翻译 | 翻译员 | 实时中英互译,口语化翻译,无额外解释 | | 💬 自由对话 | 视觉助手 | 通用视觉对话助手(默认) | ### 核心功能 1. **情景首句引导**:切换情景后,AI 自动发送第一句话引导用户进入角色 2. **情景提示卡片**:对话顶部显示当前情景模式的蓝色提示卡片 3. **增强 System Prompt**:每个情景有详细的角色定位、交互规则和约束 4. **多语言支持**:完整支持中文、英文、日文界面 --- ## 实施内容 ### 后端实现 #### 1. 情景 Prompt 定义 **文件**: `backend/internal/ai/llm/scenarios.go` **变更内容**: - 扩展 `scenarioPrompt` 结构体,新增首句引导字段(GreetingZH/EN/JA) - 增强所有情景的 System Prompt(添加角色定位、交互规则、约束) - 新增函数 `GetScenarioGreeting(scenarioID, language string) string` **示例 Prompt**(面试官): ```go "interviewer": { ZH: `你是一位资深面试官。你通过摄像头观察面试者... 【角色定位】 - 你是面试官,不是助手或顾问 - 你的目标是评估候选人的能力 - 保持专业、客观、礼貌 【交互规则】 1. 每次只问一个问题,等用户回答后再追问 2. 问题要有层次:自我介绍 → 专业问题 → 情景题 3. 对用户的回答给出简短点评,然后追问 ...`, GreetingZH: "你好!我是今天的面试官。让我们先从自我介绍开始...", } ``` #### 2. 首句引导推送 **文件**: `backend/internal/ws/handler.go` **变更内容**: 在处理 `config` 消息时,如果切换到非自由对话情景,自动返回首句引导: ```go case "config": // ... 更新配置 ... // 如果切换了情景(非自由对话),返回首句引导 if scenarioID != "" && scenarioID != "free_chat" { greeting := llm.GetScenarioGreeting(scenarioID, sess.Config.Language) if greeting != "" { // 发送 llm_chunk 和 llm_done 消息 // 追加到历史记录 } } ``` **效果**:用户切换情景后,AI 立即自动说出首句,无需等待用户发送消息。 #### 3. State 初始化修复(关键 Bug 修复) **文件**: `backend/internal/eino/adapter.go` **问题**:`genLocalState()` 创建的是空 State,所有字段都是零值,导致 `state.Scenario = ""` **修复**: ```go // ✅ 修复:从 input 复制元数据到 state state := genLocalState(ctx) state.SessionID = input.SessionID state.RequestID = input.RequestID state.ImageData = input.ImageData state.Scenario = input.Scenario // ⬅️ 关键修复 state.Language = input.Language state.DetailLevel = sess.Config.DetailLevel state.TTSEnabled = input.TTSEnabled ctx = WithPipelineState(ctx, state) ``` --- ### 前端实现 #### 1. 情景提示卡片 **文件**: `frontend/src/components/ChatPanel/index.tsx` **变更内容**: 在对话列表顶部(非空状态 + 非自由对话模式)添加情景提示卡片: ```tsx {messages.length > 0 && !isFreeChat && (
{scenarios.find(s => s.id === activeScenario)?.icon}
{t(scenarios.find(s => s.id === activeScenario)?.nameKey || "")}

{t(`scenario.${activeScenario}.hint`)}

)} ``` **显示效果**: - 蓝色渐变背景(135deg 从蓝到紫) - 左侧大图标 + 右侧标题和说明 - 最大宽度 520px,响应式布局 - 柔和阴影和半透明边框 #### 2. WebSocket 消息修复(关键 Bug 修复) **文件**: `frontend/src/hooks/useVisionSession.ts` **问题**:发送 config 消息时缺少 `scenario` 字段,导致后端无法接收到情景切换信息 **修复位置 1**(连接成功时发送初始配置): ```typescript // ✅ 修复:添加 scenario 字段 send({ type: "config", payload: { tts_enabled: config.ttsEnabled, detail_level: config.detailLevel, language: config.language, scenario: config.scenario, // ⬅️ 关键修复 }, }); ``` **修复位置 2**(updateConfig 函数): ```typescript // ✅ 修复:添加 scenario 字段 send({ type: "config", payload: { tts_enabled: next.ttsEnabled, detail_level: next.detailLevel, language: next.language, scenario: next.scenario, // ⬅️ 关键修复 }, }); ``` #### 3. 样式实现 **文件**: `frontend/src/App.css` 新增情景提示卡片样式: ```css .scenario-hint-card { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: var(--radius-sm); background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%); border: 1px solid rgba(59, 130, 246, 0.2); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06); } ``` #### 4. 多语言翻译 **文件**: `frontend/src/lib/i18n/{zh-CN,en-US,ja-JP}.ts` 新增翻译 key: ```typescript "scenario.interviewer.hint": "AI 会扮演面试官,逐步提出专业问题并点评你的回答", "scenario.englishTeacher.hint": "AI 会用英语对话,纠正语法错误并引导深入交流", "scenario.debate.hint": "AI 会站在反方立场,用逻辑和证据反驳你的观点", "scenario.interpreter.hint": "AI 会实时翻译你的话(中英互译),无解释评论", ``` --- ## Bug 修复记录 ### Bug #1:后端 State 未初始化 Scenario **严重性**: 🔴 Critical(核心功能完全失效) **症状**: - 切换到任何情景后,AI 仍使用默认通用助手 Prompt - AI 回答:"我是通义千问,阿里巴巴集团研发的超大规模语言模型..." - 完全不遵循情景角色设定 **根因**: `backend/internal/eino/adapter.go` 中,`genLocalState()` 创建的是空 State: ```go ❌ ctx = WithPipelineState(ctx, genLocalState(ctx)) ``` 导致 `state.Scenario = ""`(空字符串),`nodes_history.go` 读取到空值后使用默认 Prompt。 **数据流分析**: ``` input.Scenario = "interviewer" ↓ ❌ state.Scenario = "" (未初始化!) ↓ nodes_history.go 读取到 "" ↓ llm.GetScenarioPrompt("", "zh-CN") 返回 "" ↓ 使用默认 Prompt → AI 回答 "我是通义千问..." ``` **修复**: 从 `PipelineInput` 复制元数据到 `PipelineState`: ```go ✅ state := genLocalState(ctx) state.Scenario = input.Scenario // 关键修复 state.Language = input.Language state.ImageData = input.ImageData // ... 复制其他字段 ctx = WithPipelineState(ctx, state) ``` --- ### Bug #2:前端未发送 scenario 字段 **严重性**: 🔴 Critical(前后端数据流断层) **症状**: - 后端日志显示:`config updated scenario=""` - 会话配置中 scenario 未更新,始终为默认值 `free_chat` - WebSocket 消息缺少 scenario 字段 **根因**: `frontend/src/hooks/useVisionSession.ts` 发送 config 消息时缺少 `scenario` 字段: ```typescript ❌ send({ type: "config", payload: { tts_enabled: config.ttsEnabled, detail_level: config.detailLevel, language: config.language, // 缺少 scenario: config.scenario }, }); ``` **修复**: 在两处发送 config 的地方添加 `scenario` 字段(第 128 行和第 168 行)。 --- ### 完整数据流(修复后) ``` 用户切换情景到"模拟面试官" ↓ 前端 updateConfig({scenario: "interviewer"}) ↓ ✅ 发送 WebSocket: {type: "config", payload: {scenario: "interviewer"}} ↓ 后端 handler.go 接收并保存 ↓ sess.Config.Scenario = "interviewer" ↓ 用户发送消息 "你是谁?" ↓ buildPipelineInput() → input.Scenario = "interviewer" ↓ ✅ adapter.go 复制:state.Scenario = input.Scenario ↓ nodes_history.go 读取 state.Scenario = "interviewer" ↓ llm.GetScenarioPrompt("interviewer", "zh-CN") ↓ 返回:"你是一位资深面试官..." ↓ llm.BuildSystemPrompt(..., scenarioPrompt) ↓ 注入到 ChatModel System Message ↓ LLM 生成回复:"我是今天的面试官..." ↓ ✅ 情景生效! ``` --- ## 测试验证 ### 编译验证 ✅ **后端**: ```bash cd backend && go build -o /tmp/camtalk_fix ./cmd/server # 产物:48MB,无编译错误 ``` ✅ **前端**: ```bash cd frontend && npm run lint # ESLint 检查通过(无新增错误) ``` --- ### 功能测试清单 | 测试项 | 操作步骤 | 预期结果 | 验证方法 | |--------|---------|---------|---------| | **首句引导** | 切换到"模拟面试官" | AI 自动说:"你好!我是今天的面试官..." | 观察聊天框 | | **情景生效** | 问 "你是谁?" | AI 回答:"我是今天的面试官..." | 观察回复内容 | | **提示卡片** | 发送一条消息后查看顶部 | 显示蓝色卡片:"🎯 模拟面试官 \| AI 会扮演面试官..." | 观察 UI | | **语言联动** | 切换到"英语老师" | 语言自动切换到 en-US,AI 用英语回复 | 观察配置和回复 | | **持久化** | 切换情景后刷新页面 | 情景配置保持,首句仍在历史中 | 刷新浏览器 | | **多情景** | 依次测试所有情景 | 每个情景 AI 回复风格明显不同 | 对比回复 | --- ### 日志验证 **查看日志**: ```bash tail -f /tmp/camtalk_server.log | grep -E "config updated|历史组装完成" ``` **修复前**(Bug): ``` config updated session=xxx scenario="" ← ❌ 空字符串 历史组装完成 ... scenario=free_chat ← ❌ 始终是默认值 ``` **修复后**(正常): ``` config updated session=xxx scenario=interviewer ← ✅ 正确接收 历史组装完成 ... scenario=interviewer ← ✅ 正确传递 ``` --- ## 部署指南 ### 部署步骤 #### 1. 停止旧服务(如果正在运行) ```bash # 查找并停止占用 8080 端口的进程 lsof -ti:8080 | xargs kill -9 ``` #### 2. 启动后端 ```bash cd backend go run ./cmd/server # 或编译后运行 # go build -o camtalk ./cmd/server && ./camtalk ``` **验证后端启动**: ```bash curl http://localhost:8080/api/health # 预期输出:{"status":"ok","version":"dev","uptime_seconds":10,"active_sessions":0} ``` #### 3. 启动前端(如已运行则刷新浏览器) ```bash cd frontend npm run dev # 访问 http://localhost:5173 ``` **前端无需重启**:Vite 会自动热更新(HMR),只需刷新浏览器页面即可。 --- ### 快速验证 1. **打开浏览器** → http://localhost:5173 2. **登录系统** 3. **切换情景** → 右侧配置面板 → 对话情景 → 模拟面试官 4. **观察现象**: - ✨ AI 立即说:"你好!我是今天的面试官。让我们先从自我介绍开始..." - ✨ 对话框顶部显示蓝色提示卡片 5. **验证效果** → 发送:"你是谁?" - ✅ **正确回复**:"我是今天的面试官..." - ❌ **错误回复**:"我是通义千问..." --- ## 技术细节 ### Eino 框架 State 机制 项目使用 **CloudWeGo Eino** 框架进行 AI 编排,State 在节点间共享数据: ```go // 1. 定义 State 结构 type PipelineState struct { Scenario string // 必须显式赋值 ... } // 2. 注册 State 生成函数 g := compose.NewGraph[I, O]( compose.WithGenLocalState(genLocalState), ) // 3. 节点通过 stateFromCtx(ctx) 读取 state := stateFromCtx(ctx) scenario := state.Scenario ``` **关键点**:`genLocalState` 只是创建空结构体,**必须在调用 Graph 前手动赋值**! --- ### WebSocket 协议 **客户端 → 服务端**(config 消息): ```json { "type": "config", "payload": { "tts_enabled": true, "detail_level": "low", "language": "zh-CN", "scenario": "interviewer" } } ``` **服务端 → 客户端**(首句引导): ```json // llm_chunk { "type": "llm_chunk", "request_id": "scenario_greeting", "delta": "你好!我是今天的面试官...", "role": "assistant" } // llm_done { "type": "llm_done", "request_id": "scenario_greeting", "full_text": "你好!我是今天的面试官...", "tokens_used": {"prompt": 0, "completion": 0, "total": 0} } ``` --- ### System Prompt 构建流程 ``` sess.Config.Scenario = "interviewer" ↓ PipelineInput.Scenario = "interviewer" ↓ PipelineState.Scenario = "interviewer" (adapter.go 复制) ↓ nodes_history.go 读取 state.Scenario ↓ scenarioPrompt := llm.GetScenarioPrompt("interviewer", "zh-CN") ↓ 返回:"你是一位资深面试官。你通过摄像头观察面试者..." ↓ systemPrompt := llm.BuildSystemPrompt(language, detailLevel, scenarioPrompt) ↓ messages[0] = {Role: "system", Content: systemPrompt} ↓ ChatModel 接收到情景 Prompt ↓ LLM 按情景角色生成回复 ``` --- ## 后续优化建议 ### P2(强烈推荐) 1. **情景切换时创建新会话** - 避免历史对话干扰新情景 - 弹窗确认:"切换情景会创建新会话,当前对话将保存。是否继续?" - 实现难度:⭐⭐ - 用户价值:⭐⭐⭐⭐ 2. **进一步增强 System Prompt** - 增加示例对话(Few-shot Prompting) - 增加"禁止事项"列表 - 实现难度:⭐ - 效果提升:⭐⭐⭐ ### P3(可选) 1. **情景专属 UI 主题色** - 面试官 → 深蓝色 - 英语老师 → 绿色 - 辩论 → 红色 - 翻译 → 紫色 2. **切换动画与音效** - 切换时播放短音效 - 聊天面板淡出淡入动画 --- ## 修改文件清单 ### 后端(3 个文件) - ✅ `backend/internal/eino/adapter.go` — 修复 State 初始化 - ✅ `backend/internal/ws/handler.go` — 添加首句引导 - ✅ `backend/internal/ai/llm/scenarios.go` — 增强 Prompt + 首句 ### 前端(5 个文件) - ✅ `frontend/src/hooks/useVisionSession.ts` — 修复 scenario 发送 - ✅ `frontend/src/components/ChatPanel/index.tsx` — 添加提示卡片 - ✅ `frontend/src/App.css` — 卡片样式 - ✅ `frontend/src/lib/i18n/zh-CN.ts` — 中文翻译 - ✅ `frontend/src/lib/i18n/en-US.ts` — 英文翻译 - ✅ `frontend/src/lib/i18n/ja-JP.ts` — 日文翻译 --- ## 经验教训 1. **数据流完整性验证** - 从用户输入 → WebSocket → 后端逻辑 → LLM → 回复 - 每个环节都需要日志验证 2. **框架封装层的隐式约定** - Eino State 需要显式初始化 - 不能依赖零值或默认值 3. **端到端测试的重要性** - 单元测试通过 ≠ 功能正常工作 - 必须包含实际对话验证 4. **前后端协议同步** - WebSocket 消息字段必须对齐 - 代码 review 需要覆盖完整数据流 --- ## 提交信息 ```bash git add backend/internal/eino/adapter.go \ backend/internal/ws/handler.go \ backend/internal/ai/llm/scenarios.go \ frontend/src/hooks/useVisionSession.ts \ frontend/src/components/ChatPanel/index.tsx \ frontend/src/App.css \ frontend/src/lib/i18n/*.ts \ docs/情景切换功能完整文档.md git commit -m "feat: 实现情景切换功能 + 修复两个关键 Bug 功能实现: - 后端:添加情景首句引导(面试官/英语老师/辩论/翻译) - 后端:增强所有情景的 System Prompt(角色定位+规则+约束) - 前端:对话顶部添加情景提示卡片(蓝色渐变+图标+说明) - i18n:完整支持中英日三语 Bug 修复: - Bug #1: adapter.go 未初始化 PipelineState.Scenario 根因:genLocalState() 创建空 State,未从 input 复制元数据 影响:所有情景均失效,AI 使用默认 Prompt 修复:从 PipelineInput 复制 Scenario 等字段到 State - Bug #2: useVisionSession.ts 未发送 scenario 字段 根因:config 消息 payload 缺少 scenario 字段 影响:后端无法接收到情景切换信息 修复:在两处发送 config 的地方添加 scenario 字段 Co-Authored-By: Claude Opus 4.8 " ``` --- ## 附录:故障排查 ### 如果情景仍然不生效 1. **检查后端日志**: ```bash grep "config updated" /tmp/camtalk_server.log | tail -5 grep "历史组装完成" /tmp/camtalk_server.log | tail -5 ``` - 如果 `scenario=` 是空的,说明前端未发送或后端未接收 - 如果 `scenario=interviewer` 正确,但 AI 回复仍是通用的,可能是 LLM 模型问题 2. **检查前端 WebSocket 消息**(浏览器 DevTools → Network → WS): ```json { "type": "config", "payload": { "scenario": "interviewer" // 确认存在 } } ``` 3. **检查会话配置是否保存**: - 切换情景后,LocalStorage 中应该有 `camtalk_config` - 内容应包含 `"scenario": "interviewer"` 4. **清除缓存重试**: ```bash # 浏览器:清除 LocalStorage # 后端:重启服务 # 前端:刷新页面 ``` --- **文档版本**: 1.0 **最后更新**: 2026-06-20 **维护人员**: CamTalk 开发团队