使用 Docker 构建可落地运行的 AI 系统——理解 Docker 中的 AI 模型

0 阅读20分钟

本章是 “Docker for AI” 从运行 Python 的 containers,转向将 models 视为 shipping artifacts 的地方。

Containers 执行 code。Models 则更像你要交付的 versioned dependencies:体积庞大、对硬件敏感,而且很容易在不经意间被改变。如果你不能控制这个 surface,就会产生 silent drift——相同的 image,却得到不同的 answers。

Docker Model Runner(DMR)以及 Docker 的 Compose model support,将这种 artifact shift 形式化。DMR 负责 pulling 和 running models,而 Compose 允许你在 compose file 中使用 models: field,将 models 声明并连接到 services 中。你从 registry 中 pull models,将它们 pin 到 tags(或 digests),然后让 platform 暴露一个稳定的 OpenAI-compatible endpoint,供你的 services 调用。

在本章中,你将学习如何把 AI models 当作 versioned artifacts,而不是 loose files 来处理,这样你的 team 就可以在 development、staging 和 production 中交付完全相同的 model,而不会出现意外。你会看到 Docker 如何通过 registries 将 models 打包并分发为 OCI artifacts,使用你已经熟悉的、与 container images 相同的 pull 和 tag workflows。

我们还会介绍如何读取 model tags,并为你的硬件选择合适的 variant;GGUF 是什么,以及为什么它已经成为 efficient local inference 的标准格式;以及如何通过在 memory footprint、throughput 和 quality 之间做平衡,来理解 Q4、Q8 和 FP16 等 quantization levels。在这个过程中,你会学习如何在 Docker Compose 中使用 models: field 声明 models,如何将 services 显式绑定到这些 models,使 Docker 能够处理 provisioning 和 lifecycle management;最后,你会学习如何使用 Docker Model Runner 在本地运行和管理 models。

本章将覆盖以下主要主题:

  • 为什么 AI workloads 需要 containers?
  • OCI artifacts 与 AI model distribution
  • GGUF 与 quantization
  • 面向 multi-service AI applications 的 Docker Compose
  • 管理 large model files 和 storage
  • Configuration 和 environment management

到本章结束时,你会建立一个清晰的 mental model,用 structured、repeatable 的方式管理 AI models,使你的 workflows 更 reliable、portable,并且更加 production-ready。

Technical requirements

在继续学习本章之前,请确保满足以下 prerequisites:

Software:

  • Docker Desktop 4.40+(macOS、Windows 10/11、Linux)
  • Docker Compose v2.38+(Compose models feature 所必需)

Features:

  • Docker Desktop 中已启用 Docker Model Runner(Settings → Features in development → Model Runner)

Environment:

  • Docker Desktop 已安装并正在运行(来自第 1 章)

Why containers for AI workloads?

在正式深入之前,先说明你将在本节中实际练习什么:你将学习如何诊断 model-deployment drift,并像解释 engineering incident 一样解释它;如何使用 practical trade-off framework,在 local 与 cloud 之间做清晰决策;以及如何将 Docker Desktop 与 Docker Model Runner(DMR)定位为你的 local model infrastructure。

如果你已经使用 Docker 部署过 services,就已经熟悉它的核心卖点:build once, run anywhere。对于 AI workloads 来说,这个卖点相同,但 failure modes 不同。Model 并不是 “just another dependency”;它是定义 product behavior 的东西。

Model drift is not only about code

既然我们已经建立了一个前提:models 定义 behavior,而不仅仅是支撑 behavior 的 dependency,那么就值得看看实践中到底哪里会出问题。大多数问题并不来自你的 container image,而来自 model 本身的细微 mismatch。

image.png

图 2.1:为什么 LLM model drift 会发生

大多数 teams 会在三个地方注意到 reproducibility 被破坏:

Model tag drift:有人 pull 了 :latest;今天的 :latest 指向了不同 artifact。

Quantization mismatch:Model “name” 可能相同,但一台机器运行 Q4,另一台使用 FP16,这通常是因为 model files 或 runtime settings 不同。这意味着不同的 memory usage 和 output quality。

