ljsysfurry's picture
Upload folder using huggingface_hub
9c388a7 verified
Raw History Blame Contribute Delete
26.1 kB
"""
ContextEngine — AgentFrame 核心引擎
====================================
整合四层上下文保持 + LLM + Embedding:
L1 MetaCog 认知层: 任务分解 / 信息缺口 / 检索指令
L2 LandmarkRouter 路由层: landmark 摘要检索 top-k
L3 AbsorbedMLA 存储层: 吸收式 MLA 压缩 (L40S 实测 28.4x)
L4 KVPager 物理层: 三级换页 + Aura 遗忘曲线
对外接口 (引擎级, 与 API/CLI 解耦):
ingest(text, tags) → chunk_id
ask(query, chat=True) → AnswerResult
stats() → 状态 dict
forget(threshold) → 遗忘列表
save(path) / load(path) → 持久化
"""
import json
import numpy as np
from ..config import AgentFrameConfig, MemoryConfig
from ..llm import create_llm, LLMProvider
from ..embed import create_embedding, EmbeddingProvider
from ..memory.store import StateStore
from ..memory.incremental import IncrementalKVStore
from .quad import QuadLayerAgent
from .couple import CouplePrefetcher
from .tiers import TieredSummarizer
# DeepSeek 工具定义 (给 Agent 的"手")
RUN_PYTHON_TOOL = [{
"type": "function",
"function": {
"name": "run_python",
"description": "执行一段 Python 代码并返回 stdout/stderr。写代码前先调用此工具验证。",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的 Python 代码"}
},
"required": ["code"]
}
}
}]
class AnswerResult:
"""ask() 的返回"""
def __init__(self, answer: str, retrieved: list, scores: list,
context_dim: int, directive=None, meta: dict = None):
self.answer = answer
self.retrieved = retrieved # [(chunk_id, text_preview)]
self.scores = scores
self.context_dim = context_dim
self.directive = directive
self.meta = meta or {}
def to_dict(self) -> dict:
return {
"answer": self.answer,
"retrieved": self.retrieved,
"scores": self.scores,
"context_dim": self.context_dim,
"meta": self.meta,
}
MEMORY_DIRECTOR_SYSTEM = (
"你是记忆主管 (MemoryDirector)。分析一段对话, 只输出 JSON 记忆决策:\n"
"{\"remember\": [\"值得长期记住的新信息(具体、可复用)\", ...], "
"\"forget\": [\"已过时/无关紧要/可丢弃的信息\", ...], "
"\"promote_importance\": 0-1, \"reason\": \"一句话理由\"}\n"
"规则: remember 只收对未来有复用价值的事实(偏好/身份/参数/数据); "
"forget 收闲聊/一次性内容/已被替代信息; 宁可少记不可乱记; "
"promote_importance 表示这段对话整体重要性。"
)
# ============================================================
# 工具执行安全防护 (P0: LLM 生成代码不可信)
# ============================================================
# 危险命令黑名单 (子串匹配, 命中即拒绝执行)
DANGEROUS_PATTERNS = [
# 破坏性文件操作
"rm -rf", "rm -fr", "rmdir /", "mkfs", "dd if=", "shred",
"> /dev/sd", "mkfs.ext", "fdisk", "parted",
# 系统级操作
"shutdown", "reboot", "init 0", "init 6", "poweroff", "halt",
"systemctl stop", "systemctl disable", "kill -9 1",
# 提权/敏感读取
"sudo ", "su -", "chmod 777 /", "chown -R",
"cat /etc/shadow", "cat /etc/passwd", "cat ~/.ssh", "cat .ssh",
"cat /root/.ssh", "cat /root/.openclaw", "cat /etc/ssl/private",
"base64 -d /etc", "openssl genrsa", "ssh-keygen",
# 反弹 shell / 外联
"nc -e", "ncat -e", "bash -i >&", "/dev/tcp/", "curl | sh", "curl|sh", "wget | sh", "wget|sh",
"curl -o /etc", "wget -o /etc", "python -c 'import os; os.system",
# 挂载/防火墙/网络篡改
"mount ", "umount", "iptables", "nft ", "ip link set", "tcpdump",
]
MAX_TOOL_CODE_LEN = 4096 # 单次工具调用代码长度上限
class ToolExecutionError(Exception):
"""工具执行被安全策略拦截"""
pass
def _check_code_safety(code: str) -> None:
"""工具代码安全检查: 长度 + 危险模式黑名单"""
if len(code) > MAX_TOOL_CODE_LEN:
raise ToolExecutionError(f"代码过长 ({len(code)} > {MAX_TOOL_CODE_LEN} 字符), 已拦截")
for pat in DANGEROUS_PATTERNS:
if pat in code:
raise ToolExecutionError(f"命中危险模式 '{pat}', 已拦截")
class ContextEngine:
"""四层上下文保持引擎"""
def __init__(self, config: AgentFrameConfig = None, llm: LLMProvider = None,
embedder: EmbeddingProvider = None):
self.config = config or AgentFrameConfig.from_env()
m: MemoryConfig = self.config.memory
# 四层核心
self.agent = QuadLayerAgent(
n_layers=m.n_layers, quant_bits=m.quant_bits,
top_k=m.top_k, seed=m.seed, reversible=m.reversible,
vram_limit_mb=m.vram_limit_mb, ram_limit_mb=m.ram_limit_mb)
# 外部能力
self.llm = llm or create_llm(
provider=self.config.llm.provider,
api_key=self.config.llm.api_key,
base_url=self.config.llm.base_url,
model=self.config.llm.model,
fast_model=self.config.llm.fast_model,
timeout=self.config.llm.timeout)
self.embedder = embedder or create_embedding("hash", dim=self.agent.store.DIM)
# 跨轮共现预取 (colibrì couple_prefetch 移植)
self.couple = CouplePrefetcher(top_k=m.couple_k)
# 增量持久化 (colibrì kv_persist 移植): 每轮 append, 不重写全量
self.incr_store = None
self.incr_path = None
# 查询前缀复用 (colibrì kv_prefix 移植)
self._last_query = None
self._last_retrieved = []
self._prefix_hits = 0
# 会话状态
self.chat_history = []
self.max_history_turns = self.config.api.max_history_turns
self.now = 0.0
self._chunk_counter = 0
# ============ 摄入 ============
def ingest(self, text: str, tags: list = None, source: str = None) -> int:
"""摄入知识块 → 压缩进 KV 缓存"""
tags = tags or []
# 文本 → 向量 (真实嵌入)
vec = self.embedder.embed(text)
# 四层摄取: 用向量作为 hidden states 输入
kv = self.agent.store.encode(vec)
kv.heat = 0.9
# 新知识 = 热: force_hot 优先进 VRAM (修复 heat 设置无效问题)
self.agent.pager.place(kv, self.now, force_hot=True)
# 建 landmark 摘要
k_prime, bias = self.agent.router.build_summary(kv.latent.reshape(1, -1))
cid = kv.chunk_id
self.agent.summaries[cid] = (k_prime, bias)
# L0/L1/L2 分层摘要 (OpenViking 启发): 写入时生成, 读取时按需展开
tiers = TieredSummarizer.make_tiers(text)
self.agent.chunk_meta[cid] = {
"tags": tags, "text": text, "source": source or "",
"created_at": self.now,
"l0": tiers["l0"],
"l1": tiers["l1"],
"l2_len": tiers["l2_len"],
}
self._chunk_counter += 1
# 增量追加持久化: 新 chunk 立即落盘 (crash-safe)
if self.incr_store is not None:
self.incr_store.append(
cid, kv.q4, kv.scales, kv.latent, kv.quant_bits,
kv.size_bytes, self.agent.chunk_meta[cid])
return cid
def ingest_batch(self, chunks: list) -> list:
"""批量摄入: [{"text":..., "tags":[...]}, ...]"""
return [self.ingest(c["text"], c.get("tags", []), c.get("source"))
for c in chunks]
# ============ 查询 ============
def _exec_tool(self, code: str, timeout: int = 15) -> str:
"""执行 run_python 工具 (带安全防护)"""
import subprocess
import sys
try:
_check_code_safety(code)
except ToolExecutionError as e:
return f"[安全拦截] {e}"
try:
r = subprocess.run([sys.executable, "-c", code],
capture_output=True, text=True, timeout=timeout,
cwd="/tmp")
out = (r.stdout or "") + (("\n[stderr] " + r.stderr) if r.stderr else "")
if r.returncode != 0:
out = f"[退出码 {r.returncode}] " + out
return (out or "(无输出) 执行成功")[:800]
except subprocess.TimeoutExpired:
return f"[超时 >{timeout}s]"
except Exception as e:
return f"[执行异常] {e}"
def _llm_with_tools(self, system: str, user: str, max_rounds: int = 4,
thinking: dict = None, use_fast: bool = False) -> str:
"""带工具循环的 LLM 调用 (脑+手)"""
messages = [{"role": "system", "content": system},
{"role": "user", "content": user}]
for _ in range(max_rounds + 1):
resp = self.llm.chat(messages, max_tokens=2000, temperature=0.8,
thinking=thinking, tools=RUN_PYTHON_TOOL,
use_fast=use_fast)
tool_calls = resp.get("tool_calls")
if not tool_calls:
return resp.get("content") or ""
# 执行工具并回传
messages.append({"role": "assistant", "content": resp.get("content") or "",
"tool_calls": tool_calls})
for tc in tool_calls:
fn = tc["function"]
try:
args = json.loads(fn.get("arguments", "{}"))
except Exception:
args = {}
result = self._exec_tool(args.get("code", ""))
messages.append({"role": "tool", "tool_call_id": tc.get("id", ""),
"content": result})
return messages[-1].get("content", "") if messages else ""
def ask(self, query: str, chat: bool = True, use_fast: bool = False,
thinking: dict = None) -> AnswerResult:
"""查询 + 生成"""
# 1. 认知层: 分解 + 检索指令
subtasks = self.agent.metacog.decompose(query)
directive = self.agent.metacog.build_directive(subtasks[0], self.agent.chunk_meta)
# 1.5 查询前缀复用 (colibrì kv_prefix 移植):
# 新 query 与上一条共享长前缀 → 直接复用上次检索结果, 保持热块
reuse = self._prefix_reuse(query)
if reuse is not None:
selection = reuse
else:
# 2. 路由层: landmark 检索 (认知层 directive 的 required_chunks 参与加权)
q_vec = self.embedder.embed(query)
selection = self.agent.router.route(
q_vec, q_vec, self.agent.summaries,
boost_chunks=directive.required_chunks)
self._last_query = query
self._last_retrieved = list(selection.chunk_ids)
# 2.5 跨轮共现预取: 记录本轮检索 + 预测下轮 → 提升冷块到 RAM
self.couple.record(selection.chunk_ids)
predicted = self.couple.predict(selection.chunk_ids)
if predicted:
self.agent.pager.prefetch(predicted, self.now)
# 3. 物理层: 访问更新 (遗忘曲线刷新) + Top-K 高精度保护
for cid, score in zip(selection.chunk_ids, selection.scores):
self.agent.pager.access(cid, abs(score), self.now)
self.agent.metacog.promote(cid, abs(score))
# Top-K 保护 (colibrì): 路由命中的块标记 16bit 高精度, 防量化翻转
self.agent.store.protect_topk(cid)
self.now += 1
# 4. 存储层: 聚合检索结果
gathered, hit_ids = [], []
for cid in selection.chunk_ids:
kv = (self.agent.pager.vram.get(cid)
or self.agent.pager.ram.get(cid)
or self.agent.pager.disk.get(cid))
if kv is not None:
gathered.append(kv.latent)
meta = self.agent.chunk_meta.get(cid, {})
hit_ids.append((cid, meta.get("text", "")[:40]))
context_dim = len(gathered[0]) if gathered else 0
# 5. 生成回答
answer = ""
if chat:
# 分层上下文 (OpenViking L0/L1 启发): 速览 + 概述, 比全文省 token
ctx_chunks = [(cid, self.agent.chunk_meta.get(cid, {}))
for cid in selection.chunk_ids]
ctx_block = TieredSummarizer.build_tiered_context(ctx_chunks)
system = (
"你是 AgentFrame 上下文保持系统。下面是从压缩缓存中检索到的知识块,"
"回答时优先使用这些知识。如果知识不足就如实说明。"
f"\n\n【检索到的知识】\n{ctx_block}")
messages = [{"role": "system", "content": system}]
messages.extend(self.chat_history[-self.max_history_turns * 2:])
messages.append({"role": "user", "content": query})
resp = self.llm.chat(messages, max_tokens=self.config.llm.max_tokens,
temperature=self.config.llm.temperature,
use_fast=use_fast)
answer = resp.get("content") or ""
# 更新会话历史 (老对话由压缩系统兜底)
self.chat_history.append({"role": "user", "content": query})
self.chat_history.append({"role": "assistant", "content": answer})
if len(self.chat_history) > self.max_history_turns * 2:
self.chat_history = self.chat_history[-self.max_history_turns * 2:]
return AnswerResult(
answer=answer,
retrieved=hit_ids,
scores=[round(float(s), 4) for s in selection.scores[:len(hit_ids)]],
context_dim=context_dim,
directive=directive,
meta={"now": self.now, "chunks": len(self.agent.chunk_meta)})
# ============ 前缀复用 (colibrì kv_prefix 移植) ============
def _prefix_reuse(self, query: str):
"""
查询前缀复用: 新 query 与上一条共享长前缀 (前 70%+ 且 ≥8 字符)
→ 直接复用上次检索结果 (跳过重新路由), 并保持上次热块不冷却。
返回 ChunkSelection 或 None (不复用)。
colibrì 对应: fed[] 序列前缀匹配, 跳过重复 prefill。
"""
if self._last_query is None or not self._last_retrieved:
return None
prev, cur = self._last_query, query
if len(cur) < 8 or len(prev) < 8:
return None
# 公共前缀长度
common = 0
for a, b in zip(prev, cur):
if a != b:
break
common += 1
# 前缀须覆盖旧 query 的 70% 且新 query 的 50% (防过度复用)
if common >= len(prev) * 0.7 and common >= len(cur) * 0.5:
# 保持上次热块: 触碰一次 (刷新遗忘曲线)
for cid in self._last_retrieved:
kv = (self.agent.pager.vram.get(cid)
or self.agent.pager.ram.get(cid)
or self.agent.pager.disk.get(cid))
if kv is not None:
self.agent.pager.access(cid, 0.5, self.now)
self._prefix_hits += 1
from .quad import ChunkSelection
return ChunkSelection(
chunk_ids=self._last_retrieved,
scores=[0.5] * len(self._last_retrieved),
weights=[1.0 / len(self._last_retrieved)] * len(self._last_retrieved))
return None
# ============ 增量持久化 (colibrì kv_persist 移植) ============
def enable_incremental(self, path: str):
"""开启增量持久化: 新 chunk 追加到日志, 重启可增量恢复"""
self.incr_path = path
self.incr_store = IncrementalKVStore(path)
return self
def load_incremental(self, path: str = None) -> int:
"""
从增量日志恢复所有 chunk (colibrì: 重启恢复, 不用重新 prefill)。
重建 chunk_meta + summaries + pager. 返回恢复的 chunk 数。
"""
path = path or self.incr_path
if not path:
return 0
store = IncrementalKVStore(path)
if not store.exists():
return 0
recs = store.load()
restored = 0
for rec in recs:
cid = rec["chunk_id"]
# 跳过已存在的
if cid in self.agent.chunk_meta:
continue
from .quad import CompressedKV
kv = CompressedKV(
chunk_id=cid,
latent=rec["latent"],
quant_bits=rec["quant_bits"],
size_bytes=rec["size_bytes"],
q4=rec["q4"],
scales=rec["scales"],
)
kv.heat = 0.5
self.agent.chunk_meta[cid] = rec["meta"]
# 重建 landmark 摘要
k_prime, bias = self.agent.router.build_summary(
rec["latent"].reshape(1, -1))
self.agent.summaries[cid] = (k_prime, bias)
self.agent.pager.place(kv, self.now)
self._chunk_counter = max(self._chunk_counter, cid + 1)
restored += 1
return restored
# ============ 工具增强版 ask ============
def ask_with_hands(self, query: str, thinking: dict = None) -> AnswerResult:
"""带工具循环的查询 (Agent 可自行执行代码验证)"""
result = self.ask(query, chat=False)
ctx_chunks = [(cid, self.agent.chunk_meta.get(cid, {}))
for cid, _preview in result.retrieved]
ctx_block = TieredSummarizer.build_tiered_context(ctx_chunks)
system = (
"你是 AgentFrame 上下文保持系统。下面是检索到的知识块。"
"回答时优先使用这些知识,知识不足就如实说明。"
"你有 run_python 工具可以执行代码验证你的方案。"
f"\n\n【检索到的知识】\n{ctx_block}")
answer = self._llm_with_tools(system, query, thinking=thinking)
return AnswerResult(answer=answer, retrieved=result.retrieved,
scores=result.scores, context_dim=result.context_dim,
meta={"hands": True, "chunks": len(self.agent.chunk_meta)})
# ============ 自主记忆管理 (MemoryDirector) ============
def _parse_memory_decision(self, raw: str) -> dict:
"""解析 LLM 输出的记忆决策 JSON (容错处理)"""
import re
m = re.search(r'\{.*\}', raw, re.DOTALL)
if not m:
return {"remember": [], "forget": [], "promote_importance": 0.5, "reason": ""}
try:
return json.loads(m.group(0))
except Exception:
return {"remember": [], "forget": [], "promote_importance": 0.5, "reason": ""}
def autopilot_memory(self, query: str, answer: str) -> dict:
"""
模型自主记忆管理: 分析一轮对话, 自动决定记什么/忘什么.
返回决策摘要.
"""
conversation = f"用户: {query}\n助手: {answer[:800]}"
resp = self.llm.chat(
[{"role": "system", "content": MEMORY_DIRECTOR_SYSTEM},
{"role": "user", "content": f"对话:\n{conversation}"}],
max_tokens=600, temperature=0.3, use_fast=False)
decision = self._parse_memory_decision(resp.get("content") or "")
# 执行决策
remembered = []
forgotten = []
# 1. remember → 摄入新知识块 (先去重: 与已有块相似则跳过)
for item in decision.get("remember", []):
if not (isinstance(item, str) and item.strip()):
continue
# 去重: 检查是否已有高相似度块
dup = False
new_vec = self.embedder.embed(item.strip())
for cid, meta in self.agent.chunk_meta.items():
old_vec = self.embedder.embed(meta.get("text", ""))
sim = float(new_vec @ old_vec)
if sim > 0.8: # 余弦相似度阈值
dup = True
break
if dup:
continue
cid = self.ingest(item.strip(), tags=["auto"])
remembered.append((cid, item.strip()))
# 2. forget → 按文本匹配降低重要性/删除
forget_list = decision.get("forget", [])
if forget_list:
for cid, meta in list(self.agent.chunk_meta.items()):
text = meta.get("text", "")
for f_item in forget_list:
if isinstance(f_item, str) and f_item and \
(f_item in text or text in f_item):
kv = (self.agent.pager.vram.get(cid)
or self.agent.pager.ram.get(cid)
or self.agent.pager.disk.get(cid))
if kv:
kv.importance *= 0.3 # 大幅降权
kv.heat = 0.1
forgotten.append((cid, f_item))
# 3. promote_importance → 记录整体重要性
imp = float(decision.get("promote_importance", 0.5))
self._last_importance = imp
return {
"remembered": remembered,
"forgotten": forgotten,
"importance": imp,
"reason": decision.get("reason", ""),
}
def ask_autopilot(self, query: str) -> AnswerResult:
"""查询 + 自主记忆管理 (一步到位)"""
result = self.ask(query, chat=True)
decision = self.autopilot_memory(query, result.answer)
result.meta["memory_decision"] = decision
return result
# ============ 遗忘 / 状态 / 持久化 ============
def forget(self, threshold: float = 0.15) -> list:
"""按遗忘曲线清理弱记忆 (同步更新内存计数, 防泄漏)"""
victims = []
for layer_name in ["disk", "ram", "vram"]:
pool = getattr(self.agent.pager, layer_name)
for cid, kv in list(pool.items()):
strength = self.agent.pager.curve.strength(kv, self.now)
if strength < threshold:
victims.append(cid)
del pool[cid]
# 同步扣减占用计数 (vram_used/ram_used), 防漂移
if layer_name == "vram":
self.agent.pager.vram_used -= kv.size_bytes
elif layer_name == "ram":
self.agent.pager.ram_used -= kv.size_bytes
self.agent.chunk_meta.pop(cid, None)
self.agent.summaries.pop(cid, None)
return victims
def stats(self) -> dict:
return {
"chunks_total": len(self.agent.chunk_meta),
"layers": self.agent.pager.stats(),
"bytes_per_token": round(self.agent.store.bytes_per_token(), 2),
"chat_history_turns": len(self.chat_history) // 2,
"now": self.now,
"compression": "吸收式MLA + INT4 = 28.4x (L40S 真实推理实测)",
}
def save(self, path: str):
"""保存引擎状态"""
store = StateStore(path)
state = {
"chunk_meta": self.agent.chunk_meta,
"summaries": self.agent.summaries,
"chat_history": self.chat_history,
"now": self.now,
"counter": self._chunk_counter,
# pager 状态
"vram": self.agent.pager.vram,
"ram": self.agent.pager.ram,
"disk": self.agent.pager.disk,
}
store.save(state)
def load(self, path: str):
"""恢复引擎状态"""
store = StateStore(path)
state = store.load()
if not state:
return False
self.agent.chunk_meta = {int(k): v for k, v in state.get("chunk_meta", {}).items()}
# JSON 序列化后 (k_prime, bias) 变 list, 转回 tuple
self.agent.summaries = {int(k): (v[0], v[1]) if isinstance(v, list) and len(v) == 2 else v
for k, v in state.get("summaries", {}).items()}
self.chat_history = state.get("chat_history", [])
self.now = state.get("now", 0.0)
self._chunk_counter = state.get("counter", 0)
# 反序列化: dict → CompressedKV
from .quad import CompressedKV
for layer_name in ["vram", "ram", "disk"]:
raw = state.get(layer_name, {})
pool = {}
for cid, d in raw.items():
if isinstance(d, CompressedKV):
pool[int(cid)] = d
elif isinstance(d, dict):
kv = CompressedKV(
chunk_id=int(d.get("chunk_id", int(cid))),
latent=np.asarray(d.get("latent", [0.0]), dtype=np.float32),
quant_bits=int(d.get("quant_bits", 4)),
heat=float(d.get("heat", 0.5)),
last_access=float(d.get("last_access", 0.0)),
size_bytes=int(d.get("size_bytes", 0)),
importance=float(d.get("importance", 0.5)),
access_count=int(d.get("access_count", 0)),
protected=bool(d.get("protected", False)),
reversible=bool(d.get("reversible", False)),
sector_id=int(d.get("sector_id", -1)),
block_id=int(d.get("block_id", -1)),
module_id=int(d.get("module_id", -1)),
)
vh = d.get("value_hist")
if isinstance(vh, np.ndarray):
kv.value_hist = vh
pool[int(cid)] = kv
setattr(self.agent.pager, layer_name, pool)
return True