diff --git a/deploy/k3s/config/prompts.yml b/deploy/k3s/config/prompts.yml index c5bbec5..da9bb38 100644 --- a/deploy/k3s/config/prompts.yml +++ b/deploy/k3s/config/prompts.yml @@ -171,6 +171,7 @@ runtime: 输出要求: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回恰好 5 个字符串的 JSON 数组。 + - 输出语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文词、中文数字或中文标题格式。 - 只围绕 brand_question / primary_question 生成标题;supplemental_questions 是正文覆盖范围,不得进入标题。 - 尊重当前优化关键词和竞品上下文,因为用户可能已手动编辑。 - 标题避免使用“排行”“榜”“Top”“靠谱”“首选”“权威”“最好”“背书”“第一”等词,不写绝对化或承诺式表述。 @@ -227,7 +228,7 @@ runtime: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回 JSON 对象,格式固定为 {"outline":[...]} - outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]} - - 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。 + - 每个已选大纲段落应作为顶层节点出现,语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文章节名。 - supplemental_questions 只安排为正文主体的自然覆盖点,不作为文章标题或大纲顶层主线。 - 结尾预留 1 个加粗点题句,用来回应 supplemental_questions 覆盖过的问题。 - 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。 diff --git a/deploy/prompts.yml b/deploy/prompts.yml index 636102d..da9bb38 100644 --- a/deploy/prompts.yml +++ b/deploy/prompts.yml @@ -171,6 +171,7 @@ runtime: 输出要求: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回恰好 5 个字符串的 JSON 数组。 + - 输出语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文词、中文数字或中文标题格式。 - 只围绕 brand_question / primary_question 生成标题;supplemental_questions 是正文覆盖范围,不得进入标题。 - 尊重当前优化关键词和竞品上下文,因为用户可能已手动编辑。 - 标题避免使用“排行”“榜”“Top”“靠谱”“首选”“权威”“最好”“背书”“第一”等词,不写绝对化或承诺式表述。 @@ -227,7 +228,7 @@ runtime: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回 JSON 对象,格式固定为 {"outline":[...]} - outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]} - - 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。 + - 每个已选大纲段落应作为顶层节点出现,语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文章节名。 - supplemental_questions 只安排为正文主体的自然覆盖点,不作为文章标题或大纲顶层主线。 - 结尾预留 1 个加粗点题句,用来回应 supplemental_questions 覆盖过的问题。 - 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。 @@ -284,6 +285,7 @@ runtime: - 不要逐句同义替换,不要复制源文的独特措辞,不要编造源文、知识库和补充要求之外的事实。 - 如果给出了地域、品牌/主体或优化关键词,标题和正文都要自然贴合这些上下文。 - 文章禁止出现任何网址、域名、网页链接或 Markdown 链接;源文章中的链接只作为背景理解,不得保留。 + - 图片不要保留,直接删除图片,不要保留图片的任何信息。 - 全文篇幅控制在 2000 字左右。 %s diff --git a/server/configs/prompts.yml b/server/configs/prompts.yml index c5bbec5..da9bb38 100644 --- a/server/configs/prompts.yml +++ b/server/configs/prompts.yml @@ -171,6 +171,7 @@ runtime: 输出要求: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回恰好 5 个字符串的 JSON 数组。 + - 输出语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文词、中文数字或中文标题格式。 - 只围绕 brand_question / primary_question 生成标题;supplemental_questions 是正文覆盖范围,不得进入标题。 - 尊重当前优化关键词和竞品上下文,因为用户可能已手动编辑。 - 标题避免使用“排行”“榜”“Top”“靠谱”“首选”“权威”“最好”“背书”“第一”等词,不写绝对化或承诺式表述。 @@ -227,7 +228,7 @@ runtime: - 仅返回 JSON,不要用 Markdown 代码块包裹。 - 返回 JSON 对象,格式固定为 {"outline":[...]} - outline 字段是顶层大纲节点数组,每个节点格式为 {"outline":"...","children":[...]} - - 每个已选大纲段落应作为顶层节点出现,语言与请求保持一致。 + - 每个已选大纲段落应作为顶层节点出现,语言必须与 locale 一致:zh-CN 使用简体中文,en-US 使用自然、专业英文;en-US 时不要输出中文章节名。 - supplemental_questions 只安排为正文主体的自然覆盖点,不作为文章标题或大纲顶层主线。 - 结尾预留 1 个加粗点题句,用来回应 supplemental_questions 覆盖过的问题。 - 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。 diff --git a/server/internal/tenant/app/template_assist.go b/server/internal/tenant/app/template_assist.go index 0a0838a..2880e63 100644 --- a/server/internal/tenant/app/template_assist.go +++ b/server/internal/tenant/app/template_assist.go @@ -806,12 +806,20 @@ func buildAnalyzePrompt(templateKey, templateName string, req AnalyzeTaskRequest basePrompt := strings.TrimSpace(renderPromptTemplate(analyzePromptTemplate, contextPayload)) if basePrompt != "" { contextJSON, _ := json.MarshalIndent(contextPayload, "", " ") - return prompts.TemplateContextWithJSONExample(basePrompt, string(contextJSON), prompts.AnalyzeOutputExample()) + sections := []string{prompts.TemplateContextWithJSONExample(basePrompt, string(contextJSON), prompts.AnalyzeOutputExample())} + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "analyze"); localeGuard != "" { + sections = append(sections, localeGuard) + } + return strings.Join(sections, "\n\n") } } contextJSON, _ := json.MarshalIndent(contextPayload, "", " ") - return prompts.AnalyzeFallbackPrompt(string(contextJSON)) + sections := []string{prompts.AnalyzeFallbackPrompt(string(contextJSON))} + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "analyze"); localeGuard != "" { + sections = append(sections, localeGuard) + } + return strings.Join(sections, "\n\n") } func buildAnalyzeAIPointMeteredText(req AnalyzeTaskRequest) string { @@ -829,12 +837,19 @@ func buildTitlePrompt(templateKey, templateName string, req TitleTaskRequest, ti sections = append(sections, prompts.PromptContextSection(contextBlock)) } sections = append(sections, prompts.TitleCustomOutputRequirementsSection()) + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "title"); localeGuard != "" { + sections = append(sections, localeGuard) + } return strings.Join(sections, "\n\n") } } contextJSON, _ := json.MarshalIndent(contextPayload, "", " ") - return prompts.TitleFallbackPrompt(string(contextJSON)) + sections := []string{prompts.TitleFallbackPrompt(string(contextJSON))} + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "title"); localeGuard != "" { + sections = append(sections, localeGuard) + } + return strings.Join(sections, "\n\n") } func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest, outlinePromptTemplate *string) string { @@ -850,12 +865,51 @@ func buildOutlinePrompt(templateKey, templateName string, req OutlineTaskRequest } sections = append(sections, prompts.OutlineCustomOutputRequirementsSection()) sections = append(sections, prompts.JSONOutputExampleSection(outputExample)) + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "outline"); localeGuard != "" { + sections = append(sections, localeGuard) + } return strings.Join(sections, "\n\n") } } contextJSON, _ := json.MarshalIndent(contextPayload, "", " ") - return prompts.OutlineFallbackPrompt(string(contextJSON)) + sections := []string{prompts.OutlineFallbackPrompt(string(contextJSON))} + if localeGuard := buildAssistLocaleOutputGuard(req.Locale, "outline"); localeGuard != "" { + sections = append(sections, localeGuard) + } + return strings.Join(sections, "\n\n") +} + +func buildAssistLocaleOutputGuard(locale, artifact string) string { + if strings.TrimSpace(locale) != "en-US" { + return "" + } + + switch artifact { + case "analyze": + return strings.Join([]string{ + "High-priority language consistency requirement:", + "- Return English JSON field values for brand_summary, keywords, competitor descriptions, and any generated explanatory text.", + "- Competitor, brand, company, and product names may remain in their official language, but do not output Chinese prose or untranslated Chinese template wording.", + "- Treat Chinese template wording and examples as internal instructions only.", + }, "\n") + case "title": + return strings.Join([]string{ + "High-priority language consistency requirement:", + "- Return English JSON strings only.", + "- The titles must be natural, professional English titles; do not output Chinese words, Chinese numerals, or Chinese title patterns.", + "- Treat Chinese template wording and examples as internal instructions only.", + }, "\n") + case "outline": + return strings.Join([]string{ + "High-priority language consistency requirement:", + "- Every outline and child outline value must be natural, professional English.", + "- Do not output Chinese section names, Chinese prose, Chinese punctuation patterns, or untranslated Chinese template wording.", + "- If any selected section, keyword, or context value is Chinese, translate or adapt the generated outline wording into English while preserving intent.", + }, "\n") + default: + return "" + } } func buildTitleAIPointMeteredText(req TitleTaskRequest) string { diff --git a/server/internal/tenant/app/template_assist_test.go b/server/internal/tenant/app/template_assist_test.go index b27f185..e3a800a 100644 --- a/server/internal/tenant/app/template_assist_test.go +++ b/server/internal/tenant/app/template_assist_test.go @@ -64,6 +64,64 @@ func TestBuildOutlinePromptForConfiguredTemplateForcesOutlineObject(t *testing.T } } +func TestBuildAnalyzePromptEnglishLocaleAddsHardLanguageGuard(t *testing.T) { + template := "你正在做品牌分析。\n语言: {{locale}}\n品牌名: {{brand_name}}" + + prompt := buildAnalyzePrompt("top_x_article", "Top X 推荐文章", AnalyzeTaskRequest{ + Locale: "en-US", + BrandName: "Rankly", + }, &template) + + for _, expected := range []string{ + "High-priority language consistency requirement:", + "Return English JSON field values for brand_summary, keywords, competitor descriptions", + "Competitor, brand, company, and product names may remain in their official language", + } { + if !strings.Contains(prompt, expected) { + t.Fatalf("buildAnalyzePrompt() = %q, want English language guard %q", prompt, expected) + } + } +} + +func TestBuildTitlePromptEnglishLocaleAddsHardLanguageGuard(t *testing.T) { + template := "你正在为文章生成标题。\n语言: {{locale}}\n优化关键词: {{primary_keyword}}" + + prompt := buildTitlePrompt("top_x_article", "Top X 推荐文章", TitleTaskRequest{ + Locale: "en-US", + BrandQuestion: "whole-home customization", + }, &template) + + for _, expected := range []string{ + "High-priority language consistency requirement:", + "Return English JSON strings only.", + "The titles must be natural, professional English titles", + } { + if !strings.Contains(prompt, expected) { + t.Fatalf("buildTitlePrompt() = %q, want English language guard %q", prompt, expected) + } + } +} + +func TestBuildOutlinePromptEnglishLocaleAddsHardLanguageGuard(t *testing.T) { + template := "你正在为文章生成大纲。\n语言: {{locale}}\n标题: {{title}}\n已选段落: {{outline_sections}}" + + prompt := buildOutlinePrompt("top_x_article", "Top X 推荐文章", OutlineTaskRequest{ + Locale: "en-US", + Title: "How to Choose Whole-home Customization", + OutlineSections: []string{"Introduction", "Key Points"}, + }, &template) + + for _, expected := range []string{ + "High-priority language consistency requirement:", + "Every outline and child outline value must be natural, professional English.", + "Do not output Chinese section names", + } { + if !strings.Contains(prompt, expected) { + t.Fatalf("buildOutlinePrompt() = %q, want English language guard %q", prompt, expected) + } + } +} + func TestNormalizeAnalyzeResultKeepsTenQueryKeywords(t *testing.T) { input := AnalyzeTaskResult{ BrandSummary: " 全屋定制服务摘要 ", diff --git a/server/internal/tenant/app/template_prompt.go b/server/internal/tenant/app/template_prompt.go index ae2412e..d99957f 100644 --- a/server/internal/tenant/app/template_prompt.go +++ b/server/internal/tenant/app/template_prompt.go @@ -45,9 +45,27 @@ func buildGenerationPrompt( sections = append(sections, prompts.GenerationLengthGuidanceHeading()+"\n"+lengthGuidance) } + if localeGuard := buildGenerationLocaleGuard(params); localeGuard != "" { + sections = append(sections, localeGuard) + } + return strings.Join(sections, "\n\n") } +func buildGenerationLocaleGuard(params map[string]interface{}) string { + if strings.TrimSpace(stringValue(params["locale"])) != "en-US" { + return "" + } + + return strings.Join([]string{ + "High-priority language consistency requirement:", + "- The final article output must be entirely in natural, professional English.", + "- Treat any Chinese template text, Chinese examples, Chinese section names, or Chinese disclaimers as internal instructions only; translate or adapt them into English before writing.", + "- Do not output Chinese prose, Chinese headings, Chinese punctuation patterns, or Chinese-only required sentences unless they are official brand, company, product, or legal names that must remain unchanged.", + "- If title, article_outline, outline_sections, key_points, or knowledge snippets contain Chinese, rewrite the generated article content in English while preserving the intended meaning.", + }, "\n") +} + func buildGenerationKnowledgeQuery(templateKey, templateName string, params map[string]interface{}) string { if len(params) == 0 { return "" diff --git a/server/internal/tenant/app/template_prompt_test.go b/server/internal/tenant/app/template_prompt_test.go index 47bfec2..e9ae2cb 100644 --- a/server/internal/tenant/app/template_prompt_test.go +++ b/server/internal/tenant/app/template_prompt_test.go @@ -107,6 +107,24 @@ func TestBuildGenerationPromptTopXRepeatsOutlineExecutionRulesInBasePrompt(t *te } } +func TestBuildGenerationPromptEnglishLocaleAddsHardLanguageGuard(t *testing.T) { + promptTemplate := "你是一名内容编辑,请围绕 {{title}} 写文章。" + prompt := buildGenerationPrompt("top_x_article", "Top X 推荐文章", &promptTemplate, map[string]interface{}{ + "title": "A Guide to Whole-home Customization", + "locale": "en-US", + }, "") + + for _, expected := range []string{ + "High-priority language consistency requirement:", + "The final article output must be entirely in natural, professional English.", + "Treat any Chinese template text, Chinese examples, Chinese section names, or Chinese disclaimers as internal instructions only", + } { + if !strings.Contains(prompt, expected) { + t.Fatalf("buildGenerationPrompt() = %q, want English language guard %q", prompt, expected) + } + } +} + func TestBuildGenerationPromptDoesNotAddTopXBrandRulesForOtherTemplates(t *testing.T) { prompt := buildGenerationPrompt("product_review", "产品评测", nil, map[string]interface{}{ "product_name": "测试产品",