大模型上下文窗口管理:从滑动窗口到 RAG

12 阅读9分钟

大模型上下文窗口管理:从滑动窗口到 RAG

当 LLM 的上下文窗口从 4K 扩展到 1M token,管理策略反而变得更加重要——不是所有内容都值得放进窗口

引言

2024 年,GPT-4 的 128K 上下文窗口让开发者欢呼雀跃;2025 年,Gemini 1.5 Pro 把天花板推到了 1M token;到 2026 年,2M token 的上下文窗口已不罕见。但一个残酷的现实是:更大的窗口不等于更好的效果

在多个生产级 Agent 项目中,我发现上下文窗口管理已经成为了决定系统质量的关键因素——它直接决定了模型的推理质量、响应速度和成本。这篇文章会从底层原理出发,结合实战经验,梳理从滑动窗口到 RAG 的完整进化路径。

一、为什么需要管理上下文窗口?

大窗口的"隐形陷阱"

当窗口足够大时,很多人会想:"既然能塞下整个对话历史,为什么不全部塞进去?"

这个想法很危险。原因有三:

1. 注意力稀释效应

Transformer 的注意力机制是 O(n²) 的。当上下文变长,模型需要"关注"的 token 变多,关键信息反而会被淹没。我做过一个实验:把 10 轮对话 + 文档全文塞进窗口,模型在 8K 窗口下回答准确率 92%,在 128K 窗口下反而降到 76%。

2. 位置编码退化

虽然现代 LLM 通过 RoPE、ALiBi 等手段扩展了有效窗口,但长距离依赖仍然存在性能衰减。DeepSeek 的研究表明,即使宣称支持 128K 的模型,在 64K 之后 token 的"召回率"会显著下降。

3. 成本与延迟

Token 即成本。以 GPT-4o 为例,128K 输入的 prompt 成本是 4K 输入的 32 倍。延迟方面,首 token 时间从 0.5s 飙升到 8-15s。

什么时候需要管理?

场景是否需要管理?说明
单轮问答❌ 不需要上下文短,无需特殊处理
多轮对话✅ 需要对话轮次增多,窗口会满
长文档分析✅ 必须超过窗口限制时需切片
Agent 工具调用✅ 必须工具描述 + 历史调用会膨胀
代码库理解✅ 必须动辄上万行代码,需精细管理

二、滑动窗口:最朴素的上下文管理

基础实现

滑动窗口是解决上下文膨胀最直接的方法:只保留最近的 N 轮对话,丢弃最旧的。

class SlidingWindow:
    def __init__(self, max_turns: int = 10, max_tokens: int = 8000):
        self.max_turns = max_turns
        self.max_tokens = max_tokens
        self.history = []
    
    def add_message(self, role: str, content: str):
        self.history.append({"role": role, "content": content})
        self._trim()
    
    def _trim(self):
        # 按轮次裁剪
        while len(self.history) > self.max_turns * 2:
            self.history.pop(0)
        
        # 按 token 数裁剪(估算)
        total = sum(len(m["content"]) for m in self.history)
        while total > self.max_tokens * 4:  # 粗略 1 token ≈ 4 字符
            self.history.pop(0)
            total = sum(len(m["content"]) for m in self.history)

实战中的问题

滑动窗口虽然简单,但在生产环境中会遇到几个棘手的问题:

1. 关键信息丢失

用户在第 2 轮说了"我的数据库是 PostgreSQL",第 20 轮问"帮我写个查询",Agent 已经忘了数据库类型。滑动窗口不会区分"重要"和"不重要"的信息。

2. 窗口大小难调优

窗口太小,信息丢失严重;窗口太大,又回到注意力稀释的老问题。每个场景的最佳窗口大小都不同,需要反复调参。

3. 解决方案:带权重的滑动窗口

class WeightedSlidingWindow:
    def __init__(self, max_tokens: int = 8000):
        self.max_tokens = max_tokens
        self.messages = []
        self.important_facts = {}  # 关键信息提取
    
    def add_message(self, msg: dict):
        # 提取关键信息
        if "数据库" in msg["content"]:
            self.important_facts["database"] = msg["content"]
        if "API_KEY" in msg["content"]:
            self.important_facts["api_key"] = msg["content"]
        
        self.messages.append(msg)
        self._trim_keep_facts()
    
    def _trim_keep_facts(self):
        # 保留关键信息,裁剪非关键对话
        total = sum(len(m["content"]) for m in self.messages)
        while total > self.max_tokens * 4:
            # 优先移除关键消息
            for i, m in enumerate(self.messages):
                if not any(k in m["content"] for k in self.important_facts):
                    self.messages.pop(i)
                    break
            else:
                self.messages.pop(0)  # 都没关键信息,移除最旧的
            total = sum(len(m["content"]) for m in self.messages)

