@Michaelzsguo: https://x.com/Michaelzsguo/status/2056842405815447684
摘要
一份实用指南,介绍如何通过分层包装系统和一致的目录结构来组织本地LLM实验,以避免模型位置漂移、标志遗忘和测试框架耦合。
查看缓存全文
缓存时间: 2026/05/20 06:27
如何在运行多个本地LLM时保持条理
§0 — 太长不看版
你正在MLX上运行Gemma 4 31B,一切正常。然后你想把它和Qwen3 30B-A3B对比一下——同一任务,不同架构。与此同时,既然听说llama.cpp现在已经支持MTP,你想看看推测解码是否真的有帮助,那为什么不试试用llama.cpp跑GGUF格式的Gemma 4呢?你让它跑起来了。然后你想把Codex CLI接上去。这也成功了,于是你好奇在同一个模型上,Qwen Code和Pi哪个工具调用效果更好。也许两个都试试,并排跑。
三个模型。两个运行时。两种格式。三个测试框架。全部在一个下午搞定。
第二天早上:Gemma在哪个端口?那次感觉很快的MLX运行用了什么–prefill-step-size?Qwen3 GGUF是Q4_K_M还是Q6_K?Codex指向的是哪个代理?
什么都没有记下来。全在你的shell历史和脑子里——直到它们消失。
本文涵盖: 一条组织规则、五个层次、一个目录结构,让每个新模型只需五分钟添加,而不是三十分钟的考古。
两种阅读方式:
- 对本地LLM设置不熟悉 → 从§1到§4顺序阅读。§3提供了一个可以直接复制的目录结构。
- 已经运行着模型,想要系统 → §2(规则)→ §4(完整演练)→ §6(三个文件添加新模型)。
§1 — 三种失败模式
在讲解决方案之前,先准确命名债务。本地LLM实验在三种特定方式下失败。
模型位置漂移。 你的GGUF文件在一个地方,MLX权重在另一个地方,LM Studio的缓存在第三个地方。你隐隐知道这些,但没有写下来。六个月后,你运行ls,发现三个目录里有四个Qwen 27B的副本,而且你不确定哪个是最新的。
参数遗忘。 让模型表现良好的参数——–prefill-step-size 2048、–prompt-cache-bytes 50GB、–ctx-size 262144——存在于你的shell历史里。它们不在文件中。当你两周后启动那个模型时,你得从头开始。你能让它跑起来,但不会是那个曾经奏效的配置。
框架耦合。 Claude Code、Codex和Qwen Code各自有指向模型的方式。没有一个共享的模型映射,每个框架都重新发明自己的配置。添加一个新模型,你要更新三个地方。删除一个模型,你希望自己三个地方都记得。
具体版本:你一直在用Qwen 27B进行编码。两周后你想重新用它。哪个运行时——MLX还是GGUF?什么端口?什么参数?代理运行了吗?你可以恢复。但这需要二十分钟的考古。每次、每个模型都要花这二十分钟,直到你把这些信息放在某个持久的地方。
§2 — 规则:每个层次一个职责
组织原则是一个分层包装系统。五个层次,每个层次只有一个工作。
这给了我们几个好属性:
- 包装器是稳定的名字,你可以不用思考就输入。
- 后台细节是版本化的、文档化的、对Agent可读的。
- MLX、llama.cpp、Claude Code、Codex和Qwen Code可以共享相同的模型配置,而不是各自发明自己的映射。
- “服务模型”和“适配Agent到模型”是分开的层次。
- 添加新模型时,我们添加一个服务器配置,然后在上面加上薄薄的包装映射。
画个图来看数据流:
关键层是配置。那是运行时参数所在的地方——模型路径、端口、缓存大小、MLX调优参数。它上面的所有东西都是名字。它下面的所有东西都是基础设施。配置是你在正常操作中唯一编辑的层。
§3 — 目录布局
/Users/michaelguo/local-llm
├── docs/
│ ├── local-coding-model-stack.md
│ └── mlx-serving.md
├── models/
│ ├── gguf/
│ └── mlx/
└── runtime/
├── bin/
│ ├── mlx-serve
│ ├── llama-serve
│ ├── claude-local
│ ├── codex-local
│ ├── qwen-code-local
│ ├── qwen-code-openai-proxy.py
│ └── qwen-code-local-fetch.cjs
├── config/
│ ├── mlx-servers.zsh
│ ├── llama-servers.zsh
│ ├── claude-wrappers.zsh
│ ├── codex-wrappers.zsh
│ ├── qwen-code-wrappers.zsh
│ └── qwen-code-defaults.json
└── logs/
├── mlx-qwen27b.log
└── qwen-code-qwen-proxy.log
以及仓库之外的:
/Users/michaelguo/.local/bin/
├── qwen-code-qwen
├── qwen-code-qwen27b
├── qwen-code-qwen35b
├── codex-qwen
├── claude-qwen
└── LOCAL_LLM_WRAPPERS.md
这个布局中隐含的三个决定值得解释。
为什么用runtime/config/而不是内联在包装器里? 包装器是稳定的。qwen-code-qwen是一个三行的文件,永远不会改变。它最终调用的参数在每次调优新模型或更新运行时都会改变。将它们分开意味着稳定的东西保持稳定。
为什么models/不在runtime/里面? 模型权重是数据,不是配置。它们很大,版本化方式不同,并且在运行时之间共享——同样的GGUF权重可能由llama.cpp或LM Studio服务。将它们分开保持界限清晰。
为什么docs/是一级目录? 因为它是会话之间、你和任何你要求与系统交互的Agent之间的契约。更多内容在§7。
~/.local/bin/中的包装器故意放在仓库之外。它们是稳定的表面——你不用思考就输入的命令。local-llm/下的仓库是会演化的。
§4 — 端到端走一遍请求
输入qwen-code-qwen。下面是每一跳具体发生的事。
在更详细的演练中
/Users/michaelguo/.local/bin/qwen-code-qwen
-> /Users/michaelguo/local-llm/runtime/bin/qwen-code-local qwen
-> /Users/michaelguo/local-llm/runtime/config/qwen-code-wrappers.zsh
profile: qwen
kind: mlx
server_profile: qwen27b
model_id: default_model
proxy: 9211
context_window: 262144
-> /Users/michaelguo/local-llm/runtime/bin/mlx-serve start qwen27b
-> /Users/michaelguo/local-llm/runtime/config/mlx-servers.zsh
model path:
/Users/michaelguo/.lmstudio/models/Brooooooklyn/Qwen3.6-27B-UD-Q6_K_XL-mlx
server:
0.0.0.0:8080
MLX flags:
--max-tokens 8192
--prefill-step-size 2048
--prompt-cache-bytes 50GB
--pipeline
--prompt-concurrency 1
-> /Users/michaelguo/local-llm/runtime/bin/qwen-code-openai-proxy.py
proxy:
127.0.0.1:9211 -> 127.0.0.1:8080/v1
injects:
{"enable_thinking": false}
-> /opt/homebrew/bin/qwen
env:
OPENAI_BASE_URL=http://127.0.0.1:9211/v1
OPENAI_MODEL=default_model
QWEN_CODE_SYSTEM_DEFAULTS_PATH=/Users/michaelguo/local-llm/runtime/config/qwen-code-defaults.json
-> MLX model server
-> Qwen3.6 27B MLX model
§5 — 代理层:为什么它不是可选的
代理是最不直观的层。大多数读者会试图把它折叠到框架配置或服务器配置里。它作为一个独立的层存在,因为它做两件事,而相邻的层都无法干净地完成。
API转换。 Qwen Code期望一个OpenAI兼容的表面——/v1/chat/completions,标准的请求/响应模式。MLX服务器说自己的方言。代理桥接它们。没有它,你需要修补框架或修补服务器,耦合了两个本应互不知晓的层。
模型特定的注入。 {“enable_thinking”: false}是Qwen3特有的标志。它在API级别控制思维链行为。如果你把它放在框架配置里,框架与之通信的每个模型都会得到它——包括那些不适用这个标志的模型。如果你把它放在服务器配置里,服务器必须了解框架级别的语义。代理是正确的层:它知道正在服务哪个模型,并且它在框架的下游,所以框架保持干净。
总体原则:代理是模型特定怪癖的埋葬地。 代理之上的所有东西都看到一个干净的OpenAI兼容表面。代理之下的所有东西看到标准的服务器请求。代理吸收它们之间的阻抗不匹配。
§6 — 扩展系统:添加一个新模型
添加qwen-code-qwen35b——一个不同的Qwen3变体——只需要改动三个文件。
步骤1:在mlx-servers.zsh中添加一个服务器配置
qwen35b)
MODEL_PATH="$HOME/.lmstudio/models/Vendor/Qwen3.5-35B-MLX"
SERVER_PORT=8082
MLX_FLAGS="--max-tokens 8192 --prefill-step-size 2048 --prompt-cache-bytes 40GB --pipeline"
;;
步骤2:在qwen-code-wrappers.zsh中添加一个包装器配置
qwen35b)
KIND=mlx
SERVER_PROFILE=qwen35b
PROXY_PORT=9212
CONTEXT_WINDOW=131072
;;
步骤3:在~/.local/bin/qwen-code-qwen35b中创建shell别名
#!/bin/bash
exec "$LOCAL_LLM_DIR/runtime/bin/qwen-code-local" qwen35b "$@"
不需要改动代理脚本。不需要改动启动器。不需要改动框架配置。三个新的配置块插入到一个已经知道如何路由它们的现有结构中。
与无结构版本对比:从shell历史复制参数,编辑三个独立的框架配置文件,手动设置新的OPENAI_BASE_URL,希望自己记得哪个代理端口已被占用。
§7 — 文档层:为你和为Agent
docs/local-coding-model-stack.md有两个读者。
第一个是你,两周后,试图重新使用一个自上次不同项目以来没碰过的模型。文档告诉你什么在运行、什么端口、什么参数,以及为什么选择那些参数。
第二个读者是Agent。当你让Claude Code或Codex“启动我的编码模型”时,一篇写得很好的文档给Agent提供了所需的一切——读取配置、调用正确的启动器、验证服务器正在运行——而你不需要指定细节。文档设计成机器可读:结构化、具体、没有无意义的填充文字。
这是值得命名的转变。大多数项目中的文档是针对人的,是理想化的——事后编写,描述应该做的事。在这个系统中,docs/是一级操作层。它描述实际上做的事,用足够具体到自动化流程可以据此行动的术语。docs/中的契约是使系统可被Agent操作的关键。
§8 — 让实验可持续的纪律
系统不是为了组织而组织。
当添加一个新模型只需五分钟而不是三十分钟时,你会尝试更多模型。当切换运行时不破坏你的框架时,你会切换运行时。当一个会话有效的参数被写下来时,你就能精确复现那个会话。结构带来了速度——而不是拖慢它。
任何新增的测试都很简单:它是否只需要触及正确的层?一个新模型应该只添加一个服务器配置和一个包装器配置。一个新框架应该只添加一个包装器配置。一个新运行时应该只添加一个启动器和一个服务器配置模板。如果你发现自己为了添加新东西而编辑一个稳定的层,那么抽象错了——修复抽象,而不是修复文件。
每添加一个模型,应该是满足一次好奇心,而不是一次系统维护。
相似文章
@degenrsc: https://x.com/degenrsc/status/2064714047241736302
一份关于使用多 LLM 系统与持久内存构建研究代理框架的详细指南,通过基于文件的身份、项目文档和记忆索引,让研究人员无需在每次会话中重复解释上下文。
@tom_doerr: 本地LLM工具和硬件精选列表 https://github.com/0xSojalSec/LLMs-local…
一份精选列表,包含用于本地运行大语言模型的平台、工具、模型、硬件和资源,托管在GitHub上。
@bytebytego: 如何在本地运行LLMs
一份指南,说明如何在您自己的硬件上本地运行大型语言模型。
本地LLM推理优化:完整指南
一份关于在消费级硬件上优化本地LLM推理的全面指南,涵盖llama.cpp、vLLM和LM Studio等工具,并提供关于内存层次结构、层放置和常见故障模式的实用建议。
@DanKornas: 每个层都有自己的笔记本,从零开始构建LLM就更容易了。EveryonesLLM是一个基于Google Colab的教程…
EveryonesLLM是一个开源的基于Google Colab的教程仓库,用于从零开始构建nanoGPT风格的LLM,包含逐步章节,涵盖数据加载、嵌入、注意力机制、训练和指令调优。