跳到主要内容

工具套件:模型的手脚如何精确落到真实目标

30 秒导读: 模型只会"说话"。工具套件给它装上手脚——让它说的那段话,精确、可靠地落到某个真实目标(一个文件、一个 shell 进程、一次网络请求、一个子 agent)上。本章解剖 packages/agent-core/src/tools/ 里内置工具的统一形态,再逐类走一遍最能体现工程量的几支,最后讲 skill 系统怎么把一份 SKILL.md 变成可注入的能力。

本章不讲什么(交给别章):

  • 工具如何被回合循环调度(一次工具调用从请求到结果的一生)——见 01-loop.md
  • 工具如何过权限闸门(approval、permission mode)——见 02-agent.md。本章只讲工具自己声明了哪些用于闸门的字段(approvalRule / matchesRule / accesses),不讲闸门怎么判。
  • 抹平大模型、抹平执行环境的 kosong / kaos 两层——见 04-providers.md

1. 先建立直觉:一支工具是什么

在 Kimi Code 里,一支内置工具就是一个实现了 ExecutableTool 接口的。它对模型暴露三样东西——名字、说明书、参数格式;对运行时暴露一个方法——resolveExecution

先看接口本身,只有一个方法比普通的 kosong Tool 多:

// packages/agent-core/src/loop/types.ts:157 —— 真实源码
export interface ExecutableTool<Input = unknown> extends Tool {
resolveExecution(input: Input): ToolExecution | Promise<ToolExecution>;
}

Tool(来自 kosong)提供 name / description / parameters 三个字段,是给模型看的"说明书";resolveExecution 是给运行时的"动作解析器"。内置工具的类型别名就是它(agent/tool/types.ts:5,BuiltinTool<Input> = ExecutableTool<Input>)。

一句话直觉: 说明书(name/description/parameters)是模型的;resolveExecution 返回的那个对象是运行时执行 + 授权 + 渲染的。两者严格分家。


2. 一支工具的解剖:五件套

Kimi Code 每支内置工具都由同样五件套拼成。理解了这五件,就理解了整个套件——剩下的都是同一个模子里换动作。以文件编辑工具 EditTool(builtin/file/edit.ts)为范例。

2.1 五件套一览

是什么EditTool 里的样子源码锚点
① 输入 schemazod 对象,描述模型该传什么EditInputSchema(path / old_string / new_string / replace_all)edit.ts:27
② 描述文本同名 .md 文件,?raw 原样注入import EDIT_DESCRIPTION from './edit.md?raw'edit.ts:22
③ execute真正干活的异步函数private execution(args, path)edit.ts:95
④ 声明式 accesses这次调用碰哪些资源(给并发/权限用)ToolAccesses.readWriteFile(path)edit.ts:75
⑤ 权限规则生成/匹配审批规则的字符串与函数approvalRule + matchesRuleedit.ts:84-90

2.2 这五件怎么拼成一个类

工具类的三个只读字段就是给模型的说明书,构造时一次算好:

