feat(tenant): enforce English output for en-US template generation
When the article locale is en-US, Chinese prompt templates could leak Chinese wording into generated titles, outlines, and article bodies. Add high-priority language-consistency guards so the model treats the Chinese template text as internal instructions and emits English only. - Append per-artifact locale guards (analyze/title/outline) in template assist prompts and an article-body guard in the generation prompt, only when locale is en-US - Reinforce locale consistency in the title/outline runtime prompts and drop retained images from the rewrite prompt - Cover the new guards with unit tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 覆盖过的问题。
|
||||
- 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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 覆盖过的问题。
|
||||
- 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: " 全屋定制服务摘要 ",
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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": "测试产品",
|
||||
|
||||
Reference in New Issue
Block a user