跳到主要内容

工具系统:加载、去重调度、审批与 MCP

30 秒导读: 模型只会"说话"——它输出一段 JSON 说"我要跑 ls"。本章讲 Kimi CLI 怎么把这句话 接到真实的手脚上并安全执行:KimiToolset 在启动时按 模块:类 路径把工具动态装载并注入依赖; 运行时把模型的每个调用并发调度、对重复调用做同步/跨步去重(重复到一定次数就升级提醒、直到强制 停手);每个有副作用的动作在真正落地前经 Approval 向用户要许可;需要外部能力时经 MCPWire 把第三方工具挂进同一条流水线。读完你能追一条工具调用从"模型出参"到"落地 + 审批 + 去重"的全链路。

本章属于 Kimi CLI 系列。上一章 03-context-and-memory.md 讲上下文怎么存; 主循环怎么"走一步"见 02-agent-loop.md;工具里最巧的时间回溯/自动循环见 05-clever-mechanisms.md;把整个任务委派给子智能体见 06-subagents-and-background.md。全景与阅读地图在 index.md


1. 先建立直觉:工具是模型缺的"手脚"

大模型只有嘴、没有手。 它能生成"要执行的动作"的描述,但不能自己动手。工具(tool)就是那只手—— 一段真实代码,加上一份告诉模型"这个动作叫什么、要传什么参数"的 schema。

一次典型的工具往返长这样(高层视角,先不进代码):

模型输出 KimiToolset 真实世界
───────── ─────────── ────────
{name:"Shell", ──▶ ① 找到 Shell 工具
arguments: ② 解析/规范化参数
{command:"ls"}} ③ 去重检查(见过吗?)
④ PreToolUse 钩子
⑤ 请求审批 ──────────────▶ 用户点"允许"
⑥ 真正执行 ──────────────▶ 子进程跑 `ls`
⑦ PostToolUse 钩子 + 遥测
文件列表 ◀──────────── ⑧ 打包成 ToolResult 回给模型

这条链的每一步都由本章的四块拼图负责:

拼图干什么核心文件
装载启动时按 模块:类 路径动态导入工具、注入依赖soul/toolset.py:KimiToolset.load_tools
调度 + 去重并发执行、对重复调用同步/跨步去重并升级提醒soul/toolset.py:KimiToolset.handle
审批每个副作用落地前向用户要许可soul/approval.py:Approval.request
外接把 MCP 服务器 / Wire 外部工具挂进同一条流水线soul/toolset.py:MCPTool / WireExternalTool

KimiToolset(soul/toolset.py:207)是把这四块缝在一起的中枢。它实现了 kosong 框架要求的 Toolset 协议(toolset.py:88-91type_check 就是在编译期断言这一点),所以主循环里 kosong.step(...) 只认这一个对象——见 02-agent-loop.md


2. 工具装载:从字符串到能被调用的对象

2.1 工具清单是一串 模块:类 字符串

Kimi 不把工具写死在代码里,而是在**智能体配置(agent.yaml)**里列一串路径。默认智能体的清单 (src/kimi_cli/agents/default/agent.yaml)节选:

tools:
- "kimi_cli.tools.agent:Agent"
- "kimi_cli.tools.shell:Shell"
- "kimi_cli.tools.file:ReadFile"
- "kimi_cli.tools.file:Grep"
# ...

装配阶段(soul/agent.py:451)把这串路径连同一张依赖表交给 toolset.load_tools(tools, tool_deps)。依赖表(soul/agent.py:445-456)是"类型 → 实例"的字典: Approval → runtime.approvalEnvironment → runtime.environment 等,后面注入时按类型对号入座。

2.2 _load_tool:动态导入 + 按类型注解注入依赖

单条路径的装载在 KimiToolset._load_tool(toolset.py:610)。它做三件事:

  1. 拆分并导入:把 "kimi_cli.tools.shell:Shell" 从最后一个 : 切成 模块名类名, 用 importlib.import_module 动态导入(toolset.py:613-623)。导入失败只记 warning、返回 None(不炸整个启动)。
  2. 按位置参数的类型注解注入依赖:如果工具类重写了 __init__,就遍历它的签名参数, 拿每个参数的类型注解去依赖表里取实例填进去(toolset.py:632-641)。
  3. 一遇到 keyword-only 参数就停止注入(toolset.py:635-637)——这是它区分"要注入的依赖"和 "工具自己的配置项"的约定。