// edit.ts:58-61 —— 真实源码(节选)
export class EditTool implements BuiltinTool<EditInput> {
readonly name = 'Edit' as const;
readonly description = EDIT_DESCRIPTION; // ② 同名 .md
readonly parameters = toInputJsonSchema(EditInputSchema); // ① zod → JSON Schema

注意 parameters 不是直接把 zod 丢出去,而是过一道 toInputJsonSchema(第 4 节详解)。构造函数只收两个依赖——kaos(执行环境抽象)和 workspace(工作区配置);它不碰 node 的 fs,所有 I/O 走 kaos。

2.3 resolveExecution:把一次调用"解析"成一个动作

resolveExecution(args) 不执行任何 I/O,它把参数解析成一个 ToolExecution 描述对象,交给循环。这是整套设计最关键的一层:

// edit.ts:68-92 —— 真实源码(节选)
resolveExecution(args: EditInput): ToolExecution {
const path = resolvePathAccessPath(args.path, { // 先过路径安全策略
kaos: this.kaos, workspace: this.workspace, operation: 'write',
});
return {
accesses: ToolAccesses.readWriteFile(path), // ④ 声明:读写这一个文件
description: `Editing ${args.path}`, // 给 UI 的一句话
display: { kind: 'file_io', operation: 'edit', path, before: args.old_string, after: args.new_string },
approvalRule: literalRulePattern(this.name, path), // ⑤ 生成审批规则字符串
matchesRule: (ruleArgs) => matchesPathRuleSubject(/* ... */), // ⑤ 判断某条已有规则是否覆盖本次
execute: () => this.execution(args, path), // ③ 真正干活的闭包
};
}

ToolExecution 的字段契约在 loop/types.ts:140RunnableToolExecution为什么要分成"解析"和"执行"两步? 因为循环需要在真正执行前就拿到 accesses(排并发)、approvalRule(问权限)、display(先渲染出"正在编辑 X")。把这些从副作用里拆出来,循环就能在不触碰磁盘的前提下决定"这次调用能不能跑、要不要问人、和谁冲突"。

2.4 那道 .md?raw 是什么魔法

?raw 是 Vite/构建时的导入后缀:把同目录下的 edit.md纯字符串读进来,原样作为工具描述。好处是说明书和代码分离——edit.md 是给模型读的长篇散文(几十行的用法约束),不塞进 .ts 里污染代码。

有些工具的描述还要按运行时状态改写。例如 BashTool 的描述模板里含 ${...} 占位,构造时用 renderPrompt 填入真实超时值(bash.ts:132),还会根据"这个 agent 能不能用后台任务"删掉整段后台说明(withoutBackgroundDescription,bash.ts:136)。说明书是动态的,但一旦装进 tools[] 就要求会话内逐字节稳定(见 select-tools.ts:40 的注释,变动的东西只能进公告、不能进描述)。


3. 深挖 EditTool:"精确字符串替换"而非"模糊匹配"

编辑代码文件是编码 agent 工程含量最高的动作。业界有两条路:一条是模糊匹配(容忍缩进差异、相似块 fallback),一条是 Kimi Code 选的——精确字符串替换。理解它的四条硬约束,就理解了这支工具为什么"宁可报错也不猜"。

3.1 核心动作:首个 / 全部,非唯一即报错

execute 从 kaos 读出文件,转成"模型视图"后做替换。默认只换第一个匹配,但会先数一遍:

// edit.ts:109-137 —— 真实源码(节选,非 replace_all 分支)
if (!replaceAll) {
let count = 0, pos = 0;
while (pos < content.length) {
const idx = content.indexOf(args.old_string, pos);
if (idx === -1) break;
count++;
pos = idx + args.old_string.length;
}
if (count === 0) return { isError: true, output: `old_string not found in ${args.path} ...` };
if (count > 1) return { isError: true, output: `old_string is not unique ... found ${count} occurrences ...` };
// 唯一命中 → 替换、写回
}

为什么"非唯一即报错"是关键设计? 如果 old_string 在文件里出现两次,模型往往并不知道自己想改哪一个。此时是危险的——很可能改错地方。工具直接报错,并在错误信息里教模型两条出路:要么 replace_all=true 全换,要么"把周围上下文加进 old_string 让它唯一"(edit.ts:127-128)。错误信息本身就是给模型的提示词

replace_all 分支用 content.split(old_string).join(new_string)(edit.ts:140),一次全换。

3.2 三条不显眼但要命的护栏

护栏防的是什么源码
old_string 必须非空(.min(1))空串会让 indexOf("", pos) 永远命中 → while 死循环edit.ts:34,注释在 edit.ts:24-26
old_string === new_string 提前拦什么都没改还写盘,纯浪费edit.ts:96-101
EISDIR 单独兜底把目录当文件编辑,给人话而非栈edit.ts:155-156

第一条尤其精妙:min(1) 不是随手加的校验,注释里白纸黑字写明"非 replace_all 分支用 indexOf('', pos) 走位,空串会无限循环"。这是一条防死循环的护栏,伪装成一个 schema 约束。

3.3 CRLF / LF:模型看到的和磁盘上的不是同一份

Windows 文件是 \r\n,Unix 是 \n。如果让模型在 old_string 里精确匹配 \r\n,它几乎必然匹配失败。Kimi Code 的解法是造一个**"模型文本视图"**:读盘后把纯 CRLF 文件的 \r\n 全折成 \n 给模型看;写回时再还原成 CRLF。

// line-endings.ts:32-47 —— 真实源码(节选)
export function toModelTextView(raw: string): ModelTextView {
const lineEndingStyle = detectLineEndingStyle(raw); // 'lf' | 'crlf' | 'mixed'
if (lineEndingStyle !== 'crlf') return { text: raw, lineEndingStyle };
return { text: raw.replaceAll('\r\n', '\n'), lineEndingStyle }; // 纯 CRLF 折成 LF
}
export function materializeModelText(text: string, style: LineEndingStyle): string {
if (style !== 'crlf') return text;
return text.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'); // 写回还原
}

EditTool.execution 就是先 toModelTextView(raw) 拿视图、在视图上做替换、再 materializeModelText(newContent, style) 写回(edit.ts:105-135)。混合行尾(mixed,既有 CRLF 又有裸 CR)不折叠,而是让 Read 把 \r 显式渲染成 \r(makeCarriageReturnsVisible,line-endings.ts:49),edit.md 里也明确要求这种文件用真实 \r 转义(edit.md:12-13)。同一套 line-endings.ts 被 Read 和 Edit 共用,保证"模型读到的"和"模型能改的"是同一份视图。


4. 横切机制:所有工具共享的地基

五件套只是单支工具的形状。真正让这套东西成体系的,是每支工具都踩在同一批横切设施上。逐个看。

4.1 声明式 accesses:工具自己说"我碰什么",循环据此排并发

工具不直接管并发,它只声明这次调用会读/写/搜哪些路径。ToolAccesses 是一组构造器(loop/tool-access.ts:22):readFile / writeFile / readWriteFile / searchTree / readTree …… 还有两个极端:none()(不碰文件资源)和 all()(全局互斥)。

关键在 conflict:两个访问是否冲突,由"是否有写 + 路径是否重叠"决定。

// tool-access.ts:74-96 —— 真实源码(节选)
function resourceAccessesConflict(left, right): boolean {
if (left.kind === 'all' || right.kind === 'all') return true; // all 与一切冲突
if (!fileOperationsConflict(left.operation, right.operation)) return false; // 两个只读 → 不冲突
return fileAccessesOverlap(left, right); // 都写/一读一写 → 看路径是否重叠
}

于是:两个 Read 同一文件不冲突(可并发);Read 与 Edit 同一文件冲突(串行);两把 Edit 打不同文件不冲突select_toolsAgentSwarm 不声明 accesses,默认降级成 all(),和同批一切串行——这是刻意的(select-tools.ts:11-17 注释:两个 select 并发结算会重复注入同一份 schema)。

4.2 输入 schema:zod → JSON Schema 的两处坑

toInputJsonSchema(support/input-schema.ts:26)把 zod schema 转成模型看的 draft-07 JSON Schema。它专门处理两个坑:

