LLM上下文窗口工程2026:超长上下文的正确使用姿势

5 阅读1分钟

上下文窗口军备竞赛背后的工程现实

2024年初,能处理32K token已经是旗舰模型的标配。到2026年,Gemini 1.5 Pro支持100万token,Claude支持20万token,GPT-4o也已扩展到128K。一本厚厚的技术书籍、一个中等规模的代码仓库、数月的对话历史——理论上都可以塞进单次请求。

然而,上下文窗口的膨胀并不意味着"越长越好"。工程实践中,滥用超长上下文带来的问题往往比它解决的问题更多:成本急剧攀升、延迟大幅增加、"迷失在中间"(Lost in the Middle)效应导致准确率下降。

本文从工程视角出发,系统解析上下文窗口的正确使用方式、性能特性,以及如何在成本、准确率和延迟之间找到最优平衡点。

上下文的物理特性

Token成本的非线性增长

上下文长度对成本的影响并非简单的线性关系。以Claude claude-opus-4-5为例:

输入token单价:$15 / 1M tokens(标准)
               $3 / 1M tokens(缓存命中后)

100K token的单次请求成本:~$1.50
但如果100K中有80K是不变的系统提示/文档 → 启用Prompt Cache后:
  首次:$1.50(建立缓存)
  后续:0.8 × $0.30 + 0.2 × $1.50 = $0.54(节省64%)

关键洞察:超长上下文的主要成本来自重复传输不变的内容,Prompt Cache是应对这一问题的核心技术。

"迷失在中间"效应

斯坦福大学2023年的研究表明,LLM在处理长上下文时存在显著的"位置偏差"——模型对靠近上下文开头和结尾的信息记忆最准确,而中间部分的信息往往被"遗忘"。

# 验证Lost in the Middle效应的实验代码
import anthropic
import json

def test_lost_in_middle(needle: str, position_ratio: float, context_length: int = 50000):
    """
    在context_length个token的上下文中,
    将关键信息(needle)放在position_ratio位置处
    测试模型能否准确检索
    """
    client = anthropic.Anthropic()
    
    # 生成填充文本
    filler = "这是一段与测试无关的技术文档内容。" * (context_length // 15)
    
    # 计算needle插入位置
    insert_pos = int(len(filler) * position_ratio)
    context = filler[:insert_pos] + f"\n【关键信息】{needle}\n" + filler[insert_pos:]
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=200,
        messages=[{
            "role": "user",
            "content": f"{context}\n\n问题:上文中的关键信息是什么?"
        }]
    )
    
    answer = response.content[0].text
    return needle.lower() in answer.lower()

# 测试不同位置的准确率
positions = [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0]
results = {pos: test_lost_in_middle("密钥XK-7749-ALPHA", pos) for pos in positions}
# 典型结果:开头和结尾准确率~95%,中间位置(0.4-0.6)准确率降至70-80%

注意力分散效应

上下文越长,模型注意力越分散。一个经验规则:

  • < 8K token:注意力集中,几乎无损失
  • 8K - 32K:轻度稀释,关键信息需要在上下文中加强(重复或突出显示)
  • 32K - 128K:注意力显著稀释,需要结构化文档和清晰的导航
  • > 128K:慎用,除非任务本身就是全文检索类任务

工程策略一:分层上下文管理

不要把所有信息无差别地塞进上下文。按照信息的"重要性"和"变动频率"分层管理:

from dataclasses import dataclass
from typing import List, Optional
import tiktoken

@dataclass
class ContextLayer:
    """上下文分层管理"""
    name: str
    content: str
    priority: int      # 1=最高优先级
    is_cacheable: bool # 是否可以被Prompt Cache缓存
    max_tokens: int    # 该层最大token配额

