深入解析 oh-my-pi 的 Gemma 4 工具调用方言:call:NAME{key:value,…}token 流格式与流式解析实现
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
Gemma 4 是 Google 开放权重模型家族中对工具调用范式的一次彻底革新:它告别了 Gemma 3 与托管版 Gemini 沿用的 Pythonictool_code风格,改用专用特殊 token 与紧凑的花括号语法。本文以 oh-my-pi(下称 OMP)中gemma方言的实现为核心,完整讲解这一 token 分隔式工具调用协议——从特殊 token、轮次结构、工具目录序列化,到GemmaInbandScanner的流式解析原理与各类解析陷阱,读者读完即可理解并复现整套工具调用与结果回填的编解码流程。
一、背景:Gemma 4 的 token 化工具调用与 OMP 方言体系
在 OMP 中,不同厂商、不同家族的模型各自对应一套"方言"(dialect),统一由 factory.ts 中的DIALECT_DEFINITIONS注册表管理(glm、hermes、kimi、xml、anthropic、deepseek、minimax、harmony、qwen3、gemini、gemma共 11 种)。每个方言通过 types.ts 中的DialectDefinition接口约束四个核心能力:
createScanner:创建流式扫描器(InbandScanner),负责把模型输出流中的工具调用/推理块解析为事件;renderToolCall/renderAssistantToolCalls:把结构化ToolCall渲染回文本;renderToolResults:把工具执行结果渲染回模型上下文;renderThinking/renderTranscript:推理块渲染与整段对话历史渲染。
Gemma 4 的gemma方言定义在 gemma.ts,其格式指南即本文主文档 gemma.md,更详细的协议说明见 docs/toolconv/gemma.md。该方言的目标模型为 Google Gemma 4 指令微调系列(如google/gemma-4-E2B-it)。与 Gemma 3 / 托管版 Gemini 的tool_code/default_api形式(见 docs/toolconv/gemini.md)完全不兼容,两者不可互换。
二、特殊 token 表:不对称的管道位置
Gemma 4 用成对的特殊 token 包裹每个结构单元。注意其不对称管道规则:开启符的管道在左侧(<|x>),闭合符的管道在右侧(<x|>)。
| 开启符 | 闭合符 | 用途 |
|---|---|---|
<bos> | — | 序列开始 |
<\|turn> | <turn\|> | 一个对话轮次;role 名是正文第一行 |
<\|tool_call> | <tool_call\|> | 模型发出的一次工具调用 |
<\|tool_response> | <tool_response\|> | 回填给模型的工具结果 |
<\|channel> | <channel\|> | 推理通道;<\|channel>thought开启思维链(以<channel\|>闭合) |
<\|"\|> | <\|"\|> | 字符串字面量定界符(两端相同) |
<eos> | — | 序列结束 |
源码中的对应常量定义于 gemma.ts:
const CALL_OPEN = "<|tool_call>"; const CALL_CLOSE = "<tool_call|>"; const STRING = '<|"|>'; const RESPONSE_OPEN = "<|tool_response>"; const RESPONSE_CLOSE = "<tool_response|>"; const THOUGHT_OPEN = "<|channel>thought\n"; const THOUGHT_CLOSE = "<channel|>";之所以用<|"|>这样一个token而非 ASCII 引号作为字符串定界符,是因为字符串值内部可以出现原始的双引号与逗号而无需任何转义——唯一不能出现在字符串内的字节序列就是<|"|>本身。这一点由gemmaValue(gemma.ts)的渲染逻辑保证:字符串一律"${STRING}${value}${STRING}"包裹。
三、对话轮次结构:<|turn>与 role 映射
每个轮次为<|turn>{role}\n{body}<turn|>,轮次之间直接拼接、无任何分隔符,只有 role 后的\n是字面量。role 取值包括system、user、model;OMP 中的developer消息在gemma方言下渲染为system。这一映射体现在 gemma.ts:
const role = message.role === "developer" ? "system" : message.role; out += gemmaTurn(role, messageContentText(message.content));gemmaTurn(gemma.ts)把 role 与正文拼成<|turn>${role}\n${body}<turn|>。生成提示时流会在<|turn>model\n处结束,由模型续写。工具调用与其结果被放在同一个model轮次内——重渲染历史时<|tool_response>紧跟对应的<|tool_call>块。renderTranscript(gemma.ts)遍历消息数组时,通过assistantTranscriptParts拆分出推理、正文与工具调用,并用collectToolResultRun聚合紧随的toolResult消息,从而把调用与结果合并进同一model轮次。
四、工具定义的双轨序列化
gemma方言的 prompt 会携带每个工具归一化后的 wire schema,采用两种互补的呈现方式:
1. 行内工具目录(renderToolCatalog):由 catalog.ts 生成,每个工具一个紧凑的 OpenAI 风格 JSON 对象、每行一个,放在<tools></tools>之内;随后通过 prompt-template.md 中的{{TOOLS}}与{{DIALECT}}占位符,把工具目录与 gemma.md 格式指南拼进系统提示(见renderInbandToolPrompt,catalog.ts):
<tools> {"type":"function","function":{"name":"get_current_temperature","description":"Gets the current temperature for a given location.","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city name, e.g. San Francisco"}},"required":["location"]}}} </tools>2. 详细工具清单(renderToolInventory):由 inventory.ts 生成,供系统提示与/dump命令共用。它以## functions开头,输出一个namespace functions { … }代码块:每个工具的描述作为//注释行放在type NAME = (_: PARAMS);类型声明上方,配置过的示例以 JSDoc 风格的// @example注释呈现。它不输出 Markdown 小节,也不输出原生 Gemma 的<|tool_call>示例。
五、工具调用格式:call:NAME{key:value,…}值文法
模型每次调用输出一个<|tool_call>…<tool_call|>块,正文为call:NAME{ARGS},其中ARGS是逗号分隔的key:value对列表:
<|tool_call>call:get_current_temperature{location:<|"|>London<|"|>}<tool_call|>{…}内部的值文法如下:
| 值类型 | 编码 | 示例 |
|---|---|---|
| 字符串 | <\|"\|>text<\|"\|> | location:<\|"\|>London<\|"\|> |
| int / float | 裸值 | count:42 |
| 布尔 | 裸值 | flag:true |
| null | 裸值 | unit:null |
| 列表 | [v,v,…] | tags:[<\|"\|>a<\|"\|>,<\|"\|>b<\|"\|>] |
| 嵌套对象 | {k:v,…} | config:{theme:<\|"\|>dark<\|"\|>} |
其中 key 必须匹配/^[A-Za-z_]\w*$/。OMP 渲染端renderToolCall(gemma.ts)按key:value拼接、gemmaValue递归编码各类值,保证模型看到的格式与解析器完全一致。
六、流式解析原理:GemmaInbandScanner状态机
OMP 对 Gemma 块的解析不是一次性正则匹配,而是流式的GemmaInbandScanner(gemma.ts),它维护outside/tool/thinking三种状态,通过feed(text)增量投喂字节、flush()在流结束时冲刷缓存。
对每个<|tool_call>块,解析流程分三步:
- 找闭合符:
findCallClose(gemma.ts)从缓冲中寻找<tool_call|>,期间用skipGemmaString(gemma.ts)跳过所有<|"|>…<|"|>字符串区间——因此即使字符串值内部出现了<tool_call|>序列,也不会提前截断块; - 匹配头部:
parseGemmaCall用正则/^call:\s*([A-Za-z_]\w*)\s*\{/匹配call:NAME{,随后matchDelim(gemma.ts)按括号深度找到匹配的}; - 切分参数:
parseGemmaArgs用splitTopLevel(gemma.ts)在顶层逗号处切分key:value对——[]/{}括号深度与<|"|>字符串区间均被跳过——再由parseGemmaValue(gemma.ts)按文法递归解码:<|"|>开头为字符串、[开头为列表、{开头为嵌套对象、true/false为布尔、null/none/None为 null、数值形如/^[+-]?(\d|\.)/则尝试转为数字,其余当作裸字符串(如未加引号的枚举或类型名STRING)。
值得注意的行为边界:
- 事件只在完整闭合后发出:只有收到完整的
<tool_call|>闭合符后才触发toolStart/toolEnd事件(id 由mintToolCallId现场铸造),不存在部分参数事件; - 未闭合块的处理:若流被
flush()且存在未闭合的 tool 块,OMP 会丢弃该不完整块;但语法上已闭合、仅缺结尾参数括号的块,仍会基于可用正文完成解析; - 推理通道:当
parseThinking(默认开启)时,扫描器把<|channel>thought\n…<channel|>路由为thinkingStart/thinkingDelta/thinkingEnd事件,使其不进入可见回复,同时继续解析其后出现的工具调用;renderThinking则把推理原样往返回同一格式。若以parseThinking: false构造,通道内容会留在可见文本中(见 gemma.ts)。
七、并行工具调用:更多的块,而非块内的更多条目
与 JSONtool_calls[]数组"一个块多条"不同,Gemma 4 的并行是一个块一次调用:连续输出多个<|tool_call>…<tool_call|>块即表示并行调用,按出现顺序返回;应用侧按同样顺序为每次调用回填一个<|tool_response>。
八、工具结果格式:response:NAME{output:…}
每个结果为<|tool_response>response:NAME{output:VALUE}<tool_response|>。renderToolResults(gemma.ts)始终把结果包在单一outputkey 下,并先用JSON.parse尝试解析工具文本:若工具输出是 JSON,则解析为花括号语法中的嵌套对象/数组;若是普通字符串,则包上<|"|>…<|"|>:
<|tool_response>response:get_current_weather{output:{temperature:15,weather:<|"|>sunny<|"|>}}<tool_response|> <|tool_response>response:read{output:<|"|>FILE<|"|>}<tool_response|>Gemma 的 wire 格式没有独立的成功/失败字段:OMP 对isError的结果也渲染为与成功结果相同的response:NAME{output:…}形状,任何失败指示都必须体现在结果文本自身中。
九、端到端示例:一次天气查询的完整往返
renderTranscript对一次天气查询的输出如下(系统轮次还携带<tools>目录与格式指南,此处省略;模型调用与其工具响应合并进同一model轮次,最终回答是下一个model轮次;轮次间无分隔符,仅 role 后的\n为字面量):
<bos><|turn>system You are a helpful assistant.<turn|><|turn>user Hey, what's the weather in Tokyo right now?<turn|><|turn>model <|tool_call>call:get_current_weather{location:<|"|>Tokyo, JP<|"|>}<tool_call|><|tool_response>response:get_current_weather{output:{temperature:15,weather:<|"|>sunny<|"|>}}<tool_response|><turn|><|turn>model The current weather in Tokyo is 15 degrees Celsius and sunny.<turn|>十、解析陷阱与实战注意事项
结合主文档 gemma.md 的规则与 docs/toolconv/gemma.md 的提示,整理如下关键约束:
- 字符串定界符是 token 而非引号:
<|"|>…<|"|>内部的"与,都是字面数据——例如<|"|>The city and state, e.g. "San Francisco, CA"…<|"|>同时包含两者。只能在<|"|>…<|"|>区间之外按,/}切分参数;渲染端同样禁止 HTML 转义(写a & b,绝不写a & b)。 - 管道位置不对称:闭合符是
<tool_call|>,不是</tool_call>也不是<|tool_call>——写错管道方向将永远无法闭合块。 - 一次一块:并行 = 更多块,而不是一个块里更多条目。
- 裸标量:未用
<|"|>包裹的值中,true/false→ 布尔,null/none→ null,数字 → 数字,其余按裸字符串处理。 - 调用 id 是合成的:格式本身不带 id;OMP 在收到完整闭合块后铸造新 id 并发出相邻的
toolStart/toolEnd事件,渲染出的响应通过消息顺序/名称关联。 - NAME 必须匹配已列出函数:参数为逗号分隔的
key:value对;多个调用必须输出为连续块,正文放在块外;推理只能放在调用前的<|channel>thought…<channel|>块内,且绝不能在推理块里放工具调用;每个<|tool_response>必须按调用顺序读取,模型侧永远不要自己写出<|tool_response>块;每次调用必须写完整后再停止,不能先宣布再停顿。 - 与 Gemma 3 / 托管 Gemini 的区别:它们使用
gemini.md中的 Pythonictool_code/default_api形式;Gemma 4 用本 token 语法取而代之,两者不可互换。 - Gemma 3 自动选择警告:OMP 当前的家庭亲和映射把 Gemma 3 与 Gemma 4 的模型 id 都归到
gemma。若某个 Gemma 3 模型标记为supportsTools: false,则tools.format=auto会为它错误地选择 Gemma 4 语法——此时需显式设置tools.format=gemini以使用 Pythonic 约定(参见 demotion.ts 中对跨模型推理降级的处理:Harmony 与 Gemma 的renderThinking会输出聊天模板控制 token,不允许出现在结构化原生消息内,因此降级为普通<think>块)。
十一、小结
Gemma 4 的 token 化工具调用方言把"协议"从提示词工程推进到了专用 token 层:<|tool_call>call:NAME{…}承载调用、<|tool_response>response:NAME{output:…}承载结果、<|channel>thought承载推理,配合<|"|>字符串定界符规避了传统 JSON 的转义地狱。OMP 通过 gemma.ts 中的流式扫描器与一组对称的渲染器,实现了对该协议的完整双向支持;理解其状态机与括号/字符串感知的切分逻辑,是排查任何 Gemma 4 工具调用问题的钥匙。若需在 OMP 中为其他模型家族实现类似协议,factory.ts 的DialectDefinition注册表与createInbandScanner提供了可直接套用的标准骨架。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考