  1. io: 'input' 视图。 zod v4 默认出输出视图,会把带 .default() 的字段标成 required——自相矛盾(既有默认值又必填),还让运行时 AJV 拒掉合法调用。强制 input 视图,defaulted 字段保持可选。
  2. 补回 additionalProperties: false input 视图会丢掉这个闭合约束,于是 closeObjectNodes 递归给每个 object 节点补回(input-schema.ts:49)。因为运行时没有 zod 解析步,只有 AJV 校验;不闭合的话,一个拼错的参数会静默通过、被无声忽略。

真正的运行时参数校验在 args-validator.ts:按 $schema 或关键字选对应 draft 的 AJV 实例(ajvFor,args-validator.ts:30——draft-07 / 2019 / 2020 三个实例分开,因为 items 在不同 draft 语义不同),编译一次、每次调用 validateToolArgs,并把 AJV 报错翻成人话(must have required property 'x')。

4.3 路径访问策略:目录内可相对,目录外须绝对

所有文件工具(Read/Write/Edit/Grep/Glob/ReadMedia)在 resolveExecution 里第一步都调 resolvePathAccessPath(policies/path-access.ts:245)。这道策略做三件事,resolvePathAccess(path-access.ts:201)是核心:

用户传入 path

├─ 展开 ~ / 规范化 (win32 盘符等)
├─ 词法 canonical(解析 .. 和 . ,不跟随 symlink) ← canonicalizePath, path-access.ts:103
├─ 是否命中敏感文件? .env / id_rsa / credentials … ← isSensitiveFile, sensitive.ts:48
│ └─ 命中 → 抛 PATH_SENSITIVE(挡密钥外泄)
└─ 在工作区内吗? ← isWithinWorkspace, path-access.ts:154
├─ 在 → 放行(相对路径也行)
└─ 不在 → 看 guardMode:
'absolute-outside-allowed':原始路径是绝对的才放行,相对的报错

一句话规则:工作区内的路径可以相对写,工作区外的必须写绝对路径。 相对路径逃逸工作区(如 ../../etc)被拒,因为它多半是模型"以为自己在别处"的误判;而显式绝对路径是模型明确要访问外部,放行。敏感文件(sensitive.ts:12 的一小撮:.env、SSH 私钥、credentials)则无论如何都挡,还专门处理改名规避(id_rsa.key.env.bak)和白名单(.env.example)。

注意注释诚实标明:这套是纯词法的,不跟随 symlink——一个指向工作区外的软链能绕过,因为 kaos 层还没有 realpath 支持(path-access.ts:262-265)。

4.4 审批规则:literal 与 glob 两种口径

rule-match.ts 给每支工具生成/匹配审批规则。literalRulePattern(toolName, subject) 生成形如 Edit(/abs/path) 的规则串,并转义 glob 元字符(rule-match.ts:9);匹配时路径类工具用 matchesPathRuleSubject(rule-match.ts:21,按路径 glob 语义,支持 ! 取反),命令/查询类用 matchesGlobRuleSubject。工具只负责给出规则和匹配函数,判不判由权限层(02 章)。

4.5 结果构建、渲染、注册表

