# 命令行式工具调用与 Token 精简

> 受限的命令行语法怎样代替 JSON 参数调用四个工具，工具名与参数的容错修复，以及 bash 输出的清理、按形状剪裁、截断改进与交互模式。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.net/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.net/manual/mimo-code/token-efficient-shell
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

官方博客在“Tool-Call Syntax”一节里说：受限的命令行语法表达同样的调用意图更省 token、格式错误也更少，而且刻意不支持管道、重定向和变量展开；同一节末尾也注明，MiMo Code 还没有把工具调用迁移到这种格式。v0.1.14 的代码与这段说明一致：命令行式调用已经实现，但默认关闭，只有 4 个工具支持。

另一条省 token 的线是压缩 bash 工具的输出，同样藏在实验开关后面。两件事互不依赖，本篇分开讲，最后顺带讲 bash 工具在截断与交互上的改动。上游 Shell 工具的基本机制（tree-sitter 解析、命令元数、超时竞速）见 [Shell 工具](https://daiw.net/manual/opencode/shell-tool)；那篇对照的新版文件叫 `shell.ts`，v1.14.19 与 MiMo 里仍叫 `bash.ts`。

## 怎么用

- **命令行式调用**：配置 `tool.invocation_style` 设为 `shell` 对全部工具生效，或用 `tool.invocation_style_by_tool` 逐个指定（`packages/opencode/src/config/config.ts:232`），默认 `json`。只有定义里带 `shell` 字段的 `actor`、`task`、`cron`、`session` 会真正切换，其余工具保持 JSON，并在日志里警告一次。
- **输出精简**：`MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY=1` 打开通用清理；再加 `MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY_HEURISTIC=1` 才启用按命令形状的剪裁（`packages/opencode/src/flag/flag.ts:324`、`flag.ts:340`）。两者都不受总开关 `MIMOCODE_EXPERIMENTAL` 影响。
- **调参**：`MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY_MAX_LINE_CHARS`（默认 500）、`MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY_LINE_HEAD_KEEP`（默认 160）、`MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY_NEVER_WORSE_MARGIN`（默认 0），只接受正整数（`flag.ts:331`）。
- **单条命令不清理**：命令里带 `# nofilter` 或 `# raw`，或设置 `MIMOCODE_BASH_RAW=1`；这两种方式只对通用清理有效，见下文。
- **bash 新参数**：`max_output_tokens` 限制内联返回的大致 token 数；`interactive` 把终端交给用户输入密码或确认。

一个逐工具开启的配置示例：

```jsonc
{
  "tool": {
    "invocation_style_by_tool": { "task": "shell", "actor": "shell" }
  }
}
```

## 同一个工具的两种 schema

`ToolRegistry` 组装工具定义时逐个判断风格（`packages/opencode/src/tool/registry.ts:478`）：`resolveInvocationStyle`（`packages/opencode/src/tool/invocation-style.ts:15`）按“逐工具配置、全局配置、`json`”取值；结果为 `shell` 且工具带 `shell` 字段时，用 `shellWrap` 包一层，描述换成该工具的 `*.shell.txt`（`registry.ts:483`）。包装后的工具只剩一个参数（`packages/opencode/src/tool/shell-wrap.ts:5`）：

```ts
export const shellInputSchema = z.object({
  script: z.string().min(1).describe(
    [
      "Multi-line shell-style script. Each non-blank line is one command; commands run sequentially and stop on first failure.",
      'Quoting: "..." preserves literal newlines and processes \\" \\\\ escapes; \'...\' is verbatim.',
      "<<EOF heredoc bodies are fully verbatim (no escape, no $vars); the delimiter may be quoted (<<'EOF', <<\"EOF\") or use <<-.",
      "Use REAL line breaks between commands and inside heredoc bodies — a literal \\n (backslash-n) is not a newline.",
      "# starts a line comment to end-of-line (quoted # is literal).",
      "Variables ($VAR, ${VAR}) are preserved as literal text — no expansion.",
      "See the tool description for the verb table.",
    ].join(" "),
  ),
})
```

所以在 API 层面，这仍是一次普通的函数调用，变的只是参数形状：`task` 原本要写嵌套的 `operation` 对象，现在写一段 `script` 字符串。`packages/opencode/src/tool/task.shell.txt:41` 的例子：

```text
# multi-command (one per line, set-e on first failure):
    task create "Lexer"
    task create "Parser"
    task create "Type checker"
```

`shellWrap` 的执行过程（`shell-wrap.ts:32`）：

1. **没有可用的 `script`**：先调用工具自己的 `recover`，把 JSON 形状的参数直接转成解析结果执行，成功时不加任何提示；转不了就返回一段教学式错误（`shell-wrap.ts:39`）。
2. **解析失败**：如果脚本里有字面量的 `\n`、`\t`，替换成真实的换行与制表符再解析一次；这次成功就执行，并在结果前加一段 `<notice>`，提醒模型下次输出真实换行（`shell-wrap.ts:80`）。
3. **逐条执行**：每条命令的结果包进 `<command index="1" operation="create">` 这样的块；某条失败就停，剩下的记为 `<not-executed>`（`shell-wrap.ts:113`）。

记忆召回提醒里的示例也按工具的调用风格渲染（`packages/opencode/src/session/prompt.ts:153`），注释的理由是 JSON 形状的示例会诱使 shell 模式下的模型输出 JSON。

## 分词器：借 shell 的写法，不借 shell 的能力

`tokenize`（`packages/opencode/src/tool/shell-tokenize.ts:253`）分四步：

1. **抽出 heredoc**（`shell-tokenize.ts:54`）：认 `<<EOF`、`<<-EOF`、`<< EOF`、`<<'EOF'`、`<<"EOF"`，正文原样保存，原位置换成占位符并保留行号；`<<<` 和同一行 `<<EOF` 之后还有内容都报错，找不到结束行报 `unclosed-heredoc`。
2. **去注释**（`shell-tokenize.ts:194`）：词边界上的 `#` 到行尾删掉，词中间的 `#` 转义成普通字符。
3. **切行**（`shell-tokenize.ts:322`）：引号里的换行不算分隔，行尾反斜杠续行；每段先查未闭合的引号。
4. **交给 `shell-quote` 解析**：变量回调原样返回 `$` 加变量名，所以不展开；解析结果里只要出现运算符对象就拒绝（`shell-tokenize.ts:284`）：

```ts
        if (typeof t === "object" && t !== null && "op" in t) {
          const tok = t as { op: string; pattern?: string }
          const detail =
            tok.op === "glob" && tok.pattern != null
              ? `unsupported glob pattern: ${tok.pattern}`
              : `unsupported shell operator: ${tok.op}`
          return Effect.fail<ParseError>({
            kind: "unsupported-operator",
            line: seg.line,
            detail,
          })
        }
```

管道、重定向、`&&`、`;` 与通配符在 `shell-quote` 的输出里都是带 `op` 的对象，于是一律报错，这正对应博客所说的“受限”。每行得到的 argv 再交给工具自己的动词表：比如 `task` 要求首词是 `task`，再把 `create`、`list`、`start` 等动词和 `--parent`、`--session` 之类的旗标映射回原来的 JSON 参数（`packages/opencode/src/tool/task.ts:143`），最后仍由原工具的 `execute` 与 zod 校验把关。

## 工具名与参数的容错

与之配套的还有一层兜底。模型给的工具名或参数键不规范、AI SDK 校验失败时，会回调 `experimental_repairToolCall`（`packages/opencode/src/session/llm.ts:778`）。v1.14.19 在这里只尝试把工具名转成小写；MiMo 改为调用 `repairToolCall`（`packages/opencode/src/util/tool-compat.ts:196`）：

- **工具名**：先精确匹配，再忽略大小写，最后比较规范形，即去掉 `-`、`_` 与空白后转小写，`mcp__server__tool` 这种写法先改成 `server_tool`（`tool-compat.ts:5`）。
- **参数键**：按 schema 把 `filePath`、`file-path` 这类键改回 `file_path`，精确键优先，递归处理嵌套对象、数组元素和 `allOf`、`anyOf`、`oneOf` 分支（`tool-compat.ts:56`）。
- **JSON 文本**：解析失败时，修复被空白拆开的 `\uXXXX` 转义（注释举的例子是流式边界把 `\u7ed1` 切成了 `\u7 ed1`）后再试（`tool-compat.ts:110`）。
- **`exec` 特例**：参数是一段裸字符串时，直接包成只含 `code` 字段的对象（`tool-compat.ts:204`）。

修不好的，仍像上游一样改调 `invalid` 工具，把错误信息交给模型。

## bash 输出精简：两条管线

清理插在 bash 工具收尾处（`packages/opencode/src/tool/bash.ts:1146`）：

```ts
      const cleaned =
        !file && Flag.MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY
          ? BashTokenEfficient.clean(end.text, { command: input.command })
          : null
```

`file` 非空表示输出已经溢出、写进了截断文件，这时两条管线都跳过，磁盘上的归档保持原始字节；TUI 实时预览用的 `metadata.output` 也始终是原始输出。启发式管线接在通用管线之后，要求两个开关同时打开（`bash.ts:1161`）。开关默认关闭，也就意味着默认情况下 bash 输出里的密钥不会被遮盖。

**通用管线**（`packages/opencode/src/tool/bash_token_efficient_pipeline.ts:156`）按固定顺序串起四个插件：

| 插件 | 做什么 |
| --- | --- |
| `progress` | 每行只保留最后一个回车符之后的内容，折叠进度条重绘 |
| `ansi` | 删除 ANSI 的 CSI、OSC、DCS 序列，处理退格覆写，删除除制表符、换行、回车外的控制字符 |
| `redact` | 先把整块 PEM 换成 `<redacted-pem-block>`，再遮盖 Bearer、JWT、AWS、GitHub、OpenAI、Anthropic、Slack 凭据与 `api_key=` 式赋值 |
| `longline` | 超过 500 字符的行只留前 160 字符，后接 `…<elided N chars>` |

文件头注释交代了顺序：进度条折叠必须在去 ANSI 之前，遮盖必须在长行截断之前，免得长密钥被截断后认不出来。整条链跑完字节数没有变小，就退回原文（`bash_token_efficient_pipeline.ts:177`）。`# nofilter`、`# raw` 与 `MIMOCODE_BASH_RAW=1` 的检查也在这里（`bash_token_efficient_pipeline.ts:90`）。

**启发式管线**（`packages/opencode/src/tool/bash_token_efficient_heuristic.ts:543`）先放过“用户已经在自己做投影”的命令：含 `--json`、`--format json`、`-o json`、`--no-color`，或者管道给 `tee`、`xxd`、`hexdump`。然后依次按命令名、输出前 4096 个字符的指纹挑一个形状（`bash_token_efficient_heuristic.ts:81`）：

| 形状 | 触发 | 剪裁 |
| --- | --- | --- |
| `gitdiff` | `git diff`、`git show`，或输出含 `diff --git` | lockfile、`.min.js`、`dist/`、`build/` 等路径只留文件头与增删行数；单个 hunk 超过 100 行截断；文件尾追加增删统计 |
| `pytest` | `pytest`，或 test session starts 横幅 | 四态状态机，只留 collected、`E` 开头的断言行、`文件:行号:`、`FAILED` 与 short test summary |
| `npm` | npm、pnpm、yarn 的 install、i、add | 连续的 deprecated 警告合成一行计数并列出前 3 个；其余只留 added、漏洞、funding 摘要与 warn、err 行 |
| `make` | `make`、`cmake`、`automake` | 删掉进出目录提示、裸编译命令与 `^` 指示行 |
| `stacktrace` | Python 的 Traceback、`at 文件:行:列`、Rust 的 `error[E` 前缀 | 连续的依赖栈帧（site-packages、node_modules、标准库等）折成一行 |
| `tsc` | `tsc` | 按错误码取前 5、按文件取前 8，各带一条样本，其他行最多留 20 行 |
| `kubectl` | `kubectl get pod` 或 `pods` | 连续 2 行以上 Running 且 0 重启的合成一行，末尾建议改用 `-o json` |
| `json` | 输出以左花括号或左方括号开头 | 解析后把 `embedding`、`body`、`content`、`base64`、`data` 等键下超过 200 字符的字符串、超过 20 项的数组换成省略标记 |
| `md` | `gh pr view`、`gh issue view` | 删 HTML 注释、徽章行、纯图片行、分隔线，合并多余空行 |
| `gostest` | `go test` 且带 `-json` | 按包汇总 pass、fail、skip，失败的包附最后 20 条输出 |

启发式管线同样遵守“不变差”，只是没有余量参数：字节数不减就丢弃改写（`bash_token_efficient_heuristic.ts:556`）。

与设计文档 `docs/harness/MiMo Token Efficient Mode.md` 对照，有几处要以代码为准：

- 文档引用的 `bash_token_efficient.ts` 不存在，实现拆成了上面两个文件。
- 文档把 `# nofilter`、`# raw` 列为启发式管线的放行条件，代码只在通用管线里检查它们（`MIMOCODE_BASH_RAW` 也一样）。两个开关都打开时，带 `# raw` 的 `pytest` 命令仍会被 pytest 形状剪裁。
- 文档说 JSON 形状还有一种只输出键与类型的模式，代码里只有裁大字段这一种。
- 文档里 `Shape` 接口的 `match` 负责挑选，代码中十个形状的 `match` 都是恒返回假的空函数，挑选完全由 `shapeFor` 的两张表决定，算出来的 `tail4k` 也没有用到。

## 截断：头尾都留

MiMo 还改了 bash 输出过长时的截断：

- 传了 `max_output_tokens` 时，字节上限按 token 数乘 4 计算，不限行数；溢出后返回开头一半字节加末尾部分，第一行注明原始 token 数，中间注明截掉了多少 token（`bash.ts:1020`、`bash.ts:1183`）。
- 没传时仍只取尾部 2000 行或 50 KiB（`packages/opencode/src/tool/truncate.ts:15`）；但若尾部最后 2048 个字符匹配 `error`、`exception`、`failed`、`fatal`、`traceback`、`panic`、`exit code` 之一（不区分大小写），就从截断文件里读出头部，行数与字节都取上限的 70%，拼成“头部、省略、尾部”（`bash.ts:654`、`bash.ts:1186`）。

## 交互式命令：把终端交还给用户

`interactive` 为真时（`bash.ts:1317`），bash 工具照常完成权限询问，但不自己起进程，而是经 `BashInteractive.request`（`packages/opencode/src/tool/bash-interactive.ts:103`）发布 `bash.interactive.asked` 事件，然后挂起等待：

1. TUI 收到事件后暂停渲染、清屏，用 `sh -c`（Windows 上是 `cmd /c`）以继承的标准输入输出运行命令，用户直接在终端里输入（`packages/opencode/src/cli/cmd/tui/app.tsx:1381`）。
2. 进程退出后恢复界面，向 `POST /bash-interactive/:id/reply` 提交退出码（`packages/opencode/src/server/routes/instance/bash-interactive.ts:49`），失败时 500 毫秒后重试一次。
3. 服务端用这份结果完成挂起的 Deferred，工具返回。

输出直接给了终端，模型拿到的只有 `(interactive command completed with exit code N)` 这样一句话，看不到命令打印了什么。等待没有超时；代码里只有 TUI 订阅了这个事件。

下一篇：[Git 隔离与安全护栏](https://daiw.net/manual/mimo-code/git-guards)——隔离子会话的 git 限制、主工作树提示、删除二次确认、先读后改与权限超时。