Context window mismatch:Prompts 在 development 中可以工作,但在 staging 中失败,因为 context_size 不同,或被 flags 覆盖了。

即使你交付的是同一个 container image,如果 model artifact(以及它的 runtime knobs)没有被同样 pin 和 configure,你仍然可能得到不同 answers。

Local vs cloud: Decide with a clean frame

一旦你理解 model configuration 是 drift 的主要来源,下一个实际问题就是:这个 model 到底应该在哪里运行?Local 还是 cloud?答案并不是意识形态式的,而是 trade-offs。

Local inference 并不是免费的。你要为 hardware、storage,以及保持 dev machines 稳定所需的 operational tax 付出成本。Cloud APIs 也并不简单。你会承担 latency variance、data governance,以及随着每个 test suite 和每个 developer 增长而膨胀的 usage bill。

image.png

图 2.2:运行 LLM Model 的 Local vs Cloud 决策框架

一个 practical 的决策方式是区分你要优化什么:

When local wins

  • Fast iteration loops(prompt + code changes),不需要 network round-trip。
  • Data control:sensitive prompts、internal docs 或 regulated inputs。
  • Development 阶段 predictable latency(没有 shared API throttling)。

When cloud wins

  • 你需要 load 下的一致 throughput、autoscaling,或者你自己没有的 GPU capacity。
  • 你要面向无法本地运行 inference 的 users 交付,例如 mobile、low-power edge。
  • 你希望 managed upgrades,不想自己 operate model runtime。

大多数 teams 最终会采用 hybrid:local models 用于 development 和 staging,managed endpoints 用于 production,但保持相同的 model tag discipline,使 behavior 可以比较。

Docker Offload(第 4 章)非常自然地嵌入这种 hybrid story,因为它承认一个简单现实:并不是每台 developer machine 都适合很好地运行 large models。Offload 不是强迫每个人都配备 powerful local hardware,而是让 Docker Desktop 在保持 local workflow 不变的情况下,把 heavy lifting 转移到 remote、managed environment。

从 developer 的视角看,你仍然是在 pull 一个 image、run 一个 container,并与 local endpoint 通信;区别只是当 resources 受限时,execution 会发生在其他地方。这减少了 local GPU contention,也让 development 能继续推进,而不需要在 application 中引入一个单独的 “cloud mode”。

Where Docker Desktop and DMR fit

到目前为止,我们已经看过问题本身,也就是 model drift,以及 deployment decision,也就是 local vs cloud。下一步是理解 Docker 如何真正帮助你标准化所有这些内容,而这正是 Docker Desktop 和 DMR 登场的地方。

DMR 的价值并不在于它可以 run 一个 model;而在于它让 models 看起来像 registry artifacts。你可以 pull、cache、version,并通过 OpenAI-compatible endpoint 暴露它们。这是一种 integration contract,使你的 app 可以在 models 演进时保持稳定。

为了让这一点更具体,下面的图展示了 models 如何在 Docker Desktop 中被 surfaced 和 managed。

image.png

图 2.3:Docker Desktop Models catalog

在底层,workflow 映射的是你已经在 images 上做的事情:从 registry pull,pin 一个 tag,并将 updates 视为 deliberate changes。

现在你已经看到,即使 containers 保持不变,model drift 和 environment differences 仍然可能破坏 reproducibility。自然产生的下一个问题是:如何标准化 model distribution,使同一个 artifact 能可靠地出现在所有环境中?这就是 Open Container Initiative(OCI)artifacts 发挥作用的地方。

OCI artifacts and AI model distribution

本节你将做这些事情:学习如何解释 OCI artifacts,以及为什么它们让 model distribution 变得 registry-native;学习如何像 operator 一样浏览 Docker Hub,并解读 model cards 和 tags;同时通过使用 explicit tags(必要时使用 digests)来应用 versioning discipline,以确保 reproducibility。