  • ToolResultBuilder(support/result-builder.ts):统一的输出封装。默认 5 万字符上限、单行 2000 字符截断,越界写 [...truncated]。Bash/Grep/Web 都用它,保证"塞给模型的结果"不会撑爆上下文。
  • display:每个 ToolExecution 带一个 display 对象(kind: 'file_io' | 'command' | 'agent_call' | 'todo_list' | ...),schema 来自 @moonshot-ai/protocol(display/schemas.ts)。这是给 TUI/Web 前端渲染"正在做什么"的结构化描述,和给模型的 output 分开。
  • store(tools/store.ts):一个类型安全的 KV,给需要跨调用记状态的工具用。TodoListTooldeclare module 往里挂 todo: TodoItem[](todo-list.ts:38),写走 tools.update_store 所以能在 wire replay 里重现。
  • 注册表 ToolManager(agent/tool/index.ts):持有 builtinTools / userTools / mcpTools 三张表。initializeBuiltinTools(agent/tool/index.ts:683)是总装配线——按能力/开关条件 new 出每支工具:
// agent/tool/index.ts:707-769 —— 真实源码(高度节选)
new b.ReadTool(kaos, workspace),
new b.EditTool(kaos, workspace),
new b.BashTool(kaos, cwd, background, { /* ... */ }),
goalToolsEnabled && new b.CreateGoalTool(this.agent), // 按开关挂
this.agent.rpc?.requestQuestion && new b.AskUserQuestionTool(this.agent), // 按能力挂
this.agent.cron && new b.CronCreateTool(this.agent.cron),
toolServices?.webSearcher && new b.WebSearchTool(toolServices.webSearcher), // 无 provider 就不挂

哪些工具存在,取决于 agent 的能力、profile 开关、有没有注入 provider——工具集是被"算"出来的,不是写死的。


5. 逐类工具巡览

五件套是模子,下面按类别过一遍具体的手脚。每类只点最能体现工程量的地方。

5.1 file —— 五支文件工具

工具干什么最值得看的点源码
Read按行读文本行号视图、1000 行 / 100KB 双上限、tail 负偏移、二进制/图像识别后转介 ReadMediaFilefile/read.ts
Write整文件写与 Edit 共用 line-endings 视图file/write.ts
Edit精确串替换见第 3 节file/edit.ts
Grepripgrep 内容搜索三种输出模式、分页、敏感文件二次过滤file/grep.ts
Globripgrep 文件名匹配按修改时间倒序、cwd 钉在搜索根解决相对 globfile/glob.ts

Read 的巧思: 结果分两路——文件内容进 output,而"读了 N 行 / 共 M 行 / 已截断"这类状态行note 侧信道(model-only,不进 UI),包在 <system> 里(read.ts:529-536)。这样状态元数据不会污染"模型看到的文件内容"本身。上限是常量:MAX_LINES=1000 / MAX_BYTES=100KB / MAX_LINE_LENGTH=2000(read.ts:16-18)。

Grep / Glob 共享 ripgrep 地基: 两支都 shell 出 rg,共用二进制定位(rg-locator)、子进程管道(run-rg)、.gitignore 处理。安全上有一处对称设计——敏感 glob 排除追加在用户 glob 之后,这样用户写个宽泛的 **/.env 也无法覆盖掉排除(grep.ts:490-495glob.ts:322-327)。Glob 还有一处关键:把 rg 的 cwd 钉到搜索根、用 . 当搜索路径(glob.ts:202),否则 src/**/*.ts 这种带 / 的 pattern 会被拿去匹配绝对路径而永远匹配不上。

ReadMediaFile: 读图/视频成多模态内容,门控模型的 image_in / video_in 能力。图像支持 region(按原图像素抠矩形)和 full_resolution(跳过默认降采样),压缩上限由 owner 级的 ImageLimits(support/image-limits.ts:27,env > 配置 > 内置默认三级优先)解析。压缩绝不静默:note 里明说是原样发、降采样、还是裁切。

5.2 shell —— BashTool

一支工具,却是最重的一支。核心难点是进程生命周期:前台跑、超时自动转后台、手动 ctrl+b 分离、SIGTERM→宽限→SIGKILL 两段杀。执行不走 node:child_process,全经 kaos,并交给 BackgroundManager 托管(bash.ts:359)。几处硬化:

