Files
geo/server/internal/tenant/app/knowledge_query_test.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

142 lines
5.5 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 (
"context"
"errors"
"strings"
"testing"
"github.com/geo-platform/tenant-api/internal/shared/llm"
)
func TestBuildGenerationKnowledgeQueryIncludesContextAndFallbackQuestions(t *testing.T) {
query := buildGenerationKnowledgeQuery("top_x_article", "Top X 推荐文章", map[string]interface{}{
"topic": "合肥全屋定制",
"brand_name": "安徽海翔家居",
"region": "合肥",
"primary_keyword": "合肥定制家具",
"supplemental_questions": []string{"合肥家具品牌怎么选", "全屋定制避坑"},
"key_points": "突出门店与服务能力",
})
for _, expected := range []string{
"文章类型:推荐榜文章",
"模板名称:Top X 推荐文章",
"主题:合肥全屋定制",
"品牌:安徽海翔家居",
"地域:合肥",
"主关键词:合肥定制家具",
"兜底检索问题:",
} {
if !strings.Contains(query, expected) {
t.Fatalf("buildGenerationKnowledgeQuery() = %q, want %q", query, expected)
}
}
questions := normalizeKnowledgeResolveQueries(query)
if len(questions) != maxKnowledgeQueryQuestions {
t.Fatalf("normalizeKnowledgeResolveQueries() len = %d, want %d: %#v", len(questions), maxKnowledgeQueryQuestions, questions)
}
for _, question := range questions {
if !strings.HasSuffix(question, "") {
t.Fatalf("question = %q, want Chinese question mark", question)
}
}
}
func TestParseKnowledgeQueryRewriteOutputAcceptsObjectAndArray(t *testing.T) {
objectQuestions := parseKnowledgeQueryRewriteOutput(`{"questions":["安徽海翔家居在合肥有哪些门店和联系方式?","安徽海翔家居主营哪些全屋定制产品和服务?","合肥全屋定制用户选择时关注哪些案例和售后?"]}`)
if len(objectQuestions) != maxKnowledgeQueryQuestions {
t.Fatalf("object questions len = %d, want %d: %#v", len(objectQuestions), maxKnowledgeQueryQuestions, objectQuestions)
}
if objectQuestions[0] != "安徽海翔家居在合肥有哪些门店和联系方式?" {
t.Fatalf("first object question = %q", objectQuestions[0])
}
arrayQuestions := parseKnowledgeQueryRewriteOutput(`["安徽海翔家居有什么品牌优势?","合肥全屋定制有哪些案例可引用?","全屋定制选型需要哪些避坑信息?"]`)
if len(arrayQuestions) != maxKnowledgeQueryQuestions {
t.Fatalf("array questions len = %d, want %d: %#v", len(arrayQuestions), maxKnowledgeQueryQuestions, arrayQuestions)
}
}
func TestNormalizeKnowledgeResolveQueriesPrefersFallbackQuestionBlock(t *testing.T) {
query := strings.Join([]string{
"文章类型:推荐榜文章",
"主题:合肥全屋定制",
"品牌:安徽海翔家居",
"重点要求:",
"请突出本地服务",
"不要写成硬广",
"兜底检索问题:",
"- 安徽海翔家居在合肥有哪些门店和联系方式?",
"- 安徽海翔家居主营哪些全屋定制产品和服务?",
"- 合肥全屋定制用户选择时关注哪些案例和售后?",
}, "\n")
got := normalizeKnowledgeResolveQueries(query)
want := []string{
"安徽海翔家居在合肥有哪些门店和联系方式?",
"安徽海翔家居主营哪些全屋定制产品和服务?",
"合肥全屋定制用户选择时关注哪些案例和售后?",
}
if len(got) != len(want) {
t.Fatalf("normalizeKnowledgeResolveQueries() = %#v, want %#v", got, want)
}
for index := range want {
if got[index] != want[index] {
t.Fatalf("normalizeKnowledgeResolveQueries()[%d] = %q, want %q", index, got[index], want[index])
}
}
}
func TestRewriteKnowledgeResolveQueriesUsesKnowledgeURLModelAndJSONObject(t *testing.T) {
client := &fakeKnowledgeQueryLLM{
content: `{"questions":["安徽海翔家居在合肥有哪些门店和联系方式?","安徽海翔家居主营哪些全屋定制产品和服务?","合肥全屋定制用户选择时关注哪些案例和售后?"]}`,
}
svc := &KnowledgeService{
llmClient: client,
cfg: knowledgeRuntimeConfig{URLMarkdownModel: "knowledge-url-fast"},
}
questions, err := svc.rewriteKnowledgeResolveQueries(context.Background(), "品牌:安徽海翔家居\n地域:合肥")
if err != nil {
t.Fatalf("rewriteKnowledgeResolveQueries() error = %v", err)
}
if len(questions) != maxKnowledgeQueryQuestions {
t.Fatalf("questions len = %d, want %d: %#v", len(questions), maxKnowledgeQueryQuestions, questions)
}
if client.request.Model != "knowledge-url-fast" {
t.Fatalf("Generate model = %q, want knowledge-url-fast", client.request.Model)
}
if client.request.ResponseFormat == nil || client.request.ResponseFormat.Type != llm.ResponseFormatTypeJSONObject {
t.Fatalf("ResponseFormat = %#v, want json_object", client.request.ResponseFormat)
}
if !strings.Contains(client.request.Prompt, "品牌:安徽海翔家居") || !strings.Contains(client.request.Prompt, "必须输出 3 个问题") {
t.Fatalf("Prompt = %q, want raw user input and three-question instruction", client.request.Prompt)
}
}
type fakeKnowledgeQueryLLM struct {
request llm.GenerateRequest
content string
err error
}
func (f *fakeKnowledgeQueryLLM) Validate() error {
if f.err != nil {
return f.err
}
return nil
}
func (f *fakeKnowledgeQueryLLM) Generate(_ context.Context, req llm.GenerateRequest, _ func(string)) (*llm.GenerateResult, error) {
f.request = req
if f.err != nil {
return nil, f.err
}
if strings.TrimSpace(f.content) == "" {
return nil, errors.New("empty fake content")
}
return &llm.GenerateResult{Content: f.content, Model: req.Model}, nil
}