feat:添加对话情景功能
This commit is contained in:
@@ -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 流式推理的一个增量片段。
|
||||
|
||||
@@ -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)}},
|
||||
})
|
||||
|
||||
// 历史消息
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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句话以内,除非用户要求详细说明。")
|
||||
|
||||
55
backend/internal/ai/llm/scenarios.go
Normal file
55
backend/internal/ai/llm/scenarios.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user