在大模型API的实际使用中,请求优化与缓存策略是降低成本、提升响应速度的核心手段。本节将从Prompt压缩、批处理请求、语义缓存和响应缓存四个维度,系统讲解如何减少无效API调用,在不牺牲输出质量的前提下实现成本与效率的双重优化。
大模型API按Token计费,每次调用都产生成本。在实际业务中,我们发现大量请求存在冗余信息、重复调用或可缓存内容。通过系统化的请求优化,通常可降低30%-60%的API成本。
请求优化的三个核心方向:
┌─────────────────────────────────────────────┐ │ 请求优化策略全景图 │ ├──────────┬──────────┬──────────┬─────────────┤ │ Prompt │ 批处理 │ 语义 │ 响应 │ │ 压缩 │ 请求 │ 缓存 │ 缓存 │ ├──────────┼──────────┼──────────┼─────────────┤ │ 删除冗余 │ 合并请求 │ 向量 │ TTL过期 │ │ 关键词 │ 异步 │ 相似度 │ LRU淘汰 │ │ 提取 │ 并发 │ 匹配 │ 分级缓存 │ │ 摘要生成 │ 流水线 │ 语义 │ 读写穿透 │ └──────────┴──────────┴──────────┴─────────────┘
Prompt压缩的核心思路是:在不改变语义意图的前提下,减少输入Token的数量。主要方法包括:
方法一:基于规则的去冗余
方法二:基于模型的小型Prompt重写
方法三:LLMLingua等专用压缩工具
语义缓存的核心思想是:当两个请求在语义上等价或高度相似时,可以直接返回之前缓存的响应,避免重复调用API。
与传统的精确匹配缓存不同,语义缓存通过向量嵌入计算请求之间的语义相似度,能够处理表述不同但意图相同的请求。
语义缓存的关键组件:
响应缓存适用于那些输入完全相同、输出确定性的场景。常见策略包括:
以下代码实现了一个基于规则和模型辅助的Prompt压缩器:
import re from typing import Optional class PromptCompressor: """Prompt压缩器:通过规则和模型辅助减少Token数量""" def __init__(self, api_client=None): self.api_client = api_client # 常见冗余模式 self.redundant_patterns = [ (r'请注意[,,]', ''), (r'你需要[,,]', ''), (r'请帮我[,,]', ''), (r'我希望你[,,]', ''), (r'作为一个[^\n]+?[,,]\s*', ''), (r'([^\)]*?)\s*([^\)]*?)', lambda m: m.group(0)), ] def compress_by_rules(self, prompt: str) -> str: """基于规则压缩Prompt""" compressed = prompt for pattern, replacement in self.redundant_patterns: compressed = re.sub(pattern, replacement, compressed) # 去除多余空行(保留单个空行) compressed = re.sub(r'\n{3,}', '\n\n', compressed) return compressed.strip() def compress_with_model(self, prompt: str) -> Optional[str]: """使用低成本模型压缩Prompt""" if not self.api_client: return None compress_instruction = ( "请将以下Prompt压缩到原始长度的50%以内," "保留所有关键指令和约束条件:\n" ) response = self.api_client.chat( model="qwen-7b", # 使用低成本模型 messages=[ {"role": "system", "content": compress_instruction}, {"role": "user", "content": prompt} ], max_tokens=len(prompt) // 2 ) return response.choices[0].message.content def compress(self, prompt: str, use_model: bool = False) -> dict: """综合压缩策略""" original_len = len(prompt) result = self.compress_by_rules(prompt) stats = { "original_length": original_len, "compressed_length": len(result), "compression_ratio": len(result) / original_len, "method": "rule-based" } if use_model and self.api_client: model_result = self.compress_with_model(prompt) if model_result and len(model_result) < len(result): result = model_result stats.update({ "compressed_length": len(model_result), "compression_ratio": len(model_result) / original_len, "method": "model-assisted" }) return {"compressed_prompt": result, "stats": stats}
以下代码实现了一个完整的语义缓存系统:
import numpy as np from typing import Optional, Dict, Any from collections import OrderedDict import hashlib import time import json class SemanticCache: """语义缓存:基于向量相似度匹配避免重复API调用""" def __init__( self, embedding_func, similarity_threshold: float = 0.95, max_size: int = 1000, ttl_seconds: int = 3600 ): self.embedding_func = embedding_func self.threshold = similarity_threshold self.max_size = max_size self.ttl = ttl_seconds self.cache: OrderedDict = OrderedDict() def _get_embedding(self, text: str) -> np.ndarray: """获取文本的向量嵌入""" return self.embedding_func(text) def _cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float: """计算余弦相似度""" return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) def get(self, query: str) -> Optional[Dict[str, Any]]: """查找缓存:语义相似度超过阈值则命中""" query_emb = self._get_embedding(query) best_match = None best_sim = 0.0 for key, entry in self.cache.items(): sim = self._cosine_similarity(query_emb, entry["embedding"]) if sim > best_sim: best_sim = sim best_match = (key, entry) if best_match and best_sim >= self.threshold: key, entry = best_match # 检查TTL是否过期 if time.time() - entry["timestamp"] < self.ttl: # LRU: 移到末尾 self.cache.move_to_end(key) return { "response": entry["response"], "similarity": best_sim, "hit": True } else: # 过期则删除 del self.cache[key] return {"hit": False, "similarity": best_sim} def set(self, query: str, response: Any) -> None: """写入缓存""" emb = self._get_embedding(query) cache_key = hashlib.md5(query.encode()).hexdigest()[:12] # LRU淘汰 if len(self.cache) >= self.max_size: self.cache.popitem(last=False) self.cache[cache_key] = { "embedding": emb, "response": response, "timestamp": time.time(), "query_preview": query[:100] }
将缓存、压缩与批处理整合为统一的API调用客户端:
from concurrent.futures import ThreadPoolExecutor, as_completed class OptimizedAPIClient: """集成缓存与压缩的优化API客户端""" def __init__( self, api_client, embedding_func, cache_ttl: int = 3600, cache_threshold: float = 0.95 ): self.client = api_client self.compressor = PromptCompressor(api_client) self.cache = SemanticCache( embedding_func, similarity_threshold=cache_threshold, ttl_seconds=cache_ttl ) self.stats = { "total_requests": 0, "cache_hits": 0, "tokens_saved": 0 } def chat(self, prompt: str, compress: bool = True) -> dict: """单次请求(自动缓存与压缩)""" self.stats["total_requests"] += 1 # 1. 查缓存 cached = self.cache.get(prompt) if cached["hit"]: self.stats["cache_hits"] += 1 return { "response": cached["response"], "source": "cache", "similarity": cached["similarity"] } # 2. 压缩Prompt if compress: result = self.compressor.compress(prompt) final_prompt = result["compressed_prompt"] saved = len(prompt) - len(final_prompt) self.stats["tokens_saved"] += saved else: final_prompt = prompt # 3. 调用API response = self.client.chat( model="default", messages=[{"role": "user", "content": final_prompt}] ) result_text = response.choices[0].message.content # 4. 写缓存 self.cache.set(prompt, result_text) return {"response": result_text, "source": "api"} def batch_chat(self, prompts: list, max_workers: int = 5) -> list: """批处理请求""" results = [None] * len(prompts) with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit(self.chat, p): i for i, p in enumerate(prompts) } for future in as_completed(futures): idx = futures[future] results[idx] = future.result() return results
Q1:语义缓存的相似度阈值应该设为多少?
A1:取决于业务对准确性的要求。对于事实性问答类任务,建议设为0.97-0.99以确保准确性;对于创意写作类任务,可以适当降低至0.90-0.95,因为即使语义略有差异,生成的结果通常也是可接受的。建议从高阈值开始,逐步调整找到最佳平衡点。
Q2:Prompt压缩会影响输出质量吗?
A2:适当的压缩通常不会显著影响质量,但过度压缩可能丢失关键约束。建议的实践是:先用规则方法去除明显的冗余(通常可减少15%-25%),然后对压缩后的Prompt进行测试对比。对于包含复杂约束的任务,避免使用过于激进的模型辅助压缩。
Q3:多轮对话场景如何实现缓存?
A3:多轮对话的缓存较为复杂,因为上下文是累积的。推荐做法是:将完整对话历史作为一个整体进行缓存Key的计算,或者只缓存那些不依赖前序轮次的独立请求。另一种策略是"片段缓存"——对每轮独立的指令进行缓存,再在需要时组装完整上下文。
最佳实践:
常见避坑:
缓存命中率优化循环 ┌──────────┐ │ 监控命中率 │ <──────────────────┐ └────┬─────┘ │ ▼ │ ┌──────────┐ │ │ 分析未命中 │ ──→ 识别高频未命中 │ └────┬─────┘ │ ▼ │ ┌──────────┐ │ │ 调整阈值 │ ──→ 优化Embedding │ └────┬─────┘ │ ▼ │ ┌──────────┐ │ │ 验证效果 │ ──→ 对比质量指标 │ └────┬─────┘ │ └──────────────────────────┘
本节系统介绍了大模型API请求优化的四大策略:Prompt压缩、批处理请求、语义缓存和响应缓存。通过规则与模型相结合的压缩方法,可减少15%-50%的输入Token;通过语义相似度匹配的缓存系统,可避免大量重复调用。在实际项目中,建议采用分级缓存架构,并根据业务场景灵活调整参数,在成本与质量之间找到最佳平衡点。后续章节将进一步探讨并发控制与模型加速等更深层次的优化手段。
请求优化效果对比 未优化 ████████████████████ 100% 成本基准 压缩优化 ███████████████ 70% 降低30% 缓存优化 ████████ 40% 降低60% 综合优化 █████ 25% 降低75%