跳转至

如何在有限 Token 下保留关键信息?

Token 是所有大模型应用的硬通货,所谓“记忆管理”,本质上就是在有限 Token 里做信息蒸馏。关键不是存得多,而是让存下来的每一个 token 都在“刀刃上”。


🎯 核心逻辑:把对话当成“被遗忘权”的资源

image.png

我把它拆成三个连续动作:定义关键 → 榨取信息密度 → 动态调度。


1️⃣ 先定义“什么是关键”——价值滤波器

不是每句话都值得留。我会把对话信息分成三类:

查看内嵌表格

def info_value(message: str) -> float:
    """给消息打分,决定是否进入记忆流程"""
    if any(kw in message for kw in ["我叫", "电话是", "地址在"]):
        return 1.0
    if any(kw in message for kw in ["偏好", "以后", "不喜欢", "记住"]):
        return 0.9
    if len(message) < 5:
        return 0.0
    return 0.2   # 普通陈述

2️⃣ 压缩信息密度——摘要与结构化抽取

当对话长度逼近 Token 预算时,不要粗暴截断,而是把旧的对话“炼”成浓缩摘要。

我习惯用一个小型、快速的模型(或同一个大模型但低温度)把过去 N 轮“榨”成三个字段:

def compress_messages(messages: list[dict], llm) -> dict:
    prompt = f"""
    请从以下对话中提取:
    1. 关键事实(用户姓名、偏好、已确认的决定)
    2. 当前任务进度
    3. 最重要的用户情绪或偏好变化
    对话:
    {format_messages(messages)}
    输出JSON。
    """
    return json.loads(llm.invoke(prompt))

这样 2000 Token 的旧对话,可能就被压成一段 200 Token 的摘要,信息密度提升 10 倍。


3️⃣ 动态调度:Token 预算的“零和博弈”

窗口是一个动态平衡的容器,我维护三个逻辑分区:

查看内嵌表格

关键实现:

import tiktoken

class TokenBudget:
    def __init__(self, max_tokens=4000):
        self.max_tokens = max_tokens
        self.enc = tiktoken.get_encoding("cl100k_base")
        self.system_msg = ""
        self.messages = []        # 原始最近消息
        self.archive = ""         # 压缩后的历史摘要

    def count(self):
        return (len(self.enc.encode(self.system_msg)) +
                sum(len(self.enc.encode(m["content"])) for m in self.messages) +
                len(self.enc.encode(self.archive)))

    def add_message(self, msg):
        self.messages.append(msg)
        self._rebalance()

    def _rebalance(self):
        """超过预算时,把最旧的消息合并进 archive,并压缩 archive"""
        while self.count() > self.max_tokens and len(self.messages) > 2:
            # 取出最旧的一条,丢给压缩机
            oldest = self.messages.pop(1)  # 保留系统消息和最新用户
            self.archive = self._merge_into_archive(oldest["content"], self.archive)

        # 如果 archive 本身太大,对它再做一次摘要压缩
        if len(self.enc.encode(self.archive)) > 500:
            self.archive = self._summarize(self.archive)

    def _merge_into_archive(self, text, current_archive):
        # 实际场景会用 LLM 做增量合并
        return current_archive + "\n" + text

    def _summarize(self, text):
        # 调用 LLM 将 text 压缩到 300 tokens 以内
        return f"[摘要] {text[:300]}..."   # 示意

这样,窗口始终控制在预算内,且 archive 里保留了整段对话的“知识结晶”。


4️⃣ 完整示例:一个可运行的记忆管理器

import tiktoken
from collections import deque

class CondensedMemory:
    def __init__(self, llm, max_tokens=4000):
        self.llm = llm
        self.max_tokens = max_tokens
        self.enc = tiktoken.get_encoding("cl100k_base")
        self.system = ""
        self.recent = deque()          # 保留最近的原始消息
        self.summary = ""              # 压缩积累的摘要

    def add(self, role, content):
        if self._should_discard(content):
            return
        self.recent.append({"role": role, "content": content})
        self._maybe_compress()

    def get_context(self):
        """返回拼接好的上下文"""
        parts = [self.system]
        if self.summary:
            parts.append(f"[历史摘要] {self.summary}")
        parts.extend([f"{m['role']}: {m['content']}" for m in self.recent])
        return "\n".join(parts)

    def _should_discard(self, content):
        return content.strip() in ["好的", "收到", "ok"] or len(content) < 4

    def _maybe_compress(self):
        current_tokens = len(self.enc.encode(self.get_context()))
        if current_tokens <= self.max_tokens * 0.8:
            return

        # 将 recent 的前半部分压缩进 summary
        split_point = len(self.recent) // 2
        old_messages = [self.recent.popleft() for _ in range(split_point)]
        text_block = "\n".join([f"{m['role']}: {m['content']}" for m in old_messages])

        # 调用 LLM 做摘要(替换为真实调用)
        new_summary = self.llm.compress(text_block)
        if self.summary:
            self.summary = self.llm.merge_summaries(self.summary, new_summary)
        else:
            self.summary = new_summary

🧠 一个值得反思的点

我们都认为“保留关键信息”是为了更准确,但实验发现:适当丢弃低权重的上下文噪音,反而能提升复杂推理的正确率。因为模型在某些任务上会被无关细节带偏。所以,“有限 Token”不是缺陷,而是一个隐式正则化器。面试时抛出这一点,往往能把讨论拉到更高维度。