llama-server 开发文档
本文是llama-server的开发者文档,详细介绍了其后端架构、特性范围、批处理机制、线程管理、请求处理流程、可恢复流式传输(SSE重播缓冲区)、测试方法、工具API、路由模式下的子进程通信与模型管理API,以及基于SvelteKit的Web UI的架构、功能与技术栈。文章面向贡献者,深入探讨了核心设计决策和实现细节。
这份文档为 llama-server 提供了深入的技术概述,面向维护者和贡献者。
如果你是作为终端用户使用 llama-server 产品,请参考主 README 文档。
功能范围
范围内的功能类型:
- 后端:
- 基础推理功能:文本补全、嵌入输出
- 对话功能:聊天补全、工具调用
- 第三方 API 兼容,例如 OAI 兼容、Anthropic 兼容
- 多模态输入/输出
- 内存管理:保存/加载状态、上下文检查点
- 模型管理
- Web UI 所需的功能
- 前端:
- 对话功能,例如:基础聊天、图片上传、编辑消息
- 智能体功能,例如:MCP
- 模型管理
注意:出于安全原因,需要读取或写入外部文件的功能必须默认禁用。这包括:MCP、模型保存/加载等。
范围外的功能:
- 后端:
- 需要外部 API 调用循环的功能,例如服务器端智能体循环。这是因为在 C++ 中进行外部 API 调用的维护成本很高。任何复杂的第三方逻辑都应在服务器代码之外实现。
- 暴露模型内部状态给 API 的功能,例如通过 API 获取中间激活值。这是因为 llama.cpp 不支持稳定的 API 来实现此操作,而依赖
eval_callback会使维护变得复杂,因为该 API 并非设计用于多序列场景。 - 特定于模型的功能。所有 API 调用和功能必须保持模型无关。
- 前端:
- 第三方插件,维护公开的插件 API 成本很高。用户可以根据需要创建自己的 MCP 服务器。
- 可定制的主题,维护成本也很高。虽然我们注重美观,但我们会通过完善少量主题来实现这一点。
- 特定于浏览器的功能,例如:Chrome 内置 AI API。
后端
概述
服务器支持两种主要操作模式:
- 推理模式:默认模式,使用单个加载的 GGUF 模型执行推理。
- 路由器模式:允许在单个 API 端点后管理多个推理服务器实例。请求将根据请求的模型自动路由到相应的后端实例。
核心架构由以下组件组成:
server_context:保存主要推理状态,包括主llama_context和所有活动槽位。server_slot:对 llama.cpp 中单个“序列”的抽象,负责管理单个并行推理请求。server_routes:server_context和 HTTP 接口之间的中间件层;处理 JSON 解析/格式化和请求路由逻辑。server_http_context:使用cpp-httplib实现 HTTP 服务器。server_queue:HTTP 工作线程用来向server_context提交新任务的线程安全队列。server_response:server_context用来向 HTTP 工作线程返回结果的线程安全队列。server_response_reader:对上述两个队列的更高级封装,使代码更清晰。server_task:推送到server_queue的工作单元。server_task_result:推送到server_response的结果单元。server_tokens:Token 序列的统一表示(支持文本和多模态 Token);由server_task和server_slot使用。server_prompt_checkpoint:对于循环(例如 RWKV)和 SWA 模型,存储 KV 缓存状态的快照。当后续请求共享相同的提示前缀时,可以重复使用,从而节省冗余计算。server_models:独立组件,用于管理多个后端实例(在路由器模式下使用)。它与server_context完全独立。stream_session_manager:进程范围内所有可恢复 SSE 流会话的所有者,以对话 ID 为键。是server-stream.cpp内的文件静态单例,通过server_stream_session_manager_start/stop驱动。为重放缓冲区提供支持,允许客户端在 HTTP 断开后重新连接到生成过程。请参阅下面的“可恢复流”部分。
graph TD
API_User <--> server_http_context
server_http_context <-- 路由器模式 --> server_models
server_http_context <-- 推理模式 --> server_routes
server_routes -- server_task --> server_queue
subgraph server_context
server_queue --> server_slot
server_slot -- server_task_result --> server_response
server_slot[多个 server_slot]
end
server_response --> server_routes
批处理
服务器上下文维护一个在所有槽位之间共享的单个批次。当调用 update_slots() 时,系统会遍历所有活动槽位以填充此批次。对于每个槽位,要么添加上一个解码步骤生成的 Token,要么添加可用的提示 Token。
批处理存在约束:槽位只有在配置兼容时才能一起批处理。例如,使用特定 LoRA 适配器的槽位可以相互批处理,但不能与使用不同 LoRA 适配器或未使用适配器的槽位一起批处理。
一旦批次达到容量或所有槽位都已处理完毕,就会调用 llama_decode 来执行推理。此操作代表了 update_slots() 中的主要计算瓶颈。
解码完成后,系统会检索嵌入或使用 common_sampler_sample 采样下一个 Token。如果某个槽位仍有剩余的提示 Token 需要处理,它会等待,直到下一次 update_slots() 迭代。
线程管理
server_context 在专用的单线程上运行。由于它是单线程的,应避免进行繁重的后处理(尤其是在 Token 生成之后),因为这会直接影响多序列的吞吐量。
每个传入的 HTTP 请求都由 HTTP 库管理的自己的线程处理。以下操作在 HTTP 工作线程中执行:
- JSON 请求解析
- 聊天模板应用
- 分词
- 将
server_task_result转换为最终的 JSON 响应 - 错误格式化为 JSON
- 跟踪部分/增量响应(例如,流式工具调用或推理步骤)
应遵循的最佳实践:
- 所有 JSON 格式化和聊天模板逻辑必须保留在 HTTP 层。
- 避免在 HTTP 层和
server_slot之间传递原始 JSON。相反,应尽早将所有内容解析为原生 C++ 类型。
请求示例跟踪
以下是用于文本补全的 API 请求示例跟踪:
- 请求到达 HTTP 层。
- 请求被路由到
server_routes中相应的处理程序。本例中,调用handle_completions_impl。 - 处理程序解析输入请求,构造一个新的
server_task,并将其传递给server_res_generator。 server_res_generator为每个任务创建一个新的task_result_state:task_result_state保留在 HTTP 层,负责跟踪响应的当前状态(例如,解析工具调用或思考消息)。server_task被移入server_context内的server_queue。
server_context通过将任务移入可用槽位来启动任务(请参阅launch_slot_with_task())。update_slot()按照上述“批处理”部分所述处理任务。- 结果可以通过
send_partial_response或send_final_response发送,这会创建新的server_task_result并将其推送到响应队列。 - 同时,
server_res_generator监听响应队列并检索此响应。 - 由于响应是无状态的,
server_res_generator调用response->update()来用当前状态更新响应。 - 然后
server_res_generator调用response->to_json()并将响应传递给 HTTP 层。
可恢复流(SSE 重放缓冲区)
默认情况下,流式生成与其 HTTP 套接字绑定:当套接字断开(刷新、关闭标签页、应用进入后台、瞬时网络问题)时,生成将中止,实时流丢失。此功能保持生成在服务器端运行,并允许客户端重新连接。
通过在 POST /v1/chat/completions 上设置 X-Conversation-Id 头部来启用此功能。没有此头部时,OAI 严格路径保持不变。对话 ID 是整个通信过程中的唯一标识(服务器映射键、客户端 localStorage 键、路由路径),并且可选的 ::model 后缀用于路由器模式下的直接路由。
此功能完全位于 server-stream.{h,cpp} 中,并依赖于三种类型:
stream_session:一个有界环形缓冲区(上限 4 MiB,最早字节先丢弃)加上一个条件变量。append推送原始 SSE 字节,read_from从任意偏移量读取并阻塞等待实时字节或完成,finalize唤醒读取器,cancel设置生产者轮询的标志。一个对话最多对应一个实时会话。stream_session_manager:server-stream.cpp内的文件静态单例(g_stream_sessions),拥有所有以 conv_id 为键的会话,通过create_or_replace强制执行一个对话一个会话的不变量,并运行一个 GC 线程,该线程丢弃超过其 TTL 的已完成会话。仅通过server_stream_session_manager_start/stop暴露给主程序。stream_pipe_producer/stream_pipe_consumer:写入端和读取端。生产者拥有会话生命周期,并在析构时完成会话;消费者是只读的,从不完成会话,因此读取器断开不会终止正在运行的生成。
实现隐藏在 server-stream.cpp 中(pimpl)。头文件仅暴露路由处理程序工厂、server_res_spipe 响应基类、server_stream_conv_id_from_headers 和 GC 生命周期;会话、管理器、消费者以及 server_stream_create_spipe 工厂保留在 .cpp 中。
生产者侧:server_res_generator 继承自 server_res_spipe,这使所有 spipe 逻辑远离通用的 server_http_res。set_req 在存在头部时附加一个生产者,封装的 next 在将每个块发送到套接字之前将其也写入环形缓冲区,这样因网络断开而丢失的块已经在缓冲区中。在附加状态下,should_stop 忽略对等方断开:只有 DELETE 会停止生成。当对等方过早断开时,on_complete 在 HTTP 工作线程上将尾随数据写入环形缓冲区。
生命周期安全性:会话不持有对响应的反向引用,因此 spipe 是一个普通的 unique_ptr,仅由 HTTP 工作线程访问。cancel 设置一个生产者轮询的原子标志;生产者在析构函数中完成会话,该析构函数还会运行 ~server_response_reader::stop() 以在队列级别取消生成。DELETE 通过设置标志并让工作线程展开来停止工作。
消费者侧:GET /v1/stream?conv_id=<id>&from=N 打开一个 text/event-stream,它从偏移量 N 开始重放缓冲的字节,并阻塞等待实时字节,因此浏览器可以像新的 EventSource 一样重新连接。如果偏移量低于已丢弃的前缀,则返回 400。
路由:
GET /v1/stream?conv_id=<id>&from=N:重放或实时重新连接。ID 放在查询字符串中,因为它可能包含模型名称(可能包含斜杠)。POST /v1/streams/lookup带有{"conversation_ids": [...]}:仅返回调用者已拥有的 ID 的会话状态。没有列举路由,因此无法枚举实时会话(之前的GET /v1/streams正是因为这个原因被移除)。DELETE /v1/stream?conv_id=<id>:显式停止,幂等(evict_and_cancel)。
路由器模式将相同的路径绑定到代理处理程序。一个 conv_id -> child 映射(conv_models),在 POST 被路由时填充,通过一次查找(无需轮询)解析出所属子进程。该映射按子进程对 ID 进行分组;GET 和 DELETE 直接代理到所有者。这个回环 REST 跳转预计未来会迁移到 WebSocket IPC,仅替换传输层。
生命周期:server_stream_session_manager_start() 在通用初始化之后在 main 中运行,server_stream_session_manager_stop() 在 clean_up() 中最先运行,完成所有实时会话,这样就没有读取器挂起。读取器阻塞和断开后排空都在 httplib 工作线程上运行,它们会在条件变量上阻塞而不是自旋。
| 常量 | 值 | 作用 |
|---|---|---|
STREAM_SESSION_TTL_SECONDS |
300 | 已完成会话在 GC 之前的保留时间 |
STREAM_SESSION_MAX_BYTES |
4 MiB | 每个会话的环形缓冲区容量 |
STREAM_SESSION_GC_INTERVAL_SECONDS |
60 | GC 触发间隔 |
STREAM_READ_WAKE_INTERVAL_MS |
200 | read_from 唤醒以重新检查 should_stop |
STREAM_LOOKUP_TIMEOUT_MS |
250 | 路由器到子进程回环预算时间 |
graph TD
Client -- "POST + X-Conversation-Id" --> RG[server_res_generator]
RG -- attach --> Prod[stream_pipe_producer]
Prod -- "写入,对等方断开时排空" --> Sess
subgraph g_stream_sessions
Sess[stream_session: 环形缓冲区, 4 MiB]
GC[GC 线程] -- TTL 后丢弃 --> Sess
end
Sess -- read_from offset --> Cons[stream_pipe_consumer]
Cons -- "GET /v1/stream?conv_id=id&from=N" --> Client
DEL[DELETE /v1/stream?conv_id=id] -- evict_and_cancel --> Sess
图表显示了缓冲区的接触点。实时线路(在正常生成期间流式传输到原始客户端的块)是生产者的默认输出,如上文“生产者侧”所述。
测试
llama-server 包含一个基于 pytest 的自动化测试套件。
该框架会自动启动一个 llama-server 实例,发送请求并验证响应。
有关详细说明,请参阅 测试文档。
工具 API
此端点旨在供 Web UI 内部使用,并且将来可能会更改或删除。
GET /tools
获取工具列表,每个工具有以下字段:
tool(字符串):工具的 ID 名称,用于 POST 调用。示例:read_filedisplay_name(字符串):要在 UI 上显示的名称。示例:Read filetype(字符串):对于内置工具为"builtin",对于由 MCP 服务器暴露的工具为"mcp"permissions(对象):一个字符串到布尔值的映射,指示此工具所需的权限。这对于 UI 在调用工具前询问用户很有用。目前,唯一支持的权限是"write"definition(对象):此工具的 OAI 兼容定义
POST /tools
调用工具,请求体是一个包含以下字段的 JSON 对象:
tool(字符串):工具名称params(对象):从参数名称(字符串)到参数值的映射
返回 JSON 对象。有两种响应格式(MCP 工具也使用这两种格式:它们的结果内容连接到 plain_text_response 中,RPC 或工具错误通过 error 字符串暴露):
格式 1:纯文本。文本将放入名为 plain_text_response 的字段中,示例:
{
"plain_text_response": "这是一段文本响应"
}
客户端应提取此值并将其放入消息内容中(注意:内容不再是 JSON),示例:
{
"role": "tool",
"content": "这是一段文本响应"
}
格式 2:普通 JSON 响应,示例:
{
"error": "无法打开此文件"
}
在格式化为消息内容时需要 JSON.stringify:
{
"role": "tool",
"content": "{\"error\":\"无法打开此文件\"}"
}
在请求体中设置 stream: true 可以流式传输工具的输出,而不是等待其完成。只有某些工具支持此功能(例如 exec_shell_command);如果工具不支持,则返回 404。
响应是 SSE 流,每块一行 data: <json>:
{"chunk": "hello\n"}
随后在工具返回时发送一个最终事件:
{"done": true}
或者,如果 invoke() 抛出异常:
{"done": true, "error": "..."}
没有 [DONE] 标记(与 /chat/completions 不同),流在 done 后结束。
路由器模式:子进程 <--> 路由器如何通信
当使用 subprocess 生成新的子进程时,子进程和路由器都监听 stdout/stderr(合并)。
从子进程到路由器的方向:
- 通用消息是日志,将转发到路由器的 stdout
- 特殊的状态更新消息以
cmd_child_to_router:state:为前缀,后跟一个 JSON。更多信息请参阅server_models::handle_child_state
从路由器到子进程的方向:
- 服务器发送
cmd_router_to_child:exit时,子进程应优雅退出 --> 如果超过DEFAULT_STOP_TIMEOUT子进程仍在运行,则强制终止它。
模型管理 API(路由器模式)
模型管理 API 通过 PR #23976 添加。
此 API 的主要目标是允许从 Web UI 下载模型和/或删除模型。它在底层依赖于模型缓存基础设施来动态管理模型列表。
我们没有从头开始构建一切(就像大多数 AI Agent 在要求实现类似功能时所做的),而是基于代码库中现有的、已经精心设计的组件构建:
- 如上所述,模型缓存基础设施(
common/download.h) - 服务器响应队列(
server-queue.h)。我们使用此功能将事件广播给 SSE 客户端。 - 服务器路由器线程管理(
server-models.h)。我们重用了用于管理子进程生命周期的相同线程模型,但这一次我们不创建新的子进程,而是直接在内部启动下载。
下载新模型的流程:
- POST 请求到来 -->
post_router_models--> 验证 - 将使用特殊的
SERVER_CHILD_MODE_DOWNLOAD生成一个新的llama-server子进程 - 子进程运行下载并通过 stdin/out 向路由器报告状态
- 如果收到停止请求,路由器会要求子进程停止(与在子进程中运行模型的机制相同)
- 否则,完成后,我们调用
load_models()来刷新模型列表
值得关注的关联 PR
- 初始服务器实现:https://github.com/ggml-org/llama.cpp/pull/1443
- 并行解码支持:https://github.com/ggml-org/llama.cpp/pull/3228
- 引入
server_queue和server_response的重构:https://github.com/ggml-org/llama.cpp/pull/5065 - 重排序端点:https://github.com/ggml-org/llama.cpp/pull/9510
- 多模态模型支持(
libmtmd):https://github.com/ggml-org/llama.cpp/pull/12898 - 统一 KV 缓存处理:https://github.com/ggml-org/llama.cpp/pull/16736
- 将 HTTP 逻辑分离到专用文件:https://github.com/ggml-org/llama.cpp/pull/17216
- 大规模代码库拆分为多个小文件:https://github.com/ggml-org/llama.cpp/pull/17362
- 引入路由器模式:https://github.com/ggml-org/llama.cpp/pull/17470
- 推测解码:https://github.com/ggml-org/llama.cpp/pull/17808 以及在 https://github.com/ggml-org/llama.cpp/pull/17808 中的重做
- INI 预设:https://github.com/ggml-org/llama.cpp/pull/17859 (+ 重构:https://github.com/ggml-org/llama.cpp/pull/18169)
- 休眠模式:https://github.com/ggml-org/llama.cpp/pull/18228
- 可恢复流(SSE 重放缓冲区):https://github.com/ggml-org/llama.cpp/pull/23226
Web UI
该项目包含一个基于 Web 的用户界面,用于与 llama-server 交互。它支持单模型(MODEL 模式)和多模型(ROUTER 模式)操作。
基于 SvelteKit 的 Web UI 在此 PR 中引入:https://github.com/ggml-org/llama.cpp/pull/14839
特性
- 聊天界面,支持流式响应
- 多模型支持(ROUTER 模式)- 在模型间切换,选择时自动加载
- 模态验证 - 确保所选模型支持会话的附件(图片、音频)
- 对话管理 - 分支、重新生成、编辑,保留历史记录
- 附件支持 - 图片、音频、PDF(带有视觉/文本回退)
- 可配置参数 - temperature、top_p 等,与服务器默认值同步
- 深色/浅色主题
技术栈
- SvelteKit - 前端框架,使用 Svelte 5 runes 实现响应式状态
- TailwindCSS + shadcn-svelte - 样式和 UI 组件
- Vite - 构建工具
- IndexedDB(Dexie) - 对话的本地存储
- LocalStorage - 用户设置持久化
架构
UI 遵循分层架构:
路由 → 组件 → Hooks → 存储 → 服务 → 存储/API
- 存储 - 响应式状态管理(
chatStore、conversationsStore、modelsStore、serverStore、settingsStore) - 服务 - 无状态的 API/数据库通信(
ChatService、ModelsService、PropsService、DatabaseService) - Hooks - 可复用逻辑(
useModelChangeValidation、useProcessingState)
有关详细的架构图,请参阅 tools/ui/docs/:
high-level-architecture.mmd- 包含所有模块的完整架构high-level-architecture-simplified.mmd- 简化概览data-flow-simplified-model-mode.mmd- 单模型模式的数据流data-flow-simplified-router-mode.mmd- 多模型模式的数据流flows/*.mmd- 每个领域的详细流程(聊天、对话、模型等)
开发
## 确保已安装 Node.js
cd tools/ui
npm i
## 运行开发服务器(带热重载)
npm run dev
## 运行测试
npm run test
## 构建生产包
npm run build
生成 public/index.html 后,按照构建部分所述重新构建 llama-server,以包含更新后的 UI。
注意: Vite 开发服务器会自动将 API 请求代理到 http://localhost:8080。在开发期间,请确保 llama-server 在此端口上运行。
- 原文链接: github.com/ggml-org/llam...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~