把本地大模型变成 OpenAI 兼容 API:llama-server 服务化实战

跑通了本地模型,下一个问题就是怎么把它当服务用——像云厂商的 API 那样被自己的脚本、Agent、其他程序反复调用。llama.cpp 的 llama-server 就是干这个的:常驻内存、暴露 OpenAI 兼容接口、多轮对话自动复用缓存。这篇用 supergemma4-26B 讲完整套实战。

为什么非要服务方式

调用方式能否复用 KV 缓存说明
llama-cli 每次单独跑(-n 后退出)进程退出,缓存清零
llama-cli 交互模式(同窗口连续提问)同进程多轮复用
llama-server 常驻服务OpenAI 兼容 API,天然复用,多客户端并发

缓存复用依赖「进程常驻 + 上下文不销毁」,服务方式天然满足。多轮 Agent 每次带完整 history 调 API,服务端只 prefill 新增的增量,首字延迟和内存开销都小得多。

服务端启动(常驻)

入口脚本 serve-full.cmd

@echo off
chcp 65001 >nul
setlocal
set LLAMA_DIR=C:\...\Temp\opencode\llama
set MODEL=C:\...\supergemma4-26b-uncensored-fast-v2-Q4_K_M.gguf

"%LLAMA_DIR%\llama-server.exe" -m "%MODEL%" -ngl 99 -t 8 -ctk q8_0 -ctv q8_0 -rea off --host 127.0.0.1 --port 8080
endlocal
  • -rea off:关闭思考块,正文直接进 content(supergemma4 默认带推理链,不开它会返回空 content)
  • -ctk q8_0 -ctv q8_0:KV 量化,每 token 约 210KB(f16 是 420KB),省一半内存
  • 不传 -c 默认用模型上下文(实测 n_ctx=228608)——本机受显存限制实际上不了那么长,见文末
  • 启动后日志应显示 llama_server: listening on http://127.0.0.1:8080

一个关键坑:后台进程必须在你自己的终端手动运行。schtasks / WScript / Start-Process 拉起的进程都会被回收,无法常驻。

客户端调用(OpenAI 兼容)

import json, urllib.request

body = {
    "model": "supergemma4",
    "messages": [{"role": "user", "content": "用两句话解释为什么天空是蓝色的"}],
    "max_tokens": 120,
    "temperature": 0.7,
    "stream": False,
}
req = urllib.request.Request(
    "http://127.0.0.1:8080/v1/chat/completions",
    data=json.dumps(body).encode("utf-8"),
    headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=300) as resp:
    data = json.loads(resp.read().decode("utf-8"))
print(data["choices"][0]["message"]["content"])

响应结构兼容 OpenAI,还带推理与计时的扩展字段:

字段
message.content正文回答(-rea off 后直接输出)
message.reasoning_content思考内容(-rea off 后为 null)
usage.prompt_tokens_details.cached_tokens缓存命中的 token 数
timings.prompt_per_second / predicted_per_second服务端测到的 pp / tg 速度

多轮对话写法:每轮把全部历史作为 messages 数组传上去(system/user/assistant 交替),服务端自动前缀缓存。

前缀缓存实测

3 轮对话(每轮带全量历史),cached_tokens 递增:

轮次prompt_tokenscached_tokens说明
1191首次全量 prefill
26914历史复用
310364历史越长,命中越多

5 轮递增历史测试更直观:首轮 prefill 1.75s,之后每轮稳定 ~1.2s(只算新增 token),不随历史长度增长。第二轮起即走前缀缓存

上下文上限探底

级别结果内存
≤ 16,124 token(16K)✅ 稳定20.8 / 23.5 GB
~20,000 token(20K)❌ HTTP 500
{"error":{"code":500,"message":"decode() failed: vk::Queue::submit: ErrorDeviceLost","type":"server_error"}}

瓶颈不是系统内存(16K 时还剩 ~2.8GB),是 Arc 140T 驱动在长 prefill 时 Vulkan ErrorDeviceLost——模型 15.63GB 大于 Vulkan device-local 堆 13.42GB,强靠 host 溢出撑,长 prefill 触发驱动动态预算崩溃。本机实测可用上下文上限 ≈ 16K;调优方法见20K 长上下文崩溃排查

总结

  • 常驻服务的正确形态:llama-server + OpenAI 兼容 API + 前缀缓存,Agent 多轮只算增量
  • 必须 -rea off,否则 supergemma4 思考块把正文顶空
  • 服务必须你自己终端手动运行才能常驻
  • 上下文上限受核显驱动限制(本机 ≈16K),长上下文要调 -ub 切块
