ljsysfurry commited on
Commit
73bd8e4
·
verified ·
1 Parent(s): 927c2a8

Upload folder using huggingface_hub

Browse files
agentframe/README.md CHANGED
@@ -2,7 +2,7 @@
2
 
3
  **脑 = DeepSeek · 手 = 工具执行 · 记忆 = 四层上下文保持**
4
 
5
- 版本 4.2.0 · GPL-3.0 · Cloud LTE Studio
6
 
7
  ---
8
 
@@ -35,6 +35,8 @@
35
  | **Aura 遗忘** | S(t)=I·2^(-t/τ) 指数遗忘 + 访问增强 | ✓ 有测试 |
36
  | **LFRU 滞回** | 历史热块信用折减驱逐分数, 防抖动 (colibrì #441/#497) | ✓ 有测试 |
37
  | **Couple 预取** | 跨轮共现预取: 检索到 A 后预取常与 A 共现的 B (colibrì couple) | ✓ 有测试 |
 
 
38
  | **脑+手** | function calling 工具循环, Agent 自验证代码 | ✓ 有测试 + 安全拦截 |
39
  | **多会话** | 每会话独立引擎, 状态可持久化 | ✓ |
40
 
@@ -256,6 +258,15 @@ GPL-3.0 © Cloud LTE Studio
256
 
257
  ## Changelog
258
 
 
 
 
 
 
 
 
 
 
259
  ### v4.2.0 (2026-08-18) — 真 INT4 打包 (28.4x 落地)
260
 
261
  **新增**
 
2
 
3
  **脑 = DeepSeek · 手 = 工具执行 · 记忆 = 四层上下文保持**
4
 
5
+ 版本 4.3.0 · GPL-3.0 · Cloud LTE Studio
6
 
7
  ---
8
 
 
35
  | **Aura 遗忘** | S(t)=I·2^(-t/τ) 指数遗忘 + 访问增强 | ✓ 有测试 |
36
  | **LFRU 滞回** | 历史热块信用折减驱逐分数, 防抖动 (colibrì #441/#497) | ✓ 有测试 |
37
  | **Couple 预取** | 跨轮共现预取: 检索到 A 后预取常与 A 共现的 B (colibrì couple) | ✓ 有测试 |
38
+ | **KV 增量持久化** | 每轮 append 不重写全量, crash-safe 坏行跳过 (colibrì kv_persist) | ✓ 有测试 |
39
+ | **前缀复用** | 相同前缀 query 复用上次检索, 保持热块 (colibrì kv_prefix) | ✓ 有测试 |
40
  | **脑+手** | function calling 工具循环, Agent 自验证代码 | ✓ 有测试 + 安全拦截 |
41
  | **多会话** | 每会话独立引擎, 状态可持久化 | ✓ |
42
 
 
258
 
259
  ## Changelog
260
 
261
+ ### v4.3.0 (2026-08-18) — KV 增量持久化 + 前缀复用
262
+
263
+ **新增**
264
+ - **IncrementalKVStore** (移植 colibrì kv_persist.h): 每轮 ingest 新 chunk 追加一行 JSON, 不重写全量快照; 行级独立 = crash-safe (坏行跳过); `enable_incremental()` 开启, `load_incremental()` 重启恢复
265
+ - **查询前缀复用** (移植 colibrì kv_prefix.h): 新 query 与上一条共享 ≥70% 前缀 → 直接复用上次检索结果 (跳过重新路由) + 保持热块; `_prefix_hits` 统计
266
+ - engine.ingest() 自动 append 到增量日志
267
+
268
+ **测试**: 核心测试 16 项 (+test_incremental_persist / test_prefix_reuse / test_incremental_engine)
269
+
270
  ### v4.2.0 (2026-08-18) — 真 INT4 打包 (28.4x 落地)
271
 
272
  **新增**
agentframe/__init__.py CHANGED
@@ -5,9 +5,9 @@ AgentFrame — Agent 专用上下文保持框架
5
  手 = 工具执行 (Function Calling)
6
  记忆 = 四层上下文保持 (认知 × 路由 × 存储 × 物理)
7
 
8
- 版本: 4.2.0 (先明·第四代: 真 INT4 打包 + Couple 预取 + LFRU 滞回 + 安全加固)
9
  """
10
- __version__ = "4.2.0"
11
 
12
  from .core.quad import (
13
  CompressedKV,
 
5
  手 = 工具执行 (Function Calling)
6
  记忆 = 四层上下文保持 (认知 × 路由 × 存储 × 物理)
7
 
8
+ 版本: 4.3.0 (先明·第四代: KV 增量持久化 + 前缀复用 + 真 INT4 + Couple + LFRU)
9
  """
10
+ __version__ = "4.3.0"
11
 
12
  from .core.quad import (
13
  CompressedKV,
agentframe/api/server.py CHANGED
@@ -107,7 +107,7 @@ class AgentFrameAPI:
107
 
108
  @app.get("/health")
109
  def health():
110
- return jsonify({"status": "ok", "version": "4.2.0",
111
  "sessions": len(self.sessions),
112
  "system": "AgentFrame Context API"})
113
 
@@ -257,7 +257,7 @@ def main():
257
  config = AgentFrameConfig.from_env()
258
  port = int(sys.argv[1]) if len(sys.argv) > 1 else config.api.port
259
  api = AgentFrameAPI(config)
260
- print(f"🚀 AgentFrame API v4.2.0")
261
  print(f" 四层上下文保持: 认知×路由×存储×物理")
262
  print(f" 会话: POST /v1/sessions | 摄入: /ingest | 查询: /ask")
263
  print(f" 监听: {config.api.host}:{port}")
 
107
 
108
  @app.get("/health")
109
  def health():
110
+ return jsonify({"status": "ok", "version": "4.3.0",
111
  "sessions": len(self.sessions),
112
  "system": "AgentFrame Context API"})
113
 
 
257
  config = AgentFrameConfig.from_env()
258
  port = int(sys.argv[1]) if len(sys.argv) > 1 else config.api.port
259
  api = AgentFrameAPI(config)
260
+ print(f"🚀 AgentFrame API v4.3.0")
261
  print(f" 四层上下文保持: 认知×路由×存储×物理")
262
  print(f" 会话: POST /v1/sessions | 摄入: /ingest | 查询: /ask")
263
  print(f" 监听: {config.api.host}:{port}")
agentframe/core/engine.py CHANGED
@@ -22,6 +22,7 @@ from ..config import AgentFrameConfig, MemoryConfig
22
  from ..llm import create_llm, LLMProvider
23
  from ..embed import create_embedding, EmbeddingProvider
24
  from ..memory.store import StateStore
 
25
  from .quad import QuadLayerAgent
26
  from .couple import CouplePrefetcher
27
 
@@ -140,6 +141,15 @@ class ContextEngine:
140
  # 跨轮共现预取 (colibrì couple_prefetch 移植)
141
  self.couple = CouplePrefetcher(top_k=m.couple_k)
142
 
 
 
 
 
 
 
 
 
 
143
  # 会话状态
144
  self.chat_history = []
145
  self.max_history_turns = self.config.api.max_history_turns
@@ -167,6 +177,11 @@ class ContextEngine:
167
  "created_at": self.now,
168
  }
169
  self._chunk_counter += 1
 
 
 
 
 
170
  return cid
171
 
172
  def ingest_batch(self, chunks: list) -> list:
@@ -230,9 +245,17 @@ class ContextEngine:
230
  subtasks = self.agent.metacog.decompose(query)
231
  directive = self.agent.metacog.build_directive(subtasks[0], self.agent.chunk_meta)
232
 
233
- # 2. 路由层: landmark 检索
234
- q_vec = self.embedder.embed(query)
235
- selection = self.agent.router.route(q_vec, q_vec, self.agent.summaries)
 
 
 
 
 
 
 
 
236
 
237
  # 2.5 跨轮共现预取: 记录本轮检索 + 预测下轮 → 提升冷块到 RAM
238
  self.couple.record(selection.chunk_ids)
@@ -295,6 +318,89 @@ class ContextEngine:
295
  directive=directive,
296
  meta={"now": self.now, "chunks": len(self.agent.chunk_meta)})
297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  # ============ 工具增强版 ask ============
299
 
300
  def ask_with_hands(self, query: str, thinking: dict = None) -> AnswerResult:
 
22
  from ..llm import create_llm, LLMProvider
23
  from ..embed import create_embedding, EmbeddingProvider
24
  from ..memory.store import StateStore
25
+ from ..memory.incremental import IncrementalKVStore
26
  from .quad import QuadLayerAgent
27
  from .couple import CouplePrefetcher
28
 
 
141
  # 跨轮共现预取 (colibrì couple_prefetch 移植)
142
  self.couple = CouplePrefetcher(top_k=m.couple_k)
143
 
144
+ # 增量持久化 (colibrì kv_persist 移植): 每轮 append, 不重写全量
145
+ self.incr_store = None
146
+ self.incr_path = None
147
+
148
+ # 查询前缀复用 (colibrì kv_prefix 移植)
149
+ self._last_query = None
150
+ self._last_retrieved = []
151
+ self._prefix_hits = 0
152
+
153
  # 会话状态
154
  self.chat_history = []
155
  self.max_history_turns = self.config.api.max_history_turns
 
177
  "created_at": self.now,
178
  }