  • 立即关 stdin(closeProcessStdin,bash.ts:588):交互命令(catreadpython -c 'input()')收到 EOF 而不是挂死。
  • 非交互环境变量:注入 NO_COLOR=1 / TERM=dumb / GIT_TERMINAL_PROMPT=0(bash.ts:281-289),让 git 不弹分页器、输出不带颜色码。
  • 超时分层:前台默认 60s / 上限 5min,后台默认 10min / 上限 24h,schema 用 superRefine 按前后台选对上限校验(bash.ts:82-93)。前台命中超时默认转后台而非杀死(可配),这套语义还会反向改写工具描述(withoutAutoBackgroundOnTimeout 等一串函数),让模型看到的说明书和实际行为一致。

5.3 web —— 接口在核心,实现由 host 注入

FetchURLTool(web/fetch-url.ts:67)和 WebSearchTool(web/web-search.ts:43)在 agent-core 里只定义接口——UrlFetcher(fetch-url.ts:38)、WebSearchProvider(web-search.ts:29)。没注入 provider,工具就不注册、不暴露给模型(见 5.6 装配线的 toolServices?.webSearcher && ...)。

真实实现放在 tools/providers/:MoonshotWebSearchProvider(providers/moonshot-web-search.ts:39)打 Moonshot 搜索 API,用 bearer token(带 apiKey 兜底),把 search_results 映射成统一的 WebSearchResult;还有 moonshot-fetch-url.ts 和给本地/自托管用的 local-fetch-url.ts这是依赖倒置:核心声明契约,宿主填实现,同一支工具能接不同后端。

两支工具都把"引用提醒"和数据放一起进 output(而非 message,因为 message 会在投给 provider 时被丢),提醒模型引用页面时写成 markdown 链接(web-search.ts:92fetch-url.ts:112)。

5.4 planning —— 进/出计划模式

EnterPlanModeTool(planning/enter-plan-mode.ts:22)/ ExitPlanModeTool。进计划模式无需审批,execute 里直接调 agent.planMode.enter(),返回一段"你现在处于计划模式,只能用只读工具调研、然后写计划文件"的工作流指令(enter-plan-mode.ts:56)。工具本体极薄——真正的状态在 agent.planMode,工具只是模型触发它的把手。

5.5 collaboration —— 派活给别的 agent / 问人 / 用技能

这一类是"让模型协调其它执行体"的工具。

工具干什么关键设计源码
Agent派一个子 agent 跑一个任务前台等结果/后台立返;resume 可续跑同一子 agent;子类型描述动态拼进 descriptioncollaboration/agent.ts:107
AgentSwarm一批子 agent 并行prompt_template + items 展开;去重相同 prompt;上限 128;resume_agent_ids 续跑collaboration/agent-swarm.ts:86
AskUserQuestion向用户提结构化选择题反向 RPC 到宿主 UI;问题/选项文本必须唯一(AJV 表达不了,execute 里再查一遍)collaboration/ask-user.ts:133
Skill模型主动调用一个已注册技能递归深度硬顶 MAX_SKILL_QUERY_DEPTH=3;把技能正文作为 user 消息注入collaboration/skill-tool.ts:77

Agent 工具的巧思: 输入 schema 用 z.preprocess 做兜底——不给 subagent_type 也不给 resume 时默认 'coder'(agent.ts:42-59)。子 agent 也经 BackgroundManager 托管,前台调用等 waitForForegroundRelease;结果里给足 resume_hint,并反复叮嘱"后台任务会自动通知、别 poll"(agent.ts:333)。

Skill 工具的防递归: 一个技能可能再调技能。MAX_SKILL_QUERY_DEPTH(skill-tool.ts:29)封死 Skill→Skill 无界递归,越界抛结构化 NestedSkillTooDeepError(而非软 tool-error),好让运行时区分"模型误派"和"安全网触发"(skill-tool.ts:104-113)。调用成功时,它把渲染好的技能正文用 agent.context.appendUserMessage 注入对话(skill-tool.ts:142),返回一句"技能已内联加载,按其指示做"。

5.6 goal / state / background / cron