真实源码(toolset.py:632-642):

if "__init__" in tool_cls.__dict__:
for param in inspect.signature(tool_cls).parameters.values():
if param.kind == inspect.Parameter.KEYWORD_ONLY:
break # 碰到 kw-only 就不再注入
if param.annotation not in dependencies:
raise ValueError(f"Tool dependency not found: {param.annotation}")
args.append(dependencies[param.annotation])
return tool_cls(*args)

对照 Shell 的构造签名 def __init__(self, approval: Approval, environment: Environment, runtime: Runtime)(tools/shell/__init__.py:67),这三个位置参数的注解正好命中依赖表——于是 Shell 拿到了自己需要的审批器、环境、运行时,而模型完全不用关心这些。这是一套基于类型注解的 简易依赖注入,让工具"声明我要什么",框架负责"喂进来"。

2.3 SkipThisTool:工具可以拒绝上架

工具在装载时能主动"弃权"。load_tools 捕获 SkipThisTool(tools/__init__.py:9), 一旦某工具的构造抛出它,就跳过这个工具、继续装别的(toolset.py:598-599)。用途:某工具在当前 环境不可用(比如缺依赖、平台不支持)时,自己声明"别装我",而不是让整个智能体起不来。

2.4 extract_key_argument:给每个工具挑一个"展示用关键参数"

装载之外,tools/__init__.py 还提供了一个纯展示用的辅助——extract_key_argument (tools/__init__.py:16)。UI 在流式渲染工具调用时(ui/shell/visualize/_blocks.py:374acp/session.py:107)要给用户显示一句"它在干嘛",于是按工具名从(可能还没流式接收完整的) JSON 参数里挑出最有代表性的那个字段:

工具挑出来当标题的字段
Shellcommand
ReadFile / WriteFile / Glob / Greppath / pattern
Agentdescription
SearchWeb / FetchURLquery / url
SetTodoList / SendDMail返回 None(不显示参数)

挑完还会用 shorten_middle(..., width=50) 把过长的值中间截断(tools/__init__.py:96), 路径类的值先经 _normalize_path 去掉 cwd 前缀(tools/__init__.py:99)。它甚至能吃 streamingjson.Lexer——参数还没接收完就先渲染个大概。

2.5 内置工具清单(职责一览)

默认智能体装载的内置工具,按目录分布在 src/kimi_cli/tools/ 下。想读某个工具的真章,直接打开对应 文件里的类:

工具类职责源码
Shell跑 shell 命令,支持前台/后台、超时tools/shell/__init__.py:60
ReadFile读文本文件(带行偏移)tools/file/read.py:63
ReadMediaFile读图片/音视频等媒体文件tools/file/read_media.py:50
Glob按 glob 模式找文件tools/file/glob.py:55
Grep在代码里搜正则tools/file/grep_local.py:385
WriteFile写/追加文件(有副作用,需审批)tools/file/write.py:40
StrReplaceFile字符串替换式改文件(需审批)tools/file/replace.py:43
SearchWeb网页搜索tools/web/search.py:41
FetchURL抓取一个 URL 的内容tools/web/fetch.py:22
AgentTool把子任务委派给子智能体(名叫 Agent)tools/agent/__init__.py:65
AskUserQuestion反问用户tools/ask_user/__init__.py
SetTodoList维护待办清单tools/todo/__init__.py
TaskList/TaskOutput/TaskStop管理后台任务tools/background/__init__.py
EnterPlanMode/ExitPlanMode进/出"只读规划"模式tools/plan/enter.py:30tools/plan/__init__.py:81

注: 每个工具目录里的 *.md(如 shell/bash.md)是给模型看的工具描述文本,装载时由 load_desc(...) 读进来当 description(见 tools/shell/__init__.py:70)。子智能体委派工具 Agent、后台任务、D-Mail 等的深入解析分别见 0605


3. 调度与去重:一步内并发、跨步防打转

这是本章工程含量最高的一块。它要解决两个真实痛点:

  • 一步里模型可能同时发多个工具调用——要并发执行,还要处理"同一步里重复发同一个调用"。
  • 模型会打转——连续多步反复发一模一样的调用却毫无进展(常见的智能体病)。Kimi 用一套 "重复即升级、直到强制停手"的机制掐断它。这条设计是本章的关键。