179
  self._chunk_counter += 1
180
+ # 增量追加持久化: 新 chunk 立即落盘 (crash-safe)
181
+ if self.incr_store is not None:
182
+ self.incr_store.append(
183
+ cid, kv.q4, kv.scales, kv.latent, kv.quant_bits,
184
+ kv.size_bytes, self.agent.chunk_meta[cid])
185
  return cid
186
 
187
  def ingest_batch(self, chunks: list) -> list:
 
245
  subtasks = self.agent.metacog.decompose(query)
246
  directive = self.agent.metacog.build_directive(subtasks[0], self.agent.chunk_meta)
247
 
248
+ # 1.5 查询前缀复用 (colibrì kv_prefix 移植):
249
+ # 新 query 与上一条共享长前缀 → 直接复用上次检索结果, 保持热块
250
+ reuse = self._prefix_reuse(query)
251
+ if reuse is not None:
252
+ selection = reuse
253
+ else:
254
+ # 2. 路由层: landmark 检索
255
+ q_vec = self.embedder.embed(query)
256
+ selection = self.agent.router.route(q_vec, q_vec, self.agent.summaries)
257
+ self._last_query = query
258
+ self._last_retrieved = list(selection.chunk_ids)
259
 
260
  # 2.5 跨轮共现预取: 记录本轮检索 + 预测下轮 → 提升冷块到 RAM
