跳转至

第 2 章:CLI 启动——从 Cobra 命令树到交互会话

一、入口只有一行,但责任不止一层

main.gomain() 只做两件事:创建 CLI,并把后台 context 交给 Cobra 执行。真正的命令树在 cmd/cmd.goNewCLI 中组装。

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 等配置集成

可以把它们分成三类:

类别 命令 共同特征
服务控制 serveps 依赖 HTTP server 已启动或创建 server
模型生命周期 pullpushcreatecopydelete 通过 API 或 manifest 改变本地模型状态
交互产品 runlaunch 连接 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.gowaitForServer 每 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 还是子进程;它只看到 ChatResponseFuncPullProgressFunc 这样的回调。

五、交互与无头模式

cmd/cmd.goinitlaunch 包的默认选择器、确认提示、spinner 替换为 Bubble Tea TUI 实现。这样 launch 可以保持无 UI 的业务协议,交互界面通过函数注入。

launch 业务层
  ├── DefaultSingleSelector
  ├── DefaultMultiSelector
  ├── DefaultConfirmPrompt
  └── DefaultSpinner
             ↑ init() 注入
cmd/tui Bubble Tea 实现

当 stdin/stdout 不是终端时,选择器会报错并提示使用 --model 的 headless 模式。这是一个小但重要的边界:自动化环境不应该意外进入交互选择流程。

六、设计取舍

  1. CLI 不嵌入推理逻辑:可测试、可被 App/SDK 复用。
  2. 服务通过 API 自调用run 和外部客户端使用同一协议,避免“两套行为”。
  3. UI 采用依赖注入:脚本、桌面、终端可以选择不同交互实现。
  4. 鉴权在 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 能解决的单点。