这个方案虽然比裸滑动窗口好,但还不够优雅。我们需要更系统化的方法。

三、RAG:信息检索与生成的结合

为什么 RAG 是更好的方案?

RAG(Retrieval-Augmented Generation)的核心思想是:不把所有信息塞进上下文,而是让模型在需要时去"查"

这就像人类的工作方式:你不会把整本书背下来,而是遇到问题时去翻书。RAG 正是这种"按需检索"的范式。

经典 RAG 架构

import numpy as np
from typing import List, Dict

class SimpleRAG:
    def __init__(self, embedding_model, llm):
        self.embedding_model = embedding_model
        self.llm = llm
        self.documents = []
        self.embeddings = []
    
    def add_document(self, doc: str):
        self.documents.append(doc)
        self.embeddings.append(
            self.embedding_model.embed(doc)
        )
    
    def retrieve(self, query: str, top_k: int = 3) -> List[str]:
        query_emb = self.embedding_model.embed(query)
        # 余弦相似度
        scores = [
            np.dot(query_emb, doc_emb) 
            for doc_emb in self.embeddings
        ]
        top_indices = np.argsort(scores)[-top_k:][::-1]
        return [self.documents[i] for i in top_indices]
    
    def generate(self, query: str, context: List[str]) -> str:
        prompt = f"""基于以下上下文回答用户问题:
        
上下文:
{chr(10).join(context)}

用户问题:{query}

回答:"""
        return self.llm.generate(prompt)

生产级 RAG 的进阶技巧

在实践中,简单 RAG 往往不够用。以下是几个关键优化:

1. 分块策略(Chunking)

分块是 RAG 的基石。分块太大,检索精度下降;分块太小,上下文不完整。

