Files
Blizzard 79e834e8e9 feat(rag): 大文件 RAG 做深 —— 并发入库/图谱窗口化 + Bleve落盘 + file_id治理
几十万字文件从"广而浅"到准生产级:
- 向量化串行→并发分批保序(embedAll);几十万字上千块快数倍
- 图谱整篇喂LLM(爆上下文只抽开头)→窗口化并发抽(extractGraphWindowed),
  全覆盖;窗口/封顶/并发 env 可配;图谱可单配便宜模型(GRAPH_CHAT_*,未配回退主chat)
- Bleve 内存索引(重启即丢、三路退两路)→落盘 scorch(env BLEVE_PATH,失败退内存兜底)
- 下游键改稳定 file_id:Neo4j 关系打 file_id(实体仍 kb+name 共享);
  新增 kb_delete 工具 + Engine.DeleteDoc 级联删 Milvus/Bleve/Neo4j
- 单测:窗口化/去重/env可配/落盘持久/图谱模型回退

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:37:45 +08:00

109 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package rag
import (
"strings"
"testing"
)
// TestPackGraphWindows_Groups 把多块合并成 ~target 的窗口,窗口数远少于块数。
func TestPackGraphWindows_Groups(t *testing.T) {
var chunks []string
for i := 0; i < 50; i++ {
chunks = append(chunks, strings.Repeat("字", 200)) // 每块 200 字
}
// target=4000 → 每窗约 20 块 → ~3 窗(不再是 50 次 LLM 调用)。
windows, truncated := packGraphWindows(chunks, 4000, 60)
if truncated {
t.Fatal("50×200=1万字不该触发封顶")
}
if len(windows) >= len(chunks) {
t.Fatalf("窗口应远少于块数:windows=%d chunks=%d", len(windows), len(chunks))
}
for i, w := range windows {
if rl := runeLen(w); rl > 4000+200 { // 容一块的溢出
t.Fatalf("窗口 %d 超目标过多:%d 字", i, rl)
}
}
}
// TestPackGraphWindows_Truncate 超大文档封顶窗口数并报 truncated。
func TestPackGraphWindows_Truncate(t *testing.T) {
var chunks []string
for i := 0; i < 1000; i++ {
chunks = append(chunks, strings.Repeat("字", 500))
}
windows, truncated := packGraphWindows(chunks, 4000, 60)
if !truncated {
t.Fatal("50万字应触发封顶 truncated=true")
}
if len(windows) > 60 {
t.Fatalf("窗口数应封顶 ≤60got %d", len(windows))
}
}
// TestDedupeTriples 去重 + 去空项。
func TestDedupeTriples(t *testing.T) {
in := []Triple{
{S: "李明华", P: "是", O: "总设计师"},
{S: "李明华", P: "是", O: "总设计师"}, // 重复
{S: "星云一号", P: "发射于", O: "2023"},
{S: "", P: "x", O: "y"}, // 空主体 → 丢
{S: "a", P: "", O: "b"}, // 空关系 → 丢
{S: "李明华", P: "领导", O: "团队"}, // 同主体不同关系 → 保留
}
out := dedupeTriples(in)
if len(out) != 3 {
t.Fatalf("应剩 3 条(去 1 重复 + 2 空),got %d: %+v", len(out), out)
}
}
// TestEnvInt env 正整数读取:缺省/0/非法都退 def。
func TestEnvInt(t *testing.T) {
if envInt("NO_SUCH_ENV_XZ", 7) != 7 {
t.Fatal("缺省应返回 def")
}
t.Setenv("X_TEST_INT", "12")
if envInt("X_TEST_INT", 7) != 12 {
t.Fatal("应读 env=12")
}
t.Setenv("X_TEST_INT", "0")
if envInt("X_TEST_INT", 7) != 7 {
t.Fatal("非正数应退 def")
}
t.Setenv("X_TEST_INT", "abc")
if envInt("X_TEST_INT", 7) != 7 {
t.Fatal("非法应退 def")
}
}
// TestGraphKnobs_EnvConfigurable 图谱封顶/窗口可经 env 调(瓶颈①)。
func TestGraphKnobs_EnvConfigurable(t *testing.T) {
t.Setenv("GRAPH_MAX_WINDOWS", "2")
if graphMaxWindows() != 2 {
t.Fatalf("env 应覆盖封顶为 2got %d", graphMaxWindows())
}
if graphWindowRunes() != 4000 {
t.Fatalf("未设时默认窗口应 4000got %d", graphWindowRunes())
}
}
// TestGraphChatClient_FallsBackToMain 未配专用图谱模型 → 回退主 chat;配了 → 用专用(瓶颈⑤)。
func TestGraphChatClient_FallsBackToMain(t *testing.T) {
e := &Engine{chat: newChatClient("http://main", "k", "m")}
if e.graphChatClient() != e.chat {
t.Fatal("未配专用模型应回退主 chat")
}
e.graphChat = newChatClient("http://cheap", "k", "cheap")
if e.graphChatClient() != e.graphChat {
t.Fatal("配了专用模型应用专用")
}
}
// TestPackGraphWindows_Empty 空输入不 panic。
func TestPackGraphWindows_Empty(t *testing.T) {
w, trunc := packGraphWindows(nil, 4000, 60)
if len(w) != 0 || trunc {
t.Fatalf("空输入应得空窗口、不截断,got %d %v", len(w), trunc)
}
}