3.1 参数规范化:什么算"同一个调用"

判重的前提是"两个调用一样"要有稳定定义。_canonical_tool_arguments(toolset.py:162)把参数 递归按 key 排序(_sort_json_value,toolset.py:153)再序列化成紧凑 JSON——于是 {"a":1,"b":2}{"b":2,"a":1} 会规范成同一个字符串。一个调用的去重键就是 (工具名, 规范化参数)(ToolCallKey,toolset.py:85)。

3.2 handle:每个工具调用的入口

主循环里 kosong.step 对模型发出的每个调用调用一次 KimiToolset.handle(toolset.py:318)。 它返回一个 asyncio.Task(不是直接的结果)——所以一步内的多个调用天然并发。handle 的流程:

handle(tool_call):
├─ 设 current_tool_call ContextVar (让工具内部能拿到自己的调用)
├─ 工具不存在? → ToolNotFoundError
├─ 参数 JSON 解析失败? → ToolParseError
├─ 算 call_key、记进本步 _current_step_calls
├─ ① 同步去重:本步已有相同 key 的任务?→ 等它、复制结果(不重跑)
├─ ② 跨步去重:_seen_call_keys 里见过?→ 算 streak、挑升级提醒、记遥测
└─ 建 asyncio.Task 执行 _call(),存进 _current_step_tasks 返回

① 同步(同一步)去重(toolset.py:346-356):如果这一步内已经有一个相同 call_key 正在跑,新的调用不会再跑一遍——它 await 原任务、把结果复制一份(换上自己的 tool_call_id) 返回。省掉重复执行,又保证每个 tool_call_id 都有应答。

② 跨步去重(toolset.py:358-375):如果这个 key 在之前的步骤里出现过(_seen_call_keys), 就不拦、但追加一段升级式提醒到工具结果尾部,并把 _dedup_triggered 置真;若升级到最高档还会 把 _force_stop_turn 置真。

3.3 执行体 _call:钩子 + 遥测 + 错误兜底

真正跑工具的是内层协程 _call(toolset.py:379),顺序:

  1. PreToolUse 钩子(toolset.py:385-404):触发用户配置的前置钩子;任一钩子返回 block 就 直接返回 ToolError,工具不执行(见 05 的钩子机制)。
  2. 执行工具:ret = await tool.call(arguments)(toolset.py:409)。抛异常则记 exception、 触发 PostToolUseFailure 钩子(fire-and-forget)、发 outcome="error" 遥测,返回 ToolRuntimeError(toolset.py:410-448)。
  3. PostToolUse 钩子 + 成功/失败遥测(toolset.py:459-492):遥测里带 dup_type 标注是否跨步重复。

3.4 升级式提醒:r1 → r2 → r3 → 强停

这是"防打转"的核心。KimiToolset 维护一条连续重复计数——_consecutive_key + _consecutive_count(toolset.py:221-222)。每步结束时 _advance_consecutive_streak (toolset.py:290)扫这一步的调用:和上一个 key 相同就 count += 1,否则重置为 1。也就是说, 它数的是"连续发同一个调用"的次数,一旦换了别的调用,streak 归零。

_build_repeat_reminder(toolset.py:139)按 streak 阈值选动作:

streak(连续重复次数)动作追加给模型的提醒
≥ 3r1"你在重复完全相同的调用,请分析上次结果、换方法或换参数"
≥ 5r2更严厉:列出 tool / repeated_times / arguments,"别再用完全相同的参数调这个工具"
≥ 8r3"你陷入死胡同了,立即停止所有函数调用"
≥ 12stop同 r3 文本,_force_stop_turn = True,本轮强制收尾

阈值常量在 toolset.py:131-134(_REPEAT_REMINDER_1/2/3_START = 3/5/8_REPEAT_FORCE_STOP_STREAK = 12)。提醒文本靠 _append_reminder_to_return_value (toolset.py:184)拼到工具返回值的最后一个 TextPart 尾部——模型下一步就能"看见"这句话。

主循环怎么消费这两个信号(soul/kimisoul.py):

  • 每步开始 toolset.begin_step(self._last_tool_calls)(kimisoul.py:1081)——把上一步的调用 并进 _seen_call_keys、重置本步状态。
  • 每步结束 self._last_tool_calls = toolset.end_step()(kimisoul.py:1160)——推进 streak、 把本步调用并进 _seen_call_keys
  • 最后检查 if toolset.force_stop_turn:(kimisoul.py:1214)——为真就发 turn_force_stopped 遥测、以 stop_reason="tool_call_repeat" 结束本轮。

