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
142 lines
5.5 KiB
Go
142 lines
5.5 KiB
Go
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
|
||
}
|