我们如何为Junie Agent优化Qwen 3.6模型
不久前,我们启动了一个长期项目,旨在让用户能够在各种硬件配置上完全本地运行 Junie,并支持本地推理。在万众期待之后,我们最近发布了 Junie Local 的初始版本,该版本可在搭载 Qwen3.6-27B 的 MacBook M5 上运行。
在这篇博客文章中,我将分享实现这一目标所需付出的努力,以及我们为什么选择基于 Qwen3.6-27B 发布,而不是 Qwen3.8-27B。我们对整个技术栈进行了优化,从 Junie 智能体本身到我们选择的推理引擎。
让我们从 Junie 开始——毕竟,这是你开始与本地模型交互的地方。
Junie 优化
扩展智能体的滚动上下文
与任何其他编码智能体一样,Junie 有一个主执行循环,所有工作都在其中完成:
- 首先,用户指定任务。
- 然后,Junie 将其发送给 LLM。
- 接着,LLM 以一些工具调用作为响应(如 Bash 命令、读写文件的指令等)。
- 最后,Junie 将该命令的结果返回。
下面是一个极简化的流程图,展示底层发生的情况: [LOADING...]
从图中可以看出,LLM 持续接收扩展上下文的请求,因而能够部分复用其已经处理过的信息。具体来说,这意味着我们可以将上一个请求的预填充(prefill)数据复用到下一个请求,这些数据被称为 KV 缓存。
但是,当我们要求 Junie 在同一会话中执行第二个任务时,它只会从上下文中提取相关部分并将其放入窗口: [LOADING...]
对于云端模型,这通常表现完美,因为即使模型再次需要某些文件的内容,它也会重新请求访问并重新处理。而且预填充也非常快。
对于本地模型来说,情况并非如此——预填充没那么快,而且“读取”文件实际上需要相当长的时间。
为了解决这个问题,我们改变了本地推理的逻辑。现在,我们将每个新请求直接添加到滚动上下文中: [LOADING...]
这样一来,我们就可以复用之前任务的 KV 缓存,也就是说,如果模型已经读取过某个文件,该文件就会保留在上下文窗口中,我们无需再次“读取”它。
最大化初始可复用前缀
我们做的另一个类似优化与 Junie 在开始新编码会话时发送的系统提示和初始上下文有关。
在上一节中,流程有所简化:在第一次 LLM 请求时,实际发送给 LLM 的数据比图中显示的多得多: [LOADING...]
如你所见,发送给 LLM 的是一整套不同的信息。而且所有这些信息都会在每个新会话开始时被发送和处理。自然,我们希望能够以某种方式将其缓存 🙂
考虑到这一点,我们改变了发送这些数据的顺序: [LOADING...]
我们还在推理引擎中添加了特殊逻辑,用于缓存直到用户请求之前的前缀,因此在同一项目的后续任务中,可以直接复用。我们省去了用户请求之后的项目上下文,因为它非常小,且主要由顶层项目文件组成,这些文件可能会频繁变化。
让进度更新正常工作
对于更强大的云端模型,Junie 会要求 LLM 添加一个类 XML 格式的特殊块,其中包含将展示给用户的更新。不幸的是,Qwen 3.6 大多会忽略此类请求。与此同时,模型会将其操作以纯文本形式写入 LLM 请求结果中——也就是说,LLM 会发送一些工具调用,并附带一些解释这些调用的文本。
因此,解决办法很简单——直接用 Qwen 3.6 生成的这段文本作为给用户的更新。这种适配是模型特定的;也就是说,我们很幸运 Qwen 3.6 是这种表现——有些模型不会输出任何内容,而另一些则会生成过多文本。
移除不必要的 LLM 调用
下一个优化——禁用所有可选的 LLM 请求——看起来可能微不足道,但它确实让智能体更加高效。实际上,这意味着我们禁用了所有生成简短任务描述的逻辑。诚然,这样做牺牲了一些用户体验,但我们并不认为这种损失过大。此外,我们完全禁用了多智能体模式,因为在 M5 上处理 LLM 请求最有效的方式是顺序处理,所以启用多个智能体没有意义——它们无论如何都会受到推理瓶颈的限制。
模型参数优化
reasoning_effort: None
当我们在内部测试 Qwen3.6-27B 的云端版本时,我们注意到启用推理并不会带来显著的质量提升。因此,对于本地版本,我们决定完全禁用推理。这很重要,因为从推理引擎的角度来看,推理 token 与用于生成主要响应的 token 是相同的。因此,禁用推理后,我们需要生成的 token 数量减少 2-3 倍,这意味着任务执行速度提升 2 倍,而对质量的影响微不足道。
量化
我们决定使用 4 位版本,因为它在基准测试中的表现仅比 8 位版本稍差一点,而且由于生成过程受内存带宽限制,使用 4 位版本比 8 位版本快约 2 倍。然而,当我们比较 8 位和 4 位版本的预填充速度时,我们注意到它们是一样的……我们觉得这很奇怪,于是进行了更深入的研究。
推理引擎优化
预填充技巧
有些人可能会想,我们为什么还要关心预填充呢?毕竟,整个互联网上到处都是生成速度基准和优化选项。
嗯,在独立 GPU(如 RTX 5090)上,质疑其重要性或许是合理的。在这类硬件上,预填充确实非常快,因为预填充是计算密集型任务,而独立 GPU 通常相当强大。这意味着在默认配置下,你可以获得大约 3,700 token/秒的预填充速度。在开箱即用的 M5 上,速度大约在 650 token/秒左右。因此,当模型请求文件内容(即进行探查)时,大部分时间都花在了预填充上,而不是生成上!
更糟糕的是,4 位、8 位或 16 位量化在预填充速度上没有差异。但这是为什么呢?因为预填充是计算密集型任务,我们根本不受内存速度限制。事实证明,预填充期间的大部分矩阵运算都是以完整的 16 位模式执行的。也就是说,所有 4 位权重在执行运算前都被转换为 16 位数字。但 M5 处理器具有针对 8 位数字的特殊运算指令,其速度明显快于 16 位运算。因此,当我们对 MLX-VLM 包应用补丁,将预填充期间的部分^*^矩阵运算切换到 8 位后,预填充速度提升了约 40%!
顺便说一下,这也是我们决定专注于 M5 芯片的主要原因。M4 芯片没有这些 8 位算术指令,M4 的 16 位算术会使预填充速度慢 20-30%。
^*^ Qwen3.6-27B 同时使用全注意力层和自注意力层。我们发现,即使在 4 位量化下,全注意力层的权重仍以完整的 16 位精度存储。由于这些层无论何种情况都保持全精度,我们没有对它们应用此优化——该优化仅应用于自注意力层,在那里它确实减少了内存/计算量。以下是 MLX-VLM 中补丁的链接。此外,同样的优化也可以应用于 vLLM,只需编辑模型的配置文件即可。配置示例
通过 MTP 和 n-gram 匹配进行推测解码
作为标准优化,我们应用了以下方法:
- MTP(多 token 预测)与单独的草稿模型:一种推测解码方法,由较小的草稿模型提前提出多个 token,再由主模型进行验证。
- N-gram 推测解码:这种方法不使用草稿模型,而是在上下文中查找之前重复出现的 token 序列,并通过与这些序列匹配来“预测”接下来的 token。
我们同时启用了这两种方法。实际上,这意味着在生成过程中,我们有时不仅接受 MTP 提出的约 3 个 token,还会额外接受 n-gram 方法产生的多达 8 个 token。下图显示了原始生成的 token,并按产生每个被接受 token 的方法(草稿模型 vs. n-gram)进行了颜色编码。 [LOADING...]
两者结合,生成速度最高可提升 2 倍。
Qwen3.8-27b
既然这样,为什么我们不使用 3.8 而非 3.6 呢?
不幸的是,Qwen 3.8 需要启用推理模式才能正常运作。如果不启用,输出质量会显著下降——在典型任务上,甚至可能完全失败,陷入无限重复同一工具调用的循环。但启用推理模式会大幅增加生成的 token 数量:在中度推理强度下,大约会产生 5 倍的 token。由于预填充时间大致保持不变,实际变慢的程度接近 4 倍,而非完整的 5 倍。这仍然是一个巨大的代价——因此,就目前而言,在 Mac 硬件上,Qwen3.6-27b 仍然是更好的选择。
结束语
我希望在了解了我们的历程之后,你能明白,在典型的智能体编程任务中,只关注生成速度(token/秒)是错误的做法。
你需要优化技术栈的所有部分,包括:
- 生成与预填充:逐 token 解码过程和初始上下文处理阶段。
- 模型参数与量化:模型权重的精度和配置。
- 智能体框架:驱动模型的周边编排层(工具调用、控制流、提示逻辑)。
所以,这就是我们未来的计划。M5 支持只是第一步——我们已经有了 DGX Spark 和 RTX 5090 的原型(甚至还在关注 24 GB 显存的显卡),敬请期待!