一句话:**重复不被硬拦,而是被"越来越大声地劝阻",劝到第 12 次直接替模型收手。**这比"直接禁止 重复"更稳——偶尔的合理重试不会被误杀,真打转才会被强停。


4. 审批:每个副作用落地前先问用户

工具能读能写,但"写"这类有副作用的动作在真正落地前必须过一道用户许可。审批不在 handle 里 统一做,而是由工具自己在动作前调用 approval.request(...)——因为只有工具知道自己这一步危不危险。

4.1 工具侧:请求一次审批

Shell 为例(tools/shell/__init__.py:92-103):

result = await self._approval.request(
self.name, # sender: 谁在请求
"run command", # action: 动作标识(用于"本会话都批准")
f"Run command `{command}`", # description: 给用户看的话
display=[ShellDisplayBlock(language="bash", command=command)],
)
if not result:
return result.rejection_error()

注意 if not result:——ApprovalResult(approval.py:22)重载了 __bool__,所以它既能带 feedback,又能像布尔一样用。被拒时 result.rejection_error() 生成一个 ToolRejectedError 返回给模型。WriteFile(tools/file/write.py:146)、StrReplaceFile (tools/file/replace.py:160)、后台 Shell(tools/shell/__init__.py:154)同理。

4.2 Approval.request:四条快速通道 + 一条等待路径

Approval.request(approval.py:157)的判定顺序:

request(sender, action, description):
├─ 不在工具调用上下文? → 抛 RuntimeError (审批只能从工具里发)
├─ is_auto_approve()? → 直接批准 (yolo 或 afk)
├─ action ∈ auto_approve_actions? → 直接批准 (本会话已对该 action 说"都批")
└─ 否则:建请求、经 Wire 推给 UI、await 用户响应

自动批准的两种来源(approval.py:126-140):

  • yolo——用户显式开的"全放行"。
  • afk(away-from-keyboard,无人值守)——--afk/--print 等场景;is_auto_approve() 里 "afk 隐含 auto-approve"(approval.py:123-131)。afk 又分持久(state.afk,随会话保存)和 本次调用(runtime_afk,不持久,approval.py:66-72)两层。

"本会话都批准"——它不是一个独立方法,而是 Approval.requestawait 到用户响应后、对 返回值做 match 时的一个分支 case "approve_for_session":(approval.py:242,match response 起于 approval.py:234)。命中它时,把 action 加进 auto_approve_actions(approval.py:248), 并且顺手把队列里所有同 action 的待决请求一并放行(遍历 list_pending()、逐个 resolve, approval.py:250-252)。下次同类动作就走上面第三条快速通道,不再打扰用户。

4.3 子智能体的拒绝语更"劝退"

ApprovalResult.rejection_error(approval.py:33)对子智能体有特化。它用 get_current_approval_source_or_none() 看当前审批来源是否带 agent_id(即来自子智能体, approval.py:41-42);是的话返回一段更强硬的拒绝语:"换个思路完成任务,或在总结里说明限制…… 不要重试同一个调用,也不要绕过这个限制"(approval.py:43-52)。这是为了防子智能体在被拒后 反复硬撞或绕道。

4.4 ApprovalRuntime:请求的登记簿与等待器

真正管"请求→等待→响应"生命周期的是 ApprovalRuntime(approval_runtime/runtime.py:48):

  • create_request(runtime.py:61)——建一条 ApprovalRequestRecord、发事件、经 Wire 推给 前端。
  • wait_for_response(runtime.py:86)——为该请求建/取一个共享 Futureawait;支持 多个观察者共享同一等待器、支持超时(超时即取消并当作拒绝)。
  • resolve(runtime.py:129)——UI 有了答复就 set_result,唤醒等待者。
  • list_pending(runtime.py:183)——按创建时间排序列出待决请求,"本会话都批准"就靠它批量放行。

ApprovalSource(approval_runtime/models.py:16)标明这次审批"从哪来":kindforeground_turn(前台对话)还是 background_agent(后台智能体);带不带 agent_id/ subagent_type 决定了上面 §4.3 的拒绝语措辞,以及能否按来源批量取消(cancel_by_source, runtime.py:170)。