Traditional container images 打包的是 executable filesystem:binaries、libraries、config 和 entrypoints。Model artifact 则不同:大部分 bytes 是 weights,外加描述如何运行它们的 metadata。Docker 的 approach 是为 models 复用 OCI distribution,也就是 registries、tags 和 promotion,而不是发明一套平行的 model-package ecosystem。

OCI artifacts

现在我们已经明确:models 需要与 container images 一样的 discipline。接下来看看让这件事成为可能的机制:OCI artifacts。

OCI artifacts 是存储在 OCI registry 中的 “non-image” content。Mechanics 相同:tags、digests、access control、replication,以及跨 environments 的 promotion。Payload 不同:model weights、GGUF metadata,以及 runtime hints。

从 operational 角度看,这非常重要,因为它给你提供了一条 standard supply chain——你已经信任的、用于 images 的 registry workflows,现在也同样适用于 models。

How to read model cards like an operator

一旦 models 像 registry artifacts 一样被存储和分发,下一项技能就是知道如何正确评估它们。这就是 model cards 发挥作用的地方。不过,你需要以不同于预期的方式阅读它们。

Model cards 不是 marketing pages。你应该像读 runbook 一样读它们:

  • Hardware requirements(RAM / VRAM),以及 CPU-only inference 是否现实。
  • Supported context length,以及任何 known constraints。
  • License 和 usage restrictions(你不想在 shipping 之后才发现这些问题)。
  • Tag naming conventions(parameters + quantization + fine-tune hints)。

Tags vs digests: What teams should pin

到这里,你已经知道 models 如何存储,以及如何评估它们。下一步是控制你的 system 到底运行哪个 exact version。这就是 tags 和 digests 发挥作用的地方。

Tags 是 human-friendly 的;digests 是 immutable 的。对大多数 teams 来说,只要不使用 :latest,explicit tags 对 dev / staging 已经足够。如果你需要 hard guarantees,例如 audits 或 regulated environments,那么除了 tags,还应该使用 pin-by-digest。

Discipline 与 images 完全一样:把 “model version” 视为 code review surface。

Promotion workflow: From local pull to shared artifact

一旦你选择并 pin 了正确的 model,下一步就是让它成为 team workflow 的一部分,确保每个人都使用相同的 approved artifact。

DMR 支持一种熟悉的 registry flow:

Pull 一个 model from Docker Hub:

docker model pull smollm2:360M-Q4_K_M

Retag 到你的 org namespace 下(optional):

docker model tag smollm2:360M-Q4_K_M registry.example.com/myteam/smollm2:360M-Q4_K_M

Push 到你的 registry:

docker model push registry.example.com/myteam/smollm2:360M-Q4_K_M

重点并不是 command syntax;重点是 workflow:你的 team 可以控制哪些 model artifacts 存在于自己的 registry 中,以及哪些 artifacts 被批准用于 staging 或 production。

Mini exercise: Pick a model tag and justify it

为了让这一点更具体,花一点时间应用刚才学到的内容。

选择一个你可能用于 local development 的 model。选择两个 tags,例如 Q4 和 Q8,并写下 5 行 justification,说明你会把哪一个标准化为 team default,以及原因是什么,包括 RAM、speed、quality 和 edge feasibility。

在本节中,你已经看到 models 如何使用 OCI artifacts 被打包和分发,以及如何正确选择和 version 它们。现在我们向下深入一层:这些 model files 中到底有什么,它们如何在真实 hardware 上 runnable?这就是 GGUF 和 quantization 要解决的问题。

GGUF and quantization

在深入之前,先说明你将在本节中练习什么:你将学习如何解释 GGUF 是什么,以及为什么它主导了 local inference workflows;如何根据 hardware 和 quality needs 选择 Q4、Q8 和 FP16 等 quantization levels;以及如何用适合 ADR 或 PR 的方式,清晰记录这些 trade-offs。

如果只记住一件事,那就是:local inference 通常受 memory(RAM / VRAM)和 bandwidth 限制,而不是受 “raw compute” 限制。GPT-Generated Unified Format(GGUF)和 quantization 存在的原因,就是移动 full-precision weights 成本太高。

