设计上下文窗口

M1
深入解析 · 记忆与上下文工程

设计上下文窗口:预算、排序与位置效应。

在推理时,上下文窗口是大语言模型唯一真正拥有的记忆。其他一切——向量库、草稿区、数据库——存在的意义都是为了决定哪些内容进入这个窗口。要把窗口当作一种稀缺的、需要精心设计的资源,给它一个明确的预算,而不是当成一个不断往里塞、直到溢出的口袋。

STEP 1

精确表述上下文问题。

大语言模型是其输入令牌(token)的纯函数。它没有任何能跨调用存活的隐藏状态。一个长期运行的智能体——会浏览、调用工具、跨多轮推理——会生成一条无界的轨迹:消息、工具结果、观察、中间推理。而窗口是有限的(200K、1M,无论多少)。整个上下文工程学科,就是对这种失配的管理。

随着轨迹增长,有三种力量在与你作对:

  • 硬性上限:超过窗口大小,请求就会失败或被静默截断。
  • 成本与延迟:注意力大致随令牌数线性到超线性增长;一个 150K 令牌的提示在每一轮都慢且贵,而不只是一次。
  • 质量衰减:即便远在上限之内,模型的注意力也分布不均。埋在巨大上下文中段的相关事实实际上等于不可见——这就是"中间迷失"(lost in the middle)效应。

更大的上下文窗口并不能解决上下文问题。它抬高了天花板,却让质量衰减问题更严重,因为现在你可以在每一轮都塞进 80 万令牌大多无关的历史,然后眼睁睁看着准确率悄悄下滑。

STEP 2

给每一轮一个明确的令牌预算。

在做任何巧妙的检索或摘要之前,你需要一个数字。按轮决定每一内容被允许占用多少令牌。其余的一律压缩、丢弃,或移到外部记忆。

# context/budget.py
from dataclasses import dataclass

@dataclass
class ContextBudget:
    total: int            # model window minus a safety margin
    reserve_output: int   # tokens kept free for the response
    system: int           # instructions, persona, policies
    tools: int            # tool schemas / definitions
    long_term: int       # retrieved memories & documents
    working: int         # recent turns / scratchpad

    def input_budget(self) -> int:
        return self.total - self.reserve_output

    def check(self) -> None:
        used = self.system + self.tools + self.long_term + self.working
        if used > self.input_budget():
            raise ValueError(
                f"over budget by {used - self.input_budget()} tok")

# Example: 200K window, conservative split.
BUDGET = ContextBudget(
    total=200_000, reserve_output=8_000,
    system=3_000, tools=4_000,
    long_term=60_000, working=120_000,
)

具体数字与工作负载相关,你会用评估(eval)来调优。重要的是这种纪律:每一类都有上限,当某一类溢出时,组装器被强制做出选择,而不是任由提示无限增长。

STEP 3

按位置排序内容,而不是按方便排序。

Transformer 并不会对所有位置一视同仁。经验上,上下文开头结尾的内容被回忆的可靠性远高于中段内容。两个实际推论:

  • 稳定、权威的内容放在最前:系统指令、任务定义、工具契约。它们也因不随轮次变化而受益于提示缓存。
  • 与决策最相关的内容放在最后:当前问题、最新的工具结果、排名最高的检索块。这是模型在生成前读到的最后内容。
  • 大体量、低置信度的材料放在中段——并且你要接受它可能被注意力忽视,所以它绝不应是某个关键事实的唯一载体。

一个有用的组装模板,从前到后:

[ system + policies ]        ← stable, cached, authoritative
[ tool definitions ]         ← stable, cached
[ retrieved long-term memory] ← bulk; ranked best-last
[ compacted older turns ]    ← summary, not raw
[ recent working turns ]     ← verbatim, high fidelity
[ current user / task turn ] ← last token the model reads

如果某个事实是关键的——一条智能体绝不能违反的约束——不要指望它能在一个 10 万令牌提示的中段存活下来。在系统块中重述它,并且在依赖它的动作之前再次紧贴着重述。

STEP 4

度量利用率,而不只是"塞下了没有"。

"没报错"不等于成功。给组装器加上埋点,让每一轮都记录预算如何花掉,以及其中有多少真正被模型用上。

# context/assemble.py
def assemble(budget, system, tools, memories, history, task):
    parts, used = [], {}

    parts.append(system);           used["system"] = ntok(system)
    parts.append(tools);            used["tools"]  = ntok(tools)

    # Fill long-term up to its ceiling, best-ranked LAST.
    mem = fit(memories, budget.long_term, keep="tail")
    parts.append(mem);              used["long_term"] = ntok(mem)

    # Working set: verbatim recent, compact the overflow.
    work = fit(history, budget.working, keep="tail",
               overflow=compact)
    parts.append(work);             used["working"] = ntok(work)

    parts.append(task)              # always last, never dropped

    log_metrics(used, budget)       # for offline analysis
    return "\n\n".join(parts)

在一个有代表性的评估集上跟踪这些指标:

  • 每类的填充率——是不是 long_term 总是塞满而 working 只用了一半?重新平衡。
  • 驱逐率——内容被丢弃的频率有多高,任务成功率是否与被丢弃的内容相关?
  • 承载答案内容的位置——如果回答问题的那个块位于位置 0.5(正中间),那就要预期失败并重新排序。
STEP 5

心智模型:把上下文当作工作集。

借用操作系统的术语。一个进程有巨大的虚拟地址空间,却只有很小的物理内存;操作系统把工作集——当下真正需要的页——驻留在内存中,其余换出到磁盘。一个智能体有无界的轨迹,却只有很小的上下文窗口;上下文工程把工作集保留在窗口内,把其余的换出到外部记忆(向量库、键值库、文件),需要时通过检索把它缺页换回。

本节其余的一切——短期与长期记忆、记忆类型、检索增强记忆、压缩、记忆存储、评估——都是针对三种操作之一的策略:保留什么驻留换出什么如何把正确的东西缺页换回。预算是让这些决策变得明确且可度量的会计层。

经验法则:如果你说不出在典型一轮中,你的上下文有多少令牌是系统、多少是工具、多少是记忆、多少是工作集,那你不是在做上下文工程——你是在祈祷。从预算开始。