fix: close answerability gate review gaps

This commit is contained in:
mlogclub
2026-05-02 14:40:16 +08:00
parent 8f968fe3ca
commit 2737f660f7
4 changed files with 231 additions and 15 deletions
@@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"cs-agent/internal/ai/rag"
"cs-agent/internal/ai/runtime/internal/impl/callbacks"
"cs-agent/internal/ai/runtime/internal/impl/factory"
"cs-agent/internal/ai/runtime/internal/impl/retrievers"
@@ -283,6 +285,12 @@ func (g *KnowledgeAnswerabilityGate) gradeAnswerability(ctx context.Context, sta
return state, nil
}
state.Grade = decision
if err := validateAnswerabilitySupport(decision, state.RetrieveResult); err != nil {
state.FallbackReply = resolveKnowledgeHumanSupportFallback(req.AIAgent)
state.ErrorMessage = err.Error()
state.recordAnswerabilityWithLatency(answerabilityStatusUnanswerable, "answerability supporting chunks invalid", err, started)
return state, nil
}
if !decision.Answerable {
state.FallbackReply = resolveKnowledgeHumanSupportFallback(req.AIAgent)
state.recordAnswerabilityWithLatency(answerabilityStatusUnanswerable, decision.Reason, nil, started)
@@ -352,6 +360,76 @@ func buildAnswerabilityContext(result *retrievers.KnowledgeRetrieveResult) strin
return strings.TrimSpace(builder.String())
}
func validateAnswerabilitySupport(decision answerabilityDecision, result *retrievers.KnowledgeRetrieveResult) error {
if !decision.Answerable {
return nil
}
if len(decision.SupportingChunkIDs) == 0 {
return fmt.Errorf("answerable decision requires supportingChunkIds")
}
items := []ragRetrieveItem(nil)
if result != nil {
if len(result.ContextResults) > 0 {
items = appendRetrieveItems(items, result.ContextResults)
} else {
items = appendRetrieveItems(items, result.Hits)
}
}
if len(items) == 0 {
return fmt.Errorf("answerable decision has no retrieved chunks to support it")
}
allowed := make(map[string]struct{})
for _, item := range items {
addAllowedSupportingChunkIDs(allowed, item)
}
for _, supportingChunkID := range decision.SupportingChunkIDs {
supportingChunkID = strings.TrimSpace(supportingChunkID)
if supportingChunkID == "" {
continue
}
if _, ok := allowed[supportingChunkID]; !ok {
return fmt.Errorf("supporting chunk id %q was not retrieved", supportingChunkID)
}
}
return nil
}
type ragRetrieveItem struct {
KnowledgeBaseID int64
DocumentID int64
FaqID int64
ChunkID int64
}
func appendRetrieveItems(dst []ragRetrieveItem, src []rag.RetrieveResult) []ragRetrieveItem {
for _, item := range src {
dst = append(dst, ragRetrieveItem{
KnowledgeBaseID: item.KnowledgeBaseID,
DocumentID: item.DocumentID,
FaqID: item.FaqID,
ChunkID: item.ChunkID,
})
}
return dst
}
func addAllowedSupportingChunkIDs(allowed map[string]struct{}, item ragRetrieveItem) {
if item.ChunkID <= 0 {
return
}
chunkID := strconv.FormatInt(item.ChunkID, 10)
allowed[chunkID] = struct{}{}
allowed["chunk:"+chunkID] = struct{}{}
allowed["chunkId:"+chunkID] = struct{}{}
allowed["chunk-"+chunkID] = struct{}{}
if item.KnowledgeBaseID > 0 && item.DocumentID > 0 {
allowed[fmt.Sprintf("kb:%d:doc:%d:chunk:%d", item.KnowledgeBaseID, item.DocumentID, item.ChunkID)] = struct{}{}
}
if item.KnowledgeBaseID > 0 && item.FaqID > 0 {
allowed[fmt.Sprintf("kb:%d:faq:%d:chunk:%d", item.KnowledgeBaseID, item.FaqID, item.ChunkID)] = struct{}{}
}
}
func parseAnswerabilityDecision(raw string) (answerabilityDecision, error) {
text := trimMarkdownFence(raw)
if text == "" {