做大模型应用开发,提示词长度是个绕不开的问题。单轮对话还好,一旦涉及多轮对话、RAG知识库检索或者长文档处理,上下文很容易膨胀到几万token。token多了不只是花钱多,模型对长上下文的注意力分配也会变得稀疏,中间部分的内容容易被忽略,业界常说的lost in the middle现象就是这么来的。这篇文章聊聊怎么通过上下文压缩和关键信息提取,把提示词控制在合理范围内。

为什么提示词过长是个必须解决的问题
首先是最直观的成本问题。无论调用哪家模型的API,计费基本都按输入加输出的token算。一个客服系统如果每轮对话都把完整历史塞进去,第十轮的输入量可能是第一轮的十倍,费用随之线性上涨。对于日活高的产品,这笔账算下来非常吓人。
其次是性能和质量问题。长上下文会显著拉长首token响应时间,用户体验变差。更隐蔽的是质量问题:模型对上下文中间位置的信息利用率明显偏低,开头和结尾的内容记得牢,中间的容易丢。你精心拼进去的检索结果如果排在中间位置,模型可能压根没注意。
最后是硬性限制。每家模型都有最大上下文窗口,虽然现在不少模型号称支持128K甚至更长,但一旦超限直接报错,或者在长输入下截断早期内容,对话的连贯性就被破坏了。与其被动触发限制,不如主动做压缩管理。
上下文压缩的主流方案对比
上下文压缩的核心思路是:用更少的token保留尽可能多的有效信息。工程上常见的做法有几种,各有适用场景。
第一种是摘要式压缩。用模型本身对历史对话做总结,把几十轮对话压成一段几百token的摘要,每轮对话后增量更新。这种方式信息保留度最高,但会引入额外的模型调用成本,而且摘要过程可能丢失关键细节,比如用户提过的订单号、具体时间点。解决办法是把关键实体单独抽取出来存储,不依赖摘要传递。
第二种是滑动窗口。只保留最近N轮对话,更早的直接丢弃。实现最简单,几乎零成本,但会丢失久远的上下文。适合闲聊、简单问答这类对历史依赖弱的场景,不适合需要记住用户早期偏好的任务。
第三种是混合策略,也是实践中效果最稳的:系统提示词和关键事实永久保留,最近几轮对话原样保留,中间的历史用摘要压缩。下面是一段示例代码,演示如何组织这种分层上下文。
def build_context(system_prompt, summary, recent_turns, keep_recent=4):
"""构建压缩后的上下文:系统提示 + 摘要 + 最近对话"""
parts = [system_prompt]
if summary:
parts.append("之前的对话摘要:\n" + summary)
# 只保留最近 keep_recent 轮的完整对话
for turn in recent_turns[-keep_recent:]:
parts.append(f"用户:{turn['user']}\n助手:{turn['assistant']}")
return "\n\n".join(parts)
def update_summary(old_summary, finished_turns, llm):
"""用增量方式更新历史摘要,控制摘要长度"""
prompt = f"请将以下内容合并进已有摘要,输出不超过300字的精简摘要:\n已有摘要:{old_summary}\n新增对话:{finished_turns}"
return llm.generate(prompt)这段代码的关键点在于分层:系统提示词永远在,摘要在中间承接久远历史,最近对话保持原汁原味。这样模型既有完整背景,又能看到最新的表达细节。
关键信息提取:让检索结果只留精华
RAG场景是提示词膨胀的重灾区。一次向量检索往往返回top5甚至top10的文档片段,每个片段几百token,拼起来轻松过万。这时候需要在拼进提示词之前做一轮关键信息提取。
第一步是重排序。向量检索的召回结果按相似度排序,但相似度高不等于回答问题需要的信息多。可以用交叉编码器对查询和每个片段做精细打分,只保留重排后得分最高的两三个片段。这一步通常能砍掉一半以上的token。
第二步是片段内抽取。一个500token的文档片段里,真正相关的可能就一两句话。可以让模型先做一轮抽取,只保留与问题直接相关的句子,再送入最终回答。这相当于两段式调用:第一次调用做信息过滤,第二次调用基于干净的信息生成回答。虽然多了一次调用,但第二次调用的输入大幅缩短,总成本往往更低,回答质量还更好。
def extract_relevant(query, chunks, llm, max_chars=600):
"""从检索片段中抽取与问题相关的关键信息"""
joined = "\n---\n".join(chunks)
prompt = (
f"问题:{query}\n\n"
f"以下是检索到的资料片段,请只提取与问题直接相关的内容,"
f"总长度不超过{max_chars}字,去掉无关描述:\n{joined}"
)
return llm.generate(prompt)第三步别忘了指令本身的精简。不少团队写系统提示词喜欢堆规则,几十条要求写上几千token。其实很多规则可以合并、删减,示例从五个减到两个往往效果不减。指令部分是每轮都要付费的固定开销,值得反复打磨。可以用工具统计每个部分的token占比,优先优化占比最大的那块。
压缩过程中容易踩的坑
第一个坑是压缩时机选错。有人每轮对话都重新做一次全量摘要,成本翻倍。合理做法是设置阈值,比如上下文超过3000token才触发压缩,平时不动。
第二个坑是关键信息丢失。摘要最容易把数字、日期、编号这类硬信息弄丢。建议维护一个独立的结构化存储,比如用户档案字典,把订单号、姓名、偏好这些字段单独存,每轮拼在上下文里,体积小且永不丢失。
第三个坑是过度压缩。把上下文压得太狠,模型会开始编造缺失的信息,幻觉率上升。压缩的目标是去掉冗余,不是去掉内容本身。上线前建议做A/B测试,对比压缩前后的回答准确率,找到一个质量和成本都 acceptable 的平衡点。
总的来说,上下文压缩和关键信息提取不是一次性的技术选型,而是需要持续监控和调优的工程环节。把token用量打点记录下来,观察成本曲线和回答质量的变化,才能让这套优化真正落地见效。