feat: 优化情景切换功能
This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
# 修复文本输入时仍调用摄像头的问题
|
||||
|
||||
## 问题描述
|
||||
|
||||
当用户关闭视频对话,改用文本输入聊天时,AI 仍然会说"画面是黑色的",然后才回答问题。这是因为即使摄像头关闭,前端仍然发送了图像数据(黑屏)给后端。
|
||||
|
||||
## 问题根源
|
||||
|
||||
### 前端问题
|
||||
在 `frontend/src/hooks/useVisionSession.ts` 中:
|
||||
|
||||
1. **sendTextMessage 函数**(第 441 行):无论摄像头是否开启,都会调用 `captureFrame()` 捕获画面
|
||||
2. **待发消息队列处理**(第 140 行):连接成功后 flush 待发消息时,也会无条件捕获画面
|
||||
|
||||
```typescript
|
||||
// 问题代码
|
||||
const frame = captureFrame(); // 即使摄像头关闭也会捕获(返回黑屏)
|
||||
send({
|
||||
type: "query",
|
||||
image: frame ? dataUrlToBase64(frame) : "", // 发送黑屏数据
|
||||
text: text.trim(),
|
||||
});
|
||||
```
|
||||
|
||||
### 后端行为
|
||||
在 `backend/internal/eino/nodes_history.go` 中:
|
||||
|
||||
- 第 68-86 行:只要收到图像数据(`len(imageData) > 0`),就会构建多模态消息发送给 LLM
|
||||
- LLM 会分析这个黑色画面,导致回复中提到"画面是黑色的"
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 修改内容
|
||||
|
||||
修改了两处代码,都在 `frontend/src/hooks/useVisionSession.ts`:
|
||||
|
||||
#### 1. sendTextMessage 函数(第 421-462 行)
|
||||
|
||||
```typescript
|
||||
// 修改前
|
||||
const frame = captureFrame();
|
||||
|
||||
// 修改后
|
||||
// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI
|
||||
const frame = isCameraOn ? captureFrame() : null;
|
||||
```
|
||||
|
||||
并在依赖数组中添加了 `isCameraOn`:
|
||||
```typescript
|
||||
[captureFrame, send, connect, accessToken, isCameraOn]
|
||||
```
|
||||
|
||||
#### 2. 待发消息队列处理(第 124-156 行)
|
||||
|
||||
```typescript
|
||||
// 修改前
|
||||
const frame = captureFrame();
|
||||
|
||||
// 修改后
|
||||
// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI
|
||||
const frame = isCameraOn ? captureFrame() : null;
|
||||
```
|
||||
|
||||
### 效果
|
||||
|
||||
修改后:
|
||||
- ✅ 摄像头**开启**时,文本输入仍然会携带当前画面(多模态对话)
|
||||
- ✅ 摄像头**关闭**时,文本输入只发送文字(纯文本对话)
|
||||
- ✅ AI 不会再说"画面是黑色的"
|
||||
- ✅ 节省 token 消耗(不发送无用的图像数据)
|
||||
|
||||
## 测试场景
|
||||
|
||||
1. **开启视频对话 → 关闭摄像头 → 文本输入聊天**
|
||||
- 预期:AI 只回答文本问题,不提及画面
|
||||
|
||||
2. **直接文本输入(未开启摄像头)**
|
||||
- 预期:纯文本对话,不发送图像数据
|
||||
|
||||
3. **开启摄像头 → 文本输入**
|
||||
- 预期:AI 可以看到画面并结合文本回答
|
||||
|
||||
4. **未连接时文本输入 → 自动连接**
|
||||
- 预期:待发消息在连接成功后发送,根据当时的摄像头状态决定是否携带画面
|
||||
|
||||
## 替代方案(未采用)
|
||||
|
||||
### 方案二:后端过滤空图像
|
||||
在后端 `nodes_history.go` 中增加图像有效性检测:
|
||||
|
||||
```go
|
||||
// 检测图像是否有效(非全黑/全白)
|
||||
func isValidImage(data []byte) bool {
|
||||
// 检测文件大小(黑屏图像通常很小)
|
||||
if len(data) < 1024 { // 小于 1KB
|
||||
return false
|
||||
}
|
||||
// 可以进一步检测像素方差等
|
||||
return true
|
||||
}
|
||||
|
||||
// 在 NewHistoryLambda 中
|
||||
if len(imageData) > 0 && isValidImage(imageData) {
|
||||
// 构建多模态消息
|
||||
} else {
|
||||
// 纯文本消息
|
||||
}
|
||||
```
|
||||
|
||||
**未采用原因**:
|
||||
- 前端方案更直接,从源头解决问题
|
||||
- 避免后端做额外的图像检测(性能开销)
|
||||
- 节省网络传输(不发送无用的图像数据)
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `frontend/src/hooks/useVisionSession.ts` - 核心会话逻辑
|
||||
- `backend/internal/eino/nodes_history.go` - 后端历史消息组装
|
||||
- `docs/02-接口文档.md` - WebSocket 协议定义
|
||||
|
||||
## 提交信息
|
||||
|
||||
```
|
||||
fix: 关闭摄像头后文本输入不再发送图像数据
|
||||
|
||||
修复了用户关闭摄像头后,使用文本输入时 AI 仍会分析黑色画面的问题。
|
||||
|
||||
变更:
|
||||
- sendTextMessage: 只在摄像头开启时捕获画面
|
||||
- 待发消息队列: 根据摄像头状态决定是否携带画面
|
||||
|
||||
效果:
|
||||
- 摄像头关闭时纯文本对话,不提及画面
|
||||
- 节省 token 消耗和网络带宽
|
||||
```
|
||||
664
docs/情景切换功能完整文档.md
Normal file
664
docs/情景切换功能完整文档.md
Normal file
@@ -0,0 +1,664 @@
|
||||
# 情景切换功能实现与修复完整文档
|
||||
|
||||
**项目**: 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 && (
|
||||
<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**(连接成功时发送初始配置):
|
||||
```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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:故障排查
|
||||
|
||||
### 如果情景仍然不生效
|
||||
|
||||
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 开发团队
|
||||
Reference in New Issue
Block a user