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
This commit is contained in:
xiaowei 2026-05-30 19:17:12 +08:00
parent 04057637e0
commit 31712986de
1 changed files with 74 additions and 33 deletions

View File

@ -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) // 保留原始大小写
}
}
}