418b3c4998
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>
235 lines
8.4 KiB
Go
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
|
|
}
|