class HierarchicalContextManager:
    """
    上下文分层管理器
    层次(由高到低优先级):
    1. 系统指令(固定,可缓存)
    2. 长期记忆/知识库(半固定,可缓存)
    3. 会话历史(动态,按重要性截断)
    4. 当前用户输入(固定,不可省略)
    """
    
    def __init__(self, total_budget: int = 100000, model: str = "cl100k_base"):
        self.total_budget = total_budget
        self.encoder = tiktoken.get_encoding(model)
        
        # 各层配额分配
        self.layer_budgets = {
            "system": int(total_budget * 0.15),      # 15% 系统提示
            "knowledge": int(total_budget * 0.40),   # 40% 知识库/RAG
            "history": int(total_budget * 0.35),      # 35% 对话历史
            "input": int(total_budget * 0.10),         # 10% 当前输入
        }
    
    def count_tokens(self, text: str) -> int:
        return len(self.encoder.encode(text))
    
    def truncate_history(self, messages: List[dict], budget: int) -> List[dict]:
        """
        智能截断对话历史
        策略:保留最近的N条,但始终保留带有关键标记的消息
        """
        if not messages:
            return []
        
        # 标记为"重要"的消息不截断
        important = [m for m in messages if m.get("important", False)]
        regular = [m for m in messages if not m.get("important", False)]
        
        # 计算重要消息占用的token
        important_tokens = sum(self.count_tokens(m["content"]) for m in important)
        remaining_budget = budget - important_tokens
        
        if remaining_budget <= 0:
            return important[-5:]  # 至少保留最近5条重要消息
        
        # 从最新开始添加普通消息,直到预算耗尽
        selected_regular = []
        token_used = 0
        
        for msg in reversed(regular):
            tokens = self.count_tokens(msg["content"])
            if token_used + tokens <= remaining_budget:
                selected_regular.insert(0, msg)
                token_used += tokens
            else:
                break
        
        # 按时间顺序合并
        all_messages = sorted(
            important + selected_regular,
            key=lambda m: m.get("timestamp", 0)
        )
        return all_messages
    
    def build_context(
        self,
        system_prompt: str,
        knowledge_chunks: List[str],
        history: List[dict],
        user_input: str
    ) -> dict:
        """
        构建最终上下文,严格控制各层token预算
        """
        # 系统提示(固定)
        system_tokens = self.count_tokens(system_prompt)
        if system_tokens > self.layer_budgets["system"]:
            raise ValueError(f"系统提示超出预算:{system_tokens} > {self.layer_budgets['system']}")
        
        # 知识库(截断到预算)
        knowledge_content = ""
        knowledge_budget = self.layer_budgets["knowledge"]
        for chunk in knowledge_chunks:
            chunk_tokens = self.count_tokens(chunk)
            if self.count_tokens(knowledge_content) + chunk_tokens > knowledge_budget:
                break
            knowledge_content += f"\n---\n{chunk}"
        
        # 对话历史(智能截断)
        truncated_history = self.truncate_history(history, self.layer_budgets["history"])
        
        return {
            "system": system_prompt,
            "knowledge": knowledge_content,
            "history": truncated_history,
            "input": user_input,
            "token_usage": {
                "system": system_tokens,
                "knowledge": self.count_tokens(knowledge_content),
                "history": sum(self.count_tokens(m["content"]) for m in truncated_history),
                "input": self.count_tokens(user_input)
            }
        }

工程策略二:Prompt Cache的正确使用

Prompt Cache是处理超长上下文的成本杀手,但需要正确使用才能发挥效果:

import anthropic
from typing import List

class CachedKnowledgeAssistant:
    """
    利用Prompt Cache处理大规模知识库查询
    核心原则:将不变的部分放在前面,可变的部分放在后面
    """
    
    def __init__(self, knowledge_base: str, system_instructions: str):
        self.client = anthropic.Anthropic()
        # 将不变的内容构建为可缓存的块
        self.cached_system = system_instructions
        self.cached_knowledge = knowledge_base
        self._cache_initialized = False
    
    def query(self, user_question: str, conversation_history: List[dict] = None) -> dict:
        """
        发送查询,最大化缓存利用率
        关键:缓存块必须放在messages的最前面,且content相同才能命中
        """
        
        # 构建消息,缓存块在前
        messages = [
            # 固定的知识库(设置cache_control)
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"知识库内容:\n{self.cached_knowledge}",
                        "cache_control": {"type": "ephemeral"}  # 标记为可缓存
                    }
                ]
            },
            {
                "role": "assistant",
                "content": "我已理解知识库内容,请提问。"
            }
        ]
        
        # 追加对话历史(不可缓存,每次都变化)
        if conversation_history:
            messages.extend(conversation_history[-10:])  # 只保留最近10轮
        
        # 追加当前问题
        messages.append({"role": "user", "content": user_question})
        
        response = self.client.messages.create(
            model="claude-opus-4-5",
            max_tokens=2000,
            system=[
                {
                    "type": "text",
                    "text": self.cached_system,
                    "cache_control": {"type": "ephemeral"}
                }
            ],
            messages=messages
        )
        
        # 分析缓存效果
        usage = response.usage
        cache_hit_ratio = 0
        if hasattr(usage, 'cache_read_input_tokens') and usage.input_tokens > 0:
            cache_hit_ratio = usage.cache_read_input_tokens / usage.input_tokens
        
        return {
            "answer": response.content[0].text,
            "cache_hit_ratio": cache_hit_ratio,
            "total_input_tokens": usage.input_tokens,
            "cache_read_tokens": getattr(usage, 'cache_read_input_tokens', 0),
            "new_tokens": getattr(usage, 'cache_creation_input_tokens', usage.input_tokens)
        }

Prompt Cache最佳实践

✅ 适合缓存的内容:
  - 系统提示(几乎不变)
  - 完整文档/代码库
  - Few-shot示例集合
  - 规则和约束列表

❌ 不要尝试缓存的内容:
  - 包含动态时间戳的内容
  - 每次都变化的用户历史
  - 包含随机数/UUID的内容
  - 短于1000 token的内容(不划算)

工程策略三:动态上下文压缩

当上下文增长超过预算时,需要智能压缩策略:

