feat: P0 recall fallback + P2 trust scoring for graph edges

P0: When bge-embed/IPC recall fails, fall back to graph.db keyword search (FallbackTextSearch) instead of 500 error. Response includes X-Fallback: graph header.

P2: Add trust_score, retrieval_count, helpful_count columns to graph_edges table. New POST /api/v1/graph/edge/feedback endpoint. UpdateEdgeTrustScores batch calculation.
This commit is contained in:
小唯 2026-07-02 00:22:24 +08:00
parent fa0eb004a8
commit 72cbf73583
9 changed files with 194 additions and 9 deletions

View File

@ -23,9 +23,10 @@ type API struct {
Reranker *storage.Reranker
Pipeline *storage.RecallPipeline
ConflictDetector *governance.ConflictDetector
GraphStore governance.GraphStore
}
func NewAPI(ldb storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker, cd *governance.ConflictDetector) *API {
func NewAPI(ldb storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker, cd *governance.ConflictDetector, gs governance.GraphStore) *API {
pipeline := storage.NewRecallPipeline(emb, ldb, rerank)
pipeline.SetPrefetchPusher(&WSPrefetchAdapter{})
return &API{
@ -34,6 +35,7 @@ func NewAPI(ldb storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker
Reranker: rerank,
Pipeline: pipeline,
ConflictDetector: cd,
GraphStore: gs,
}
}
@ -238,8 +240,15 @@ func (a *API) Recall(w http.ResponseWriter, r *http.Request) {
results, err := a.Pipeline.Recall(
req.Query, req.Namespace, req.Limit, req.Diversity)
if err != nil {
respondError(w, 500, "recall failed: "+err.Error())
return
// P0: 降级到 graph.db 关键词搜索
fallbackResults := a.GraphStore.FallbackTextSearch(req.Query, req.Namespace, req.Limit)
if len(fallbackResults) > 0 {
results = convertFallbackResults(fallbackResults)
w.Header().Set("X-Fallback", "graph")
} else {
respondError(w, 500, "recall failed: "+err.Error())
return
}
}
// E4.2: 跨 agent 知识共享 — recall 结果 < 3 时,补充搜索 "shared" namespace
@ -652,3 +661,70 @@ func minInt3(a, b int) int {
}
return b
}
// convertFallbackResults 将 FallbackTextSearch 结果map转换为 RecallResult 格式
func convertFallbackResults(rows []map[string]interface{}) []models.RecallResult {
results := make([]models.RecallResult, 0, len(rows))
for _, r := range rows {
score := 0.5
if pr, ok := r["pagerank"].(float64); ok {
score = pr
}
id := ""
if v, ok := r["id"]; ok {
id = fmt.Sprintf("%v", v)
}
name := ""
if v, ok := r["name"]; ok {
name = fmt.Sprintf("%v", v)
}
relation := ""
if v, ok := r["relation"]; ok {
relation = fmt.Sprintf("%v", v)
}
source := ""
if v, ok := r["source"]; ok {
source = fmt.Sprintf("%v", v)
}
target := ""
if v, ok := r["target"]; ok {
target = fmt.Sprintf("%v", v)
}
// Build readable content from graph edge info
content := fmt.Sprintf("[graph] %s --[%s]--> %s", source, relation, target)
if name != "" {
content = fmt.Sprintf("[graph] %s: %s --[%s]--> %s", name, source, relation, target)
}
results = append(results, models.RecallResult{
ID: id,
Content: content,
Category: "graph_fallback",
Score: score,
})
}
return results
}
// POST /api/v1/graph/edge/feedback — P2: 边反馈
func (a *API) EdgeFeedback(w http.ResponseWriter, r *http.Request) {
var req struct {
EdgeID string `json:"edge_id"`
Helpful bool `json:"helpful"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.EdgeID == "" {
respondError(w, 400, "edge_id required")
return
}
err := a.GraphStore.AddEdgeFeedback(req.EdgeID, req.Helpful)
if err != nil {
respondError(w, 500, "feedback update failed: "+err.Error())
return
}
// 触发信任评分更新
_ = a.GraphStore.UpdateEdgeTrustScores()
respond(w, 200, map[string]string{"status": "ok"})
}

View File

@ -59,7 +59,14 @@ func NewServer() http.Handler {
// 冲突检测器E4.1: Commit 时矛盾检测依赖此实例)
conflictDetector := governance.NewConflictDetector()
api := routes.NewAPI(ldb, emb, rerank, conflictDetector)
// 图谱(带 Navigate 结果缓存TTL 5min容量 500热点实体缓存命中加速
graphStore, cachedGraphStoreRef := storage.NewCachedGraphStore(initGraphStore())
api := routes.NewAPI(ldb, emb, rerank, conflictDetector, graphStore)
// G1: Recall 管线挂图谱扩展
api.Pipeline.SetGraphExpander(graphStore)
// 启动时初始化 Prometheus 指标
go func() {
@ -76,13 +83,10 @@ func NewServer() http.Handler {
}()
// 图谱(带 Navigate 结果缓存TTL 5min容量 500热点实体缓存命中加速
graphStore, cachedGraphStoreRef := storage.NewCachedGraphStore(initGraphStore())
// 已在上面初始化
graphUpdater := governance.NewAutoGraphUpdater(graphStore)
graphAPI := routes.NewGraphAPI(graphStore)
// G1: Recall 管线挂图谱扩展
api.Pipeline.SetGraphExpander(graphStore)
// G4: 自动蒸馏 → 注入图谱更新器
routes.SetGraphUpdater(graphUpdater)
@ -315,6 +319,8 @@ func NewServer() http.Handler {
mux.HandleFunc("/api/v1/graph/stats", graphAPI.Stats)
mux.HandleFunc("/api/v1/graph/query", graphAPI.Query)
mux.HandleFunc("/api/v1/graph/navigate", graphAPI.Navigate)
// P2: 边反馈
mux.HandleFunc("/api/v1/graph/edge/feedback", api.EdgeFeedback)
// 新增添加关系边POST JSON body: {"from":"实体A","to":"实体B","relation":"关系类型","namespace":""}
mux.HandleFunc("/api/v1/graph/edge", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {

View File

@ -442,6 +442,16 @@ func (fg *FileGraph) CleanupNoiseNodes(dryRun bool) (int, []string, error) {
return 0, nil, nil
}
// P0: FallbackTextSearch FileGraph stub已迁移到 SQLite
func (fg *FileGraph) FallbackTextSearch(query, namespace string, limit int) []map[string]interface{} {
return nil
}
// P2: 信任评分 stubFileGraph 不持久化信任数据)
func (fg *FileGraph) AddEdgeFeedback(edgeID string, helpful bool) error { return nil }
func (fg *FileGraph) IncrementEdgeRetrieval(edgeID string) error { return nil }
func (fg *FileGraph) UpdateEdgeTrustScores() error { return nil }
// ─── 图谱扩展 ────────────────────────────────────────────
func (fg *FileGraph) ExpandFromResults(results []models.RecallResult, namespace string, maxHops int) []models.RecallResult {

View File

@ -76,4 +76,13 @@ func (fg *FileGraph) lockFile(fd *os.File, exclusive bool) error {
// unlockFile 暂不实现no-op
func (fg *FileGraph) unlockFile(fd *os.File) {
}
}
// ─── P0/P2 StubsWindows FileGraph────────────────────
func (fg *FileGraph) FallbackTextSearch(query, namespace string, limit int) []map[string]interface{} {
return nil
}
func (fg *FileGraph) AddEdgeFeedback(edgeID string, helpful bool) error { return nil }
func (fg *FileGraph) IncrementEdgeRetrieval(edgeID string) error { return nil }
func (fg *FileGraph) UpdateEdgeTrustScores() error { return nil }

View File

@ -501,6 +501,17 @@ func (g *InMemoryGraph) ListNodes(namespace string) []map[string]interface{} {
return g.ListNodesByType("", namespace)
}
// P0: FallbackTextSearch 内存版 stub
func (g *InMemoryGraph) FallbackTextSearch(query, namespace string, limit int) []map[string]interface{} {
// InMemoryGraph 不支持 SQL LIKE降级到 SearchNodes
return g.SearchNodes(query, namespace)
}
// P2: 信任评分 stubInMemoryGraph 不持久化)
func (g *InMemoryGraph) AddEdgeFeedback(edgeID string, helpful bool) error { return nil }
func (g *InMemoryGraph) IncrementEdgeRetrieval(edgeID string) error { return nil }
func (g *InMemoryGraph) UpdateEdgeTrustScores() error { return nil }
func searchSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {

View File

@ -136,6 +136,11 @@ func (gs *SQLiteGraphStore) migrate() error {
// 修复孤儿边:自动补充缺失的节点
gs.repairOrphanEdges()
// P2: Trust scoring columns for graph_edges
gs.migrateAddColumn("graph_edges", "trust_score", "REAL DEFAULT 0.5")
gs.migrateAddColumn("graph_edges", "retrieval_count", "INTEGER DEFAULT 0")
gs.migrateAddColumn("graph_edges", "helpful_count", "INTEGER DEFAULT 0")
return nil
}
@ -981,6 +986,50 @@ func (gs *SQLiteGraphStore) GetEntityDegree(entity string) int {
return gs.EvidenceCount(entity)
}
// P0: FallbackTextSearch — 关键词降级搜索(向量搜索不可用时使用)
func (gs *SQLiteGraphStore) FallbackTextSearch(query, namespace string, limit int) []map[string]interface{} {
gs.mu.RLock()
defer gs.mu.RUnlock()
nsClause := "1=1"
if namespace != "" {
nsClause = fmt.Sprintf("e.namespace = '%s'", escape(namespace))
}
// 模糊匹配 node name + edge relation按 pagerank 排序
sql := fmt.Sprintf(
`SELECT DISTINCT e.id, e.source, e.target, e.relation, e.weight, n.name, n.pagerank
FROM graph_edges e
JOIN graph_nodes n ON e.source = n.id
WHERE (n.name LIKE '%%%s%%' OR e.relation LIKE '%%%s%%') AND %s
ORDER BY n.pagerank DESC
LIMIT %d`,
escape(query), escape(query), nsClause, limit)
return queryRows(gs.db, sql)
}
// P2: AddEdgeFeedback 记录边反馈helpful=true 增加 helpful_count否则增加 retrieval_count
func (gs *SQLiteGraphStore) AddEdgeFeedback(edgeID string, helpful bool) error {
gs.mu.Lock()
defer gs.mu.Unlock()
if helpful {
return execSQL(gs.db, fmt.Sprintf("UPDATE graph_edges SET helpful_count = helpful_count + 1 WHERE id = '%s'", escape(edgeID)))
}
return execSQL(gs.db, fmt.Sprintf("UPDATE graph_edges SET retrieval_count = retrieval_count + 1 WHERE id = '%s'", escape(edgeID)))
}
// IncrementEdgeRetrieval 递增边的检索计数
func (gs *SQLiteGraphStore) IncrementEdgeRetrieval(edgeID string) error {
gs.mu.Lock()
defer gs.mu.Unlock()
return execSQL(gs.db, fmt.Sprintf("UPDATE graph_edges SET retrieval_count = retrieval_count + 1 WHERE id = '%s'", escape(edgeID)))
}
// UpdateEdgeTrustScores 批量更新边的信任评分trust_score = helpful_count / retrieval_count
func (gs *SQLiteGraphStore) UpdateEdgeTrustScores() error {
gs.mu.Lock()
defer gs.mu.Unlock()
return execSQL(gs.db, `UPDATE graph_edges SET trust_score = CASE WHEN retrieval_count > 0 THEN CAST(helpful_count AS REAL) / retrieval_count ELSE 0.5 END`)
}
// ─── CGO 工具 ──────────────────────────────────────────
// UpdatePageRanks 批量更新节点的 pagerank 值§2.5.5

View File

@ -44,4 +44,12 @@ type GraphStore interface {
// 清理图谱脏数据:删除名称含编码噪音的节点(如 fts=、括号不匹配等)
// 返回被删除的节点数和节点 ID 列表
CleanupNoiseNodes(dryRun bool) (int, []string, error)
// P0: 关键词文本搜索降级(当向量搜索不可用时)
FallbackTextSearch(query, namespace string, limit int) []map[string]interface{}
// P2: 信任评分
AddEdgeFeedback(edgeID string, helpful bool) error
IncrementEdgeRetrieval(edgeID string) error
UpdateEdgeTrustScores() error
}

View File

@ -161,6 +161,22 @@ func (c *cachedGraphStore) CleanupNoiseNodes(dryRun bool) (int, []string, error)
return c.inner.CleanupNoiseNodes(dryRun)
}
// P0: FallbackTextSearch 透传到内层
func (c *cachedGraphStore) FallbackTextSearch(query, namespace string, limit int) []map[string]interface{} {
return c.inner.FallbackTextSearch(query, namespace, limit)
}
// P2: 信任评分透传
func (c *cachedGraphStore) AddEdgeFeedback(edgeID string, helpful bool) error {
return c.inner.AddEdgeFeedback(edgeID, helpful)
}
func (c *cachedGraphStore) IncrementEdgeRetrieval(edgeID string) error {
return c.inner.IncrementEdgeRetrieval(edgeID)
}
func (c *cachedGraphStore) UpdateEdgeTrustScores() error {
return c.inner.UpdateEdgeTrustScores()
}
// ── 缓存内部方法 ──
func (c *cachedGraphStore) navigateKey(entity string, maxHops int, relFilter []string) string {

Binary file not shown.