Kimi 动态工具加载是怎么回事:按需注入工具与前缀缓存
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
一句话说清:Kimi 的动态加载工具(Dynamically Loaded Tools)不是新增了什么 API 端点,而是允许你在 messages 里插入一条 role 为 system、带 tools 字段的消息,把工具声明追加到对话中途,从这条消息所在的位置开始对模型可见。它要解决的是官方文档里点名的「工具定义膨胀(Tool Definition Bloat)」——所有工具一次性塞在请求顶层 tools 字段里,每个请求都得携带全部工具的描述和参数 schema,token 消耗高,而且候选工具越多,模型越容易选错工具、构造出错误的调用参数。它最值钱的性质是「追加不破前缀」:工具声明只往 messages 尾部追加,已有对话前缀保持不变,因此不会打掉已经建立起来的前缀缓存,可以和上下文缓存叠加使用。但它有两条硬限制经常被忽略:按官方文档说明,这个能力目前仅 kimi-k3 支持,在其他模型上会返回 tokenization failed 错误;而且这条携带 tools 的 system 消息不能再带 content 字段,否则请求会以 400 报错。
先搞清楚它到底在治什么病
常规的工具调用写法是把所有工具定义放进请求顶层的 tools 数组。工具少的时候这没什么问题,一旦一个 Agent 挂到几十上百个工具,两个代价就同时出现了。
第一个代价是 token。官方文档把这件事说得很直白:每个请求都要携带全部工具的描述和参数 schema。注意这里是每个请求——多轮对话里你不是只付一次工具定义的钱,而是每一轮都要重新把整份工具目录送进去。工具的 parameters 是完整的 JSON Schema,嵌套一深,单个工具的声明就不算短了。
第二个代价是准确率。官方的说法是候选工具越多,模型也越容易选错工具、构造出错误的调用参数。这一条其实比 token 那条更麻烦,因为 token 花超了你从账单上看得见,选错工具你得从业务结果里倒查。
动态加载工具给出的思路是:先只挂载少量核心工具,当对话进展到需要某个工具时,再把它动态插入 messages 中。这样同一轮请求里实际存在的工具声明始终只有少量几个,上下文占用和模型的选择压力都能压住。
注入的写法:一条带 tools 字段的 system 消息
具体做法是在 messages 中插入一条 role 为 system 的消息,通过这条消息的 tools 字段声明要加载的工具。官方文档强调了三点格式约束,每一点都值得单独记住:
声明格式与请求顶层 tools 字段完全一致。 也就是说 type 仍然是 function,里面仍然是 function 对象带 name、description、parameters。官方在注意事项里点明这个设计的用意:动态工具声明与全局 tools 声明格式完全统一,接入方无需维护两套 schema,迁移成本低。对已经写好一套工具注册表的项目来说,这意味着你不用重新序列化,把同一份工具定义换个位置放就行。
必须提供完整信息。 官方原文写的是需要提供工具的完整信息(name、description、parameters),并且明确说了:动态注入的工具声明必须是完整的工具定义,不能只传工具名或引用全局已声明的工具。这一条挺关键——不存在「先在顶层登记、后面按名字激活」这种轻量引用写法,每次注入都是一份完整声明。
动态加载的工具与顶层 tools 声明的全局工具并存。 模型可以同时看到两类工具。所以这不是替换关系,你不需要为了用动态加载而把顶层 tools 清空。
调用方式上没有任何特殊之处:官方给的 curl 示例仍然打到 https://api.moonshot.cn/v1/chat/completions,Python 示例用的是 OpenAI SDK,base_url 指向 https://api.moonshot.cn/v1。文档在注意事项里补了一句很实用的:使用 OpenAI SDK 时可直接在 messages 中透传 tools 字段,无需 extra_body。如果你刚从别家平台迁过来、习惯了用 extra_body 塞非标准字段,这里可以省一层包装。Kimi 侧的基础接入步骤可以参考Kimi API 怎么接入。
位置即可见性:这个设计有点反直觉
官方对这条消息的定位是:携带 tools 的 system 消息与普通的 input messages 地位相同——它出现在 messages 列表的哪个位置,工具就从哪个位置开始对模型可见。
这一点值得停下来想一下。顶层 tools 字段是无位置概念的,声明了就是全程可见;而动态注入的工具带上了「时间」这个维度:注入点之前的那段对话,模型是看不到这个工具的。
这个性质有好有坏。好处是它天然支持分阶段编排——第一阶段只给检索类工具,等确定了任务方向再把写操作类工具放出来,从模型视角看,前半段对话里那些危险工具根本不存在,不需要靠 prompt 去劝阻。坏处是排查问题时的直觉容易失灵:你在请求体里明明看到了工具声明,模型却没调用它,那就要回头看这条声明是不是插在了那句用户提问的后面。
还有一条状态相关的规则必须记牢:动态工具声明按请求生效,不会被服务端记住。 服务端不维护「这个会话已经加载过哪些工具」的状态。官方给的建议是在后续请求中原样保留已加载的工具声明,这样工具保持可用、前缀也保持稳定;文档同时说明你也可以根据业务需要自行决定是否继续携带,但若不再携带,该工具声明即失效,如果这个工具没有在其他位置声明,模型就无法调用它。
工具上百个时:官方推荐自己实现 tool search
官方文档里有一句话说得很干脆:API 层面没有专门的 tool search 接口。 想要工具检索能力,得靠「自定义 search 工具 + 动态加载工具」自己拼出来。官方给出的组合方式是四步:
- 在请求顶层
tools中只声明一个search_tools工具,由你的后端实现,按关键词返回匹配的工具名称和简介; - 在 system prompt 中声明可被搜索的关键词(例如工具目录、领域标签),引导模型在需要工具时先调用
search_tools; - 根据
search_tools返回的结果,由你的应用把对应工具的完整声明,通过一条携带tools的system消息动态插入messages; - 模型即可在后续生成中直接调用这些新加载的工具。
官方对这套编排的结论是:这样无论工具总量有多大,每一轮请求中实际存在的工具声明都只有少量几个,上下文窗口和模型的选择压力都可控。
在配套的工具调用最佳实践页里,官方还补了一个细节:模型可以选择不调用任何工具、直接凭记忆作答,所以为了确保它先检索再回答,首轮请求可以设置 tool_choice 为 required,检索完成后再把 tool_choice 恢复为 auto。这里有个对缓存友好的性质——官方明确写了修改 tool_choice 不会破坏前缀缓存,可以按请求粒度调整。
同一页还提到请求顶层的 reasoning_effort 取值有 low、high、max 三档(以官方文档当前版本为准),并建议在会话开始前就把这个配置定下来。
和前缀缓存的关系:追加,不要插入
这是整个特性里最需要动脑子的部分,也是它真正的价值所在。
上下文缓存按前缀匹配:只有当前请求与之前请求完全一致的前缀部分才能命中缓存,前缀中任何位置发生变化,该位置之后的缓存都会失效。所以工具声明的注入方式直接决定缓存命中率。官方给了三条原则:
追加,不要插入。 新的工具声明一律追加到 messages 末尾,已有前缀保持不变,不影响已建立的缓存。反过来,向对话中间插入或修改任何消息——包括已注入的工具声明本身——都会使变更位置之后的缓存无法命中。很多人以为「我只是把某个用不上的工具声明删掉,省点 token」是稳赚的,实际上删除动作发生在对话中间,它后面那一大段前缀的缓存就一起没了。
保留已注入的声明。 承接上面那条状态规则:既然服务端不记住,那么继续原样携带既能让工具保持可用,又能让前缀保持稳定,有利于持续命中缓存。若不再携带,除了工具失效,messages 本身也发生了变化,变更位置之后的前缀缓存同样可能无法命中。
核心工具固定在顶层声明,之后不再改动。 官方的说法是顶层全局工具声明不影响缓存命中,保持稳定即可让前缀缓存持续有效;只有按需使用的工具才做动态注入。这条给出了一个清晰的分工判据:每轮都要用的放顶层,偶尔才用的走动态注入。
官方还用一张表把四种操作对前缀缓存的影响列了出来:在 messages 末尾追加工具声明——不影响已有前缀缓存;后续请求原样保留已注入的工具声明——前缀保持稳定,有利于持续命中缓存;删除、修改对话中间的消息或在中间插入新声明——变更位置之后的缓存可能无法命中;在顶层 tools 字段声明全局工具——不影响缓存命中。
另外有一道容易被忽略的门槛:官方规定了一个 prompt tokens 的下限,只有前一个请求的 prompt tokens 高于该门槛,新的请求才能命中前缀缓存;低于门槛的请求不会被缓存而是被丢弃。具体数值以官方上下文缓存文档为准。这意味着对话开头那几轮很短的请求,缓存本来就不参与,别把命中率算在它们头上。缓存这件事在跨厂商层面的通用机制,可以对照API 缓存计费机制理解。
上下文缓存页里还有一条与工具编排直接相关的提示:请确保知识内容、system prompt 和工具定义相对稳定,以获得更好的缓存命中率。这句话和动态加载并不矛盾——动态加载改的是「新增什么」,不是「改动已有的什么」。
四条限制,先看完再动手
官方注意事项部分列了几条,逐条拆开:
目前仅 kimi-k3 支持。 官方原文说明动态加载工具目前仅 kimi-k3 支持,在其他模型(如 kimi-k2.6)上请求会返回 tokenization failed 错误(以官方文档当前版本为准)。这个错误信息比较有迷惑性——它长得像是你的输入内容有问题,实际上是模型不支持这种消息结构。如果你的系统里有模型路由或降级逻辑,请求可能被路由到了不支持的模型上,这时错误信息不会告诉你「模型不支持该特性」,只会给你一个分词失败。
带 tools 的 system 消息不能再带 content 字段。 否则请求会以 400 报错,报错信息里带 cannot be used with content。这一条对代码结构是有影响的:如果你的消息构造函数默认给每条 system 消息填一个空字符串 content,那这条消息就会稳定 400。构造动态注入消息时得走一条单独的分支,而不是复用通用的 system 消息构造器。
这条消息同样占用上下文长度。 官方特意提醒:携带 tools 的 system 消息同样会占用上下文长度,请只对当前对话真正需要的工具做动态注入。动态加载省的是「不相关工具的声明」,不是「工具声明本身」——注入进来的那份完整 schema 该占多少还是占多少。配合上面「保留已注入的声明」那条一起看,一个长会话如果无脑注入,工具声明会像雪球一样越滚越大,最后又回到了工具定义膨胀。所以检索出来的候选工具要控制条数,这是应用层要做的判断,官方文档没有给出具体的推荐条数。
格式统一,迁移成本低。 这是唯一一条好消息:动态工具声明与全局 tools 声明格式完全统一,接入方无需维护两套 schema。
最容易栽的坑
按官方文档梳理下来,实际接入时最容易出问题的是三处,按踩中概率排序:
一是模型选错。这个特性绑定在特定模型上,而 tokenization failed 这个错误信息不会提示你去查模型支持范围,容易往输入内容上排查,白费半天工夫。
二是消息构造器复用。带 tools 的 system 消息和普通 system 消息长得太像了,共用一个构造函数几乎必然带上 content 字段,然后拿到那个 400。
三是为了省 token 去删中间的工具声明。直觉上删声明是省钱,实际上删除动作打掉的是它之后的整段前缀缓存,账单上很可能是负收益。真要控制上下文,正确做法是在注入前就克制——少注入几个,而不是注入后再删。
想估算这套编排到底省不省,别只看单轮请求里少了几个工具声明,得把缓存命中率的变化一起算进去。用量与成本层面的监控方法可以参考API 成本监控怎么做,Kimi 侧的计费口径见Kimi API 计费说明。