feat:添加对话情景功能

This commit is contained in:
2026-06-14 20:32:30 +08:00
parent c3118dd72a
commit b60e513317
20 changed files with 438 additions and 59 deletions

View File

@@ -15,10 +15,11 @@ type Service interface {
// Request 推理请求。
type Request struct {
Image []byte // JPEG 图片(已从 Base64 解码)
Text string // 用户语音识别后的文本
History []models.Message // 最近 N 轮对话历史
Language string // 语言,如 "zh-CN"
Image []byte // JPEG 图片(已从 Base64 解码)
Text string // 用户语音识别后的文本
History []models.Message // 最近 N 轮对话历史
Language string // 语言,如 "zh-CN"
SystemPrompt string // 情景自定义 system prompt非空时覆盖默认 prompt
}
// Chunk 流式推理的一个增量片段。

View File

@@ -209,10 +209,10 @@ func (o *OpenAIService) parseSSEStream(body io.Reader, ch chan<- Chunk) {
func (o *OpenAIService) buildMessages(req Request) []chatMessage {
var messages []chatMessage
// System prompt
// System prompt(情景覆盖优先)
messages = append(messages, chatMessage{
Role: "system",
Content: []contentPart{{Type: "text", Text: BuildSystemPrompt(req.Language, "")}},
Content: []contentPart{{Type: "text", Text: BuildSystemPrompt(req.Language, "", req.SystemPrompt)}},
})
// 历史消息

View File

@@ -242,9 +242,9 @@ func TestBuildSystemPrompt(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := BuildSystemPrompt(tt.language, tt.detailLevel)
got := BuildSystemPrompt(tt.language, tt.detailLevel, "")
if !strings.Contains(got, tt.wantContain) {
t.Errorf("BuildSystemPrompt(%q, %q) should contain %q", tt.language, tt.detailLevel, tt.wantContain)
t.Errorf("BuildSystemPrompt(%q, %q, \"\") should contain %q", tt.language, tt.detailLevel, tt.wantContain)
}
})
}

View File

@@ -2,10 +2,26 @@ package llm
import "strings"
// BuildSystemPrompt 根据语言细节级别构建系统提示词。
func BuildSystemPrompt(language, detailLevel string) string {
// BuildSystemPrompt 根据语言细节级别和情景 prompt 构建系统提示词。
// scenarioPrompt 非空时,覆盖默认视觉助手 prompt。
func BuildSystemPrompt(language, detailLevel, scenarioPrompt string) string {
isChinese := strings.HasPrefix(language, "zh")
// 情景模式:使用自定义 prompt 作为基础
if scenarioPrompt != "" {
var prompt strings.Builder
prompt.WriteString(scenarioPrompt)
if detailLevel == "high" {
if isChinese {
prompt.WriteString(" 请在涉及视觉内容时提供更详细的描述,包括颜色、位置、数量等细节。")
} else {
prompt.WriteString(" When describing visual content, provide detailed descriptions including colors, positions, quantities, and other details.")
}
}
return prompt.String()
}
// 默认模式:视觉助手
var prompt strings.Builder
if isChinese {
prompt.WriteString("你是一个视觉助手。用户通过摄像头看到一个场景,并用语音向你提问。请用简洁自然的中文回答。如果涉及视觉描述,先说\"我看到……\"。回答控制在3-5句话以内除非用户要求详细说明。")

View File

@@ -0,0 +1,55 @@
package llm
import "strings"
// scenarioPrompt 定义单个情景的多语言 system prompt。
type scenarioPrompt struct {
ZH string
EN string
JA string
}
// scenarioPrompts 预置情景 → prompt 映射表。
// key 为情景 ID与前端 Scenario.id 对齐)。
var scenarioPrompts = map[string]scenarioPrompt{
"interviewer": {
ZH: "你是一位资深面试官。你通过摄像头观察面试者并根据他们的背景和表现提出面试问题。规则1) 每次只问一个问题等用户回答后再追问2) 问题要有层次从自我介绍到专业问题再到情景题3) 对用户的回答给出简短点评然后追问4) 如果摄像头能看到用户的环境可以结合环境提出相关话题5) 回答控制在2-4句话。",
EN: "You are a senior interviewer. You observe the interviewee through their camera and ask interview questions based on their background and performance. Rules: 1) Ask one question at a time, wait for the answer before following up; 2) Questions should progress from self-introduction to professional questions to situational questions; 3) Give brief feedback on answers then follow up; 4) If the camera shows the user's environment, incorporate it into the conversation; 5) Keep responses to 2-4 sentences.",
JA: "あなたはベテラン面接官です。カメラで面接者を見て、バックグラウンドと実績に基づいて面接質問をします。ルール1) 一度に一つの質問だけし、回答を待ってから追及する2) 質問は自己紹介から専門質問、シチュエーション質問へと段階的に3) 回答に短いコメントをしてから次の質問へ4) 回答は2〜4文以内。",
},
"english_teacher": {
ZH: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material (e.g., 'I can see a bookshelf behind you. What's your favorite book?'); 6) Keep responses to 3-5 sentences.",
EN: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material; 6) Keep responses to 3-5 sentences.",
JA: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material; 6) Keep responses to 3-5 sentences.",
},
"debate": {
ZH: "你是一位辩论赛对手。用户提出一个观点你需要站在反方进行反驳。规则1) 逻辑严密用事实和论据反驳不要人身攻击2) 每次提出1-2个核心反驳点并给出简要论据3) 如果用户论证有力承认其合理性但仍要寻找突破口4) 适时提出反问引导用户深入思考5) 回答控制在3-5句话。",
EN: "You are a debate opponent. The user presents a viewpoint, and you argue against it. Rules: 1) Use logic and evidence, no personal attacks; 2) Present 1-2 core counterarguments with brief evidence; 3) Acknowledge strong points but look for weaknesses; 4) Ask counter-questions to provoke deeper thinking; 5) Keep responses to 3-5 sentences.",
JA: "あなたはディベートの相手です。ユーザーが提示した观点に対して反論します。ルール1) 論理と証拠で反論し、人格攻撃はしない2) 1〜2つの核心的な反論を提示する3) 相手の有力な論点は認めつつも突破口を探す4) 深い思考を促す反问をする5) 回答は3〜5文以内。",
},
"interpreter": {
ZH: "你是一名同声翻译员。将用户说的话实时翻译为目标语言。规则1) 只输出翻译结果不加任何解释或评论2) 保持口语化自然流畅3) 如果用户说中文翻译成英文如果用户说英文翻译成中文4) 如果不确定目标语言默认中英互译5) 对于专有名词,首次翻译时在括号中注明原文。",
EN: "You are a simultaneous interpreter. Translate what the user says in real-time. Rules: 1) Only output the translation, no explanations or comments; 2) Keep it conversational and natural; 3) If the user speaks Chinese, translate to English; if English, translate to Chinese; 4) Default to Chinese-English translation if the target language is unclear; 5) For proper nouns, note the original in parentheses on first use.",
JA: "あなたは同時通訳者です。ユーザーの発言をリアルタイムで翻訳します。ルール1) 翻訳結果のみ出力し、説明やコメントは加えない2) 口語的で自然な表現を維持する3) ユーザーが中国語を話せば英語に、英語を話せば中国語に翻訳する4) 固有名詞は初出時に原文を括弧で注記する。",
},
}
// GetScenarioPrompt 根据情景 ID 和语言获取对应的 system prompt。
// 返回空字符串表示无此情景(使用默认 prompt
func GetScenarioPrompt(scenarioID, language string) string {
if scenarioID == "" || scenarioID == "free_chat" {
return ""
}
p, ok := scenarioPrompts[scenarioID]
if !ok {
return ""
}
switch {
case strings.HasPrefix(language, "zh"):
return p.ZH
case strings.HasPrefix(language, "ja"):
return p.JA
default:
return p.EN
}
}

View File

@@ -17,6 +17,7 @@ type SessionConfig struct {
TTSEnabled bool `json:"tts_enabled"`
DetailLevel string `json:"detail_level"` // "low" | "high"
Language string `json:"language"`
Scenario string `json:"scenario"` // 情景 ID如 "free_chat"、"interviewer"
}
// DefaultSessionTitle 默认会话标题。
@@ -24,7 +25,7 @@ const DefaultSessionTitle = "新对话"
// DefaultConfig 默认会话配置。
func DefaultConfig() SessionConfig {
return SessionConfig{TTSEnabled: true, DetailLevel: "low", Language: "zh-CN"}
return SessionConfig{TTSEnabled: true, DetailLevel: "low", Language: "zh-CN", Scenario: "free_chat"}
}
// SessionConfigPatch 会话配置增量更新(指针字段表示"未传则不更新")。
@@ -32,6 +33,7 @@ type SessionConfigPatch struct {
TTSEnabled *bool `json:"tts_enabled,omitempty"`
DetailLevel *string `json:"detail_level,omitempty"`
Language *string `json:"language,omitempty"`
Scenario *string `json:"scenario,omitempty"`
}
// Apply 将 patch 中的非 nil 字段覆盖到 cfg。
@@ -45,6 +47,9 @@ func (p SessionConfigPatch) Apply(cfg *SessionConfig) {
if p.Language != nil {
cfg.Language = *p.Language
}
if p.Scenario != nil {
cfg.Scenario = *p.Scenario
}
}
// User 用户。
@@ -81,6 +86,7 @@ type WsConfig struct {
TTSEnabled *bool `json:"tts_enabled,omitempty"`
DetailLevel *string `json:"detail_level,omitempty"`
Language *string `json:"language,omitempty"`
Scenario *string `json:"scenario,omitempty"`
} `json:"payload"`
}

View File

@@ -193,12 +193,13 @@ func (p *Pipeline) ProcessQuery(
})
// Step 2+3: LLM 流式推理 + TTS 并行合成
log.Infow("开始 LLM 推理", "request_id", req.RequestID)
log.Infow("开始 LLM 推理", "request_id", req.RequestID, "scenario", sess.Config.Scenario)
llmReq := llm.Request{
Image: image,
Text: userText,
History: history,
Language: sess.Config.Language,
Image: image,
Text: userText,
History: history,
Language: sess.Config.Language,
SystemPrompt: llm.GetScenarioPrompt(sess.Config.Scenario, sess.Config.Language),
}
llmStream, err := p.llmService.ChatStream(ctx, llmReq)

View File

@@ -276,6 +276,7 @@ func serveWS(c *gin.Context, sessionMgr session.Manager, orch orchestrator.Orche
TTSEnabled: msg.Payload.TTSEnabled,
DetailLevel: msg.Payload.DetailLevel,
Language: msg.Payload.Language,
Scenario: msg.Payload.Scenario,
}
if err := client.sessionMgr.UpdateConfig(context.Background(), sessionID, patch); err != nil {
errors.SendWSError(client, errors.CodeInternalError, "", err)