5. 外接工具:MCP 与 Wire

内置工具之外,Kimi 还能把外部能力挂进同一条 handle 流水线——它们和内置工具在 _tool_dict 里平权,模型看不出区别。两条路子:

5.1 MCP 工具:连第三方服务器

MCP(Model Context Protocol,模型上下文协议)是一套让工具跨进程/跨服务暴露的标准。Kimi 用 fastmcp 当客户端。

后台加载与状态机。 MCP 服务器可能慢,所以装载是延迟 + 后台的: defer_mcp_tool_loading(toolset.py:563)先把配置存起来, start_deferred_mcp_tool_loading(toolset.py:571)再择机启动 load_mcp_tools(..., in_background=True)(toolset.py:645)。每台服务器一个 MCPServerInfo(toolset.py:801),状态在这几个值间流转:

pending ──▶ connecting ──▶ connected (list_tools 成功、工具已 add 进 toolset)
└─▶ failed (连接抛异常)
└─(需 OAuth 但未授权)─▶ unauthorized (提示 `kimi mcp auth <name>`)

连接在 _connect_server(toolset.py:685)里做:进入 client 上下文、list_tools()、把每个 远端工具包成 MCPTool 加进 toolset。has_pending_mcp_tools/wait_for_mcp_tools (toolset.py:771775)让别处能查/等后台加载完成。mcp_status_snapshot(toolset.py:542) 给 UI 一个只读快照(连了几台、多少工具)。

MCPTool.__call__(toolset.py:833)和内置工具一样先 approval.request(动作名 mcp:<工具名>,toolset.py:831),批准后经 fastmcp 客户端 call_tool,带 tool_call_timeout_ms 超时。超时被特判成一句友好的 ToolError(提示用户超时配置可能太低, toolset.py:857-869)。

输出预算裁剪。 MCP 工具(尤其像返回整个 DOM 的 Playwright)可能吐超大结果,会撑爆上下文。 convert_mcp_tool_result(toolset.py:938)按 MCP_MAX_OUTPUT_CHARS = 100_000 (toolset.py:924)做共享字符预算:文本部分超预算就就地截断,媒体部分(图/音/视频,按 data-URL 长度算)超预算就整块丢弃并留占位说明(toolset.py:963-983);任何被裁的情况都在 末尾追加一句"[Output truncated…]"(toolset.py:988-996)。内置工具走 ToolResultBuilder 的 50K 预算,MCP 给到 100K——因为多模态多部件结果更常见,但仍必须封顶。

5.2 Wire 外部工具:反问 UI 去执行

有些工具不由 Python 侧执行,而要交给前端(UI/宿主)去做——比如 ACP 集成里由编辑器执行的动作。 WireExternalTool(toolset.py:878)就是这种"壳工具":register_external_tool (toolset.py:516)按 UI 传来的 name/description/parameters 注册它。

它的 __call__(toolset.py:886)不干活,而是:拿当前 tool_call,包成 ToolCallRequest,经 Wire 的 soul_side.send(...) 发给 UI 端,然后 await external_tool_call.wait() 等 UI 把结果回传(toolset.py:906-909)。Wire 是 Soul(智能体侧)和 UI 之间的双向通道—— 详见 02-agent-loop.md。这样"工具在哪执行"对模型完全透明:它照样发一个工具调用, 只是这次的手脚长在前端。


6. 全链路回放:一条 Shell 调用的一生

把四块拼在一起,追一次 Shell(command="ls"):

① 模型输出 {name:"Shell", arguments:{command:"ls"}}
② kosong.step 对它调 KimiToolset.handle (toolset.py:318)
③ 设 current_tool_call ContextVar;查到 Shell 工具
④ 规范化参数 → call_key=("Shell",'{"command":"ls"}') (toolset.py:340-341)
⑤ 本步已有同 key 任务? 无 → 继续 (toolset.py:346)
⑥ _seen_call_keys 里见过? 若见过则算 streak、追加升级提醒 (toolset.py:358)
⑦ 建 Task 跑 _call:
├─ PreToolUse 钩子(可 block) (toolset.py:385)
├─ tool.call(arguments) → Shell.__call__
│ └─ approval.request("run command", …) (shell:92)
│ └─ 非 auto/未记忆 → 经 ApprovalRuntime 推 UI、await 用户
│ └─ 批准 → 跑子进程 `ls`、收集 stdout
└─ PostToolUse 钩子 + 遥测 (toolset.py:459)
⑧ 返回 ToolResult;end_step 推进 streak、并入 _seen_call_keys (kimisoul.py:1160)
⑨ 主循环检查 force_stop_turn,决定是否强制收尾 (kimisoul.py:1214)

