oh-my-pi 的 MiniMax 带内工具协议:<minimax:tool_call>Owned 方言全解析
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
本文基于 oh-my-pi(OMP)仓库中docs/toolconv/minimax.md参考文档展开,系统讲解 OMP 为 MiniMax 家族模型实现的提示词驱动、带内(in-band)工具调用协议:从tools.format: minimax的会话级选择、工具目录与格式指南注入,到调用信封结构、参数编码与类型强制转换、并行调用、<function_results>结果协议,以及增量扫描器在流式场景下的生命周期与容错恢复行为。读完本文,你能完整理解该方言的报文契约,并知道每个行为背后对应的源码实现位置。
一、什么是 MiniMax 带内工具协议
MiniMax 家族模型(MiniMax M1 等)在实践中对 OpenAI 风格的 JSONtool_calls支持不理想,常见的工程对策是改用纯文本协议:工具调用不是结构化的 provider 消息,而是模型在普通助手文本里写出的标记块。OMP 将这类协议统一称为owned dialect(自有方言)——调用是普通的助手文本:一个<minimax:tool_call>信封包裹一个或多个<invoke>元素;OMP 执行解析出的调用后,在下一轮 user 消息中回传一个<function_results>块。由于报文中不携带任何调用 id,调用与结果靠顺序关联。
需要明确边界:docs/toolconv/minimax.md描述的是OMP 自己实现的转换器,而不是 MiniMax 官方 provider 的结构化工具 API。参考文档指明该行为以以下源码为准:
- packages/ai/src/dialect/minimax.ts —— 方言定义:提示词、渲染器与扫描器配置;
- packages/ai/src/dialect/anthropic.ts —— 共享的增量 invoke/parameter 扫描器与类型强制转换逻辑;
- packages/ai/src/dialect/catalog.ts —— 提示词组装(工具目录 + 方言指南);
- packages/ai/src/dialect/owned-stream.ts —— 流式投影。
从源码结构看,MiniMax 方言并非独立实现一个解析器:minimax.ts 复用了 Anthropic 方言的AnthropicInbandScanner,只是换了一套标签前缀(<minimax:tool_call>、<invoke>、<parameter>等)与提示词,因此两个方言共享同一套状态机、强制转换与容错语义。
二、方言选择与请求转换
显式启用
在~/.omp/agent/config.yml或项目/overlay 配置中显式指定格式:
tools: format: minimaxtools.format: minimax会在整个会话中强制启用该自有方言。在auto模式下,OMP 保留 provider 原生工具调用,除非所选模型显式标记为supportsTools: false;对 MiniMax 家族的模型 id,这一回退解析为minimax。
这一解析逻辑在 packages/catalog/src/identity/dialect.ts 中可以确认:preferredDialect(modelId)先通过classifyModel对模型 id 做家族分类,命中minimax分支即返回"minimax"方言;未识别的模型回退到FALLBACK_DIALECT("xml")。Dialect联合类型本身也列在 dialect.ts 中,与docs/toolconv/目录下的其他方言文档一一对应。
启用自有方言后 OMP 做了什么
当一个 owned 方言生效时,OMP 执行四步转换(引自参考文档):
- 从 provider 请求中移除原生结构化
tools字段——工具不再通过 API 的tools参数下发; - 在系统提示词末尾追加带内工具目录与 MiniMax 格式指南;
- 将历史中已有的结构化助手调用与工具结果消息改写为该文本协议;
- 把模型的文本流逆向扫描回结构化工具调用事件。
其中第 3 步的实现是 packages/ai/src/dialect/history.ts 的encodeInbandToolHistory:它遍历消息序列,把 assistant 消息中的toolCall块渲染成文本(保留原有 prose,拼接在信封之前),并把连续的一批toolResult消息合并编码为一条合成的user消息,内容就是渲染后的<function_results>块。这保证了会话中途切换或加载历史时,整段对话在模型看来始终是同一套文本协议。
三、工具定义与提示词注入
注入的提示词模板是 packages/ai/src/dialect/prompt-template.md,结构固定为:开头# Tools声明“工具调用以文本形式发出,而非原生 provider 工具消息”,随后列出<tools></tools>内的可用函数,最后追加方言专属指南({{TOOLS}}与{{DIALECT}}两个占位符)。
packages/ai/src/dialect/catalog.ts 的renderToolCatalog把每个工具序列化为每行一个紧凑的 OpenAI 风格函数对象,字段为name、description和经toolWireSchema归一化后的parameters线型 schema:
<tools> {"type":"function","function":{"name":"read","description":"Read a file","parameters":{"type":"object","properties":{"path":{"type":"string"},"count":{"type":"number"}},"required":["path"]}}} </tools>目录之后追加的是 packages/ai/src/dialect/minimax.md 这份注入给模型的格式指南。该契约要求:name必须匹配已列出的函数名;字符串/标量参数写精确文本且保留空格(因为正文是按定界符正则匹配而非 XML 解析器读取,所以永远不要 HTML 转义——写a & b而不是a & b);列表/对象用 JSON;多个调用写在同一个<minimax:tool_call>里;允许在调用前写可见文本;禁止输出tool_callsJSON;禁止使用<function_calls>或旧式<tool_name>/<parameters>调用语法;按调用顺序读取每条<result>/<error>,且永远不要自己输出<function_results>;只有在调用完整写出后才允许停止生成。
四、调用信封结构
一次单调用形如:
<minimax:tool_call> <invoke name="read"><parameter name="path">src/main.ts</parameter><parameter name="count">40</parameter></invoke> </minimax:tool_call>精确结构约定:
| 元素 | 含义 |
|---|---|
<minimax:tool_call>…</minimax:tool_call> | 提示词契约中要求的模型输出信封 |
<invoke name="TOOL">…</invoke> | 一次调用;name必须是已列出的工具 |
<parameter name="ARG">VALUE</parameter> | 一个具名参数,直接位于 invoke 内部 |
转义规则值得注意:渲染器对工具名与参数名(属性位置)做 XML 属性转义,但参数正文刻意不做 XML 转义——因为该协议是靠定界符匹配而非 XML 解析。例如字符串正文a & b < c会原样输出,而不是a & b < c;唯一被保留的序列是</parameter>,因为它会关闭该参数。这一点在 minimax.ts 的renderInvoke中可以直接看到:属性用escapeXmlAttr,正文直接拼接。
扫描器比提示词契约更宽容:它既接受带命名空间的规范信封,也接受不带前缀的<tool_call>包裹,甚至接受包裹之外裸露的<invoke>。但模型仍应输出规范的<minimax:tool_call>形式,避免行为依赖恢复路径。minimax.ts 中MINIMAX_WRAPPER_TAGS只登记了tool_call一个包裹标签,baseTagPrefixes同时包含<minimax:tool_call与<tool_call两组前缀,正是这种“规范形 + 恢复形”双轨支持的实现。
五、参数编码与类型强制转换
编码依据所选工具的 schema:
| 声明/取值类型 | 渲染出的参数正文 | 解析后的值 |
|---|---|---|
| schema 声明为 string 且运行时值也是 string | 逐字文本,含首尾空格与换行 | 逐字字符串 |
数字、布尔、null、数组或对象 | JSON | 解析后的 JSON 值 |
| 值没有匹配到 string schema | JSON(字符串会带引号) | 合法时按 JSON 解析 |
例如:
<invoke name="write"><parameter name="path">notes/a & b.txt</parameter><parameter name="options">{"append":false,"tags":["x","y"]}</parameter></invoke>“哪些参数算字符串”由 schema 判定:packages/ai/src/dialect/coercion.ts 的isStringOnlySchema收集 schema 中所有类型(递归处理anyOf/oneOf/allOf与enum/const),去掉null后若只剩string才判定为纯字符串参数。渲染侧据此决定正文原样输出还是stringifyJson;扫描侧据此决定正文逐字保留还是走 JSON 解析。
参数属性可以覆盖 schema 的判定:
string="true"(以及除false、0、no以外的任意值)强制按逐字字符串处理;string="false"、string="0"或string="no"对 schema 声明的字符串也强制 JSON 解析。
对应实现是 anthropic.ts 的parseStringAttribute(大小写不敏感、只识别false/0/no三个否定值)与#coerceParameterValue(显式string属性优先于 schema 解析结果)。
对非字符串参数,外围空白仅为 JSON 解析而 trim。OMP 使用可修复的 JSON 解析器parseJsonWithRepair(来自@oh-my-pi/pi-utils,见 anthropic.ts);若仍解析失败,保留原始正文作为字符串而不是丢弃参数。空正文保持空字符串;没有可用name的参数被忽略。
六、多个调用与并行调用
并行调用是同一个信封内的兄弟<invoke>元素,按发出顺序排列:
<minimax:tool_call> <invoke name="read"><parameter name="path">src/a.ts</parameter></invoke> <invoke name="read"><parameter name="path">src/b.ts</parameter></invoke> </minimax:tool_call>由于线型格式没有 id,扫描器会为每个 invoke铸造一个内部 id——coercion.ts 中的mintToolCallId生成形如ptc_<时间戳36>_<计数器36>的进程内 id,仅用于事件关联。OMP 可以把整批调用作为 batch 派发;工具结果必须按相同顺序返回,因为结果协议没有任何 call id 可用于修复乱序。
七、工具结果信封
OMP 把连续的工具结果批量打包进一个<function_results>块,成功与失败使用不同的记录类型:
<function_results> <result> <tool_name>read</tool_name> <stdout>file contents</stdout> </result> <error> <tool_name>read</tool_name> <stderr>ENOENT: file not found</stderr> </error> </function_results>每条结果的规则:成功用<result>+<stdout>;isError: true用<error>+<stderr>;<tool_name>做 XML 文本转义;stdout/stderr 正文逐字插入;没有 call id,模型按调用顺序读取记录。
packages/ai/src/dialect/minimax.ts 的renderToolResults即此实现:按isError选择result/stdout或error/stderr标签,tool_name经escapeXmlText,正文原样拼接。history.ts 进一步规定了消息装配:该文本被放进一条合成的user消息;单个工具结果中的多个文本块会被拼接,而图片结果块在渲染文本之后保持为图片块。模型永远不应该自己输出<function_results>或<tool_response>——这是协议里明确划给 OMP 一侧的职责。
八、思考块与可见文本
OMP 将保留的推理块渲染为:
<thinking> reasoning text </thinking>在正常的 owned 工具流中,thinking 解析是开启的。MiniMax 扫描器识别<thinking>、<think>、<scratchpad>(含受支持的带前缀形式,见 anthropic.ts 的ANTHROPIC_THINKING_TAG_PREFIXES),发出独立的 thinking 事件(thinkingStart/thinkingDelta/thinkingEnd),并使其内容不进入可见的助手文本。若某个直接调用扫描器的消费方禁用了parseThinking,这些标签会原样留在可见文本中。未闭合的 thinking 块会在流 flush 时被逻辑闭合,已累计的内容保留。
可见 prose 可以出现在工具信封之前;调用之外的文本保持为助手文本,而包裹内部的非调用文本会被扫描器丢弃(见 anthropic.ts 的#consumeSection:section 状态下非 invoke/thinking 的文本直接丢弃,不产生事件)。
九、流式解析、畸形输出与恢复
扫描器是增量且对块边界安全的:开闭标签与参数正文可能分散在不同的 provider delta 中到达。核心状态机在 anthropic.ts 的AnthropicInbandScanner中,状态为outside / section / invoke / parameter / thinking;#peekTag配合 256 字符的部分标签缓冲(MAX_PARTIAL_TAG_LENGTH)保证一个标签被跨 delta 截断时不会误发文本事件。
可观察的生命周期:
- 非空
<invoke name="…">立即发出toolStart(见#startInvoke:name为空则started为假,不发事件); - 每个具名参数正文在文本块到达时发出带 key 的
toolArgDelta事件; - 匹配的
</invoke>执行最终强制转换,发出携带完整参数与精确原始 invoke 块的toolEnd。
关键失败行为(参考文档归纳,均可在扫描器源码中对照):
- 缺少调用名:该 invoke 不发出任何工具生命周期事件;
- 缺少参数名:该参数被忽略(
#finishParameter仅在#paramName非空时写入#args); - 畸形 JSON:回退为原始参数文本(
#coerceParameterValue的 catch 分支); - 超大参数:输入上限为 1,000,000 个 JS 字符串码元(
MAX_PARAMETER_VALUE_LENGTH,anthropic.ts),超出部分替换为已接受前缀加显式截断标记…[parameter truncated: exceeded …]; - 未完成的 invoke:flush 时重置扫描器局部调用状态、不发
toolEnd。但 OMP 的流投影器(owned-stream.ts)已经基于toolStart物化了一次调用:在正常停止的响应上会保留该部分调用、把该轮标记为工具使用并可能派发;已流出的参数文本保持未强制转换状态,没有任何参数文本的调用得到{}。provider 的length停止原因保持length,不会被伪装成可运行的工具使用; - 包裹未闭合但内部 invoke 已完成:已闭合的 invoke 仍然有效,包裹的关闭标签并不是发出其
toolEnd事件的前提(#consumeSection直接透传 invoke 事件); - 未完成的 thinking:保留为 thinking 并在 flush 时逻辑结束。
此外,OMP 防护模型在调用后伪造工具输出的行为:对该方言,第一个<function_results>或<tool_response>边界即停止投影——owned-stream.ts 中minimax方言登记的伪造结果边界正是这两个序列。默认tools.abortOnFabricatedResult: true时立即中止生成;关闭该选项时,OMP 会排空 provider 流但丢弃伪造的后续内容。
十、端到端示例
注入的工具定义(节选相关目录行):
<tools> {"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"},"units":{"type":"string"}},"required":["city"]}}} </tools>助手调用:
I'll check both cities. <minimax:tool_call> <invoke name="get_weather"><parameter name="city">Tokyo</parameter><parameter name="units">celsius</parameter></invoke> <invoke name="get_weather"><parameter name="city">Oslo</parameter><parameter name="units">celsius</parameter></invoke> </minimax:tool_call>OMP 生成的下一轮 user 消息:
<function_results> <result> <tool_name>get_weather</tool_name> <stdout>{"temperature":28,"condition":"clear"}</stdout> </result> <result> <tool_name>get_weather</tool_name> <stdout>{"temperature":14,"condition":"rain"}</stdout> </result> </function_results>助手随后可以正常作答,或再发出一个完整的 MiniMax 调用信封。
十一、解析注意事项与常见坑
参考文档最后列出的 gotchas,逐条都有源码依据,汇总如下:
- 不是真正的 XML:不要对参数正文做实体转义,也不要丢给 XML DOM 解析器;匹配基于协议定界符。
- 一个信封,多个 invoke:并行是
<minimax:tool_call>内的兄弟调用,既不是 JSONtool_calls,也不是每个 batch 一个信封。 - 字符串判定依赖 schema:不提供工具定义时,即便渲染值本身是字符串,也会被 JSON 加引号渲染——渲染/扫描 API 需要传入工具定义才能完成往返(
renderInvoke依赖buildArgShapes提供的stringArgs集合)。 - 线上没有 id:OMP 生成的 id(
ptc_…)纯属内部,必须保持调用/结果顺序一致。 - 错误是一等记录:用
<error>/<stderr>,而不是在成功的<result>里夹带带外错误标志。 - 规范包裹 vs 可接受的恢复语法:解析器接受裸 invoke 与
<tool_call>,但注入契约要求<minimax:tool_call>。 - 停止前必须完成 invoke:自然语言里“我准备调用某工具”的许诺不是调用;闭合
</invoke>才是最终化强制转换与正常生命周期的触发点(这也是注入格式指南 minimax.md 末尾“写完整调用,再发停止序列”规则针对的失败模式)。
十二、测试覆盖与延伸阅读
该方言的行为由 packages/ai/test/inband-tools.test.ts 覆盖,包括:提示词渲染、调用往返、分块参数 delta、原始块捕获、string="true"/string="false"属性覆盖、MiniMax 包裹恢复、以及伪造<function_results>边界的截断行为(如裸 invoke 后紧跟<function_results>的用例即验证了“边界停止投影”)。
相关文件索引(仓库根相对路径):
- docs/toolconv/minimax.md —— 本文主体参考文档;
- packages/ai/src/dialect/minimax.ts —— 方言定义与渲染器;
- packages/ai/src/dialect/minimax.md —— 注入给模型的格式指南;
- packages/ai/src/dialect/anthropic.ts —— 共享增量扫描器与强制转换;
- packages/ai/src/dialect/catalog.ts 与 prompt-template.md —— 工具目录与系统提示词注入;
- packages/ai/src/dialect/history.ts 与 owned-stream.ts —— 历史转换、流式投影、不完整调用行为与伪造结果边界;
- packages/catalog/src/identity/dialect.ts —— MiniMax 家族亲和与
tools.format解析; - 同系列其他方言文档位于 docs/toolconv/,如 anthropic.md、harmony.md、xml.md,可对照阅读 OMP 的完整 owned 方言矩阵。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考