第 2 章:CLI 启动——从 Cobra 命令树到交互会话¶
一、入口只有一行,但责任不止一层¶
main.go 的 main() 只做两件事:创建 CLI,并把后台 context 交给 Cobra 执行。真正的命令树在 cmd/cmd.go 的 NewCLI 中组装。
func main() {
cobra.CheckErr(cmd.NewCLI().ExecuteContext(context.Background()))
}
这是一种有意的“薄入口”设计:进程级错误交给 cobra.CheckErr,命令组合和参数定义集中在 cmd 包,业务实现再按 handler 拆开。
二、命令树按生命周期分组¶
快照中的 root command 注册了这些主要分支:
ollama
├── serve 启动本地 HTTP 服务
├── run 加载模型并进入聊天/生成
├── create / show 从 Modelfile 创建、查看元数据
├── pull / push 下载、上传模型
├── list / ps 列出本地模型、运行中 runner
├── copy / delete 管理模型引用
├── signin / signout cloud 身份
├── runner / gpu 原生 runner 与 GPU 探测
└── launch 为 Claude/Codex/OpenCode 等配置集成
可以把它们分成三类:
| 类别 | 命令 | 共同特征 |
|---|---|---|
| 服务控制 | serve、ps |
依赖 HTTP server 已启动或创建 server |
| 模型生命周期 | pull、push、create、copy、delete |
通过 API 或 manifest 改变本地模型状态 |
| 交互产品 | run、launch |
连接 API,再在 TUI/终端中消费流式响应 |
三、run 的真实路径¶
RunHandler 不是直接打开权重,而是先解析运行选项、确认服务、调用 showOrPullModel,然后把流式 chat 结果交给终端显示。
sequenceDiagram
participant U as 用户
participant CLI as cmd.RunHandler
participant C as api.Client
participant S as server
participant R as runner
U->>CLI: ollama run gemma4
CLI->>C: Heartbeat()
CLI->>C: Show/Pull()
C->>S: /api/show 或 /api/pull
CLI->>C: Chat(messages, callback)
C->>S: /api/chat
S->>R: schedule + Chat
R-->>S: NDJSON chunks
S-->>C: NDJSON
C-->>CLI: ChatResponse callback
CLI-->>U: TUI/终端渲染
macOS/Windows 下服务可能由 App 拉起,所以 cmd/start.go 的 waitForServer 每 500 ms 做一次 heartbeat,最多等待 5 秒。这比让每个命令自行启动 daemon 更清晰:服务启动是平台层责任,命令只等待它可用。
四、api.Client 是 CLI 与 server 的隔离层¶
客户端的两个底层方法值得重点读:
do:JSON 请求—完整 JSON 响应。stream:JSON 请求—按行扫描的 NDJSON 响应。
stream 将 scanner buffer 提高到 8 MiB,逐行反序列化并交给 callback;还统一处理鉴权、HTTP 错误和 signin_url。见 api/client.go。
因此 cmd 不需要知道 HTTP 细节,终端也不需要知道 server 内部是 channel 还是子进程;它只看到 ChatResponseFunc、PullProgressFunc 这样的回调。
五、交互与无头模式¶
cmd/cmd.go 的 init 把 launch 包的默认选择器、确认提示、spinner 替换为 Bubble Tea TUI 实现。这样 launch 可以保持无 UI 的业务协议,交互界面通过函数注入。
launch 业务层
├── DefaultSingleSelector
├── DefaultMultiSelector
├── DefaultConfirmPrompt
└── DefaultSpinner
↑ init() 注入
cmd/tui Bubble Tea 实现
当 stdin/stdout 不是终端时,选择器会报错并提示使用 --model 的 headless 模式。这是一个小但重要的边界:自动化环境不应该意外进入交互选择流程。
六、设计取舍¶
- CLI 不嵌入推理逻辑:可测试、可被 App/SDK 复用。
- 服务通过 API 自调用:
run和外部客户端使用同一协议,避免“两套行为”。 - UI 采用依赖注入:脚本、桌面、终端可以选择不同交互实现。
- 鉴权在 client 统一完成:cloud、远程 API 和本地调用共享签名逻辑,但本地默认不强制认证。
七、阅读练习¶
建议自己沿着以下符号确认一遍:
rg -n 'func NewCLI|func RunHandler|func RunServer|func \(c \*Client\) stream' \
ollama-main/cmd ollama-main/api
然后问自己:ollama run 退出时,谁负责关闭 HTTP 连接?谁负责取消 context?谁负责 runner 卸载?答案分别落在 client、命令 context 和 Scheduler/服务关闭路径,而不是一个 defer 能解决的单点。