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:
2026-07-09 19:38:55 +08:00
parent f69edc2218
commit 2c394d436a
7 changed files with 159 additions and 7 deletions
+2 -1
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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 覆盖过的问题。
- 尊重当前优化关键词、竞品和关键要点,因为用户可能已手动编辑。
+58 -4
View File
@@ -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": "测试产品",