oh-my-pi Kimi 工具调用格式(Tool-Call Dialect)完整指南:从提示词规范到流式解析实现
2026/9/10 5:00:40 网站建设 项目流程

oh-my-pi Kimi 工具调用格式(Tool-Call Dialect)完整指南:从提示词规范到流式解析实现

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

导读

本文基于 packages/ai/src/dialect/kimi.md 这份 Kimi 方言(dialect)格式指南展开,系统讲解 oh-my-pi 在与 Kimi 系列模型对接时使用的带内工具调用(in-band tool calling)协议:从模型端必须遵循的 section / call / argument 标记语法与行为规则,到仓库源码中对应的渲染器、流式状态机扫描器与容错修复机制。读完本文,你将掌握这套格式的完整语法、每一条规则背后的工程原因,以及它在 oh-my-pi 的packages/ai包中如何被实现、测试与复用。

一、背景:为什么需要一套独立的工具调用方言

在 packages/ai/src/dialect/ 目录下,oh-my-pi 为不同模型家族维护了各自的"方言"(dialect),包括anthropicdeepseekgeminiglmharmonyhermesminimaxqwen3xml以及本文主角kimi。每种方言定义两件核心事:

  • 渲染(Render):把内部统一的Message/ToolCall结构转换成该模型聊天模板能识别的文本形式;
  • 扫描(Scan):把模型流式吐出的文本增量解析回结构化的工具调用事件。

方言注册表在 packages/ai/src/dialect/factory.ts 中,kimi与其余 10 种方言一同挂在DIALECT_DEFINITIONS上,通过getDialectDefinition("kimi")createInbandScanner("kimi", options)对外提供能力。kimi.md这份文档正是注入给模型的提示词正文——kimi.ts中通过import dialectPrompt from "./kimi.md" with { type: "text" }将其作为definition.prompt使用,要求模型严格按此格式输出工具调用。

二、Format Guide:工具调用的三种核心标记

kimi.md规定:每一轮(turn)的所有工具调用必须放在同一个 section 之内,每次调用由固定形式的 idfunctions.NAME:INDEX加一个 JSON 参数对象组成。完整范式如下:

<|tool_calls_section_begin|><|tool_call_begin|>functions.NAME:INDEX<|tool_call_argument_begin|>{"arg":"value"}<|tool_call_end|><|tool_calls_section_end|>

拆解这条格式,共涉及五个标记,全部在 packages/ai/src/dialect/kimi.ts 顶部以常量形式定义:

标记常量语义
<|tool_calls_section_begin|>KIMI_SECTION_BEGIN一次(可能并行的)调用区块的开始
<|tool_call_begin|>KIMI_CALL_BEGIN单次工具调用的开始
functions.NAME:INDEX调用 id,NAME为函数名,INDEX从 0 递增
<|tool_call_argument_begin|>KIMI_ARG_BEGIN参数 JSON 对象的分隔标记
<|tool_call_end|>KIMI_CALL_END单次调用结束
<|tool_calls_section_end|>KIMI_SECTION_END调用区块结束

参数必须是一个 JSON 对象,例如{"path":"src/main.rs","count":5}。仓库测试 packages/ai/test/inband-tools.test.ts 中给出了该语法的真实样例:

<|tool_calls_section_begin|><|tool_call_begin|>functions.read:0<|tool_call_argument_begin|> {"path":"src/a.ts"} <|tool_call_end|><|tool_calls_section_end|>

注意参数对象与<|tool_call_argument_begin|>之间允许存在空白字符,扫描器会先trim再解析。

2.1 工具结果的返回格式

工具结果不会出现在同一 section 内,而是以独立的轮次(turn)稍后到达:正文以## Return of functions.NAME:INDEX作为标题头,随后紧跟逐字(verbatim)的工具输出:

<|im_system|>NAME<|im_middle|>## Return of functions.NAME:INDEX verbatim tool result<|im_end|>

这里出现了第二种模板语法<|im_role|>…<|im_middle|>…<|im_end|>,它是 Kimi 的 ChatML 风格角色分隔符。在 packages/ai/src/dialect/rendering.ts 中对应kimiTurn函数:

export function kimiTurn(role: "assistant" | "system" | "user", name: string, body: string): string { return `<|im_${role}|>${name}<|im_middle|>${body}<|im_end|>`; }

渲染工具结果时(renderToolResults),每个结果被包装为一条system角色轮次,name 为函数名,body 以## Return of ${kimiCallId(...)}开头再拼接结果文本——与kimi.md规定的格式一一对应。

三、Rules:模型必须遵守的六条铁律