261
  self.couple.record(selection.chunk_ids)
 
318
  directive=directive,
319
  meta={"now": self.now, "chunks": len(self.agent.chunk_meta)})
320
 
321
+ # ============ 前缀复用 (colibrì kv_prefix 移植) ============
322
+
323
+ def _prefix_reuse(self, query: str):
324
+ """
325
+ 查询前缀复用: 新 query 与上一条共享长前缀 (前 70%+ 且 ≥8 字符)
326
+ → 直接复用上次检索结果 (跳过重新路由), 并保持上次热块不冷却。
327
+ 返回 ChunkSelection 或 None (不复用)。
328
+ colibrì 对应: fed[] 序列前缀匹配, 跳过重复 prefill。
329
+ """
330
+ if self._last_query is None or not self._last_retrieved:
331
+ return None
332
+ prev, cur = self._last_query, query
333
+ if len(cur) < 8 or len(prev) < 8:
334
+ return None
335
+ # 公共前缀长度
336
+ common = 0
337
+ for a, b in zip(prev, cur):
338
+ if a != b:
339
+ break
340
+ common += 1
341
+ # 前缀须覆盖旧 query 的 70% 且新 query 的 50% (防过度复用)
342
+ if common >= len(prev) * 0.7 and common >= len(cur) * 0.5:
343
+ # 保持上次热块: 触碰一次 (刷新遗忘曲线)
344
+ for cid in self._last_retrieved:
345
+ kv = (self.agent.pager.vram.get(cid)
346
+ or self.agent.pager.ram.get(cid)
347
+ or self.agent.pager.disk.get(cid))
348
+ if kv is not None:
349
+ self.agent.pager.access(cid, 0.5, self.now)
350
+ self._prefix_hits += 1
351
+ from .quad import ChunkSelection
352
+ return ChunkSelection(
353
+ chunk_ids=self._last_retrieved,
354
+ scores=[0.5] * len(self._last_retrieved),
355
+ weights=[1.0 / len(self._last_retrieved)] * len(self._last_retrieved))
356
+ return None
357
+
358
+ # ============ 增量持久化 (colibrì kv_persist 移植) ============
359
+
360
+ def enable_incremental(self, path: str):
361
+ """开启增量持久化: 新 chunk 追加到日志, 重启可增量恢复"""
362
+ self.incr_path = path
363
+ self.incr_store = IncrementalKVStore(path)
364
+ return self
365
+
366
+ def load_incremental(self, path: str = None) -> int:
367
+ """
368
+ 从增量日志恢复所有 chunk (colibrì: 重启恢复, 不用重新 prefill)。
369
+ 重建 chunk_meta + summaries + pager. 返回恢复的 chunk 数。
370
+ """
371
+ path = path or self.incr_path
372
+ if not path:
373
+ return 0
374
+ store = IncrementalKVStore(path)
375
+ if not store.exists():
376
+ return 0
377
+ recs = store.load()
378
+ restored = 0
379
+ for rec in recs:
380
+ cid = rec["chunk_id"]
381
+ # 跳过已存在的
382
+ if cid in self.agent.chunk_meta:
383
+ continue
384
+ from .quad import CompressedKV
385
+ kv = CompressedKV(
386
+ chunk_id=cid,
387
+ latent=rec["latent"],
388
+ quant_bits=rec["quant_bits"],
389
+ size_bytes=rec["size_bytes"],
390
+ q4=rec["q4"],
391
+ scales=rec["scales"],
392
+ )
393
+ kv.heat = 0.5
394
+ self.agent.chunk_meta[cid] = rec["meta"]
395
+ # 重建 landmark 摘要
396
+ k_prime, bias = self.agent.router.build_summary(
397
+ rec["latent"].reshape(1, -1))
398
+ self.agent.summaries[cid] = (k_prime, bias)
399
+ self.agent.pager.place(kv, self.now)
400
+ self._chunk_counter = max(self._chunk_counter, cid + 1)
401
+ restored += 1
402
+ return restored
403
+
404
  # ============ 工具增强版 ask ============
