# 装上就跑 · 第一次对话发生了什么

> 先把 grok 装起来跑一遍——预编译二进制或从源码 cargo run——再顺着「你按下回车之后」这条线，把第一次对话经过的组件走一遍，为后面逐层深入建立实感。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/install-and-run
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 装上就跑 · 第一次对话发生了什么

读源码之前，先让它在你机器上转起来。有实感，后面看代码才不悬空。

## 怎么用：两种装法

**一、装预编译二进制**（最快，对应官方 user-guide `01-getting-started`）：

```sh
curl -fsSL https://x.ai/cli/install.sh | bash   # macOS / Linux / Git Bash
irm https://x.ai/cli/install.ps1 | iex          # Windows PowerShell
grok --version
```

**二、从源码构建**（我们要读源码，这条更相关）：

```sh
cargo run -p xai-grok-pager-bin              # 构建并启动 TUI
cargo build -p xai-grok-pager-bin --release  # release 二进制:target/release/xai-grok-pager
cargo check -p xai-grok-pager-bin            # 只做快速校验，不产出二进制
```

几个要点：

- 工具链由 `rust-toolchain.toml` 钉死，`rustup` 首次构建会自动装对应版本。
- proto 代码生成需要 `protoc`：仓库通过 [DotSlash](https://dotslash-cli.com) 解析 `bin/protoc`，所以构建前先装好 `dotslash` 并放进 `PATH`。
- 二进制产物叫 **`xai-grok-pager`**，官方安装包把它装成 **`grok`**——记住这个名字对应关系，后面找入口不会迷路。

首次启动会打开浏览器完成认证（细节见 user-guide `02-authentication`，实现我们放到第 6 部分）。

## 入口在哪：组合根

`cargo run -p xai-grok-pager-bin` 里的 `xai-grok-pager-bin` 是整个 workspace 的**组合根（composition root）**——它本身几乎不含业务逻辑，职责是把几十个 crate 拼装成一个能跑的程序，然后把控制权交给 TUI crate `xai-grok-pager`。

<Callout type="info">
  「组合根」是个值得留意的模式：所有依赖在这一个包里被创建、连接、注入，其余 crate 之间尽量只依赖接口。这让近百个 crate 能各自独立测试、独立编译。第 6 部分会回来看它到底拼了些什么。
</Callout>

## 你按下回车之后

装好之后，随手问它一句、让它改个文件，屏幕上发生的事，顺着这条线走一遍（每一步后面都有专章，这里只建立骨架）：

1. **TUI 收到输入** —— `xai-grok-pager` 的输入层拿到你这行字，交给 agent 运行时。
2. **开一个 turn** —— `xai-grok-shell` 的会话 actor（`SessionActor`）启动一轮：system prompt 由 `xai-grok-agent` 的 `prompt/` 模块事先组装好，你的话拼进消息历史（`xai-chat-state`）。
3. **请求模型** —— 交给采样器 `xai-grok-sampler`：它以 actor 的方式发起流式请求，token 一边到一边往回吐。
4. **模型要调工具** —— 流里出现工具调用，agent 把它路由到 `xai-grok-tools` 里对应的实现（比如「编辑文件」或「跑命令」）。
5. **工具真正干活** —— 改文件走编辑工具 + diff 追踪（`xai-hunk-tracker`），跑命令走 `xai-grok-tools` 的终端工具，可能还套一层沙箱。
6. **结果回灌** —— 工具输出作为新消息喂回模型，循环继续，直到这一轮没有更多工具要调。
7. **渲染** —— 全程的文字、diff、工具状态由 TUI 渲染进滚动缓冲（scrollback），就是你看到的画面。

```mermaid
sequenceDiagram
  participant TUI as xai-grok-pager (TUI)
  participant AG as xai-grok-shell (会话循环)
  participant SM as xai-grok-sampler (模型)
  participant TL as xai-grok-tools (工具)
  TUI->>AG: 你的输入
  AG->>AG: 组装 prompt + 历史
  AG->>SM: 流式请求
  SM-->>AG: token / 工具调用
  AG->>TL: 路由工具调用
  TL-->>AG: 工具结果
  AG->>SM: 把结果喂回，继续
  SM-->>AG: 最终回复
  AG-->>TUI: 渲染
```

## 补充：模型怎么「知道」该调工具了？

上面第 4 步一句「流里出现工具调用」带过，这里值得停下来讲清一个初学者常困惑的点：**模型自己并不会去执行任何工具**。它唯一能做的事，是输出文本（或结构化数据）。所谓「调用工具」，不过是模型输出了一段**标准化的调用意图（tool call）**，再由 Agent 或宿主环境把它解析出来、真正执行、把结果回传给模型——**执行永远发生在模型之外**。

这正是当下业界的主流范式：不追求让模型「亲自动手」，而是让它**表达意图**，把「做」交给可信的宿主。唯其如此才谈得上权限、沙箱、审计——因为真正动手的是你的代码，不是模型。

### 一个 OpenAI Function Calling 风格的例子

以 OpenAI 的 Function Calling 协议为例（grok 走自己的流式协议，道理完全一样）。请求里除了对话消息，你还多带一个 `tools` 数组，用 JSON Schema 声明每个工具「叫什么名字、收哪些参数」：

```json
{
  "messages": [{ "role": "user", "content": "北京现在天气怎么样?" }],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "查询某座城市的当前天气",
        "parameters": {
          "type": "object",
          "properties": { "city": { "type": "string" } },
          "required": ["city"]
        }
      }
    }
  ]
}
```

模型收到后有两条路：

- 觉得直接回答就够了，就正常返回一段文字 `content`，`finish_reason` 为 `stop`；
- 判断需要外部能力，就**不返回**普通文字，转而返回一个 `tool_calls`，`finish_reason` 变成 `tool_calls`：

```json
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\"}"
      }
    }
  ]
}
```

**判断「模型要调工具了」的信号，就是响应里出现了 `tool_calls` 而非 `content`。** 还有个细节值得留意：`arguments` 是一段 JSON **字符串**、而不是对象——模型生成的本就是文本，参数也是它一个字一个字「写」出来的，所以宿主拿到后必须自己解析并校验，不能盲信（模型完全可能写出不合 schema 的参数）。

接下来全由你的代码接手：解析出 `name` 与 `arguments` → 调用真正的 `get_weather("北京")` → 把返回值作为一条 `role: "tool"` 的新消息追加进对话历史，再发起下一轮请求：

```json
{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "{\"temp\": 32, \"desc\": \"晴\"}"
}
```

模型这时才第一次「看到」天气数据，据此生成给用户的最终答复。一个完整回合，就是「模型出意图 → 宿主执行 → 结果回灌 → 模型续写」这样一次乃至多次往返。

### 回到 grok

对照本章第 4 到 6 步：grok 流里冒出的工具调用，正是上面 `tool_calls` 的**流式版本**；`xai-grok-shell` 的会话循环扮演那个「宿主」，把意图路由到 `xai-grok-tools` 执行，再把结果喂回——和 OpenAI 协议是同一套骨架，只换了传输格式。想通这一层，你也就明白第 2 部分为什么能对工具做**权限门控、沙箱、审计**：因为动手的始终是 grok 的代码，模型只递了一张「意图单」。

<Callout type="info">
  一句话记住：**模型负责「说要做什么」，宿主负责「真去做」。** 这条边界是所有 coding agent 安全模型的地基——模型不可全信，但它递出的每一个意图，都能被宿主逐条审查、放行、拦截或改写。
</Callout>

### 主流模型如今对工具调用支持到什么程度？

一句话：工具调用如今已是主流模型的**标配**——几乎所有前沿闭源模型（Claude、Gemini、GPT 系）和主流开源模型（GLM、DeepSeek、Qwen、Kimi、MiniMax 等）都原生支持。但「支持」分层次：能吐出一个格式合法的 `tool_call` 只是最低门槛；能不能**选对**工具、把参数填得**合乎 schema**、多步链式调用不跑偏、出错后自己恢复——差距全在这里，而这恰恰是 coding agent 最吃重的能力。

有两个公开榜单，正好从两个角度把这件事看清（数据截至 2026 年 7 月，两榜均实时更新，请以线上为准）：

- **[Agent Arena 榜](https://arena.ai/leaderboard/agent)** 量的是**用得好不好**：基于上百万真实 agent 会话，按工具可靠性、任务完成、可纠偏性、工具幻觉率等信号排名。截至 7 月，前排是 Claude Fable 5、GPT 5.6 Sol、Claude Opus 4.8、GPT 5.5、Claude Sonnet 5，开源阵营里 GLM 5.2 挤进前十。它衡量的正是 grok 这类 agent 真正依赖的「把工具用稳」的本事。
- **[OpenRouter Tool Calls 榜](https://openrouter.ai/rankings#tools)** 量的是**被用得多不多**：按各模型在 OpenRouter 上工具调用用量的**占比**排名，反映真金白银的生产选择。格局和能力榜几乎倒过来——走量领先的是中国开源 MoE 和各家廉价的「Flash」档：Tencent Hy3（free）约 18.7%、Xiaomi MiMo 约 14.9%、DeepSeek V4 Flash 约 9.2% 居前，GLM 5.2、MiniMax M3、谷歌 Gemini Flash 系紧随；而能力榜前排的 Claude、GPT 顶配，反倒挤不进这个占比榜的前列——因为「份额」奖励的是便宜、免费、能自托管的走量选手。

两榜对照，能得出一个对写 agent 的人很实用的结论：**「能力最强」与「被用最多」几乎是两拨模型**——质量榜前排的 Claude、GPT 在用量占比榜上并不靠前，而占比榜前列的开源 MoE 更多是靠便宜、免费与可自托管走量。前者由质量决定，后者被价格、延迟、部署方式等工程因素推着走；真到自己选模型时，这两个维度都得一起掂量。

对 grok 这类 agent 而言，好在换模型成本不高：它的采样器（`xai-grok-sampler`）刻意做成传输与格式无关，能对接 OpenAI 兼容、Anthropic 等多种 API——上面这些主流模型，大多都能作为后端接上。这套「一份契约、多种后端」的设计，留到第 1 部分讲采样器时再细看。

### OpenAI 与 Anthropic 的工具调用格式差在哪？

上面反复说模型输出「标准化的调用意图」，但**「标准」其实各家不一**。同一件事——定义工具、模型发起调用、回灌结果——OpenAI 与 Anthropic 的报文格式有好几处明显不同（下表以 OpenAI 最经典的 Chat Completions API 与 Anthropic 的 Messages API 对照，字段名取自两家官方文档）：

| 环节 | OpenAI · Chat Completions | Anthropic · Messages |
| --- | --- | --- |
| 工具定义 | 裹在 `function` 里，schema 键为 `parameters` | 平铺，schema 键为 `input_schema` |
| 模型的调用 | 独立的 `tool_calls` 数组 | 与文本并列的 `tool_use` 内容块 |
| 参数类型 | `arguments` 是 **JSON 字符串**（要自己 parse） | `input` 是**已解析的对象** |
| 回灌结果 | 一条 `role: "tool"` 消息 | `role: "user"` 消息里的 `tool_result` 块 |
| 关联 id | `call_…` ↔ `tool_call_id` | `toolu_…` ↔ `tool_use_id` |

挑三处最扎眼的看具体报文：

**① 定义工具**（放进请求的 `tools`）：OpenAI 把元数据裹进 `function`、schema 键叫 `parameters`；Anthropic 平铺、键叫 `input_schema`。

**OpenAI · Chat Completions**

```json
{ "type": "function",
  "function": { "name": "get_weather", "parameters": { "type": "object", "properties": { "city": { "type": "string" } } } } }
```

**Anthropic · Messages**

```json
{ "name": "get_weather",
  "input_schema": { "type": "object", "properties": { "city": { "type": "string" } } } }
```

**② 模型发起调用**：OpenAI 把调用单独挂在 `tool_calls` 数组里、参数是**字符串**；Anthropic 把它作为一个 `tool_use` 内容块**和文本并列**、参数已是**对象**。

**OpenAI · Chat Completions**

```json
{ "role": "assistant", "content": null,
  "tool_calls": [ { "id": "call_abc", "type": "function",
    "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } } ] }
```

**Anthropic · Messages**

```json
{ "role": "assistant",
  "content": [
    { "type": "text", "text": "我查一下" },
    { "type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": { "city": "北京" } } ] }
```

**③ 回灌结果**：OpenAI 用一条 `role: "tool"` 的独立消息、靠 `tool_call_id` 对应；Anthropic 把结果塞进一条 `role: "user"` 消息的 `tool_result` 块、靠 `tool_use_id` 对应。

**OpenAI · Chat Completions**

```json
{ "role": "tool", "tool_call_id": "call_abc", "content": "{\"temp\":32}" }
```

**Anthropic · Messages**

```json
{ "role": "user", "content": [
    { "type": "tool_result", "tool_use_id": "toolu_01A", "content": "{\"temp\":32}" } ] }
```

一句话概括这套差异：**Anthropic 把工具调用当成对话内容的一等公民**（`text`、`tool_use`、`tool_result` 都是并列的 content block，参数直接是对象）；**OpenAI 则把它当成消息的旁挂字段**（`tool_calls` 与 `content` 分开，参数是待解析的字符串，结果另用一种 `role`）。顺带一提，连 OpenAI 自己都不止一套——较新的 Responses API 又把工具定义拍平、把这些字段改叫 `function_call` / `call_id` / `function_call_output`。

对 grok 这类要跨模型跑的 agent，这种「各家标准不一」正是采样器必须做成**格式无关**的另一半理由——上一节讲的是换后端，这一节是换报文格式。采样器内部只认一套统一的工具表示，到边界再翻译成各家线上格式（嵌套还是平铺、字符串还是对象、`role: "tool"` 还是 `tool_result`、`call_` 还是 `toolu_`），这层「翻译」的落点同样在第 1 部分。

这张时序图就是整本书的目录——后面每一部分，都是把其中一步放大来看。下一章先把这些 crate 的**全景与分层**摆清楚，你才知道每一步落在代码的哪一层。

---

上一篇：[grok 到底是什么](https://daiw.org/manual/grok-build/what-is-grok-build) · 下一篇：[Rust workspace 全景](https://daiw.org/manual/grok-build/workspace-map)
