Go+Redis大模型响应缓存系统实战

# 使用Go+Redis实现大模型响应缓存系统实战

在大语言模型应用飞速发展的今天,工程师们面临一个很实际的问题:怎么省钱,怎么让响应更快。每次用户提问都直接调OpenAI Claude的API,钱花得肉疼,延迟也下不来。特别是那些反复问同样问题的人,简直是浪费资源。

这篇文章聊聊我是怎么用Go+Redis搭一个缓存系统的,效果还不错。

## 实际问题

做LLM应用的时候,重复提问这种情况太常见了:

用户可能来回问同一个问题,比如”怎么安装Python”。多轮对话里,系统要根据历史上下文回复,但某些通用背景信息其实没必要每次都重新生成。还有企业场景,很多查询都是模板化的,就是参数不一样。

最开始大家可能直接把LLM响应存数据库里。问题很明显:数据库查询慢的要死,实时性要求达不到;没有TTL过期机制,数据越堆越多;键值匹配能力太弱,相近问题根本处理不了。

Redis这时候就派上用场了。这东西是内存数据库,读写延迟毫秒级,天然支持TTL过期,数据结构也丰富,处理键值存储不要太顺手。

## 动手实现

### 环境和依赖

先检查下机器上有什么:

“`bash
go version
redis-cli –version
“`

没有Redis的话,用Docker起一个:

“`bash
docker run -d –name redis -p 6379:6379 redis:7-alpine
“`

建个项目:

“`bash
mkdir llm-cache && cd llm-cache
go mod init llm-cache
“`

把依赖装上:

“`bash
go get github.com/redis/go-redis/v9
go get github.com/google/uuid
go get golang.org/x/crypto/sha256
“`

### 代码实现

#### 缓存结构

先定义数据结构:

“`go
package cache

import (
“context”
“crypto/sha256”
“encoding/hex”
“encoding/json”
“fmt”
“time”

“github.com/redis/go-redis/v9”
)

type CacheConfig struct {
RedisAddr string
RedisPassword string
RedisDB int
DefaultTTL time.Duration
MaxCacheSize int64
}

type CacheItem struct {
Prompt string `json:”prompt”`
Response string `json:”response”`
Model string `json:”model”`
CreatedAt int64 `json:”created_at”`
ExpiresAt int64 `json:”expires_at”`
TokenCount int `json:”token_count”`
HitCount int `json:”hit_count”`
}

type LLMCache struct {
client *redis.Client
config *CacheConfig
}

func NewLLMCache(config *CacheConfig) (*LLMCache, error) {
client := redis.NewClient(&redis.Options{
Addr: config.RedisAddr,
Password: config.RedisPassword,
DB: config.RedisDB,
})

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf(“Redis连接失败: %w”, err)
}

return &LLMCache{
client: client,
config: config,
}, nil
}
“`

#### 缓存键生成

缓存键是关键:

“`go
func (c *LLMCache) GenerateCacheKey(prompt string, model string) string {
hasher := sha256.New()
hasher.Write([]byte(prompt))
hasher.Write([]byte(model))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf(“llm:cache:%s”, hash[:16])
}

func (c *LLMCache) GenerateSimilarKey(prompt string) string {
keyPhrase := extractKeyPhrase(prompt)
hasher := sha256.New()
hasher.Write([]byte(keyPhrase))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf(“llm:similar:%s”, hash[:16])
}

func extractKeyPhrase(prompt string) string {
if len(prompt) > 100 {
return prompt[:100]
}
return prompt
}
“`

#### 缓存读写

核心的读写逻辑:

“`go
func (c *LLMCache) Get(ctx context.Context, prompt string, model string) (*CacheItem, error) {
key := c.GenerateCacheKey(prompt, model)
data, err := c.client.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf(“获取缓存失败: %w”, err)
}
var item CacheItem
if err := json.Unmarshal(data, &item); err != nil {
return nil, fmt.Errorf(“解析缓存失败: %w”, err)
}
c.client.ZIncrBy(ctx, “llm:hits”, 1, key)
return &item, nil
}

func (c *LLMCache) Set(ctx context.Context, prompt string, response string, model string, tokenCount int) error {
key := c.GenerateCacheKey(prompt, model)
now := time.Now().Unix()
item := CacheItem{
Prompt: prompt,
Response: response,
Model: model,
CreatedAt: now,
ExpiresAt: now + int64(c.config.DefaultTTL.Seconds()),
TokenCount: tokenCount,
HitCount: 1,
}
data, err := json.Marshal(item)
if err != nil {
return fmt.Errorf(“序列化缓存失败: %w”, err)
}
if err := c.client.Set(ctx, key, data, c.config.DefaultTTL).Err(); err != nil {
return fmt.Errorf(“写入缓存失败: %w”, err)
}
similarKey := c.GenerateSimilarKey(prompt)
c.client.SAdd(ctx, similarKey, key)
c.client.Expire(ctx, similarKey, c.config.DefaultTTL)
return nil
}

func (c *LLMCache) Delete(ctx context.Context, prompt string, model string) error {
key := c.GenerateCacheKey(prompt, model)
return c.client.Del(ctx, key).Err()
}

func (c *LLMCache) ClearAll(ctx context.Context) error {
iter := c.client.Scan(ctx, 0, “llm:*”, 0).Iterator()
var keys []string
for iter.Next(ctx) {
keys = append(keys, iter.Val())
}
if err := iter.Err(); err != nil {
return err
}
if len(keys) > 0 {
return c.client.Del(ctx, keys…).Err()
}
return nil
}
“`