GGUF: One file that actually matters

GGUF 是一种 single-file format,被 llama.cpp-style runtimes 广泛使用。它将 tensors 和 metadata 存储在一起。这些 metadata 在 operational 层面非常有用:tokenizer details、architecture hints、context length,以及其他会影响 behavior 的 signals。

为了理解 GGUF 为什么如此 practical,需要看到它如何将 runtime 所需的一切打包进一个 self-contained file 中。

image.png

图 2.4:GGUF file layout(metadata 和 tensor table)

理解 file format 之后,下一个问题就是:如何让这些 models 小到能够真正运行?

Quantization: The real trade space

Quantization 会降低 precision,从而缩小 model footprint。收益是 models 可以在 commodity hardware 上运行。代价是可能出现 quality regression,有时 performance characteristics 也会不同。

Quantization 用图来理解更容易。下图展示了降低 numeric precision 如何直接转化为更小的 model size:

image.png

图 2.5:Quantization 压缩 numeric precision(示例:FP32 → INT4)

那么这在实践中意味着什么?你实际上是在选择该 model 的一个 specific representation,使其符合你的 constraints。同一个 base model 会因为 quantization 方式不同而表现很不一样,这也是为什么 teams 会标准化到 specific variants,而不是让每个 developer 自行决定。下表给出一种 practical 的思考方式。

VariantTypical useHardware fitTrade-off
Q4(例如 Q4_K_M)Fast iteration、dev laptops、edge tests8–16 GB RAM(通常 CPU-only)有一定 quality loss;最适合作为 “learning” default
Q8在减小 size 的同时获得更好 quality16–32 GB RAM;CPU ok,GPU optional比 Q4 更大,有时也更慢
FP16 / BF16Max quality、GPU inference、production benchmarks对大多数 models 来说需要 GPU VRAM体积大、移动和 cache 成本高

表 2.1:Quantization levels 的 practical comparison,展示 Q4、Q8 和 FP16 variants 在 model size、hardware requirements 和 quality trade-offs 上的差异

Context size isn't free

很容易只关注 model size,但还有另一个 hidden lever 会影响 memory 和 performance:context size。

Context length 不只是 app concern。增加 context_size 会改变 memory use 和 latency。在 local setups 中,KV cache 可能在你甚至还没触及 model weights 之前,就成为真正的 memory bottleneck。

Operational takeaway:把 context_size 当作 capacity knob。明确设置它、记录它,不要让它在 environments 之间漂移。

因此,当你真正选择 model variant 时,不应该凭感觉猜,而应该遵循一条简单 decision path。

当你选择 model tag 时,按以下顺序走 checklist:

  1. Target machine:RAM / VRAM,以及是否期望 CPU-only inference。
  2. Latency budget:Interactive chat 还是 background batch jobs。
  3. Context requirements:Prompts、retrieved context 和 tool outputs 会快速累积。
  4. Quality sensitivity:User-facing answers 还是 dev-only scaffolding。

Comparison exercise: 8 GB laptop vs 32 GB workstation

为了让这个问题更 practical,把它放在一个你很可能面对的真实场景中。选择同一个 base model,并比较两个 quantizations。写下:

  • download size;
  • expected peak RAM / VRAM;
  • 在较小机器上最先出问题的会是什么。

这正是当 team 提议更换 model tags 时,你希望出现在 PR 中的 note。

在本节中,你学习了 GGUF 如何通过将 models 打包成 single、metadata-rich file,使 local inference 变得 practical;也学习了 quantization 如何减少 model size,以适配真实 hardware constraints。你还探索了 precision、context size 和 memory 如何相互影响,以及在为不同 environments 选择 model variants 时,如何做 structured decisions。

Docker Compose for multi-service AI applications

上一节中,你学习了如何选择正确的 model variant,在 quantization、memory 和 performance 之间取得平衡。但这些 decisions 只有被编码进系统,并且以 repeatable way 表达出来,才真正有用。这就是 Docker Compose 的作用:它将 model choices 转化为 declarative infrastructure。