405
 
406
  def ask_with_hands(self, query: str, thinking: dict = None) -> AnswerResult:
agentframe/memory/incremental.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IncrementalKVStore — 增量追加持久化 (colibrì kv_persist.h 移植)
3
+ =================================================================
4
+ colibrì 做法: 每轮对话后把压缩 MLA KV 增量 append 到 .coli_kv,
5
+ nrec 计数最后写 = crash-safe, 重启直接恢复不用重新 prefill。
6
+
7
+ AgentFrame 移植:
8
+ - 每次 ingest 的新 chunk 追加为一行 JSON (不重写全量快照)
9
+ - 行级独立 = 崩溃时最多丢最后一条, 坏行跳过 (crash-safe)
10
+ - load() 读全部记录重建 chunk + 自动重建 landmark 摘要
11
+ - 与全量快照 (StateStore) 互补: 增量管内容, 快照管运行时热度状态
12
+ """
13
+ import json
14
+ import os
15
+ import numpy as np
16
+
17
+
18
+ class IncrementalKVStore:
19
+ """增量追加 KV 日志 (colibrì kv_persist 思想)"""
20
+
21
+ MAGIC = "AFKV1"
22
+
23
+ def __init__(self, path: str):
24
+ self.path = path
25
+
26
+ # ============ 写入 ============
27
+
28
+ def append(self, chunk_id: int, q4: np.ndarray, scales: np.ndarray,
29
+ latent: np.ndarray, quant_bits: int, size_bytes: int,
30
+ meta: dict) -> int:
31
+ """
32
+ 追加一条 chunk 记录 (每行一个 JSON, 追加模式).
33
+ 返回当前记录数 (类似 colibrì 的 nrec).
34
+ """
35
+ rec = {
36
+ "magic": self.MAGIC,
37
+ "chunk_id": int(chunk_id),
38
+ "quant_bits": int(quant_bits),
39
+ "size_bytes": int(size_bytes),
40
+ "q4": q4.tobytes().hex() if q4 is not None else None,
41
+ "scales": scales.tolist() if scales is not None else None,
42
+ "latent": latent.tolist(),
43
+ "meta": meta,
44
+ }
45
+ os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
46
+ with open(self.path, "a") as f:
47
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
48
+ return self.count()
49
+
50
+ def count(self) -> int:
51
+ """统计有效记录数 (跳过坏行)"""
52
+ return len(self._read_raw())
53
+
54
+ # ============ 读取 ============
55
+
56
+ def _read_raw(self) -> list:
57
+ """读全部行, 跳过损坏/不完整行 (crash-safe 核心)"""
58
+ if not os.path.exists(self.path):
59
+ return []
60
+ recs = []
61
+ with open(self.path) as f:
62
+ for line in f:
63
+ line = line.strip()
64
+ if not line:
65
+ continue
66
+ try:
67
+ rec = json.loads(line)
68
+ if rec.get("magic") != self.MAGIC:
69
+ continue
70
+ recs.append(rec)
71
+ except Exception:
72
+ continue # 崩溃残留的半行: 跳过
73
+ return recs
74
+
75
+ def load(self) -> list:
76
+ """
77
+ 读全部有效记录, 反序列化为可重建的数据结构.
78
+ 返回: [{chunk_id, q4, scales, latent, quant_bits, size_bytes, meta}, ...]
79
+ """
80
+ out = []
81
+ for rec in self._read_raw():
82
+ q4 = None
83
+ if rec.get("q4"):
84
+ q4 = np.frombuffer(bytes.fromhex(rec["q4"]), dtype=np.uint8)
85
+ scales = None
86
+ if rec.get("scales"):
87
+ scales = np.asarray(rec["scales"], dtype=np.float32)
88
+ out.append({
89
+ "chunk_id": int(rec["chunk_id"]),
90
+ "quant_bits": int(rec.get("quant_bits", 4)),
91
+ "size_bytes": int(rec.get("size_bytes", 0)),
92
+ "q4": q4,
93
+ "scales": scales,
94
+ "latent": np.asarray(rec.get("latent", [0.0]), dtype=np.float32),
95
+ "meta": rec.get("meta", {}),
96
+ })
97
+ return out
98
+
99
+ def exists(self) -> bool:
100
+ return os.path.exists(self.path)
agentframe/pyproject.toml CHANGED
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "agentframe"
7
- version = "4.2.0"
8
- description = "AgentFrame v4.2 — 真 INT4 打包 (29.1x) + Couple 跨轮预取 + LFRU 滞回 + MemoryDirector"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
11
  license = { text = "GPL-3.0" }
 
4
 
5
  [project]
6
  name = "agentframe"
7
+ version = "4.3.0"
8
+ description = "AgentFrame v4.3 — KV 增量持久化 + 前缀复用 + 真 INT4 (29.1x) + Couple 预取 + LFRU 滞回"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
11
  license = { text = "GPL-3.0" }
agentframe/tests/test_core.py CHANGED
@@ -216,6 +216,69 @@ def test_int4_packing():
216
  print(f"✅ test_int4_packing (cos={sim:.4f}, {ratio:.1f}x)")
217
 
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  if __name__ == "__main__":
220
  test_ingest_and_retrieve()
221
  test_similar_text_retrieval()
@@ -228,4 +291,7 @@ if __name__ == "__main__":
228
  test_api_auth()
229
  test_couple_prefetch()
230
  test_int4_packing()
 
 
 
231
  print("\n🎉 全部核心测试通过!")
 
216
  print(f"✅ test_int4_packing (cos={sim:.4f}, {ratio:.1f}x)")
217
 
218
 
219
+ def test_incremental_persist():
220
+ """增量持久化 (colibrì kv_persist 移植): append + crash-safe 恢复"""
221
+ import tempfile, os
222
+ from agentframe.memory.incremental import IncrementalKVStore
223
+
224
+ path = tempfile.mktemp(suffix=".kv")
225
+ store = IncrementalKVStore(path)
226
+ # 追加 3 条
227
+ import numpy as np
228
+ n1 = store.append(0, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32),
229
+ np.zeros(576, dtype=np.float32), 4, 352, {"text": "A"})
230
+ n2 = store.append(1, np.ones(288, dtype=np.uint8), np.ones(16, dtype=np.float32) * 2,
231
+ np.ones(576, dtype=np.float32), 4, 352, {"text": "B"})
232
+ n3 = store.append(2, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32),
233
+ np.zeros(576, dtype=np.float32), 4, 352, {"text": "C"})
234
+ assert n3 == 3
235
+ # 模拟崩溃: 写入半行垃圾
236
+ with open(path, "a") as f:
237
+ f.write('{"magic": "AFKV1", "chunk_id": 99, "q4": "zz')
238
+ recs = store.load()
239
+ assert len(recs) == 3, f"坏行应被跳过, 实际 {len(recs)}"
240
+ assert recs[1]["meta"]["text"] == "B"
241
+ print("✅ test_incremental_persist (append + crash-safe 坏行跳过)")
242
+ os.unlink(path)
243
+
244
+
245
+ def test_prefix_reuse():
246
+ """查询前缀复用 (colibrì kv_prefix 移植)"""
247
+ eng = make_engine()
248
+ eng.ingest("KV 压缩 29 倍", ["kv"])
249
+ eng.ingest("LFRU 滞回驱逐", ["method"])
250
+ eng.ingest("Couple 预取", ["method"])
251
+ # 第一轮: 正常检索
252
+ r1 = eng.ask("KV 压缩是多少倍", chat=False)
253
+ first = r1.retrieved
254
+ # 第二轮: 相同前缀 → 复用
255
+ r2 = eng.ask("KV 压缩是多少倍呢", chat=False)
256
+ assert eng._prefix_hits == 1, "应触发前缀复用"
257
+ assert [c for c, _ in r2.retrieved] == [c for c, _ in first]
258
+ # 第三轮: 完全不同 → 不复用
259
+ r3 = eng.ask("今天天气怎么样", chat=False)
260
+ assert eng._prefix_hits == 1, "不同 query 不应复用"
261
+ print("✅ test_prefix_reuse (前缀命中复用, 不同 query 不复用)")
262
+
263
+
264
+ def test_incremental_engine():
265
+ """引擎级增量恢复: ingest → enable → load_incremental 重建"""
266
+ import tempfile, os
267
+ path = tempfile.mktemp(suffix=".kv")
268
+ eng = make_engine()
269
+ eng.enable_incremental(path)
270
+ eng.ingest("增量持久化内容 A", ["a"])
271
+ eng.ingest("增量持久化内容 B", ["b"])
272
+ # 新引擎从日志恢复
273
+ eng2 = make_engine()
274
+ n = eng2.load_incremental(path)
275
+ assert n == 2, f"应恢复 2 块, 实际 {n}"
276
+ r = eng2.ask("增量持久化", chat=False)
277
+ assert len(r.retrieved) >= 1
278
+ os.unlink(path)
279
+ print("✅ test_incremental_engine (引擎级增量恢复 + 检索)")
280
+
281
+
282
  if __name__ == "__main__":
283
  test_ingest_and_retrieve()
284
  test_similar_text_retrieval()
 
291
  test_api_auth()
292
  test_couple_prefetch()
293
  test_int4_packing()
294
+ test_incremental_persist()
295
+ test_prefix_reuse()
296
+ test_incremental_engine()
297
  print("\n🎉 全部核心测试通过!")