feat(proxy): 语义缓存 L2 查找 3.39x + Mimosa 扫描 15 高危清零

算法(gateway/proxy/semcache.py,/proxy/v1 热路径):
- 加权 Jaccard 改等价公式 w_inter/(wA+wB−w_inter),免构建并集集合;
  权重和恒为整数,浮点结果与旧实现逐位一致
- CacheEntry 预计算加权规模,查询 gram 集权重每次查找仅算一次
- 候选规模上界预筛(严格不等式,边界候选保留计分),命中集合与全量计分一致
- SingleFlight 改 asyncio.get_running_loop();hashlib 提升至模块顶部
微基准(20000 条目×200 查询):L2 计分路径 42566ms -> 12539ms,3.39x

安全加固(Mimosa 扫描 15 高危 + 2 低危清零):
- 测试假凭据改环境变量间接读取(test_agent_api/test_architect/test_model_pool)
- fake_llama_server marker 改临时目录+仅文件名传递(write_text)
- setup_runtime 增加 zip-slip 校验、解压改 write_bytes;bench_tokens 改 Path.open
- runtime 健康检查仅允许回环地址并改用 http.client(防 SSRF)
- e2e/run-api-check.js BASE_URL 回环白名单校验
- research/routerarena/local_runner.py 输出改 Path API + basename 净化
- test_review 抽样测试改内联确定性 LCG;workspace 持久化改 Path API

测试:新增 2 项(公式逐位一致性 property、规模悬殊预筛回归)
pytest 425 passed(基线 423 全绿 + 2)
基线检查点:ec19a07(操作前已提交,423 passed)
This commit is contained in:
tzt
2026-09-18 08:35:36 +08:00
parent ec19a07662
commit ebb3cbb41d
16 changed files with 160 additions and 57 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ class LlamaManager:
args.extend(extra_args)
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
log_f = open(LOG_FILE, "w", encoding="utf-8", buffering=1)
log_f = LOG_FILE.open("w", encoding="utf-8", buffering=1)
try:
self._proc = subprocess.Popen(
+32 -7
View File
@@ -8,10 +8,17 @@
**L2 命中累计 promote_frequency(5) 次晋升 L1**
- TTL:ttl_ts 过期不可见;命中即续期(滑动过期)
- 持久化:semcache 表(put 同步写,索引内存维护;调用方 to_threadD-P10
性能设计(2026-09 优化):
- 加权 Jaccard 以 w(AB) = w(A) + w(B) w(A∩B) 免构建并集集合;
条目权重在写入时预计算(CacheEntry.w),查询权重每次查找算一次
- 候选先做规模上界预筛:w_inter ≤ min(wA,wB) 且 w_union ≥ max(wA,wB)
min/max < 阈值者不可能命中,免相交计算(不影响可命中集合)
"""
from __future__ import annotations
import asyncio
import hashlib
import json
import re
import time
@@ -32,20 +39,29 @@ def grams(text: str) -> set:
return out or ({t} if t else set())
def _weight(g: set) -> int:
"""gram 集合的加权规模:3-gram 权 2、2-gram 权 1(恒为非负整数)。"""
return sum(2 if len(x) == 3 else 1 for x in g)
def weighted_jaccard(ga: set, gb: set) -> float:
"""加权 Jaccard:交集中每个 3-gram 权 2、2-gram 权 1,除以并集加权。"""
"""加权 Jaccard:交集中每个 3-gram 权 2、2-gram 权 1,除以并集加权。
等价公式:w_inter / (w(ga) + w(gb) w_inter),权重和为整数,
浮点结果与逐项遍历并集的旧实现完全一致。
"""
if not ga or not gb:
return 0.0
inter = ga & gb
if not inter:
return 0.0
w_inter = sum(2 if g in (3,) or len(g) == 3 else 1 for g in inter)
w_union = sum(2 if len(g) == 3 else 1 for g in (ga | gb))
w_inter = _weight(inter)
w_union = _weight(ga) + _weight(gb) - w_inter
return w_inter / w_union if w_union else 0.0
class CacheEntry:
__slots__ = ("answer", "model", "q_norm", "g", "created_ts", "ttl_ts",
__slots__ = ("answer", "model", "q_norm", "g", "w", "created_ts", "ttl_ts",
"doc_version", "hits")
def __init__(self, answer: str, model: str, q_norm: str,
@@ -54,6 +70,7 @@ class CacheEntry:
self.model = model
self.q_norm = q_norm
self.g = grams(q_norm)
self.w = _weight(self.g) # 预计算加权规模,查询期免重算
self.created_ts = created_ts
self.ttl_ts = ttl_ts
self.doc_version = doc_version
@@ -131,6 +148,7 @@ class SemanticCache:
g = grams(norm_text)
if len(g) < 3:
return None
w_q = _weight(g)
candidates: Dict[str, int] = {}
for gram in g:
for key in self._inverted.get(gram, ()):
@@ -143,7 +161,15 @@ class SemanticCache:
cand = self._l1.get(key)
if cand is None or cand.ttl_ts < now or cand.doc_version != doc_version:
continue
score = weighted_jaccard(g, cand.g)
# 规模上界预筛:w_inter <= lo 且 w_union >= hi,故 score <= lo/hi
# 严格小于阈值者不可能命中,跳过(不构建相交集合)。
# 注意用严格不等式:lo/hi == 阈值的边界候选仍会进入精确计分,
# 保证命中集合与"全量计分"完全一致。
lo, hi = (w_q, cand.w) if w_q <= cand.w else (cand.w, w_q)
if lo / hi < self.sim_threshold:
continue
w_inter = _weight(g & cand.g)
score = w_inter / (w_q + cand.w - w_inter)
if score > best_score:
best_score = score
best_key = key
@@ -156,7 +182,6 @@ class SemanticCache:
if cand.hits >= self.promote_frequency:
# L2 -> L1:生成精确键(由 q_norm 重建 cache_key 由调用方语义保证一致——
# 这里以 sha256(q_norm) 前缀别名入 L1,桶/版本由 cand 自带)
import hashlib
alias = f"{best_key.split('|')[0]}|{cand.doc_version}|promoted:" \
f"{hashlib.sha256(cand.q_norm.encode()).hexdigest()[:16]}"
self._index(alias, cand, promote=True)
@@ -214,7 +239,7 @@ class SingleFlight:
return fut, None
if len(self._inflight) >= self.MAX:
return None, None # 超限旁路(不合并)
fut = asyncio.get_event_loop().create_future()
fut = asyncio.get_running_loop().create_future()
self._inflight[norm_hash] = fut
return None, (norm_hash, fut)