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:
2026-07-10 20:10:28 +08:00
parent 5463319b40
commit 418b3c4998
6 changed files with 839 additions and 16 deletions
@@ -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) {