feat(tenant): update retrieval configuration with increased recall limit and rerank top N values
Deployment Config CI / Deployment Config (push) Successful in 1m51s
Backend CI / Backend (push) Successful in 17m3s

This commit is contained in:
2026-06-01 15:49:14 +08:00
parent 76df672133
commit 85bb373a8b
6 changed files with 204 additions and 17 deletions
+2 -2
View File
@@ -237,8 +237,8 @@ retrieval:
chunk_size: 900 chunk_size: 900
chunk_overlap: 120 chunk_overlap: 120
embedding_batch_size: 16 embedding_batch_size: 16
recall_limit: 12 recall_limit: 30
rerank_top_n: 6 rerank_top_n: 8
max_chunks_per_doc: 6 max_chunks_per_doc: 6
overlap_tokens: 60 overlap_tokens: 60
+3 -3
View File
@@ -219,8 +219,8 @@ retrieval:
chunk_size: 900 chunk_size: 900
chunk_overlap: 120 chunk_overlap: 120
embedding_batch_size: 16 embedding_batch_size: 16
recall_limit: 12 recall_limit: 30
rerank_top_n: 6 rerank_top_n: 8
max_chunks_per_doc: 6 max_chunks_per_doc: 6
overlap_tokens: 60 overlap_tokens: 60
@@ -284,4 +284,4 @@ media_supply:
published_max_pages: 3 published_max_pages: 3
search_options_ttl: 12h search_options_ttl: 12h
upstream_lock_ttl: 2m upstream_lock_ttl: 2m
upstream_min_interval: 800ms upstream_min_interval: 800ms
+2 -2
View File
@@ -223,8 +223,8 @@ retrieval:
chunk_size: 900 chunk_size: 900
chunk_overlap: 120 chunk_overlap: 120
embedding_batch_size: 16 embedding_batch_size: 16
recall_limit: 12 recall_limit: 30
rerank_top_n: 6 rerank_top_n: 8
max_chunks_per_doc: 6 max_chunks_per_doc: 6
overlap_tokens: 60 overlap_tokens: 60
@@ -112,7 +112,11 @@ func TestKnowledgeSnippetsFromSearchPointsSkipsMissingTextAndDedupes(t *testing.
{ {
ID: "point-2", ID: "point-2",
Payload: map[string]any{ Payload: map[string]any{
"text": "支持全屋定制方案", "group_id": int64(10),
"group_name": "产品资料",
"item_id": int64(100),
"item_name": "旗舰款",
"text": "支持全屋定制方案",
}, },
}, },
{ {
@@ -129,6 +133,84 @@ func TestKnowledgeSnippetsFromSearchPointsSkipsMissingTextAndDedupes(t *testing.
} }
} }
func TestKnowledgeSearchTextIncludesMetadataForRetrieval(t *testing.T) {
got := buildKnowledgeSearchText(knowledgeSearchTextInput{
GroupName: "产品资料",
ItemName: "旗舰款",
SourceType: "website",
SourceURI: "https://example.com/product",
Text: "支持全屋定制方案",
})
for _, expected := range []string{
"目录: 产品资料",
"标题: 旗舰款",
"来源类型: website",
"来源链接: https://example.com/product",
"内容:\n支持全屋定制方案",
} {
if !strings.Contains(got, expected) {
t.Fatalf("buildKnowledgeSearchText() = %q, want %q", got, expected)
}
}
}
func TestKnowledgeSnippetsFromSearchPointsKeepsSearchTextForRerank(t *testing.T) {
snippets := knowledgeSnippetsFromSearchPoints([]retrieval.SearchPoint{
{
ID: "point-1",
Score: 0.91,
Payload: map[string]any{
"group_id": int64(10),
"group_name": "产品资料",
"item_id": int64(100),
"item_name": "旗舰款",
"source_type": "website",
"source_uri": "https://example.com/product",
"text": "支持全屋定制方案",
"search_text": "目录: 产品资料\n标题: 旗舰款\n内容:\n支持全屋定制方案",
},
},
})
if len(snippets) != 1 {
t.Fatalf("snippets length = %d, want 1", len(snippets))
}
if snippets[0].Text != "支持全屋定制方案" {
t.Fatalf("Text = %q, want original chunk text", snippets[0].Text)
}
if snippets[0].RerankText() != "目录: 产品资料\n标题: 旗舰款\n内容:\n支持全屋定制方案" {
t.Fatalf("RerankText() = %q, want search text", snippets[0].RerankText())
}
if snippets[0].SourceURI == nil || *snippets[0].SourceURI != "https://example.com/product" {
t.Fatalf("SourceURI = %#v, want payload source uri", snippets[0].SourceURI)
}
}
func TestDedupeKnowledgeSearchCandidatesUsesMetadataAwareSearchText(t *testing.T) {
candidates := []KnowledgeSnippet{
{
PointID: "p1",
KnowledgeItemID: 1,
ItemName: "A 款",
Text: "支持全屋定制方案",
SearchText: "标题: A 款\n内容:\n支持全屋定制方案",
},
{
PointID: "p2",
KnowledgeItemID: 2,
ItemName: "B 款",
Text: "支持全屋定制方案",
SearchText: "标题: B 款\n内容:\n支持全屋定制方案",
},
}
deduped := dedupeKnowledgeSearchCandidates(candidates)
if len(deduped) != 2 {
t.Fatalf("deduped length = %d, want 2: %#v", len(deduped), deduped)
}
}
func TestSelectKnowledgeRerankedSnippetsFillsAndDiversifiesResults(t *testing.T) { func TestSelectKnowledgeRerankedSnippetsFillsAndDiversifiesResults(t *testing.T) {
candidates := []KnowledgeSnippet{ candidates := []KnowledgeSnippet{
{PointID: "p1", KnowledgeItemID: 1, Text: "同一文档片段 1", Score: 0.90}, {PointID: "p1", KnowledgeItemID: 1, Text: "同一文档片段 1", Score: 0.90},
@@ -154,6 +236,36 @@ func TestSelectKnowledgeRerankedSnippetsFillsAndDiversifiesResults(t *testing.T)
} }
} }
func TestSelectKnowledgeRerankedSnippetsKeepsOriginalChunkText(t *testing.T) {
candidates := []KnowledgeSnippet{
{
PointID: "p1",
KnowledgeItemID: 1,
Text: "原始正文片段",
SearchText: "目录: 产品资料\n标题: 旗舰款\n内容:\n原始正文片段",
Score: 0.90,
},
}
selected := selectKnowledgeRerankedSnippets(candidates, []retrieval.RerankResult{
{
Index: 0,
RelevanceScore: 0.99,
Text: "目录: 产品资料\n标题: 旗舰款\n内容:\n原始正文片段",
},
}, 1)
if len(selected) != 1 {
t.Fatalf("selected length = %d, want 1", len(selected))
}
if selected[0].Text != "原始正文片段" {
t.Fatalf("selected text = %q, want original chunk text", selected[0].Text)
}
if selected[0].Score != 0.99 {
t.Fatalf("selected score = %v, want rerank score", selected[0].Score)
}
}
func containsExactString(items []string, target string) bool { func containsExactString(items []string, target string) bool {
for _, item := range items { for _, item := range items {
if item == target { if item == target {
@@ -145,6 +145,7 @@ type KnowledgeSnippet struct {
SourceType string SourceType string
SourceURI *string SourceURI *string
Text string Text string
SearchText string
Score float64 Score float64
PreciseFacts []string PreciseFacts []string
} }
@@ -900,7 +901,7 @@ func (s *KnowledgeService) ResolveContext(
documents := make([]string, 0, len(candidates)) documents := make([]string, 0, len(candidates))
for _, candidate := range candidates { for _, candidate := range candidates {
documents = append(documents, candidate.Text) documents = append(documents, candidate.RerankText())
} }
reranked, err := s.provider.Rerank(ctx, query, documents, minInt(runtimeCfg.RerankTopN, len(documents))) reranked, err := s.provider.Rerank(ctx, query, documents, minInt(runtimeCfg.RerankTopN, len(documents)))
@@ -1018,19 +1019,40 @@ func knowledgeSnippetsFromSearchPoints(points []retrieval.SearchPoint) []Knowled
if text == "" { if text == "" {
continue continue
} }
sourceURI := optionalKnowledgeString(point.Payload["source_uri"])
snippets = append(snippets, KnowledgeSnippet{ snippets = append(snippets, KnowledgeSnippet{
PointID: strings.TrimSpace(point.ID), PointID: strings.TrimSpace(point.ID),
GroupID: anyInt64(point.Payload["group_id"]), GroupID: anyInt64(point.Payload["group_id"]),
GroupName: strings.TrimSpace(anyString(point.Payload["group_name"])), GroupName: strings.TrimSpace(anyString(point.Payload["group_name"])),
KnowledgeItemID: anyInt64(point.Payload["item_id"]), KnowledgeItemID: anyInt64(point.Payload["item_id"]),
ItemName: strings.TrimSpace(anyString(point.Payload["item_name"])), ItemName: strings.TrimSpace(anyString(point.Payload["item_name"])),
SourceType: strings.TrimSpace(anyString(point.Payload["source_type"])),
SourceURI: sourceURI,
Text: text, Text: text,
SearchText: strings.TrimSpace(anyString(point.Payload["search_text"])),
Score: point.Score, Score: point.Score,
}) })
} }
return snippets return snippets
} }
func (snippet KnowledgeSnippet) RerankText() string {
if text := strings.TrimSpace(snippet.SearchText); text != "" {
return text
}
return buildKnowledgeSearchText(knowledgeSearchTextInput{
GroupName: snippet.GroupName,
ItemName: snippet.ItemName,
SourceType: snippet.SourceType,
SourceURI: derefKnowledgeString(snippet.SourceURI),
Text: snippet.Text,
})
}
func (snippet KnowledgeSnippet) dedupeKey() string {
return normalizeKnowledgeSnippetTextKey(snippet.RerankText())
}
func dedupeKnowledgeSearchCandidates(candidates []KnowledgeSnippet) []KnowledgeSnippet { func dedupeKnowledgeSearchCandidates(candidates []KnowledgeSnippet) []KnowledgeSnippet {
if len(candidates) == 0 { if len(candidates) == 0 {
return []KnowledgeSnippet{} return []KnowledgeSnippet{}
@@ -1046,7 +1068,7 @@ func dedupeKnowledgeSearchCandidates(candidates []KnowledgeSnippet) []KnowledgeS
} }
seenPoints[pointID] = struct{}{} seenPoints[pointID] = struct{}{}
} }
textKey := normalizeKnowledgeSnippetTextKey(candidate.Text) textKey := candidate.dedupeKey()
if textKey == "" { if textKey == "" {
continue continue
} }
@@ -1090,7 +1112,7 @@ func selectKnowledgeRerankedSnippets(
return return
} }
} }
textKey := normalizeKnowledgeSnippetTextKey(snippet.Text) textKey := snippet.dedupeKey()
if textKey == "" { if textKey == "" {
return return
} }
@@ -1118,9 +1140,6 @@ func selectKnowledgeRerankedSnippets(
} }
snippet := candidates[item.Index] snippet := candidates[item.Index]
snippet.Score = item.RelevanceScore snippet.Score = item.RelevanceScore
if text := strings.TrimSpace(item.Text); text != "" {
snippet.Text = text
}
appendSnippet(snippet, true) appendSnippet(snippet, true)
} }
@@ -1870,7 +1889,18 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
} }
} }
embeddings, err := s.embedChunks(ctx, chunks) searchTexts := make([]string, 0, len(chunks))
for _, chunk := range chunks {
searchTexts = append(searchTexts, buildKnowledgeSearchText(knowledgeSearchTextInput{
GroupName: item.GroupName,
ItemName: item.Name,
SourceType: item.SourceType,
SourceURI: derefKnowledgeString(item.SourceURI),
Text: chunk,
}))
}
embeddings, err := s.embedChunks(ctx, searchTexts)
if err != nil { if err != nil {
_ = s.markKnowledgeIngestionFailed(context.Background(), job.TenantID, job.ItemID, job.ParseTaskID, err) _ = s.markKnowledgeIngestionFailed(context.Background(), job.TenantID, job.ItemID, job.ParseTaskID, err)
return return
@@ -1910,7 +1940,9 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
"item_version": item.ItemVersion, "item_version": item.ItemVersion,
"status": "active", "status": "active",
"source_type": item.SourceType, "source_type": item.SourceType,
"source_uri": derefKnowledgeString(item.SourceURI),
"text": chunk, "text": chunk,
"search_text": searchTexts[index],
}, },
}) })
metas = append(metas, knowledgeChunkMetaInput{ metas = append(metas, knowledgeChunkMetaInput{
@@ -2227,6 +2259,49 @@ func derefKnowledgeString(value *string) string {
return strings.TrimSpace(*value) return strings.TrimSpace(*value)
} }
func optionalKnowledgeString(value any) *string {
text := strings.TrimSpace(anyString(value))
if text == "" {
return nil
}
return &text
}
type knowledgeSearchTextInput struct {
GroupName string
ItemName string
SourceType string
SourceURI string
Text string
}
func buildKnowledgeSearchText(input knowledgeSearchTextInput) string {
text := strings.TrimSpace(input.Text)
if text == "" {
return ""
}
metadata := make([]string, 0, 4)
if groupName := strings.TrimSpace(input.GroupName); groupName != "" {
metadata = append(metadata, "目录: "+groupName)
}
if itemName := strings.TrimSpace(input.ItemName); itemName != "" {
metadata = append(metadata, "标题: "+itemName)
}
if sourceType := strings.TrimSpace(input.SourceType); sourceType != "" {
metadata = append(metadata, "来源类型: "+sourceType)
}
if sourceURI := strings.TrimSpace(input.SourceURI); sourceURI != "" {
metadata = append(metadata, "来源链接: "+sourceURI)
}
if len(metadata) == 0 {
return text
}
metadata = append(metadata, "内容:", text)
return strings.Join(metadata, "\n")
}
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, groupID int64) error { func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, groupID int64) error {
var exists bool var exists bool
err := s.pool.QueryRow(ctx, ` err := s.pool.QueryRow(ctx, `
+2 -2
View File
@@ -77,11 +77,11 @@ func knowledgeRuntimeFromConfig(retrievalCfg config.RetrievalConfig, llmCfg conf
} }
recallLimit := retrievalCfg.RecallLimit recallLimit := retrievalCfg.RecallLimit
if recallLimit <= 0 { if recallLimit <= 0 {
recallLimit = 12 recallLimit = 30
} }
rerankTopN := retrievalCfg.RerankTopN rerankTopN := retrievalCfg.RerankTopN
if rerankTopN <= 0 { if rerankTopN <= 0 {
rerankTopN = 6 rerankTopN = 8
} }
model := strings.TrimSpace(llmCfg.KnowledgeURLModel) model := strings.TrimSpace(llmCfg.KnowledgeURLModel)