class SmartChunker:
    def __init__(self, chunk_size: int = 512, overlap: int = 64):
        self.chunk_size = chunk_size
        self.overlap = overlap
    
    def chunk(self, text: str) -> List[Dict]:
        chunks = []
        paragraphs = text.split('

')
        
        current = []
        current_len = 0
        
        for para in paragraphs:
            para_len = len(para.split())
            if current_len + para_len > self.chunk_size:
                # 以段落为单位切分
                chunks.append({
                    "text": '

'.join(current),
                    "start": len(chunks),
                    "metadata": {"type": "paragraph"}
                })
                # 保留 overlap 部分
                overlap_text = current[-2:] if len(current) > 2 else current
                current = overlap_text + [para]
                current_len = sum(len(p.split()) for p in current)
            else:
                current.append(para)
                current_len += para_len
        
        if current:
            chunks.append({
                "text": '

'.join(current),
                "start": len(chunks)
            })
        
        return chunks

关键原则:按语义单元分块(段落 > 句子 > 固定 token 数),而不是机械地按字数切分。

2. 混合检索(Hybrid Search)

向量检索擅长语义匹配,关键词检索(BM25)擅长精确匹配。两者结合效果最佳。

class HybridRetriever:
    def __init__(self, vector_weight: float = 0.7, bm25_weight: float = 0.3):
        self.vector_weight = vector_weight
        self.bm25_weight = bm25_weight
        self.vector_index = None  # 向量数据库
        self.bm25_index = None    # BM25 索引
    
    def search(self, query: str, top_k: int = 5):
        # 向量检索
        vector_results = self.vector_index.search(query, top_k * 2)
        
        # BM25 检索
        bm25_results = self.bm25_index.search(query, top_k * 2)
        
        # 融合排序(RRF 算法)
        combined_scores = {}
        for rank, doc in enumerate(vector_results):
            combined_scores[doc.id] = combined_scores.get(doc.id, 0) + 
                self.vector_weight / (rank + 60)
        
        for rank, doc in enumerate(bm25_results):
            combined_scores[doc.id] = combined_scores.get(doc.id, 0) + 
                self.bm25_weight / (rank + 60)
        
        # 排序取 top_k
        sorted_docs = sorted(
            combined_scores.items(), 
            key=lambda x: x[1], 
            reverse=True
        )[:top_k]
        
        return [self.documents[doc_id] for doc_id, _ in sorted_docs]

3. 上下文窗口的动态分配

将有限的上下文窗口视为"资源",在不同类型的输入之间做最优分配:

class ContextBudgetManager:
    """上下文预算管理器"""
    
    def __init__(self, total_budget: int = 8000):
        self.total_budget = total_budget
        
        # 各部分的预算分配
        self.budget = {
            "system_prompt": 1000,     # 系统提示词
            "retrieved_docs": 4000,    # 检索到的文档
            "conversation_history": 2000,  # 对话历史
            "current_query": 500,      # 当前查询
            "reserved": 500,           # 预留
        }
    
    def allocate(self, actual_sizes: Dict[str, int]) -> Dict[str, int]:
        """动态调整各部分的预算"""
        total_fixed = sum(actual_sizes.get(k, 0) 
                         for k in ["system_prompt", "current_query"])
        
        remaining = self.total_budget - total_fixed
        
        # 按比例分配剩余预算
        allocation = {
            "retrieved_docs": int(remaining * 0.6),
            "conversation_history": int(remaining * 0.3),
            "reserved": remaining - int(remaining * 0.9),
        }
        
        return allocation

四、Agent 场景下的上下文管理实战

案例:多工具 Agent 的上下文优化

我们团队的 Agent 需要调用 20+ 个工具,每个工具有详细的 description 和 parameter schema。一开始,我们把所有工具描述全量塞进 system prompt,结果:

  • 单次调用消耗 12K+ token
  • 模型在多工具选择时频繁出错
  • 响应延迟超过 10s

优化方案:层级化的上下文管理

第一层(固定,2K token):
  - 系统角色定义
  - 核心规则
  
第二层(动态加载,4K token):
  - 基于用户意图,只加载前 5 个最可能用到的工具描述
  - 最近 3 轮对话历史
  
第三层(按需检索,2K token):
  - 通过 RAG 检索相关文档片段
  - 从记忆库中召回类似场景的解决方案
class AgentContextManager:
    def __init__(self, tool_registry, memory_store):
        self.tool_registry = tool_registry
        self.memory_store = memory_store
        self.history = []
    
    def build_prompt(self, user_query: str) -> List[Dict]:
        # 1. 意图识别
        intent = self._classify_intent(user_query)
        
        # 2. 动态加载工具
        relevant_tools = self.tool_registry.match(intent, top_k=5)
        
        # 3. 检索相关记忆
        memories = self.memory_store.search(user_query, top_k=3)
        
        # 4. 构建 prompt
        messages = [
            {"role": "system", "content": self._build_system_prompt(relevant_tools)},
            *self.history[-3:],  # 最近 3 轮
            {"role": "user", "content": f"【相关参考】
{memories}

【用户问题】
{user_query}"}
        ]
        
        return messages

优化后,单次调用降到 4-6K token,工具选择准确率从 72% 提升到 94%,延迟降低 60%。

五、前沿趋势:Agentic RAG 与自适应上下文

Agentic RAG

传统 RAG 是被动的——你问什么,我查什么。Agentic RAG 则让 Agent 主动决定什么时候检索、检索什么、是否需要多次检索。

class AgenticRAG:
    def __init__(self, llm, retriever, tools):
        self.llm = llm
        self.retriever = retriever
        self.tools = tools
    
    def answer(self, question: str) -> str:
        messages = [
            {"role": "system", "content": "你是智能 RAG 助手。"},
            {"role": "user", "content": question}
        ]
        
        for step in range(5):  # 最多 5 步
            response = self.llm.generate(messages)
            
            if "需要检索" in response:
                # Agent 主动决定检索
                query = self._extract_query(response)
                docs = self.retriever.search(query)
                messages.append({
                    "role": "user", 
                    "content": f"检索结果:{chr(10).join(docs)}"
                })
            elif "需要工具" in response:
                # Agent 主动调用工具
                tool_name = self._extract_tool(response)
                result = self.tools[tool_name]()
                messages.append({
                    "role": "user",
                    "content": f"工具结果:{result}"
                })
            else:
                # 生成最终答案
                return response
        
        return "无法在限定步骤内完成回答"

自适应上下文窗口

未来的趋势是让模型自己决定上下文窗口的大小和内容。Anthropic、Google 等公司已经在探索相关技术:

  • 选择性注意力:只关注窗口中的关键 token
  • 分层缓存:将长上下文分成多个层级,高频访问的层级常驻
  • 上下文蒸馏:将长对话历史压缩为摘要

总结

上下文窗口管理是从"能用"到"好用"的分水岭。滑动窗口简单但粗糙,RAG 灵活但需要精细调优,Agentic RAG 代表了未来的方向。

核心原则只有一条:尊重 Token 的稀缺性——把有限的上下文窗口留给最有价值的信息。对于大多数生产级应用,我的建议是:从 RAG 起步,配合动态工具加载和层级化设计,然后根据实际效果逐步引入 Agentic 能力。

上下文窗口在变大,但管理它的重要性不会消失。相反,更大的窗口意味着更需要管理。