  • goal:CreateGoal(goal/create-goal.ts:33)/ GetGoal / UpdateGoal / SetGoalBudget。把"目标"变成 agent 持有的结构化持久状态(agent.goal),而非从 slash 命令解析的文本。按 goalToolsEnabled 开关挂载。
  • state:TodoListTool(state/todo-list.ts:92)。一支工具兼读写:传 todos 是替换、传空数组是清空、不传是查询。状态进 ToolStore(见 4.5)。
  • background:TaskList / TaskOutput / TaskStop(tools/background/)。给 Bash/Agent 派出的后台任务做列/看/停。它们的存在与否决定了 Bash/Agent 能不能开后台(allowBackground)。
  • cron:CronCreate / CronList / CronDelete(tools/cron/)。定时触发,按 agent.cron 存在与否挂载;cron/scheduler.ts 是调度器,cron-expr.ts 解析表达式。

5.7 动态工具集:select_tools 与渐进式披露

MCP 工具可能几十上百个,全塞进顶层 tools[] 会撑爆上下文。Kimi Code 的解法是渐进式披露:MCP 工具的完整 schema 不进 tools[],模型先在 <tools_added> 公告里看到名字,需要时调 select_tools(names) 把它们的定义追加进对话。

// select-tools.ts:103-113 —— 真实源码(节选)
this.agent.context.appendMessage({
role: 'system', content: [], toolCalls: [],
tools, // messages[].tools 线协议:携带工具定义
origin: { kind: 'injection', variant: DYNAMIC_TOOL_SCHEMA_VARIANT },
});
manager.markDynamicToolsLoaded(toLoad); // 抢在 schema 消息落史前先记账

被选中的工具下一步就可调——循环每步重读可执行工具表(select-tools.ts:8)。混合输入按名逐个结算:命中的加载、已加载的报告、未知的单独报错,绝不"一个拼错全批重来"(select-tools.ts:79-90)。ToolManager 侧,loadableDynamicToolNames(agent/tool/index.ts:559)给出可选名单,getMcpToolSchema(agent/tool/index.ts:610)从活注册表(不从历史)读 schema。"已加载"账本以历史为唯一真相,能扛 resume、undo,并在压缩时清空——让模型重新按需选。


6. Skill 系统:把一份 SKILL.md 变成可注入能力

工具是编译进二进制的手脚;技能(skill)是用 Markdown 写的、可热加载的能力。5.5 的 Skill 工具只是模型触发技能的把手,技能本身怎么被发现、解析、渲染,在 packages/agent-core/src/skill/

6.1 一条技能的一生

磁盘上的 SKILL.md / *.md 文件

├─ ① 发现:扫描若干根目录 scanner.ts:discoverSkills (scanner.ts:132)
│ 项目/用户/内置/插件多来源,首名胜出去重,深度上限 8 防软链成环

├─ ② 解析:切 frontmatter + 正文 parser.ts:parseSkillText (parser.ts:107)
│ YAML frontmatter → SkillMetadata;正文 trim;顺带抽 mermaid/d2 图

├─ ③ 入册:按名归一化建索引 registry.ts:SessionSkillRegistry (registry.ts:26)

└─ ④ 渲染:调用时展开占位 + 包 XML registry.renderSkillPrompt (registry.ts:91)
→ 作为 user 消息注入对话,模型照做

6.2 发现:多来源、首名胜出、防成环

resolveSkillRoots(scanner.ts:61)按固定优先级收集根目录:项目品牌目录(.kimi-code/skills)、项目通用目录(.agents/skills)、用户目录、插件根、内置。discoverSkills(scanner.ts:132)递归走每个根,遇到含 SKILL.md 的目录就当一个技能包登记;readdir 结果排序后再处理,让跨目录同名冲突的"首名胜出"是确定性的而非依赖文件系统顺序(scanner.ts:154-156)。递归深度封在 MAX_SKILL_SCAN_DEPTH=8(scanner.ts:17),防目录软链成环转圈。

6.3 解析:frontmatter + 参数占位展开

parseFrontmatter(parser.ts:82)手写切 --- 围栏,YAML 用 js-yaml。类型只认三种可内联的(prompt / inline,及 type 缺省)加 flow / reference,不认的抛 UnsupportedSkillTypeError 被跳过(types.ts:73scanner.ts:402)。

技能正文可带参数占位,expandSkillParameters(parser.ts:176)负责展开:$NAME(具名)、$0/$1(位置)、$ARGUMENTS(整串)、${KIMI_SKILL_DIR}(技能目录,好让技能引用自带脚本);参数串按 shell 风格分词(引号成组)。若正文根本没占位但传了参数,就整串追加成一行 ARGUMENTS:(parser.ts:210-212)。所有插值都过 escapeXmlTags 防注入。

6.4 渲染 + 注入:包成 XML 块喂给模型

SessionSkillRegistry.renderSkillPrompt(registry.ts:91)展开占位、若来自插件再前置插件说明。注入时由 agent/skill/prompt.ts 包成 <kimi-skill-loaded name=... trigger=... source=... dir=... args=...>正文</kimi-skill-loaded> 块(prompt.ts:45),触发来源分 user-slash / model-tool / nested-skill。给模型的技能清单单独渲染(getModelSkillListing,registry.ts:133),只列可模型调用、非子技能的,并冠一句"忽略更早的清单,当前可用技能如下"来对抗上下文里的过期清单。

一句话直觉: 工具是"装好的手脚",技能是"随时能塞进脑子的一段专业操作手册"——发现即插即用,不用改一行代码、不用重编译。


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

按符号名 grep 比按行号更抗漂移。下表是读源码时该打开的关键文件与符号。

主题文件路径符号
工具接口(比 kosong Tool 多一个方法)packages/agent-core/src/loop/types.tsExecutableTool / RunnableToolExecution / ToolExecution
内置工具类型别名packages/agent-core/src/agent/tool/types.tsBuiltinTool
精确串替换范例packages/agent-core/src/tools/builtin/file/edit.tsEditTool / replaceOnceLiteral / EditInputSchema
CRLF/LF 模型视图packages/agent-core/src/tools/builtin/file/line-endings.tstoModelTextView / materializeModelText / detectLineEndingStyle
声明式资源访问与冲突packages/agent-core/src/loop/tool-access.tsToolAccesses / resourceAccessesConflict
zod→JSON Schema(input 视图)packages/agent-core/src/tools/support/input-schema.tstoInputJsonSchema / closeObjectNodes
运行时参数校验(AJV)packages/agent-core/src/tools/args-validator.tsajvFor / validateToolArgs
路径访问策略packages/agent-core/src/tools/policies/path-access.tsresolvePathAccess / isWithinWorkspace / canonicalizePath
敏感文件检测packages/agent-core/src/tools/policies/sensitive.tsisSensitiveFile
审批规则生成/匹配packages/agent-core/src/tools/support/rule-match.tsliteralRulePattern / matchesPathRuleSubject
结果封装/截断packages/agent-core/src/tools/support/result-builder.tsToolResultBuilder
按行读文本packages/agent-core/src/tools/builtin/file/read.tsReadTool / finishReadResult
ripgrep 内容搜索packages/agent-core/src/tools/builtin/file/grep.tsGrepTool / buildRgArgs
ripgrep 文件名匹配packages/agent-core/src/tools/builtin/file/glob.tsGlobTool / splitCompletePaths
图像限额解析packages/agent-core/src/tools/support/image-limits.tsImageLimits
shell 执行 + 进程托管packages/agent-core/src/tools/builtin/shell/bash.tsBashTool / closeProcessStdin
web 工具契约packages/agent-core/src/tools/builtin/web/fetch-url.ts · web-search.tsUrlFetcher / WebSearchProvider
web provider 实现packages/agent-core/src/tools/providers/moonshot-web-search.tsMoonshotWebSearchProvider
派子 agentpackages/agent-core/src/tools/builtin/collaboration/agent.tsAgentTool
子 agent 群packages/agent-core/src/tools/builtin/collaboration/agent-swarm.tsAgentSwarmTool / createAgentSwarmSpecs
结构化问人packages/agent-core/src/tools/builtin/collaboration/ask-user.tsAskUserQuestionTool / questionUniquenessError
调用技能(防递归)packages/agent-core/src/tools/builtin/collaboration/skill-tool.tsSkillTool / MAX_SKILL_QUERY_DEPTH
进计划模式packages/agent-core/src/tools/builtin/planning/enter-plan-mode.tsEnterPlanModeTool
目标状态packages/agent-core/src/tools/builtin/goal/create-goal.tsCreateGoalTool
TODO 状态packages/agent-core/src/tools/builtin/state/todo-list.tsTodoListTool
动态工具集(渐进披露)packages/agent-core/src/tools/builtin/select-tools.tsSelectToolsTool
工具装配线 + 注册表packages/agent-core/src/agent/tool/index.tsToolManager.initializeBuiltinTools / loadableDynamicToolNames / getMcpToolSchema
技能发现packages/agent-core/src/skill/scanner.tsdiscoverSkills / resolveSkillRoots
技能解析 + 占位展开packages/agent-core/src/skill/parser.tsparseSkillText / expandSkillParameters
技能注册表packages/agent-core/src/skill/registry.tsSessionSkillRegistry / renderSkillPrompt / getModelSkillListing
技能注入 promptpackages/agent-core/src/agent/skill/prompt.tsrenderSkillLoadedBlock

相关章节:工具怎么被调度见 01-loop.md;权限闸门怎么判见 02-agent.md;kaos/kosong 两层抹平见 04-providers.md