| # ScholarMind 高级缓存加速方案 |
|
|
| > 本文档补充原有 4 级缓存体系,新增 **7 层缓存机制**,覆盖从应用层语义缓存到 GPU KV Cache 硬件层的全栈加速。 |
|
|
| --- |
|
|
| ## 缓存全景图 |
|
|
| ``` |
| ┌──────────────────────────────────────────────────────────────────────────────────┐ |
| │ ScholarMind 7层缓存加速栈 │ |
| ├──────────────────────────────────────────────────────────────────────────────────┤ |
| │ │ |
| │ Layer 1 ─── 语义缓存 (GPTCache) │ |
| │ "What is SnapKV?" ≈ "Explain SnapKV" → 同一缓存命中 │ |
| │ 延迟: ~5ms (缓存命中) vs ~1500ms (LLM调用) │ |
| │ 节省: ~100× 延迟, ~100% API成本 │ |
| │ │ |
| │ Layer 2 ─── 检索结果缓存 (语义级) │ |
| │ 相似query复用检索结果, 跳过向量搜索+图谱遍历+重排 │ |
| │ 延迟: ~2ms vs ~300ms │ |
| │ 节省: 避免重复检索计算 │ |
| │ │ |
| │ Layer 3 ─── API Provider 提示缓存 │ |
| │ OpenAI: 自动, ≥1024 tokens → 90%输入token折扣 │ |
| │ Anthropic: cache_control 标记 → 90%折扣 │ |
| │ DeepSeek: 自动磁盘KV → 90%折扣 │ |
| │ 节省: 50-90% API成本, 最高80%延迟降低 │ |
| │ │ |
| │ Layer 4 ─── vLLM 前缀缓存 (APC) │ |
| │ 共享 system prompt + 文档chunk 的 KV Cache │ |
| │ 节省: 2-8× TTFT (首Token延迟) 降低 │ |
| │ │ |
| │ Layer 5 ─── RAG KV Cache 复用 (LMCache/CacheBlend) │ |
| │ 预计算每个文档chunk的KV状态, 组合时选择性重算5-15% │ |
| │ 节省: 2.2-3.3× TTFT, 2.8-5× 吞吐提升 │ |
| │ │ |
| │ Layer 6 ─── KV Cache 压缩 (SnapKV/Quest) │ |
| │ 长文档场景: 只保留注意力关键的20% KV位置 │ |
| │ 节省: 3.6× 解码加速, 8.2× 显存效率 │ |
| │ │ |
| │ Layer 7 ─── 多轮对话状态缓存 │ |
| │ 缓存Agent中间状态 + 检索上下文 + 部分答案 │ |
| │ 追问时跳过路由+检索+图谱查询 │ |
| │ 节省: 追问响应 ~60% 延迟降低 │ |
| │ │ |
| └──────────────────────────────────────────────────────────────────────────────────┘ |
| |
| 请求完整路径: |
| User Query |
| → [L1 语义缓存] 命中? → 直接返回 (~5ms) |
| → [L2 检索缓存] 命中? → 跳过检索, 直接生成 |
| → [L7 对话缓存] 追问? → 复用上下文 |
| → Retriever (向量+图谱+RAPTOR) |
| → Prompt组装 (static prefix + retrieved chunks + query) |
| → [L3 Provider缓存] 系统提示+文档chunk已缓存 → 90%折扣 |
| → [L4 vLLM APC] 共享前缀KV命中 → 跳过prefill |
| → [L5 CacheBlend] 非前缀chunk KV复用 → 部分重算 |
| → [L6 SnapKV] 长上下文KV压缩 → 加速decode |
| → Response → 写入 L1 + L2 |
| ``` |
|
|
| --- |
|
|
| ## Layer 1: 语义缓存 (GPTCache) |
|
|
| ### 原理 |
|
|
| 传统 Redis 缓存只能精确匹配 key。但学术问答中,同一个问题有多种表达方式: |
| - "SnapKV 是什么?" ≈ "解释一下 SnapKV 的原理" ≈ "What does SnapKV do?" |
|
|
| **语义缓存**将查询编码为向量,通过**相似度搜索**查找语义等价的历史查询,命中则直接返回缓存答案。 |
|
|
| ``` |
| 查询 → Embedding → 向量相似度搜索 → 相似度 > 阈值? |
| ├── 是: 返回缓存答案 (~5ms) |
| └── 否: 调用LLM → 存入缓存 |
| ``` |
|
|
| ### 实现 |
|
|
| ```python |
| # ===== 方案A: GPTCache (推荐, 7k⭐) ===== |
| # pip install gptcache |
| |
| from gptcache import cache |
| from gptcache.adapter import openai |
| from gptcache.embedding import Onnx |
| from gptcache.manager import CacheBase, VectorBase, get_data_manager |
| from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation |
| from gptcache.processor.pre import get_prompt |
| |
| # 初始化: FAISS向量索引 + SQLite存储 (开发) |
| onnx = Onnx() |
| data_manager = get_data_manager( |
| CacheBase("sqlite"), |
| VectorBase("faiss", dimension=onnx.dimension) |
| ) |
| |
| cache.init( |
| pre_embedding_func=get_prompt, # 只用user query做缓存key (排除检索上下文) |
| embedding_func=onnx.to_embeddings, |
| data_manager=data_manager, |
| similarity_evaluation=SearchDistanceEvaluation(), |
| ) |
| |
| # 使用: 直接替换 openai 调用 |
| response = openai.ChatCompletion.create( |
| model="gpt-4o-mini", |
| messages=[ |
| {"role": "system", "content": "You are ScholarMind..."}, |
| {"role": "user", "content": "What is attention mechanism?"} |
| ] |
| ) |
| # 第二次语义相似查询 → 缓存命中, ~5ms返回 |
| ``` |
|
|
| ```python |
| # ===== 方案A (生产版): Milvus/Redis后端 ===== |
| from gptcache.embedding import OpenAI as OpenAIEmbedding |
| from gptcache.manager import CacheBase, VectorBase, get_data_manager |
| |
| # 使用 Qdrant 作为向量后端 (复用已有基础设施) |
| data_manager = get_data_manager( |
| CacheBase("postgresql", sql_url="postgresql://user:pass@localhost/gptcache"), |
| VectorBase("milvus", host="localhost", port=19530, dimension=1536) |
| # 也可用 VectorBase("qdrant", url="http://localhost:6333", collection_name="gptcache") |
| ) |
| |
| # 使用 OpenAI Embedding (与检索管道同模型, 一致性最高) |
| openai_emb = OpenAIEmbedding() |
| cache.init( |
| pre_embedding_func=get_prompt, |
| embedding_func=openai_emb.to_embeddings, |
| data_manager=data_manager, |
| similarity_evaluation=SearchDistanceEvaluation(), |
| ) |
| ``` |
|
|
| ```python |
| # ===== 方案B: LangChain SemanticCache (更简洁) ===== |
| from langchain_community.cache import RedisSemanticCache |
| from langchain_openai import OpenAIEmbeddings |
| import langchain |
| |
| langchain.llm_cache = RedisSemanticCache( |
| redis_url="redis://localhost:6379", |
| embedding=OpenAIEmbeddings(), |
| score_threshold=0.85, # 学术领域建议较严格 |
| ) |
| # 所有 LangChain LLM 调用自动走语义缓存 |
| ``` |
|
|
| ### 关键参数调优 |
|
|
| | 参数 | 推荐值 | 说明 | |
| |------|--------|------| |
| | 相似度阈值 | **0.85** (学术) | 太低→错误答案; 太高→命中率低; 通用场景可用0.75 | |
| | 嵌入模型 | text-embedding-3-small | 与检索管道一致, 避免语义偏差 | |
| | TTL | **24h** | 学术知识相对稳定 | |
| | 淘汰策略 | **LRU** | 最近最少使用 | |
| | 缓存key | **仅user query** | 排除检索context, 否则同一问题不同检索结果无法命中 | |
|
|
| ### 效果预估 |
|
|
| | 场景 | 命中率 | 延迟节省 | |
| |------|--------|---------| |
| | 同一用户追问变体 | ~70% | ~300× (5ms vs 1.5s) | |
| | 多用户热门问题 | ~30-40% | ~300× | |
| | 全新问题 | 0% | 无节省 (还多~10ms嵌入开销) | |
| | **加权平均** | **~35%** | **总QPS提升~50%** | |
|
|
| --- |
|
|
| ## Layer 2: 语义检索结果缓存 |
|
|
| ### 原理 |
|
|
| 混合检索 (向量+图谱+RAPTOR+重排) 耗时约 300ms。如果两个查询语义相似,它们的检索结果往往也相似。 |
|
|
| ```python |
| class SemanticRetrievalCache: |
| """语义级检索结果缓存 — 相似query复用检索结果""" |
| |
| def __init__(self, qdrant_client, collection="retrieval_cache", threshold=0.90): |
| self.client = qdrant_client |
| self.collection = collection |
| self.threshold = threshold |
| self.embed_model = load_embedding_model() |
| |
| # 创建缓存collection |
| self.client.create_collection( |
| collection_name=collection, |
| vectors_config=models.VectorParams(size=1536, distance=models.Distance.COSINE), |
| ) |
| |
| async def get_or_fetch(self, query: str, retriever) -> list: |
| """查缓存, 没有则检索并缓存""" |
| query_vec = self.embed_model.encode(query) |
| |
| # 1. 在缓存中搜索语义相似的历史查询 |
| hits = self.client.search( |
| collection_name=self.collection, |
| query_vector=query_vec, |
| limit=1, |
| score_threshold=self.threshold, |
| ) |
| |
| if hits and hits[0].score >= self.threshold: |
| # 缓存命中 — 直接返回历史检索结果 |
| cached = hits[0].payload["results"] |
| return cached |
| |
| # 2. 缓存未命中 — 执行完整检索 |
| results = await retriever.retrieve(query, mode="hybrid") |
| |
| # 3. 存入缓存 |
| self.client.upsert( |
| collection_name=self.collection, |
| points=[models.PointStruct( |
| id=hash(query) % (2**63), |
| vector=query_vec, |
| payload={ |
| "query": query, |
| "results": results, |
| "timestamp": time.time(), |
| } |
| )] |
| ) |
| |
| return results |
| ``` |
|
|
| ### 缓存失效 |
|
|
| ```python |
| async def invalidate_on_new_papers(self, paper_ids: list): |
| """新论文导入时, 清除可能受影响的缓存""" |
| # 策略1: 全量清除 (简单但激进) |
| self.client.delete_collection(self.collection) |
| |
| # 策略2: 精准失效 (复杂但精确) |
| # 检索包含这些paper_id的缓存条目并删除 |
| for paper_id in paper_ids: |
| self.client.delete( |
| collection_name=self.collection, |
| points_selector=models.FilterSelector( |
| filter=models.Filter( |
| must=[models.FieldCondition( |
| key="results[].metadata.paper_id", |
| match=models.MatchValue(value=paper_id), |
| )] |
| ) |
| ) |
| ) |
| ``` |
|
|
| --- |
|
|
| ## Layer 3: API Provider 提示缓存 |
|
|
| ### 核心原理 |
|
|
| ``` |
| Prompt 结构: |
| ┌─────────────────────────────────────────┐ |
| │ System Prompt (固定, ~500 tokens) │ ← 这部分每次都一样 |
| │ "You are ScholarMind, a research..." │ Provider 自动缓存 |
| ├─────────────────────────────────────────┤ |
| │ 检索到的论文片段 (半固定, ~2000 tokens) │ ← 热门论文反复被检索到 |
| │ Paper chunk A: "Attention is..." │ 高概率命中缓存 |
| │ Paper chunk B: "We propose BERT..." │ |
| ├─────────────────────────────────────────┤ |
| │ 用户问题 (动态, ~50 tokens) │ ← 每次不同 |
| │ "Compare BERT and GPT-2 on GLUE" │ 不被缓存 |
| └─────────────────────────────────────────┘ |
| |
| 关键: 固定内容放前面, 动态内容放最后! |
| ``` |
|
|
| ### OpenAI — 全自动 (零配置) |
|
|
| ```python |
| from openai import OpenAI |
| client = OpenAI() |
| |
| # 技巧: 构造≥1024 tokens的静态前缀, OpenAI自动缓存 |
| SYSTEM_PROMPT = """You are ScholarMind, an expert academic research assistant. |
| You have access to a knowledge base of 1000+ academic papers spanning NLP, |
| computer vision, and machine learning. When answering questions: |
| 1. Always cite specific papers with [Author, Year] format |
| 2. Include quantitative results where available |
| 3. Compare methods objectively |
| 4. Acknowledge limitations and open questions |
| ... (填充到≥1024 tokens) |
| """ |
| |
| response = client.chat.completions.create( |
| model="gpt-4o-mini", |
| messages=[ |
| {"role": "system", "content": SYSTEM_PROMPT}, # ~1200 tokens, 自动缓存 |
| {"role": "user", "content": f""" |
| Based on these papers: |
| {retrieved_chunks} |
| |
| Question: {user_question} |
| """} |
| ] |
| ) |
| |
| # 检查缓存效果 |
| usage = response.usage |
| cached = usage.prompt_tokens_details.cached_tokens |
| total = usage.prompt_tokens |
| print(f"Cache hit: {cached}/{total} tokens ({cached/total:.0%})") |
| # 首次: 0%, 后续相同前缀: ~70-90% |
| ``` |
|
|
| **成本节省**: |
| | 场景 | 正常价格 | 缓存命中价格 | 节省 | |
| |------|---------|-------------|------| |
| | GPT-4o input | $2.50/M | $1.25/M | 50% | |
| | GPT-4o-mini input | $0.15/M | $0.075/M | 50% | |
| | GPT-4o 长前缀 (>128K) | — | 高达 90% off | 90% | |
|
|
| ### Anthropic — 显式标记 (精细控制) |
|
|
| ```python |
| import anthropic |
| client = anthropic.Anthropic() |
| |
| response = client.messages.create( |
| model="claude-sonnet-4-20250514", |
| max_tokens=2048, |
| system=[ |
| { |
| "type": "text", |
| "text": SYSTEM_PROMPT, # 系统指令 |
| "cache_control": {"type": "ephemeral"} # ← 缓存断点1 |
| } |
| ], |
| messages=[{ |
| "role": "user", |
| "content": [ |
| { |
| "type": "text", |
| "text": retrieved_chunks, # 检索到的论文片段 |
| "cache_control": {"type": "ephemeral"} # ← 缓存断点2 |
| }, |
| { |
| "type": "text", |
| "text": user_question # 动态部分, 不缓存 |
| } |
| ] |
| }] |
| ) |
| |
| # 查看缓存统计 |
| print(f"Cache write: {response.usage.cache_creation_input_tokens} tokens") |
| print(f"Cache read: {response.usage.cache_read_input_tokens} tokens") |
| # 首次: 全部write; 5分钟内再次调用相同前缀: 全部read → 90%折扣 |
| ``` |
|
|
| **Anthropic 缓存定价**: |
| | 类型 | Sonnet 4 | Haiku 3.5 | |
| |------|----------|-----------| |
| | 正常输入 | $3/M | $0.80/M | |
| | 缓存写入 (首次) | $3.75/M (+25%) | $1.00/M (+25%) | |
| | 缓存读取 (命中) | $0.30/M (**-90%**) | $0.08/M (**-90%**) | |
| | TTL | 5分钟 (每次命中刷新) | 5分钟 | |
|
|
| ### DeepSeek — 自动磁盘缓存 |
|
|
| ```python |
| from openai import OpenAI |
| client = OpenAI(api_key="sk-xxx", base_url="https://api.deepseek.com") |
| |
| response = client.chat.completions.create( |
| model="deepseek-chat", |
| messages=[ |
| {"role": "system", "content": LONG_SYSTEM_PROMPT}, |
| {"role": "user", "content": user_question} |
| ] |
| ) |
| |
| # DeepSeek 自动缓存到磁盘, 无需配置 |
| usage = response.usage |
| print(f"Cache hit: {usage.prompt_cache_hit_tokens} tokens @ 10% price") |
| print(f"Cache miss: {usage.prompt_cache_miss_tokens} tokens @ 100% price") |
| # 磁盘缓存持续时间 > Anthropic的5分钟, 更适合低流量场景 |
| ``` |
|
|
| --- |
|
|
| ## Layer 4: vLLM 前缀缓存 (APC) |
|
|
| ### 原理 |
|
|
| vLLM 的 Automatic Prefix Caching 将 KV Cache 按 block (16-32 tokens) 分割,每个 block 通过 hash(tokens + position) 索引。新请求到来时,从头匹配已缓存的 blocks,跳过已有 blocks 的 prefill 计算。 |
|
|
| ``` |
| 请求1: [System 500 tokens] + [Chunk A 200 tokens] + [Query 1] |
| ↓ 全部计算 KV, 缓存所有blocks |
| |
| 请求2: [System 500 tokens] + [Chunk A 200 tokens] + [Query 2] |
| ↓ 前700 tokens 命中缓存! 只需计算 Query 2 的KV |
| → prefill 从 ~700 tokens 降到 ~50 tokens → 14× TTFT降低 |
| ``` |
|
|
| ### 配置 |
|
|
| ```bash |
| # vLLM serving — APC默认开启 (v1+) |
| vllm serve meta-llama/Llama-3.1-8B-Instruct \ |
| --enable-prefix-caching \ |
| --gpu-memory-utilization 0.95 \ |
| --max-model-len 32768 |
| |
| # 查看缓存统计 |
| curl http://localhost:8000/metrics | grep prefix_cache |
| # vllm_prefix_cache_hit_rate: 0.73 |
| # vllm_prefix_cache_queries_total: 10000 |
| ``` |
|
|
| ```python |
| # Python API |
| from vllm import LLM, SamplingParams |
| |
| llm = LLM( |
| model="meta-llama/Llama-3.1-8B-Instruct", |
| enable_prefix_caching=True, |
| gpu_memory_utilization=0.95, |
| ) |
| |
| # 关键: 所有请求共享相同的长前缀 |
| SHARED_PREFIX = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|> |
| |
| You are ScholarMind, an expert academic research assistant with access to |
| 1000+ research papers. Answer precisely with citations. |
| |
| <|eot_id|><|start_header_id|>user<|end_header_id|> |
| |
| Based on the following paper excerpts: |
| {FREQUENTLY_RETRIEVED_CHUNKS} |
| """ |
| |
| # 每次请求只改变 query 部分 — 前缀全部缓存命中 |
| responses = llm.generate([ |
| SHARED_PREFIX + "What is the main contribution of BERT?<|eot_id|>", |
| SHARED_PREFIX + "Compare BERT and GPT-2 on GLUE<|eot_id|>", |
| ]) |
| ``` |
|
|
| ### ⚡ 优化技巧: 文档chunk排序一致化 |
|
|
| ```python |
| def order_chunks_for_cache(chunks: list, query: str) -> list: |
| """ |
| 将检索到的chunk按确定性顺序排列, 最大化前缀缓存命中率 |
| |
| 策略: 按chunk_id排序 (而非按相关性排序) |
| → 不同查询检索到相同chunk集合时, 前缀完全一致 → 缓存命中 |
| """ |
| # 按paper_id + page_idx 确定性排序 |
| return sorted(chunks, key=lambda c: (c["metadata"]["paper_id"], c["metadata"]["page"])) |
| |
| # 注意: 这会牺牲一点"最相关chunk排前面"的优势 |
| # 折中方案: 前N个按相关性, 后面按ID排序 |
| ``` |
|
|
| --- |
|
|
| ## Layer 5: RAG KV Cache 复用 (LMCache / CacheBlend) |
|
|
| ### 问题 |
|
|
| 标准前缀缓存要求 chunks 以**完全相同的顺序**出现。但 RAG 检索结果的顺序经常变化: |
| - Query 1 → [Chunk A, B, C] |
| - Query 2 → [Chunk B, A, D] ← Chunk A, B 都出现了, 但前缀不同 |
|
|
| CacheBlend 解决这个问题: **预计算每个chunk的独立KV状态**, 组合时只选择性重算 5-15% 的 tokens。 |
|
|
| ### 实现 |
|
|
| ```bash |
| # 安装 LMCache (CacheBlend 的生产实现) |
| pip install lmcache lmcache-vllm |
| ``` |
|
|
| ```python |
| # LMCache 集成 vLLM 的示例 |
| import lmcache_vllm |
| from vllm import LLM, SamplingParams |
| |
| # LMCache 在 vLLM 之上透明地管理 KV 缓存 |
| # 支持 GPU VRAM → Host RAM → SSD 三级缓存层次 |
| llm = LLM( |
| model="meta-llama/Llama-3.1-8B-Instruct", |
| # LMCache 通过环境变量或配置文件集成 |
| ) |
| |
| # 预热: 将所有高频文档chunk的KV预计算并缓存 |
| for chunk in top_1000_chunks: |
| llm.encode(chunk["text"]) # 预计算KV → 缓存到GPU+RAM |
| |
| # 推理: 组合时自动复用已缓存的chunk KV, 只重算5-15% |
| response = llm.generate( |
| system_prompt + chunk_a + chunk_b + user_query |
| ) |
| # chunk_a 和 chunk_b 的 KV 从缓存加载, 仅选择性重算跨chunk注意力 |
| ``` |
|
|
| ### 效果 |
|
|
| | 指标 | 无缓存 | vLLM APC | CacheBlend | |
| |------|--------|----------|------------| |
| | TTFT (首token) | 基准 | 2-8× 降低 (仅限相同前缀) | **2.2-3.3× 降低** (任意chunk组合) | |
| | 吞吐 | 基准 | 1.5× 提升 | **2.8-5× 提升** | |
| | 适用场景 | — | 固定前缀 | **任意RAG检索结果** | |
|
|
| > **论文**: CacheBlend (arxiv:2405.16444), LMCache GitHub: `github.com/LMCache/LMCache` |
|
|
| --- |
|
|
| ## Layer 6: KV Cache 压缩 (长文档场景) |
|
|
| ### SnapKV — 关注度投票压缩 |
|
|
| 当输入很长(多篇论文全文)时,保留完整 KV Cache 会耗尽显存。SnapKV 只保留每个注意力头**真正关注的 20% 位置**。 |
|
|
| ``` |
| 完整KV: [tok1, tok2, tok3, ..., tok10000] → 160GB VRAM (70B模型) |
| SnapKV: [tok3, tok45, tok202, ..., tok9998] → 32GB VRAM (仅保留20%) |
| ~5× 显存节省, ~3.6× 解码加速 |
| ``` |
|
|
| ```python |
| # pip install snapkv |
| # 核心参数 |
| config = { |
| "compression_ratio": 0.2, # 保留20%的KV位置 |
| "observation_window": 32, # 用最后32个token投票决定保留哪些位置 |
| "kernel_size": 5, # 投票时的池化窗口 |
| } |
| |
| # SnapKV 集成方式: 修改 attention 层 |
| # 适用: 处理多篇完整论文时 (>16K tokens) |
| # 结果: 单A100可处理380K token上下文 (原本~32K) |
| ``` |
|
|
| ### Quest — 查询感知的稀疏注意力 |
|
|
| ``` |
| 每个KV页面维护元数据 (K向量的min/max值) |
| → 新query来了, 用Q和元数据估算每页的重要性 |
| → 只加载Top-K重要的页面 |
| → 7× self-attention加速 |
| ``` |
|
|
| > **论文**: SnapKV (arxiv:2404.14469, GitHub: `fasterdecoding/snapkv`) |
| > **论文**: Quest (arxiv:2406.10774, GitHub: `mit-han-lab/quest`) |
|
|
| --- |
|
|
| ## Layer 7: 多轮对话状态缓存 |
|
|
| ### 原理 |
|
|
| 学术问答常见多轮追问: |
| 1. "BERT在GLUE上表现如何?" → 检索+推理+生成 |
| 2. "和GPT-2相比呢?" → **不需要重新检索BERT的信息!** |
|
|
| ```python |
| from langgraph.checkpoint.memory import MemorySaver |
| |
| class ConversationCache: |
| """多轮对话缓存 — 避免追问时重复检索""" |
| |
| def __init__(self): |
| self.checkpointer = MemorySaver() # LangGraph 内置状态持久化 |
| self.context_cache = {} # session_id → {retrieved_docs, entities, graph_context} |
| |
| async def handle_query(self, session_id: str, query: str): |
| # 检查是否是追问 |
| if session_id in self.context_cache: |
| prev = self.context_cache[session_id] |
| |
| # 追问检测: 如果query引用了上一轮的实体, 复用上下文 |
| if self._is_followup(query, prev["entities"]): |
| # 直接复用之前的检索结果 + 图谱上下文 |
| return await self._generate_with_cached_context( |
| query=query, |
| retrieved_docs=prev["retrieved_docs"], |
| graph_context=prev["graph_context"], |
| history=prev["history"], |
| ) |
| |
| # 非追问: 完整检索流程 |
| result = await full_retrieval_and_generation(query) |
| |
| # 缓存上下文供追问使用 |
| self.context_cache[session_id] = { |
| "retrieved_docs": result["retrieved_docs"], |
| "graph_context": result["graph_context"], |
| "entities": result["entities"], |
| "history": result["messages"], |
| "timestamp": time.time(), |
| } |
| |
| return result |
| |
| def _is_followup(self, query: str, prev_entities: list) -> bool: |
| """检测是否是追问: 包含代词、比较词、或引用上一轮实体""" |
| followup_signals = ["compared to", "和...比", "那", "它", "this method", "上面提到的"] |
| has_pronoun = any(s in query.lower() for s in followup_signals) |
| references_entity = any(e["name"].lower() in query.lower() for e in prev_entities) |
| return has_pronoun or references_entity |
| ``` |
|
|
| ### LangGraph 内置检查点 |
|
|
| ```python |
| from langgraph.graph import StateGraph |
| from langgraph.checkpoint.postgres import PostgresSaver |
| |
| # 使用 PostgreSQL 持久化 Agent 状态 |
| checkpointer = PostgresSaver.from_conn_string("postgresql://...") |
| |
| # 编译时传入 checkpointer |
| agent = build_agent_graph().compile(checkpointer=checkpointer) |
| |
| # 每次调用使用 thread_id 标识会话 |
| config = {"configurable": {"thread_id": session_id}} |
| |
| # 第一次: 完整执行 |
| result1 = await agent.ainvoke({"query": "BERT在GLUE上表现如何?"}, config) |
| |
| # 追问: LangGraph 自动恢复之前的状态 |
| result2 = await agent.ainvoke({"query": "和GPT-2相比呢?"}, config) |
| # → Agent 已有上一轮的 retrieved_docs, 只需补充检索GPT-2信息 |
| ``` |
|
|
| --- |
|
|
| ## 集成架构: 完整缓存流水线 |
|
|
| ```python |
| class CachedQAEngine: |
| """集成7层缓存的问答引擎""" |
| |
| def __init__(self): |
| # L1: 语义缓存 |
| self.semantic_cache = GPTCacheWrapper(threshold=0.85, ttl_hours=24) |
| |
| # L2: 检索结果缓存 |
| self.retrieval_cache = SemanticRetrievalCache(qdrant, threshold=0.90) |
| |
| # L7: 对话状态缓存 |
| self.conversation_cache = ConversationCache() |
| |
| # L3-L6: 由 LiteLLM / vLLM / Provider 自动处理 |
| |
| async def query(self, session_id: str, query: str) -> dict: |
| |
| # === L1: 语义缓存检查 === |
| cached_answer = await self.semantic_cache.get(query) |
| if cached_answer: |
| return {"answer": cached_answer, "cache": "L1_semantic", "latency_ms": 5} |
| |
| # === L7: 追问检查 === |
| if self.conversation_cache.is_followup(session_id, query): |
| result = await self.conversation_cache.handle_followup(session_id, query) |
| if result: |
| return {**result, "cache": "L7_followup"} |
| |
| # === L2: 检索缓存检查 === |
| retrieved = await self.retrieval_cache.get_or_fetch(query, self.retriever) |
| |
| # === 组装 Prompt (L3/L4/L5友好的结构) === |
| prompt = self._build_prompt( |
| system=STATIC_SYSTEM_PROMPT, # → L3 Provider缓存 |
| chunks=order_chunks_for_cache(retrieved), # → L4 vLLM APC缓存 |
| query=query # → 动态部分 |
| ) |
| |
| # === LLM调用 (L3/L4/L5/L6 自动生效) === |
| answer = await self.llm.complete(prompt, task="generation") |
| |
| # === 写入缓存 === |
| await self.semantic_cache.set(query, answer) |
| self.conversation_cache.update(session_id, query, retrieved, answer) |
| |
| return {"answer": answer, "cache": "miss", "retrieved": retrieved} |
| |
| def _build_prompt(self, system: str, chunks: list, query: str) -> list: |
| """ |
| Prompt结构优化: |
| 1. 固定系统提示放最前 (Provider缓存 + vLLM前缀缓存) |
| 2. 检索chunks按确定性排序 (最大化vLLM前缀命中) |
| 3. 用户query放最后 (动态部分) |
| """ |
| return [ |
| {"role": "system", "content": system}, # ≥1024 tokens → OpenAI自动缓存 |
| {"role": "user", "content": |
| "Based on these paper excerpts:\n\n" + |
| "\n---\n".join([c["text"] for c in chunks]) + |
| f"\n\nQuestion: {query}" |
| } |
| ] |
| ``` |
|
|
| --- |
|
|
| ## 性能收益汇总 |
|
|
| ``` |
| ┌───────────────────────────────────────────────────────────────────┐ |
| │ 7层缓存预估收益 (1000篇论文, 日均500查询) │ |
| ├──────────┬──────────┬──────────────┬──────────────┬──────────────┤ |
| │ 缓存层 │ 命中率 │ 延迟节省 │ 成本节省 │ 实现复杂度 │ |
| ├──────────┼──────────┼──────────────┼──────────────┼──────────────┤ |
| │ L1 语义 │ ~35% │ 300×(5ms) │ ~100%(命中时) │ ⭐⭐ 中 │ |
| │ L2 检索 │ ~25% │ ~60×(5ms) │ 检索计算 │ ⭐⭐ 中 │ |
| │ L3 API │ ~70% │ 最高80% │ 50-90% │ ⭐ 低 │ |
| │ L4 APC │ ~60% │ 2-8× TTFT │ GPU算力 │ ⭐ 低 │ |
| │ L5 Cache │ ~40% │ 2-3× TTFT │ GPU算力 │ ⭐⭐⭐ 高 │ |
| │ Blend │ │ │ │ │ |
| │ L6 SnapKV│ N/A │ 3.6×解码 │ 5×显存 │ ⭐⭐ 中 │ |
| │ L7 对话 │ ~20% │ ~60%(追问) │ 检索+推理 │ ⭐⭐ 中 │ |
| ├──────────┼──────────┼──────────────┼──────────────┼──────────────┤ |
| │ 综合 │ — │ P50: 1.5s │ API成本 │ │ |
| │ 效果 │ │ → ~400ms │ 降低60%+ │ │ |
| │ │ │ P99: 4s │ │ │ |
| │ │ │ → ~1.5s │ │ │ |
| └──────────┴──────────┴──────────────┴──────────────┴──────────────┘ |
| ``` |
|
|
| --- |
|
|
| ## 实施优先级 |
|
|
| | 优先级 | 缓存层 | 理由 | 工作量 | |
| |--------|--------|------|--------| |
| | **P0 (立即)** | L3 Provider缓存 | 零代码改动, 只需调整prompt结构 | 2小时 | |
| | **P0 (立即)** | L4 vLLM APC | 默认已开启, 确认配置即可 | 1小时 | |
| | **P1 (本周)** | L1 语义缓存 | GPTCache几十行代码, 收益最高 | 1天 | |
| | **P1 (本周)** | L7 对话缓存 | LangGraph checkpointer, 追问体验质变 | 1天 | |
| | **P2 (下周)** | L2 检索缓存 | 复用Qdrant基础设施 | 2天 | |
| | **P3 (后续)** | L6 SnapKV | 仅长文档场景需要 | 3天 | |
| | **P3 (后续)** | L5 CacheBlend | 需要LMCache集成, 侵入性较大 | 1周 | |
|
|
| --- |
|
|
| ## 相关论文 |
|
|
| | 论文 | ArXiv ID | 核心贡献 | |
| |------|---------|---------| |
| | GPTCache | ACL 2023 NLPOSS | 语义缓存框架 | |
| | GPT Semantic Cache | 2411.05276 | 语义缓存基准评测 | |
| | PagedAttention (vLLM) | 2309.06180 | 分页KV Cache管理 | |
| | RAGCache | 2404.12457 | RAG专用多级KV缓存 | |
| | CacheBlend | 2405.16444 | 非前缀KV复用 | |
| | SnapKV | 2404.14469 | 注意力投票KV压缩 | |
| | Quest | 2406.10774 | 查询感知稀疏注意力 | |
| | StreamingLLM | 2309.17453 | 注意力sink+滚动窗口 | |
| | Prompt Cache | 2311.04934 | 模块化KV状态复用 | |
| | KV Cache Survey | 2412.19442 | KV Cache管理全面综述 | |
|
|
| --- |
|
|
| ## 开源项目 |
|
|
| | 项目 | GitHub | Stars | 用途 | |
| |------|--------|-------|------| |
| | GPTCache | zilliztech/GPTCache | 7k+ | 语义缓存 | |
| | LMCache | LMCache/LMCache | — | CacheBlend生产实现 | |
| | SnapKV | fasterdecoding/snapkv | 311 | KV压缩 | |
| | Quest | mit-han-lab/quest | 382 | 稀疏注意力 | |
| | vLLM | vllm-project/vllm | 45k+ | APC前缀缓存 | |
| | KV Cache Survey | TreeAI-Lab/Awesome-KV-Cache-Management | 314 | 综述索引 | |
|
|