纲要

  • 上下文窗口是什么
  • Token 的本质与计算方式
  • 上下文窗口的限制因素
    • Transformer 注意力机制的复杂度
    • 硬件显存与响应时间
    • 训练数据的短文本倾向
    • 注意力衰减与长距离依赖
  • 应对上下文窗口限制的策略
    • 检索增强生成
    • 滑动窗口
    • 新架构探索
  • LangChain 中的 Token 追踪
    • usage_metadataresponse_metadata
    • 流式输出中开启 stream_usage
  • 实战:基于滑动窗口的长文本处理与 Token 监控
    • 项目结构
    • 使用 tiktoken 计算 Token 数
    • 实现滑动窗口切分
    • 结合 ChatOpenAI 调用并记录用量
  • 代码

上下文窗口的概念

大模型的“上下文窗口”是指模型一次能够处理的最大 Token 数量。可以把它想象成一个固定大小的视野:模型只能“看到”窗口内的内容,超出窗口的部分会被忽略。这个窗口长度是人为设定的,直接决定了模型能处理多长的输入和输出。

不同模型的上下文窗口差异很大,例如 GPT-4 有 8K、32K 甚至 128K 的版本,而某些模型可能只有 4K。窗口越大,模型处理长文档、多轮对话的能力越强,但对计算资源的要求也越高。

Token:大模型的“基本语言单位”

人类使用字符和单词交流,但模型底层处理的并不是原始文本,而是 Token。Token 是模型能够理解的最小信息单元,它可以是单词的一部分、整个词、标点符号,甚至是图片或音频的片段(在多模态模型中)。

以 OpenAI 的分词方式为例,英文中大约 1 个 Token 相当于 0.75 个单词,或者 4 个英文字符。句子 “LangChain is cool.” 会被切分成 ["Lang", "Chain", " is", " cool", "."] 共 5 个 Token,而字符数是 18。可见 Token 的信息密度远高于原始字符。

因为 Token 并非与字符一一对应,所以在开发 AI Agent 时,必须精确计算文本的 Token 数,避免输入超出上下文窗口导致截断或响应异常。

为什么上下文窗口不能无限大

上下文窗口的大小受到多方面因素的制约:

  • 计算复杂度:当前主流的大模型均采用 Transformer 架构,其核心的自注意力机制计算复杂度为 O(n²)n 为序列长度)。序列长度翻倍,计算量和显存需求会增长约四倍。这是限制窗口长度的根本原因。
  • 硬件限制:GPU 显存大小直接决定了能容纳的序列长度上限。长序列需要更多显存来存储中间状态,同时也会使推理延迟显著增加。
  • 训练数据分布:预训练语料中长文本比例通常较低,模型在学习长距离依赖关系时存在天然困难。如果窗口设得过大,模型在早期信息和当前信息之间容易“遗忘”或混淆。
  • 注意力衰减:随着序列增长,模型对早期 Token 的关注度会逐渐下降,导致生成质量变差,尤其是需要引用前文的问答场景。

这些因素共同决定了每个模型都必须设置一个权衡后的上下文窗口值,而不是任意扩大。

突破窗口限制的常见策略

当实际需求超出模型上下文窗口时,可以采用以下方法:

  • 检索增强生成:将历史对话或文档存储到外部向量数据库中,每次请求时根据当前问题检索相关片段,只将最相关的内容拼接进 Prompt,从而“外挂”记忆。
  • 滑动窗口:维护一个固定长度的 Token 窗口,当新内容加入时,移除窗口最左侧的部分(即最早的内容)。这样可以始终在有限窗口内保留最近、最相关的信息。
  • 新架构探索:研究者也在尝试用状态空间模型、线性注意力等新结构来替代 Transformer,以突破平方复杂度限制,但目前尚在演进阶段。

这些策略并非互斥,在实际 Agent 开发中常常组合使用。接下来,我们重点实现一种简单的滑动窗口处理方式,并结合 LangChain 的 Token 追踪功能,直观展示窗口管理全流程。

LangChain 中的 Token 用量追踪

LangChain 的大模型组件返回的 AIMessage 中包含标准化的 usage_metadata 字段,可直接获取输入、输出及总 Token 数。无论底层是 OpenAI、DeepSeek 还是 Anthropic,该字段都会被封装(部分模型可能为空)。

此外,某些模型(如 OpenAI)在流式输出时支持开启 stream_usage=True,能够在每一个 chunk 中附带累计的 Token 用量,方便实时监控成本。下面通过代码具体展示。

项目结构

本示例将实现一个简单的滑动窗口处理器,能够:

  • 使用 tiktoken 计算 Token 数;
  • 将超长文本按照指定窗口大小和重叠步长切分成多个片段;
  • 逐片段调用大模型并记录每个窗口的 Token 消耗。

目录结构如下:

├── main.py          # 滑动窗口处理与 Token 追踪
└── requirements.txt # 依赖列表

依赖清单(requirements.txt):

langchain-openai>=0.3.0
tiktoken>=0.7.0

完整可运行代码

import os
from typing import List, Tuple
import tiktoken
from langchain_openai import ChatOpenAI