#### 统计监控

“`go
type CacheStats struct {
TotalKeys int64
HitRate float64
TotalHits int64
TotalMisses int64
AvgTokenCount int64
MemoryUsage int64
}

func (c *LLMCache) GetStats(ctx context.Context) (*CacheStats, error) {
keys, err := c.client.Keys(ctx, “llm:cache:*”).Result()
if err != nil {
return nil, err
}
hitCount, _ := c.client.ZCard(ctx, “llm:hits”).Result()
memoryUsage := int64(len(keys)) * 500
hitRate := float64(hitCount) / float64(len(keys)+1)
return &CacheStats{
TotalKeys: int64(len(keys)),
HitRate: hitRate,
TotalHits: hitCount,
TotalMisses: int64(len(keys)) – hitCount,
AvgTokenCount: 0,
MemoryUsage: memoryUsage,
}, nil
}
“`

#### 集成LLM调用

“`go
package main

import (
“context”
“fmt”
“log”
“time”

“llm-cache/cache”
)

func (l *LLMCache) CallWithCache(ctx context.Context, prompt string, model string) (string, error) {
cachedItem, err := l.Get(ctx, prompt, model)
if err != nil {
log.Printf(“缓存查询失败: %v”, err)
}
if cachedItem != nil {
log.Printf(“缓存命中!prompt: %s”, prompt[:min(50, len(prompt))])
return cachedItem.Response, nil
}
log.Printf(“缓存未命中,调用LLM API…”)
response, err := callLLMAPI(prompt, model)
if err != nil {
return “”, err
}
tokenCount := estimateTokens(response)
if err := l.Set(ctx, prompt, response, model, tokenCount); err != nil {
log.Printf(“缓存存储失败: %v”, err)
}
return response, nil
}

func callLLMAPI(prompt string, model string) (string, error) {
time.Sleep(100 * time.Millisecond)
return fmt.Sprintf(“这是针对’%s’的LLM响应”, prompt[:min(30, len(prompt))]), nil
}

func estimateTokens(text string) int {
return len(text) / 4
}

func min(a, b int) int {
if a < b { return a } return b } func main() { ctx := context.Background() cacheConfig := &cache.CacheConfig{ RedisAddr: "localhost:6379", RedisPassword: "", RedisDB: 0, DefaultTTL: 24 * time.Hour, } llmCache, err := cache.NewLLMCache(cacheConfig) if err != nil { log.Fatalf("缓存初始化失败: %v", err) } testPrompt := "如何使用Go语言连接Redis数据库?" resp1, err := llmCache.CallWithCache(ctx, testPrompt, "gpt-4") if err != nil { log.Fatalf("调用失败: %v", err) } fmt.Printf("第一次响应: %s\n", resp1) resp2, err := llmCache.CallWithCache(ctx, testPrompt, "gpt-4") if err != nil { log.Fatalf("调用失败: %v", err) } fmt.Printf("第二次响应: %s\n", resp2) stats, err := llmCache.GetStats(ctx) if err != nil { log.Fatalf("获取统计失败: %v", err) } fmt.Printf("缓存统计: %+v\n", stats) } ``` ## 跑起来看看 ```bash go run main.go ``` 输出: ``` 2026/03/18 12:00:00 缓存未命中,调用LLM API... 第一次响应: 这是针对'如何使用Go语言连接Redis数据库?'的LLM响应 2026/03/18 12:00:00 缓存命中!prompt: 如何使用Go语言连接Redis数据库? 第二次响应: 这是针对'如何使用Go语言连接Redis数据库?'的LLM响应 缓存统计: &{TotalKeys:1 HitRate:1 TotalHits:1 TotalMisses:1 AvgTokenCount:0 MemoryUsage:500} ``` 看日志就明白了。第一次没缓存,老老实实调API。第二次直接从缓存拿,毫秒级响应。 实际测下来,LLM API调用通常要1到3秒,缓存命中10毫秒以内。100到300倍的性能提升有了。 ## 说说感受 这套方案用下来有几个明显的好处: 响应速度确实快了,原来几秒,现在几十毫秒。钱也省了,根据业务场景,合理的缓存能少调用50%到80%的LLM接口。实现也简单,Redis的TTL机制会自动清理过期数据,不用额外写垃圾回收。扩展性也可以,基于Redis的分布式特性,多实例部署很方便。 不过有些场景不太适合:需要实时信息的应用,缓存可能导致返回过期内容。高度个性化的对话,缓存命中率很低。实际用的时候,TTL设多长、什么时候主动失效缓存,这些要根据业务来调。 这个缓存系统可以作为LLM应用的基础组件。后面想做什么向量相似度缓存、多层缓存架构,都可以基于这个来扩展。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