538 lines
16 KiB
Go
538 lines
16 KiB
Go
|
|
package app
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"net/url"
|
|||
|
|
"regexp"
|
|||
|
|
"sort"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"unicode"
|
|||
|
|
|
|||
|
|
"github.com/geo-platform/tenant-api/internal/shared/llm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type KnowledgeFactKind string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
KnowledgeFactKindCompanyFoundingDate KnowledgeFactKind = "company_founding_date"
|
|||
|
|
KnowledgeFactKindCompanyExperienceYears KnowledgeFactKind = "company_experience_years"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var ErrKnowledgeFactConflict = errors.New("generated article contradicts canonical knowledge facts")
|
|||
|
|
|
|||
|
|
type KnowledgeFactConstraint struct {
|
|||
|
|
Kind KnowledgeFactKind `json:"kind"`
|
|||
|
|
Subject string `json:"subject"`
|
|||
|
|
Aliases []string `json:"aliases,omitempty"`
|
|||
|
|
Value string `json:"value"`
|
|||
|
|
SourceText string `json:"source_text"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type KnowledgeFactConflict struct {
|
|||
|
|
Kind KnowledgeFactKind
|
|||
|
|
Subject string
|
|||
|
|
CanonicalValue string
|
|||
|
|
ConflictingValue string
|
|||
|
|
ConflictingFact string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type KnowledgeFactViolation struct {
|
|||
|
|
Kind KnowledgeFactKind
|
|||
|
|
Subject string
|
|||
|
|
Expected string
|
|||
|
|
Actual string
|
|||
|
|
Excerpt string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type KnowledgeFactGuardError struct {
|
|||
|
|
Violations []KnowledgeFactViolation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (e *KnowledgeFactGuardError) Error() string {
|
|||
|
|
if e == nil || len(e.Violations) == 0 {
|
|||
|
|
return ErrKnowledgeFactConflict.Error()
|
|||
|
|
}
|
|||
|
|
parts := make([]string, 0, len(e.Violations))
|
|||
|
|
for _, violation := range e.Violations {
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s/%s expected %s, got %s", violation.Subject, violation.Kind, violation.Expected, violation.Actual))
|
|||
|
|
}
|
|||
|
|
return ErrKnowledgeFactConflict.Error() + ": " + strings.Join(parts, "; ")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (e *KnowledgeFactGuardError) Unwrap() error {
|
|||
|
|
return ErrKnowledgeFactConflict
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
knowledgeFactSubjectPattern = regexp.MustCompile(`([\p{Han}A-Za-z0-9·()()]{2,64}(?:有限责任公司|股份有限公司|有限公司|公司|品牌|家居))\s*(?:正式)?(?:注册成立|成立|创立|创办|创建)`)
|
|||
|
|
knowledgeFactFoundingPattern = regexp.MustCompile(`(?:注册成立|成立|创立|创办|创建)(?:时间)?\s*(?:于|为|是)?\s*((?:19|20)\d{2})\s*年?(?:\s*(\d{1,2})\s*月)?`)
|
|||
|
|
knowledgeFactExperiencePattern = regexp.MustCompile(`(\d{1,3})\s*年(?:以上)?(?:的)?(?:本地)?(?:服务|行业|从业|经营|实战|设计|施工|定制)?(?:经验|历程|沉淀)`)
|
|||
|
|
knowledgeFactDeepExperiencePattern = regexp.MustCompile(`(?:深耕|从业|经营)\D{0,12}(\d{1,3})\s*年`)
|
|||
|
|
percentEncodedKnowledgeTextPattern = regexp.MustCompile(`%[0-9A-Fa-f]{2}`)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func buildKnowledgeFactConstraints(facts []string) ([]KnowledgeFactConstraint, []KnowledgeFactConflict) {
|
|||
|
|
constraints := make([]KnowledgeFactConstraint, 0, len(facts))
|
|||
|
|
conflicts := make([]KnowledgeFactConflict, 0)
|
|||
|
|
|
|||
|
|
for _, fact := range facts {
|
|||
|
|
for _, candidate := range parseKnowledgeFactConstraints(fact) {
|
|||
|
|
matched := -1
|
|||
|
|
for index := range constraints {
|
|||
|
|
if constraints[index].Kind == candidate.Kind && knowledgeFactSubjectsEquivalent(constraints[index].Subject, candidate.Subject) {
|
|||
|
|
matched = index
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if matched < 0 {
|
|||
|
|
constraints = append(constraints, candidate)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if constraints[matched].Value == candidate.Value {
|
|||
|
|
constraints[matched].Aliases = mergeKnowledgeFactAliases(constraints[matched].Aliases, candidate.Aliases)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
conflicts = append(conflicts, KnowledgeFactConflict{
|
|||
|
|
Kind: candidate.Kind,
|
|||
|
|
Subject: constraints[matched].Subject,
|
|||
|
|
CanonicalValue: constraints[matched].Value,
|
|||
|
|
ConflictingValue: candidate.Value,
|
|||
|
|
ConflictingFact: candidate.SourceText,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return constraints, conflicts
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func MergeKnowledgeFactConstraints(
|
|||
|
|
primary []KnowledgeFactConstraint,
|
|||
|
|
fallback []KnowledgeFactConstraint,
|
|||
|
|
) []KnowledgeFactConstraint {
|
|||
|
|
merged := append([]KnowledgeFactConstraint{}, primary...)
|
|||
|
|
for _, candidate := range fallback {
|
|||
|
|
matched := -1
|
|||
|
|
for index := range merged {
|
|||
|
|
if merged[index].Kind == candidate.Kind && knowledgeFactSubjectsEquivalent(merged[index].Subject, candidate.Subject) {
|
|||
|
|
matched = index
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if matched < 0 {
|
|||
|
|
merged = append(merged, candidate)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
merged[matched].Aliases = mergeKnowledgeFactAliases(merged[matched].Aliases, candidate.Aliases)
|
|||
|
|
}
|
|||
|
|
return merged
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseKnowledgeFactConstraints(fact string) []KnowledgeFactConstraint {
|
|||
|
|
fact = strings.TrimSpace(fact)
|
|||
|
|
if fact == "" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
subject := extractKnowledgeFactSubject(fact)
|
|||
|
|
if subject == "" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
aliases := buildKnowledgeFactAliases(subject, fact)
|
|||
|
|
constraints := make([]KnowledgeFactConstraint, 0, 2)
|
|||
|
|
|
|||
|
|
if match := knowledgeFactFoundingPattern.FindStringSubmatch(fact); len(match) > 1 {
|
|||
|
|
constraints = append(constraints, KnowledgeFactConstraint{
|
|||
|
|
Kind: KnowledgeFactKindCompanyFoundingDate,
|
|||
|
|
Subject: subject,
|
|||
|
|
Aliases: aliases,
|
|||
|
|
Value: normalizeKnowledgeDateValue(match[1], submatchValue(match, 2)),
|
|||
|
|
SourceText: fact,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if years := extractKnowledgeExperienceYears(fact); years != "" {
|
|||
|
|
constraints = append(constraints, KnowledgeFactConstraint{
|
|||
|
|
Kind: KnowledgeFactKindCompanyExperienceYears,
|
|||
|
|
Subject: subject,
|
|||
|
|
Aliases: aliases,
|
|||
|
|
Value: years,
|
|||
|
|
SourceText: fact,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return constraints
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractKnowledgeFactSubject(fact string) string {
|
|||
|
|
match := knowledgeFactSubjectPattern.FindStringSubmatch(fact)
|
|||
|
|
if len(match) < 2 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
subject := strings.TrimSpace(match[1])
|
|||
|
|
for {
|
|||
|
|
separator := strings.LastIndexAny(subject, "::/||")
|
|||
|
|
if separator < 0 {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
subject = strings.TrimSpace(subject[separator+1:])
|
|||
|
|
}
|
|||
|
|
return strings.Trim(subject, "-—_ ,,。;;::")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildKnowledgeFactAliases(subject string, fact string) []string {
|
|||
|
|
aliases := []string{strings.TrimSpace(subject)}
|
|||
|
|
trimmedSubject := strings.TrimSpace(subject)
|
|||
|
|
for _, suffix := range []string{"有限责任公司", "股份有限公司", "有限公司", "公司"} {
|
|||
|
|
if strings.HasSuffix(trimmedSubject, suffix) {
|
|||
|
|
aliases = append(aliases, strings.TrimSpace(strings.TrimSuffix(trimmedSubject, suffix)))
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if separator := strings.IndexAny(fact, "::"); separator > 0 {
|
|||
|
|
label := strings.TrimSpace(fact[:separator])
|
|||
|
|
if slash := strings.LastIndexAny(label, "/||"); slash >= 0 {
|
|||
|
|
label = strings.TrimSpace(label[slash+1:])
|
|||
|
|
}
|
|||
|
|
for _, suffix := range []string{"公司简介", "企业简介", "品牌简介", "简介", "介绍", "资料"} {
|
|||
|
|
label = strings.TrimSpace(strings.TrimSuffix(label, suffix))
|
|||
|
|
}
|
|||
|
|
if len([]rune(label)) >= 4 {
|
|||
|
|
aliases = append(aliases, label)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return normalizeKnowledgeFactAliases(aliases)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeKnowledgeDateValue(year string, month string) string {
|
|||
|
|
year = strings.TrimSpace(year)
|
|||
|
|
month = strings.TrimSpace(month)
|
|||
|
|
if month == "" {
|
|||
|
|
return year
|
|||
|
|
}
|
|||
|
|
monthValue, err := strconv.Atoi(month)
|
|||
|
|
if err != nil || monthValue < 1 || monthValue > 12 {
|
|||
|
|
return year
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s-%02d", year, monthValue)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractKnowledgeExperienceYears(text string) string {
|
|||
|
|
for _, pattern := range []*regexp.Regexp{knowledgeFactExperiencePattern, knowledgeFactDeepExperiencePattern} {
|
|||
|
|
if match := pattern.FindStringSubmatch(text); len(match) > 1 {
|
|||
|
|
return strings.TrimSpace(match[1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func knowledgeFactSubjectsEquivalent(left string, right string) bool {
|
|||
|
|
leftKey := normalizeKnowledgeFactComparable(left)
|
|||
|
|
rightKey := normalizeKnowledgeFactComparable(right)
|
|||
|
|
if leftKey == "" || rightKey == "" {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if leftKey == rightKey {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
shorter := leftKey
|
|||
|
|
longer := rightKey
|
|||
|
|
if len([]rune(shorter)) > len([]rune(longer)) {
|
|||
|
|
shorter, longer = longer, shorter
|
|||
|
|
}
|
|||
|
|
return len([]rune(shorter)) >= 4 && strings.Contains(longer, shorter)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeKnowledgeFactComparable(value string) string {
|
|||
|
|
var builder strings.Builder
|
|||
|
|
for _, r := range strings.ToLower(strings.TrimSpace(value)) {
|
|||
|
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
|||
|
|
builder.WriteRune(r)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return builder.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeKnowledgeFactAliases(values []string) []string {
|
|||
|
|
seen := make(map[string]struct{}, len(values))
|
|||
|
|
aliases := make([]string, 0, len(values))
|
|||
|
|
for _, value := range values {
|
|||
|
|
value = strings.TrimSpace(value)
|
|||
|
|
key := normalizeKnowledgeFactComparable(value)
|
|||
|
|
if value == "" || len([]rune(key)) < 4 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if _, exists := seen[key]; exists {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
seen[key] = struct{}{}
|
|||
|
|
aliases = append(aliases, value)
|
|||
|
|
}
|
|||
|
|
sort.SliceStable(aliases, func(i, j int) bool {
|
|||
|
|
return len([]rune(aliases[i])) > len([]rune(aliases[j]))
|
|||
|
|
})
|
|||
|
|
return aliases
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func mergeKnowledgeFactAliases(left []string, right []string) []string {
|
|||
|
|
return normalizeKnowledgeFactAliases(append(append([]string{}, left...), right...))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func removeConflictingKnowledgeFacts(facts []string, conflicts []KnowledgeFactConflict) []string {
|
|||
|
|
if len(facts) == 0 || len(conflicts) == 0 {
|
|||
|
|
return facts
|
|||
|
|
}
|
|||
|
|
ignored := make(map[string]struct{}, len(conflicts))
|
|||
|
|
for _, conflict := range conflicts {
|
|||
|
|
ignored[conflict.ConflictingFact] = struct{}{}
|
|||
|
|
}
|
|||
|
|
filtered := make([]string, 0, len(facts))
|
|||
|
|
for _, fact := range facts {
|
|||
|
|
if _, exists := ignored[fact]; exists {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
filtered = append(filtered, fact)
|
|||
|
|
}
|
|||
|
|
return filtered
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ValidateGeneratedKnowledgeFacts(content string, constraints []KnowledgeFactConstraint) []KnowledgeFactViolation {
|
|||
|
|
if strings.TrimSpace(content) == "" || len(constraints) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
content = decodeKnowledgeFactContent(content)
|
|||
|
|
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
|||
|
|
violations := make([]KnowledgeFactViolation, 0)
|
|||
|
|
seen := make(map[string]struct{})
|
|||
|
|
currentHeading := ""
|
|||
|
|
|
|||
|
|
for _, rawLine := range lines {
|
|||
|
|
line := strings.TrimSpace(rawLine)
|
|||
|
|
if line == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if strings.HasPrefix(line, "#") {
|
|||
|
|
currentHeading = strings.TrimSpace(strings.TrimLeft(line, "#"))
|
|||
|
|
}
|
|||
|
|
scope := strings.TrimSpace(currentHeading + " " + line)
|
|||
|
|
|
|||
|
|
for _, constraint := range constraints {
|
|||
|
|
if !knowledgeFactScopeMatches(scope, constraint) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
switch constraint.Kind {
|
|||
|
|
case KnowledgeFactKindCompanyFoundingDate:
|
|||
|
|
for _, match := range knowledgeFactFoundingPattern.FindAllStringSubmatch(line, -1) {
|
|||
|
|
actual := normalizeKnowledgeDateValue(submatchValue(match, 1), submatchValue(match, 2))
|
|||
|
|
if knowledgeDateMatchesConstraint(actual, constraint.Value) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
appendKnowledgeFactViolation(&violations, seen, KnowledgeFactViolation{
|
|||
|
|
Kind: constraint.Kind,
|
|||
|
|
Subject: constraint.Subject,
|
|||
|
|
Expected: constraint.Value,
|
|||
|
|
Actual: actual,
|
|||
|
|
Excerpt: line,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, founding := range constraintsByKind(constraints, KnowledgeFactKindCompanyFoundingDate) {
|
|||
|
|
if !knowledgeFactScopeMatches(scope, founding) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
actualYears := extractKnowledgeExperienceYears(line)
|
|||
|
|
if actualYears == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
experience := matchingKnowledgeFactConstraint(constraints, KnowledgeFactKindCompanyExperienceYears, founding.Subject)
|
|||
|
|
if experience != nil && experience.Value == actualYears {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
expected := "知识库未提供该年限"
|
|||
|
|
if experience != nil {
|
|||
|
|
expected = experience.Value
|
|||
|
|
}
|
|||
|
|
appendKnowledgeFactViolation(&violations, seen, KnowledgeFactViolation{
|
|||
|
|
Kind: KnowledgeFactKindCompanyExperienceYears,
|
|||
|
|
Subject: founding.Subject,
|
|||
|
|
Expected: expected,
|
|||
|
|
Actual: actualYears,
|
|||
|
|
Excerpt: line,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return violations
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func decodeKnowledgeFactContent(content string) string {
|
|||
|
|
if !percentEncodedKnowledgeTextPattern.MatchString(content) {
|
|||
|
|
return content
|
|||
|
|
}
|
|||
|
|
decoded, err := url.PathUnescape(content)
|
|||
|
|
if err != nil {
|
|||
|
|
return content
|
|||
|
|
}
|
|||
|
|
return decoded
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func knowledgeFactScopeMatches(scope string, constraint KnowledgeFactConstraint) bool {
|
|||
|
|
scopeKey := normalizeKnowledgeFactComparable(scope)
|
|||
|
|
if scopeKey == "" {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
aliases := append([]string{constraint.Subject}, constraint.Aliases...)
|
|||
|
|
for _, alias := range aliases {
|
|||
|
|
aliasKey := normalizeKnowledgeFactComparable(alias)
|
|||
|
|
if len([]rune(aliasKey)) >= 4 && strings.Contains(scopeKey, aliasKey) {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func knowledgeDateMatchesConstraint(actual string, expected string) bool {
|
|||
|
|
if actual == expected {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
actualYear, _, _ := strings.Cut(actual, "-")
|
|||
|
|
expectedYear, _, _ := strings.Cut(expected, "-")
|
|||
|
|
return actualYear == expectedYear && !strings.Contains(actual, "-")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func constraintsByKind(constraints []KnowledgeFactConstraint, kind KnowledgeFactKind) []KnowledgeFactConstraint {
|
|||
|
|
filtered := make([]KnowledgeFactConstraint, 0)
|
|||
|
|
for _, constraint := range constraints {
|
|||
|
|
if constraint.Kind == kind {
|
|||
|
|
filtered = append(filtered, constraint)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return filtered
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func matchingKnowledgeFactConstraint(
|
|||
|
|
constraints []KnowledgeFactConstraint,
|
|||
|
|
kind KnowledgeFactKind,
|
|||
|
|
subject string,
|
|||
|
|
) *KnowledgeFactConstraint {
|
|||
|
|
for index := range constraints {
|
|||
|
|
if constraints[index].Kind == kind && knowledgeFactSubjectsEquivalent(constraints[index].Subject, subject) {
|
|||
|
|
return &constraints[index]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func appendKnowledgeFactViolation(
|
|||
|
|
violations *[]KnowledgeFactViolation,
|
|||
|
|
seen map[string]struct{},
|
|||
|
|
violation KnowledgeFactViolation,
|
|||
|
|
) {
|
|||
|
|
key := strings.Join([]string{string(violation.Kind), violation.Subject, violation.Expected, violation.Actual, violation.Excerpt}, "\x00")
|
|||
|
|
if _, exists := seen[key]; exists {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
seen[key] = struct{}{}
|
|||
|
|
*violations = append(*violations, violation)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GenerateArticleWithKnowledgeFactGuard(
|
|||
|
|
ctx context.Context,
|
|||
|
|
client llm.Client,
|
|||
|
|
req llm.GenerateRequest,
|
|||
|
|
constraints []KnowledgeFactConstraint,
|
|||
|
|
onDelta func(string),
|
|||
|
|
) (*llm.GenerateResult, error) {
|
|||
|
|
if len(constraints) == 0 {
|
|||
|
|
return client.Generate(ctx, req, onDelta)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result, err := client.Generate(ctx, req, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if result == nil {
|
|||
|
|
return nil, errors.New("knowledge fact guarded generation returned no result")
|
|||
|
|
}
|
|||
|
|
violations := ValidateGeneratedKnowledgeFacts(result.Content, constraints)
|
|||
|
|
if len(violations) == 0 {
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
repairReq := req
|
|||
|
|
repairReq.Prompt = buildKnowledgeFactRepairPrompt(req.Prompt, result.Content, constraints, violations)
|
|||
|
|
repaired, err := client.Generate(ctx, repairReq, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if repaired == nil {
|
|||
|
|
return nil, errors.New("knowledge fact repair returned no result")
|
|||
|
|
}
|
|||
|
|
remaining := ValidateGeneratedKnowledgeFacts(repaired.Content, constraints)
|
|||
|
|
if len(remaining) > 0 {
|
|||
|
|
return nil, &KnowledgeFactGuardError{Violations: remaining}
|
|||
|
|
}
|
|||
|
|
return repaired, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func buildKnowledgeFactRepairPrompt(
|
|||
|
|
originalPrompt string,
|
|||
|
|
draft string,
|
|||
|
|
constraints []KnowledgeFactConstraint,
|
|||
|
|
violations []KnowledgeFactViolation,
|
|||
|
|
) string {
|
|||
|
|
var builder strings.Builder
|
|||
|
|
builder.WriteString(strings.TrimSpace(originalPrompt))
|
|||
|
|
builder.WriteString("\n\n# 强制事实纠错\n")
|
|||
|
|
builder.WriteString("上一版草稿违反了品牌资料/知识库规范事实。请输出完整的修正版 Markdown,不要解释修改过程。规范事实优先于草稿、常识、推测和搜索结果;不得新增品牌资料或知识库未明确提供的服务年限。\n\n")
|
|||
|
|
builder.WriteString("## 规范事实\n")
|
|||
|
|
for _, constraint := range constraints {
|
|||
|
|
builder.WriteString("- ")
|
|||
|
|
builder.WriteString(renderKnowledgeFactConstraint(constraint))
|
|||
|
|
builder.WriteByte('\n')
|
|||
|
|
}
|
|||
|
|
builder.WriteString("\n## 已检测到的冲突\n")
|
|||
|
|
for _, violation := range violations {
|
|||
|
|
builder.WriteString(fmt.Sprintf("- %s:应为 %s,草稿写成 %s\n", violation.Subject, violation.Expected, violation.Actual))
|
|||
|
|
}
|
|||
|
|
builder.WriteString("\n## 待修正草稿\n")
|
|||
|
|
builder.WriteString(strings.TrimSpace(draft))
|
|||
|
|
return builder.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func renderKnowledgeFactConstraint(constraint KnowledgeFactConstraint) string {
|
|||
|
|
switch constraint.Kind {
|
|||
|
|
case KnowledgeFactKindCompanyFoundingDate:
|
|||
|
|
return fmt.Sprintf("%s:成立时间=%s", constraint.Subject, renderKnowledgeDateValue(constraint.Value))
|
|||
|
|
case KnowledgeFactKindCompanyExperienceYears:
|
|||
|
|
return fmt.Sprintf("%s:明确服务/从业年限=%s年", constraint.Subject, constraint.Value)
|
|||
|
|
default:
|
|||
|
|
return fmt.Sprintf("%s:%s=%s", constraint.Subject, constraint.Kind, constraint.Value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func renderKnowledgeDateValue(value string) string {
|
|||
|
|
year, month, found := strings.Cut(value, "-")
|
|||
|
|
if !found {
|
|||
|
|
return strings.TrimSpace(value) + "年"
|
|||
|
|
}
|
|||
|
|
monthValue, err := strconv.Atoi(month)
|
|||
|
|
if err != nil {
|
|||
|
|
return year + "年"
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s年%d月", year, monthValue)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func submatchValue(match []string, index int) string {
|
|||
|
|
if index < 0 || index >= len(match) {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return match[index]
|
|||
|
|
}
|