From 31712986de92810a88cd5888fbd6f092acedd3cc Mon Sep 17 00:00:00 2001 From: xiaowei Date: Sat, 30 May 2026 19:17:12 +0800 Subject: [PATCH] fix(graph_sqlite): fix ExpandFromResults - wrong field access + Chinese entity extraction Root causes fixed: 1. ExpandFromResults accessed p["to"] but SQLiteGraphStore.Navigate returns {"source","target","relation","weight"} - field name mismatch caused graph expansion to always return empty 2. Original logic checked seen[r.ID] (memory ID) against entity name (neighbor string), which never matched - removed stale logic Also improved extractPotentialEntities: - Now handles Chinese continuous character sequences (2-8 chars) - Chinese entities extracted as whole words, not split by spaces - English capitalized words preserved with original casing Changes: - ExpandFromResults: correct field access (source/target/relation), bidirectional neighbor detection (forward + reverse edges), separate seenEntities map for entity dedup - extractPotentialEntities: full rewrite for Chinese + better English word tokenization without space-based splitting --- go/internal/governance/graph_sqlite.go | 107 +++++++++++++++++-------- 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/go/internal/governance/graph_sqlite.go b/go/internal/governance/graph_sqlite.go index 8f408c2..765b7e1 100644 --- a/go/internal/governance/graph_sqlite.go +++ b/go/internal/governance/graph_sqlite.go @@ -556,10 +556,11 @@ func (gs *SQLiteGraphStore) GetGraph(namespace string, limit int) ([]map[string] func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, namespace string, maxHops int) []models.RecallResult { // 从 recall 结果提取实体,展开图谱邻居 - expanded := make([]models.RecallResult, len(results)) - copy(expanded, results) + expanded := make([]models.RecallResult, 0, len(results)*2) + expanded = append(expanded, results...) - seen := make(map[string]bool) + seen := make(map[string]bool) // 追踪已访问的记忆 ID + seenEntities := make(map[string]bool) // 追踪已访问的实体(用于去重扩展结果) for _, r := range results { if seen[r.ID] { continue @@ -569,53 +570,93 @@ func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, nam // 从内容中提取可能作为实体的关键词 entities := extractPotentialEntities(r.Content) for _, entity := range entities { + if seenEntities[entity] { + continue + } + seenEntities[entity] = true + nodeID := normalizeEntityID(entity) paths, _ := gs.Navigate(nodeID, maxHops, namespace) for _, p := range paths { - if target, ok := p["to"].(string); ok && !seen[target] { - seen[target] = true - expanded = append(expanded, models.RecallResult{ - Content: fmt.Sprintf("[graph] %s --[%s]--> %s", entity, p["relation"], p["to"]), - Score: r.Score * 0.5, - }) + // Navigate 返回的是单条边 (source/target/relation/weight) + // 有两种情况: + // 1. source == entity(正向边):target 是下游邻居 + // 2. target == entity(反向边):source 是上游邻居 + var neighbor, rel string + src, _ := p["source"].(string) + tgt, _ := p["target"].(string) + relVal, _ := p["relation"].(string) + if src == entity && tgt != "" { + neighbor = tgt + rel = relVal + } else if tgt == entity && src != "" { + neighbor = src + rel = "↩ " + relVal } + if neighbor == "" { + continue + } + if seenEntities[neighbor] { + continue + } + seenEntities[neighbor] = true + expanded = append(expanded, models.RecallResult{ + Content: fmt.Sprintf("[graph] %s --[%s]--> %s", entity, rel, neighbor), + Score: r.Score * 0.5, + }) } } } return expanded } -// extractPotentialEntities 从文本中提取可能作为图谱实体的关键词 +// extractPotentialEntities 从文本中提取可能作为图谱实体的关键词(支持中文连续字符) func extractPotentialEntities(text string) []string { var entities []string seen := make(map[string]bool) - for _, w := range strings.Fields(text) { + runes := []rune(text) + for i := 0; i < len(runes); { + r := runes[i] + // 中文字符:收集连续的中文字符序列(2-8字) + if r >= 0x4E00 && r <= 0x9FFF { + start := i + i++ + for i < len(runes) && runes[i] >= 0x4E00 && runes[i] <= 0x9FFF { + i++ + } + chinese := string(runes[start:i]) + // 不等式:2 <= len(chinese) <= 8 + if len(chinese) >= 2 && len(chinese) <= 8 && !seen[chinese] { + seen[chinese] = true + entities = append(entities, chinese) + } + continue + } + // 非中文字符,收集整个单词 + start := i + for i < len(runes) { + r2 := runes[i] + if r2 >= 0x4E00 && r2 <= 0x9FFF { + break // 遇到汉字则停止 + } + i++ + } + if i-start < 2 { + continue + } + w := string(runes[start:i]) + // 去除首尾标点 w = strings.Trim(w, ",.;:!?,。;:!?、\"'()()[]【】") if len(w) < 2 { continue } - // 大写开头(英文命名实体) - runes := []rune(w) - if len(runes) >= 2 && runes[0] >= 'A' && runes[0] <= 'Z' { - if !seen[strings.ToLower(w)] { - seen[strings.ToLower(w)] = true - entities = append(entities, w) - } - } - // 纯中文字符 2-8 字 - chineseOnly := true - chineseRunes := 0 - for _, r := range runes { - if r >= 0x4E00 && r <= 0x9FFF { - chineseRunes++ - } else { - chineseOnly = false - } - } - if chineseOnly && chineseRunes >= 2 && chineseRunes <= 8 { - if !seen[w] { - seen[w] = true - entities = append(entities, w) + // 英文大写字母开头的词 + first := []rune(w) + if len(first) > 0 && first[0] >= 'A' && first[0] <= 'Z' { + lower := strings.ToLower(w) + if !seen[lower] { + seen[lower] = true + entities = append(entities, w) // 保留原始大小写 } } }