在本节中,你将学习如何在 Compose 中使用 top-level models element 声明 models,如何将 services 显式绑定到 models,并控制 endpoints 如何被 injected,以及如何将 context_size 和 flags 等 runtime knobs 配置为可 review 的 infrastructure。

Compose models 会把 “which model does this service use?” 变成 platform 可以理解的东西。你可以把它想象成声明 databases 或 queues——只不过这里的 dependency 是一个 model endpoint。

Access is explicit: Services only see models they're granted

既然我们正在把 models 当作 first-class dependencies,下一个重要 idea 就是 access control。Services 应该只看到它们真正需要的 models。

Compose 使 model access 显式化。一个 service 只有声明了对应 dependency,才能访问某个 model。这在 multi-model setups 中尤其重要;你不希望每个 container 默认都能看到每个 model。

Minimal Compose example(short syntax)

有了这个原则之后,来看在 Compose 中声明 model dependency 的最简单方式。最简单情况下,在 Compose 中声明 model dependency 看起来与声明其他 external resource 非常类似。Application 不需要知道 model 在哪里运行,或者如何启动;它只需要声明自己依赖一个 model。Compose 会处理 wiring。

在下面的示例中,service 明确说明自己需要哪个 model,而 model 本身则单独定义。这使 application configuration 和 model configuration 解耦。

一个适用于 Python ML projects 的 minimal Dockerfile pattern:

services:
  app:
    build: .
    models:
      - llm

models:
  llm:
    model: smollm2:360M-Q4_K_M

从上到下阅读:

  • app service 声明自己依赖一个名为 llm 的 model。
  • models section 定义 llm 实际是什么,在本例中是一个 specific model image 和 quantization。
  • Service 通过 logical name 引用 model,而不是通过 endpoint 或 binary。这种 indirection 是有意设计的:你可以改变或升级 model definition,而无需修改 application code。
  • Compose 会将必要的 connection details 注入 container,例如 AI_MODEL_URL,使 service 可以调用 model runner,而不需要 hard-code endpoints 或 credentials。

Controlled injection and runtime knobs(long syntax)

一旦你超出 simple setups,就会希望更精细地控制 models 如何被注入,以及 runtime 如何配置。这时 long syntax 会很有用。当你需要更细地控制 model 如何注入 service,以及它如何运行时,Compose 提供 model dependencies 的 long syntax。它允许你:

  • 命名 injected environment variable,而不是依赖 default。
  • 配置 runtime options,例如 context size 和 custom flags。
  • 让 model configuration 保持 declarative,并且可以在 version control 中 review。

在真实 codebase 中,这些 controls 会清楚表达某个 service 期望什么,以及 model 应该如何 behave,同时保持 service code 干净且 environment-agnostic。

示例如下:

services:
  app:
    build: .
    models:
      llm:
        endpoint_var: MODEL_URL

models:
  llm:
    model: smollm2:360M-Q4_K_M
    context_size: 4096
    runtime_flags:
      - "--some-flag"
      - "--another-flag=42"

每一部分的含义如下:

llm under services.app.models:不是简单列出 model name,而是把它映射到一个 block,用来指定它如何被 injected。

endpoint_var: MODEL_URL:告诉 Compose,将 model endpoint 以 environment variable MODEL_URL 的形式注入 service container。你的 application 可以读取 MODEL_URL,而不是依赖默认的 AI_MODEL_URL

context_size: 4096:设置 model 在 runtime 应该使用的最大 context window。这会让 intended runtime behavior 在 Compose configuration 中显式化。

runtime_flags:传给 model runner、用于启动该 model 的 flags list。Flags 可以控制 performance knobs、feature toggles 或其他 binary runtime options。

将这些放入 version control 后,reviewers 可以很容易看到:

  • 一个 service 依赖哪些 models;
  • 它期望哪些 environment variables;
  • model 在 production 或 dev environments 中应该如何配置和运行。

这会将 runtime concerns 从 application code 中解耦出来,并将 model configuration centralized,使其更容易 audit 和 maintain。

