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:
parent
fa0eb004a8
commit
72cbf73583
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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: 信任评分 stub(FileGraph 不持久化信任数据)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 Stubs(Windows 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 }
|
||||
|
|
@ -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: 信任评分 stub(InMemoryGraph 不持久化)
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
BIN
go/zhiyid-new
BIN
go/zhiyid-new
Binary file not shown.
Loading…
Reference in New Issue