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

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

作者 David更新于 第 26 篇(共 33 篇)

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

另一条省 token 的线是压缩 bash 工具的输出,同样藏在实验开关后面。两件事互不依赖,本篇分开讲,最后顺带讲 bash 工具在截断与交互上的改动。上游 Shell 工具的基本机制(tree-sitter 解析、命令元数、超时竞速)见 Shell 工具;那篇对照的新版文件叫 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 把终端交给用户输入密码或确认。

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

{
  "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):

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 的例子:

# 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):
        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):

      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):

形状触发剪裁
gitdiffgit diff、git show,或输出含 diff --gitlockfile、.min.js、dist/、build/ 等路径只留文件头与增删行数;单个 hunk 超过 100 行截断;文件尾追加增删统计
pytestpytest,或 test session starts 横幅四态状态机,只留 collected、E 开头的断言行、文件:行号:、FAILED 与 short test summary
npmnpm、pnpm、yarn 的 install、i、add连续的 deprecated 警告合成一行计数并列出前 3 个;其余只留 added、漏洞、funding 摘要与 warn、err 行
makemake、cmake、automake删掉进出目录提示、裸编译命令与 ^ 指示行
stacktracePython 的 Traceback、at 文件:行:列、Rust 的 error[E 前缀连续的依赖栈帧(site-packages、node_modules、标准库等)折成一行
tsctsc按错误码取前 5、按文件取前 8,各带一条样本,其他行最多留 20 行
kubectlkubectl get pod 或 pods连续 2 行以上 Running 且 0 重启的合成一行,末尾建议改用 -o json
json输出以左花括号或左方括号开头解析后把 embedding、body、content、base64、data 等键下超过 200 字符的字符串、超过 20 项的数组换成省略标记
mdgh pr view、gh issue view删 HTML 注释、徽章行、纯图片行、分隔线,合并多余空行
gostestgo 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 隔离与安全护栏——隔离子会话的 git 限制、主工作树提示、删除二次确认、先读后改与权限超时。

本页目录