llama-server 开发文档

ggml-org 发布于 2026-07-28 阅读 34

本文是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_routesserver_context 和 HTTP 接口之间的中间件层;处理 JSON 解析/格式化和请求路由逻辑。
  • server_http_context:使用 cpp-httplib 实现 HTTP 服务器。
  • server_queue:HTTP 工作线程用来向 server_context 提交新任务的线程安全队列。
  • server_responseserver_context 用来向 HTTP 工作线程返回结果的线程安全队列。
  • server_response_reader:对上述两个队列的更高级封装,使代码更清晰。
  • server_task:推送到 server_queue 的工作单元。
  • server_task_result:推送到 server_response 的结果单元。
  • server_tokens:Token 序列的统一表示(支持文本和多模态 Token);由 server_taskserver_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_responsesend_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_managerserver-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_resset_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_file
  • display_name (字符串):要在 UI 上显示的名称。示例:Read file
  • type (字符串):对于内置工具为 "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_queueserver_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
  • 存储 - 响应式状态管理(chatStoreconversationsStoremodelsStoreserverStoresettingsStore
  • 服务 - 无状态的 API/数据库通信(ChatServiceModelsServicePropsServiceDatabaseService
  • Hooks - 可复用逻辑(useModelChangeValidationuseProcessingState

有关详细的架构图,请参阅 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论