1. 项目思路拆解:为什么需要 context-mode 这样的东西
先说清楚这个项目的定位。我最初接触 context-mode,是因为手头一个基于大语言模型的项目总在"失忆"——对话稍微长一点,模型就开始答非所问,或者把早先明确说过的信息给忘了。后来我去翻了模型的上下文窗口机制,才发现问题不在模型本身,而在于我根本没有好好管理"送进模型的那段文本"。
在大模型应用开发里,context-mode 指的不是某个官方 SDK 的功能,而是一套你自己设计的上下文组织方式。它决定了一件事:在每一轮请求里,你往大模型的输入里塞什么、不塞什么、按什么顺序塞、塞到多少量为止。就这么一个问题,往小了说是"提示词怎么拼",往大了说是一个完整的信息管理工程。
我见过不少刚入行的同学,写 AI 应用的时候习惯性地把整个对话历史一股脑全丢给模型。这种做法在小 demo 里完全没问题,一旦上线面对真实用户和长时间对话,立刻会撞上三堵墙:
第一堵墙是上下文窗口上限。像 GPT-4 这类模型虽然有 128k 的窗口,但也扛不住连续几天的多轮对话累积。就算窗口够大,模型对超长上下文的注意力也会分散,早期的关键信息会被"淹没"在一堆无关中间内容里。第二堵墙是成本。每次请求都带全量历史,Token 消耗是指数级上升的,一个日活一万的客服机器人,每月光是上下文费用就能吃掉大量预算。第三堵墙是信息污染。用户中途聊了无关话题,这些噪音会污染模型对主任务的判断,导致忠实度下降。
context-mode 做的事情,就是把这三堵墙一层层拆掉。它本质上是一个分级缓存 + 动态组装的思路:把上下文按重要程度和时效性分成不同层级,每轮请求只组装当前任务真正需要的那部分,而不是把"所有发生过的事情"全搬进去。
我做这个项目的另一个原因,是发现市面上的开源方案要么太重、要么太死。LangChain 有 ConversationSummaryMemory,但它的摘要策略比较粗糙,触发压缩后会把细节全部抹平;外接向量库做记忆检索,又需要额外维护一套基础设施。我想要的是一个轻量的、可插拔的、能自己控制每一个细节的上下文管理器。于是就有了自己造轮子的 context-mode。
这个项目适合几类人参考:做 Agent 应用但发现模型总是"忘事"的开发者;做客服或问答类产品但 Token 成本压不住的同学;以及想理解 LangChain 等框架底层记忆机制是如何设计的学习者。后面我讲的每一个方案,都是我在真实项目里跑过、调过、踩过坑之后沉淀下来的。
2. 核心模式拆解:三种基础 context-mode 的设计与取舍
2.1 固定上下文模式:系统提示词与常量信息注入
最基础也是最重要的一个模式,是固定上下文。它的核心思想是:把"每一轮都必须存在"的信息,放在一个不会变动的块里,每次请求都原样携带。
这类信息通常包括三类:
- 系统级设定:你是谁、你扮演什么角色、你的回答风格是什么
- 任务级约束:当前项目的目标、不能逾越的边界、输出格式要求
- 场景常量:固定知识(如产品价格表、企业介绍)、用户核心画像(如会员等级、VIP 标识)
在设计这个模式时,我学到的第一个教训是:系统提示词不是写散文,而是在写代码式的指令。很多初学者喜欢在系统提示词里堆砌华丽的描述,比如"你是一个充满智慧且善解人意的贴心助手,你会用温暖亲切的语言回答用户的问题"。实际上这类描述占用了大量 Token,但对模型行为的约束力非常弱。明确、简短、无歧义的指令,效率远高于修饰词。
我的实操建议是给固定上下文设一个 Token 预算上限,通常控制在总上下文窗口的 10%~15%。比如使用 8k 窗口的模型时,固定上下文尽量控制在 1000~1200 Token 以内。超过这个量,要么是你要注入的固定知识太多,该考虑外挂检索;要么是你的系统提示词里堆了大量废话,需要精简。
另一个关键点:固定上下文的位置。多数模型注意力机制对开头和结尾的内容更敏感,也就是所谓的"迷失在中间"问题。所以我会把最重要的任务指令放在固定上下文的最开头,把一些补充性的约束放在结尾处,让中间段放相对次要的常量信息。
2.2 滑动窗口模式:对话历史的时间与容量管理
第二种模式是滑动窗口,也是最直接的一种对话历史管理方式。核心逻辑很简单:只保留最近 N 轮对话,更早的内容一律丢弃。
这个模式适合什么场景?适合那些单轮交互性弱、但连续决策依赖近期上下文的场景,比如代码编辑器里的 AI 补全、短对话客服(用户问完一个问题就走)。在这种场景里,近期对话已经包含了完成任务所需的 90% 信息,没必要背更早的包袱。
但在真正实施时,"只保留最近 N 轮"这句话里有太多细节要处理。我的经验是,滑动窗口的容量不应该按"轮数"算,而应该按 Token 数算。因为每一轮的对话长度可能差异巨大,固定轮数会导致 Token 总量忽高忽低,超过窗口上限时请求直接报错。比如用户发了一段 3000 Token 的长文本,加历史可能 3 轮就爆炸了;而如果用户只发"嗯"、"好的"这类短消息,50 轮也远不到上限。
我最终采用的方案是"先定预算,再数轮次":
- 从总窗口里减去固定上下文和输出预留的 Token,得到可分配给对话历史的预算 H
- 从最新的对话开始,逐条往回加入历史
- 每加入一条,计算它的 Token 数,累加到总消耗超过 H 就停止
- 如果最后一条消息会把预算超出太多(超过 30%),还要做一些截断处理
这个模式的最大缺点,是它天然会遗忘早期信息。如果用户在第 3 轮说了一句决定需求走向的关键话,到第 30 轮时这段记忆已经没了。解决这个问题,需要把滑动窗口和下面说的其他模式组合使用——在滑动窗口之外,单独把"关键用户画像"和"关键任务决策"抽取出来,放入固定上下文。
2.3 摘要压缩模式:用低成本保住长期记忆
当对话长度超过了滑动窗口的承载能力,又不能简单丢掉早期信息时,就该上摘要压缩模式了。它的思路是:与其保留原始对话记录,不如把重要信息浓缩成一句话或一段摘要,永久存放在一个特殊区域里。
这个模式我最早是在客服工单场景中验证的。用户可能断断续续聊了两三天,中间隔了很久,但每一轮里都提到了自己对某个功能的需求。如果用滑动窗口,第二天的对话早就看不到第一天的内容了;但如果把每轮的关键信息持续压缩进一份"工单摘要",模型任何时候都能拿到用户需求的全貌。
实现摘要压缩有几个注意点,这些是我实际调试后才确定的:
注意一:摘要不是一次性生成的,而是渐进式合并的。旧摘要 + 新对话 → 新摘要。用语言模型把两份信息合并成一份,比你每隔固定轮次就重新生成一份完整摘要更省 Token,而且信息连续性更好。
注意二:压缩触发条件要有阈值,不能每次对话都压缩。我给自己的项目设定的是:当对话历史的 Token 数达到滑动窗口预算的 70% 时,触发一次压缩;压缩后如果仍然过高,再继续压缩更早的部分。因为压缩本身也是一次模型调用,是会消耗成本和增加延迟的,频繁触发反而得不偿失。
注意三:摘要里要保留"可回溯的线索"。比如"用户在 14:30 提到了价格太贵",比"用户提到了价格问题"更有价值。这样当用户之后对价格展开讨论时,模型能拼出完整的时间因果线。
摘要模式也有它的软肋:信息失真。语言模型做摘要时会做"概括",而概括天然会遗漏细节。所以我在实际项目中做了个兜底:摘要只负责保留需要长期参考的信息,而最近几轮的原始对话始终保留在滑动窗口里。两者配合,而不是互相替代。
2.4 混合模式:把以上策略组合成真正的 context-mode
我最终做出来的 context-mode 项目,并不是这三个模式中单独某一个,而是它们的组合。我称它为混合上下文模式,它在每一轮请求前动态决定上下文的结构,形如:
[固定上下文区](系统提示词 + 用户画像 + 任务常量) [摘要记忆区](压缩后的长期关键信息) [滑动历史区](最近 N 轮原文) [检索命中区](从外部知识库召回的相关片段) [当前输入区](这一轮用户的新消息)这个顺序不是随便排的,它遵循着一条"从稳定到易变"的递进原则。固定上下文最稳定,优先级最高;当前输入最易变,但恰好是模型此刻必须响应的焦点,放在最后相当于给模型一个"起跑指令"。
这个模式下,每一轮请求的组装都变成了一个动态计算的过程。首先预估这轮请求的各种内容需要多少 Token,然后按优先级和弹性空间做取舍——预算不够就先压缩摘要,再不行就裁剪滑动历史,但固定上下文永远不被动。
我在下面的第三、第四部分会详细展开这个混合模式的工程实现和调优过程,包括关键参数怎么定、压缩触发怎么判断、以及最容易出错的几个坑。这些内容才是这个项目真正的核心价值所在。
3. 工程实现:context-mode 的核心逻辑与实践细节
3.1 顶层设计:用"分栏"思路拆解上下文组装流程
在工程上,我实现 context-mode 的第一件事不是写代码,而是画出上下文字段的"布局"。你可以把它想象成用 CSS 做页面布局:浏览器渲染页面时不关心你给它多少内容,只关心每个区块占据什么位置、多大尺寸、溢出怎么办。上下文组装也是一样,我把整个 prompt 当作一个有固定版心的页面,各个内容区块在这个版心里做弹性排布。
对应到代码层面,我定义了这样的数据结构:
@dataclass class ContextBlock: block_id: str # 区块唯一标识 block_type: str # "fixed" | "summary" | "history" | "retrieval" | "current" priority: int # 0-100,越高代表越不可裁剪 max_tokens: int # 该区块的硬上限 estimated_tokens: int # 当前预估用量 content: str # 实际内容 metadata: dict # 备用信息,如生成时间、来源等五个区块中,每种都有自己的预算上限,但合计必须控制在一个总预算以内。我定的总预算计算公式是:
总预算 = 模型窗口上限 - 输出预留 - 安全缓冲其中模型窗口上限来自 API 参数或模型配置文件,输出预留是你期望模型回答的最大长度,安全缓冲则是给 token 计算误差留在的余量。举个例子,模型窗口 8192,输出预留 1024,安全缓冲 256,那么上下文总预算就是 6912 Token。这个数比 8192 看起来小,但它保证了你永远不会触顶报错。
3.2 Token 预算计算:哪种计数方式靠谱?
做 context-mode 项目,你必须非常认真地对待 Token 计数这件事。用 len(text) 数字符、用 len(text.split()) 数字数,都不靠谱。中文字符的 Token 密度和英文字符完全不同,一句话里混杂代码、URL、特殊符号时更是千差万别。
我的建议是务必用模型对应的 Tokenizer 来计算,而不是自己拍脑袋估算。
# 以 OpenAI 的 tiktoken 为例 import tiktoken def count_tokens(text: str, model: str = "gpt-4") -> int: encoding = tiktoken.encoding_for_model(model) return len(encoding.encode(text))这套方法在 OpenAI 生态里最顺手。如果你用的是 Anthropic 的 Claude,就用 claude-tokenizer 这类对应工具。如果你用的是本地开源模型,通常 Hugging Face 的 tokenizer 接口也能拿到准确的 token 数。
这里有个实践上的小技巧:不要把每一轮请求都重新 tokenize 一遍全部内容。一次性编码整个长文是很耗 CPU 的,尤其在高并发场景下更是累赘。我采用的是"增量计数 + 缓存"方案:
- 给每一条进入上下文的文本,在写入时就预先算好 token 数,缓存起来
- 需要总计时,累加各缓存值即可
- 只有当某条文本要基于旧内容做修改(比如摘要合入新对话)时,才重新计算
这样组装一个请求的成本,从"全量编码"降到了"做加法",速度上能快出好几倍。
3.3 组装调度器:核心类的核心逻辑
调度器是整个 context-mode 的心脏。它的职责是:拿到五个区块的内容素材,计算所有 token 预算,按优先级依次裁剪,最终拼合成一条适合发给模型的 prompt。
我最初写这个调度器时,把逻辑写成了一个长函数,加了各种 if/elif 判断,结果越改越乱。后来我彻底重构,采用了一个简单的循环:
class ContextScheduler: def __init__(self, total_budget: int): self.total_budget = total_budget self.blocks = [] def add_block(self, block: ContextBlock): self.blocks.append(block) def assemble(self) -> str: # 第一遍:分配预算优先级 sorted_blocks = sorted(self.blocks, key=lambda b: b.priority, reverse=True) alloc = {} remaining = self.total_budget # 第二遍:逐区块按优先级确认是否保留 for block in sorted_blocks: if block.estimated_tokens <= block.max_tokens and block.estimated_tokens <= remaining: alloc[block.block_id] = block.content remaining -= block.estimated_tokens else: # 压缩或裁剪逻辑 processed = self._compress_or_truncate(block) if processed is not None: alloc[block.block_id] = processed remaining -= count_tokens(processed) # 第三遍:按固定顺序拼接(不是按优先级) ordered = ["fixed", "summary", "history", "retrieval", "current"] final_parts = [alloc[bid] for bid in ordered if bid in alloc] return "\n\n".join(final_parts)这个逻辑有一个很重要的取舍:优先级决定"谁先被保障",但拼接顺序是另一个维度,不能混为一谈。在真实场景里,模型对上下文开头和结尾的注意力最强,所以固定上下文必须放在开头,当前输入必须放结尾。中间的部分按回忆的重要性依次排布。优先级高低只影响裁剪时谁先被砍掉,不影响拼接时的物理位置。
3.4 压缩与裁剪策略:被砍掉的内容去哪了
调度器在预算不足时,会调用一个压缩/裁剪函数。这里面有一个关键的思路:**能压缩就别直接删。**直接删除等于永久牺牲信息,压缩只是降低信息的密度。
我实现的压缩流程大致是:
- 对触发压缩的区块,先判断它的内容类型。如果是 user/assistant 对话,使用"延续摘要"策略;如果是检索片段,则按相似度得分排序,从最低分的开始裁减。
- 写一个专门的摘要函数,它接收"旧摘要 + 当前区块里的新内容",用一次模型调用生成合并摘要。
- 处理完成后,用最新的 token 计数替换掉原 content 和 estimated_tokens。
- 如果压缩后还是超过预算,把区块按时间切成几段,保留最新的,丢最旧的。
在实际调试中,我遇到的典型情况是:滑动历史区先触发压缩,摘要区的内容渐渐变多,结果摘要区自己也撑爆了预算。所以我又加了一个递归逻辑——摘要区超限时,对摘要再执行一次"摘要的摘要"。直到最后,如果摘要已经压无可压,才允许切割摘要里最早的部分直接丢弃。
3.5 集成示例:把 context-mode 接入一个实际 AI 应用
理论讲了不少,直接看一段简化后的集成代码更容易理解。这是我项目里一个"知识库问答助手"的简化版:
# app.py 核心逻辑 from context_scheduler import ContextScheduler from token_counter import count_tokens # 初始化常量 MODEL = "gpt-4" WINDOW_LIMIT = 8192 OUTPUT_RESERVE = 1024 SAFETY_BUFFER = 256 TOTAL_BUDGET = WINDOW_LIMIT - OUTPUT_RESERVE - SAFETY_BUFFER # 6912 def build_prompt(system_prompt, user_profile, memory_summary, history_messages, retrieved_chunks, user_input): scheduler = ContextScheduler(TOTAL_BUDGET) # 固定上下文:系统提示 + 用户画像,优先级最高,不可被裁剪 fixed_content = system_prompt + "\n" + user_profile scheduler.add_block(ContextBlock( block_id="fixed", block_type="fixed", priority=100, max_tokens=1024, estimated_tokens=count_tokens(fixed_content), content=fixed_content )) # 摘要记忆:长期关键信息 scheduler.add_block(ContextBlock( block_id="summary", block_type="summary", priority=70, max_tokens=2048, estimated_tokens=count_tokens(memory_summary), content=memory_summary )) # 滑动历史:最近几轮原始对话 history_text = "\n".join(history_messages[-6:]) # 先粗过滤到6轮 scheduler.add_block(ContextBlock( block_id="history", block_type="history", priority=50, max_tokens=2048, estimated_tokens=count_tokens(history_text), content=history_text )) # 检索命中:从知识库召回内容 retrieval_text = "\n".join(retrieved_chunks[:3]) scheduler.add_block(ContextBlock( block_id="retrieval", block_type="retrieval", priority=30, max_tokens=1024, estimated_tokens=count_tokens(retrieval_text), content=retrieval_text )) # 当前输入 scheduler.add_block(ContextBlock( block_id="current", block_type="current", priority=100, max_tokens=512, estimated_tokens=count_tokens(user_input), content=user_input )) return scheduler.assemble()这个集成代码的实际含义是:普通情况下历史 6 轮足够用,固定信息和用户画像常驻,摘要用于回溯,检索片段只在相关时才出现。当某轮对话特别长、预算吃紧时,调度器会先砍检索片段,再压缩历史,实在不够再从摘要的早期部分开刀。
在真实部署中,这些区块内容并不需要每一轮都全部重新传进去。我维护了一个会话缓存,其中固定区块和摘要区块是跨轮次复用的,只有滑动历史和当前输入是每轮新拼的。这样做的好处非常明显:省掉了大量 tokenize 和组装时间,也让调度器的行为更容易预测和调试。
4. 常见问题与排查技巧实录
4.1 Token 超限:为什么预算算好了还会炸?
我最开始做 context-mode 时,总预算明明设定得很保守,但跑着跑着还是会报"maximum context length exceeded"错误。排查了很久才发现,问题出在模型输出上——模型生成的回答可能远比我预想的更长,比如我预留了 1024 Token 给输出,但它生成了 1500 Token,总消耗超出窗口限制。
这个问题没有完美的解,但有几个实用的应对手段:
- 你可以在 API 请求里设置 max_tokens 参数,它会把模型输出的硬上限压死,不给超溢机会。代价是当模型本来能写出更长回答时,会被截断。
- 把输出预留值调高一点,比如从 1024 提到 1400,牺牲一点上下文空间,换取生成自由度。
- 给生成结果做后置检查:如果发现输出触顶被截断,就在提示词里追加一句"请继续",再用上次的上下文加之前生成的输出拼一段续写请求。
这三个办法各有利弊,我在生产环境里用的是第一个,因为稳定性和可控性比生成自由更重要。
4.2 信息污染:摘要里混进了不该存的东西
做了摘要压缩后,有一个特别隐蔽的坑:摘要并非纯客观的记录,它是模型"理解后的重述",所以模型的主观判断会被带进摘要里。如果模型在某一轮把用户的一句玩笑话理解成了真实需求,摘要里就会多出一条错误信息;这条错误信息之后会永远驻留在摘要区,反复污染模型对用户意图的判断。
对这种"污染进摘要"的问题,我的防范手段是用规则而不是模型来过滤摘要的候选内容。具体来说,我维护了一个黑白名单:黑名单包含"玩笑标记""拟撤回""误触"等词,一旦在对话中命中就直接不打进摘要候选池;白名单则包含"用户要求""必须""禁止""优先级"等强需求词,这些内容进摘要时会被标记为高置信信息。
另一方面,摘要的更新也不是无脑合入旧摘要和新内容。我会让摘要函数在合并时对每一条新信息标注可信度和时效性,低可信度的信息不合并进已有摘要,而是单独放在一个待确认区。如果后续对话确实印证了这条信息,再把它正式提升为摘要条目。这种做法相当于给记忆系统加了"缓冲期",能挡掉不少脏数据。
4.3 摘要丢失细节:用户要的信息在摘要里被"概括掉了"
摘要做得太"精炼",也是一种灾难。我有一次测试中,用户在第 2 轮提到了自己对某个功能的具体参数要求,例如"延迟要小于 200 毫秒"。这句信息被某次摘要压缩后变成了"用户对延迟有要求"。之后我把这个摘要喂给模型,模型在回答里完全把"200 毫秒"这个硬指标丢了,答出来的方案里是"尽量降低延迟"。
这个问题的根因是:摘要函数在压缩时,会用"语义重要性"来决定保留什么。而"精确数字"往往在语义上被模型视为可以舍弃的修饰语。要解决它,不能在摘要环节解决,要在摘要前就为关键信息做标注。
我的做法是:在把原始对话送入压缩函数前,先用一个简单的规则提取器把包含数字、型号、日期、价格、单位的信息单独抓出来,打上"锚点"标签,作为独立的字段附在摘要后面。比如:
摘要主体:用户希望新方案性价比高 锚点字段:延迟 < 200ms, 预算 5000 以内, 截止日 6月30日这样模型既能看到语义化的概述,又能看到不容损失的精确细节。实测这个改动之后,摘要记忆的可用率提升非常明显。
4.4 检索命中率低:相关问题召回太少
很多同学在 context-mode 里加入检索反馈后,会发现"检索命中"这个区块多数时候是空的,而当一个空区块占位时又白白浪费固定预算。问题基本出在召回策略和相关性筛选上。
我的经验是:不要把向量检索结果一股脑全塞进上下文。向量相似度只是语义相关性的一个粗筛,它经常把"主题相近但答案无用"的内容排在高位。要做的事有两件:
- 第一,设一个严格的相关性阈值。低于阈值的候选片段直接不进入上下文。这个阈值需要根据你的知识库内容粒度反复调,我在自己的项目里用 0.72~0.75 比较合适,高了召回太少,低了噪音太多。
- 第二,在向量检索之外,加一层基于关键词的硬过滤。如果用户的问题里含有了产品型号、订单号这样的确定性关键词,直接用关键词匹配命中对应的文档片段,优先于向量召回的片段。
另外,检索区块在上下文中的位置也很有讲究。我最终是把检索结果放在滑动历史和当前输入之间。这样做的好处是,检索内容不会压过当前指令的"起跑"位置,但在模型开始推理前刚好能被注意到。
4.5 性能开销:组装上下文本身成了瓶颈
在高并发的场景下,context-mode 的组装动作如果处理得不好,会成为一个延迟瓶颈。我发现很多实现里的性能问题都出在"重复计算"上:
- 每轮都对固定上下文重新做 tokenize,完全没有必要,固定内容是不变的
- 每轮都把整个摘要重新编码一次,其实摘要只在更新时才会变化
- 每次请求都重新从数据库读用户画像,其实用户画像是低频更新的
针对这些问题,我在项目里做了三层优化:
第一层,缓存恒定内容。固定上下文和用户画像的 token 数在会话开始时算好,存入内存缓存,整个会话期间除非显式更新,否则不再重复计算。第二层,增量更新摘要。只有新增对话进入摘要件时才触发一次合并摘要,而不是每次请求前都重跑摘要逻辑。第三层,异步预取。在模型还在处理上一轮请求的时候,后台线程已经开始组装下一轮可能需要的检索结果和摘要状态,等到真正请求发出时,上下文已经"热"了。
这三层优化做完后,我项目的端到端延迟从平均 1200ms 降到了 750ms 上下,上下文组装的耗时从不可忽视变成了近乎为零。
5. 扩展与改进方向:context-mode 还能往哪里长
5.1 从"手动配置"走向"自适应参数"
我现在实现的 context-mode 里有大量可调参数,比如各区块的优先级、Token 预算比例、压缩触发阈值。这些参数在静态场景下工作得很好,但面对动态变化的使用模式——比如某个时段用户特别活跃,一轮里包含大量长文本;另一个时段用户问题很短但轮次特别多——固定参数就会显得僵硬。
接下来一个值得做的方向,是给 context-mode 增加自适应参数调整能力。基本思路是:把每一次请求的 Token 消耗、压缩频率、检索命中率、模型回答质量记录下来,定期用一个优化模块根据历史表现调整区块比例。比如,如果系统发现某类用户问题几乎总需要检索知识库,那么就应该提高检索区块的预算上限,同时适当压缩历史区。
这个能力说白了就是把 context-mode 变成一个带反馈回路的最优化系统,从静态配置进化为动态策略。我实现了一个粗糙版本,核心是给每个区块维护一个"使用价值分",价值分高就自动涨预算,价值分低就自动降预算。实测下来,它在处理混合业务类型的 C 端产品上效果很好,适配成本大大降低。
5.2 多模态上下文的引入
我目前做的项目主要处理文本。但未来的上下文一定不止文本,图片、音频、视频特征和各种形式的用户行为数据都可能成为上下文的一部分。多模态上下文管理会带来全新的挑战:图片占用的 token 怎么计算?不同模态内容在上下文里的优先级怎么设定?摘要压缩时图像信息怎么"概括"?
我自己还没完全跑通多模态的 context-mode,但已经试过把图片的 OCR 结果和图片描述作为"降级文本"塞进上下文的方式,效果不错。这种方式的核心思路是:先用视语言模型把图片转成结构化文本,存入摘要区,而不是把原始图片直接塞进每一次请求。这样既保留了图片的信息价值,又避免了图片 token 消耗过大的问题。未来如果底层模型对多模态输入的成本降下来,这块还能继续演进。
5.3 和 Agent 记忆系统的融合
另一个我非常看好的方向,是把 context-mode 与 Agent 的长期记忆系统打通。现在我的 context-mode 主要用于单会话内或短期会话的记忆管理,它本质上还是"每一轮内如何选信息"。但 Agent 需要的是跨会话的长期记忆——昨天聊完今天打开新会话,模型还能记住关键的状态。
做这个融合时,我打算把 context-mode 里"摘要区块"的输出直接沉淀到外部向量库,作为某个用户/某个任务的长期记忆条目。下次新会话开始时,固定上下文里放上"该用户有 3 条长期记忆需要关注",检索区块里再加载相关的回忆内容。这样,context-mode 从"单会话优化器"升级成了"跨会话记忆基础设施"。
当然,这种融合也会引入新的工程复杂度,比如记忆的冲突检测、记忆的衰减机制、记忆的合并策略等,都还需要大量真实的场景测试来打磨。
6. 实操经验结语:context-mode 设计中的三个心法
写到这里,这个项目的核心内容基本上讲完了。回顾整个开发过程,如果要提炼三条真正让我少走弯路的心法,大概是这些。
心法一:先定预算,再定策略。关于上下文的一切讨论,都从一个前提开始"模型窗口就这么大,你打算怎么分?" 如果你连总预算都没算清楚就直接设计各种模式,最后一定会陷入"又想保留全部信息,又塞不下"的两难里。把预算写在代码最前面,当作全局常量对待,优先级和裁剪策略自然地就推出来了。
心法二:上下文不是越多越好,而是越贴合越好。我刚开始做的时候走了极端,认为上下文越多模型回答越精准。实测发现并非如此,大量无关内容反而会拉低回答质量。信息密度这个指标一定要盯住,每条上下文都应该能回答"它存在的必要性是什么"。
心法三:让压缩机制本身可观测、可回滚。摘要压缩是一个有损过程,一旦信息被压掉就再也回不来了。所以我在项目里做了两层保护:一是每次压缩前的原始上下文会以 JSON 形式落盘存一段时间,方便 debug 时回溯;二是压缩触发时,会输出一份"压缩日志",记录哪些内容被保留、哪些内容被精炼。这两张安全网让所有模糊的记忆问题都有据可查,而不是对着黑盒猜。
我在实际部署这个项目时,最先服务的两个场景分别是电商客服和文档问答。电商客服场景里,滑动窗口 + 摘要模式的组合让模型在连续 200 轮的对抗性对话中仍然保持了高忠实度;文档问答场景里,检索区块的引入让回答准确率从 61% 提高到了 83%。这些数字算不上惊艳,但对我自己的项目来说,已经足够说明一套好的 context-mode 设计能实实在在改变应用的上限。
如果你也正在做 AI 应用,遇到模型"失忆"、Token 成本失控、上下文组装混乱这些问题,我建议别一上来就上重型框架,先实现一个极简的 context-mode 调度器:三五个区块、一份预算、一个压缩函数,跑通一个最小闭环。再根据自己的业务特点,慢慢调整区块比例和触发阈值。这个渐进式的过程,比直接搬任何现成方案都要靠谱。
最后分享一个小建议:动手之前,给每一种模式画一张它们各自"管什么、不管什么"的边界清单。等你的应用上线后,你会发现大多数问题不是某个单一模式不好用,而是多个模式之间的交接处没有设计好。把边界画清楚,比把单个模式做得更完美,更能决定这个项目的上限。