Semantic Cache¶
This guide shows how to add semantic caching to a Google ADK agent so that near-duplicate prompts return a cached LLM response instead of making a new call.
For the concepts behind semantic caching, see Semantic Caching.
Option A: Self-hosted with RedisVL¶
Use RedisVLCacheProvider when you run your own Redis instance and want full
control over the vectorizer and cache index.
Prerequisites¶
- Redis 8.4+ running locally (see Redis setup).
pip install 'adk-redis[search]'
Setup¶
from google.adk import Agent
from redisvl.utils.vectorize import HFTextVectorizer
from adk_redis import (
LLMResponseCache,
LLMResponseCacheConfig,
RedisVLCacheProvider,
RedisVLCacheProviderConfig,
create_llm_cache_callbacks,
)
# 1. Create a vectorizer (runs locally, no API key needed)
vectorizer = HFTextVectorizer(model="redis/langcache-embed-v1")
# 2. Create the cache provider
provider = RedisVLCacheProvider(
config=RedisVLCacheProviderConfig(
redis_url="redis://localhost:6379",
name="my_cache",
ttl=3600,
distance_threshold=0.1,
),
vectorizer=vectorizer,
)
# 3. Create the cache and wire callbacks into the agent
llm_cache = LLMResponseCache(
provider=provider,
config=LLMResponseCacheConfig(first_message_only=True),
)
before_cb, after_cb = create_llm_cache_callbacks(llm_cache)
agent = Agent(
model="gemini-2.0-flash",
name="cached_agent",
before_model_callback=before_cb,
after_model_callback=after_cb,
)
See the semantic_cache example for a runnable version.
Attaching to a pre-provisioned index¶
By default the provider creates the cache index, which needs FT.CREATE plus
an FT.INFO probe to detect an existing one. Where a platform team
provisions the index out of band and the agent credential is denied index
management, set create_index=False to attach to it instead. Requires
redisvl>=0.26.0.
provider = RedisVLCacheProvider(
config=RedisVLCacheProviderConfig(
redis_url="redis://cache-user:password@redis.internal:6379",
name="my_cache", # must match the pre-provisioned index
ttl=3600,
distance_threshold=0.1,
create_index=False,
),
vectorizer=vectorizer,
)
Provisioning the index¶
RedisVL derives the schema from the cache name and the vectorizer. For
name="my_cache" over a 768 dimension vectorizer, provision the equivalent
of:
FT.CREATE my_cache ON HASH PREFIX 1 my_cache SCHEMA
prompt TEXT response TEXT inserted_at NUMERIC updated_at NUMERIC
prompt_vector VECTOR FLAT 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE
The index prefix is the bare name while entry keys are my_cache:<entry_id>.
So a credential's key pattern must cover my_cache*, not my_cache:*, and a
second cache whose name extends this one, such as my_cache_v2, is covered by
this index and returns the other cache's entries as hits. Give each cache a
name that is not a prefix of another.
The attached index is not validated
RedisVL checks nothing about an index it did not create (verified on redisvl 0.26.0). Which mismatches are loud depends on the field:
- Wrong or absent
name:check()raisesRedisSearchErroron the first lookup. - Wrong vector dimensions, key prefix, storage type, or distance metric: silent. Entries are written and TTLs set, every lookup misses, and the cache stops saving model calls without erroring.
Confirm the index with FT.INFO <name> from a credential allowed to run
it before deploying.
Minimum ACL¶
Measured on Redis 8.4, where FT.SEARCH carries both @read and @search
while FT.CREATE and FT.INFO carry only @search. So
+@read +@write ~my_cache* covers the entire runtime path and only index
creation needs @search: store() issues HSET and EXPIRE, check()
issues FT.SEARCH plus one EXPIRE per hit to refresh its TTL,
delete_by_id() issues UNLINK, and clear() scans the key prefix and
deletes. Three caveats:
- Because
check()refreshes TTLs, the read path needs@writetoo. A read-only credential fails on lookup, not just on write. - On Redis Open Source 7.x with the RediSearch module, and on Redis Software
or Redis Cloud databases earlier than 8.2, module commands belong to no
category, so the grant must name the command:
+@read +@write +FT.SEARCH. - A
redis_urlnaming a non-zero database also needs+SELECT, which is in@connection. Keep the cache on database 0 or grant it explicitly.
An ACL that revokes search explicitly, +@read +@write -@search, loses
FT.SEARCH as well: entries still store, but every lookup fails and the
cache never serves a hit.
Seeing failures while developing¶
A cache backend failure is logged and the turn proceeds, so a broken cache
costs latency rather than breaking the agent. That also means a
misconfiguration can read as "caching just isn't working". While developing,
set ignore_errors=False to raise instead:
llm_cache = LLMResponseCache(
provider=provider,
config=LLMResponseCacheConfig(ignore_errors=False),
)
Misconfiguration is still loud by default: a bad redis_url, a schema
mismatch, a denied FT.INFO, or a too-old redisvl all raise when the
provider is constructed. Only runtime lookup and store failures are
absorbed. To turn up the logs instead, raise the verbosity of the
adk_redis.cache logger.
Clearing entries¶
RedisVL refuses clear() and delete() on an index it does not manage, so
clear() deletes entries by scanning the <name>: key prefix instead. It
issues no index command and never drops the index; delete_by_id() is
unaffected. Neither is a barrier, because SCAN takes no snapshot, so an
entry written concurrently may survive a clear().
Option B: Managed with LangCache¶
Use LangCacheProvider with
Redis LangCache for a fully managed service. No
local vectorizer or Redis instance needed; embeddings are handled server-side.
Prerequisites¶
- A LangCache account and cache ID (sign up at redis.io/langcache).
pip install 'adk-redis[langcache]'
Setup¶
from google.adk import Agent
from adk_redis import (
LLMResponseCache,
LLMResponseCacheConfig,
LangCacheProvider,
LangCacheProviderConfig,
create_llm_cache_callbacks,
)
provider = LangCacheProvider(
config=LangCacheProviderConfig(
cache_id="your-cache-id",
api_key="your-api-key",
server_url="https://aws-us-east-1.langcache.redis.io",
ttl=3600,
),
)
llm_cache = LLMResponseCache(
provider=provider,
config=LLMResponseCacheConfig(first_message_only=False),
)
before_cb, after_cb = create_llm_cache_callbacks(llm_cache)
agent = Agent(
model="gemini-2.0-flash",
name="langcache_agent",
before_model_callback=before_cb,
after_model_callback=after_cb,
)
See the langcache_cache example for a runnable version.
Configuration options¶
| Option | Provider | Default | Description |
|---|---|---|---|
distance_threshold |
Both | 0.1 (RedisVL), None (LangCache) |
Max vector distance for a cache hit (lower = stricter). LangCache applies its server-side default when unset |
ttl |
Both | 3600 (RedisVL), None (LangCache) |
Time-to-live in seconds for cache entries |
name |
RedisVL | adk_semantic_cache |
Redis index name |
redis_url |
RedisVL | redis://localhost:6379 |
Redis connection string |
create_index |
RedisVL | True |
Whether RedisVL creates and validates the index. False attaches to a pre-provisioned index and issues no index command. Requires redisvl>=0.26.0 |
overwrite |
RedisVL | False |
Whether to drop and recreate an existing index. Cannot be combined with create_index=False |
cache_id |
LangCache | Required | LangCache instance identifier |
api_key |
LangCache | Required | LangCache API key |
first_message_only |
Cache config | True |
Only cache the first message per session |
ignore_errors |
Cache config | True |
Log cache backend failures and carry on. Set False while developing to raise instead |