Multi-model pattern(architecture only)

到目前为止,我们看的是一个 service 使用一个 model。但在真实系统中,通常会有多个 models 协同工作。一个常见 pattern 是一个 web app 调用多个 specialized models:

  • Chat LLM(interactive)
  • Embeddings model(search / RAG)
  • Vision model 或 small classifier

示例:

services:
  app:
    build: .
    models:
      llm:
        endpoint_var: MODEL_URL

models:
  llm:
    model: smollm2:360M-Q4_K_M
    context_size: 4096
    runtime_flags:
      - "--some-flag"
      - "--another-flag=42"

  vision:
    model: stable-diffusion
    endpoint: http://vision:8000

为了可视化这些 dependencies 如何组合在一起,下图展示了一个 single service 如何依赖由 platform provision 的多个 model endpoints。

image.png

图 2.6:Compose graph:app service 依赖一个或多个由 platform provision 的 model endpoints

我们现在还不会构建完整 demo。这里的重点是 contract:models 是 declaratively declared 和 bound 的。第 3 章会 hands-on 使用 Model Runner,并连接真实 application。

Managing large model files and storage

上一节中,你看到了如何在 Compose 中声明和绑定 models,使它们成为 application infrastructure 的一部分。但一旦这些 models 开始被 pull 并在本地使用,一个非常实际的问题就会迅速出现:storage。

在深入之前,先说明你将在本节中练习什么:你将学习如何使用 docker model commands 在本地 operate model lifecycle,如何监控 disk usage,以避免 “why is my laptop full?” 的时刻,以及如何为 unused models 和 variants 实施 cleanup strategy。

Models 很大。Disk pressure 不是 edge case,而是 default。如果你测试 3 个 models,并且每个 model 有 3 种 quantizations,那么你可能在没注意到的情况下消耗几十 GB,直到下一次 Docker build 失败。

Lifecycle commands(DMR)

要有效管理这一点,你需要一个清晰的 mental model 来理解 model lifecycle:models 如何被 pulled、run、inspected 和 cleaned up。

image.png

图 2.7:用于管理 large model files 的 lifecycle commands 以及 disk hygiene best practices

一个 minimal operator workflow:

# Pull(cached locally)
docker model pull smollm2:360M-Q4_K_M

# List locally available models
docker model list

# Run interactively(CLI)
docker model run smollm2:360M-Q4_K_M

# Inspect logs(CLI)
docker model logs

# Remove a model
docker model rm smollm2:360M-Q4_K_M

# Purge cached model data(be careful)
docker model purge

在 Docker Desktop 中,你可以通过 Models tab 获得相同 lifecycle:从 Docker Hub pull,在本地 run,inspect logs 和 requests。为了看看实践中它是什么样,下图展示了 model logs 和 activity 如何在 Docker Desktop 中被 surfaced:

image.png

图 2.8:Docker Desktop 中的 Model Runner logs

Disk hygiene: keep a cleanup strategy

理解 lifecycle 之后,下一步是首先防止 disk usage 失控。

真正适用于 teams 的 best practices:

  • 在 iteration 阶段先从 smaller quantized models 开始,例如 Q4 variants;只有有理由时再 pull bigger ones。
  • 使用 explicit tags,避免意外 cache 多个 unnamed variants。
  • 将 disk checks 纳入 workflow:docker system df 仍然是你的朋友。
  • 设定 cleanup cadence(weekly 就足够),并把它写入 onboarding。

Where storage goes(and why it surprises people)

即使有良好习惯,storage usage 也可能快速增长,主要是因为 experimentation 在实践中就是这样运作的。

Storage consumption 通常来自:

  • 同一个 base model 的 multiple quantizations(Q4、Q8、FP16)。
  • 为 experiments pull 多个 model families。
  • 因为 tags float,或者 laptops 配置不一致,导致 repeated pulls。

如果你想减少意外,就为每个 use case(chat、embeddings)标准化一个 “dev default” model tag,并要求对 pulling heavyweight variants 给出 justification。

