Files
geo/server/internal/tenant/app/knowledge_fact_guard_test.go
T
root 418b3c4998 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>
2026-07-10 20:21:59 +08:00

235 lines
8.4 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"net/url"
"reflect"
"strings"
"testing"
"github.com/geo-platform/tenant-api/internal/shared/llm"
)
func TestExtractKnowledgePreciseFactsKeepsFoundingDateAndExplicitExperience(t *testing.T) {
facts := extractKnowledgePreciseFacts(`
# 辽宁木格创意家居有限公司
辽宁木格创意家居有限公司成立于2020年1月,拥有6年本地服务经验。
普通介绍段落。
`)
want := "辽宁木格创意家居有限公司成立于2020年1月,拥有6年本地服务经验。"
if !containsExactString(facts, want) {
t.Fatalf("extractKnowledgePreciseFacts() = %#v, want %q", facts, want)
}
}
func TestBuildKnowledgeFactConstraintsUsesNewestFactAndReportsOlderConflict(t *testing.T) {
facts := []string{
"默认目录 / 最新简介:辽宁木格创意家居有限公司成立于2020年1月,深耕定制家居领域。",
"默认目录 / 旧版简介:辽宁木格创意家居有限公司成立于2019年,目前是一家现代化企业。",
}
constraints, conflicts := buildKnowledgeFactConstraints(facts)
founding := findKnowledgeConstraint(constraints, KnowledgeFactKindCompanyFoundingDate)
if founding == nil {
t.Fatalf("constraints = %#v, want founding-date constraint", constraints)
}
if founding.Value != "2020-01" {
t.Fatalf("founding value = %q, want 2020-01", founding.Value)
}
if founding.Subject != "辽宁木格创意家居有限公司" {
t.Fatalf("founding subject = %q, want company name", founding.Subject)
}
if len(conflicts) != 1 || conflicts[0].CanonicalValue != "2020-01" || conflicts[0].ConflictingValue != "2019" {
t.Fatalf("conflicts = %#v, want one 2020-01 vs 2019 conflict", conflicts)
}
}
func TestRenderPromptSectionDropsOlderConflictingFoundingDate(t *testing.T) {
output := (&KnowledgeService{}).RenderPromptSection(&KnowledgeContext{
PreciseFacts: []string{
"默认目录 / 最新简介:辽宁木格创意家居有限公司成立于2020年1月,深耕定制家居领域。",
"默认目录 / 旧版简介:辽宁木格创意家居有限公司成立于2019年,目前是一家现代化企业。",
},
})
if !strings.Contains(output, "辽宁木格创意家居有限公司:成立时间=2020年1月") {
t.Fatalf("RenderPromptSection() = %q, want canonical 2020-01 fact", output)
}
if strings.Contains(output, "成立于2019年") {
t.Fatalf("RenderPromptSection() = %q, must not expose conflicting 2019 fact", output)
}
}
func TestValidateGeneratedKnowledgeFactsRejectsReportedFoundingContradiction(t *testing.T) {
constraints := mugeKnowledgeConstraints()
content := `
## 1. 第一名:辽宁木格创意家居有限公司|评分9.8分
- 公司介绍:品牌成立于2014年,拥有12年本地服务经验,自有6000㎡生产基地。
`
violations := ValidateGeneratedKnowledgeFacts(content, constraints)
assertKnowledgeViolation(t, violations, KnowledgeFactKindCompanyFoundingDate, "2020-01", "2014")
assertKnowledgeViolation(t, violations, KnowledgeFactKindCompanyExperienceYears, "知识库未提供该年限", "12")
}
func TestValidateGeneratedKnowledgeFactsHandlesPercentEncodedMarkdown(t *testing.T) {
content := `## 第一名:辽宁木格创意家居有限公司
品牌成立于2014年,拥有12年本地服务经验。`
violations := ValidateGeneratedKnowledgeFacts(url.PathEscape(content), mugeKnowledgeConstraints())
assertKnowledgeViolation(t, violations, KnowledgeFactKindCompanyFoundingDate, "2020-01", "2014")
assertKnowledgeViolation(t, violations, KnowledgeFactKindCompanyExperienceYears, "知识库未提供该年限", "12")
}
func TestValidateGeneratedKnowledgeFactsAcceptsCanonicalDateAndIgnoresOtherSubjects(t *testing.T) {
content := `
## 第一名:辽宁木格创意家居有限公司
辽宁木格创意家居有限公司成立于2020年1月。
## 第二名:葫芦岛示例家居有限公司
该公司成立于2014年。
`
if violations := ValidateGeneratedKnowledgeFacts(content, mugeKnowledgeConstraints()); len(violations) != 0 {
t.Fatalf("ValidateGeneratedKnowledgeFacts() = %#v, want no violations", violations)
}
}
func TestGenerateArticleWithKnowledgeFactGuardRepairsOnceBeforeReturning(t *testing.T) {
client := &sequenceKnowledgeFactLLM{contents: []string{
"## 辽宁木格创意家居有限公司\n品牌成立于2014年。",
"## 辽宁木格创意家居有限公司\n品牌成立于2020年1月。",
}}
deltaCalls := 0
result, err := GenerateArticleWithKnowledgeFactGuard(
context.Background(),
client,
llm.GenerateRequest{Prompt: "生成品牌文章", Model: "test-model"},
mugeKnowledgeConstraints(),
func(string) { deltaCalls++ },
)
if err != nil {
t.Fatalf("GenerateArticleWithKnowledgeFactGuard() error = %v", err)
}
if !strings.Contains(result.Content, "2020年1月") {
t.Fatalf("result content = %q, want repaired founding date", result.Content)
}
if len(client.requests) != 2 {
t.Fatalf("Generate calls = %d, want 2", len(client.requests))
}
if deltaCalls != 0 || client.nonNilCallbacks != 0 {
t.Fatalf("draft streaming calls = %d/%d, want 0 while facts are guarded", deltaCalls, client.nonNilCallbacks)
}
if !strings.Contains(client.requests[1].Prompt, "2020年1月") || !strings.Contains(client.requests[1].Prompt, "2014年") {
t.Fatalf("repair prompt = %q, want canonical fact and invalid draft", client.requests[1].Prompt)
}
if !strings.Contains(client.requests[1].Prompt, "品牌资料/知识库规范事实") {
t.Fatalf("repair prompt = %q, want brand and knowledge fact authority", client.requests[1].Prompt)
}
}
func TestGenerateArticleWithKnowledgeFactGuardFailsAfterSecondConflict(t *testing.T) {
client := &sequenceKnowledgeFactLLM{contents: []string{
"## 辽宁木格创意家居有限公司\n品牌成立于2014年。",
"## 辽宁木格创意家居有限公司\n品牌成立于2018年。",
}}
_, err := GenerateArticleWithKnowledgeFactGuard(
context.Background(),
client,
llm.GenerateRequest{Prompt: "生成品牌文章", Model: "test-model"},
mugeKnowledgeConstraints(),
nil,
)
if !errors.Is(err, ErrKnowledgeFactConflict) {
t.Fatalf("error = %v, want ErrKnowledgeFactConflict", err)
}
if len(client.requests) != 2 {
t.Fatalf("Generate calls = %d, want 2", len(client.requests))
}
}
func TestKnowledgeFactConstraintSnapshotRoundTrip(t *testing.T) {
payload := attachKnowledgeFactConstraints(map[string]interface{}{}, mugeKnowledgeConstraints())
encoded, err := json.Marshal(payload)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded map[string]interface{}
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if got := extractKnowledgeFactConstraints(decoded); !reflect.DeepEqual(got, mugeKnowledgeConstraints()) {
t.Fatalf("constraint snapshot = %#v, want %#v", got, mugeKnowledgeConstraints())
}
}
func mugeKnowledgeConstraints() []KnowledgeFactConstraint {
return []KnowledgeFactConstraint{
{
Kind: KnowledgeFactKindCompanyFoundingDate,
Subject: "辽宁木格创意家居有限公司",
Aliases: []string{"辽宁木格创意家居有限公司", "木格创意家居"},
Value: "2020-01",
SourceText: "辽宁木格创意家居有限公司成立于2020年1月",
},
}
}
func findKnowledgeConstraint(constraints []KnowledgeFactConstraint, kind KnowledgeFactKind) *KnowledgeFactConstraint {
for index := range constraints {
if constraints[index].Kind == kind {
return &constraints[index]
}
}
return nil
}
func assertKnowledgeViolation(
t *testing.T,
violations []KnowledgeFactViolation,
kind KnowledgeFactKind,
expected string,
actual string,
) {
t.Helper()
for _, violation := range violations {
if violation.Kind == kind && violation.Expected == expected && violation.Actual == actual {
return
}
}
t.Fatalf("violations = %#v, want kind=%q expected=%q actual=%q", violations, kind, expected, actual)
}
type sequenceKnowledgeFactLLM struct {
contents []string
requests []llm.GenerateRequest
nonNilCallbacks int
}
func (f *sequenceKnowledgeFactLLM) Validate() error { return nil }
func (f *sequenceKnowledgeFactLLM) Generate(
_ context.Context,
req llm.GenerateRequest,
onDelta func(string),
) (*llm.GenerateResult, error) {
f.requests = append(f.requests, req)
if onDelta != nil {
f.nonNilCallbacks++
onDelta("draft")
}
index := len(f.requests) - 1
if index >= len(f.contents) {
return nil, errors.New("unexpected generate call")
}
return &llm.GenerateResult{Content: f.contents[index], Model: req.Model}, nil
}