Files
geo/server/internal/tenant/app/knowledge_query.go
T
root ba2f117265 feat(knowledge): multi-query retrieval with LLM query rewrite
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
2026-06-02 14:50:12 +08:00

294 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}