# ---------- 滑动窗口切分器 ----------
class SlidingWindowSplitter:
    """将文本按 Token 数切分为滑动窗口片段"""
    def __init__(self, model_name: str = "gpt-4", max_tokens: int = 2000, overlap: int = 200):
        """
        :param model_name: 对应 tiktoken 的编码模型名
        :param max_tokens: 每个窗口的最大 Token 数
        :param overlap: 相邻窗口之间的重叠 Token 数
        """
        self.encoding = tiktoken.encoding_for_model(model_name)
        self.max_tokens = max_tokens
        self.overlap = overlap

    def token_count(self, text: str) -> int:
        return len(self.encoding.encode(text))

    def split(self, text: str) -> List[str]:
        tokens = self.encoding.encode(text)
        total = len(tokens)
        chunks = []
        start = 0
        while start < total:
            end = min(start + self.max_tokens, total)
            chunk_tokens = tokens[start:end]
            chunks.append(self.encoding.decode(chunk_tokens))
            start = end - self.overlap  # 重叠部分
        return chunks

# ---------- Token 追踪与调用 ----------
def demo_sliding_window_processing():
    # 1. 初始化模型(使用 GPT-4,需设置 OPENAI_API_KEY 环境变量)
    llm = ChatOpenAI(
        model="gpt-4",
        temperature=0.3,
        max_tokens=500,           # 每次回复的最大长度
        api_key=os.getenv("OPENAI_API_KEY"),
    )

    # 2. 模拟一篇很长的对话记录或文档
    long_text = """
    在人工智能的发展历程中,大语言模型的出现是一个重要的里程碑。
    它使得机器能够理解和生成自然语言,从而在对话系统、内容创作、代码生成等领域展现出巨大的潜力。
    然而,大模型并不是万能的,其固有的上下文窗口限制经常导致长文档处理、多轮对话等场景中的信息丢失。
    为了突破这一限制,研究者们提出了多种方案,包括检索增强生成、滑动窗口、以及探索全新的模型架构。
    其中,滑动窗口是一种简单而有效的方法:它将长文本切分成固定大小的片段,每个片段之间保留一定的重叠区域,
    使得模型可以在窗口内处理局部上下文,并通过对多个窗口结果的后处理来获得全局理解。
    这种技术在实际的 AI Agent 开发中被广泛采用,尤其适用于问答、摘要等需要长上下文的任务。
    """ * 50  # 构造一个长度超过 5000 Token 的文本

    splitter = SlidingWindowSplitter(model_name="gpt-4", max_tokens=2000, overlap=200)
    print(f"原文总 Token 数: {splitter.token_count(long_text)}")
    windows = splitter.split(long_text)
    print(f"切分成 {len(windows)} 个窗口\n")

    # 3. 对每个窗口调用模型,并追踪 Token 用量
    total_input_tokens = 0
    total_output_tokens = 0
    results = []

    for i, win in enumerate(windows):
        print(f"--- 处理窗口 {i+1} (Token 数: {splitter.token_count(win)}) ---")
        msg = llm.invoke(f"请用中文总结以下文本的核心要点:\n{win}")
        # 记录用量
        usage = msg.usage_metadata
        if usage:
            print(f"  输入 Token: {usage.get('input_tokens')}, 输出 Token: {usage.get('output_tokens')}, 总计: {usage.get('total_tokens')}")
            total_input_tokens += usage.get('input_tokens', 0)
            total_output_tokens += usage.get('output_tokens', 0)
        results.append(msg.content)
        # 演示流式追踪(可选,只针对最后一个窗口展示)
        if i == len(windows) - 1:
            print("  流式追踪演示:")
            for chunk in llm.stream(win, stream_usage=True):
                # stream_usage 会将累计用量放在 chunk.usage_metadata 中
                if chunk.usage_metadata:
                    print(f"    当前累计: {chunk.usage_metadata}")
                # 这里只打印用量,不打印内容
            print()

    print(f"\n全部窗口总消耗 - 输入 Token: {total_input_tokens}, 输出 Token: {total_output_tokens}, 合计: {total_input_tokens + total_output_tokens}")
    return results

if __name__ == "__main__":
    demo_sliding_window_processing()

代码说明

  • SlidingWindowSplitter 使用 tiktoken 将文本按 Token 数切分,并可设置重叠量保证语义连贯。
  • ChatOpenAI 调用时,通过 msg.usage_metadata 提取标准化用量;对于流式模式,开启 stream_usage=True 后可在每个 chunk 的 usage_metadata 中获得累计统计。
  • 实际开发中,可结合 LangChain 的 RunnableLambda 将滑动窗口封装为链式步骤,方便嵌入 Agent 工作流。

关键要点总结

  • 上下文窗口是模型一次能处理的 Token 上限,受计算复杂度、硬件、训练数据等因素限制。
  • Token 是模型理解的最小单元,不等同于字符或单词,开发前务必用专门工具(如 tiktoken)进行精确计算。
  • 突破窗口限制的实用方法包括 RAG(外挂记忆)和 滑动窗口(局部上下文)。
  • LangChain 提供 usage_metadataresponse_metadata 两个标准化属性来追踪 Token 消耗,支持流式监控。
  • 滑动窗口策略实现简单且效果可靠,是 AI Agent 处理长文本时的常用技术。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