两台机器跑本地 Agent 这件事,我原本最担心的是显存不够、推理太慢,真正跑起来之后才发现,用户嘴里常说的"长上下文"和 Agent 实际用到的"长上下文",完全不是一回事。标题里提到的那款开源 MoE 模型,我拆了两台闲置服务器来托管它,前后折腾了小两周,最后结论是:长上下文能不能用,不取决于它接不接受 128K token,而取决于你塞进 128K 里的东西长什么样。
这篇文章不聊榜单指标,只讲我在真实 Agent 场景里的踩坑、调参和修复过程。如果你也想用便宜双机跑一个能调工具、能读多文档、能连续干活的本地 Agent,这篇可以直接当参考。
1. 先更正一个常见误区:Agent 场景说的“长上下文”,和模型评测里的“长上下文”不是一回事
很多评测指标都很好看,喂一个 80K 的文档进去,然后问"第三段提到了哪个日期",这类任务只要序言位置没丢,基本都能答出来。但 Agent 不是这样用的。Agent 的上下文是动态滚动的:文档片段、工具返回值、上一次调用的报错、用户追问、中间推理过程,全都混在一个超长 prompt 里,而且越往后越密集。
1.1 为什么通用长文档任务和真实长任务不一样
我这次用的某开源 MoE 模型,官方给的窗口上限很大,本地部署时我也把推理服务配到了接近上限。但真正跑 Agent 时,有效上下文会迅速缩水。原因很简单:通用长文档评测往往是"事实抽取",信息可以分散存储;Agent 任务需要的是多跳的关联追踪——先读文档 A 得出一个结论,再带这个结论去调工具 B,然后把返回值 C 和最初的用户目标做对比。这个链条上的每一步都在产出新 token,而每一步新增的 token 都会稀释掉前面信息的注意力权重。
还有一个隐蔽的问题:通用长文档测试通常只让模型做"回答",Agent 却要求模型做"决策"。决策比回答脆弱得多。模型在长上下文中即便还记得正确的信息,也可能低估它,转而去调用一个看起来更新、权重更高的错误工具。所以我说,评测里的长上下文只证明"能读进去",不代表"能接着用"。
1.2 双机方案里谁是推理机、谁是调度机
标题里的"两台",我的理解不能只是两台机器跑同一个模型,那是浪费。我的分工是这样的:一台专门跑推理,负责加载模型权重、维护 KV cache、处理每次请求;另一台跑 Agent 主循环,负责意图识别、RAG 检索、工具调度、上下文裁剪和状态记忆。推理机只管"算",调度机只管"想"。
这么做的好处是,Agent 主循环非常吃并发和 I/O,如果和推理进程挤在一台机器上,一个工具请求就能把 KV cache 的预填充和 decode 全部拖住。分开之后,我在调度机上随便加日志、改提示词、重启 Python 进程,都不会影响推理机那边已经加载好的模型权重。更重要的是,长上下文场景里 KV cache 占用动不动就十几 GB,推理机需要把大部分内存留给 KV,不能让 Agent 主循环来争抢。
2. 双机部署参数与启动顺序:三个最容易翻车的配置点
部署本身不复杂,但有几个参数直接决定长上下文能不能稳定跑,我踩过的坑都在这几个地方。
2.1 角色分工和资源的最低要求
先说我最后稳定使用的环境配置:推理机内存尽量大,且内存通道带宽要够,因为 MoE 模型权重拆得比较开,首 token 延迟和内存带宽的关系比和显存容量的关系更大。KV cache 的压缩策略一定要开启,不然序列一长,显存和内存会被 cache 吃干。
调度机的需求相对低,但它承担了 RAG 的向量检索、文档切分、工具返回结果的临时存储,如果 Agent 同时挂着多个任务,调度机内存也不能太小。我把两边的职责和参数整理成了下面这张表:
| 机器角色 | 主要任务 | 关键资源 | 最容易忽略的配置 |
|---|---|---|---|
| 推理机 | 加载权重、KV cache、token 生成 | 内存总量、内存带宽、CPU核心数 | KV cache 压缩、最大序列长度、swap 空间 |
| 调度机 | Agent 循环、RAG、工具调用、状态管理 | 内存、磁盘IO、网络带宽 | 上下文裁剪逻辑、并发请求数限制 |
2.2 权重 offload 与 KV offload 要分开看
很多人一提到大模型本地部署就以为必须一次性把全部权重塞进显存。实际上 MoE 模型因为只有一个 top-k 路由,激活参数往往只占总量很小一部分,整个权重可以放在内存,再用高性能引擎做 CPU 与加速卡的协同推理。推理引擎启动时有一类参数是控制权重的,另一类是控制 KV cache 的,这两者要分开调整。
如果权重放内存,KV cache 也放内存,那就得先估算序列长度。粗略公式是:KV cache 大小 ≈ 层数 × 注意力头数 × 单头维度 × 序列长度 × 精度字节数。序列从 8K 涨到 80K,KV 开销几乎线性翻十倍。这也是我会在推理机上开启 KV cache 压缩的核心理由:它能明显压低长序列带来内存膨胀。
2.3 前缀缓存和输出长度限制:Agent 场景下的两个隐形大坑
第一个坑是前缀缓存。推理引擎在连续请求之间如果前缀没变,可以复用之前的计算结果,这对 Agent 场景几乎是雪中送炭,因为每次 Agent 循环都会在 prompt 开头放一大段固定工具定义。开启前缀缓存后,固定部分不用重复计算,首 token 延迟能降一个量级。一开始我没开,长上下文一上来,每轮工具调用都要把全套工具说明重新预填充一遍,慢得我想把服务器砸了。
第二个坑更隐蔽,就是 max output token 限制。Agent 的工具调用往往伴随很长的 JSON 输出,如果输出上限设置得不够高,模型会在工具调用中间被强制截断,产生一个不完整的 JSON。这个不完整 JSON 被调度机解析失败后,Agent 会自动重试,重试又把前面的输出重新塞回上下文,上下文越来越长,输出上限还是那个数值,最后进入死循环。建议输出上限至少留出工具调用 JSON 的两倍余量,宁可多不可少。
3. 把上下文塞到 45K token 之后,Agent 的"有效期"集中在哪个位置
部署稳定后,我做了几组针对性的长上下文压测。最核心的一组,是用真实工作流把一个 Agent 任务的上下文慢慢推到 45K 左右,然后观察它在不同位置的表现。
3.1 我是怎么构造一个真实的长上下文工作负载的
我没有用现成的长文本数据集,而是模拟了实际工作场景:把三份业务文档作为初始材料,交叉检索其中的信息,再通过五个连续工具调用完成一个汇总类任务。初始文档大概占 20K token,工具定义占 4K,前几轮对话和返回结果堆到 15K,最后的用户提问挤到了 45K 附近。这样构造的好处是上下文的"中间段"和"尾部"都是真实语义节点,而不是人为填进去的填充文本。
按这个构造方式,我在同一台双机环境里反复跑了很多轮。为了对比,我刻意关闭了上下文裁剪能力,让 Agent 原样携带所有历史信息。结果非常直观:当用户提问被推得很靠后时,模型对前段文档细节的定位明显变弱,但对系统中预设的头几条工具定义还能保持稳定。这说明模型在实际长窗口中的注意力往往更倾向于头部与尾部,中段信息最容易"失忆"。
3.2 长上下文压测里出现的三种典型退化模式
三种退化模式反复出现,我整理如下:
第一,中间参数串位。模型不是完全忘记中段的某条信息,而是把它和另一条相似信息混淆,导致工具参数错乱。比如检索工具返回了两条相似订单,模型会张冠李戴,把第一单的金额填到第二单的结果里。
第二,调用链断裂。任务需要先查 A,再根据 A 的结果决定调 B 还是调 C。当上下文超过一定长度后,模型会跳过中间决策,直接调用最终工具,省略了必要的条件判断,结果自然不可用。
第三,重复调用。模型反复调用同一个只读工具,入参相同,却每次都期望得到不同的结果。排查日志后我意识到,这是上下文太长导致模型不再把之前的返回值当作既成事实,而是丧失了对"已获取信息"的信任。
3.3 45K 不是悬崖,而是渐进式下滑
如果期待上下文长度到了某个值就突然崩溃,那我要说一句:没有悬崖,只有渐进的、缓慢的下滑。但从我的实测曲线看,30K 以内任务成功率还保持在正常范围内;超过 45K 后,中段信息引用准确率明显下降,工具调用成功率从接近 90% 掉到六成左右。而且长上下文下生成速度也在变慢,因为每轮 decode 都要处理更长的历史。
所以我的结论是:长上下文本身是能用的,但"能用的长上下文"是有限的。与其让 Agent 背着一整本流水账跑到 40K,不如在工程上做减法,让模型始终在一个健康的上下文区间里工作。
4. 一次非常有代表性的失败:模型在中段把参数读串了
前面说的退化模式太抽象,这里讲一个让我调了一个晚上的具体故障。
4.1 现场还原
我有一个任务查询场景,文档里有两条记录,一条是"当前状态为待审批",另一条是"备注中提到了某位负责人的旧标识"。Agent 的初始提示词里只要求它查询状态,但当上下文已经堆到接近 35K 时,模型开始画蛇添足,把备注里的旧标识当成查询条件去调用查询工具,然后带着这个错误的上下文继续到下一步,最后生成了一段完全基于串位数据的总结。
最让人迷惑的是,这个错误不是每次必现的,偶尔增加一次中间工具的调用日志,错误就消失了,再加长一点,又回来了。这说明问题不在提示词本身,而在上下文长度导致的注意力分布变化——中段信息的权重被后续内容压下去了。
4.2 完整的排查链路
我先检查了调度机的工具调用日志,找到模型传给查询工具的参数,发现明显张冠李戴。我当时没急着修 prompt,而是回看了推理服务的实际请求记录,把当时发送给模型的 prompt 完整恢复了一遍。
恢复以后发现,模型收到的是一个包含大量对话历史、RAG 片段、工具定义和上一个工具返回值的超长 prompt,而真正关键的字段信息被夹在中间。更麻烦的是,工具定义因为每次都重复拼接,占用了大量位置,而系统提示词又要求"优先参考最新信息",模型在这种压力下选择了一个看似最新却错误的参数组合。
我最初的修复方向是改提示词,试图让模型"更仔细地阅读中段文档",效果很糟糕。因为模型不是"没看到",而是"看到了但权重不够"。到了这一步我才明确,必须从上下文组织方式下手。
4.3 我最终采用的三个工程手段
第一个手段是固定工具定义区域。把工具定义放在 prompt 最前面,长度控制在 2K 以内,并且开启前缀缓存,让固定区域每次都命中缓存,不再挤占 KV cache 预算。
第二个手段是把中段文档改造成"先摘要再留存"。我让调度机在每轮 Agent 循环后自动生成短摘要,旧文档不再以原始全文形式进入下一轮。长度一下被控住了,模型可以始终在一个稳定的上下文区间里工作。
第三个手段是引入分层记忆。核心状态和最近几轮对话保留完整,历史细节只保留摘要和引用链接,模型需要细节时再去检索。这个方式特别像人处理工作时的做法,不重要的背景不反复摆在桌面上,需要时再翻出来。
修完这三处之后,同一个任务从 35K 长上下文的"偶发抽风"恢复到接近满分,时间长也不退化。
5. 双机资源利用率实况:内存压力与速度瓶颈
双机方案里,真正的资源瓶颈往往不是模型计算能力,而是内存和显存之间的数据搬运。我实测下来最明显的一个现象是:只要序列长度一涨,内存带宽立刻成为天花板。
5.1 内存通道决定首 token 延迟
首 token 延迟是 Agent 感知最明显的指标,因为每一轮工具调用都在等待新 token 返回。实际运行时,大部分首 token 延迟花在了把权重和上下文 key/value 读取到位。换句话说,这不是"算力"问题,而是"喂给算力的数据够不够快"的问题。
如果机器用的是普通内存条,带宽有限,长上下文的预填充速度会非常难看。解决办法是优先把 KV cache 的冷热分离:活跃的短期记忆放在最快的存储层级,历史摘要和旧文档放在大容量慢速存储,只在需要时唤醒。这样既保住了有效上下文,又不会让长文本把整条内存通道打满。
5.2 KV cache 压缩是长上下文第一保命技
我在部署时特意确认了推理框架是否启用 KV cache 压缩。没启用前,长上下文的内存占用增长非常快,一旦爬升到接近物理内存上限,系统就开始写 swap,整个推理速度直接崩溃。启用之后,内存占用明显下降,序列长度也敢往上推了。虽然压缩会带来一点精度损耗,但对 Agent 场景的收益远大于损失。
另外要注意 batch 大小。Agent 场景大多是单请求逐个推理,batch 基本为 1,这时特别要小心不要把推理框架的并发设置调太高。之前我为了追求吞吐,开了比较大的并发,结果多个长上下文请求同时触发预填充,内存瞬间被打爆,调度机那边的请求全部超时。后来我把并发限制调低,优先级改成先到先得,长上下文任务才稳定下来。
5.3 值得保留的一条底线
用双机方案跑本地 Agent,无论怎么调优,都应该给自己划一道上下文预算线。我的经验是:在模型宣称长上下文能力较强的场景,实际 Agent 工作建议控制在总窗口的三分之二以内,预留一部分给模型输出和紧急检索结果。超长边缘作战可以偶尔为之,不适合作为日常默认状态。
6. 长上下文在本地 Agent 上"真正可用"的边界条件
折腾到最后,我对"长上下文到底能不能用"这个问题的回答,已经是了一组边界条件,而不是一句简单的"能"或"不能"。
6.1 三类场景的可用性判断
第一类,纯长文档问答场景。模型只在开头接收文档、结束时回答问题,这种场景只要权重加载完整,长上下文基本没问题,属于最容易命中的场景。
第二类,多轮工具调用场景。上下文会不断追加工具结果和中间日志,这种情况的长上下文很快会失效,必须配合前缀缓存、上下文裁剪和摘要记忆,才有稳定的可用性。
第三类,跨多文档的多跳决策场景。这是最难的,上下文里存在多个真实语义实体,需要模型反复对比和引用。这类场景我实测基本无法直接通过"拉长窗口"解决,必须让调度机提前把需要的证据片段检索出来,压缩后放在一个紧凑的上下文里,才能保持准确率。
6.2 我的最终建议:随时准备一个小助手
我会在调度机上常驻一个子 Agent,专门负责"上下文管家":每次主 Agent 结束一轮调用后,管家自动判断哪些内容已经没用,把过期的中间日志压成一行摘要,把已经成功的工具调用历史归档,再决定哪些 RAG 片段可以留在下一轮里。这个设计看着多了一层调度开销,实际上让主 Agent 的长上下文有效长度直接翻倍。
如果你也想复现这套双机方案,我给的最小配置建议是:推理机内存至少能承受两倍模型权重,并且一定要开启 KV cache 压缩;调度机上保留一个独立的 RAG 存储,不要让 Agent 把所有检索结果都塞进 prompt。配置的顺序也很重要:先开前缀缓存,再调内存通道,最后再考虑要不要拉长窗口。
我在实测中最意外的一点是:真正让长上下文不堪重负的,往往不是用户的问题太长,而是 Agent 自己一轮轮产生的新 token。所以不要只关注模型的上下文窗口数字,更要关注 Agent 循环里每一轮悄悄丢进去的日志、返回值和中间结果。控制住这些,本地 Agent 才算真正从"能跑"变成了"能干活"。