最近后台收到好几条同类私信:为什么拿 qwen3.5-9b 这类 9B 参数量级的小模型跑长对话,前几十轮还挺正常,后面越聊越像“失忆”?还有人在折腾多 Agent 任务编排时发现,A 智能体说过的话,B 智能体一概不认。这些问题表面看是模型能力,实际八成是上下文管理出了问题。如果你也在折腾 qwen3.5-9b,或者任何小参数模型的长文本应用,这篇上下文指南就是按我自己的实战踩坑记录整理的。它不会教你背模型卡参数,而是把上下文窗口、上下文工程、执行上下文这些经常被混着用的词拆开,再给一套能直接落地的分配、压缩、持久化操作方案。
1. 先搞清“上下文”到底在管什么
1.1 三个都被叫“上下文”,但完全不是一回事
聊这个项目的过程中,我发现最容易被卡住的不是技术,是概念。群里经常出现“上下文不够了”“上下文污染了”“执行上下文出错了”三种话,主语都是“上下文”,但指的根本不是同一样东西。
第一种,模型上下文窗口(Context Window)。这是最广为人知的含义,指模型一次推理最多能读多少 token。qwen3.5-9b 这类小参数模型的窗口值在模型卡上写得很明确,缺省范围通常从 8K 到 128K 不等。这个值本质上是硬约束,代表模型的“工作台面”有多大,台面上的东西太多,要么报错,要么把最早的记忆挤出桌面。
第二种,执行上下文(Execution Context)。这个词是从编程领域借来的,最典型的就是 JavaScript 里的执行上下文,指的是代码运行时的环境快照,包含变量、作用域链、this 指向这些东西。很多人搜“js 执行上下文”搜到这个标题,其实两者不是同一层级的术语。但有意思的是,在 Agent 开发里,执行上下文和模型上下文真的会相遇——工具调用返回的结果被塞进对话历史,那部分内容同时影响“程序状态”和“模型记忆”。我在后面讲 Agent 时会专门展开。
第三种,上下文工程(Context Engineering)。这是比提示词工程更“横”的一套方法论,核心是把喂给模型的上下文当成一种可设计、可编排的资源来处理——决定哪些信息该进入窗口、按什么顺序排、什么时候丢掉、什么时候压缩。它不是提示词技巧的变体,而是更接近“数据流设计”。
把这三种分清之后,再回头看网上关于“上下文影响”的吵架,其实大多数时候是概念错位。有人吹 1M 上下文全量可用,有人骂 9B 模型开长窗口就废,两边说的可能根本不是同一个维度的事。
1.2 9B 参数模型的窗口边界:不是开得越大越好
先上一组量级估算,这是我自己推算 KV Cache 显存占用时用的近似公式:每 token 的 KV Cache 大小约等于 2(K 和 V 两组)× 层数 × KV 头数 × 头维度 × 2(FP16 字节数)。按 qwen3.5-9b 常见配置来估,假设 32 层、8 个 KV 头、每个头 128 维,算下来每个 token 大约吃 128KB。如果开满 128K 窗口,单单 KV Cache 就要 16GB 左右,量化后能砍到一半,但这个量级对消费级显卡依然不轻松。
所以“上下文指南”的第一课其实是告诉你:窗口是稀缺资源,不是免费餐券。很多本地部署玩家把上下文从 32K 调到 128K,结果模型生成速度肉眼可见地变慢,偶尔还直接爆显存,这就是没算账的后果。更隐蔽的问题是,窗口变大之后模型的注意力密度被稀释,中段内容经常被“滑过去”,这就是学术界常说的迷失在中间问题。
实操中建议按这张表规划可用空间:
| 配置项 | token 数 | 说明 |
|---|---|---|
| 总上下文窗口 | 32768 | 调低一些,稳定优先 |
| 系统提示词 | 1500 | 固定角色、规则、输出格式 |
| 输出预留 | 4096 | 打满会直接截断,必须留 |
| 历史对话 | 24000 | 轮数越多,单轮可用的 token 越少 |
| 当前任务材料 | 3172 | 足够放当轮的问题和少量参考 |
关键点在于:输出 token 的预留经常被忽略。很多人设了 32K 窗口,却不知道模型生成答案也要占窗口,长回答打到一半就触顶,表现为“话没说完就停了”。我在 9B 模型上默认预留 4K 以上输出空间,宁可输入少一点,也别让回答腰斩。
2. 从提示词工程到上下文工程:不是升级是换层
2.1 Harness 层:提示词之上的“第四层”
有些热词里提到“提示词 上下文 harness 第四层”,这套说法我很认同。如果要给大模型应用分层,我会这样分:第一层是提示词工程,管的是“这轮对话怎么说”;第二层是模型本身,管的是“怎么根据输入做推理”;第三层是检索与工具,管的是“信息从哪来”;第四层就是 harness 层,管的是“整场对话的信息如何组装、排序、淘汰”。
这四层里,大多数教程只教第一层,但真正决定长对话质量的往往是第四层。打个比方,提示词工程是“把这张 PPT 写漂亮”,上下文工程是“决定这 30 张 PPT 里哪 5 张上桌、先后顺序是什么、哪张讲完就撤”。很多 9B 模型跑长对话变笨,不是模型不行,是 harness 层没做好——陈年旧事占着窗口,新任务的关键资料反而没位置。
我自己的习惯是把 harness 拆成三个子功能:组装(Assembly)、压缩(Compression)、刷新(Refreshing)。组装决定当前窗口里装什么,压缩决定装不下时哪些内容要瘦身,刷新决定哪些旧内容必须被清出。这三件事不是靠“多写几句提示词”能完成的,得靠代码逻辑和数据结构来做。
以 qwen3.5-9b 为底座做一个客服机器人,最简单的 harness 就是在每次生成前执行三步:把系统提示词固定在头部,从消息池里取最近的 N 轮对话,把超长工具结果替换成摘要字段。这三步用代码写出来不超过 50 行,但效果远胜于在提示词里反复说“请记住之前的内容”。
2.2 Token 预算是上下文分配的核心手段
既然窗口有限,就必须像做预算一样分配 token。我给团队定的默认比例是:系统提示词占 5% 到 8%,检索资料占 20% 到 30%,历史消息占 45% 到 55%,当前任务占 10% 到 15%,输出预留固定。这套比例不是拍脑袋想的,它保证了模型在任何时候都能看到“自己是谁”“手头有什么材料”“最近聊了什么”“现在要我干什么”。
真正拉开差距的是上下文学习示例的选择策略。9B 模型不是 GPT 级的大模型,few-shot 示例给多了浪费窗口,给少了学不到格式。我的经验是:只挑 2 到 4 个与当前任务最相似的示例,且优先放在提示词开头和结尾两段,因为模型的注意力对首尾更敏感。示例的相似度比数量重要得多,一个强相关的案例远胜于五个无关样例。
举个具体场景:要用 qwen3.5-9b 做日志异常分类。第一版提示词里我塞了 10 个不同场景的示例,分类效果一般。后来删到 3 个,只保留“网络超时”“权限拒绝”“内存溢出”三类最能代表常见误判的样例,准确率反而上来了。原因就是窗口变小之后,模型对示例模式的抓取更聚焦了。这件事值得反复强调:上下文工程的起点不是“装更多”,而是“选更准”。
3. 长上下文实战:从 128K 到 1M 的真相
3.1 三层信息存储:原始窗口、摘要层、事实层
窗口再大也不能无限堆对话原文。我见过很多人开了 1M 窗口之后干脆把整本聊天记录都塞进去,结果早期内容在生成时被严重稀释,用户问“我们三天前怎么说的”,模型答得牛头不对马嘴。
更稳的做法是把上下文拆成三层来管理:
第一层是原始窗口,只保留最近几轮和当前任务直接相关的片段。第二层是摘要层,由模型定期把更早的对话压缩成结构化摘要,这个摘要可以做成固定格式,比如“用户目标 / 已完成事项 / 未决问题 / 偏好约束”。第三层是事实层,也叫记忆锚点层,专门存那些不能丢的硬信息,比如用户的名字、项目截止日期、技术选型。
这三层里,事实层最容易被忽略,但它恰恰是最关键的。我在 Agent 对话里见过太多这样的情况:摘要里写着“已确认使用 Redis 做缓存”,但等上下文一压缩,模型又把技术方案说成 MySQL,就是因为事实层没有独立于摘要存在。
具体的落地方式是在系统提示词里预留两个固定区段。我常用的模板是:
【会话摘要】 (这里放上轮生成的摘要文本,保留最新 3 轮) 【事实记录】 - 项目:XX 系统重构 - 截止时间:2025-06-30 - 技术约束:必须兼容现有 Python 3.8 环境然后给模型一个明确的维护指令:每 5 轮对话结束后,把新出现的约定更新进【事实记录】,并把更早的对话压缩进【会话摘要】。这样即使窗口被截断,只要这两段还在,对话就能续上,而不是从零开始失忆。
3.2 1M 上下文是什么意思,什么时候才值得开
最近“1M 上下文”这个词很火,先是海外 Claude Code 把 1M 上下文做成正式功能,主打一次性把整个代码仓库塞进窗口;接着本地模型社区也开了类似玩法,接口层出现“请启用 1M 上下文后重试”之类的提示。那 1M 上下文到底是什么意思?简单说就是一次推理可以读约 100 万个 token,相当于几千页文档。
但你得清醒一点:1M 上下文是给“全文检索式问答”和“海量代码库理解”准备的,不是给普通聊天准备的。对 9B 参数的小模型来说,开 1M 窗口最大的问题还不是显存,而是注意力的“像素”会被摊薄到几乎看不见。模型确实能在长文本里找到片段,但对跨 300 页文档的因果关系推理往往很弱,甚至出现“看到了但没理解”的情况。
我见过最典型的翻车现场是:开发者在 API 配置里把上下文窗口调到 1M,结果模型每轮生成前要做超长 prefill,首 token 延迟从 0.8 秒变成 20 多秒,几乎没法交互。后来我把窗口调回 64K,再用 RAG 按需检索资料,速度和准确率都回来了。
所以我的建议是:先问业务场景,再决定窗口尺寸。如果任务只是“从 20 万 token 的资料里找一段原文并回答”,1M 有用;如果任务是“多轮对话 + 工具调用”,16K 到 64K 反而更稳。“1M 上下文已经全量可用”这类消息听听就好,对 9B 模型而言,窗口大不等于脑容量大。
3.3 Agent 场景中的上下文变量与工具结果回流
多 Agent 场景下,另一个容易翻车的是执行上下文管理。热词里提到的 Swarm 框架,核心概念是 Agent、Handoff 和上下文变量,这套理念值得借鉴:每个 Agent 在交接给下一个 Agent 时,不是把整段对话历史 dump 过去,而是只传递一个显式的上下文变量包。
用代码来表达,就是别这么干:
# 错误示范:把历史对话全部传给下一个 agent next_agent.run(messages=current_messages)而是这样:
# 正确示范:构造受控的上下文变量 handoff_context = { "user_intent": "查询订单状态", "collected_info": {"order_id": "A12345"}, "pending_action": "去查物流系统", "constraints": {"max_steps": 2} } next_agent.run(context_variables=handoff_context)这两者的差别就是“显式状态”和“隐式记忆”的差别。隐式记忆看起来很省事,但很容易污染——上一个 Agent 的碎碎念、中间失败的工具调用、无用的调试输出,全被下一个 Agent 当成了有效上下文。显式的上下文变量则强制你思考:这个 Agent 真正需要知道什么。
工具调用结果回流是另一个大坑。很多人的 Agent 调用一个天气 API 后,把一大段 JSON 原封不动塞回对话历史,几个工具一跑,窗口就被塞爆了。正确做法是在工具函数里就对输出做裁剪,只保留模型决策需要的字段。比如天气接口返回 2KB 数据,处理完变成一行摘要“上海,多云,26-32℃,东北风3级,适合户外”,再放回上下文。这一步省下的 token 非常可观,尤其是 9B 模型跑 Agent 任务时。
4. 上下文丢失与故障排查:现场实录和速查表
4.1 切换账号之后旧对话加载不出来,怎么办
这个坑的热度比想象中高。不少人在使用各类客户端工具时,会在多个账号之间切换使用,然后就遇到一个经典问题:切回原来的账号之后,之前对话的上下文不能加载。我一开始也以为是模型支持的问题,后来发现完全不是,是会话管理的问题。
拆开看,原因基本逃不出三类:第一,会话 ID 变了,客户端换了新会话,自然找不到旧消息;第二,历史消息只存在本地内存,没有持久化,切换过程把内存清了;第三,远端服务端把长期未活跃的会话标记为过期,虽然聊天记录还在列表里,但重建上下文时拿不到完整消息流。
解决方案要分两步走。第一步是预防:切换账号之前,先把关键对话导出或者复制成文本,至少留个备份。第二步是恢复:如果切换后回不到原会话,就新建一个会话,把之前导出的摘要和关键事实注入回去。如果你跑的是本地模型,最简单粗暴但有效的方法是给每次对话建一个“会话存档文件”,里面存会话 ID、摘要、事实记录。下次无论怎么切,只要把这个存档作为初始上下文灌进去,对话就能无缝续上。
这个思路看起来笨,其实恰恰是上下文工程的本质:不要依赖客户端帮你记,要让信息本身具备可移植性。我自己在本地跑 9B 模型时,所有长对话都会定期把摘要和事实写进一个 JSON 文件,就当是游戏的“存档点”。
4.2 常见问题排查速查表
我把自己踩过的和帮别人排查过的典型问题整理成一张表,适合直接截图存着:
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 对话越聊越“健忘”,早期约定记不住 | 窗口塞满后未压缩摘要 | 启用摘要层,每 N 轮强制生成固定格式摘要 |
| 回答到一半突然中断 | 输出 token 预留不足 | 把 max_new_tokens 调大,别让上下文把输出空间挤没 |
| 报“请启用 1M 上下文后重试” | 当前配置窗口小于任务需求 | 确认任务是否真的需要长窗口,必要时升级配置 |
| 开了 1M 窗口但首字延迟极高 | KV Cache 算不过来了 | 回退到 64K 以下,改用 RAG 按需检索 |
| 多个 Agent 之间消息串味 | 直接拼接对话历史传递状态 | 改用显式上下文变量 + handoff 传参 |
| 工具调用后对话质量明显变差 | 工具原始 JSON 塞回窗口占用大量 token | 工具输出裁剪成摘要字段后再回流 |
| 切换账号后旧对话无法加载 | 会话 ID 变化或未持久化 | 导出历史,用摘要快照手动重建上下文 |
| 示例给多了效果反而更差 | few-shot 示例相互干扰 | 精选 2 到 4 个高相似示例,放首尾 |
表格背后的原则其实就一条:上下文是工程资源,不是聊天记录垃圾桶。任何失效问题都能通过“更主动的管理”解决,而不是靠模型硬扛。
4.3 最后一道防线:给上下文做“强制快照”
有朋友问过我:如果模型窗口已经炸了,摘要也没来得及生成,还有没有救?我的答案是:有,但前提是你在对话里埋了“记忆锚点”。
具体操作是:在系统提示词里加一条硬性规则——每当对话中出现关键约定、用户偏好、任务参数变更时,模型必须把它们以固定格式写入一个标记区,比如:
[STATE_UPDATE] {"confirmed_tech": "Redis", "deadline": "2025-06-30", "user_refuses": ["短信通知"]}同时,每隔几轮就要求模型输出一次完整的状态快照。这样就算窗口后来被完全清空,只要你有最近一次的 [STATE_UPDATE],就能把上下文“救活”。这不是提示词小聪明,而是把生成过程变成了可恢复的状态机。
我在一个 48 轮的超长任务里试过这个方案,中途人为清空了大部分历史,只留了系统提示词和最近一张快照,模型仍然能准确说出用户的约束条件和未完成事项。从那以后,我给所有长对话项目都默认加上了快照规则。
5. 最后说点个人体会
折腾完这一圈,我对 qwen3.5-9b 这类小参数模型最大的体会是:上下文管理是一种克制,不是堆砌。很多人默认“窗口越大越聪明”,但实际体验下来,9B 模型最舒服的状态往往是 16K 到 64K 窗口配合强力的摘要和记忆锚点,而不是硬开 1M 制造“伪记忆”。我自己的习惯是,把上下文想象成一张桌游桌面——牌在不打的时候就得收进牌堆,桌面永远只留当前回合用得上的几张,该出的牌随手能摸到,这场对话就不会崩。希望这篇指南能帮你在折腾上下文时少踩几个我踩过的坑,有新的实战经验也欢迎回来交流。