ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI应用开发中的上下文压缩技术:从摘要生成到工程实践

AI应用开发中的上下文压缩技术:从摘要生成到工程实践 1. 项目概述为什么“压缩”是AI应用开发的关键一步如果你正在学习AI应用开发并且已经走到了需要处理“上下文消息”这一步那么恭喜你你已经触及了构建实用AI应用的核心挑战之一。无论是开发一个智能客服助手、一个文档分析工具还是一个多轮对话的创意伙伴你很快会发现大模型如GPT、Claude等有一个绕不过去的“硬约束”上下文长度限制。这个限制就像一条高速公路虽然宽阔但长度有限。你的对话历史、提供的文档资料、系统指令所有东西都在这条路上排队。当信息太多超出了路的尽头最早进入的信息就会被“挤下去”——这就是模型会遗忘对话开头内容的原因。“15天学会AI应用开发”这个系列前面可能已经带你搭建了环境、调用了API、处理了基础对话。而第五天的主题——“使用AI摘要来压缩上下文消息”正是从“能用”到“好用”的关键跃升。这不仅仅是技术上的一个技巧更是一种重要的工程化思维。直接给模型塞入一篇万字长文让它总结和先让模型对长文进行分段摘要、再基于摘要进行深度分析效果和成本是天壤之别。后者就是“压缩上下文”的核心思想我们不是要抛弃信息而是要通过AI本身提炼信息的精华用更少的“令牌”Token承载相同的语义价值。最近的热词里频繁出现“ClaudeCode压缩上下文命令”、“AI大模型应用开发”这正说明了行业已经将上下文管理视为一项基础且关键的能力。无论是降低API调用成本还是突破模型上下文窗口限制以处理更长文本亦或是提升模型在长文档中的关注精度消息压缩技术都是必由之路。本文将从一个实践者的角度深入拆解如何利用AI生成摘要来有效压缩对话或文档上下文。我会分享具体的策略、不同场景下的代码实现、以及我在项目中踩过的坑和总结的优化技巧。我们的目标很明确用有限的“道路”承载更远、更精准的“行程”。2. 核心思路拆解摘要压缩的本质与策略选择在动手写代码之前我们必须想清楚我们到底要压缩什么以及为什么要用“摘要”这种方式来压缩2.1 理解上下文消息的构成与瓶颈一个典型的AI对话上下文通常由以下几部分组成系统指令System Prompt定义AI的角色、行为规范和回答格式。这部分通常较为固定是每次对话的“背景板”。对话历史Chat History用户与AI之间一来一往的多轮消息。这是随着对话进行不断增长的部分。当前查询Current Query用户本次提出的问题或请求。知识文档Retrieved Documents从外部知识库中检索出来的、与当前查询相关的参考材料。在RAG检索增强生成应用中这部分可能非常庞大。瓶颈主要出现在对话历史和知识文档上。一场长达50轮的对话其历史消息的令牌数可能高达数千甚至上万。如果检索系统一次性返回了5篇相关的PDF文档内容令牌数轻松突破数万。而目前主流模型的上下文窗口在8K到128K之间昂贵的128K窗口模型其单次调用成本也相当可观。2.2 摘要压缩 vs. 其他压缩策略压缩上下文并非只有摘要这一条路但摘要往往是平衡“信息保留度”和“压缩率”的最佳选择之一。我们来对比几种常见策略压缩策略工作原理优点缺点适用场景简单截断直接丢弃最早的消息保留最新的消息。实现简单零成本。丢失关键历史信息可能导致对话逻辑断裂。对历史依赖不强的闲聊场景。滑动窗口保留一个固定大小的最近消息窗口。相对简单能保留近期上下文。仍然会丢失窗口外的历史且窗口大小难以设定。短期记忆重要的任务型对话。关键信息提取使用NER命名实体识别等技术提取人名、地点、时间等关键实体。保留核心事实要素。丢失实体间的逻辑关系和叙述脉络信息碎片化。事实查询、信息核对类应用。AI生成摘要本文重点调用AI模型将一段冗长的上下文提炼成简洁的摘要。保留语义和逻辑关系信息密度高压缩效果好。需要额外的模型调用产生少量成本摘要质量依赖提示词。绝大多数需要长期记忆和复杂逻辑理解的场景如深度分析、多轮规划、长文档处理。向量记忆将历史消息编码成向量存入数据库需要时通过相似度检索召回。理论上可以记忆无限长的历史。实现复杂检索可能不准确存在“幻觉”召回风险无法直接作为模型上下文。通常作为摘要压缩的补充用于存储非常长期或非连续性的记忆。实操心得在实际项目中我很少使用单一策略。一个成熟的方案往往是“摘要压缩”为主“滑动窗口”保底再结合“向量记忆”作为长期档案库。例如每10轮对话就用AI将前10轮摘要成一段话然后将这段话作为新的“历史起点”清空原始消息。这样既能保持逻辑连贯又能将令牌消耗控制在恒定水平。2.3 设计摘要压缩的工作流一个健壮的摘要压缩模块其工作流应该是清晰且可配置的触发判断何时需要压缩是基于令牌数阈值如总令牌 4000还是基于对话轮数如每5轮或是基于自定义规则如用户开启新话题内容选取压缩哪部分内容是压缩全部对话历史还是仅压缩系统指令之外的部分是否需要区分用户消息和AI消息摘要生成使用什么样的提示词Prompt来指导AI生成高质量的摘要如何确保摘要客观、全面上下文重组生成摘要后如何用它替换原有的冗长历史是直接替换还是将摘要作为一条特殊的“系统消息”插入元数据保留原始消息中可能包含一些无法被摘要的元信息如消息ID、时间戳、特定标记是否需要以及如何保留在接下来的部分我们将深入每个环节给出具体的代码实现和优化技巧。3. 核心细节解析构建一个可用的摘要压缩器让我们暂时抛开复杂的架构先实现一个最基础、但完全可用的摘要压缩函数。我们将使用OpenAI的GPT模型作为摘要引擎但思路适用于任何Chat模型。3.1 环境准备与基础依赖首先确保你的开发环境已就绪。这里假设你使用Python并已安装openai库。pip install openai你需要一个有效的OpenAI API密钥并将其设置为环境变量。import os import openai from typing import List, Dict, Any # 设置你的API密钥 openai.api_key os.getenv(OPENAI_API_KEY) # 定义一个简单的消息结构与OpenAI API兼容 Message Dict[str, str] # 例如{role: user, content: ...}3.2 实现基础摘要生成函数这个函数接收一段文本即我们要压缩的对话历史并返回模型生成的摘要。def generate_summary(text_to_compress: str, model: str gpt-3.5-turbo) - str: 使用AI模型生成给定文本的摘要。 参数: text_to_compress: 需要被压缩/摘要的文本内容。 model: 用于生成摘要的模型默认为gpt-3.5-turbo以控制成本。 返回: 生成的摘要文本。 # 构建摘要生成的提示词。这是影响摘要质量的关键 summary_prompt f 请将以下对话内容提炼成一个简洁、客观的摘要。摘要需要保留对话的核心事实、用户的主要诉求以及AI的关键回应。避免添加任何评价性语言或新的信息。 对话内容 {text_to_compress} 摘要 try: response openai.ChatCompletion.create( modelmodel, messages[ {role: system, content: 你是一个专业的文本摘要助手。}, {role: user, content: summary_prompt} ], temperature0.2, # 低温度保证摘要的客观性和稳定性 max_tokens500 # 限制摘要长度可根据需要调整 ) summary response.choices[0].message.content.strip() return summary except Exception as e: print(f生成摘要时出错: {e}) # 降级策略如果摘要失败返回一个简单的截断提示 return f[对话历史摘要因错误而简化原始内容已部分移除。错误{e}]注意事项提示词Prompt是摘要质量的灵魂。上面的提示词强调了“客观”、“保留核心事实和诉求”。你可以根据你的应用场景调整它。例如如果是客服场景可以要求“重点摘要用户的问题和已提供的解决方案”如果是创意写作场景可以要求“保留故事主线、人物关系和关键情节转折”。3.3 实现上下文压缩与重组函数现在我们创建一个更智能的函数它负责管理整个对话上下文并在需要时触发摘要压缩。class ConversationCompressor: def __init__(self, token_threshold: int 3000, summary_model: str gpt-3.5-turbo): 初始化对话压缩器。 参数: token_threshold: 触发压缩的令牌数阈值。注意这里是估算值精确计算需使用tiktoken库。 summary_model: 用于摘要的模型。 self.token_threshold token_threshold self.summary_model summary_model self.conversation_history: List[Message] [] # 存储完整的消息历史 self.compressed_history: List[Message] [] # 存储压缩后的摘要块 def _estimate_tokens(self, text: str) - int: 简单估算文本的令牌数。一个粗略的方法是对于英文1个token约等于0.75个单词对于中文1个token约等于1.5-2个汉字。 生产环境建议使用 tiktoken 库进行精确计算。 # 这是一个非常粗略的估算仅用于演示。 # 假设中英文混合平均按1个token对应2个字符估算。 return len(text) // 2 def _get_history_text(self, messages: List[Message]) - str: 将消息列表格式化为纯文本用于摘要生成。 history_text for msg in messages: role msg[role].upper() content msg[content] history_text f{role}: {content}\n return history_text.strip() def add_message(self, role: str, content: str): 添加一条新消息到完整历史记录。 self.conversation_history.append({role: role, content: content}) def get_compressed_context(self) - List[Message]: 获取用于最终发送给AI模型的压缩后上下文。 此方法会检查当前历史长度如果超过阈值则对最早的部分进行摘要压缩。 # 估算当前完整历史的令牌数 full_context self.compressed_history self.conversation_history full_text self._get_history_text(full_context) estimated_tokens self._estimate_tokens(full_text) # 如果未超阈值直接返回组合后的上下文 if estimated_tokens self.token_threshold: return full_context # 如果超阈值需要压缩 print(f上下文令牌数({estimated_tokens})超过阈值({self.token_threshold})触发压缩...) # 策略将当前完整的 conversation_history 进行摘要 if self.conversation_history: history_to_compress self._get_history_text(self.conversation_history) summary generate_summary(history_to_compress, self.summary_model) # 将生成的摘要作为一个特殊的“系统”消息添加到压缩历史中 summary_message {role: system, content: f【历史对话摘要】{summary}} self.compressed_history.append(summary_message) # 清空当前对话历史新的对话将从这里开始累积 self.conversation_history [] print(f已生成摘要并压缩历史。摘要内容{summary[:100]}...) # 返回压缩后的历史 可能为空的新对话历史 return self.compressed_history self.conversation_history def get_full_history_for_logging(self) - List[Message]: 获取完整的、未压缩的历史记录用于日志或调试。 return self.compressed_history self.conversation_history这个ConversationCompressor类提供了一个基本框架。它的核心逻辑是维护两个列表compressed_history存放摘要块和conversation_history存放最新未压缩的对话。每次需要获取发送给模型的上下文时get_compressed_context检查总长度。如果超长就将当前的conversation_history用AI摘要成一段话把这段话作为一个system角色消息存入compressed_history然后清空conversation_history。最终返回compressed_history conversation_history作为模型的实际输入。实操心得将摘要作为system角色消息是一个小技巧。因为system消息通常用于定义AI行为模型会高度重视其中的内容。这确保了被压缩的历史摘要能持续影响后续对话。你也可以使用user角色但可能会与真实用户消息混淆。4. 实操过程集成到AI应用并处理复杂场景有了核心压缩器我们需要将其集成到一个真实的对话循环中并处理一些更复杂的实际情况。4.1 构建一个简单的对话循环下面是一个集成了摘要压缩功能的简单对话应用示例def main_chat_loop(): 一个集成了上下文压缩的简单对话循环。 compressor ConversationCompressor(token_threshold1500) # 设置较低的阈值以便演示 # 初始系统指令 system_instruction 你是一个乐于助人的AI助手。请根据对话历史友好、准确地回答用户的问题。 compressor.add_message(system, system_instruction) print(对话开始输入‘退出’结束) while True: # 获取用户输入 user_input input(\n用户: ) if user_input.lower() in [退出, exit, quit]: break # 1. 将用户消息添加到历史 compressor.add_message(user, user_input) # 2. 获取压缩后的上下文准备发送给AI messages_for_ai compressor.get_compressed_context() # 3. 调用AI模型获取回复 try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, messagesmessages_for_ai, temperature0.7, max_tokens500 ) ai_reply response.choices[0].message.content.strip() # 4. 将AI回复添加到历史 compressor.add_message(assistant, ai_reply) print(fAI助手: {ai_reply}) # 可选打印当前上下文结构用于调试 # print(f[调试] 当前压缩历史块数{len(compressor.compressed_history)}) # print(f[调试] 当前未压缩轮数{len(compressor.conversation_history)}) except Exception as e: print(f调用AI API时出错: {e}) ai_reply 抱歉我暂时无法处理您的请求。 print(fAI助手: {ai_reply}) print(\n对话结束。) print( 完整对话历史含摘要) for msg in compressor.get_full_history_for_logging(): print(f{msg[role]}: {msg[content][:200]}...) # 只打印前200字符 if __name__ __main__: main_chat_loop()运行这个程序进行一段较长的对话。当对话轮数增加总令牌数估计超过1500时你会看到控制台输出“触发压缩...”的提示之后的历史将被一个摘要块替代。4.2 处理知识文档RAG场景的压缩在RAG应用中我们检索到的文档知识往往是令牌消耗的大头。针对文档的压缩策略需要微调。我们不应该压缩整个对话历史时把文档也混进去而是应该单独对检索到的文档进行摘要。def compress_retrieved_documents(documents: List[str], query: str, max_doc_tokens: int 1000) - str: 压缩检索到的文档使其更适合放入上下文。 参数: documents: 检索到的原始文档文本列表。 query: 用户当前查询用于指导摘要更有针对性。 max_doc_tokens: 所有文档摘要合并后的目标令牌数。 返回: 压缩合并后的文档摘要文本。 if not documents: return combined_text \n\n---文档分割线---\n\n.join(documents) # 使用针对性的提示词要求摘要围绕用户查询展开 doc_prompt f 用户正在查询“{query}” 以下是检索到的相关文档内容。请为这些文档生成一个统一的、简洁的摘要。 摘要应聚焦于与用户查询最相关的信息提取关键事实、数据和观点。 如果不同文档间存在矛盾请在摘要中指出。 请将摘要控制在{max_doc_tokens // 2}字以内中文。 文档内容 {combined_text} 统一摘要 try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, messages[ {role: system, content: 你是一个文档分析专家擅长提炼核心信息。}, {role: user, content: doc_prompt} ], temperature0.1, # 极低温度确保事实准确性 max_tokensmax_doc_tokens ) summary response.choices[0].message.content.strip() return summary except Exception as e: print(f文档摘要生成失败: {e}) # 降级策略返回前N个文档的拼接可进一步截断 fallback \n.join(doc[:200] for doc in documents[:2]) # 取前两个文档的前200字 return f[文档摘要生成失败以下是部分原始内容]\n{fallback}在你的RAG流程中在将文档插入最终上下文之前先调用这个函数进行压缩# 假设你已经通过检索获取了 raw_docs compressed_knowledge compress_retrieved_documents(raw_docs, user_question) # 然后将 compressed_knowledge 作为一条单独的消息如 role: “system” content: f“参考知识{compressed_knowledge}”加入对话上下文 # 这样无论检索到多少文档最终占用的令牌数都是可控的。4.3 分层与增量压缩策略对于超长对话或极其复杂的任务单次压缩可能不够。我们可以采用分层或增量压缩策略。分层压缩对不同“重要性”的历史采用不同的压缩粒度。例如将对话分为“当前会话”最近10轮和“过往会话”。对“过往会话”进行高度凝练的摘要如一句话总结主题对“当前会话”进行稍详细的摘要。增量压缩不是等积累了很多历史再一次性压缩而是定期进行“微压缩”。例如每3轮对话后就将这3轮对话压缩成一句话插入历史。这样上下文始终由一系列连续的“摘要句”和最新的原始对话组成逻辑链更清晰且令牌数增长非常缓慢。实现增量压缩只需修改触发判断逻辑将基于令牌阈值改为基于轮数并在add_message方法中自动触发即可。5. 常见问题与排查技巧实录在实际开发和部署中你会遇到各种各样的问题。下面是我总结的一些典型坑点和解决方案。5.1 摘要质量不佳丢失关键信息问题表现AI生成的摘要过于笼统遗漏了用户之前提到的具体数字、要求或关键细节导致后续回答出错。排查与解决优化提示词在提示词中明确要求“保留具体数字、日期、名称等细节信息”。例如“摘要需保留所有提及的具体数据如价格、数量、日期、产品名称和人名。”分而治之如果一段历史包含多个独立主题先将其按主题分割再分别摘要最后合并。这比一次性摘要一大段混合内容效果更好。提升模型能力对于关键任务使用更强大的模型如GPT-4进行摘要虽然成本更高但准确性和细节保留度显著提升。人工模板辅助对于高度结构化的信息如订单信息、用户资料可以设计模板让AI提取信息后填入模板而不是自由摘要。例如“用户需求{需求}已同意方案{方案}待解决问题{问题}”。5.2 压缩后对话逻辑断裂或AI“失忆”问题表现进行压缩后AI似乎忘记了摘要中已包含的某些信息或者对上下文的连贯性理解变差。排查与解决检查摘要角色确保摘要消息的角色是system。如前所述system指令对模型有更强的影响力。在摘要中明确时间/逻辑关系让摘要包含简单的逻辑连接词。例如不要只写“用户喜欢蓝色。用户预定了会议室。”而应写“首先用户表达了对蓝色产品的偏好。随后用户预定了明天下午2点的101会议室。”保留最近原始消息采用“摘要最近N轮原始消息”的混合模式。即使压缩了早期历史也始终保留最新的2-3轮原始对话。这能保证最近互动的鲜活度和精确性。测试不同压缩触发点过早压缩阈值太低会导致信息过早被“抽象化”增加模型理解负担。过晚压缩则失去意义。需要通过实际测试找到适合你应用场景的最佳阈值。5.3 令牌数估算不准导致意外超限或成本浪费问题表现自己估算的令牌数与API实际消耗的令牌数差异很大要么频繁触发压缩影响体验要么迟迟不压缩导致调用失败超出模型上下文限制或成本过高。排查与解决必须使用官方库精确计算对于OpenAI模型使用tiktoken库。安装pip install tiktoken。下面是一个计算函数import tiktoken def num_tokens_from_messages(messages, modelgpt-3.5-turbo-0613): 返回消息列表的精确令牌数。来自OpenAI官方Cookbook。 try: encoding tiktoken.encoding_for_model(model) except KeyError: encoding tiktoken.get_encoding(cl100k_base) tokens_per_message 3 # 每条消息的额外开销 tokens_per_name 1 num_tokens 0 for message in messages: num_tokens tokens_per_message for key, value in message.items(): num_tokens len(encoding.encode(value)) if key name: num_tokens tokens_per_name num_tokens 3 # 回复开始的额外开销 return num_tokens为系统预留缓冲区不要将压缩阈值设置为模型上下文窗口的极限如128K。应为AI的回复预留空间。通常我会设置阈值为0.7 * 模型上下文窗口。例如对于8K窗口阈值设为5600左右。监控与告警在生产环境中记录每次API调用的实际令牌消耗响应中有usage字段并设置告警。如果发现平均消耗持续高于预期需要重新评估压缩策略。5.4 摘要生成API调用失败或超时问题表现在生成摘要时由于网络或服务问题导致调用失败进而使整个对话流程中断。排查与解决实现健壮的降级策略正如我们在generate_summary函数中做的必须有try-except块。一旦摘要失败不能崩溃而应启用降级方案。最简单的降级就是回退到滑动窗口截断丢弃最早的一部分历史并插入一条“[系统]部分早期对话因技术原因被移除”的提示。设置超时与重试对摘要生成的API调用设置合理的超时时间如10秒并实现简单的重试逻辑如最多重试2次。异步处理如果应用允许可以将摘要生成任务放入后台异步执行不影响主对话线程的响应。主线程可以先使用旧上下文或截断的上下文进行响应待摘要生成完成后再在下一轮对话中更新上下文。5.5 成本控制与优化问题摘要压缩本身也需要调用AI会产生额外成本。优化技巧使用更经济的模型进行摘要如使用gpt-3.5-turbo甚至更小的专用摘要模型来压缩历史而用gpt-4等更强大的模型来处理核心对话。这被称为“模型级联”。控制摘要频率和长度通过调整令牌阈值和摘要的max_tokens参数在信息保留和成本之间找到平衡点。摘要不必事无巨细抓住主干即可。缓存摘要如果某些对话模式或文档内容频繁出现可以考虑缓存其摘要结果避免重复生成。例如对于产品说明书等静态文档可以预生成摘要并存储。上下文消息的智能压缩是AI应用从玩具走向生产力的标志性技术之一。它要求开发者不仅会调用API更要理解信息流动的效率和成本。通过本文的拆解希望你不仅获得了一个可运行的代码模块更建立起一种“为上下文做减法”的工程思维。在实际项目中没有银弹你需要根据自己应用的特点——是重逻辑的编程助手还是重事实的客服系统或是重创意的写作伙伴——来调整和优化你的压缩策略。多测试多观察模型在压缩后的表现不断迭代你的提示词和触发逻辑最终你会得到一个在有限上下文内拥有“长期记忆”的智能应用。
返回列表