c89cad6643
- Rename UI/prompt labels: "品牌主问题" → "优化关键词", "搜索问题" → "搜索词" - Add brand/competitor entity exclusion filter in question expansion (AI distill path) - Refactor combination question text normalization, remove manual question-mark appending - Update prompts to emphasize commercial-intent search queries over editorial phrasing - Sync prompt changes across server/configs, deploy, and k3s configs - Update i18n, frontend views (BrandQuestionCreate, TemplateWizard), and test fixtures Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
265 lines
8.3 KiB
Go
265 lines
8.3 KiB
Go
package prompts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPlatformTemplateSeedsInjectPromptConfig(t *testing.T) {
|
|
SetConfigFile("")
|
|
t.Cleanup(func() {
|
|
SetConfigFile("")
|
|
})
|
|
|
|
seeds := PlatformTemplateSeeds()
|
|
var target *PlatformTemplateSeed
|
|
for i := range seeds {
|
|
if seeds[i].Key == "top_x_article" {
|
|
target = &seeds[i]
|
|
break
|
|
}
|
|
}
|
|
if target == nil {
|
|
t.Fatal("top_x_article seed not found")
|
|
}
|
|
if !strings.Contains(target.PromptTemplate, "第三方行业观察型内容编辑") {
|
|
t.Fatalf("PromptTemplate = %q, want yaml-backed template content", target.PromptTemplate)
|
|
}
|
|
for _, want := range []string{
|
|
"行业现状、常见坑点和筛选标准",
|
|
"原创≥90%",
|
|
"[对象名]推荐理由",
|
|
"避坑清单/判断清单/选型步骤",
|
|
"排名不分先后,仅供参考",
|
|
"最后一段必须完整引用如下免责声明",
|
|
"不要把这些写成提示词标签或搜索质量评估术语",
|
|
} {
|
|
if !strings.Contains(target.PromptTemplate, want) {
|
|
t.Fatalf("PromptTemplate = %q, want master-instruction marker %q", target.PromptTemplate, want)
|
|
}
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal([]byte(target.CardConfigJSON), &payload); err != nil {
|
|
t.Fatalf("unmarshal card config: %v", err)
|
|
}
|
|
|
|
wizard, _ := payload["wizard"].(map[string]interface{})
|
|
if wizard == nil {
|
|
t.Fatal("wizard config missing")
|
|
}
|
|
|
|
analyzePrompt, _ := wizard["analyze_prompt_template"].(string)
|
|
titlePrompt, _ := wizard["title_prompt_template"].(string)
|
|
outlinePrompt, _ := wizard["outline_prompt_template"].(string)
|
|
|
|
if !strings.Contains(analyzePrompt, "推荐类文章做品牌与竞品分析") {
|
|
t.Fatalf("analyze_prompt_template = %q, want yaml-backed analyze prompt", analyzePrompt)
|
|
}
|
|
if !strings.Contains(analyzePrompt, "全屋定制哪家好") || !strings.Contains(analyzePrompt, "搜索词候选") {
|
|
t.Fatalf("analyze_prompt_template = %q, want question-like long-tail query guidance", analyzePrompt)
|
|
}
|
|
if !strings.Contains(titlePrompt, "推荐类文章生成 5 个候选标题") {
|
|
t.Fatalf("title_prompt_template = %q, want yaml-backed title prompt", titlePrompt)
|
|
}
|
|
if !strings.Contains(outlinePrompt, "推荐类文章生成实用大纲") {
|
|
t.Fatalf("outline_prompt_template = %q, want yaml-backed outline prompt", outlinePrompt)
|
|
}
|
|
}
|
|
|
|
func TestPlatformTemplatePromptsUseMasterInstructionStructure(t *testing.T) {
|
|
SetConfigFile("")
|
|
t.Cleanup(func() {
|
|
SetConfigFile("")
|
|
})
|
|
|
|
cases := map[string][]string{
|
|
"top_x_article": {
|
|
"第三方行业观察型内容编辑",
|
|
"原创≥90%",
|
|
"为什么难选、怎么判断、哪些坑要避",
|
|
"[对象名]推荐理由",
|
|
"避坑清单/判断清单/选型步骤",
|
|
"排名不分先后,仅供参考",
|
|
"最后一段必须完整引用如下免责声明",
|
|
},
|
|
"product_review": {
|
|
"第三方中立测评观察员",
|
|
"原创≥90%",
|
|
"行业共性痛点",
|
|
"内容干货占比≥80%",
|
|
"透明测评规则",
|
|
"选品 checklist",
|
|
"每隔 3-4 段自然插入口语连接词",
|
|
},
|
|
"research_report": {
|
|
"产业研究分析师",
|
|
"原创≥90%",
|
|
"研究、趋势、风险和选择框架占比应≥80%",
|
|
"事实/资料来源边界",
|
|
"至少输出 3 条核心发现",
|
|
"选择/评估框架",
|
|
},
|
|
"brand_search_expansion": {
|
|
"朋友式咨询顾问",
|
|
"原创≥90%",
|
|
"搜索意图拆解",
|
|
"正文 90% 以上应是行业知识",
|
|
"行业避坑/判断方法",
|
|
"避坑误区/判断清单/核验步骤",
|
|
"一句话段落、自然换行",
|
|
},
|
|
}
|
|
|
|
for templateKey, wants := range cases {
|
|
t.Run(templateKey, func(t *testing.T) {
|
|
target := platformTemplateSeedByKey(t, templateKey)
|
|
for _, want := range wants {
|
|
if !strings.Contains(target.PromptTemplate, want) {
|
|
t.Fatalf("%s PromptTemplate missing %q:\n%s", templateKey, want, target.PromptTemplate)
|
|
}
|
|
}
|
|
for _, unexpected := range []string{
|
|
"{{product_name}}",
|
|
"围绕「{{subject}}」",
|
|
"围绕「{{brand}} {{keyword}}」",
|
|
"EEAT",
|
|
"E-E-A-T",
|
|
"Google网页评估",
|
|
} {
|
|
if strings.Contains(target.PromptTemplate, unexpected) {
|
|
t.Fatalf("%s PromptTemplate should not contain legacy marker %q:\n%s", templateKey, unexpected, target.PromptTemplate)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func platformTemplateSeedByKey(t *testing.T, key string) PlatformTemplateSeed {
|
|
t.Helper()
|
|
seeds := PlatformTemplateSeeds()
|
|
for _, seed := range seeds {
|
|
if seed.Key == key {
|
|
return seed
|
|
}
|
|
}
|
|
t.Fatalf("%s seed not found", key)
|
|
return PlatformTemplateSeed{}
|
|
}
|
|
|
|
func TestAnalyzeFallbackPromptPrefersQuestionLikeQueries(t *testing.T) {
|
|
SetConfigFile("")
|
|
t.Cleanup(func() {
|
|
SetConfigFile("")
|
|
})
|
|
|
|
prompt := AnalyzeFallbackPrompt(`{"brand_name":"测试品牌","input_params":{"keyword":"全屋定制"}}`)
|
|
for _, want := range []string{
|
|
"真实用户会直接搜索的问题型长尾词",
|
|
"哪家好",
|
|
"最多返回 6 个竞品和 10 个搜索词候选",
|
|
} {
|
|
if !strings.Contains(prompt, want) {
|
|
t.Fatalf("AnalyzeFallbackPrompt() missing %q in prompt:\n%s", want, prompt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuestionExpansionPromptsLoadFromYaml(t *testing.T) {
|
|
SetConfigFile("")
|
|
t.Cleanup(func() {
|
|
SetConfigFile("")
|
|
})
|
|
|
|
distillPrompt := QuestionDistillPrompt("测试品牌", `["竞品A"]`, "全屋定制")
|
|
for _, want := range []string{
|
|
"用户输入的搜索词",
|
|
"目标达成词(公司/品牌,仅用于说明最终内容要指向的对象,不是文章关键词,不得写入候选): 测试品牌",
|
|
`竞品背景(仅用于排除具体对象,不是文章关键词,不得写入候选): ["竞品A"]`,
|
|
"用户搜索词: 全屋定制",
|
|
"目标达成词不是文章关键词",
|
|
"强商业决策意图",
|
|
"服务商推荐、公司推荐、口碑比较",
|
|
"这类题目不好推企业和宣传企业",
|
|
"候选中禁止出现公司名、品牌名、竞品名",
|
|
} {
|
|
if !strings.Contains(distillPrompt, want) {
|
|
t.Fatalf("QuestionDistillPrompt() missing %q in prompt:\n%s", want, distillPrompt)
|
|
}
|
|
}
|
|
|
|
fillPrompt := QuestionCombinationFillPrompt("测试品牌", "https://example.com", "品牌描述", `["竞品A"]`, "合肥、华东", `{"core":["装修"]}`)
|
|
for _, want := range []string{
|
|
"拓词工具",
|
|
"品牌: 测试品牌",
|
|
"官网: https://example.com",
|
|
"合肥、华东",
|
|
`{"core":["装修"]}`,
|
|
} {
|
|
if !strings.Contains(fillPrompt, want) {
|
|
t.Fatalf("QuestionCombinationFillPrompt() missing %q in prompt:\n%s", want, fillPrompt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPromptConfigReloadsAndKeepsLastGoodVersion(t *testing.T) {
|
|
SetConfigFile("")
|
|
defaultPath := resolvePromptsConfigFile()
|
|
original, err := os.ReadFile(defaultPath)
|
|
if err != nil {
|
|
t.Fatalf("read default prompts config: %v", err)
|
|
}
|
|
|
|
tmpPath := filepath.Join(t.TempDir(), "prompts.yml")
|
|
if err := os.WriteFile(tmpPath, original, 0o600); err != nil {
|
|
t.Fatalf("write temp prompts config: %v", err)
|
|
}
|
|
|
|
SetConfigFile(tmpPath)
|
|
t.Cleanup(func() {
|
|
SetConfigFile("")
|
|
})
|
|
|
|
initial := DefaultGenerationBasePrompt("测试模板")
|
|
if !strings.Contains(initial, "专业内容编辑") {
|
|
t.Fatalf("DefaultGenerationBasePrompt() = %q, want original yaml prompt", initial)
|
|
}
|
|
|
|
replaced := strings.Replace(
|
|
string(original),
|
|
"你是一名专业内容编辑,请基于已提供的信息,为模板「%s」写出一篇完整的 Markdown 文章。",
|
|
"请为模板「%s」输出新的实时版本。",
|
|
1,
|
|
)
|
|
if err := os.WriteFile(tmpPath, []byte(replaced), 0o600); err != nil {
|
|
t.Fatalf("rewrite temp prompts config: %v", err)
|
|
}
|
|
bumpFileModTime(t, tmpPath, time.Now().Add(2*time.Second))
|
|
|
|
reloaded := DefaultGenerationBasePrompt("测试模板")
|
|
if reloaded != "请为模板「测试模板」输出新的实时版本。" {
|
|
t.Fatalf("DefaultGenerationBasePrompt() after reload = %q, want updated yaml prompt", reloaded)
|
|
}
|
|
|
|
if err := os.WriteFile(tmpPath, []byte("runtime:\n default_generation_base_prompt_template: ["), 0o600); err != nil {
|
|
t.Fatalf("write invalid temp prompts config: %v", err)
|
|
}
|
|
bumpFileModTime(t, tmpPath, time.Now().Add(4*time.Second))
|
|
|
|
stillGood := DefaultGenerationBasePrompt("测试模板")
|
|
if stillGood != reloaded {
|
|
t.Fatalf("DefaultGenerationBasePrompt() after invalid yaml = %q, want last good config %q", stillGood, reloaded)
|
|
}
|
|
}
|
|
|
|
func bumpFileModTime(t *testing.T, path string, ts time.Time) {
|
|
t.Helper()
|
|
if err := os.Chtimes(path, ts, ts); err != nil {
|
|
t.Fatalf("chtimes %s: %v", path, err)
|
|
}
|
|
}
|