AgentFrame-v3 / agentframe /tests /test_core.py
ljsysfurry's picture
Upload folder using huggingface_hub
079e669 verified
Raw
History Blame Contribute Delete
13.7 kB
"""AgentFrame 核心测试 (离线, 无需 API key)"""
import os
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from agentframe.config import AgentFrameConfig
from agentframe.core.engine import ContextEngine
from agentframe.embed.provider import HashEmbedding
def make_engine() -> ContextEngine:
cfg = AgentFrameConfig.from_env()
cfg.llm.provider = "mock" # 离线
return ContextEngine(cfg)
def test_ingest_and_retrieve():
eng = make_engine()
eng.ingest("KV 缓存压缩测试数据", ["kv"])
eng.ingest("注意力分数不等于任务重要性", ["agent"])
eng.ingest("Minecraft 1.20.4 逆向完成", ["mc"])
assert len(eng.agent.chunk_meta) == 3
r = eng.ask("KV 压缩多少倍?", chat=False)
assert len(r.retrieved) >= 1
print("✅ test_ingest_and_retrieve")
def test_similar_text_retrieval():
eng = make_engine()
eng.ingest("吸收式 MLA 缓存 576 维潜在向量", ["method"])
eng.ingest("HiLS 分层软max 端到端块选择", ["method"])
eng.ingest("今天天气很好适合出去玩", ["life"])
# 查询与第一条相关
r = eng.ask("MLA 潜在向量维度是多少?", chat=False)
tops = [cid for cid, _ in r.retrieved]
assert 0 in tops, f"期望命中 chunk_0, 实际 {tops}"
print(f"✅ test_similar_text_retrieval (top: {tops[:3]})")
def test_forget_curve():
eng = make_engine()
eng.ingest("A", ["x"])
eng.ingest("B", ["x"])
eng.ingest("C", ["x"])
# 时间推进 (默认半衰期 100, 500 轮后 decay=2^-5=0.031)
for _ in range(500):
eng.now += 1
# 阈值设 0.1: strength = 0.5*0.031 + 0 ≈ 0.016 < 0.1 → 应遗忘
victims = eng.forget(0.1)
assert len(victims) >= 2, f"长时间不访问应遗忘, 实际 {len(victims)}"
print(f"✅ test_forget_curve (遗忘 {len(victims)}/3)")
def test_save_load():
eng = make_engine()
eng.ingest("持久化测试内容", ["test"])
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
path = f.name
eng.save(path)
eng2 = make_engine()
ok = eng2.load(path)
assert ok
assert len(eng2.agent.chunk_meta) == 1
os.unlink(path)
print("✅ test_save_load")
def test_hash_embedding_deterministic():
emb = HashEmbedding(dim=576)
v1 = emb.embed("同一句话")
v2 = emb.embed("同一句话")
v3 = emb.embed("完全不同的话")
assert (v1 == v2).all()
sim_same = float(v1 @ v2)
sim_diff = float(v1 @ v3)
assert sim_same > sim_diff, f"{sim_same} vs {sim_diff}"
print(f"✅ test_hash_embedding (同句相似度 {sim_same:.3f} > 异句 {sim_diff:.3f})")
def test_tool_exec():
eng = make_engine()
out = eng._exec_tool("print(6*7)")
assert "42" in out
bad = eng._exec_tool("print(undefined_var)")
assert "Traceback" in bad or "Error" in bad
print("✅ test_tool_exec")
def test_lfru_hysteresis():
"""LFRU 滞回驱逐: 历史热块获得保护, 信用随时间衰减 (colibrì #441/#497)"""
import numpy as np
from agentframe.core.quad import KVPager, CompressedKV
def mk(cid, max_heat, acc):
return CompressedKV(
chunk_id=cid, latent=np.zeros(576, dtype=np.float32),
quant_bits=4, heat=max_heat, size_bytes=1000,
importance=0.5, access_count=acc, max_heat=max_heat,
last_access=0)
# 冷却 200 轮 (2 个半衰期): 历史热块应被保护
p = KVPager(vram_limit_mb=0.006, ram_limit_mb=64)
for kv in [mk(0, 0.95, 0), mk(1, 0.4, 0), mk(2, 0.5, 0),
mk(3, 0.5, 0), mk(4, 0.5, 0)]:
p.vram[kv.chunk_id] = kv
p.vram_used += kv.size_bytes
victim = p.evict(200.0, "vram")
assert victim == 1, f"历史热块0应被保护, 实际驱逐 {victim}"
# 完全冷透 (2000 轮): 信用衰减殆尽, 照常驱逐
p2 = KVPager(vram_limit_mb=0.006, ram_limit_mb=64)
for kv in [mk(0, 0.95, 0), mk(1, 0.4, 0), mk(2, 0.5, 0),
mk(3, 0.5, 0), mk(4, 0.5, 0)]:
p2.vram[kv.chunk_id] = kv
p2.vram_used += kv.size_bytes
assert p2.evict(2000.0, "vram") is not None
print("✅ test_lfru_hysteresis (滞回保护 + 信用衰减)")
def test_tool_safety():
"""工具执行安全: 危险命令/超长代码应被拦截"""
eng = make_engine()
out = eng._exec_tool("import os; os.system('rm -rf /tmp/x')")
assert "安全拦截" in out, out
out2 = eng._exec_tool("import subprocess; subprocess.run(['shutdown'])")
assert "安全拦截" in out2, out2
print("✅ test_tool_safety (危险命令拦截)")
def test_api_auth():
"""API 认证: token 模式 + 回环模式"""
import os
from agentframe.api.server import AgentFrameAPI
from agentframe.config import AgentFrameConfig
os.environ["AGENTFRAME_STATE_DIR"] = "/tmp/af_test_auth"
cfg = AgentFrameConfig.from_env()
cfg.api.token = "test-token"
api = AgentFrameAPI(cfg)
client = api.app.test_client()
assert client.get("/v1/sessions").status_code == 401
ok = client.get("/v1/sessions",
headers={"Authorization": "Bearer test-token"})
assert ok.status_code == 200
print("✅ test_api_auth (Bearer 认证)")
def test_couple_prefetch():
"""跨轮共现预取: 学习共现 → 预测 → disk→RAM 提升 (colibrì couple 移植)"""
import numpy as np
from agentframe.core.couple import CouplePrefetcher
from agentframe.core.quad import KVPager, CompressedKV
def mk(cid):
return CompressedKV(chunk_id=cid, latent=np.zeros(576, dtype=np.float32),
quant_bits=4, heat=0.1, size_bytes=1000,
importance=0.5)
# 1. 共现学习: 轮1检索{0,1}, 轮2检索{1,2}, 轮3检索{2,3}
cp = CouplePrefetcher(top_k=8)
cp.record([0, 1])
cp.record([1, 2])
cp.record([2, 3])
# 轮2后: (0,2)共现1, (1,2)共现1; 轮3后: (1,2)再+1, (1,3)共现1, (2,3)共现1
assert cp.cooccur[0][2] == 1
assert cp.cooccur[1][2] == 2 # 轮2(1在prev,2在cur) + 轮3(1在prev,2在cur)
assert cp.cooccur[2][3] == 1
assert cp.cooccur[1][3] == 1
# 2. 预测: 当前检索{1} → 预测 2 (共现1)
pred = cp.predict([1])
assert 2 in pred, f"应预测到块2, 实际 {pred}"
assert 1 not in pred, "预测不应包含当前集合中的块"
print(f"✅ test_couple_prefetch (学习+预测: {pred[:4]})")
# 3. 预取落地: 块2在 disk, prefetch 后应到 RAM (不进 VRAM)
p = KVPager(vram_limit_mb=64, ram_limit_mb=0.006) # 6KB RAM
for cid in (0, 1, 3):
p.disk[cid] = mk(cid)
p.disk[2] = mk(2) # 目标块在 disk
moved = p.prefetch([2], now=1.0)
assert 2 in moved
assert 2 in p.ram and 2 not in p.vram and 2 not in p.disk
print("✅ test_couple_prefetch (预取 disk→RAM, 不占 VRAM)")
# 4. RAM 满时腾位: 塞满 RAM 后预取新块
p2 = KVPager(vram_limit_mb=64, ram_limit_mb=0.005) # 5KB RAM (最多5块)
for cid in range(5):
p2.ram[cid] = mk(cid)
p2.ram_used += 1000
p2.disk[99] = mk(99)
moved2 = p2.prefetch([99], now=1.0)
assert 99 in p2.ram
assert len(p2.ram) == 5 # 腾位后仍不超限
print("✅ test_couple_prefetch (RAM 满时 LFRU 腾位)")
def test_topk_protection():
"""Top-K 保护 (colibrì 接入): 路由命中块标记高精度, 检索不翻转"""
import numpy as np
from agentframe.core.quad import (AbsorbedMLA, ReversibleQuantizer,
LandmarkRouter)
# 1. 保护标记: 命中后 size 变 1152B (16bit), 未命中保持 352B (4bit)
m = AbsorbedMLA(n_layers=27, quant_bits=4, n_ch=16)
rng = np.random.default_rng(42)
latents = np.tanh(rng.normal(0, 1, (20, 576)).astype(np.float32))
for L in latents:
m.encode(L)
assert all(not kv.protected for kv in m.chunks.values())
m.protect_topk(0)
m.protect_topk(1)
assert m.chunks[0].protected and m.chunks[0].size_bytes == 1152
assert m.chunks[2].protected is False and m.chunks[2].size_bytes == 352
print("✅ test_topk_protection (16bit 保护标记 + 大小区分)")
# 2. 检索一致性: 原始 latent 构建摘要 vs INT4 解包摘要
router = LandmarkRouter(top_k=8, seed=42)
summaries_orig = {b: router.build_summary(latents[b].reshape(1, -1))
for b in range(20)}
flip_orig = 0
for q in range(10):
qv = latents[rng.integers(0, 20)]
s1 = router.route(qv, qv, summaries_orig)
flip_orig += 0 # 无损路径不翻转
# 3. 解包路径翻转率 (对照): 应显著高于无损路径
flip_deq = 0
summaries_deq = {}
for b in range(20):
q4, sc, _ = ReversibleQuantizer.quantize_int4(latents[b], n_ch=16)
deq = ReversibleQuantizer.dequant_int4(q4, sc, 576)
k, bias = router.build_summary(deq.reshape(1, -1))
summaries_deq[b] = (k, bias)
for q in range(10):
qv = latents[rng.integers(0, 20)]
s_orig = router.route(qv, qv, summaries_orig)
s_deq = router.route(qv, qv, summaries_deq)
if s_orig.chunk_ids != s_deq.chunk_ids:
flip_deq += 1
print(f" ✅ 无损路径 0 翻转 | INT4 解包路径 {flip_deq}/10 翻转")
assert flip_deq >= 1, "对照: INT4 解包路径应存在翻转 (证明保护必要性)"
print("✅ test_topk_protection (无损检索 0 翻转, 对照解包路径有翻转)")
def test_int4_packing():
"""真 INT4 打包 (colibrì quant.h 移植): 往返精度 + 真实压缩比"""
import numpy as np
from agentframe.core.quad import ReversibleQuantizer, AbsorbedMLA
rng = np.random.default_rng(42)
latent = np.tanh(rng.normal(0, 1, 576).astype(np.float32))
q4, scales, size = ReversibleQuantizer.quantize_int4(latent, n_ch=16)
deq = ReversibleQuantizer.dequant_int4(q4, scales, 576)
# 往返精度: 余弦相似度 > 0.99
sim = float(deq @ latent) / (np.linalg.norm(deq) * np.linalg.norm(latent))
assert sim > 0.99, f"INT4 往返相似度 {sim:.4f}"
# 真实压缩: 352B vs 原始 2304B
assert size == 288 + 64, f"打包大小 {size} != 352"
# 每 token 压缩比对齐 L40S 实测 28.4x
m = AbsorbedMLA(n_layers=27, quant_bits=4, n_ch=16)
ratio = 270 * 1024 / m.bytes_per_token()
assert ratio > 25, f"压缩比 {ratio:.1f}x 不足"
print(f"✅ test_int4_packing (cos={sim:.4f}, {ratio:.1f}x)")
def test_incremental_persist():
"""增量持久化 (colibrì kv_persist 移植): append + crash-safe 恢复"""
import tempfile, os
from agentframe.memory.incremental import IncrementalKVStore
path = tempfile.mktemp(suffix=".kv")
store = IncrementalKVStore(path)
# 追加 3 条
import numpy as np
n1 = store.append(0, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32),
np.zeros(576, dtype=np.float32), 4, 352, {"text": "A"})
n2 = store.append(1, np.ones(288, dtype=np.uint8), np.ones(16, dtype=np.float32) * 2,
np.ones(576, dtype=np.float32), 4, 352, {"text": "B"})
n3 = store.append(2, np.zeros(288, dtype=np.uint8), np.ones(16, dtype=np.float32),
np.zeros(576, dtype=np.float32), 4, 352, {"text": "C"})
assert n3 == 3
# 模拟崩溃: 写入半行垃圾
with open(path, "a") as f:
f.write('{"magic": "AFKV1", "chunk_id": 99, "q4": "zz')
recs = store.load()
assert len(recs) == 3, f"坏行应被跳过, 实际 {len(recs)}"
assert recs[1]["meta"]["text"] == "B"
print("✅ test_incremental_persist (append + crash-safe 坏行跳过)")
os.unlink(path)
def test_prefix_reuse():
"""查询前缀复用 (colibrì kv_prefix 移植)"""
eng = make_engine()
eng.ingest("KV 压缩 29 倍", ["kv"])
eng.ingest("LFRU 滞回驱逐", ["method"])
eng.ingest("Couple 预取", ["method"])
# 第一轮: 正常检索
r1 = eng.ask("KV 压缩是多少倍", chat=False)
first = r1.retrieved
# 第二轮: 相同前缀 → 复用
r2 = eng.ask("KV 压缩是多少倍呢", chat=False)
assert eng._prefix_hits == 1, "应触发前缀复用"
assert [c for c, _ in r2.retrieved] == [c for c, _ in first]
# 第三轮: 完全不同 → 不复用
r3 = eng.ask("今天天气怎么样", chat=False)
assert eng._prefix_hits == 1, "不同 query 不应复用"
print("✅ test_prefix_reuse (前缀命中复用, 不同 query 不复用)")
def test_incremental_engine():
"""引擎级增量恢复: ingest → enable → load_incremental 重建"""
import tempfile, os
path = tempfile.mktemp(suffix=".kv")
eng = make_engine()
eng.enable_incremental(path)
eng.ingest("增量持久化内容 A", ["a"])
eng.ingest("增量持久化内容 B", ["b"])
# 新引擎从日志恢复
eng2 = make_engine()
n = eng2.load_incremental(path)
assert n == 2, f"应恢复 2 块, 实际 {n}"
r = eng2.ask("增量持久化", chat=False)
assert len(r.retrieved) >= 1
os.unlink(path)
print("✅ test_incremental_engine (引擎级增量恢复 + 检索)")
if __name__ == "__main__":
test_ingest_and_retrieve()
test_similar_text_retrieval()
test_forget_curve()
test_save_load()
test_hash_embedding_deterministic()
test_tool_exec()
test_lfru_hysteresis()
test_tool_safety()
test_api_auth()
test_couple_prefetch()
test_int4_packing()
test_topk_protection()
test_incremental_persist()
test_prefix_reuse()
test_incremental_engine()
print("\n🎉 全部核心测试通过!")