AI Agent开发实战⑱上下文压缩与选择让LLM看到最有价值的信息检索到了50篇文档但LLM的上下文窗口只能塞5篇。选哪5篇平均选会漏掉关键信息全塞进去会爆Token。上下文压缩和选择策略就是解决这个矛盾用最少的Token承载最多的信息。一、上下文窗口的困境典型场景 检索返回50篇文档共30000字符 LLM上下文4000 tokens约6000字符 可用窗口4000 - 1000Query输出预留 3000 tokens 问题 - 50篇文档塞不进去 - 随机选会漏掉关键信息 - 每篇都压缩会丢失细节上下文管理要解决三个问题选择从50篇里选哪几篇压缩如何在不丢失信息的前提下压缩排序关键信息放在上下文的什么位置二、上下文选择策略2.1 基于分数的选择最简单按检索分数选Top-K。defselect_by_score(docs:list[dict],k:int5)-list[dict]:按分数选择Top-Ksorted_docssorted(docs,keylambdax:x[score],reverseTrue)returnsorted_docs[:k]问题分数高的文档可能内容重复浪费窗口。2.2 基于多样性的选择MMRMaximal Marginal Relevance选既相关又多样的文档。importnumpyasnpdefmmr_selection(docs:list[dict],query_vec:np.ndarray,doc_vecs:np.ndarray,k:int5,lambda_param:float0.7)-list[dict]: MMR选择 lambda_param: 相关性权重1.0只看相关性0.0只看多样性 推荐0.7相关性和多样性的平衡 selected[]selected_indices[]remaininglist(range(len(docs)))for_inrange(k):ifnotremaining:breakbest_score-np.inf best_idxNoneforidxinremaining:# 相关性与Query的相似度relevancenp.dot(doc_vecs[idx],query_vec)/(np.linalg.norm(doc_vecs[idx])*np.linalg.norm(query_vec))# 多样性与已选文档的最大相似度越小越多样ifselected_indices:max_similaritymax(np.dot(doc_vecs[idx],doc_vecs[s])/(np.linalg.norm(doc_vecs[idx])*np.linalg.norm(doc_vecs[s]))forsinselected_indices)else:max_similarity0# MMR分数mmr_scorelambda_param*relevance-(1-lambda_param)*max_similarityifmmr_scorebest_score:best_scoremmr_score best_idxidxifbest_idxisnotNone:selected.append(docs[best_idx])selected_indices.append(best_idx)remaining.remove(best_idx)returnselected2.3 基于覆盖度的选择选择能覆盖最多查询关键词的文档组合。defcoverage_selection(docs:list[dict],query:str,k:int5)-list[dict]:基于关键词覆盖度选择# 提取查询关键词query_keywordsset(jieba.cut(query))selected[]covered_keywordsset()whilelen(selected)kanddocs:# 找能覆盖最多新关键词的文档best_docNonebest_new_coverage0fordocindocs:doc_keywordsset(jieba.cut(doc[content]))new_coveragelen(doc_keywordsquery_keywords-covered_keywords)ifnew_coveragebest_new_coverage:best_new_coveragenew_coverage best_docdocifbest_doc:selected.append(best_doc)docs.remove(best_doc)# 更新已覆盖关键词covered_keywords|set(jieba.cut(best_doc[content]))query_keywordselse:breakreturnselected三、上下文压缩策略3.1 摘要压缩用LLM生成文档摘要。classContextCompressor:上下文压缩器def__init__(self,llm):self.llmllmdefcompress_doc(self,doc:str,query:str,max_length:int200)-str:压缩单个文档iflen(doc)max_length:returndoc promptf 用户查询{query}文档内容{doc}请提取与用户查询最相关的内容压缩到{max_length}字以内。 要求 1. 保留关键信息 2. 保留具体数字、名称 3. 不要添加原文没有的信息 压缩结果 responseself.llm.invoke(prompt)returnresponse.content.strip()[:max_length]defcompress_batch(self,docs:list[str],query:str,max_total_length:int2000)-list[str]:批量压缩# 每个文档的预算长度budget_per_docmax_total_length//len(docs)compressed[]fordocindocs:compressed.append(self.compress_doc(doc,query,budget_per_doc))returncompressed3.2 提取式压缩提取关键句子不重新生成。classExtractiveCompressor:提取式压缩器def__init__(self,sentence_embedder):self.embeddersentence_embedderdefcompress(self,doc:str,query:str,max_sentences:int3)-str:提取关键句子# 分句sentences[s.strip()forsindoc.split(。)ifs.strip()]iflen(sentences)max_sentences:returndoc# 计算每个句子与查询的相似度query_vecself.embedder.embed(query)sentence_vecs[self.embedder.embed(s)forsinsentences]scores[np.dot(sv,query_vec)/(np.linalg.norm(sv)*np.linalg.norm(query_vec))forsvinsentence_vecs]# 选Top-K句子top_indicesnp.argsort(scores)[::-1][:max_sentences]top_indicessorted(top_indices)# 保持原文顺序selected[sentences[i]foriintop_indices]return。.join(selected)。3.3 LLM自适应压缩让LLM自己决定压缩策略。classAdaptiveCompressor:自适应压缩器def__init__(self,llm):self.llmllmdefcompress(self,docs:list[str],query:str,max_tokens:int2000)-str:自适应压缩多文档# 合并文档all_text\n\n---\n\n.join([f文档{i1}{doc}fori,docinenumerate(docs)])promptf 用户查询{query}以下是与查询相关的多个文档片段{all_text}请从中提取与查询最相关的信息整合成一段连贯的文字。 要求 1. 总长度不超过{max_tokens}字 2. 保留所有关键信息数字、名称、结论 3. 去除重复内容 4. 按逻辑组织不要简单拼接 整合结果 responseself.llm.invoke(prompt)returnresponse.content四、上下文排序策略研究表明LLM对上下文不同位置的信息关注度不同。4.1 Lost in the Middle问题研究发现 - 开头的信息关注度高 - 中间的信息关注度低Lost in the Middle - 结尾的信息关注度中等 建议关键信息放在开头或结尾不要埋在中间4.2 实现策略defreorder_context(docs:list[dict],strategy:strrelevance_first)-list[dict]:重新排序上下文ifstrategyrelevance_first:# 相关性高的放前面returnsorted(docs,keylambdax:x[score],reverseTrue)elifstrategyrelevance_both_ends:# 相关性高的放两端sorted_docssorted(docs,keylambdax:x[score],reverseTrue)nlen(sorted_docs)result[]foriinrange(n):ifi%20:result.append(sorted_docs[i//2])else:result.append(sorted_docs[n-1-i//2])returnresultelifstrategyimportant_first:# 包含关键信息的放前面returnsorted(docs,keylambdax:x.get(importance,0),reverseTrue)returndocs五、实测对比5.1 测试设置测试数据-文档每轮检索返回50篇-查询100个测试查询-LLMGPT-4-Turbo4096tokens上下文-评估Answer Accuracy5.2 选择策略对比选择策略Accuracy平均Token数说明Top-Kk568.2%1850基线MMRλ0.772.1%19203.9%覆盖度选择70.5%17802.3%5.3 压缩策略对比压缩策略AccuracyToken消耗信息保留不压缩68.2%1850100%摘要压缩65.3%82082%提取式压缩67.1%95089%自适应压缩69.8%110091%关键发现MMR选择效果最好但Token略高自适应压缩在压缩和信息保留之间平衡最好六、完整方案集成classContextManager:上下文管理器选择压缩排序def__init__(self,llm,embedder,max_tokens:int3000):self.llmllm self.embedderembedder self.max_tokensmax_tokensdefprocess(self,docs:list[dict],query:str)-str:处理上下文# 第一步选择MMRselectedself._select(docs,query,k10)# 第二步压缩compressedself._compress(selected,query)# 第三步排序reorderedself._reorder(compressed)# 第四步格式化contextself._format(reordered)returncontextdef_select(self,docs:list[dict],query:str,k:int)-list[dict]:MMR选择query_vecself.embedder.embed(query)doc_vecs[self.embedder.embed(d[content])fordindocs]returnmmr_selection(docs,query_vec,np.array(doc_vecs),kk)def_compress(self,docs:list[dict],query:str)-list[dict]:压缩# 计算每篇文档的预算budgetself.max_tokens//len(docs)compressorExtractiveCompressor(self.embedder)compressed[]fordocindocs:iflen(doc[content])budget:doc[content]compressor.compress(doc[content],query,max_sentences3)compressed.append(doc)returncompresseddef_reorder(self,docs:list[dict])-list[dict]:排序相关性高的放两端returnreorder_context(docs,strategyrelevance_both_ends)def_format(self,docs:list[dict])-str:格式化return\n\n.join([f[{i1}]{doc[content]}fori,docinenumerate(docs)])七、总结策略效果Token消耗推荐度MMR选择3.9%中⭐⭐⭐⭐⭐提取式压缩-1.1%-45%⭐⭐⭐⭐自适应压缩1.6%-40%⭐⭐⭐⭐⭐两端排序2.1%0%⭐⭐⭐⭐最佳实践MMR选择 自适应压缩 两端排序。下篇预告「生成优化Prompt工程与自我检验」——检索到了正确信息如何让LLM准确生成答案需要完整上下文管理代码的同学可以看我主页的付费资源专栏。有问题欢迎评论区留言大家一起讨论