如何用llama-server以OpenAI兼容模式运行本地模型,涵盖环境变量配置、--jinja聊天模板、端口等待等避坑细节。
llama-server 对 OpenAI 的 HTTP 格式支持得足够好,大多数客户端库只需把 base_url 指向本地机器就能直接使用。但在运行时才会暴露的差异是明确的,值得提前了解。
最小配置只需要一个模型,其他都走默认,因为现在的默认值已经做了很多工作:-ngl 默认为 auto,-c 默认为模型训练时的 context,--host 是 127.0.0.1,--port 是 8080,--jinja(使服务器使用 GGUF 中嵌入的 chat template)默认启用。
llama-server -m ./models/qwen2.5-7b-instruct-q4_k_m.gguf \
-c 8192 \
-ngl 99 \
--host 127.0.0.1 --port 8080 \
--api-key "$LLAMA_API_KEY"
这些 flag 都有对应的环境变量,在容器场景下正合所需:LLAMA_ARG_CTX_SIZE、LLAMA_ARG_N_GPU_LAYERS、LLAMA_ARG_PORT 等等。等屏幕上出现服务器正在监听的提示后再调用——HTTP 端口在模型加载和 KV cache 分配完成后才会打开,大模型这个过程并非瞬间完成。
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LLAMA_API_KEY" \
-d '{
"model": "local",
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why is prefill faster than generation?"}
],
"temperature": 0.2,
"max_tokens": 200
}'
响应是熟悉的数据信封:有 id,有 choices 数组(第一个元素包含 message 和 finish_reason),有 usage 对象(含 prompt_tokens、completion_tokens 和 total_tokens)。请求中的 model 字段不参与路由——一个服务器进程只服务一个模型,你传什么字符串它就原样 echo 回去。需要根据列表验证模型名称的客户端应调用 GET /v1/models,服务器已实现了这个端点。
加上 "stream": true 即可获得 OpenAI delta 格式的 server-sent events,以字面量 data: [DONE] 行结束。除 OpenAI 兼容层外,服务器还暴露了 /v1/completions、/v1/embeddings,以及当前构建版本中的 /v1/responses——此外还有自己的原生端点,用于暴露 OpenAI 协议中没有对应字段的 llama.cpp 专用参数。
兼容意味着请求和响应的 schema 足够接近,客户端库可以正常解析。但这不代表行为完全一致,差异是系统性的而非随机的:
采样参数在两个方向上互为超集和子集。llama.cpp 支持 OpenAI 没有定义的参数——min_p、repeat_penalty、mirostat、tfs_z——这些作为 extra fields 传入,大多数客户端都允许。反过来,某些仅在托管服务存在的字段会被接受但被忽略。
结构化输出底层是 grammar。带 json_schema 的 response_format 会被转换为 GBNF 并由 sampler 强制执行,保证比 prompt 指令更强,但 converter 能支持的 schema 特性也仅此而已。参见 GBNF grammars。
工具调用取决于模型的 template。服务器可以将模型原生格式的工具调用解析为 OpenAI 的 tool_calls 形状,但仅限它认识的格式。如果模型的 template 以特殊形式输出工具调用,这些调用就会作为普通消息文本返回。
不存在组织级概念。没有 projects,没有按 key 的速率限制,没有用量仪表盘。一把 key、一个模型、一个进程。
“能跑但答案很差”最常见的根本原因是 chat template 错误。服务器接收你的 messages 数组,用 GGUF 中存储的 Jinja template 将其渲染成模型实际看到的单个扁平字符串。特殊 token 不匹配不会报错——模型只是收到了它未训练过的格式的 prompt,表现得像一个状态不佳的 base model。
--chat-template 按名称覆盖嵌入的 template,--chat-template-file 从磁盘加载一个 template。只有在 GGUF 转换时没有 template 或用了错误的 template 时才需要动用这些选项;嵌入的 template 正确的概率远高于手动猜测。如果感觉 system message 被忽略了,这是首先要检查的具体症状——某些 template 把 system message 放在模型权重处理方式不同的位置,更老的一些甚至会丢弃它。
服务器将其 KV cache 分割为多个 slot,每个 slot 服务一个序列。-np/--parallel 设置数量,默认为 auto。这里有一道很多人漏掉的算术题:-c 32768 -np 4,每个 slot 只得到 8192 个 token,而非 32768。一个请求超过一个 slot 的份额就会失败或被截断,尽管总量看起来很宽裕。
Slot 还实现了跨请求的 prompt 前缀复用,这就是共享的 system prompt 不会每次调用都重新计算的原因——与托管服务上的 prompt caching 机制相同,只是 cache 在你自己的 RAM 里。代价是持有长缓存前缀的 slot 无法用于需要不同前缀的请求;参见 server slots。
--host 默认为 127.0.0.1,而容器无法连接时的常见第一反应是改成 0.0.0.0。这会绑定所有网卡。--api-key 默认为空,此时任何能路由到这个端口的人都可以免费使用你的 GPU,并读取 prompt cache 中的任何内容。绑定到私有网卡、设置 key,如果需要让服务离开本机就再加一层反向代理。
这里一种常见形态是:本地 llama-server 处理廉价或私密路径,本地做不了的就转向托管模型——长 context、更难的任务,或者机器休眠时。用两个后端的差异不只是 URL:token 计量、streaming 事件形状、工具调用格式和错误体都有细微差别,所以 fallback 逻辑最终要写两遍。类似 Multigrid 这样的网关在单一调用点背后统一了两个后端,这种做法在需要按请求而非按部署切换时是值得的。
本文中的端点列表和 flag 默认值来源于撰写时 llama.cpp server README 的 master 分支。这个接口会定期新增端点——/v1/responses 就是最近加入的——所以请查看你所运行构建版本对应的 README。