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:
@@ -0,0 +1,234 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user