Prompt 压缩技术:用 LLMLingua 把长 Prompt 压缩到原始长度的 30% Prompt 压缩技术用 LLMLingua 把长 Prompt 压缩到原始长度的 30%一、深度引言与场景痛点RAG 系统里最常见的一个操作是检索回 10 个文档块每个 500 字加上 system prompt 和用户问题拼在一起凑成 6000 Token 的 prompt 发给 LLM。一次请求 6000 Token10 QPS 就是 6 万 Token/秒。一个月下来Token 费用可能超过服务器成本。但仔细观察会发现检索回来的文档块里有一大半是废话。前置语、转折句、格式描述、重复信息——这些内容对 LLM 推理帮助很小但照样按 Token 计费。如果能有一种方法在送入 LLM 之前先把这些冗余信息压缩掉既保留语义又省钱那就太好了。LLMLingua 就是干这个的。它基于一个简单但有效的洞察用小语言模型来评估大语言模型的每 Token 重要性把不重要的 Token 删掉把重要的保留。压缩率可达 2~5 倍也就是能把 prompt 压到原始长度的 20%~50%。二、底层机制与原理深度剖析flowchart TD A[原始 Prompt (N tokens)] -- B[分词: Tokenize] B -- C[用小模型计算每个 token 的 perplexity] C -- D[筛选: perplexity 低的 token → 高信息密度] D -- E[计算 token 间的注意力分数] E -- F[根据注意力分数和 budget 控制器选择保留 token] F -- G{是否达到目标压缩比?} G --|否| H[放宽筛选阈值, 增加保留 token] H -- F G --|是| I[去标记化: Detokenize] I -- J[压缩后 Prompt (M tokens, M N)]LLMLingua 不直接调大模型做压缩而是用一个小模型如 GPT-2 Small 或 LLaMA-7B来估算每个 Token 的信息密度。它的假设是如果一个 Token 让模型感到意外perplexity 高说明它携带了大量信息应该保留。反之可以安全删除的词如的了我们注意到perplexity 低可以被压缩。注意力分数是另一个关键筛选维度。LLMLingua 计算 Token 之间的注意力权重如果某个 Token 对其他 Token 的注意力贡献接近零说明它在上下文中是孤立的可以删除。这个机制对清理 RAG 检索块里的格式噪音特别有效——文档里的页码、脚注、版权声明通常和其他内容没有注意力关联。Budget 控制器保证压缩不会过度。你可以设一个目标压缩率比如 0.3 即压缩到 30%LLMLingua 会按 perplexity 和注意力分数的综合得分从高到低选 Token直到满足目标。三、生产级代码实现from __future__ import annotations import asyncio from dataclasses import dataclass from typing import Optional dataclass class CompressedPrompt: original_tokens: int compressed_tokens: int compression_ratio: float text: str removed_ratio: float class PromptCompressor: Prompt 压缩器 —— 封装 LLMLingua 的调用逻辑 def __init__( self, model_name: str microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank, device: str cpu, target_ratio: float 0.3, ): self._target_ratio target_ratio self._initialized False self._model_name model_name self._device device self._llmlingua None async def _ensure_initialized(self) - None: if self._initialized: return try: from llmlingua import PromptCompressor as LLPromptCompressor self._llmlingua LLPromptCompressor( model_nameself._model_name, device_mapself._device, use_auth_tokenFalse, ) self._initialized True except ImportError: raise RuntimeError(请安装 llmlingua: pip install llmlingua) async def compress( self, prompt: str, target_ratio: Optional[float] None, context_budget: Optional[int] None, ) - CompressedPrompt: await self._ensure_initialized() ratio target_ratio or self._target_ratio try: compressed await asyncio.to_thread( self._llmlingua.compress_prompt, prompt, rateratio, force_tokens[\n, ?, 。, , ], chunk_end_tokens[\n\n, 。\n], context_budgetcontext_budget or inf, ) except Exception as e: # 压缩失败时返回原始 prompt return CompressedPrompt( original_tokenslen(prompt), compressed_tokenslen(prompt), compression_ratio1.0, textprompt, removed_ratio0.0, ) original_len len(compressed.get(original_prompt, prompt)) compressed_len len(compressed.get(compressed_prompt, prompt)) return CompressedPrompt( original_tokenscompressed.get(original_tokens, 0), compressed_tokenscompressed.get(compressed_tokens, 0), compression_ratiocompressed_len / max(original_len, 1), textcompressed.get(compressed_prompt, prompt), removed_ratio1.0 - compressed_len / max(original_len, 1), ) class RAGPromptPipeline: RAG 场景的 Prompt 管道检索 → 压缩 → LLM def __init__( self, compressor: PromptCompressor, llm_fn, max_prompt_tokens: int 8000, ): self._compressor compressor self._llm llm_fn self._max_tokens max_prompt_tokens async def process( self, query: str, retrieved_chunks: list[str], system_prompt: str ) - str: # 拼原始 prompt raw_context \n\n.join( f[文档{i 1}]\n{chunk} for i, chunk in enumerate(retrieved_chunks) ) full_prompt f{system_prompt}\n\n参考文档:\n{raw_context}\n\n问题: {query} # 压缩 compressed await self._compressor.compress( full_prompt, target_ratio0.35, context_budgetself._max_tokens, ) # 调用 LLM try: return await self._llm(compressed.text) except Exception: # 压缩后仍失败用原始 prompt 重试 return await self._llm(full_prompt[: self._max_tokens])compress方法有两个降级策略。第一如果 LLMLingua 本身加载或调用失败直接返回原始 prompt虽然不省钱但不影响功能。第二asyncio.to_thread把压缩操作放到线程池避免阻塞事件循环——因为 LLMLingua 内部可能做同步推理。RAGPromptPipeline把压缩嵌入到标准 RAG 管道里。压缩后的 prompt 如果还是超过了 LLM 的 context window会触发context_budget限制LLMLingua 会在这个预算内尽量保留关键 Token。最后一层保障是即使压缩后的 prompt 调用 LLM 失败也会用截断的原始 prompt 重试。四、边界分析与架构权衡LLMLingua 虽然好用但不是所有场景都适合。第一个禁区是代码生成。Prompt 里的代码片段不能压缩删掉一个括号语义就变了。所以在代码场景下应该把代码块标记为不可压缩。第二个边界是数学推理。Prompt 里的数值、公式、逻辑符号必须原样保留。压缩可能把x 3.14159压成x 3.14导致计算结果完全偏移。第三个提醒是压缩后 prompt 的可读性会下降很多人已经看不懂了。这意味着调试变难了。建议在开发环境保留原始 prompt 和压缩后的 prompt 的对照日志方便排查 LLM 响应异常时到底发生了什么。性能开销也需要考量。LLMLingua 用 BERT 级别的小模型做压缩在 CPU 上处理 6000 Token 的 prompt 大约需要 200~500ms。如果你的 RAG 服务追求 p99 延迟在 1 秒内这 500ms 的压缩开销可能不划算。GPU 推理可以把压缩延时降到 50ms 以内但需要额外 GPU 资源。五、总结Prompt 压缩是 RAG 落地的隐形省钱神器。LLMLingua 用小模型评估大模型的 Token 信息密度在保留语义的前提下把 prompt 压到原始 30% 左右。核心优势是节省 Token 成本副作用是压缩耗时和可读性下降。落地建议只在上下文超过 2000 Token 时触发压缩代码和数学内容标记为不可压缩CPU 环境预留 300ms 压缩耗时压缩失败时要能降级到原始 prompt 继续服务。省钱不能以牺牲可用性为代价。