大模型推理成本,说白了就是Token成本。每一轮对话、每一次RAG检索、每一段系统提示词,最终都换算成Token,再从你的账户里扣钱。长文本场景下,这个问题尤为突出------上下文窗口越开越大,但每次请求塞进去的Token也越来越多,成本就这样不知不觉地失控了。
但有一个容易被忽视的事实:Token消耗量的高速增长并不意味着信息密度的等比提升。用户输入的长文里,真正有用的可能只有一小段;多轮对话的历史记录中,Agent真正需要记住的核心信息远比它"看完"的少。
这篇文章聚焦Prompt压缩与上下文窗口优化,给出四层可落地的降本策略,附带代码实现。
一、从认知层看Token浪费的本质
省钱的第一性原理不是"选便宜的模型",而是减少无效消耗。大模型服务的Token计费模型是线性的------输入Token越多越贵,输出Token越多越贵。根据实测数据,采用有效的上下文管理策略,Token使用量可以减少约65%。
Token浪费主要有三个来源:
- 上下文冗余:把整个几百行代码贴进对话框,让模型反复读那些根本没改过的部分
- 方向跑偏返工:需求没想清就开干,写了一半发现方向不对,推倒重来
- 重复交代:每个新会话都从头介绍背景信息,十次对话重复十次
核心应对策略可以概括为:减少冗余上下文,提高单次命中率。与其在单价上纠结,不如在效率上较真。
二、Prompt层面的压缩策略
2.1 设定"极简"人设
最简单有效的压缩方法是让模型少说废话。大模型经过RLHF对齐后往往喜欢做"端水大师"------"好的,我来为您解答......首先、其次、最后......希望这个回答对您有帮助......"。
在System Prompt里直接剥夺它的"客服属性":
python
COMPRESSED_SYSTEM_PROMPT = """
你是一个惜字如金的资深专家。
- 回答必须直击要害
- 禁止使用寒暄、过渡句、总结性废话
- 禁止说"好的""当然""综上所述"
- 如果不知道答案,直接说"无法回答"
"""
2.2 格式约束与Few-shot
把模型框死在特定格式里,它就没空间说废话了:
python
# 设置严格输出格式
FORMAT_CONSTRAINT = """
严格以JSON格式返回结果,不要包含任何JSON之外的文本。
不要说"这是您的JSON"或任何解释。
"""
# Few-shot示例让模型学会精简
FEW_SHOT_EXAMPLES = """
用户:中国的首都是哪?
助手:北京。
用户:水的化学式?
助手:H2O。
用户:{question}
助手:
"""
实测表明,Few-shot让模型模仿简洁的问答风格,比单纯说"请精简回答"有效得多。
2.3 句子级智能压缩
Token级压缩虽然灵活,但切除中间词元可能导致句子不连贯、语义受损。更优的做法是句子级压缩------根据句子与问题相关度决定保留还是删除。
python
import numpy as np
from typing import List, Tuple
class SentenceCompressor:
"""
基于相关度分数的句子级压缩器
"""
def __init__(self, threshold: float = 0.3):
self.threshold = threshold
def compute_relevance(self, sentence: str, query: str) -> float:
"""
计算句子与查询的相关度
实际场景可替换为Cross-Encoder或Embedding相似度
"""
# 简化实现:关键词重叠度
query_words = set(query.split())
sent_words = set(sentence.split())
overlap = len(query_words & sent_words) / max(len(query_words), 1)
return overlap
def compress(self, text: str, query: str) -> Tuple[str, float]:
"""
按句子切分、打分、过滤
返回压缩后的文本和压缩率
"""
# 按句子分隔(简化版)
sentences = text.replace("。", "。\n").split("\n")
sentences = [s.strip() for s in sentences if s.strip()]
scored_sentences = []
for sent in sentences:
score = self.compute_relevance(sent, query)
scored_sentences.append((sent, score))
# 保留分数高于阈值的句子
kept = [sent for sent, score in scored_sentences if score >= self.threshold]
# 如果全部被过滤,至少保留最相关的一句
if not kept:
kept = [max(scored_sentences, key=lambda x: x[1])[0]]
compressed = "。".join(kept)
ratio = len(compressed) / max(len(text), 1)
return compressed, ratio
# 使用示例
compressor = SentenceCompressor(threshold=0.25)
long_text = """RAG是检索增强生成的缩写。它通过检索外部知识库来增强LLM的生成能力。今天天气不错。RAG的核心是检索器与生成器的协同工作。"""
query = "什么是RAG?"
compressed, ratio = compressor.compress(long_text, query)
print(f"压缩率: {ratio:.2f}")
print(compressed)
# 输出:RAG是检索增强生成的缩写。它通过检索外部知识库来增强LLM的生成能力。RAG的核心是检索器与生成器的协同工作。
三、上下文窗口的"瘦身"架构
3.1 多级分层记忆
对于长对话场景,单一的记忆策略难以兼顾效率与准确性。推荐采用分层记忆架构:将对话历史分为"原始消息层"、"段落摘要层"和"标签摘要层",根据查询类型动态检索需要的信息。
python
class MultiLayerMemory:
"""
三层记忆架构:
- Level 0: 原始对话(完整保留最近N轮)
- Level 1: 段落摘要(对较早历史做结构化摘要)
- Level 2: 标签索引(跨会话的关键信息索引)
"""
def __init__(self, max_recent_turns: int = 5):
self.max_recent_turns = max_recent_turns
self.recent_history = [] # Level 0
self.summaries = [] # Level 1
self.tag_index = {} # Level 2
def build_context(self, query: str, max_tokens: int = 3000) -> str:
"""
在预算内组装上下文
"""
context_parts = []
token_budget = max_tokens
# 1. 优先加入最近的对话(最可能相关)
for turn in self.recent_history[-self.max_recent_turns:]:
context_parts.append(turn)
# 2. 如果还有预算,加入相关摘要
# 3. 如果还有预算,从标签索引中检索关键事实
# (具体实现取决于Token计数策略)
return "\n".join(context_parts)
这种架构的关键优势在于:即使原始对话有200K Token,实际组装进Prompt的也可以控制在30K以内,同时保证关键信息不丢失。
3.2 基于注意力权重动态剪枝
对于单次大文档推理,可以利用模型自身的注意力权重来识别重要Token。该策略通过分析自注意力层的权重分布,判断每个Token对最终输出的贡献度,并据此决定保留或丢弃。
python
def prune_by_attention(
tokens: List[str],
attention_weights: List[float],
threshold: float = 0.1,
key_entities: set = None
) -> List[str]:
"""
根据注意力权重剪枝低价值token
"""
if key_entities is None:
key_entities = set()
filtered = []
for token, weight in zip(tokens, attention_weights):
# 保留高注意力token或关键实体
if weight > threshold or token in key_entities:
filtered.append(token)
return filtered
这一方法在RAG-MCP架构中被用于在"语义解析层"提炼核心语义,从而显著缩短传给生成模型的Prompt长度。
3.3 Squeezed Attention:KV Cache层面的压缩
更进一步,可以从注意力机制本身的优化入手。Squeezed Attention提出了一种针对固定长上下文的优化思路:对固定上下文中的Key向量进行离线聚类,每个簇用一个中心向量代表;在线推理时,只将用户Query与这些中心向量比较,仅保留语义相关的原始Key参与注意力计算。
python
class SqueezedAttention:
"""
基于K-means聚类的KV Cache压缩
"""
def __init__(self, num_clusters: int = 64):
self.num_clusters = num_clusters
self.centroids = None
self.cluster_to_keys = {}
def compress_fixed_context(self, key_vectors: np.ndarray):
"""
离线:对固定上下文的Key进行聚类
"""
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=self.num_clusters)
labels = kmeans.fit_predict(key_vectors)
self.centroids = kmeans.cluster_centers_
# 记录每个簇对应的原始Key索引
for idx, label in enumerate(labels):
self.cluster_to_keys.setdefault(label, []).append(idx)
def select_keys_for_query(self, query_vector: np.ndarray) -> List[int]:
"""
在线:根据Query与聚类中心的相似度选择重要簇
"""
# 计算Query与所有簇中心的相似度
similarities = query_vector @ self.centroids.T
# 取Top-K个最相关的簇
top_clusters = np.argsort(similarities)[-5:] # 取5个簇
# 返回这些簇对应的原始Key索引
selected_indices = []
for cluster in top_clusters:
selected_indices.extend(self.cluster_to_keys[cluster])
return selected_indices
官方实验表明,该方法在LLaMA-2-7B-32K模型上,实现了3.1倍KV预算压缩而无明显精度损失 ,在Prefill和生成阶段均获得了4倍以上的加速。
四、系统性降本策略组合
单个技术点的优化效果有限,真正的"瘦身"需要组合拳:
| 层次 | 技术手段 | 预计收益 |
|---|---|---|
| Prompt层 | 精简系统提示 + Few-shot + 停止词 | 10-30% |
| 上下文层 | 句子级压缩 + 分层记忆 | 30-50% |
| 推理层 | KV Cache压缩 + 注意力剪枝 | 30-60% |
| 架构层 | 缓存复用 + 模型路由分级 | 20-40% |
综合应用多项技术后,Token消耗降低50-70%是完全可行的。
API调用时的附加技巧 :合理使用stop参数可以在模型输出达到预期结果时物理截断生成,避免"希望这个回答对您有帮助"这类后缀词元产生。调高frequency_penalty和presence_penalty参数也能减少重复短语的生成,进一步压缩输出长度。
总结
Token"瘦身"的本质不是"少用AI",而是更精细地分配AI资源。从Prompt人设设计到句子级智能压缩,从分层记忆架构到KV Cache层面的注意力优化,每一层都有可挖掘的降本空间。
核心要点:
- Token浪费的主因是上下文冗余和无效输出,而非模型单价
- Few-shot示例比一句"请精简回答"更能教会模型如何"少说话"
- 分层记忆架构可以在长对话场景下将200K上下文压缩到30K内运行
- Squeezed Attention等底层优化可在精度无损前提下实现3-8倍压缩
记住:省下来的每一个Token,都是直接落到净利润上的。在规模化部署的场景下,这些优化带来的成本差异会以指数级放大。