跳转至

第 1 章:全貌——Ollama 不是模型文件,而是模型运行时

一、三个身份

Ollama 同时扮演三个角色:

身份 面向谁 主要代码
CLI 产品 人类用户 main.gocmd/cmd/tui/
本地模型服务 SDK、Web UI、IDE server/api/middleware/
推理运行时 机器和 GPU discover/llm/x/mlxrunner/llama/

app/ 又把 CLI/service 包进桌面产品;agent/ 则把同一套 chat/tool 能力组织成 Agent Loop。理解这些身份之间的边界,比记住目录名更重要。

二、顶层依赖关系

flowchart TB
    subgraph Product[产品入口]
        CLI[main.go + cmd]
        APP[app/ 桌面 App]
        SDK[api.Client]
        AGENT[agent.Session]
    end
    subgraph Service[服务层]
        ROUTES[server/routes.go]
        PROMPT[server/prompt.go]
        COMPAT[middleware + openai]
    end
    subgraph Runtime[运行时]
        MODEL[server.Model + model/]
        STORE[manifest + fs/gguf]
        SCHED[server.Scheduler]
        LLM[llm.LlamaServer]
    end
    subgraph Native[原生层]
        LLAMA[llama-server / llama/]
        MLX[x/mlxrunner]
        GPU[discover + ml]
    end
    CLI --> ROUTES
    APP --> SDK
    AGENT --> SDK
    SDK --> ROUTES
    ROUTES --> PROMPT
    ROUTES --> COMPAT
    ROUTES --> MODEL
    ROUTES --> SCHED
    MODEL --> STORE
    SCHED --> LLM
    LLM --> GPU
    LLM --> LLAMA
    LLM --> MLX

三、关键边界:API 不等于推理

server.ChatHandler 负责协议和业务编排,但它不应该知道 CUDA kernel 或 GGUF tensor 如何计算。它做的是:

  1. 绑定并验证 api.ChatRequest
  2. 解析模型引用,区分 local/cloud/remote。
  3. 读出 server.Model,合并 Modelfile 与请求 options。
  4. 通过 scheduleRunner 向 Scheduler 请求一个已加载或正在加载的 runner。
  5. 选择 prompt/template/工具调用解析路径。
  6. 将 runner 的响应变成 Ollama API 的流式对象。

实际计算被压到 llm.LlamaServer 接口后面。这个接口暴露 LoadChatCompletionEmbeddingTokenizeMemorySize 等能力,而不把具体实现泄漏给 server 层。接口入口见 llm/server.go

四、代码规模意味着什么

本地快照约有 136,922 行非测试 Go 代码;核心阅读区并不是均匀分布的:

区域 非测试 Go 行数(快照统计) 读法
cmd/ 65,517 先读 NewCLIRunHandlerRunServer,再按命令下钻
x/ 48,515 视为模型转换、传输、MLX 等实验/扩展子系统
model/ 33,648 读接口和解析器注册,不必先读每种架构
server/ 29,215 主运行时,优先 routes、sched、model、prompt
agent/ 9,398 一条相对独立的 Agent 主线

数字是帮助安排阅读时间的指标,不是性能结论。真正的主干约等于 cmd → server → scheduler → llm

五、一次请求的最小心智模型

请求
  → 模型名解析
  → 模型元数据 + options
  → Scheduler 排队/复用/加载
  → prompt 渲染或原生 chat template
  → runner 子进程
  → callback/stream
  → Ollama NDJSON
  → 客户端回调或兼容层 SSE

这里有两个很容易误判的点:

  • 模型加载不是请求的一次性副作用:Scheduler 会保留 runner,后续请求可能复用;keep_alive 甚至直接决定卸载时机。
  • 模型输出不是天然的工具调用:server 可能需要 thinkingState、内置 parser 或 generic tool parser,把原生 token 流重组为 api.Message.ToolCalls

六、设计取舍

Ollama 的分层体现了四个取舍:

  1. 兼容性优先:协议层允许不同客户端接入同一个推理核心。
  2. 资源调度集中化:把显存、并发 slot、runner 生命周期放入 Scheduler,避免各 handler 各自管理。
  3. 格式归一化:上游模型格式多,但服务核心尽量围绕 GGUF/统一 Model 工作。
  4. 原生实现隔离:Go 负责生命周期、协议和业务;C/C++/Swift/MLX 负责密集计算。

后面 11 章只是把这四个取舍分别展开。