Files
CamTalk/docs/情景切换功能完整文档.md
2026-06-20 23:24:32 +08:00

18 KiB
Raw Blame History

情景切换功能实现与修复完整文档

项目: CamTalk 多模态实时 AI 视觉对话助手
功能: 情景切换(模拟面试官、英语老师、辩论对手、同声翻译)
日期: 2026-06-20
状态: 已完成并修复


目录

  1. 功能概述
  2. 实施内容
  3. 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(面试官):

"interviewer": {
    ZH: `你是一位资深面试官。你通过摄像头观察面试者...

【角色定位】
- 你是面试官,不是助手或顾问
- 你的目标是评估候选人的能力
- 保持专业、客观、礼貌

【交互规则】
1. 每次只问一个问题,等用户回答后再追问
2. 问题要有层次:自我介绍 → 专业问题 → 情景题
3. 对用户的回答给出简短点评,然后追问
...`,
    GreetingZH: "你好!我是今天的面试官。让我们先从自我介绍开始...",
}

2. 首句引导推送

文件: backend/internal/ws/handler.go

变更内容 在处理 config 消息时,如果切换到非自由对话情景,自动返回首句引导:

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 = ""

修复

// ✅ 修复:从 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

变更内容 在对话列表顶部(非空状态 + 非自由对话模式)添加情景提示卡片:

{messages.length > 0 && !isFreeChat && (
  <div className="chat-panel__scenario-hint">
    <div className="scenario-hint-card">
      <span className="scenario-hint-card__icon">
        {scenarios.find(s => s.id === activeScenario)?.icon}
      </span>
      <div className="scenario-hint-card__text">
        <strong>{t(scenarios.find(s => s.id === activeScenario)?.nameKey || "")}</strong>
        <p>{t(`scenario.${activeScenario}.hint`)}</p>
      </div>
    </div>
  </div>
)}

显示效果

  • 蓝色渐变背景135deg 从蓝到紫)
  • 左侧大图标 + 右侧标题和说明
  • 最大宽度 520px响应式布局
  • 柔和阴影和半透明边框

2. WebSocket 消息修复(关键 Bug 修复)

文件: frontend/src/hooks/useVisionSession.ts

问题:发送 config 消息时缺少 scenario 字段,导致后端无法接收到情景切换信息

修复位置 1(连接成功时发送初始配置):

// ✅ 修复:添加 scenario 字段
send({
  type: "config",
  payload: {
    tts_enabled: config.ttsEnabled,
    detail_level: config.detailLevel,
    language: config.language,
    scenario: config.scenario,  // ⬅️ 关键修复
  },
});

修复位置 2updateConfig 函数):

// ✅ 修复:添加 scenario 字段
send({
  type: "config",
  payload: {
    tts_enabled: next.ttsEnabled,
    detail_level: next.detailLevel,
    language: next.language,
    scenario: next.scenario,  // ⬅️ 关键修复
  },
});

3. 样式实现

文件: frontend/src/App.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

"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

 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

 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 字段:

 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 生成回复:"我是今天的面试官..."
  ↓
✅ 情景生效!

测试验证

编译验证

后端

cd backend && go build -o /tmp/camtalk_fix ./cmd/server
# 产物48MB无编译错误

前端

cd frontend && npm run lint
# ESLint 检查通过(无新增错误)

功能测试清单

测试项 操作步骤 预期结果 验证方法
首句引导 切换到"模拟面试官" AI 自动说:"你好!我是今天的面试官..." 观察聊天框
情景生效 问 "你是谁?" AI 回答:"我是今天的面试官..." 观察回复内容
提示卡片 发送一条消息后查看顶部 显示蓝色卡片:"🎯 模拟面试官 | AI 会扮演面试官..." 观察 UI
语言联动 切换到"英语老师" 语言自动切换到 en-USAI 用英语回复 观察配置和回复
持久化 切换情景后刷新页面 情景配置保持,首句仍在历史中 刷新浏览器
多情景 依次测试所有情景 每个情景 AI 回复风格明显不同 对比回复

日志验证

查看日志

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. 停止旧服务(如果正在运行)

# 查找并停止占用 8080 端口的进程
lsof -ti:8080 | xargs kill -9

2. 启动后端

cd backend
go run ./cmd/server
# 或编译后运行
# go build -o camtalk ./cmd/server && ./camtalk

验证后端启动

curl http://localhost:8080/api/health
# 预期输出:{"status":"ok","version":"dev","uptime_seconds":10,"active_sessions":0}

3. 启动前端(如已运行则刷新浏览器)

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 在节点间共享数据:

// 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 消息):

{
  "type": "config",
  "payload": {
    "tts_enabled": true,
    "detail_level": "low",
    "language": "zh-CN",
    "scenario": "interviewer"
  }
}

服务端 → 客户端(首句引导):

// 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 需要覆盖完整数据流

提交信息

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 <noreply@anthropic.com>"

附录:故障排查

如果情景仍然不生效

  1. 检查后端日志

    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

    {
      "type": "config",
      "payload": {
        "scenario": "interviewer"  // 确认存在
      }
    }
    
  3. 检查会话配置是否保存

    • 切换情景后LocalStorage 中应该有 camtalk_config
    • 内容应包含 "scenario": "interviewer"
  4. 清除缓存重试

    # 浏览器:清除 LocalStorage
    # 后端:重启服务
    # 前端:刷新页面
    

文档版本: 1.0
最后更新: 2026-06-20
维护人员: CamTalk 开发团队