kimi.md的 Rules 部分是格式指南的行为约束,逐条解读如下,并给出源码侧的依据:

  1. NAME必须与已列出的函数名完全一致:id 中的函数名不允许自由发挥。解析侧用 coercion.ts 的normalizeKimiFunctionNamefunctions.NAME:INDEX中提取NAMEtrim,任何不一致都会导致工具调用无法匹配到已声明的函数。

  2. 参数必须是双引号键的 JSON 对象:单引号、裸键名、无引号值都属于非法输出。在扫描器#parseArgs中,原始参数字符串会交给parseJsonWithRepair(来自@oh-my-pi/pi-utils)做带修复的解析,解析失败则退化为空对象{},保证容错但不丢失调用本身。

  3. 字符串值只使用标准 JSON 转义(\"\\\n),严禁 HTML 转义:例如必须写a & b而不是a &amp; b。这与 XML 系方言(如glm)形成鲜明对比——那些方言恰恰需要escapeXmlText处理&<>。Kimi 走纯 JSON 通道,因此渲染端直接使用stringifyJson(rendering.ts),不做任何 HTML 转义。

  4. 并行调用 = 同一 section 内连续拼接多个<|tool_call_begin|>…<|tool_call_end|>块,INDEX从 0 递增:渲染端renderAssistantToolCalls正是这样做的——对calls数组map((call, index) => kimiInvocation(call, index))后拼成一个整体,外面套上 section 起止标记:

function renderAssistantToolCalls(calls: readonly ToolCall[], _options?: DialectRenderOptions): string { if (calls.length === 0) return ""; const body = calls.map((call, index) => kimiInvocation(call, index)).join(""); return `${KIMI_SECTION_BEGIN}${body}${KIMI_SECTION_END}`; }

kimiCallId的逻辑是:如果模型给定的 id 已经以functions.开头则原样保留,否则自动补全为functions.${name}:${index},从而保证格式统一:

function kimiCallId(name: string, id: string, index: number): string { const trimmed = id.trim(); return trimmed.startsWith("functions.") ? trimmed : `functions.${name}:${index}`; }
  1. 私有推理(private reasoning)若支持,放在工具调用 section 之前的<think>…</think>中;绝不允许把工具调用塞进<think>:渲染端renderThinking会把思考文本包装成<think>\n${text}\n</think>并置于调用块之前;扫描端在outside状态下会优先探测THINK_OPEN,进入thinking状态后把<think></think>之间的内容作为thinkingDelta/thinkingEnd事件输出,直到闭合标签出现才回到outside——从机制上杜绝了"思考中夹杂调用"的解析歧义。

  2. 按调用顺序读取每条结果轮次,模型自身绝不生成结果轮次:结果轮次由宿主注入,renderTranscript遍历消息时遇到连续的toolResult消息会通过collectToolResultRun批量取出并按序渲染,模型永远只消费不产出。

  3. 停止序列(stop sequence)只能在调用完整写出之后发出:严禁出现"先声明要调用工具(例如停在Let's run cargo clippy)却未发出<|tool_call_begin|>就停止"的行为。正确姿势是:完整写出调用 → 再发出停止序列 → 然后才停机。这条规则直指 LLM 常见的"预告式中断"缺陷,是保证流式扫描器能拿到完整调用块的前提。

四、源码纵深:KimiInbandScanner 的五状态流式状态机

kimi.md描述的是"模型侧的输出规范",而仓库源码 packages/ai/src/dialect/kimi.ts 中的KimiInbandScanner是"宿主侧的解析实现"。它实现了InbandScanner接口(见 types.ts),对外只有两个方法:

  • feed(text):喂入一段流式增量文本,返回本次触发的事件数组;
  • flush():流结束时强制清空缓冲,返回残余事件。

内部用#state字段维护五种状态:

outside → section → header → args →(回到 section)→ outside └→ thinking(独立于调用链)
  • outside:空闲态,用#nextTokenIndex在所有五类标记中取最早出现位置。若当前位置是<think>则进入thinking;若是KIMI_SECTION_BEGIN则进入section。其余文本原样作为text事件输出。
  • thinking:累积思考内容,找到</think>后发出thinkingEnd事件并回到outside
  • section:跳过空白,等待KIMI_CALL_BEGIN(进入header)或KIMI_SECTION_END(回到outside)。
  • header:在缓冲中寻找KIMI_ARG_BEGIN,其前的文本即为调用 id,调用normalizeKimiFunctionName得到函数名,发出toolStart事件,随后进入args
  • args:寻找KIMI_CALL_END,其间的原始文本trim后经#parseArgs解析为参数对象,发出toolEnd事件,回到section等待下一个并行调用或 section 结束。

两个关键容错点值得注意:

  1. 分块边界(chunk boundary)处理:流式场景下标记可能被切成两半(如<|tool_call_beg+in|>)。#consumeOutsidepartialSuffixOverlapAny(coercion.ts)计算当前缓冲尾部与各标记前缀的最大重叠长度,把可能是不完整标记的尾部"hold"住不发,等下一块数据到来再决定是补全标记还是作为普通文本输出。
  2. 未完成调用的丢弃flush()时若仍停留在header/args状态,说明调用块不完整(模型违反了第 7 条规则),调用#dropBufferedCall丢弃缓冲、重置状态,避免把残片污染给上层。

扫描器还支持parseThinking: false选项(InbandScannerOptions),关闭思考解析后<think>会被当作普通文本输出。

五、调用链:方言如何进入实际推理流程

kimi方言不是孤立代码,它通过两层复用深入整个推理管线:

  1. 常规带内工具调用factory.createInbandScanner(dialect, options)在 owned-stream.ts 中被调用,扫描器直接消费模型流式输出,把toolStart/toolArgDelta/toolEnd事件转换为结构化工具调用。
  2. 流式标记修复(Stream Markup Healing):托管模型有时不返回结构化tool_calls,而是把聊天模板标记"泄漏"进可见的content文本。 packages/ai/src/utils/stream-markup-healing.ts 针对 OpenAI 兼容 / Ollama 可见文本流选择修复模式:
export function getStreamMarkupHealingPattern(model: Model<"ollama-chat">): StreamMarkupHealingPattern { if (model.identity.class === "kimi") return "kimi"; if (model.identity.class === "deepseek") return "dsml"; return "thinking"; }

当模型身份为kimi时,复用createInbandScanner("kimi")扫描泄漏的 section/call 标记,把toolEnd事件重建为HealedToolCall(含随机生成的call_前缀 id),同时把文本通道再过一遍ThinkingInbandScanner,把泄漏的<think>推理语料也清理出来。这正是kimi.md中格式在"非官方通道"下的二次价值——即使模型没有走结构化输出,只要它按格式指南吐出标记文本,宿主依然能恢复出完整的工具调用

六、测试验证:格式约定的可执行契约

packages/ai/test/inband-tools.test.ts 把kimi.md的格式约定固化为可执行测试。其中有两条与本文主题直接相关的断言:

  • 精确原始块捕获expectRawBlock("kimi", '<|tool_calls_section_begin|><|tool_call_begin|>functions.read:0<|tool_call_argument_begin|> {"path":"src/a.ts"}\n<|tool_call_end|><|tool_calls_section_end|>', ...)验证扫描器能完整捕获一次调用,且参数前允许空白、toolEnd事件携带rawBlock用于调试。
  • 工具结果渲染归属getDialectDefinition("kimi").renderToolResults([resultBlock])验证结果渲染由方言自身负责,返回格式与kimi.md## Return of functions.NAME:INDEX一致。

此外,packages/ai/test/下的kimi-usage.test.tskimi-multi-account.test.ts以及issue-2883-moonshot-base-url.test.ts覆盖了 Kimi 通道的用量统计、多账号与 base-url 配置等外围行为;packages/ai/src/providers/kimi.tspackages/ai/src/registry/oauth/kimi.tspackages/ai/src/usage/kimi.ts则分别承担提供方接入、OAuth 登录与 token 用量统计职责,与本方言共同构成完整的 Kimi 模型支持栈。

七、与相邻方言的对比与小结

kimi与其他方言并列观察,能更清楚这套格式的设计取向:

  • 纯 JSON 通道:与需要 XML 转义的glm/xml方言不同,Kimi 的参数永远是 JSON 对象,字符串值禁止 HTML 转义,这让渲染与解析两侧都更简单、更少歧义;
  • ChatML 风格角色标记<|im_role|>harmony<|start|>assistant<|channel|>…gemma<|turn>等模板并存,说明 oh-my-pi 的方言层充分尊重各厂商的原生聊天模板,而不是强行统一;
  • 思考与调用分离<think>仅允许承载私有推理,工具调用必须位于其后的 section 内,配合流式状态机严格区分thinkingsection→header→args两条解析路径,从协议层面杜绝了推理文本混入调用参数。

理解kimi.md这份提示词指南,就同时理解了 oh-my-pi 中 Kimi 模型的渲染输出、流式解析、标记修复与测试契约。若需要在你的接入方案中复现或对齐这套格式,可直接对照 kimi.md、kimi.ts 与 inband-tools.test.ts 三份文件:前者是给模型的规范,中者是宿主的实现,后者是两者的契约校验。

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询