LocalAI 构建与测试实战指南:后端 Docker 构建、覆盖率棘轮与 UI 覆盖率门禁
本文基于 LocalAI 仓库中的 .agents/building-and-testing.md 展开,系统讲解如何为指定硬件平台构建 LocalAI 后端镜像(以 Makefile 的 docker-build-* 目标与 .github/backend-matrix.yml 为准),以及如何正确使用仓库的两道覆盖率棘轮:Go 核心代码的严格单调门禁(make test-coverage-check)与 React UI 的带容差 UI 覆盖率门禁(make test-ui-coverage-check)。读完本文,你将能独立完成"按需构建某一后端"的操作,并理解每道覆盖率门禁背后的 ginkgo/Playwright 实现细节、基线再生成规则以及"棘轮只升不降"的团队约定。
总原则:什么时候构建、去哪里找构建方式
LocalAI 的构建与测试成本不低——它由 Go 核心、React UI 和几十个独立后端(C++、Go、Python、Rust)组成,且不同平台(CPU/CUDA/ROCm/SYCL/JetPack/Darwin Metal)走不同构建路径。因此文档给出的首要原则是:除非用户/任务明确要求构建或测试,否则不要尝试构建或测试整个项目,因为所需上下文太多。
当确实需要构建时,查阅顺序为:
- 项目根目录的 Makefile:包含 Docker 内外两种构建入口,是主入口;
- 受你修改影响的各后端目录下的 Makefile(如
backend/<lang>/<name>/Makefile); - .github/workflows 中的 CI 工作流——当不确定某个组件如何构建或测试时,工作流文件就是最可靠的可执行参考。
一个细节约定:主 Makefile 同时提供了在 Docker 内外构建的目标;如果使用者没有明确偏好,应当先询问对方希望用 Docker 构建还是原生构建,而不是擅自选择。
构建指定的后端
从哪里来:docker-build-* 目标是生成的
以"为 ROCm/hipblas 构建 coqui 后端"为例。Makefile 中每个后端都有一个形如 docker-build-coqui 的目标,这些目标并非手写的,而是由 Makefile 中的 generate-docker-build-target 宏根据一组 BACKEND_* 元数据定义批量生成(见 Makefile 中 define generate-docker-build-target 及紧随其后的一长串 $(eval $(call generate-docker-build-target,...)))。元数据格式为:
BACKEND_XXX = 后端名|Dockerfile类型|构建上下文|progress标志|是否需要BACKEND参数
# 例:python 后端
BACKEND_COQUI = coqui|python|.|false|true
docker-build-$(name) 实际展开为一次 docker build,把 BUILD_TYPE、BASE_IMAGE、CUDA_MAJOR_VERSION、CUDA_MINOR_VERSION、UBUNTU_VERSION、UBUNTU_CODENAME、APT_MIRROR 等全部作为 --build-arg 传入,Dockerfile 按类型选择(backend/Dockerfile.python、backend/Dockerfile.golang、各 C++ 引擎专用 Dockerfile),产物镜像统一打为 local-ai-backend:<name>。
注意:最近新增的后端可能需要新增对应的元数据行才能生成目标,这一点在动手前先核对 Makefile 中的 BACKEND_* 列表。
关键构建参数:BUILD_TYPE 与 BASE_IMAGE
构建一个后端变体,最少需要设置 BUILD_TYPE 和 BASE_IMAGE 两个 build-arg。这两个取值的权威参考是 .github/backend-matrix.yml——它是一个纯数据 YAML(不是工作流),罗列了每个后端变体的 build-type、base-image、platforms、cuda-major/minor-version、runs-on 等字段,供 scripts/changed-backends.js 消费后分别生成 backend.yml 与 backend_pr.yml 两个 CI 工作流的矩阵。从源码结构看,这个矩阵文件还承担了"某后端发布到哪些操作系统"的源真相角色:文件头注释明确写着一个后端只会在它出现的矩阵中构建发布(Linux 走 include:,macOS 走 includeDarwin:),新增后端时若遗漏 Darwin 条目,该后端会在 macOS 上静默不可用。
几个必须掌握的取值规则:
l4t和cublas变体:除BUILD_TYPE/BASE_IMAGE外还必须提供 CUDA 大版本号与小版本号(Makefile 中对应的变量是CUDA_MAJOR_VERSION,默认13,与CUDA_MINOR_VERSION,默认0)。llama-cpp/ik-llama-cpp/turboquant:矩阵中这些条目额外设置builder-base-image,指向预构建的quay.io/go-skynet/ci-cache:base-grpc-*缓存镜像。而本地直接执行make backends/<name>时默认走BUILDER_TARGET=builder-fromsource,不需要该参数——其 Dockerfile 的 from-source 阶段会自行安装全部依赖。- 平台别名与歧义确认:用户口中的"AMD"可能指 ROCm(hipblas),"Intel"可能指 SYCL,"NVIDIA"可能指
l4t(JetPack)或cublas。遇到这类歧义应当先确认,不要猜。
完整命令示例
文档给出的 coqui + hipblas 构建命令(格式化展示):
DOCKER_MAKEFLAGS=-j$(nproc --ignore=1) \
BUILD_TYPE=hipblas \
BASE_IMAGE=rocm/dev-ubuntu-24.04:7.2.1 \
make docker-build-coqui
其中 DOCKER_MAKEFLAGS 会透传为容器内 make 的 --build-arg MAKEFLAGS,用于并行编译。
两条执行纪律(针对 Agent 与开发者同样重要):
- 除非用户明确要求执行,否则只打印命令。后端构建是长任务,输出可能溢出上下文,且并非所有 Agent 前端都擅长处理长时运行任务。
- 有时需要在
docker build上追加额外参数,例如跨平台构建的--platform或查看完整日志的--progress=plain。此时可以绕过 make 目标,直接生成等价的docker build命令(参考docker-build-backend宏展开出的 build-arg 集合)。
测试基座:make test 与 mock backend
覆盖率门禁与 make test 共享同一套前置条件,值得先交代一下。make test 依赖 prepare-test 目标,后者先做 protogen-go(用仓库内 protoc 从 backend/backend.proto 生成 gRPC 代码),再执行 build-mock-backend——把 tests/e2e/mock-backend 编译成二进制。core/http/app_test.go 驱动的就是这个 mock backend,因此默认测试不再下载多 GB 的 GGUF/whisper 模型,也不构建 llama-cpp 等真实后端;真实后端推理测试被移入 tests/e2e-backends/(按后端路径过滤)和 tests/e2e-aio/(夜间任务)。测试运行器是 ginkgo,默认带 --flake-attempts $(TEST_FLAKES)(TEST_FLAKES 默认 5 次)与 --fail-fast,构建标签为 debug。
Go 覆盖率棘轮:严格且单调递增
核心 Go 代码(./pkg、./core,外加进程内集成套件 ./tests/e2e)受一道**严格、单调递增的覆盖率棘轮(ratchet)**保护,由 Makefile 中一组专门的变量与目标驱动:
make test-coverage:以covermode=atomic插桩运行各套件,把合并后的 profile 写入coverage/coverage.out;前置依赖与make test相同。Makefile 中还特意不带--fail-fast,这样单个失败不会截断覆盖率统计。make test-coverage-check:先跑test-coverage,再由 scripts/coverage-check.sh 判断总覆盖率是否低于已提交的基线 coverage-baseline.txt(当前基线值为54.2,即 54.2%)。Linux CI 中 .github/workflows/test.yml 执行的就是make test-coverage-check而非make test。make test-coverage-baseline:用当前运行结果重新生成并覆盖coverage-baseline.txt。
--coverpkg:把集成测试的功劳记到被测包头上
这是整套机制里最关键的技巧。Makefile 中定义了:
COVERAGE_COVERPKG?=github.com/mudler/LocalAI/core/...,github.com/mudler/LocalAI/pkg/...
通过 --coverpkg,覆盖率被归属到 core + pkg 的全部包上,而不只是"当前测试包"。这才有意义地把进程内的 tests/e2e 集成套件折叠进来——该套件通过 application.New 启动真实 HTTP 服务器、经 loopback 驱动 core/http/endpoints/... 的各处理器。没有 --coverpkg 时,这些测试只会给 e2e 测试包自己记账;有了它,endpoint 覆盖率几乎翻倍(文档给出的实例:endpoints/openai 从 13.6% 提升到 52%)。
代价是:分母变成了 core + pkg 的全部语句(生成代码除外,见下),因此这个数字不能与普通按包的覆盖率数字直接比较。
哪些集成套件被折叠进来
COVERAGE_E2E_ROOTS?=./tests/e2e:非递归运行(因此排除需要容器的tests/e2e/distributed),并带--label-filter=!real-models(这些 spec 需要下载真实模型),针对prepare-test构建的 mock backend 执行。tests/integration被刻意排除:它依赖make backends/local-store(需要真实构建 local-store 后端),而覆盖率 CI 任务并不构建它。
Flake 注意点:把集成测试折叠进"严格"门禁意味着一次硬性的 e2e 失败(或某个 spec 悄悄停止运行)都会让覆盖率门禁变红,而不仅仅是测试本身。--flake-attempts 吸收可重试的瞬时失败;covermode=atomic 则保证行覆盖本身是确定性的,不受测试顺序与重试抖动影响。
为什么每个 root 单独跑一次 ginkgo(不要"简化"掉)
合并逻辑在 scripts/run-coverage.sh 中,脚本头部的注释把原因写得非常清楚:给同一个 ginkgo 调用传多个递归 root(例如 ginkgo -r ./pkg ./core)时,只有一个 root 的 coverprofile 会被写进 --output-dir/--coverprofile,其余被静默丢弃。这一点在 ginkgo 2.29.0 上被实际验证:-r ./pkg ./core 只产生 ./pkg 的覆盖数据,而单独 -r ./core 则能覆盖全部 34 个 core 包。
因此脚本的合并策略是:
- 每个单元 root(
./pkg、./core)单独递归运行一次 ginkgo,profile 落为cover-<root>.out; - 每个 e2e root 非递归运行一次(可带 label filter);
- 用 glob 收集全部 per-root profile,用 awk 按语句块求和命中次数(不是简单拼接——因为
--coverpkg会让同一个包出现在多个 root 的 profile 中,直接拼接会重复计块;对covermode=atomic而言按块求和是合法的),并按COVERAGE_EXCLUDE_RE(默认grpc/proto/.*[.]pb[.]go)丢弃生成的 protobuf,最后输出统一的mode: atomic头。
文档特别警告:不要把它"简化"回单次多 root 调用——此前正是这样,core/(包含全部 core/http,约 7.4k 条语句)一度从统计数字中静默消失。
构建标签:auth 不能少
COVERAGE_TAGS?=debug auth 经 GINKGO_TAGS 传给 ginkgo 的 --tags。其中 auth 标签是必需的:只有带它编译,真正的(sqlite 后端的)认证实现及其约 150 个 //go:build auth 测试才会进入构建与执行;不带该标签时这些文件根本不编译、测试不运行,门禁会拿一个 stub 给 auth 打分(约 3.7% 而非约 38%)。推论:新增带 build tag 的测试时,必须同时扩展 COVERAGE_TAGS,否则它们既不计入覆盖率,大概率也不会进 CI 运行。
React UI 覆盖率:唯一有测试的 UI 路径
React UI(core/http/react-ui)没有任何组件级/单元测试,它唯一的测试就是 Playwright e2e spec,运行在 tests/e2e-ui/ 的 ui-test-server 之上(该 server 由 tests/e2e-ui/main.go 编译而来)。这些 spec 确实驱动真实 UI——点击、fill、setInputFiles、getByRole/getByText、可见性与取值断言。由于 dist 是 //go:embed 进 ui-test-server 的,每次覆盖率运行都会重新编译 server。
make test-ui-coverage 的流程(见 Makefile 中 test-ui-coverage 目标):
- 构建带插桩的 bundle(
COVERAGE=true,经由vite-plugin-istanbul且forceBuildInstrument: true——否则该插件会跳过生产构建的插桩;Makefile 注释说明该目标还有一条更新的 V8 原生覆盖路径bun run build:coverage-v8+PW_V8_COVERAGE=1,端到端约快 40%,旧的 istanbul 路径仍然保留); - 重新嵌入并编译 ui-test-server;
- 运行 Playwright spec,把
nyc报告写入core/http/react-ui/coverage/; - spec 统一从
core/http/react-ui/e2e/coverage-fixtures.js导入{ test, expect }——它只是 re-export Playwright 的 API,并在每个测试结束后收割window.__coverage__到.nyc_output/。插桩默认关闭(仅COVERAGE=true时开启),因此日常开发/生产构建和普通make test-ui-e2e完全不受影响(fixture 在window.__coverage__不存在时自动空转)。
浏览器与基线规则
- 浏览器:flake 开发 shell 自带
chromium并导出PLAYWRIGHT_CHROMIUM_PATH,playwright.config.js通过launchOptions.executablePath使用它,Makefile 在此变量已设置时跳过playwright install。这是为了绕开 Playwright 自带浏览器在 NixOS 上无法解析系统库(libglib-2.0等)的问题。CI 中没有该变量时,Makefile 回退为playwright install --with-deps chromium。 - SPA 特性:应用是 React SPA,覆盖数据在应用内导航时跨路由累加;一旦发生整页
page.goto/刷新即被重置。 - 全量分母:core/http/react-ui/.nycrc.json 使用
all: true,因此每个src/**文件都出现在报告中,包括 0% 覆盖的文件——这正是发现"完全没有测试的功能"的手段(把 HTML 报告或coverage-summary.json按行覆盖率升序排序即可)。 - 门禁:
make test-ui-coverage-check运行套件后调用 scripts/ui-coverage-check.sh,若总行覆盖率低于 core/http/react-ui/coverage-baseline.txt 超过UI_COVERAGE_TOLERANCE(默认 0.8 个百分点)则失败;make test-ui-coverage-baseline重新生成基线。CI 中由 .github/workflows/tests-ui-e2e.yml 执行make test-ui-coverage-check。
为什么 UI 门禁有容差,Go 门禁没有
这是文档中最值得内化的一条工程判断。UI e2e 覆盖率本质上不确定:大量 spec 在异步/懒加载渲染仍在进行时就已断言结束,那些行只有在"渲染跑赢覆盖率收尾"时才被采集,于是总覆盖率会随机器速度/负载漂移——而且漂移分散在几十个 spec 中(脚本注释中给出了观测值:同一棵代码树,安静的本机约 39.9%,较慢/高负载的 CI 运行器约 39.0%)。容差的作用就是吸收这种漂移,所以基线必须设在慢速 CI 的下限之下,绝不能取快速本机跑出的高点,否则 CI 会反复抖动(脚本注释还提到容差曾被临时收紧到 0.1pp 校准到一次幸运的快速本机运行,结果就是 CI 抖动)。
提升 UI 覆盖率是最便宜的:
- 写一个 render-smoke spec——导航到某路由、断言其 header 渲染出来,就会挂载懒加载页面并执行完整渲染与初始 effects,几行测试就能捕获该页面大部分覆盖行(参考 core/http/react-ui/e2e/page-render-smoke.spec.js)。测试 server 中认证是关闭的(
isAdmin=true),所以RequireAdmin/RequireFeature路由无需 mock 即可渲染。 - 最确定性的收益是消除竞态:让 spec 在结束前
await某个已渲染元素(例如 core/http/react-ui/e2e/agents.spec.js 中的 AgentCreate),这样对应行每次运行都被计数。
两道门禁的共同规则
文档最后给出两条对所有维护者(尤其是 Agent)都适用的硬性规则:
- 不要弱化门禁:永远不要为了把红色门禁变绿而手动调低基线或放宽容差。棘轮只能向上移动。
- 覆盖率下降时加测试,而不是改基线:按行覆盖率升序排列
coverage-summary.json找到未测试代码,补测试。当覆盖率确实(且可复现地)上升时,再提交重新生成的基线(make test-coverage-baseline/make test-ui-coverage-baseline)。 - 两个门禁的强度差异是刻意设计:Go 门禁严格、无容差(
covermode=atomic保证了确定性);UI 门禁保留小容差,仅因为 e2e 覆盖率天然不确定。
小结
LocalAI 的构建测试体系可以概括为三层:以 Makefile 生成式 docker-build-* 目标 + .github/backend-matrix.yml 矩阵数据为准的按需后端构建;以 mock backend 为基座、--coverpkg 跨包记账 + 每 root 单跑 ginkgo 的 Go 严格覆盖率棘轮;以及以 Playwright 全量驱动真实 UI、带 0.8pp 容差的 UI 覆盖率棘轮。三者的共同设计哲学是"棘轮只升不降、门禁失败要能定位到具体代码",所有细节都能在 Makefile、scripts/run-coverage.sh、scripts/coverage-check.sh 与 scripts/ui-coverage-check.sh 中逐行核对。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00