第2131篇:大模型上下文窗口的工程使用策略——128K context不是越多越好
第2131篇:大模型上下文窗口的工程使用策略——128K context不是越多越好
适读人群:需要处理长文档和长对话的LLM工程师 | 阅读时长:约18分钟 | 核心价值:理解上下文窗口的性能特性,制定合理的上下文使用策略,避免"用了128K反而更差"的陷阱
"我们把整份合同(80页)塞进上下文,然后问合同里的细节,AI居然说不知道。"
这个问题我被问过很多次。当GPT-4o、Claude等模型把上下文窗口扩展到128K甚至200K tokens时,很多工程师的第一反应是:太好了,以后不用做分块检索了,直接把所有文档塞进去就行。
但实际测试后发现:把80页文档塞进去,让AI回答里面某段话的细节,准确率可能不如RAG(检索增强)。这不是模型偷懒,而是长上下文下LLM的注意力分配存在系统性的偏差。
长上下文的"遗忘"问题
/**
* 长上下文下的注意力问题
*
* 研究发现(Lost in the Middle,2023年):
* 把相关信息放在上下文的不同位置,会得到不同的回答质量
*
* 测试场景:
* 上下文包含20个文档,其中第1个是正确答案
*
* 正确答案位置 → 回答准确率
* 第1位: ~90%
* 第5位: ~75%
* 第10位(中间): ~65%(最差!)
* 第15位: ~72%
* 第20位(最后): ~85%
*
* 结论:LLM对上下文的开头和结尾注意力更强
* 中间部分容易被"遗忘"(Lost in the Middle)
*
* 实践意义:
* - 最重要的信息放在最前面或最后面
* - 不要依赖LLM从超长上下文中提取中间的细节
* - 超过20K tokens的上下文,中间部分的信息很可能被忽略
*
* ===== 性能特性 =====
*
* 上下文长度 平均延迟(GPT-4o)
* 4K tokens: 1-2秒
* 16K tokens: 3-5秒
* 64K tokens: 10-20秒
* 128K tokens: 30-60秒
*
* 成本:按token线性增长
* 延迟:二次方增长(注意力机制的复杂度是O(n²))
*/上下文策略决策框架
/**
* 上下文使用策略选择器
*
* 根据文档特征和查询类型,选择合适的上下文策略
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ContextStrategySelector {
private final TokenEstimator tokenEstimator;
/**
* 选择最合适的上下文策略
*
* @param documentSize 文档总token数
* @param queryType 查询类型
* @param accuracyRequirement 准确性要求(HIGH/MEDIUM/LOW)
*/
public ContextStrategy selectStrategy(
int documentSize, QueryType queryType, AccuracyRequirement accuracyRequirement) {
// 超短文档:直接放入上下文
if (documentSize <= 4000) {
return ContextStrategy.DIRECT_CONTEXT;
}
// 中等文档(4K-32K):根据查询类型决策
if (documentSize <= 32000) {
return switch (queryType) {
case SUMMARY -> ContextStrategy.FULL_CONTEXT; // 摘要需要全文
case SPECIFIC_FACT -> ContextStrategy.RAG; // 找特定信息用RAG更精确
case ANALYSIS -> ContextStrategy.CHUNKED_CONTEXT; // 分段分析
case COMPARISON -> ContextStrategy.PARALLEL_CONTEXT; // 并行处理
};
}
// 长文档(32K-128K):谨慎使用全上下文
if (documentSize <= 128000) {
if (queryType == QueryType.SUMMARY && accuracyRequirement != AccuracyRequirement.HIGH) {
return ContextStrategy.FULL_CONTEXT; // 摘要任务可以用全上下文
}
return ContextStrategy.RAG; // 其他场景用RAG
}
// 超长文档(>128K):必须用RAG或分层处理
return ContextStrategy.HIERARCHICAL_RAG;
}
/**
* 为每种策略生成执行建议
*/
public StrategyAdvice getAdvice(ContextStrategy strategy, int documentSize) {
return switch (strategy) {
case DIRECT_CONTEXT -> new StrategyAdvice(
strategy, "直接将文档放入上下文",
"文档小于4K,直接放入效率最高", null);
case FULL_CONTEXT -> new StrategyAdvice(
strategy, "全文放入上下文(适合摘要/全文分析)",
String.format("注意:%d tokens,延迟约%.0f秒,成本约$%.4f",
documentSize, estimateLatency(documentSize), estimateCost(documentSize)),
"建议把最重要的部分放在文档开头或结尾");
case RAG -> new StrategyAdvice(
strategy, "使用RAG检索相关段落",
"推荐检索Top-5,使用重排序,预期延迟<3秒",
"先做混合检索(向量+BM25),再用CrossEncoder重排序");
case HIERARCHICAL_RAG -> new StrategyAdvice(
strategy, "分层检索:先摘要后精确",
"第一层:文档级摘要检索;第二层:段落级精确检索",
"适合超长文档(>128K),两次LLM调用");
default -> new StrategyAdvice(strategy, "自定义策略", "", null);
};
}
private double estimateLatency(int tokens) {
if (tokens <= 4000) return 1.5;
if (tokens <= 16000) return 4.0;
if (tokens <= 64000) return 15.0;
return 45.0;
}
private double estimateCost(int tokens) {
return tokens * 5.0 / 1_000_000; // GPT-4o input price
}
public enum QueryType { SUMMARY, SPECIFIC_FACT, ANALYSIS, COMPARISON }
public enum AccuracyRequirement { HIGH, MEDIUM, LOW }
public enum ContextStrategy {
DIRECT_CONTEXT, FULL_CONTEXT, RAG, CHUNKED_CONTEXT,
PARALLEL_CONTEXT, HIERARCHICAL_RAG
}
record StrategyAdvice(ContextStrategy strategy, String description,
String tradeoffs, String tips) {}
}上下文位置优化
/**
* 上下文排布优化器
*
* 根据"Lost in the Middle"研究,优化文档在上下文中的位置
* 最重要的内容放在最前面和最后面
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ContextArrangementOptimizer {
/**
* 优化检索结果在上下文中的排列顺序
*
* 策略:把最相关的文档放在最前面和最后面
* 中等相关的放在中间
*/
public List<String> optimizeArrangement(List<RetrievedDocument> docs) {
if (docs.isEmpty()) return List.of();
if (docs.size() == 1) return List.of(docs.get(0).getContent());
// 按相关度排序
List<RetrievedDocument> sorted = docs.stream()
.sorted(Comparator.comparingDouble(RetrievedDocument::getScore).reversed())
.toList();
// 最高分放首位,次高分放末位,其余填充中间
// 例:scores [0.95, 0.85, 0.80, 0.75, 0.70]
// 排列:[0.95, 0.80, 0.75, 0.85, 0.70]
// ↑首位最高 ↑末位次高
List<String> arranged = new ArrayList<>();
// 首位:最高分
arranged.add(sorted.get(0).getContent());
// 中间:较低分
for (int i = 2; i < sorted.size() - 1; i++) {
arranged.add(sorted.get(i).getContent());
}
// 末位:次高分
if (sorted.size() > 1) {
arranged.add(sorted.get(1).getContent());
}
// 如果还有剩余
if (sorted.size() > 3) {
arranged.add(sorted.get(sorted.size() - 1).getContent());
}
return arranged;
}
/**
* 构建优化排列的Prompt
*/
public String buildOptimizedPrompt(
String systemPrompt,
String userQuestion,
List<RetrievedDocument> docs) {
List<String> arrangedDocs = optimizeArrangement(docs);
StringBuilder context = new StringBuilder();
for (int i = 0; i < arrangedDocs.size(); i++) {
context.append("参考资料").append(i + 1).append(":\n");
context.append(arrangedDocs.get(i)).append("\n\n");
}
return systemPrompt + "\n\n" +
context + "\n" +
"问题:" + userQuestion + "\n\n" +
// 重要:在末尾重申问题(利用末尾位置的高注意力)
"请根据以上参考资料回答:" + userQuestion;
}
}大文档的分层处理
/**
* 大文档分层处理器
*
* 对于超大文档(几百页的合同/报告)
* 用层次化策略:先了解大结构,再深入细节
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class HierarchicalDocumentProcessor {
private final ChatLanguageModel llm;
private final TokenEstimator tokenEstimator;
/**
* 两阶段处理大文档
*
* 阶段1:生成各章节的摘要
* 阶段2:根据问题,在相关章节中精确检索
*/
public String processLargeDocument(String document, String question) {
int totalTokens = tokenEstimator.countTokens(document);
if (totalTokens <= 32000) {
// 文档不太大,直接处理
return llm.generate(buildDirectPrompt(document, question));
}
log.info("大文档分层处理: totalTokens={}", totalTokens);
// 阶段1:切分文档,生成章节摘要
List<DocumentChapter> chapters = splitIntoChapters(document);
List<ChapterSummary> summaries = chapters.parallelStream()
.map(ch -> new ChapterSummary(ch.title(), summarizeChapter(ch), ch.startPos()))
.toList();
// 阶段2:用问题找最相关的章节
List<ChapterSummary> relevantChapters = findRelevantChapters(summaries, question);
// 阶段3:在相关章节原文中精确回答
String relevantContent = relevantChapters.stream()
.map(cs -> getChapterContent(chapters, cs))
.collect(Collectors.joining("\n\n---\n\n"));
return llm.generate(buildDirectPrompt(relevantContent, question));
}
private String summarizeChapter(DocumentChapter chapter) {
String prompt = """
请用50-100字总结以下章节的主要内容:
%s
总结:
""".formatted(chapter.content().substring(0, Math.min(3000, chapter.content().length())));
try {
return llm.generate(prompt);
} catch (Exception e) {
return chapter.title() + "(摘要生成失败)";
}
}
private List<ChapterSummary> findRelevantChapters(
List<ChapterSummary> summaries, String question) {
String allSummaries = summaries.stream()
.map(s -> "【" + s.title() + "】" + s.summary())
.collect(Collectors.joining("\n"));
String prompt = """
请找出以下章节中,与问题最相关的章节标题(最多3个)。
只返回章节标题,每行一个。
问题:%s
章节列表:
%s
最相关的章节:
""".formatted(question, allSummaries);
try {
String response = llm.generate(prompt);
Set<String> relevantTitles = new HashSet<>(
Arrays.asList(response.split("\n")).stream()
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList()
);
return summaries.stream()
.filter(s -> relevantTitles.contains(s.title()))
.toList();
} catch (Exception e) {
// 找不到相关章节,返回前3章
return summaries.subList(0, Math.min(3, summaries.size()));
}
}
private List<DocumentChapter> splitIntoChapters(String document) {
// 按标题分割(简化实现)
return List.of(new DocumentChapter("全文", document, 0));
}
private String getChapterContent(List<DocumentChapter> chapters, ChapterSummary summary) {
return chapters.stream()
.filter(ch -> ch.title().equals(summary.title()))
.findFirst()
.map(DocumentChapter::content)
.orElse("");
}
private String buildDirectPrompt(String context, String question) {
return "根据以下文档内容:\n\n" + context + "\n\n请回答:" + question;
}
record DocumentChapter(String title, String content, int startPos) {}
record ChapterSummary(String title, String summary, int startPos) {}
}实践建议
RAG通常比"全文塞入"更准确,尤其是细节查询
这违反直觉,但有坚实的研究支持。对于"合同第3条第2款规定了什么"这类细节查询,RAG(找到对应段落再回答)的准确率通常高于把整份合同塞入上下文。原因是:RAG把最相关的内容放在LLM的有效注意力范围内;而全文上下文中,关键内容可能被淹没在几万个token里,LLM的注意力被稀释。把超过32K的文档全塞进去,应该是最后的选项。
长上下文的延迟成本不可忽视
128K上下文的请求延迟通常在30-60秒,而精心设计的RAG(检索+生成)可以控制在3-5秒。对于交互式应用,60秒是不可接受的。即使你只是想"图省事不做RAG",也要考虑用户愿不愿意等这么久。长上下文适合的场景:批量处理(离线任务,对延迟不敏感)、大文档摘要(用户明白会慢)。
"重要信息放前后"是廉价但有效的优化
如果你必须用长上下文,至少把最重要的信息放在开头和结尾。在RAG场景:把相似度最高的文档放第一个,相似度第二高的放最后一个。在System Prompt里:把关键约束和示例放在最前面,在最后面用"特别提醒"重申核心规则。这个操作零成本,通常能提升5-10%的准确率。
