feat(adapters): 实现 RedisResponseCache

content-addressed sha256 缓存键,Redis 不可用时静默降级。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 22:39:42 -04:00
parent d79d67b1d3
commit 9dae7dda98
2 changed files with 231 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
"""RedisResponseCache 单元测试。"""
from __future__ import annotations
import pytest
from adapters.redis_cache import RedisResponseCache
from core.types import LLMResponse
@pytest.fixture()
def fake_redis():
"""创建 fakeredis 异步实例。"""
try:
import fakeredis.aioredis
except ImportError:
pytest.skip("fakeredis not installed")
return fakeredis.aioredis.FakeRedis(decode_responses=True)
@pytest.fixture()
def sample_response() -> LLMResponse:
"""构造一个标准测试用 LLMResponse。"""
return LLMResponse(
content="你好世界",
thinking="思考过程",
model="gpt-4o",
provider="openai",
prompt_tokens=10,
completion_tokens=5,
latency_ms=120,
ttft_ms=45.0,
max_inter_token_ms=12.3,
cache_hit=False,
call_id="test-call-001",
)
MESSAGES = [{"role": "user", "content": "你好"}]
class _BrokenRedis:
"""模拟 Redis 连接异常的假实现。"""
async def get(self, _key: str) -> None:
raise ConnectionError("redis down")
async def set(self, _key: str, _value: str, **_kw: object) -> None:
raise ConnectionError("redis down")
# ── 测试用例 ──────────────────────────────────────────────
@pytest.mark.asyncio
async def test_cache_miss_returns_none(fake_redis: object) -> None:
"""缓存未命中时返回 None。"""
cache = RedisResponseCache(redis=fake_redis, ttl_s=60)
result = await cache.get("gpt-4o", MESSAGES)
assert result is None
@pytest.mark.asyncio
async def test_cache_roundtrip(
fake_redis: object, sample_response: LLMResponse
) -> None:
"""set 后 get 应返回相同内容。"""
cache = RedisResponseCache(redis=fake_redis, ttl_s=300)
await cache.set("gpt-4o", MESSAGES, sample_response)
result = await cache.get("gpt-4o", MESSAGES)
assert result is not None
assert result.content == sample_response.content
assert result.thinking == sample_response.thinking
assert result.model == sample_response.model
assert result.provider == sample_response.provider
assert result.prompt_tokens == sample_response.prompt_tokens
assert result.completion_tokens == sample_response.completion_tokens
assert result.latency_ms == sample_response.latency_ms
assert result.ttft_ms == sample_response.ttft_ms
assert result.max_inter_token_ms == sample_response.max_inter_token_ms
assert result.cache_hit == sample_response.cache_hit
assert result.call_id == sample_response.call_id
@pytest.mark.asyncio
async def test_different_messages_different_keys(
fake_redis: object, sample_response: LLMResponse
) -> None:
"""不同 messages 应产生不同缓存键。"""
cache = RedisResponseCache(redis=fake_redis, ttl_s=300)
messages_a = [{"role": "user", "content": "问题A"}]
messages_b = [{"role": "user", "content": "问题B"}]
await cache.set("gpt-4o", messages_a, sample_response)
assert await cache.get("gpt-4o", messages_a) is not None
assert await cache.get("gpt-4o", messages_b) is None
@pytest.mark.asyncio
async def test_different_models_different_keys(
fake_redis: object, sample_response: LLMResponse
) -> None:
"""不同 model 应产生不同缓存键。"""
cache = RedisResponseCache(redis=fake_redis, ttl_s=300)
await cache.set("gpt-4o", MESSAGES, sample_response)
assert await cache.get("gpt-4o", MESSAGES) is not None
assert await cache.get("claude-3-opus", MESSAGES) is None
@pytest.mark.asyncio
async def test_graceful_degradation_on_error() -> None:
"""Redis 不可用时静默降级:get→None,set→不报错。"""
cache = RedisResponseCache(redis=_BrokenRedis(), ttl_s=60)
response = LLMResponse(
content="x",
thinking="",
model="m",
provider="p",
prompt_tokens=0,
completion_tokens=0,
latency_ms=0,
ttft_ms=None,
max_inter_token_ms=None,
cache_hit=False,
call_id="broken-test",
)
# get 应返回 None 而非抛异常
assert await cache.get("m", MESSAGES) is None
# set 应静默吞掉异常
await cache.set("m", MESSAGES, response)