ba2f117265
Add a shared structured query builder (knowledge_query.go) and switch all generation paths to it, so retrieval queries carry task intent (type, brand, region, keywords, key points) instead of a raw prompt. ResolveContext now rewrites the query into multiple sub-questions via the URL-markdown model, embeds and searches each, and merges/dedupes candidates before rerank; falls back to parsed fallback questions when rewrite is unavailable or returns too few. Resolve logs gain query list, count, and rewrite stage/model for observability. - knowledge_query.go/_test.go: BuildKnowledgeQuery + intent normalization - knowledge_service.go: per-query embed/search loop, query rewrite, logs - template_prompt/template_service/prompt_generate/article_imitation/ kol_generation_worker: adopt the shared query builder - generation_observability: richer task error logging
294 lines
11 KiB
Go
294 lines
11 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
const maxKnowledgeQueryQuestions = 3
|
||
|
||
type knowledgeQueryIntent struct {
|
||
TaskType string
|
||
TemplateName string
|
||
Title string
|
||
Topic string
|
||
BrandName string
|
||
Region string
|
||
PrimaryKeyword string
|
||
Keywords []string
|
||
KeyPoints string
|
||
Extra string
|
||
}
|
||
|
||
type KnowledgeQueryInput struct {
|
||
TaskType string
|
||
TemplateName string
|
||
Title string
|
||
Topic string
|
||
BrandName string
|
||
Region string
|
||
PrimaryKeyword string
|
||
Keywords []string
|
||
KeyPoints string
|
||
Extra string
|
||
}
|
||
|
||
func BuildKnowledgeQuery(input KnowledgeQueryInput) string {
|
||
intent := knowledgeQueryIntent{
|
||
TaskType: input.TaskType,
|
||
TemplateName: input.TemplateName,
|
||
Title: input.Title,
|
||
Topic: input.Topic,
|
||
BrandName: input.BrandName,
|
||
Region: input.Region,
|
||
PrimaryKeyword: input.PrimaryKeyword,
|
||
Keywords: input.Keywords,
|
||
KeyPoints: input.KeyPoints,
|
||
Extra: input.Extra,
|
||
}
|
||
return buildKnowledgeQueryInputText(intent)
|
||
}
|
||
|
||
func buildKnowledgeQueryQuestions(intent knowledgeQueryIntent) string {
|
||
questions := buildKnowledgeQueryQuestionList(intent)
|
||
return strings.Join(questions, "\n")
|
||
}
|
||
|
||
func buildKnowledgeQueryInputText(intent knowledgeQueryIntent) string {
|
||
intent = normalizeKnowledgeQueryIntent(intent)
|
||
lines := make([]string, 0, 12)
|
||
appendLine := func(label, value string) {
|
||
value = strings.TrimSpace(value)
|
||
if value != "" {
|
||
lines = append(lines, fmt.Sprintf("%s:%s", label, value))
|
||
}
|
||
}
|
||
appendLine("文章类型", intent.TaskType)
|
||
appendLine("模板名称", intent.TemplateName)
|
||
appendLine("标题", intent.Title)
|
||
appendLine("主题", intent.Topic)
|
||
appendLine("品牌", intent.BrandName)
|
||
appendLine("地域", intent.Region)
|
||
appendLine("主关键词", intent.PrimaryKeyword)
|
||
appendLine("关键词", knowledgeQueryKeywordText(intent.Keywords))
|
||
appendLine("重点要求", intent.KeyPoints)
|
||
appendLine("其他要求", intent.Extra)
|
||
fallback := buildKnowledgeQueryQuestionList(intent)
|
||
if len(fallback) > 0 {
|
||
lines = append(lines, "兜底检索问题:")
|
||
for _, question := range fallback {
|
||
lines = append(lines, "- "+question)
|
||
}
|
||
}
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
func buildKnowledgeQueryRewritePrompt(rawQuery string) string {
|
||
rawQuery = strings.TrimSpace(rawQuery)
|
||
if rawQuery == "" {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(fmt.Sprintf(`你是知识库检索 Query Rewrite 助手。请根据用户的文章生成输入,改写出 3 个最适合去企业/产品知识库中检索的自然语言问题。
|
||
|
||
目标:
|
||
1. 让检索问题覆盖“文章类型/写作任务”真正需要的知识,而不是只重复品牌名或地域词。
|
||
2. 优先围绕用户输入的标题、主题、品牌、地域、关键词、重点要求生成问题。
|
||
3. 问题应适合在知识库中召回:业务范围、产品服务、优势案例、地址联系方式、资质事实、用户痛点、对比维度、避坑点、适用场景。
|
||
4. 如果输入中出现“兜底检索问题”,只能作为参考;你需要根据上方结构化字段重新生成更贴近本次文章的 3 个问题。
|
||
|
||
规则:
|
||
- 必须输出 3 个问题。
|
||
- 每个问题 18-60 个中文字符,尽量具体。
|
||
- 不要生成泛泛的问题,如“这个品牌怎么样”。
|
||
- 不要编造输入中没有的品牌、地域、产品或事实。
|
||
- 只输出 JSON,不要 Markdown,不要解释。
|
||
|
||
用户输入:
|
||
%s`, rawQuery))
|
||
}
|
||
|
||
func parseKnowledgeQueryRewriteOutput(content string) []string {
|
||
content = strings.TrimSpace(content)
|
||
if content == "" {
|
||
return []string{}
|
||
}
|
||
content = strings.TrimPrefix(content, "```json")
|
||
content = strings.TrimPrefix(content, "```")
|
||
content = strings.TrimSuffix(content, "```")
|
||
content = strings.TrimSpace(content)
|
||
|
||
var payload struct {
|
||
Questions []string `json:"questions"`
|
||
}
|
||
if err := json.Unmarshal([]byte(content), &payload); err == nil && len(payload.Questions) > 0 {
|
||
return normalizeKnowledgeQueryQuestions(payload.Questions)
|
||
}
|
||
|
||
var list []string
|
||
if err := json.Unmarshal([]byte(content), &list); err == nil && len(list) > 0 {
|
||
return normalizeKnowledgeQueryQuestions(list)
|
||
}
|
||
return []string{}
|
||
}
|
||
|
||
func normalizeKnowledgeQueryQuestions(values []string) []string {
|
||
questions := make([]string, 0, maxKnowledgeQueryQuestions)
|
||
for _, value := range values {
|
||
appendKnowledgeQuestion(&questions, value)
|
||
if len(questions) >= maxKnowledgeQueryQuestions {
|
||
break
|
||
}
|
||
}
|
||
return questions
|
||
}
|
||
|
||
func buildKnowledgeQueryQuestionList(intent knowledgeQueryIntent) []string {
|
||
intent = normalizeKnowledgeQueryIntent(intent)
|
||
questions := make([]string, 0, maxKnowledgeQueryQuestions)
|
||
|
||
subject := firstNonEmptyText(intent.Topic, intent.Title, intent.PrimaryKeyword, intent.BrandName, "当前主题")
|
||
brandScope := firstNonEmptyText(joinKnowledgeQueryParts(",", intent.BrandName, intent.Region), intent.BrandName, subject)
|
||
keywordScope := knowledgeQueryKeywordText(intent.Keywords)
|
||
|
||
switch {
|
||
case strings.Contains(intent.TaskType, "推荐榜") || strings.Contains(strings.ToLower(intent.TaskType), "top"):
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 中有哪些与 %s 相关的品牌、产品、服务能力和选型依据?", subject, brandScope))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 写作时需要引用哪些关于 %s 的准确事实、优势、案例、地址或联系方式?", intent.TaskType, firstNonEmptyText(intent.BrandName, subject)))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 相关关键词 %s 对应的用户关注点、避坑点和对比维度是什么?", subject, firstNonEmptyText(keywordScope, intent.PrimaryKeyword, subject)))
|
||
case strings.Contains(intent.TaskType, "评测") || strings.Contains(strings.ToLower(intent.TaskType), "review"):
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 的产品特性、参数、卖点、适用人群和使用场景是什么?", subject))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 有哪些真实体验、优缺点、案例、价格或售后信息可用于评测?", firstNonEmptyText(intent.BrandName, subject)))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("围绕 %s 和 %s,用户最关心哪些购买决策问题?", subject, firstNonEmptyText(keywordScope, intent.PrimaryKeyword, subject)))
|
||
case strings.Contains(intent.TaskType, "仿写"):
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("仿写 %s 时,%s 有哪些必须保留或补充的品牌事实和业务信息?", subject, firstNonEmptyText(intent.BrandName, subject)))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 相关关键词 %s 对应的产品、服务、案例和用户需求是什么?", subject, firstNonEmptyText(keywordScope, intent.PrimaryKeyword, subject)))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("生成 %s 时需要避免遗漏哪些地址、联系方式、优势、资质或落地案例?", firstNonEmptyText(intent.TaskType, "仿写文章")))
|
||
default:
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 这篇%s需要引用哪些核心事实、产品服务、案例和用户关注点?", subject, firstNonEmptyText(intent.TaskType, "文章")))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("%s 与 %s 相关的准确资料、品牌信息、地址、联系方式和业务范围是什么?", firstNonEmptyText(intent.BrandName, subject), firstNonEmptyText(keywordScope, intent.PrimaryKeyword, subject)))
|
||
appendKnowledgeQuestion(&questions, fmt.Sprintf("围绕 %s,用户最可能关心哪些问题、痛点、对比维度和解决方案?", firstNonEmptyText(keywordScope, subject)))
|
||
}
|
||
|
||
appendKnowledgeQuestion(&questions, intent.KeyPoints)
|
||
appendKnowledgeQuestion(&questions, intent.Extra)
|
||
return questions
|
||
}
|
||
|
||
func normalizeKnowledgeQueryIntent(intent knowledgeQueryIntent) knowledgeQueryIntent {
|
||
intent.TaskType = strings.TrimSpace(intent.TaskType)
|
||
intent.TemplateName = strings.TrimSpace(intent.TemplateName)
|
||
intent.Title = strings.TrimSpace(intent.Title)
|
||
intent.Topic = strings.TrimSpace(intent.Topic)
|
||
intent.BrandName = strings.TrimSpace(intent.BrandName)
|
||
intent.Region = strings.TrimSpace(intent.Region)
|
||
intent.PrimaryKeyword = strings.TrimSpace(intent.PrimaryKeyword)
|
||
intent.KeyPoints = strings.TrimSpace(intent.KeyPoints)
|
||
intent.Extra = strings.TrimSpace(intent.Extra)
|
||
intent.Keywords = normalizeKnowledgeQueryKeywords(intent.Keywords, 6)
|
||
if intent.TaskType == "" {
|
||
intent.TaskType = strings.TrimSpace(intent.TemplateName)
|
||
}
|
||
return intent
|
||
}
|
||
|
||
func appendKnowledgeQuestion(questions *[]string, question string) {
|
||
if questions == nil || len(*questions) >= maxKnowledgeQueryQuestions {
|
||
return
|
||
}
|
||
question = normalizeKnowledgeQueryQuestion(question)
|
||
if question == "" {
|
||
return
|
||
}
|
||
for _, existing := range *questions {
|
||
if normalizeKnowledgeQueryQuestion(existing) == question {
|
||
return
|
||
}
|
||
}
|
||
*questions = append(*questions, question)
|
||
}
|
||
|
||
func normalizeKnowledgeQueryQuestion(question string) string {
|
||
question = strings.TrimSpace(question)
|
||
if question == "" {
|
||
return ""
|
||
}
|
||
question = strings.Join(strings.Fields(question), " ")
|
||
for _, bad := range []string{" ,", ", ", " ?", " ?"} {
|
||
question = strings.ReplaceAll(question, bad, strings.TrimSpace(bad))
|
||
}
|
||
question = strings.Trim(question, " ,,;;")
|
||
if question == "" {
|
||
return ""
|
||
}
|
||
if !strings.HasSuffix(question, "?") && !strings.HasSuffix(question, "?") {
|
||
question += "?"
|
||
}
|
||
return question
|
||
}
|
||
|
||
func mergeKnowledgeQueryKeywords(values ...interface{}) []string {
|
||
keywords := make([]string, 0)
|
||
for _, value := range values {
|
||
keywords = append(keywords, extractStringList(value, 16)...)
|
||
if text := strings.TrimSpace(formatPromptValue(value)); text != "" && len(extractStringList(value, 1)) == 0 {
|
||
keywords = append(keywords, text)
|
||
}
|
||
}
|
||
return normalizeKnowledgeQueryKeywords(keywords, 6)
|
||
}
|
||
|
||
func normalizeKnowledgeQueryKeywords(keywords []string, limit int) []string {
|
||
result := make([]string, 0, len(keywords))
|
||
seen := make(map[string]struct{}, len(keywords))
|
||
for _, keyword := range keywords {
|
||
keyword = strings.TrimSpace(keyword)
|
||
if keyword == "" {
|
||
continue
|
||
}
|
||
key := strings.ToLower(strings.Join(strings.Fields(keyword), " "))
|
||
if _, ok := seen[key]; ok {
|
||
continue
|
||
}
|
||
seen[key] = struct{}{}
|
||
result = append(result, keyword)
|
||
if limit > 0 && len(result) >= limit {
|
||
break
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func knowledgeQueryKeywordText(keywords []string) string {
|
||
keywords = normalizeKnowledgeQueryKeywords(keywords, 6)
|
||
return strings.Join(keywords, "、")
|
||
}
|
||
|
||
func firstNonEmptyPromptString(params map[string]interface{}, keys ...string) string {
|
||
for _, key := range keys {
|
||
if text := strings.TrimSpace(stringValue(params[key])); text != "" {
|
||
return text
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func firstNonEmptyText(values ...string) string {
|
||
for _, value := range values {
|
||
if text := strings.TrimSpace(value); text != "" {
|
||
return text
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func joinKnowledgeQueryParts(sep string, values ...string) string {
|
||
parts := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
if text := strings.TrimSpace(value); text != "" {
|
||
parts = append(parts, text)
|
||
}
|
||
}
|
||
return strings.Join(parts, sep)
|
||
}
|