我们如何针对 Junie 智能体优化 Qwen 3.6 模型
前段时间,我们启动了一个长期项目,旨在让用户能够在各种硬件设置上完全在本地运行已启用本地推理的 Junie。经过漫长的期待,我们最近发布了 Junie Local 的初始版本,该版本可以在 MacBook M5 上使用 Qwen3.6-27B 运行。
在这篇博文中,我将分享我们为实现这一目标所做的工作,以及我们选择基于 Qwen3.6-27B 而不是 Qwen3.8-27B 发布的原因。我们从 Junie 智能体本身到我们选择的推理引擎,对整个技术栈进行了优化。
我们从 Junie 开始 – 毕竟,您将从这里开始与本地模型进行交互。
Junie 优化
扩展智能体的滚动上下文
与任何其他编码智能体一样,Junie 也有一个执行全部工作的主执行循环:
- 首先,用户指定一项任务。
- 然后,Junie 将该任务发送给 LLM。
- 接着,LLM 通过一些工具调用作为响应(例如 Bash 命令、读写文件的指令等)。
- 最后,Junie 将此命令的结果发回给 LLM。
以下是揭示其底层运行逻辑的极简流程图:

如图所示,LLM 会持续收到扩展上下文的请求,因此可以部分重用之前已处理请求中的信息。更具体地说,这意味着我们可以在下一次请求中重用之前请求的预填充数据,这些数据称为 KV 缓存。
但当我们要求 Junie 在同一会话中执行第二项任务时,它只会从上下文中提取相关片段并放入窗口:

对于云端模型,这种方式通常没有任何问题,因为即使模型再次需要某些文件的内容,也会重新请求访问并再次处理这些文件。预填充的速度也非常快。
但本地模型并非如此 – 预填充的速度没有那么快,而“读取”文件实际上需要花费大量时间。
为了解决这个问题,我们更改了本地推理的逻辑。现在,我们会将每个新请求直接添加到滚动上下文中:

通过这种方式,我们便可重用之前任务的 KV 缓存。也就是说,如果模型已经读取过某个文件,该文件会保留在上下文窗口中,我们无需再次“读取”。
最大化初始可重用前缀
我们所做的另一项类似优化与 Junie 启动新编码会话时发送的系统提示和初始上下文有关。
在上一节中,流程进行了一定程度的简化:首次向 LLM 发出请求时,实际发送到 LLM 的数据远多于图中所示的数据量:

如您所见,系统会向 LLM 发送一套完全不同的信息。而且,所有这些信息都会在每个新会话启动时发送并处理。当然,我们更希望能够以某种方式缓存这些信息 🙂
基于这一考虑,我们更改了这些数据的发送顺序:

我们还为推理引擎添加了特殊逻辑,以缓存用户请求之前的所有前缀;这样,后续任务(同一项目中)便可直接重用该前缀。我们没有缓存用户请求之后的项目上下文,因为这部分内容很少,主要由上层项目文件组成,因此可能经常变化。
正常进行进度更新
使用性能更强的云端模型时,Junie 会要求 LLM 添加一个类似 XML 格式的特殊块,其中包含将向用户展示的更新内容。遗憾的是,Qwen 3.6 基本会忽略此类请求。与此同时,该模型会将其操作以纯文本形式写入 LLM 请求的结果中,也就是说,LLM 会发送一些工具调用,并附上解释这些调用的文本。
因此,修正方法很简单 – 只需将 Qwen 3.6 生成的这些文本作为更新显示给用户即可。此类调整是模型特定的,也就是说,Qwen 3.6 刚好具备这样的特性是我们的幸运 – 有些模型不会在这里输出任何内容,另一些则会生成过多文本。
移除不必要的 LLM 调用
下一项优化是禁用所有可选的 LLM 请求,这看似微不足道,但确实显著提高了智能体的效率。从实际层面来看,这意味着我们禁用了所有用于生成任务简短描述的逻辑。虽然这样做确实牺牲了一部分 UX,但我们认为这种损失并不大。此外,我们完全禁用了多智能体模式,因为在 M5 上处理 LLM 请求最高效的方式是顺序处理,因此启用多个智能体并无意义 – 无论如何,它们最终仍会受推理环节的性能瓶颈限制。
模型参数优化
reasoning_effort:无
在内部测试 Qwen3.6-27B 的云端版本时,我们注意到启用推理并不会显著提升质量。因此,我们决定在本地版本中完全禁用推理。这一点非常重要,因为从推理引擎的角度来看,推理 token 与生成主回答所用的 token 完全相同。因此,禁用推理后,需要生成的 token 数减少了 2 到 3 倍,从而使任务执行速度翻倍,且对质量几乎毫无影响。
量化
我们决定使用 4 位版本,是因为它在基准测试中的表现仅略逊于 8 位版本,同时,由于生成过程受内存瓶颈限制,4 位版本的速度约为 8 位版本的 2 倍。不过,在比较 8 位和 4 位版本的预填充速度时,我们发现两者完全相同… 我们觉得这很奇怪,于是进行了更深入的研究。
推理引擎优化
预填充技巧
有些人可能想知道,我们为什么要如此关注预填充。毕竟全网到处都是生成速度的基准测试和优化选项。
对于独立 GPU(如 RTX 5090),他们质疑预填充的重要性或许有道理。在这类硬件上,预填充速度确实非常快,因为它受计算能力限制,而独立 GPU 的计算能力通常非常强大。这意味着在使用默认配置时,预填充速度可以达到约 3,700 t/s。而在原始状态的 M5 上,这一数字约为 650 t/s。因此,当模型请求文件内容(即进行排查)时,大部分时间都花在预填充上,而不是生成上!
更糟糕的是,无论采用 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 的方法对这些 token 进行了颜色编码(草稿模型与 n-gram 对比)。

两种方法结合后,生成速度最多可以提升至原来的 2 倍。
Qwen3.8-27B
既然如此,我们为什么没有使用 3.8,而是使用了 3.6?
遗憾的是,Qwen 3.8 需要启用推理模式才能正确运行。如果不启用,输出质量会显著下降 – 对于典型任务,它甚至可能彻底失败,陷入无限重复同一个工具调用的循环。但启用推理模式会显著增加生成的 token 数:在中等推理强度下,生成的 token 数约为原来的 5 倍。由于预填充时间基本保持不变,整体速度下降接近 4 倍,而不是完整的 5 倍。这仍然是相当高的代价 – 因此,目前为止,在 Mac 硬件上,Qwen3.6-27B 仍是更好的选择。
结束语
我希望在了解我们的历程后,您能够意识到:对于典型的智能体编程任务,只关注生成 t/s 并不是正确的做法。
您需要优化技术栈的所有部分,包括:
- 生成和预填充:逐 token 解码过程和初始上下文处理阶段。
- 模型参数和量化:模型的权重精度和配置。
- 智能体框架:驱动模型的外围编排层(工具调用、控制流、提示逻辑)。
这就是我们未来计划开展的工作。支持 M5 只是第一步 – 我们已经为 DGX Spark 和 RTX 5090 制作了原型(我们甚至还在评估 24 GB 显存的显卡),敬请关注!
本博文英文原作者: