Files
geo/server/internal/tenant/app/knowledge_query.go
T

294 lines
11 KiB
Go
Raw Normal View History

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)
}