class ContextCompressor:
    """
    对话上下文的动态压缩
    当对话历史超过阈值时,自动进行摘要压缩
    """
    
    def __init__(self, max_tokens: int = 50000, compression_threshold: float = 0.8):
        self.max_tokens = max_tokens
        self.threshold_tokens = int(max_tokens * compression_threshold)
        self.encoder = tiktoken.get_encoding("cl100k_base")
        self.client = openai.OpenAI()
    
    def estimate_tokens(self, messages: List[dict]) -> int:
        total = 0
        for msg in messages:
            content = msg.get("content", "")
            if isinstance(content, str):
                total += len(self.encoder.encode(content))
            elif isinstance(content, list):
                for block in content:
                    if isinstance(block, dict) and "text" in block:
                        total += len(self.encoder.encode(block["text"]))
        return total
    
    def compress_old_messages(self, messages: List[dict], keep_recent: int = 4) -> List[dict]:
        """
        将旧消息压缩为摘要,保留最近N条完整消息
        """
        if len(messages) <= keep_recent:
            return messages
        
        old_messages = messages[:-keep_recent]
        recent_messages = messages[-keep_recent:]
        
        # 构建压缩提示
        conversation_text = "\n".join([
            f"{m['role'].upper()}: {m['content']}"
            for m in old_messages
            if isinstance(m.get('content'), str)
        ])
        
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",  # 用小模型做压缩,节省成本
            messages=[{
                "role": "user",
                "content": f"""请将以下对话历史压缩为一段300字以内的摘要,
保留所有关键信息、决策和结论:

{conversation_text}

输出格式:
[对话摘要]
主要话题:...
关键结论:...
重要上下文:..."""
            }]
        )
        
        summary = response.choices[0].message.content
        
        # 创建摘要消息替代旧消息
        summary_message = {
            "role": "system",
            "content": f"以下是早期对话的压缩摘要:\n{summary}",
            "is_summary": True
        }
        
        return [summary_message] + recent_messages
    
    def maybe_compress(self, messages: List[dict]) -> tuple[List[dict], bool]:
        """
        检查是否需要压缩,返回(处理后的消息, 是否进行了压缩)
        """
        current_tokens = self.estimate_tokens(messages)
        
        if current_tokens > self.threshold_tokens:
            compressed = self.compress_old_messages(messages)
            return compressed, True
        
        return messages, False

工程策略四:上下文感知的RAG

对于超出上下文窗口的知识库,RAG仍然是最经济的方案:

class ContextAwareRAG:
    """
    根据当前上下文使用情况,动态调整RAG检索数量
    """
    
    def __init__(self, vector_store, context_manager: HierarchicalContextManager):
        self.vs = vector_store
        self.cm = context_manager
    
    def adaptive_retrieve(self, query: str, used_tokens: int) -> List[str]:
        """
        根据已用token数,动态调整检索的chunk数量
        """
        available_for_rag = self.cm.layer_budgets["knowledge"] - used_tokens
        
        if available_for_rag <= 0:
            return []  # 没有预算,不检索
        
        # 估算每个chunk平均500 tokens
        max_chunks = min(available_for_rag // 500, 10)
        
        if max_chunks == 0:
            return []
        
        results = self.vs.similarity_search(query, k=max_chunks)
        return [r.page_content for r in results]
    
    def build_rag_context(self, query: str, history: List[dict]) -> str:
        """
        智能构建RAG上下文
        - 根据query检索相关文档
        - 对检索结果按相关性排序
        - 最相关的放在上下文末尾(靠近用户问题)
        """
        history_tokens = sum(
            len(self.cm.encoder.encode(m.get("content", "")))
            for m in history
        )
        
        chunks = self.adaptive_retrieve(query, history_tokens)
        
        if not chunks:
            return ""
        
        # 最相关的放最后(利用recency效应)
        return "\n\n相关知识:\n" + "\n---\n".join(reversed(chunks))

性能基准与选型建议

基于工程实践总结的上下文长度选型建议:

任务类型                    建议上下文长度    成本效率
─────────────────────────────────────────────────────
简单问答/聊天               < 8K             ★★★★★
代码补全/审查               8K - 32K         ★★★★☆
文档分析(单文档)          32K - 64K        ★★★☆☆
代码库分析                  64K - 128K       ★★☆☆☆(建议+RAG)
全书/大型文档集              > 128K           ★☆☆☆☆(建议换RAG)

2026年的实用原则

  1. 先用RAG,再考虑长上下文:90%的知识密集型任务,用好RAG比塞满上下文效果更好
  2. Prompt Cache是必须:只要上下文超过10K token且有重复内容,必须启用缓存
  3. 关键信息放两端:利用recency和primacy效应,重要信息放在上下文开头或结尾
  4. 定期压缩历史:超过20轮的对话应该压缩,而不是无限增长
  5. 监控"有效注意力":不是token越多越好,要确保关键信息在模型的"注意力热区"内

上下文窗口工程的本质,是在计算资源有限的前提下,让模型的注意力集中在最重要的信息上。技术在进步,但这个核心原则不会变。