模型只写了第 ①步的一句 JSON,剩下 8 步——找工具、去重、钩子、审批、执行、遥测、回填——全由 KimiToolset 这套流水线在幕后接上并守好。


7. 边界与局限

  • 去重按"完全相同的参数"判(§3.1)。参数只要差一个字节(哪怕语义等价、只是空格不同)就不算重复—— 规范化只做 key 排序,不做语义归一,所以"换汤不换药"的打转能骗过跨步去重,得靠 streak 数别的机制兜底。
  • 强停靠"连续"重复:streak 一旦换了个别的调用就归零(_advance_consecutive_streak, toolset.py:290)。A、B、A、B……交替刷两个无进展调用不会累积到强停阈值。
  • 审批要靠工具自觉:handle 不强制任何工具审批,是每个工具自己决定要不要 approval.request。新工具若忘了在副作用前请求审批,框架不会替它补。
  • MCP 输出封顶在 100K 字符(§5.1):超大结果被截断/丢弃,靠工具自身的分页/更精确查询来补—— Kimi 侧不做自动分页。
  • MCP 超时判定是字符串匹配:fastmcp 超时抛的是普通 RuntimeError,Kimi 只能靠错误信息里有没有 "timeout" 来猜(toolset.py:855-857,源码注释亦承认这点)。

8. 代码地图(导航索引)

主题文件路径符号名
工具集中枢 / Toolset 协议src/kimi_cli/soul/toolset.py:207KimiToolset
批量装载工具src/kimi_cli/soul/toolset.py:584KimiToolset.load_tools
单工具动态导入 + 依赖注入src/kimi_cli/soul/toolset.py:610KimiToolset._load_tool
工具弃权src/kimi_cli/tools/__init__.py:9SkipThisTool
展示用关键参数提取src/kimi_cli/tools/__init__.py:16extract_key_argument
调用入口(并发 + 去重)src/kimi_cli/soul/toolset.py:318KimiToolset.handle
当前调用上下文src/kimi_cli/soul/toolset.py:59current_tool_call (ContextVar)
参数规范化src/kimi_cli/soul/toolset.py:162_canonical_tool_arguments
去重键构造src/kimi_cli/soul/toolset.py:180_normalize_call_key
每步开始/结束src/kimi_cli/soul/toolset.py:263282begin_step / end_step
连续重复计数src/kimi_cli/soul/toolset.py:290_advance_consecutive_streak
升级式提醒选择src/kimi_cli/soul/toolset.py:139_build_repeat_reminder
重复阈值常量src/kimi_cli/soul/toolset.py:131-134_REPEAT_REMINDER_*_START
主循环消费去重/强停src/kimi_cli/soul/kimisoul.py:108111601214begin_step/end_step/force_stop_turn
审批请求入口src/kimi_cli/soul/approval.py:157Approval.request
"本会话都批准"分支src/kimi_cli/soul/approval.py:242Approval.requestcase "approve_for_session"
审批结果(带 bool)src/kimi_cli/soul/approval.py:22ApprovalResult
子智能体拒绝特化src/kimi_cli/soul/approval.py:33ApprovalResult.rejection_error
审批生命周期运行时src/kimi_cli/approval_runtime/runtime.py:48ApprovalRuntime
审批来源标识src/kimi_cli/approval_runtime/models.py:16ApprovalSource
MCP 工具src/kimi_cli/soul/toolset.py:808MCPTool
MCP 后台加载src/kimi_cli/soul/toolset.py:645KimiToolset.load_mcp_tools
MCP 服务器状态机src/kimi_cli/soul/toolset.py:801MCPServerInfo
MCP 输出预算裁剪src/kimi_cli/soul/toolset.py:938convert_mcp_tool_result
Wire 外部工具src/kimi_cli/soul/toolset.py:878WireExternalTool
Shell 工具(审批范例)src/kimi_cli/tools/shell/__init__.py:60Shell