第 1 章:全貌——Ollama 不是模型文件,而是模型运行时¶
一、三个身份¶
Ollama 同时扮演三个角色:
| 身份 | 面向谁 | 主要代码 |
|---|---|---|
| CLI 产品 | 人类用户 | main.go、cmd/、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 如何计算。它做的是:
- 绑定并验证
api.ChatRequest。 - 解析模型引用,区分 local/cloud/remote。
- 读出
server.Model,合并 Modelfile 与请求 options。 - 通过
scheduleRunner向 Scheduler 请求一个已加载或正在加载的 runner。 - 选择 prompt/template/工具调用解析路径。
- 将 runner 的响应变成 Ollama API 的流式对象。
实际计算被压到 llm.LlamaServer 接口后面。这个接口暴露 Load、Chat、Completion、Embedding、Tokenize、MemorySize 等能力,而不把具体实现泄漏给 server 层。接口入口见 llm/server.go。
四、代码规模意味着什么¶
本地快照约有 136,922 行非测试 Go 代码;核心阅读区并不是均匀分布的:
| 区域 | 非测试 Go 行数(快照统计) | 读法 |
|---|---|---|
cmd/ |
65,517 | 先读 NewCLI、RunHandler、RunServer,再按命令下钻 |
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 的分层体现了四个取舍:
- 兼容性优先:协议层允许不同客户端接入同一个推理核心。
- 资源调度集中化:把显存、并发 slot、runner 生命周期放入 Scheduler,避免各 handler 各自管理。
- 格式归一化:上游模型格式多,但服务核心尽量围绕 GGUF/统一
Model工作。 - 原生实现隔离:Go 负责生命周期、协议和业务;C/C++/Swift/MLX 负责密集计算。
后面 11 章只是把这四个取舍分别展开。