端口注意:默认端口 8080,llama.cpp 未来版本将改为 9931。客户端记得与启动参数保持一致。

You got a local model running — next question: how to use it as a service, like a vendor API that your scripts, agents and other programs can call repeatedly. llama.cpp's llama-server does exactly that: resident in memory, OpenAI-compatible endpoints, and automatic cache reuse across turns. This guide uses supergemma4-26B end to end.

Why serve it at all

InvocationReuses KV cache?Notes
llama-cli each run (-n then exit)process exits, cache cleared
llama-cli interactive (same window)reused across turns in-process
llama-server resident serviceOpenAI-compatible API, natural reuse, concurrent clients

Cache reuse depends on a resident process with a persistent context — something a service naturally provides. Each multi-turn agent call sends full history; the server only prefills the new increment, slashing latency and memory.

Launching the resident server

The launcher serve-full.cmd:

@echo off
chcp 65001 >nul
setlocal
set LLAMA_DIR=C:\...\Temp\opencode\llama
set MODEL=C:\...\supergemma4-26b-uncensored-fast-v2-Q4_K_M.gguf

"%LLAMA_DIR%\llama-server.exe" -m "%MODEL%" -ngl 99 -t 8 -ctk q8_0 -ctv q8_0 -rea off --host 127.0.0.1 --port 8080
endlocal
  • -rea off: disable thinking blocks so the answer lands in content (supergemma4 ships with reasoning; without this flag content comes back empty)
  • -ctk q8_0 -ctv q8_0: quantized KV cache, ~210KB per token (vs 420KB at f16) — halves memory
  • Without -c, the model's context is used (measured n_ctx=228608) — though VRAM caps you well below that here, see the end
  • You should see llama_server: listening on http://127.0.0.1:8080

One critical gotcha: run it in your own terminal. Processes spawned via schtasks / WScript / Start-Process get reaped — they won't stay resident.

Calling it (OpenAI-compatible)

import json, urllib.request

body = {
    "model": "supergemma4",
    "messages": [{"role": "user", "content": "用两句话解释为什么天空是蓝色的"}],
    "max_tokens": 120,
    "temperature": 0.7,
    "stream": False,
}
req = urllib.request.Request(
    "http://127.0.0.1:8080/v1/chat/completions",
    data=json.dumps(body).encode("utf-8"),
    headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=300) as resp:
    data = json.loads(resp.read().decode("utf-8"))
print(data["choices"][0]["message"]["content"])

The response mirrors OpenAI's schema, plus reasoning and timing extras:

FieldValue
message.contentanswer body (direct with -rea off)
message.reasoning_contentthinking (null with -rea off)
usage.prompt_tokens_details.cached_tokenstokens served from cache
timings.prompt_per_second / predicted_per_secondserver-side pp / tg speeds

Multi-turn: send the full history as the messages array each time (alternating system/user/assistant); the server auto-prefix-caches.

Prefix caching, measured

Three turns (each with full history), cached_tokens climbing:

Turnprompt_tokenscached_tokensNotes
1191first full prefill
26914history reused
310364longer history, more hits

A five-turn incremental test is clearer: first prefill 1.75s, then a steady ~1.2s per turn (new tokens only), independent of history length. Prefix cache kicks in from turn two.

Probing the context limit

LevelResultMemory
≤ 16,124 tokens (16K)✅ stable20.8 / 23.5 GB
~20,000 tokens (20K)❌ HTTP 500
{"error":{"code":500,"message":"decode() failed: vk::Queue::submit: ErrorDeviceLost","type":"server_error"}}

System RAM is not the bottleneck (~2.8GB left at 16K); it's the Arc 140T driver throwing Vulkan ErrorDeviceLost on long prefills — the 15.63GB model exceeds the 13.42GB device-local heap, host overflow keeps it alive until a long prefill trips the driver's dynamic budget. Measured usable limit here: ≈16K. Fixes: the 20K context debug.

Summary

  • The right serving shape: llama-server + OpenAI-compatible API + prefix cache; agents pay only for increments
  • -rea off is mandatory, or supergemma4's thinking blocks empty the content field
  • Run it in your own terminal to keep it resident
  • Context is capped by the iGPU driver (≈16K here); long contexts need -ub batching
Port note:Default port is 8080; future llama.cpp versions will move to 9931. Keep client and server in sync.
返回文章列表