数据截至 (上游 commit 21b29048d7bc)
服务骨架:从 CLI 到 axum 路由装配
30 秒导读: 本章讲 Tabby 这个进程"怎么起来、请求怎么被路由"——从命令行
tabby serve一路追到 axum 的路由树装配好、开始监听端口 。不讲补全算法、不讲索引检索、不讲推理后端, 只讲骨架:进程启动的主线,和各个服务(embedding / code / docsearch / completion / chat / webserver)按什么顺序被创建、注入到哪些路由上。
这是 Tabby 系列的第 1 章。全景与阅读地图见 index.md;本章后面各机制的深入见 02 补全、03 检索索引、 04 推理后端、05 企业 webserver。
1. 这是什么(零基础也能懂)
一句话定义: Tabby 是一个自托管的 AI 代码补全服务器——你在自己机器上跑起一个 HTTP 服务, IDE 插件(VSCode / Vim / IntelliJ)连上它,就能在编辑器里获得代码补全和对话。
这一章聚焦"骨架"。 一个服务器进程从被敲下命令到能接请求,中间要走一串固定动作:
- 解析命令行参数(要跑哪个模型?监听哪个端口?用 CPU 还是 GPU?)
- 读配置文件、把命令行参数合并进去
- 按需把各种"服务"造出来(补全服务、对话服务、代码检索……)
- 把这些服务挂到一条条 HTTP 路由上,拼成一棵路由树
- 绑定端口,开始监听
用起来什么样: 最小的一条启动命令——
# 用 CPU 跑一个补全模型,监听默认 8080 端口
tabby serve --model TabbyML/StarCoder-1B --device cpu
进程起来后会打印一个 ASCII 大字 logo 和监听地址(见 crates/tabby/src/routes/mod.rs:32-44
里那段 println!),然后 IDE 插件就能 POST /v1/completions 拿补全了。
一句话直觉: 把 tabby serve 想成"装配一台流水线"——先备齐零件(各服务),
再把零件按顺序接到传送带(axum Router)上;传送带的每个工位就是一条 HTTP 路由。
本章讲的就是这条装配线怎么搭,不讲每个工位内部的机器怎么运转(那是后面几章)。
本节不出现底层代码细节。目标:完全没接触过 Tabby 的人读完知道"这个进程是干嘛的、本章讲它的哪一段"。
2. 顶层全景(它大概怎么转)
2.1 一张图:main 到监听的主线
先看整条启动主线。怎么读这张图:从上到下是时间顺序,每一步产出下一步要用的东西。
tabby 二进制
│
┌────▼──────────────────────────┐
│ main() main.rs:54 │ clap 解析 CLI → Commands::Serve / Download
│ · 装 color_eyre / tracing │ 加载全局 Config、建 ~/.tabby 根目录
└────┬──────────────────────────┘
│ Commands::Serve(args)
┌────▼──────────────────────────┐
│ serve::main() serve.rs:117 │ ← 本章主战场
└────┬──────────────────────────┘
│
① merge_args 把 --model/--chat-model 合进 Config
② load_model 本地模型?没下就下载
③ 造服务(按依赖顺序,见 2.3)
④ api_router 把服务挂成一堆小 Router → merge 成 api
⑤ ApiDoc + SwaggerUi 拼出 ui(OpenAPI 文档 + /swagger-ui)
⑥ [EE] ws.attach 企业版把 GraphQL / 后台任务 / 前端 UI 挂到 api、ui 上
⑦ run_app 加 CORS/Prometheus/metrics,绑端口,axum::serve 开始监听
2.2 部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
Cli / Commands | clap 定义的命令行结构,分 Serve / Download 两个子命令 | crates/tabby/src/main.rs:17,27 |
Config | 全局配置(仓库、模型、补全、embedding、answer 等),从 ~/.tabby/config.toml 读 | crates/tabby-common/src/config.rs:18 |
ServeArgs | serve 子命令的参数(model / port / device / parallelism…) | crates/tabby/src/serve.rs:84 |
serve::main | 服务装配总指挥:合并配置→造服务→装路由→起监听 | crates/tabby/src/serve.rs:117 |
api_router | 把各服务 with_state 到具体 HTTP 路由,合并成一棵 api 树 | crates/tabby/src/serve.rs:251 |
run_app | 收尾层:CORS、Prometheus、/metrics,axum::serve 监听 | crates/tabby/src/routes/mod.rs:14 |
ApiDoc / SwaggerUi | utoipa 生成 OpenAPI JSON,Swagger UI 挂在 /swagger-ui | crates/tabby/src/serve.rs:40-81、:205-207 |
Webserver::attach(EE) | 企业版把 GraphQL / hub / OAuth / 前端 UI 挂到 api、ui 上 | ee/tabby-webserver/src/webserver.rs:59 |
2.3 主线走一遍(高层,不进代码)
serve::main(serve.rs:117-233)是本章核心。它按这个顺序把服务逐个造出来、串起来——
顺序不是随意的,后面的服务依赖前面的产物:
- 合并配置
merge_args:命令行--model覆盖进Config(serve.rs:397)。 - 备模型
load_model:本地模型没下载就先下(serve.rs:235)。 - embedding:只有开了环境变量
TABBY_EMBEDDING_ENABLED=yes才造(serve.rs:132)。 它是很多东西的地基——代码检索、文档检索都要嵌入向量。 - webserver(EE):若启用,
Webserver::new打开 SQLite、准备 logger(serve.rs:139)。 - logger:默认写本地事件日志;EE 下换成 webserver 的 composed logger(
serve.rs:147-152)。 - index_reader_provider → docsearch / code:都建立在 embedding 之上,embedding 没有就全是
None(serve.rs:154-174)。 - completion + chat:
create_completion_service_and_chat造补全服务和对话流(serve.rs:177)。 - api_router:把上面这些
Option<服务>挂成路由(serve.rs:192)。 - OpenAPI/UI + EE attach + run_app:拼文档、企业版加挂、起监听(
serve.rs:202-232)。
关键心智:几乎每个服务都是 Option。 没配对应模型 / 没开 embedding,就是 None,
对应路由要么不挂、要么挂一个直接返回 501 NOT_IMPLEMENTED 的桩。这是理解装配逻辑的主线索。
目标:看懂"大盘"——谁先造、谁依赖谁、谁可选。下面逐段拆。
3. 核心原理(逐段拆装配线)
3.1 CLI:两个子命令,一个 fatal 宏
要解决的小问题: 一个二进制既要能"起服务",又要能"单独下模型",还要能优雅退出。
结构。 clap 的派生宏定义了顶层 Cli 和 Commands 两个子命令:
| 子命令 | 作用 | 参数结构 |
|---|---|---|
Serve | 起 IDE/编辑器用的 API 服务 | ServeArgs(serve.rs:84) |
Download | 只下载模型,不起服务 | DownloadArgs(download.rs) |
main(main.rs:54-74)是全流程入口,做四件事:装 color_eyre 错误报告、初始化 tracing、
Config::load() 读配置、create_dir_all 建 ~/.tabby 根目录(Unix 下还 chmod 0700 锁权限),
最后 match 分发到 serve::main 或 download::main。
Device 枚举(main.rs:36-51)列出五种推理设备:Cpu / Cuda / Rocm / Metal / Vulkan,
用 strum::Display 把它们序列化成 "cpu" 这类小写字符串,后面拼推理参数时会用到。
fatal! 宏(main.rs:76-91)是全项目统一的"打日志然后 exit(1)":
// crates/tabby/src/main.rs:77 —— 记一条 error 级日志后直接终止进程
macro_rules! fatal {
($msg:expr) => {{ tracing::error!($msg); std::process::exit(1); }};
// 还有一个带格式化参数的分支
}
它在 run_app 绑定/监听失败时兜底(routes/mod.rs:52),保证致命错误不被静默吞掉。
to_local_config(main.rs:93-107)把"命令行传的一个模型名"转成 ModelConfig::Local。
这里藏着两个环境变量约定:非 CPU 设备时读 LLAMA_CPP_N_GPU_LAYERS(默认 9999,即"能上 GPU 的层全上"),
以及 LLAMA_CPP_FAST_ATTENTION 开关。这段把 CLI 与推理后端的参数约定连了起来。