现在你已经看到如何管理 model lifecycles,并让 disk usage 保持受控。最后一部分是确保这些内容在不同 environments 中一致配置,使 models 在 development、staging 和 production 中以相同方式 behave。

Configuration and environment management

在最后一节中,你将学习如何使用 .env files 和 Compose overrides 标准化 model configuration,如何让 model endpoints 和 settings 远离 application code,以及如何为 models 和 services pin explicit versions,以保持 reproducibility。

当 model configuration 泄漏到 code 中时,teams 就会遇到问题。App hard-codes 一个 URL,有人在自己的 laptop 上更改了 model tag,于是 “it works on my machine” 变成了 “it answers differently on my machine”。请把 model settings 当作 infra config 来处理。

Centralize model parameters in .env

执行一致性的最简单方式,是 centralize configuration,让每个 environment 都从同一个 source of truth 读取。

一个可行 baseline 是:tag + base URL + model name 放在 .env 中,而 Compose 读取它们。

# .env
OPENAI_BASE_URL=http://model-runner.docker.internal:12434/v1
OPENAI_MODEL=smollm2:360M-Q4_K_M
CONTEXT_SIZE=4096

Compose overlays for environment differences

当然,不同 environments 并不是所有东西都完全相同。Compose 不要求你重复配置,而是允许你通过 overrides 清晰地 layer differences。Compose 支持多个 files,并采用 merge behavior。将 base config 放在 compose.yaml 中,只 override 有差异的部分:

# compose.yaml(base)
services:
  app:
    build: .
    models:
      llm:
        endpoint_var: MODEL_URL
    environment:
      OPENAI_BASE_URL: ${OPENAI_BASE_URL}
      OPENAI_MODEL: ${OPENAI_MODEL}
      MODEL_URL: ${MODEL_URL}

models:
  llm:
    model: ${OPENAI_MODEL}
    context_size: ${CONTEXT_SIZE}

然后,staging override 可以替换 model tags,或收紧 runtime flags,而无需重写整个 file。

Naming conventions that scale

一旦 configuration 被 centralized,naming 就成为下一个提升 clarity 的 lever,尤其是在 multi-model systems 中。

请为 model services 使用一致命名。如果 project 有多个 models,让 names 反映 role,而不是 family:llm_chatllm_embedllm_vision。你的 code 应该将 behavior 映射到 names,再由 config 将 names 映射到 pinned tags。

这很 boring,而这正是重点:boring infrastructure 才能 scale。

Don't teach legacy Compose

最后,一个小但重要的提醒:modern Compose 已经演进,坚持 current conventions 可以避免不必要的混乱。

如果你仍然有肌肉记忆,想在文件顶部写 version: '3.9',不要这样做。Modern Compose 使用 Compose specification;version field 已经 obsolete,并且主要会在 new codebases 中制造困惑。

Summary

到这里,你应该已经能够像谈论任何其他 runtime dependency 一样谈论 local AI。Models 不再是 shared drive 上的 random files;它们是通过 registries 分发的 OCI artifacts,具备 proper versioning 和 lifecycle management。你现在也理解了 GGUF 和 quantization 不只是 implementation details,而是 operational choices,会直接影响 memory usage、performance 和 output quality。

你也看到 Compose models 如何让你将 model dependencies 与 services 一起声明,使这些关系成为 infrastructure 的一部分,显式且可 review。除了 configuration 之外,你还认识到 local model infrastructure 需要与任何其他 dependency 同等程度的 discipline,包括 disk management 和 configuration hygiene。

从实践角度看,你现在应该能够解释 model tag drift 和 quantization drift 如何破坏 reproducibility;能够基于 hardware constraints 选择并 justify 一个 model tag;能够在 Compose 中声明和绑定 model dependencies;也能够使用 lifecycle commands 有效管理 local models,而不会破坏 broader Docker environment。

下一章会进入 hands-on:我们将直接使用 Docker Model Runner,将一个真实 application 连接到它,并在 development workflows 中把 model endpoint 当作 first-class service 来处理。