MingJunDuan的博客
热爱可抵一切,探索未知之境
全站访问量

Token 不是字也不是词:300K 上下文窗口到底能装多少行代码

你在 Cursor 里把整个 Service 目录 @ 进去,说”帮我把这段下单逻辑重构一下”,结果它回你一句 context_length_exceeded。你心里纳闷:不是说好 200K、300K 的上下文窗口吗?我这才几万行代码,怎么连一个模块都塞不下?

问题的根子,在于把 token 当成了”词”。很多人以为 300K 就是”30 万个词”或”30 万个字”,于是按”一个汉字一个字、一行代码一行”去估量。但 token 既不是语言学里的”词”,也不是”字”,它是另一套东西——子词(subword)

这篇文章把”token 到底是什么”说透,然后给你一套 Java / Python 程序员能直接套用的换算表。它是《大模型无状态》 2.4 节的展开——那篇只给了三句粗经验,这篇把它们算成具体数字。


一、Token 是”子词”,不是”词”也不是”字”

1.1 先看一个真实例子

“我们去打篮球”这 6 个字,到底切成几个 token?我用 tiktoken 实际跑了一下,两个词表给出的结果完全不同:

分词器 结果 说明
cl100k_base(GPT-3.5/4) 6 个 token我们 “我们”高频→合并成 1 个;”篮”低频→被拆成 2 个字节级 token
o200k_base(GPT-4o/o 系列) 4 个 token我们 篮球 新词表里”篮球”也常见→合并成 1 个

这里有两个反直觉的点,正是理解 token 的钥匙:

  1. “我们”两个字被拼成了 1 个 token——所以 token 不是”一个字”。它俩高频共现,被整体固化成了一条词表项。
  2. “篮”这个字被拆成了 2 个 token(表里的 不是乱码,而是”这个 token 只是半个汉字的字节,单独解码不成合法 UTF-8”)——所以 token 也不是”一个词”,它甚至能拆到比字还小的字节级

同一句话,一个词表切 6 个,另一个切 4 个。这就是为什么所有”1 汉字 ≈ 1~2 token”的经验值都只能叫”粗经验”——它不是一个规则,而是”常见片段合并、生僻字拆字节“之后统计出来的平均值。

1.2 这套切法叫 BPE

模型不认识”字”,只认 token。发给模型前,整段文本会被一个分词器(tokenizer)切成 token 序列。主流的做法是 BPE(Byte Pair Encoding,字节对编码)

拿海量语料,反复统计”哪两个相邻片段最常一起出现”,把最常一起出现的那对合并成一个新片段,固化成词表里的一条。循环 N 次,得到一张固定大小的词表。之后切文本时,能整段命中词表的就整段拿走,命中不了的就往更小拆,拆到字节为止。

所以词表里既有 the我们篮球 这种”整词”,也有 uningappiness 这种”碎片”,还有 这种”半个字的字节”。token 就是这张词表里的一个条目,仅此而已。

1.3 三条规律

把上面的原理落到实际,记住三条:

① 高频片段会”打包”成 1 个 token

  • 我们 → 1 个 token(哪怕它是两个字)
  • 篮球 → 1 个(o200k)或 3 个(cl100k,因为”篮”被拆了)

② 英文比中文”划算”

  • playing1 个 token(高频整词)
  • unhappiness3 个:un + h + appiness(注意:不是语言学上的 un+happiness,纯粹是语料里 unappiness 出现得多)
  • The cat sat on the mat. → 7 个 token,且空格附着在词上 cat sat),不单独算

英文平均 1 个词 ≈ 1.3 个 token;中文平均 1 个汉字 ≈ 1~2 个 token。同样一句话,中文往往更”吃 token”,这就是中文对话更贵、更费窗口的原因。

③ 代码比散文更”重”

这段 3 行、61 字符的 Java 代码:

public void doSomething(int x) {
    if (x > 0) { return; }
}

