feat: fold sampling parameters into the cache key

Without this, five seeds over identical messages all hit the first cached
response and the reported standard deviation is silently always zero.
This commit is contained in:
2026-07-31 21:20:56 -04:00
parent 6bb64ca938
commit c31cc1adad
2 changed files with 80 additions and 3 deletions
+25 -3
View File
@@ -20,6 +20,8 @@ from loguru import logger
from polygateway.types import ChatRequest, LLMResponse
if TYPE_CHECKING:
from collections.abc import Mapping
from polygateway.ports import CacheBackend, CallNext, StructuredOutputStrategy
_KEY_PREFIX = "pgw:cache:"
@@ -50,9 +52,19 @@ def _digest_part(part: Any) -> Any:
def build_cache_key(
model_fingerprint: str, messages: list[dict[str, Any]], namespace: str, salt: str | None
model_fingerprint: str,
messages: list[dict[str, Any]],
namespace: str,
salt: str | None,
*,
sampling: Mapping[str, Any] | None = None,
) -> str:
"""缓存 key 公式;salt 仅非 None 时参与(VT 旧键语义: 不传 salt 键形不变)。"""
"""缓存 key 公式;salt 仅非 None 时参与(VT 旧键语义: 不传 salt 键形不变)。
`sampling` 仅**非空**时参与(与 salt 的"仅非 None"不同——空串是有意义的
salt,而空采样参数与不传无语义差别)。它必须进 key: 否则同 messages 跑 5 个
seed 会全部命中第一次的响应,标准差恒为 0 且不报错(issue #4 决策 C)。
"""
key_obj: dict[str, Any] = {
"model": model_fingerprint,
"messages": digest_messages(messages),
@@ -60,6 +72,8 @@ def build_cache_key(
}
if salt is not None:
key_obj["salt"] = salt
if sampling:
key_obj["sampling"] = dict(sampling)
payload = json.dumps(key_obj, sort_keys=True, ensure_ascii=False)
return _KEY_PREFIX + hashlib.sha256(payload.encode("utf-8")).hexdigest()
@@ -92,7 +106,15 @@ class CacheMW:
async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse:
namespace = request.cache_namespace or self._namespace
key = build_cache_key(self._fingerprint, request.messages, namespace, request.cache_salt)
# 读 sampling 而非 overlay: 语义明确,且不依赖"CacheMW 恰在 StructuredMW
# 外侧"这一层序巧合——结构化注入不该改变缓存身份(设计决策 C)
key = build_cache_key(
self._fingerprint,
request.messages,
namespace,
request.cache_salt,
sampling=request.sampling,
)
cached = await self._safe_get(key)
if cached is not None:
hit = self._rehydrate(cached, request)