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>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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("窗口数应封顶 ≤60,got %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 应覆盖封顶为 2,got %d", graphMaxWindows())
|
||||
}
|
||||
if graphWindowRunes() != 4000 {
|
||||
t.Fatalf("未设时默认窗口应 4000,got %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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user