切成 21 个 token(≈ 2.9 字符/token),比英文散文(≈ 4 字符/token)更密——因为 {(;、缩进空格每个都占 token。代码里的标点和缩进在模型眼里不是免费的。


二、对 Java / Python 程序员:实测 token 密度

光知道”代码更重”不够,得有个能直接用的数。我用一个接近真实业务的 Java 类和 Python 模块做了实测(o200k_base 词表):

Java 类(35 物理行,含 import / 注释 / 空行):

package com.example.order.service;

import java.util.List;

/**
 * 订单服务:负责订单的创建与状态流转。
 */
public class OrderService {

    private final OrderMapper orderMapper;

    public OrderService(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    public Order createOrder(CreateOrderRequest request) {
        if (request == null || request.getItems().isEmpty()) {
            throw new IllegalArgumentException("items 不能为空");
        }
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setStatus(OrderStatus.PENDING);
        order.setItems(request.getItems());
        return orderMapper.insert(order);
    }

    private void validatePrice(long price) {
        if (price <= 0) {
            throw new IllegalArgumentException("价格必须大于 0");
        }
    }
}

Python 模块(27 物理行):

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

logger = logging.getLogger(__name__)


@dataclass
class Order:
    order_id: str
    user_id: str
    status: str = "PENDING"
    items: Optional[List[str]] = None


def create_order(request: dict) -> Order:
    """根据请求创建一个待支付订单。"""
    if not request.get("items"):
        raise ValueError("items 不能为空")

    order = Order(
        order_id=request["order_id"],
        user_id=request["user_id"],
        items=request["items"],
    )
    logger.info("order created: %s", order.order_id)
    return order

实测结果:

样本 行数 字符数 token 数 平均
Java 类 35 行 856 191 ≈ 5.5 token/行
Python 模块 27 行 591 147 ≈ 5.4 token/行

注意这里的”行”是物理行——包含注释、空行、大括号独占行。如果只数”有效代码行”(LOC,去掉注释和空行),密度会升到 8~12 token/行。两者都常用,所以换算时要分清口径。

结论:Java / Python 一个实用的经验值——物理行按 5~8 token/行估,纯代码行按 8~12 token/行估。 下面按 6 token/物理行 和 10 token/有效代码行 两个口径换算。


三、300K / 250K / 200K 到底是多少

单位是 token,不是字也不是行。套用上面的密度,给你一张能直接查的表:

窗口 物理行(≈6 token/行) 有效代码行(≈10 token/行) 英文单词 中文汉字
200K 3.3 万行 2 万行 ≈ 15 万词 ≈ 10~20 万字
250K 4.2 万行 2.5 万行 ≈ 18.8 万词 ≈ 12.5~25 万字
300K 5 万行 3 万行 ≈ 22.5 万词 ≈ 15~30 万字

文字的手感:

  • 英文:300K ≈ 22.5 万词 ≈ 2 本普通长篇小说(每本约 8~10 万词),或《战争与和平》(约 58 万词) 的四成。
  • 中文:300K ≈ 15~30 万汉字 ≈ 一本中等技术书的量,或《三体》第一部(约 90 万字)的 1/6~1/3。

代码的手感(对咱们更重要):

  • 300K ≈ 3 万有效代码行,或 5 万物理行。
  • 一个典型 Java 服务的一个功能模块(controller/service/mapper 几十个类)通常是 1~3 万行——也就是说 200K~300K 一个窗口,大致能装下”一个功能模块 + 它的测试”,但装不下一个 10 万行以上的完整仓库。

顺带纠一个常被叫错的版本:你说到的 “Sonnet 4.7” 其实是 Claude Sonnet 4.5,官方标称 200K(1M 是 beta 档,且部分已停用)。各家标称的 200K/250K/300K 零头来自不同产品档位,量级都是”十万级”,不用纠结精确值


四、三个折扣:标称窗口 ≠ 有效窗口

上面那张表是”物理上限”,实际能用到的要打三个折扣(《大模型无状态》 3.4/3.5 节详细讲过):

  1. 输入 + 输出共享窗口:窗口 = 输入 token(system/历史/上下文/工具 schema)+ 输出 token(模型要生成的回复)。让模型整文件重写,输出本身可能吃掉几万 token,加上输入,很容易顶到上限。
  2. 迷失在中间(Lost in the Middle):即便没超限,模型对长上下文中间部分的注意力会自然衰减——开头和结尾记得牢,中间一大段”看是看见了,但没真看进去”。塞满反而”抓大放小”,逐行定位变差。
  3. 注意力退化:超长窗口下,模型对细节的抓取能力下降,精确引用、逐行修改的能力变弱。

所以一条重要心法:窗口长 ≠ 记得准。 对”精确定位、逐行修改”这类编码任务,宁可窗口里装”精准选中的少量相关代码”,也不要塞满一堆无关文件。


五、落到你的两个场景

5.1 代码分析 / 功能修改(Java / Python 为主)

  • 你不需要、也不应该喂整个仓库。 200K~300K 足够塞下:目标文件 + 它的调用链邻居 + 测试 + 一段 diff,一次改一个 feature 绰绰有余。
  • 实战建议:
    • 让 agent 工具(Cursor / Claude Code 的 grep、ripgrep)按需检索,窗口里只留”相关文件”;
    • 改动用 diff / 小步 edit,别整文件重写(既省 token,又把窗口留给最相关的上下文);
    • 一个会话聚焦一个任务,做完就新开,历史越杂噪声越多。

5.2 查 ES 做数据分析

  • 重点:别把 ES 的原始结果 dump 给模型。 JSON 极吃 token(引号、冒号、字段名重复)。实测一条 197 字符的订单 JSON:
{"order_id":"o_20260901_0001","user_id":"u_888","status":"PAID","amount":12800,"items":[{"sku":"A100","qty":2,"price":3900},{"sku":"B200","qty":1,"price":5000}],"created_at":"2026-09-01T10:23:45Z"}

这一条就 ≈ 79 token。照此估算,200K token 只能塞 几百到两千条文档;文档再大点(1~2KB 嵌套),只能塞 300~700 条

  • ES 的查询 DSL 本身只有几百 token,便宜得很。所以正确姿势是:
    • 让模型写/改 DSL,让 ES 用 size: 0 + 聚合(aggs)把结果算好,再喂聚合结果/汇总给模型分析;
    • 要原始数据就 _source 过滤只取需要的字段 + 取样 + 限制条数,而不是全量 hits

一句话:窗口是给你”放上下文”的,不是给你”倒数据”的。


六、附:自己动手数 token 的脚本

想对自己项目的真实代码有精确数,几行 Python 就够(Java 同学也可以跑,装个 tiktoken 即可):

import sys
import tiktoken

enc = tiktoken.get_encoding("o200k_base")

def count(path):
    with open(path, encoding="utf-8") as f:
        text = f.read()
    lines = text.splitlines()
    n = len(enc.encode(text))
    print(f"{n:>7} tokens | {len(lines):>4} 行 | {n/len(lines):.1f} token/行 | {path}")

for p in sys.argv[1:]:
    count(p)

用法:python tokencount.py src/**/*.java,会按文件输出 token 数和 token/行 密度。用它把你们仓库的关键模块数一遍,你对”一个模块到底占多少 token”就有精确数了,比任何经验表都靠谱。

两个提醒:

  • tiktokenOpenAI 的词表;Claude(Sonnet 4.5)用的是另一套词表,但原理相同(都是 BPE 子词),对同一段代码的具体 token 数略有出入,量级和”合并/拆分”规律一致,用来估窗口完全够用。
  • 想数 Claude 的精确词表,可以用 Anthropic 官方 SDK 的 count_tokens(Python / TypeScript 都有),但日常估量用上面这个脚本就足够了。

总结

  • token 是”子词”,不是”词”也不是”字”:高频片段合并(我们→1 个),生僻字拆字节(→2 个)。
  • 英文 1 词 ≈ 1.3 token,中文 1 字 ≈ 1~2 token,代码 1 物理行 ≈ 5~8 token。
  • 300K ≈ 3 万有效代码行 / 5 万物理行 / 15~30 万汉字——大约就是一个功能模块的量,不是一个仓库的量。
  • 标称窗口要打三个折扣:输入输出共享、迷失在中间、塞满不如塞准。
  • 对你:改代码就精准 @ 相关文件 + diff 小步编辑;查 ES 就让模型写 DSL、ES 算聚合,别倒原始 hits。

一句话记住:窗口是”工作台”,不是”仓库”,更不是”数据湖”。

本文阅读量