349 lines
9.7 KiB
Markdown
349 lines
9.7 KiB
Markdown
# 情景切换功能
|
||
|
||
## 功能概述
|
||
|
||
情景切换功能允许用户选择不同的对话场景,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 消息
|
||
// 追加到历史记录
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. State 初始化
|
||
|
||
**文件**: `backend/internal/eino/adapter.go`
|
||
|
||
从 `PipelineInput` 复制元数据到 `PipelineState`,确保情景配置正确传递到所有节点:
|
||
|
||
```go
|
||
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 消息发送
|
||
|
||
**文件**: `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, // 情景配置
|
||
},
|
||
});
|
||
```
|
||
|
||
#### 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 会实时翻译你的话(中英互译),无解释评论",
|
||
```
|
||
|
||
---
|
||
|
||
## 数据流
|
||
|
||
### 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 按情景角色生成回复
|
||
```
|
||
|
||
---
|
||
|
||
## 使用指南
|
||
|
||
### 快速验证
|
||
|
||
1. **打开浏览器** → http://localhost:5173
|
||
2. **登录系统**
|
||
3. **切换情景** → 右侧配置面板 → 对话情景 → 模拟面试官
|
||
4. **观察现象**:
|
||
- ✨ AI 立即说:"你好!我是今天的面试官。让我们先从自我介绍开始..."
|
||
- ✨ 对话框顶部显示蓝色提示卡片
|
||
5. **验证效果** → 发送:"你是谁?"
|
||
- ✅ **正确回复**:"我是今天的面试官..."
|
||
- ❌ **错误回复**:"我是通义千问..."
|
||
|
||
### 功能测试清单
|
||
|
||
| 测试项 | 操作步骤 | 预期结果 |
|
||
|--------|---------|---------|
|
||
| **首句引导** | 切换到"模拟面试官" | AI 自动说:"你好!我是今天的面试官..." |
|
||
| **情景生效** | 问 "你是谁?" | AI 回答:"我是今天的面试官..." |
|
||
| **提示卡片** | 发送一条消息后查看顶部 | 显示蓝色卡片:"🎯 模拟面试官 \| AI 会扮演面试官..." |
|
||
| **语言联动** | 切换到"英语老师" | 语言自动切换到 en-US,AI 用英语回复 |
|
||
| **持久化** | 切换情景后刷新页面 | 情景配置保持,首句仍在历史中 |
|
||
| **多情景** | 依次测试所有情景 | 每个情景 AI 回复风格明显不同 |
|
||
|
||
---
|
||
|
||
## 故障排查
|
||
|
||
### 如果情景不生效
|
||
|
||
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
|
||
# 后端:重启服务
|
||
# 前端:刷新页面
|
||
```
|
||
|
||
---
|
||
|
||
## 后续优化建议
|
||
|
||
### 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` — 日文翻译
|