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:
@@ -8,10 +8,17 @@
|
||||
**L2 命中累计 promote_frequency(5) 次晋升 L1**
|
||||
- TTL:ttl_ts 过期不可见;命中即续期(滑动过期)
|
||||
- 持久化:semcache 表(put 同步写,索引内存维护;调用方 to_thread,D-P10)
|
||||
|
||||
性能设计(2026-09 优化):
|
||||
- 加权 Jaccard 以 w(A∪B) = 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user