在 TGI 服务前加一层 Redis 缓存代理,是消除相同生成请求重复计算的常见做法。这里的关键不是给 TGI 改配置,而是在请求到达 TGI 之前,用一段自己的逻辑判断能否直接返回历史结果。前提是业务里确实存在大量相同 prompt 和相同采样参数的请求,且对输出新鲜度不敏感。下面给出一个可落地的代理骨架,以及需要注意的边界。
通常可以这样理解:TGI 本身没有通用内建 Redis 缓存,合理做法是在外层代理或 API 网关里做;缓存 key 必须覆盖 prompt 和影响输出的采样参数;命中缓存时直接返回,未命中再转发给 TGI。该方案适用于请求重复度高的场景,不适合需要实时更新的推理。建议先验证日志中的重复请求占比,再决定投入。
先判断是否值得加缓存层
如果每次生成结果的 prompt 几乎都不同,加 Redis 缓存只会增加一次网络开销和存储成本,对吞吐量没有帮助。需要关注的是多个在线用户之间是否会产生完全相同的生成请求。这类重复通常出现在固定指令场景,比如系统提示词固定、用户问题来自几个模板,或者离线任务把同一类任务反复提交。另外,当生成长度较长时,缓存能直接省去模型前向计算和显存占用,收益比短问答明显。可以先从接入层日志里统计同一 prompt 加同参数出现在不同时间点的次数,或者用一段采样脚本模拟线上请求,观察重复率。重复率不高时,不应优先做缓存层,而应该关注请求合并或服务容量。
推荐结构:TGI 前置代理服务
建议使用一个无状态的 Python 或 Node 服务接收完整的 TGI 兼容请求,代理服务先读取请求体中的 prompt、max_new_tokens、temperature、top_p、seed 等字段,将这些字段组合成缓存 key,先查询 Redis,如果命中就返回缓存文本,如果没有命中则转发给 TGI 的 /generate 接口,拿到结果后再写入 Redis。代理服务本身不参与模型计算,占用的 CPU 和内存很小,可以部署多个副本。
下面是基于 FastAPI 的一个最小代理骨架,只覆盖了关键的缓存查询和回填逻辑。实际使用时,需要把 code 中的 TGI_ENDPOINT 替换成内部 TGI 服务地址,把 REDIS_URL 替换成 Redis 连接串。
from fastapi import FastAPI, Request, Response
import hashlib, json, os
import httpx
import redis
app = FastAPI()
redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0"))
tgi_endpoint = os.getenv("TGI_ENDPOINT", "http://localhost:8080")
def make_cache_key(payload):
# 用 prompt 和采样参数做哈希,避免过长 key
content = json.dumps(payload, sort_keys=True, ensure_ascii=False)
return "tgi:cache:" + hashlib.sha256(content.encode()).hexdigest()
@app.post("/generate")
async def generate(request: Request):
payload = await request.json()
cache_key = make_cache_key(payload)
cached = redis_client.get(cache_key)
if cached is not None:
return Response(content=cached, media_type="application/json")
# 没有命中,转发到 TGI
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.post(tgi_endpoint + "/generate", json=payload)
body = resp.text
redis_client.set(cache_key, body, ex=1800) # 默认缓存 30 分钟
return Response(content=body, media_type="application/json")
缓存 key 设计与缓存策略
缓存 key 只保留必要参数,不一定要求按照整个请求体做哈希。但必须把影响生成结果的字段都纳入,例如 prompt、max_new_tokens、temperature、top_p、top_k、repetition_penalty、seed。如果业务里会固定使用某个 TGI 模型版本,key 里可以不用包含模型名;如果同一代理会转发多个模型,key 里必须包含模型名。TTL 设置需要结合业务的容忍度,比如摘要任务可以容忍几小时的老结果,建议设置为 30 分钟到 1 小时;但如果是对数据有强时效性的问答,就应该把 TTL 缩短到几分钟,甚至不做缓存。
一条缓存记录保存的内容建议是 TGI 返回的完整 JSON 体,这样命中时可以直接原样返回,代理无需重新组装字段。此外,需要处理两个缓存边界问题:
- 缓存穿透:对同一 key 的并发请求可能同时回源到 TGI。建议在写缓存时加入 set 参数加上 NX,或者在代理层使用简单的互斥锁,避免重复计算。
- 缓存击穿:热点 key 过期瞬间如果有大量请求,也可能同时打到 TGI。可以考虑把热点数据设为永不过期,再配合定期清理策略。但这里需要结合业务评估。
验证方式和风险边界
落地后可以用 curl 连续发送两个相同的请求体,第二次响应会比第一次快,并在 Redis 里确认对应 key 存在。观察代理服务的日志,确认第二次请求没有向 TGI 发起,就说明缓存链路是通的。需要注意,代理服务本身不能判断生成内容是否合法,缓存会保留 TGI 当时的输出。如果后续更新了模型版本或提示词模板,必须立即清理对应前缀的 Redis key,否则会出现旧结果与新模型混杂的问题。建议在 Redis 中使用固定前缀,方便用 SCAN 或批量删除来清理。
还有一点容易被忽视:TGI 服务重启或者 GPU 实例变化后,缓存内容仍然有效,因为缓存内容是从客户端角度返回的文本。只要模型没有变化,旧缓存仍然可以复用。反过来,如果显存或部署环境发生变化,也不影响已缓存的文本。整体上,这个方案能够在重复请求占比高的场景有效降低计算压力,但不会对请求的多样性或生成质量有任何改善。