feat(tenant): guard numeric knowledge facts against silent contradiction
Ensure numeric facts present in retrieved knowledge (especially company founding dates) cannot be silently contradicted in generated articles. - Add a typed fact guard that extracts constraints (dates, experience years, etc.) from precise facts, detects source conflicts, and validates generated content before persistence with a bounded repair retry. - Surface fact constraints through KnowledgeContext and render them as a canonical, non-derivable fact block in the knowledge prompt section. - Tighten prompt intro lines to forbid rewriting, mixing old values, or deriving service years from founding dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,10 +131,12 @@ type KnowledgeURLItemRequest struct {
|
||||
}
|
||||
|
||||
type KnowledgeContext struct {
|
||||
GroupIDs []int64
|
||||
GroupNames []string
|
||||
Snippets []KnowledgeSnippet
|
||||
PreciseFacts []string
|
||||
GroupIDs []int64
|
||||
GroupNames []string
|
||||
Snippets []KnowledgeSnippet
|
||||
PreciseFacts []string
|
||||
FactConstraints []KnowledgeFactConstraint
|
||||
FactConflicts []KnowledgeFactConflict
|
||||
}
|
||||
|
||||
type KnowledgeSnippet struct {
|
||||
@@ -936,6 +938,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
Snippets: []KnowledgeSnippet{},
|
||||
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, nil),
|
||||
}
|
||||
s.applyKnowledgeFactConstraints(baseContext)
|
||||
|
||||
vectors, err := s.provider.Embed(ctx, queries)
|
||||
if err != nil {
|
||||
@@ -994,6 +997,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.applyKnowledgeFactConstraints(baseContext)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "rerank_failed_fallback", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
@@ -1002,6 +1006,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.applyKnowledgeFactConstraints(baseContext)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "resolved", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
@@ -1348,7 +1353,7 @@ func normalizeKnowledgeSnippetTextKey(text string) string {
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) RenderPromptSection(ctx *KnowledgeContext) string {
|
||||
if ctx == nil || (len(ctx.Snippets) == 0 && len(ctx.PreciseFacts) == 0) {
|
||||
if ctx == nil || (len(ctx.Snippets) == 0 && len(ctx.PreciseFacts) == 0 && len(ctx.FactConstraints) == 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1357,6 +1362,21 @@ func (s *KnowledgeService) RenderPromptSection(ctx *KnowledgeContext) string {
|
||||
if len(preciseFacts) == 0 {
|
||||
preciseFacts = collectKnowledgePromptPreciseFacts(ctx.Snippets)
|
||||
}
|
||||
factConstraints := ctx.FactConstraints
|
||||
if len(factConstraints) == 0 {
|
||||
var conflicts []KnowledgeFactConflict
|
||||
factConstraints, conflicts = buildKnowledgeFactConstraints(preciseFacts)
|
||||
preciseFacts = removeConflictingKnowledgeFacts(preciseFacts, conflicts)
|
||||
}
|
||||
if len(factConstraints) > 0 {
|
||||
lines = append(lines,
|
||||
"以下为系统落库前会自动校验的规范事实;涉及对应主体时只能使用这些值,不得引用旧版本、推算年限或自行补充数字:",
|
||||
)
|
||||
for _, constraint := range factConstraints {
|
||||
lines = append(lines, "- "+renderKnowledgeFactConstraint(constraint))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
if len(preciseFacts) > 0 {
|
||||
lines = append(lines,
|
||||
"以下为必须严格按原文保留的高精度事实;如果正文需要引用,必须逐字一致,不得改写、翻译、推测或补全:",
|
||||
@@ -1379,6 +1399,27 @@ func (s *KnowledgeService) RenderPromptSection(ctx *KnowledgeContext) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) applyKnowledgeFactConstraints(ctx *KnowledgeContext) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
constraints, conflicts := buildKnowledgeFactConstraints(ctx.PreciseFacts)
|
||||
ctx.PreciseFacts = removeConflictingKnowledgeFacts(ctx.PreciseFacts, conflicts)
|
||||
ctx.FactConstraints = constraints
|
||||
ctx.FactConflicts = conflicts
|
||||
if s == nil || s.logger == nil {
|
||||
return
|
||||
}
|
||||
for _, conflict := range conflicts {
|
||||
s.logger.Warn("knowledge fact source conflict",
|
||||
zap.String("fact_kind", string(conflict.Kind)),
|
||||
zap.String("fact_subject", conflict.Subject),
|
||||
zap.String("canonical_value", conflict.CanonicalValue),
|
||||
zap.String("conflicting_value", conflict.ConflictingValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type knowledgePromptItemRecord struct {
|
||||
ID int64
|
||||
GroupID int64
|
||||
@@ -1601,11 +1642,11 @@ func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
|
||||
AND ki.deleted_at IS NULL
|
||||
AND ki.group_id IN (SELECT id FROM selected_groups)
|
||||
ORDER BY
|
||||
ki.updated_at DESC,
|
||||
CASE
|
||||
WHEN ki.id = ANY($4::bigint[]) THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
ki.updated_at DESC,
|
||||
ki.id DESC
|
||||
LIMIT $5
|
||||
`, tenantID, brandID, groupIDs, prioritizedItemIDs, limit)
|
||||
@@ -1783,7 +1824,9 @@ func knowledgePreciseFactLabelOnly(line string) bool {
|
||||
"主营业务", "主要业务", "核心业务", "业务范围", "经营范围", "服务范围",
|
||||
"主营产品", "主要产品", "核心产品", "产品范围",
|
||||
"main business", "core business", "business scope", "service scope",
|
||||
"main products", "core products", "product scope":
|
||||
"main products", "core products", "product scope",
|
||||
"成立时间", "创立时间", "创办时间", "创建时间", "注册时间", "注册成立时间",
|
||||
"服务年限", "行业经验", "从业经验", "经营年限", "company founded", "founded", "established":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1808,6 +1851,9 @@ func knowledgeLineContainsPreciseFact(line string) bool {
|
||||
"opening hours", "business hours", "postcode", "zip code",
|
||||
"main business", "core business", "business scope", "service scope",
|
||||
"main products", "core products", "product scope",
|
||||
"成立于", "成立时间", "创立于", "创立时间", "创办于", "创办时间", "创建于", "创建时间", "注册成立", "注册时间",
|
||||
"服务经验", "行业经验", "从业经验", "经营经验", "经营年限", "深耕",
|
||||
"founded", "established", "years of experience",
|
||||
}
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(lower, keyword) {
|
||||
@@ -1853,6 +1899,9 @@ func knowledgeLineStartsAnotherPreciseFact(line string) bool {
|
||||
"opening hours:", "business hours:",
|
||||
"main business:", "core business:", "business scope:", "service scope:",
|
||||
"main products:", "core products:", "product scope:",
|
||||
"成立时间:", "创立时间:", "创办时间:", "创建时间:", "注册时间:", "注册成立时间:",
|
||||
"服务年限:", "行业经验:", "从业经验:", "经营年限:",
|
||||
"company founded:", "founded:", "established:",
|
||||
}
|
||||
for _, label := range labels {
|
||||
if strings.HasPrefix(lower, label) {
|
||||
|
||||
Reference in New Issue
Block a user