数据截至 (上游 commit 6692a1b5195b)
技能与工具:SKILL.md 渐进式披露 + MCP 集成
30 秒导读: 这一章讲 CowAgent 的「手」。模型只会说话,真正干活要靠工具(读文件、跑 bash、搜网页——原子能力),而技能是更高一层的「可插拔能力包」:用一份
SKILL.md写清「什么时候用、怎么做」,把多个工具编排成一套工作流。两者接入 prompt 的方式截然不同:工具整份 schema 注入、模型可直接调用;技能只把一行<description>放进 prompt,模型判断相关后再用read工具读全文——这就是「渐进式披露」。本章还讲工具体系怎么被ToolManager单例管理、MCP 外部工具怎么热插拔进来、工具太多时怎么按需向量检索只注入最相关的那几个。
1. 这是什么(零基础也能懂)
先把两个最容易混的词分清楚。它们不是一回事,接入 agent 的方式也完全不同。
| 概念 | 是什么 | 粒度 | 模型怎么用 |
|---|---|---|---|
| 工具 (tool) | 一个原子能力:读文件 / 跑 shell / 搜网页 / 发消息 | 单个函数 | prompt 里给完整 schema,模型直接发 tool call 调用 |
| 技能 (skill) | 一份 SKILL.md 描述的高层工作流,内部编排多个工具 | 一整套流程 | prompt 里只给一行描述+路径;相关时先 read 全文再照做 |
一句话类比: 工具像厨房里的单件电器(烤箱、搅拌机),插上就能用;技能像一张菜谱卡——它本身不会做菜,而是告诉你「先用搅拌机、再进烤箱 180 度 20 分钟」。菜谱不能"执行",只能"读了照做"。
为什么要这么分? 因为工具数量有限、都得放进 prompt;而技能可以有几十上百个,全文塞进 prompt 会撑爆上下文。于是 CowAgent 让技能先只露一行简介,模型觉得用得上了才去读细节——省 token,又能挂载海量能力。
用起来什么样(技能视角): 系统提示里模型会看到这样一段(由 agent/prompt/builder.py 拼装):
<available_skills>
<skill>
<name>knowledge-wiki</name>
<description>Manage the personal knowledge wiki. Use when the user shares
articles, documents, or asks to organize knowledge...</description>
<location>/path/to/skills/knowledge-wiki/SKILL.md</location>
</skill>
</available_skills>
模型读到用户说「帮我把这篇文章整理进知识库」,匹配上 knowledge-wiki 的描述,于是它调用 read 工具打开那个 <location>,拿到完整 SKILL.md,再按里面的步骤(用 read/write/bash 等真实工具)去落地。
本节到此为止不碰底层。记住这张心智图就行:工具=能直接调的手;技能=读了才照做的说明书。
2. 顶层全景(它大概怎么转)
CowAgent 的「能力供给」有两条独立管线,最后在一次 LLM 调用里汇合。
┌──────────────── 技能管线(说明书)────────────────┐
skills/ (内置) ─┐ │ SkillManager │
workspace/skills ├──▶│ ①扫两处目录 → ②合并启用状态 → ③过滤(平台/依赖) │
(自定义) ─┘ │ → 只把 <description>+<location> 拼进 prompt │──┐
└───────────────────────────────────────────────────┘ │
▼
┌──────────────── 工具管线(真正的手)────────────┐ 系统
agent/tools/* ─┐ │ ToolManager (进程级单例) │ prompt
(内置工具) ├──▶│ ①load_tools 扫类 → ②_load_mcp_tools 后台起 MCP │──▶ +
~/cow/mcp.json ─┘ │ → tool_classes{} + _mcp_tool_instances{} │ tools[]
(MCP 外部工具) └───────────────────────────────────────────────────┘ │
▼
每一轮:_select_tools_for_injection()
(工具太多时按语义只注入 top_k 个 MCP)
怎么读这张图:左边是两类"能力来源",各自被一个 Manager 收编;技能只贡献文字描述、工具贡献可调 schema;两者在每一轮推理前拼进发给 LLM 的请求。
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
SkillManager | 加载/合并/过滤技能,产出技能 prompt | agent/skills/manager.py:17 |
SkillLoader | 从两处目录扫描 SKILL.md 与根级 .md | agent/skills/loader.py:13 |
format_skills_for_prompt | 把技能渲染成 <available_skills> XML | agent/skills/formatter.py:9 |
should_include_skill | 按平台/依赖/API key 判断技能可不可用 | agent/skills/config.py:69 |
ToolManager | 每工作区一例;加载内置工具类 + MCP 工具实例 | agent/tools/tool_manager.py:31 |
McpTool | 把一个 MCP 工具包成 BaseTool | agent/tools/mcp/mcp_tool.py:5 |
select_mcp_tools | 按语义相似度选出本轮要注入的 MCP 工具 | agent/tools/mcp/tool_retrieval.py:84 |
_select_tools_for_injection | 执行器里决定这一轮注入哪些工具 | agent/protocol/agent_stream.py:1068 |
技能描述具体怎么被塞进系统提示的哪个区块、每轮 prompt 如何从零重建,见 02-system-prompt.md;工具调用如何驱动多轮循环,见 01-agent-loop.md。本章只讲「手」和「能力包」本身。
3. 核心原理之一:技能的渐进式披露
3.1 它要解决的小问题
技能可能有几十个,每份 SKILL.md 动辄几百行。全塞进 prompt → token 爆炸、还稀释注意力。可完全不给 → 模型不知道有这能力。
CowAgent 的解法是「渐进式披露 (progressive disclosure)」:分两级曝光。第一级极便宜——只给每个技能一行简介和文件路径;第二级才昂贵——模型确认相关后,自己用 read 工具去读全文。
3.2 思路:技能不是工具,是「读了照做」的说明书
这是全章最关键的一点,builder.py 在系统提示里对模型明说了这条规矩(agent/prompt/builder.py:381):
重要: 技能不是工具,不能直接调用。使用技能的唯一方式是用
read读取 SKILL.md 文件,然后按文件内容操作。
也就是说,knowledge-wiki 不是一个可以 tool_call("knowledge-wiki") 的东西。它只是一段文字 + 一个路径。模型要用它,唯一动作是「read 那个路径」。
流程走一遍:
用户: "把这篇文章整理进知识库"
│
▼
① 模型扫 prompt 里 <available_skills> 各条 <description>
│ (便宜:每条就一句话)
▼
② 命中 knowledge-wiki 的描述
│
▼
③ 模型调用 read 工具, path = <location> ← 第二级披露在此发生
│
▼
④ 拿到完整 SKILL.md,按其中步骤用 read/write/bash 落地
3.3 原理演示(示意,非源码)
把「只给描述」这一层用几行代码演出来——重点看:进 prompt 的不是全文,是 description。
# 示意,非源码:技能只曝光"目录卡片",全文留在磁盘
def build_skills_prompt(skills):
lines = ["<available_skills>"]
for s in skills:
lines.append(" <skill>")
lines.append(f" <name>{s.name}</name>")
lines.append(f" <description>{s.description}</description>") # 就这一句
lines.append(f" <location>{s.file_path}</location>") # 全文在这个路径
lines.append(" </skill>")
lines.append("</available_skills>")
return "\n".join(lines)
# 重点看:s.content(几百行正文)从不进 prompt,靠 <location> 让模型按需 read
3.4 真实实现
渲染逻辑就在 format_skills_for_prompt,一眼可见它只取 name / description / location / base_dir,正文 skill.content 完全不写入(agent/skills/formatter.py:30-37):
for skill in visible_skills:
lines.append(" <skill>")
lines.append(f" <name>{_escape_xml(skill.name)}</name>")
lines.append(f" <description>{_escape_xml(skill.description)}</description>")
lines.append(f" <location>{_escape_xml(skill.file_path)}</location>")
lines.append(f" <base_dir>{_escape_xml(skill.base_dir)}</base_dir>")
lines.append(" </skill>")
SkillManager.build_skills_prompt 是入口:它先 filter_skills 拿到可用技能,渲染成上面这段,再把「已安装但依赖没配好」的技能也附一小段设置提示(agent/skills/manager.py:253)。
builder._build_skills_section 则在这段 XML 前 面加上那条铁律说明,并动态解析出 read 工具的真实名字填进指令里(工具重命名了指令也不会失效,agent/prompt/builder.py:340-352)。
3.5 关键细节:调用不存在的「技能名」会被反向引导
模型有时会误以为技能能直接调用,发出一个名为 knowledge-wiki 的 tool call。CowAgent 没有粗暴报错,而是把这当成一次教学机会:_build_tool_not_found_message 发现「这不是工具,但存在同名技能」,就当场把整份 SKILL.md 读出来塞回错误信息里,引导模型改用真实工具照做(agent/protocol/agent_stream.py:1828)。
skill_entry = skill_manager.get_skill(tool_name) # 用"找不到的工具名"去查技能
...
return (
f"Tool '{tool_name}' is not a built-in tool, but a matching skill "
f"'{skill.name}' is available. You should use existing tools (e.g. bash with curl) "
f"to accomplish this task following the skill instructions below:\n\n"
f"--- SKILL: {skill.name} (path: {skill_md_path}) ---\n"
f"{skill_content}\n" # 全文喂回去
f"--- END SKILL ---\n\n"
f"Available tools: {available_tools}"
)
妙在:一次「误调用」反而替模型完成了第二级披露——它想调的东西没有,但它需要的知识被直接送到手上。
4. 核心原理之二:技能怎么被加载、合并、过滤
模型看到的那份技能清单,背后是三步:加载 → 合并启用状态 → 过滤。
4.1 加载:两处目录 + 两种发现规则
SkillLoader.load_all_skills 扫两个来源,同名时自定义覆盖内置(agent/skills/loader.py:220-265):
| 来源 | 目录 | source 标记 | 优先级 |
|---|---|---|---|
| 内置 | 项目根 skills/ | builtin | 低 |
| 自定义 | workspace/skills/ | custom | 高(覆盖同名) |
发现规则有两种(agent/skills/loader.py:19 的 docstring 与 _load_skills_recursive):
- 根目录直接的
.md文件 → 每个算一个技能。 - 子目录里的
SKILL.md→ 这个子目录被当作一个自包含技能,一旦命中SKILL.md就停止再往下递归——防止技能合集内部的子技能(如style-collection/style-anjing)被当成独立顶层技能列出来(agent/skills/loader.py:76-85)。
每个文件由 _load_skill_from_file 解析:parse_frontmatter 抽出 YAML frontmatter 的 name/description,没有 description 的技能会被丢弃并记一条诊断(agent/skills/loader.py:160-162)。
4.2 合并:skills_config.json 记住用户开关
技能可以被用户在控制台里启用/停用。这份「开不开」的状态存在 workspace/skills/skills_config.json,由 _sync_skills_config 每次刷新时与磁盘扫描结果对账(agent/skills/manager.py:83):
磁盘扫到的技能 × 上次存的 skills_config.json
│ │
▼ ▼
┌───────────────────────────────────┐
│ 新技能 → 用 metadata.default_enabled 定初始开关 │
│ 老技能 → 保留用户之前存的 enabled │
│ 已删除的 → 从 config 里移除 │
│ 描述/来源 → 永远用最新扫描刷新 │
└───────────────────────────────────┘
关键取舍:新技能的初始开关由 SKILL.md frontmatter 的 default_enabled 决定(agent/skills/manager.py:100-103,类型见 agent/skills/types.py:32),而已存在技能则以用户意愿为准——升级带来的新技能不会悄悄默认开启,用户手动关掉的也不会被"复活"。
4.3 过滤:平台 / 依赖 / API key 决定"可不可用"
filter_skills 在渲染前再筛一遍,核心判据是 should_include_skill(agent/skills/config.py:69):
| 判据 | 规则 | 不满足时 |
|---|---|---|
metadata.os | 当前平台要在支持列表里 | 直接排除 |
metadata.always | 置 true 则无视其他要求 | 一律纳入 |
requires.bins | 所有二进制都要在 PATH | 排除 |
requires.anyBins | 至少一个二进制存在 | 排除 |
requires.env | 所有环境变量(API key)都要设置 | 排除 |
requires.anyEnv | 至少一个环境变量存在 | 排除 |
设计哲学很直白(见 docstring):要求满足就自动启用,不满足就不给;key 填错则照样启用、留给运行时报错让 LLM 处理。被"依赖没配好"卡住的技能不会消失,而是走 filter_unavailable_skills + get_missing_requirements(agent/skills/config.py:142),在 prompt 末尾以 <unavailable_skills> 形式附一句「缺什么、怎么配」,让模型能引导用户补齐(agent/skills/formatter.py:54)。
4.4 新技能即时生效
用 skill-creator 造完新技能后不用重启:执行器发现 bash 跑了 init_skill.py 就自动 refresh_skills(),重新走一遍加载→合并→过滤(agent/protocol/agent_stream.py:1757-1762)。
5. 核心原理之三:工具体系与 ToolManager 单例
5.1 一个工具长什么样
所有工具继承 BaseTool,契约极小:类属性 name / description / params(JSON Schema),加一个 execute(params) -> ToolResult(agent/tools/base_tool.py:63)。以 read 为例(agent/tools/read/read.py:80):
class Read(BaseTool):
name: str = "read"
description: str = "Read or inspect file contents. ..."
params: dict = {
"type": "object",
"properties": {"path": {...}, "offset": {...}, "limit": {...}},
"required": ["path"],
}
def execute(self, args): ... # 真正读文件
description + params 就是发给 LLM 的工具定义;execute 是模型发来 tool call 后真正跑的逻辑。工具是能直接调的——这正是它和技能的根本区别。
5.2 ToolManager:每工作区一例
ToolManager 用 __new__ + 按工作区根目录键控的实例表实现「同一工作区单例」(agent/tools/tool_manager.py:31-60)。为什么必须复用实例?因为 MCP 子进程很贵——起一个 npx/uvx 要几秒到几十秒,绝不能每个会话都重起一遍;同一工作区内的 per-session agent 共享同一批 MCP 工具实例。而它又刻意不是全进程单例:每个工作区有自己的 mcp.json,全局共享一例会让第一个启动的 Agent 替所有人决定有哪些 MCP 服务器、还把凭据带进别人的会话。
load_tools 分两步(agent/tools/tool_manager.py:121):
_load_tools_from_init从agent.tools.__all__反射出所有BaseTool子类,实例化一次拿到name,把类(不是实例)存进tool_classes{}。需要特殊初始化的(记忆工具、McpTool)在此跳过,后面动态注册。_load_mcp_tools触发 MCP 加载(下一节)。
取用时 create_tool(name) 先查内置 tool_classes,查不到再落到 _mcp_tool_instances(agent/tools/tool_manager.py:721-744)——内置与 MCP 工具对调用方是统一命名空间。
6. 核心原理之四:MCP 集成(可插拔的外部工具)
MCP(Model Context Protocol,一套让 agent 连外部工具服务器的标准协议)让 CowAgent 能挂载第三方能力:一个 MCP server 暴露若干工具,每个被包成一个 McpTool。
6.1 McpTool:把协议工具伪装成本地工具
McpTool 继承 BaseTool,__init__ 时把 MCP 返回的 schema(name/description/inputSchema)填进标准字段,execute 转发给 client.call_tool(agent/tools/mcp/mcp_tool.py:5-31)。对上层而言,一个 MCP 工具和一个内置工具长得一模一样。
6.2 后台异步启动,绝不阻塞首条消息
_load_mcp_tools 的关键设计:立刻返回,把起服务器丢进后台线程(agent/tools/tool_manager.py:344)。理由——内置工具不依赖 MCP,没必要让用户为几十秒的 npx 启动等待。
它还是幂等的:_mcp_lock 里检查 _mcp_loaded,第一个加载线程一派发就翻标志位,后续 per-session agent 初始化再调用直接 no-op,不会重复 fork 子进程(agent/tools/tool_manager.py:356-384)。
后台 worker _load_mcp_tools_async 逐个把 server 拉起来,故障隔离——一个坏 server 不影响其它,也绝不把异常抛出线程(agent/tools/tool_manager.py:472):
for cfg in servers:
client = McpClient(cfg)
if not client.initialize(): # 握手失败
status = "needs_auth" or "failed"; continue # 跳过,不拖累别人
for schema in client.list_tools():
_mcp_tool_instances[name] = McpTool(client, schema, server_name) # 就绪即发布
registry._clients[server_name] = client # 工具可见后才登记 client
status = "ready"
注意顺序:工具先进 _mcp_tool_instances,client 才登记进 registry,保证调用方永远看不到「半加载」的 server(agent/tools/tool_manager.py:516-522)。
6.3 热重载:改了 mcp.json 不用重启
refresh_mcp_if_changed 在每次 agent 创建时被调,快路径是单次 os.stat + sha256——~/cow/mcp.json 没变就立即返回,零成本(agent/tools/tool_manager.py:390)。变了才做 diff 式重载:
| 变化 | 动作 |
|---|---|
| 新增 server | 后台起它 |
| 删除 server | _teardown_mcp_server 关进程、摘掉它的工具 |
| 改了配置 | 先拆后起(重启) |
| 没动的 | 原样运行 |
它由 bridge/agent_bridge.py:909 在用户回复发出之后的后台线程里触发,任何成本都不加进用户延迟。
6.4 运行中并入新工具:sync_mcp_into_agent
一个 agent 创建时 MCP 可能还没加载完。sync_mcp_into_agent 在每轮 LLM 调用前做一次廉价字典对账(微秒级):把加载完成的新 MCP 工具加进这个 agent、把已下线的摘掉,内置工具不动(agent/tools/tool_manager.py:568,调用点 agent/protocol/agent_stream.py:1164-1168)。这让 agent 无需重启会话就能中途拿到新上线的 MCP 工具。
它同时兼容两种工具容器:AgentStream.tools 是 dict、默认 Agent.tools 是 list,两条分支分别处理。还有一道安全闸:自进化审查 agent(_evolution_restricted)一律不注入 MCP 工具,以免绕过它被刻意收窄的工具边界(见 06-self-evolution.md)。
7. 核心原理之五:按需工具检索(MCP 太多时只注入 top_k)
7.1 问题:MCP 工具会多到撑爆 schema
挂几个 MCP server,工具轻松上百。全部注入 → tool schema 巨大、烧 token、还干扰模型选择。CowAgent 的解法:语义检索,每轮只注入最相关的 top_k 个 MCP 工具;内置工具永远全量注入(技能和核心流程硬依赖它们) 。
默认关闭,超阈值才启用(config.py:301-303):
| 配置 | 默认 | 含义 |
|---|---|---|
mcp_tool_retrieval_enabled | False | 总开关 |
mcp_tool_retrieval_threshold | 20 | MCP 工具数超过才检索 |
mcp_tool_retrieval_top_k | 10 | 每轮最多注入几个 MCP 工具 |
7.2 三个纯函数 + 一个执行器编排
检索逻辑拆成 agent/tools/mcp/tool_retrieval.py 里的无状态纯函数,向量的预计算和缓存则留在 ToolManager(工具生命周期的主人):
build_retrieval_query:把最近 5 条消息的文本拼成检索 query(tool_retrieval.py:36)。为什么不用最初那句用户问题?因为多轮工具循环里需求会漂移,近窗口才抓得住当前意图,又不会被大段序列化的 tool payload 污染(只保留text块)。select_mcp_tools:算 query 与各工具向量的余弦相似度,取 top_k(tool_retrieval.py:84)。_rank_by_similarity:有 numpy 走向量化、没有则纯 Python 兜底(tool_retrieval.py:134)。
执行器 _select_tools_for_injection 负责编排(agent/protocol/agent_stream.py:1068):没开 / 没超阈值 / 拿不到 embedding provider / 任何异常 → 一律全量注入,工具绝不被静默丢弃。
7.3 两个救命的不变量
这套检索最精妙的是两条「安全网」:
① 失败即降级为全注入。 select_mcp_tools 在没有 query 向量、索引为空、维度不匹配(说明索引是用另一个 embedding 模型建的)或任何异常时返回 None,调用方据此回退全量注入(tool_retrieval.py:107-131)。检索永远不会因为出错而让 agent 少一个工具。
② 只增不减(union-accumulate)。 每轮选出的集合会和「本 run 之前已选的」求并集(_retrieved_mcp_names),存回执行器(agent/protocol/agent_stream.py:1112-1123)。为什么?因为一个已经在消息历史里产生过 tool_use 的工具,若中途从 schema 里消失,Claude/MiniMax 会直接报「消息格式错误」。只增不减保证任何已用过的工具不会半途蒸发。
# 示意,非源码:union-accumulate 的直觉
selected_this_turn = top_k_by_similarity(query, tool_vectors) # 本轮排名
injected = already_selected_in_this_run | selected_this_turn # 并集:只增不减
# 这样上一轮调过的工具即使这轮相似度掉出 top_k,仍留在 schema 里
7.4 向量从哪来
ToolManager.get_mcp_tool_vectors 懒加载地为尚未缓存的 MCP 工具算 embedding(MCP 异步上线、工具会陆续出现),没有 embedding provider 或出错就返回空 dict、让调用方回退全注入,从不抛异常(agent/tools/tool_manager.py:649)。query 侧则用 embed_query(agent/tools/tool_manager.py:663)。用于嵌入的文本就是 "{name}: {description}"(agent/tools/tool_manager.py:702-707)——所以一个 MCP 工具的 description 写得好不好,直接影响它能 不能被检中。
8. 巧妙之处(可借鉴的技术)
- 技能 ≠ 工具,用「读文件」当第二级披露。 不发明新调用机制,复用已有的
read工具就实现了按需加载海量能力包。载体是结构与 prompt 约定,不是代码开关。agent/skills/formatter.py:30、agent/prompt/builder.py:381。 - 误调用反哺教学。 模型把技能名当工具调,系统不报错,反而把 SKILL.md 全文喂回去——把一次错误变成一次成功的知识投递。
agent/protocol/agent_stream.py:1828。 - 单例 + 后台异步 + 幂等 三件套让昂贵的 MCP 子进程只起一次、不阻塞首条消息、多会话共享。
agent/tools/tool_manager.py:344-384。 - 工具就绪先于 client 登记,杜绝调用方看到半加载 server。
agent/tools/tool_manager.py:516-522。 - 热重载走「stat 快路径」,没改文件时几乎零成本,却支持改 mcp.json 即时生效。
agent/tools/tool_manager.py:390。 - 检索的两条不变量(失败降级全注入、只增不减)把「省 token」这个优化做成了永不损害正确性的优化。
agent/tools/mcp/tool_retrieval.py:84。
9. 边界与局限
- 技能靠模型自觉。 是否读、读哪个 SKILL.md 全凭模型判断
<description>的匹配度;描述写得含糊,技能就可能被漏用。系统层面只有那条「优先用技能」的指令在推动。 - 技能不能直接执行。 它没有独立的执行沙箱,一切落地都得借道现有工具(bash/read/write…);SKILL.md 里写的步骤模型也可能不完全照做。
- MCP 是最终一致,不是即时。 服务器后台加载,agent 靠每轮
sync_mcp_into_agent才逐渐拿到工具;首条消息时新 MCP 工具可能还没就绪。 - 检索质量取决于 embedding 与描述。 没有 embedding provider 就退化成全量注入;
"name: description"写得差会检不准。且它只裁剪 MCP 工具,内置工具恒定全量。 - 检索维度必须一致。 换了 embedding 模型而索引没重建,维度不匹配会让
select_mcp_tools直接返回None退回全注入(安全但失去省 token 效果)。
10. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 技能加载/合并/过滤/出 prompt | agent/skills/manager.py | SkillManager / refresh_skills / _sync_skills_config / filter_skills / build_skills_prompt |
| 两处目录扫描 + 发现规则 | agent/skills/loader.py | SkillLoader / load_all_skills / _load_skills_recursive |
| 只渲染 description+location | agent/skills/formatter.py | format_skills_for_prompt / format_unavailable_skills_for_prompt |
| frontmatter / metadata 解析 | agent/skills/frontmatter.py | parse_frontmatter / parse_metadata |
| 技能可用性(平台/依赖/env) | agent/skills/config.py | should_include_skill / get_missing_requirements |
| 技能类型(default_enabled 等) | agent/skills/types.py | SkillMetadata / Skill / SkillEntry |
| 技能 prompt 段 + 「技能非工具」铁律 | agent/prompt/builder.py | _build_skills_section |
| 误调用技能名 → 反哺 SKILL.md | agent/protocol/agent_stream.py | _build_tool_not_found_message |
| 工具基类契约 | agent/tools/base_tool.py | BaseTool / ToolResult / get_json_schema |
| 内置工具样例 | agent/tools/read/read.py | Read |
| 工具/MCP 单例管理器 | agent/tools/tool_manager.py | ToolManager / load_tools / _load_mcp_tools / _load_mcp_tools_async / refresh_mcp_if_changed / sync_mcp_into_agent / get_mcp_tool_vectors / embed_query |
| MCP 工具包装 | agent/tools/mcp/mcp_tool.py | McpTool |
| MCP 客户端(握手/列举/调用) | agent/tools/mcp/mcp_client.py | McpClient.initialize / list_tools / call_tool |
| 按需工具检索(纯函数) | agent/tools/mcp/tool_retrieval.py | build_retrieval_query / select_mcp_tools / _rank_by_similarity |
| 每轮工具注入决策 | agent/protocol/agent_stream.py | _select_tools_for_injection |
| MCP 热重载触发点 | bridge/agent_bridge.py | refresh_mcp_if_changed(后台调用) |