4. 工作记忆的实现:滑动窗口、Token限制与注意力机制

好,咱们进入正题。工作记忆,说白了就是智能体的「便签纸」。它能记多少东西,能记多久,直接决定了这个智能体是聪明还是笨。我见过太多项目,模型本身很强,但就是因为上下文管理没做好,结果连简单的多轮对话都搞砸。

这一章,咱们就聊聊怎么实现工作记忆。三个核心手段:滑动窗口、Token限制、注意力机制。一个一个来。

4.1 滑动窗口:最朴素的短期记忆

滑动窗口的思路很简单——只保留最近N条消息。就像你手机里的聊天记录,默认只显示最近100条,再早的就折叠了。

为什么用滑动窗口?因为便宜、快。你想想看,每次对话都把所有历史塞进去,Token消耗不说,模型还会被无关信息干扰。我早期做客服机器人时就踩过这个坑——用户聊了20轮后,模型突然把第3轮提到的「退货」和当前问题混在一起,回答完全跑偏。

滑动窗口的实现,核心就两个参数:

参数 含义 推荐值
window_size 保留的消息轮数 5-20轮
max_tokens 窗口内总Token上限 模型上下文长度的30%-50%

代码实现其实就几行:

class SlidingWindowMemory:
    def __init__(self, window_size=10):
        self.window_size = window_size
        self.history = []
    
    def add(self, message):
        self.history.append(message)
        if len(self.history) > self.window_size:
            self.history.pop(0)  # 丢掉最旧的
    
    def get_context(self):
        return self.history[-self.window_size:]

嗯,这里要注意:pop(0) 在Python里是O(n)操作。如果窗口很大(比如1000轮),建议用 collections.deque。我曾经在一个高并发服务里用list做滑动窗口,结果QPS一上来,GC直接爆炸。换成deque后,问题就解决了。

我的习惯:滑动窗口不要只按轮数切。我会同时设一个Token上限。比如窗口大小是10轮,但如果某轮用户发了一大段代码,总Token超了,就优先丢弃最旧的那轮。这样既保住了近期对话,又不会撑爆上下文。

4.2 Token限制下的上下文管理

滑动窗口只是第一步。真正麻烦的是——每个模型的上下文长度是固定的。GPT-3.5是4K,GPT-4是8K/32K,Claude是100K。但你不能真的把100K全塞进去,原因有两个:

  1. 成本问题:Token就是钱。100K的输入,一次调用可能就要几毛钱。
  2. 注意力稀释:模型在超长上下文里,反而会「迷失」。中间部分的信息容易被忽略。

我做过一个实验:把一份50页的文档塞进32K模型,让它回答文档第25页的一个细节。结果正确率只有60%左右。但只塞相关的那3页,正确率直接到95%。

所以,Token限制下的上下文管理,核心策略就三个字:「挑着给」。

具体怎么做?我一般用这套流程:

  • 优先级排序:系统指令 > 当前用户输入 > 最近3轮对话 > 关键记忆 > 历史摘要
  • 动态截断:从优先级最低的开始丢弃,直到总Token数低于上限
  • 摘要压缩:对早期对话做摘要,用几句话代替几十轮

看个例子:

def build_context(system_prompt, user_input, history, memory, max_tokens=4000):
    context = []
    token_count = 0
    
    # 1. 系统指令必须保留
    context.append(system_prompt)
    token_count += count_tokens(system_prompt)
    
    # 2. 当前输入
    context.append(user_input)
    token_count += count_tokens(user_input)
    
    # 3. 最近3轮对话
    for msg in history[-3:]:
        if token_count + count_tokens(msg) > max_tokens:
            break
        context.append(msg)
        token_count += count_tokens(msg)
    
    # 4. 如果还有空间,加关键记忆
    for mem in memory[:5]:
        if token_count + count_tokens(mem) > max_tokens:
            break
        context.append(mem)
        token_count += count_tokens(mem)
    
    return "\n".join(context)
我曾经踩过的坑:千万别把摘要放在系统指令里。系统指令是每轮都发的,摘要如果太长,会吃掉大量Token。我建议把摘要单独存一个字段,只在需要时拼接进去。另外,摘要本身也要有Token上限,我一般控制在200Token以内。

4.3 注意力机制简介

说到工作记忆,就绕不开注意力机制。你可能听过「Attention Is All You Need」这篇论文。但咱们不聊论文,聊工程。

注意力机制,说白了就是让模型学会「看哪里」。就像你在人群中找朋友,你会自动忽略其他人,只盯着那张脸。模型也一样——在长序列里,它需要知道哪些词更重要。

我画了一张图,帮你理解注意力机制的核心流程:

注意力机制核心流程 输入序列 Query (Q) Key (K) Value (V) 注意力分数 = Softmax(Q · K^T / √d) 加权求和:输出 = Σ(分数 × V) 加权后的表示 Q: 当前词 K: 所有词 V: 所有词 分数越高,注意力越集中

流程其实就三步:

  1. 生成QKV:每个输入词都生成三个向量——Query(我要找什么)、Key(我有什么)、Value(我是什么)
  2. 算分数:用Query和所有Key做点积,再Softmax归一化。分数越高,说明这个词和当前词越相关
  3. 加权求和:用分数对Value做加权平均。分数高的词,对输出的影响就大

你可能会问:这和工作记忆有什么关系?

关系大了。注意力机制就是工作记忆的「寻址方式」。模型在生成下一个词时,会通过注意力去「回顾」之前的内容。如果注意力分配得好,模型就能记住关键信息。如果分配得不好,就会「失忆」。

核心观点:滑动窗口决定了「能看什么」,注意力机制决定了「重点看什么」。两者配合,才能让智能体在有限的上下文里,做出最聪明的决策。

我个人的经验是:在工程实现中,不要只依赖模型的注意力机制。你需要在应用层做好上下文裁剪和优先级排序。模型再聪明,也架不住你给它塞一堆垃圾信息。你想想看,让一个天才在垃圾堆里找钻石,他也得找半天。