使用 llama.cpp、Claude Code 和 Codex 搭建本地 Agent 编码环境
我手头有一张 24GB 显存的 RTX 4090。平时写代码主要用 Codex,一些简单任务则交给本地模型,能少占一点订阅额度。
本文按实际操作顺序记录环境准备、模型启动和客户端接入。Qwen3.6-27B 已经能通过 llama-server 稳定运行,普通对话和编码工具调用都测试通过。
CUDA 环境准备
先安装添加 NVIDIA 软件源需要的工具:
sudo apt install dirmngr ca-certificates software-properties-common apt-transport-https curl -y添加 Debian 12 对应的 NVIDIA GPG 公钥:
# 下载 NVIDIA GPG 公钥
curl -fSsL https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/3bf863cc.pub \
| sudo gpg --dearmor \
| sudo tee /usr/share/keyrings/nvidia-drivers.gpg > /dev/null这台机器的旧驱动有问题,需要清理后重装。下面的命令会卸载已有的 NVIDIA 和 CUDA 软件包,驱动正常的机器不要执行:
# 清理旧环境
sudo apt-get purge -y '^nvidia-.*'
sudo apt-get purge -y '^libnvidia-.*'
sudo apt-get purge -y '^cuda-.*'
sudo apt autoremove --purge -y
apt search nvidia-driver
sudo apt install --reinstall -y nvidia-driver nvidia-driver-cuda
# 重启后确认驱动
nvidia-smi重启后运行 nvidia-smi,确认系统已经识别 RTX 4090:
Tue Jul 21 17:35:09 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 610.43.02 KMD Version: 610.43.02 CUDA UMD Version: 13.3 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 4090 On | 00000000:01:00.0 Off | Off |
| 30% 34C P8 12W / 450W | 1MiB / 24564MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+配置显卡容器支持
这台机器还需要通过 Docker 使用显卡,因此安装 NVIDIA Container Toolkit:
sudo apt install nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker如果只准备直接运行 llama.cpp,不使用 Docker,这一步可以跳过。
安装 CUDA 开发工具包
apt search cuda-toolkit
sudo apt install cuda-toolkit-13-3
# 将以下内容添加到 ~/.bashrc
echo 'export CUDA_HOME=/usr/local/cuda' >> ~/.bashrc
echo 'export PATH=$CUDA_HOME/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc这里安装的是实际使用的 cuda-toolkit-13-3。环境变量生效后,可以用 nvcc --version 再确认一次。
编译 llama.cpp
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
# 编译前清理旧的构建目录
rm -fr build
# CUDA 构建
cmake -B build -DGGML_CUDA=ON -DGGML_RPC=ON
# 纯 CPU 构建,二选一
# cmake -B build -DGGML_CUDA=OFF -DGGML_RPC=OFF
cmake --build build --config Release -j 32
./build/bin/llama-server --version这里同时启用了 CUDA 和 RPC。最后一条命令能打印 llama-server 版本,就说明编译成功了。
下载模型文件
本地模型使用 GGUF 格式。我从 ModelScope 下载了已经跑通过的 Qwen3.6-27B。
Qwen3.6-27B:已跑通
modelscope download --model unsloth/Qwen3.6-27B-GGUF --include "*Q4_K_M*" "*mmproj-F16*"这条命令只下载 Q4_K_M 量化模型和 F16 视觉投影文件,避免把仓库里的其他量化版本全部拉下来。
启动 Qwen3.6-27B
先设置本次服务使用的 API key。实际值只放在当前终端或私有配置中,不写进文章:
# 先设置实际的 llama.cpp API key,不要把 key 写进脚本或文章
export LLAMA_API_KEY='在这里填入你的实际 key'
./build/bin/llama-server --metrics --alias Qwen3.6-27B \
--model /vol1/1000/models/modelscope/models/unsloth/Qwen3.6-27B-GGUF/Qwen3.6-27B-Q4_K_M.gguf \
--mmproj /vol1/1000/models/modelscope/models/unsloth/Qwen3.6-27B-GGUF/mmproj-F16.gguf --reasoning-preserve \
--host 0.0.0.0 --port 30104 --ctx-size 262144 -ngl 100 \
--cache-type-k q4_0 --cache-type-v q4_0 --parallel 1 \
--batch-size 4096 --ubatch-size 1024 --flash-attn on --mlock --no-mmap --threads $(nproc)下面这些参数针对这台 RTX 4090 机器调过,不适合原样套到所有环境:
--ctx-size 262144将上下文设为 262K。-ngl 100尽量把模型层放到 GPU。--cache-type-k q4_0 --cache-type-v q4_0压缩 KV Cache,降低长上下文的显存占用。--parallel 1只保留一个并行槽位。--metrics开启指标接口,便于观察运行状态。--batch-size 4096设置逻辑批次上限。对llama-server来说,它限制连续批处理时每轮最多送入多少个 token;调大后可能加快长提示词的处理,也会增加内存或显存压力。--ubatch-size 1024设置一次实际计算的物理批次上限。batch-size可以再拆成多个ubatch执行,因此这里保持4096 >= 1024。--flash-attn on强制启用 Flash Attention,减少注意力计算的中间数据读写,通常能降低显存占用并提高速度。--reasoning-preserve在聊天模板中保留模型的推理链输出。Qwen3.6 是带推理能力的模型,GGUF 内置的聊天模板支持此功能;不加这个参数时,Claude Code 只能看到最终结果。保留推理内容后,排查编码任务时更容易看出模型卡在哪一步。--mlock尝试把模型占用的内存锁在 RAM 中,避免被系统换出到 swap。进程需要足够的锁页权限,物理内存也必须留有余量。--no-mmap不使用内存映射加载模型,而是直接读入内存。启动通常更慢、常驻内存更明确;这里与--mlock一起使用,目的是减少运行中发生换页的可能。--threads $(nproc)将生成阶段的 CPU 线程数设为当前系统可见的逻辑处理器数量。模型的大部分层已经交给 GPU,但剩余 CPU 计算、采样和调度仍会使用这些线程。
模型稳定启动后再配置 Claude Code。第一次报错时不要同时改上下文、KV Cache、批大小和线程数,否则很难判断究竟是哪项参数起了作用。
纯文本编码场景
如果只通过 Claude Code 处理文本和代码,不需要向模型发送图片,可以不加载视觉投影文件。下面的命令使用 --no-mmproj 明确关闭多模态支持,同时保留与前面相同的上下文和 KV Cache 配置:
./build/bin/llama-server --metrics --alias Qwen3.6-27B \
--model /vol1/1000/models/modelscope/models/unsloth/Qwen3.6-27B-GGUF/Qwen3.6-27B-Q4_K_M.gguf \
--no-mmproj --reasoning-preserve \
--host 0.0.0.0 --port 30104 --ctx-size 262144 -ngl 100 --fit off \
--cache-type-k q4_0 --cache-type-v q4_0 --parallel 1 \
--batch-size 4096 --ubatch-size 1024 --flash-attn on --mlock --no-mmap --threads $(nproc)这里保留显式的 -ngl 100,因此同时使用 --fit off 关闭显存参数自动适配,避免启动时出现自动适配无法修改 GPU 层数的警告。不加载 mmproj 后,视觉输入不可用,也不会再出现针对 Qwen-VL 图像 token 数量的提醒。
Claude Code 配置
Claude Code 的用户级配置文件是 ~/.claude/settings.json。llama-server 使用的模型别名是 Qwen3.6-27B,这里也沿用同一个名称:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"ANTHROPIC_AUTH_TOKEN": "与 LLAMA_API_KEY 相同",
"ANTHROPIC_BASE_URL": "http://localhost:30104",
"ANTHROPIC_MODEL": "Qwen3.6-27B",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "Qwen3.6-27B",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "Qwen3.6-27B",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "Qwen3.6-27B",
"ANTHROPIC_DEFAULT_FABLE_MODEL": "Qwen3.6-27B",
"CLAUDE_CODE_SUBAGENT_MODEL": "Qwen3.6-27B",
"API_TIMEOUT_MS": "3000000",
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "200000",
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "90"
},
"theme": "auto",
"statusLine": {
"type": "command",
"command": "node -e \"let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s),c=j.context_window||{},i=c.total_input_tokens||0,o=c.total_output_tokens||0,m=(j.model&&(j.model.display_name||j.model.id))||'model';console.log(m+' | Claude '+(i+o)+'/'+(c.context_window_size||200000)+' ('+(c.used_percentage||0)+'%) | Server 262144')})\"",
"padding": 1
}
}ANTHROPIC_AUTH_TOKEN 填写 llama-server 使用的 API Key,不要加 Bearer 前缀。ANTHROPIC_BASE_URL 是服务地址,其他模型字段全部指向同一个本地模型。
statusLine 会读取 Claude Code 传入的会话 JSON。Claude Code 按 200000 token 管理这个本地模型,llama-server 的实际上下文则是 262144,所以状态栏同时显示两个数值。命令使用 Node.js,不依赖 jq:
Qwen3.6-27B | Claude 16700/200000 (8.4%) | Server 262144第一次收到模型响应后,状态栏才会出现 token 数据。CLAUDE_CODE_AUTO_COMPACT_WINDOW 把自动压缩窗口设为 200000 token,CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 再把阈值设为 90。这样大约到 180000 token 时就会开始压缩,给服务端留出余量。Claude Code 会先清理旧工具输出,再摘要较早的对话。
可以随时输入 /context 查看上下文占用,或输入 /compact 手动压缩。状态栏中的 Server 262144 只是显示服务端容量,不会把 Claude Code 的自动压缩上限扩大到 262144。
先测试普通对话:
claude --model Qwen3.6-27B -p --max-turns 1 --no-session-persistence "只回复 OK"实际返回 OK。再测试 Bash 工具调用:
claude --model Qwen3.6-27B -p --max-turns 2 --no-session-persistence "使用 Bash 执行 git rev-parse --short HEAD,只回复命令输出"实际返回 c193f661。至此,Qwen3.6-27B 的普通对话和 Bash 工具调用都已跑通。Qwen-AgentWorld-35B-A3B 的接入情况单独记在文末。
Codex 配置
Codex 桌面应用、CLI 和 IDE 扩展共用 ~/.codex/config.toml。可以在保留 ChatGPT 订阅登录的同时注册自定义模型 provider,但本地模型不能直接加入桌面端的订阅模型列表。选择自定义 provider 后,请求会发往 llama-server,不会消耗 Codex 订阅额度;切回 OpenAI provider 仍可继续使用订阅模型。
llama-server 已提供 /v1/responses,Codex 可以直接走 Responses API。最好单独创建一个 profile,免得覆盖日常使用的订阅配置:
- Linux:
~/.codex/local-qwen.config.toml - Windows:
%USERPROFILE%\.codex\local-qwen.config.toml
model = "Qwen3.6-27B"
model_provider = "llama_cpp_qwen"
model_context_window = 262144
model_auto_compact_token_limit = 200000
model_reasoning_summary = "none"
approval_policy = "never"
sandbox_mode = "workspace-write"
web_search = "disabled"
[features]
apps = false
plugins = false
tool_search = false
standalone_web_search = false
[model_providers.llama_cpp_qwen]
name = "llama.cpp Qwen3.6-27B"
base_url = "http://localhost:30104/v1"
experimental_bearer_token = "与 LLAMA_API_KEY 相同的 key"
wire_api = "responses"
request_max_retries = 2
stream_max_retries = 5
stream_idle_timeout_ms = 3000000
[sandbox_workspace_write]
network_access = true
[tui]
status_line = ["model-with-reasoning", "context-remaining", "current-dir", "git-branch"]experimental_bearer_token 直接填写 API Key,不要加 Bearer 前缀,Codex 会自行生成 Authorization 请求头。这个 key 会以明文保存在配置文件中,别把文件提交到仓库或同步到公共位置。
status_line 位于交互式命令行底部,显示当前模型与推理级别、剩余上下文、当前目录和 Git 分支。输入 /status 可以查看 token 使用、审批策略和可写目录,/statusline 则用于调整底部字段。
workspace-write 允许 Codex 读取项目、修改当前工作区并执行命令,network_access = true 则允许这些命令访问网络。配合 approval_policy = "never" 后,沙箱内的操作直接执行,越界操作直接失败,不会反复弹出确认框。这仍然不是完整的系统权限。
如果几乎每条命令都要求确认,通常是 profile 仍在使用 approval_policy = "on-request",或者当前会话通过 /permissions 改过权限。这里假设项目可信,因此选择 never + workspace-write。如果希望越界时由人来决定,再改回 on-request。
另一种做法是保留交互式审批,把符合策略的请求交给审核代理:
approval_policy = "on-request"
approvals_reviewer = "auto_review"
sandbox_mode = "workspace-write"自动审核不会扩大沙箱范围,高风险操作仍可能要求人工确认。只连接本地模型、又不想频繁被打断时,approval_policy = "never" 更省事。
web_search = "disabled" 用来关闭 Codex 自带的 Web Search。当前 llama.cpp 不能处理 Responses API 的 web_search 工具类型,开着只会在服务端留下跳过警告。[features] 里的四个开关还会关闭 Apps、插件、工具延迟加载和独立 Web Search,以减少发给 llama.cpp 的命名空间工具。如果这些功能有用,就不要照搬这部分配置。
base_url 只写到 /v1,Codex 会自行追加 /responses。如果 Codex 与模型服务不在同一台机器,将 localhost 换成模型服务器的可访问地址。跨公网不要直接用 HTTP,Bearer Token 会明文传输;用 HTTPS 反向代理、VPN 或 SSH 隧道。
profile 只对 CLI 生效。直接运行 codex 会使用订阅配置,运行 codex --profile local-qwen 才会连接本地模型。桌面应用没有 profile 切换入口;要在桌面端使用本地模型,需要把配置写进 config.toml,切回订阅时再移除 model 和 model_provider。
启动交互式对话:
codex --profile local-qwen也可以只对本次启动覆盖审批和沙箱设置,不修改配置文件:
codex --profile local-qwen --ask-for-approval never --sandbox workspace-write同一条命令可以简写为 -a never -s workspace-write。配置修改后要新开会话,再输入 /status 核对审批策略、沙箱模式和可写目录。
单次执行任务用 codex exec。
测试普通回复:
codex exec --profile local-qwen "只回复 OK"在 Git 仓库中测试工具调用:
codex exec --profile local-qwen "运行 git rev-parse --short HEAD,只回复命令输出"两项都通过,才算真正接通。
我的实际体验是,即使用自有模型,Codex 仍会读取 AGENTS.md 和原有项目上下文。执行简单命令时,和订阅模型的操作方式没有明显区别。
排查 Codex 模型目录错误
使用 Codex CLI 0.144.6 连接 llama-server 时,我遇到了三条相关日志:
failed to load models cache: missing field `supports_reasoning_summaries`
failed to decode models response: missing field `slug`
Model metadata for `Qwen3.6-27B` not found. Defaulting to fallback metadatasupports_reasoning_summaries 来自共享的 %USERPROFILE%\.codex\models_cache.json。Codex 桌面应用与独立安装的 npm CLI 共用该缓存,两者内部模型目录格式不同时,CLI 会无法解析另一端写入的缓存。client_version 不等于 npm 版本号,不需要去找不存在的版本。
slug 则是另一处格式差异。llama-server 的 /v1/models 返回 OpenAI 兼容的 data[].id,Codex 内部模型目录却要求 models[].slug,还需要上下文、推理和工具元数据。解析失败后,Codex 会退回默认元数据。我的命令最终仍返回了 OK,说明 Responses API 推理正常,出问题的只是模型元数据加载。
models_cache.json 是 Codex 自己管理的缓存,不必手工补字段。需要清理时,完全退出桌面应用后删除文件,下次启动会自动重建:
Remove-Item -LiteralPath "$env:USERPROFILE\.codex\models_cache.json"桌面应用运行时会再次写入缓存,所以日志也可能回来。只要命令能返回结果,就不用围着这条日志手写 JSON。
如果启动后一直显示 Reconnecting,最终也没有结果,就该检查实际的推理请求了。experimental_bearer_token 填错时,/v1/responses 会返回:
{
"error": {
"message": "Invalid API Key",
"type": "authentication_error",
"code": 401
}
}这个 401 错误说明 experimental_bearer_token 没有替换成实际的 LLAMA_API_KEY。
排查 llama-server 工具类型警告
Codex 开始执行真实编码任务后,llama-server 可能反复输出:
W srv server_chat_: unsupported Responses tool type 'namespace' skipped
W srv server_chat_: unsupported Responses tool type 'web_search' skipped这是工具兼容性警告,不是推理错误。日志后面若还有 processing task、print_timing 和 release,请求就已经跑完了。不过,标记为 skipped 的工具并没有传给模型。能正常聊天,不代表 Agent 工具也能调用。
问题出在两端对工具格式的理解不同。Codex CLI 0.144.6 默认认为自定义 provider 支持 namespace 和 web_search,会把它们写入 Responses 请求。llama.cpp 目前只转换 type = "function",其余类型记录警告后直接跳过。这和 API Key、量化版本、上下文长度或 CUDA 参数都无关,调整 llama-server 启动参数也解决不了。
前面配置中的 web_search = "disabled" 可以去掉 web_search 警告。它必须写在 [model_providers.llama_cpp_qwen] 之前,保持为顶层配置;写在 provider 表后面会变成该表的字段。
Codex CLI 0.144.6 没有开放关闭 namespace provider 能力的配置项,namespace_tools = false 之类的写法无效。前面的完整 profile 已经关闭 Apps、插件和延迟加载工具,对应配置如下:
[features]
apps = false
plugins = false
tool_search = false
standalone_web_search = false这些开关只能减少可选扩展产生的 namespace,未必能彻底消除警告。Apps、插件和工具搜索也会随之不可用。要保留这些工具,需要换用兼容 Codex Responses 工具格式的后端,或者在 Codex 和 llama.cpp 之间加一层双向转换代理。升级 llama.cpp 前,先看它的 Responses 适配器是否已经支持非 function 工具。
“只回复 OK”只能测试文本生成。要确认 Agent 能用,得让它实际调用一次终端:
codex exec --profile local-qwen "必须调用终端执行 git rev-parse --show-toplevel,然后原样输出命令结果"看到真实仓库路径和工具调用记录,才能确认终端工具可用。如果模型只是生成了一个看似正确的路径,应当按工具接入失败处理。
相关实现可以直接查看 llama.cpp 的 Responses 工具转换代码、Codex CLI 0.144.6 的 provider 默认能力以及 Codex 的命名空间工具筛选逻辑。
检查 llama-server 接口
llama-server 从 LLAMA_API_KEY 环境变量读取 API Key,查询时带上同一个值:
curl -s http://localhost:30104/health
curl -s -H "Authorization: Bearer $LLAMA_API_KEY" http://localhost:30104/v1/models
curl -N -H "Authorization: Bearer $LLAMA_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"Qwen3.6-27B","input":"只回复 OK","stream":true}' \
http://localhost:30104/v1/responses
curl -s -H "Authorization: Bearer $LLAMA_API_KEY" http://localhost:30104/metrics
curl -s -H "Authorization: Bearer $LLAMA_API_KEY" http://localhost:30104/props
curl -s -H "Authorization: Bearer $LLAMA_API_KEY" http://localhost:30104/slots/health 用来确认服务是否在线,/v1/models 返回已加载模型。性能和运行状态分别看 /metrics、/props 与 /slots。
本次检查时,/health 返回:
{"status":"ok"}/v1/models 实际返回:
{"models":[{"name":"Qwen3.6-27B","model":"Qwen3.6-27B","modified_at":"","size":"","digest":"","type":"model","description":"","tags":[""],"capabilities":["completion","multimodal"],"parameters":"","details":{"parent_model":"","format":"gguf","family":"","families":[""],"parameter_size":"","quantization_level":""}}],"object":"list","data":[{"id":"Qwen3.6-27B","aliases":["Qwen3.6-27B"],"tags":[],"object":"model","created":1784619582,"owned_by":"llamacpp","meta":{"vocab_type":2,"n_vocab":248320,"n_ctx":262144,"n_ctx_train":262144,"n_embd":5120,"n_params":26895998464,"size":16806250496,"ftype":"Q4_K - Medium"}}]}启动时设置了 --alias Qwen3.6-27B,所以返回的模型名称和 ID 都是 Qwen3.6-27B。
/metrics 返回的主要统计如下:
llamacpp:prompt_tokens_total 70285
llamacpp:prompt_seconds_total 26.391
llamacpp:prompt_tokens_seconds 2663.22
llamacpp:tokens_predicted_total 4021
llamacpp:tokens_predicted_seconds_total 83.936
llamacpp:predicted_tokens_seconds 47.9055
llamacpp:n_decode_total 4051
llamacpp:n_tokens_max 25184
llamacpp:requests_processing 0
llamacpp:requests_deferred 0
llamacpp:n_busy_slots_per_decode 1这次记录到的提示词处理速度约为 2663 token/s,生成速度约为 47.9 token/s。/props 显示上下文为 262144,/slots 中唯一的 slot 处于空闲。指标会随请求变化,这里只记录当时的运行状态,不当作基准测试结果。
TMUX 工具
sudo apt install tmux
tmux
tmux attach -t 0进入 tmux 后运行 llama-server。离开时按 Ctrl+B,再按 D 分离会话,模型会继续运行。不要在会话里执行 exit,否则进程也会结束。
重新登录后用 tmux attach -t 0 回到原来的终端。