跳转到内容

工具系统:内置工具 + MCP 工具的统一调度

工具系统:内置工具 + MCP 工具的统一调度

学习目标

本章要解决的核心问题:AI Agent 如何在一个统一框架下调度内置工具和外部 MCP 工具,同时支持安全策略和参数预处理?

你将学到:

  • Continue 内置工具的分类和定义规范
  • MCP 工具如何桥接到统一的工具调用管线
  • 工具调用的审批策略(evaluateToolCallPolicy)
  • 参数预处理机制(preprocessArgs)

前置知识

本章涉及工具调用的通用协议,建议先阅读:

下文假设你已理解 Function Calling / Tool Use 的 JSON Schema 定义和 LLM 工具调用流程,直接聚焦 Continue 的具体实现。


项目实践

内置工具清单

Continue 定义了约 20 个内置工具,分布在 core/tools/definitions/core/tools/implementations/ 两个目录中:

工具用途实现文件
read_file读取文件内容readFile.ts
read_file_range读取文件的指定行范围readFileRange.ts
edit_existing_file修改已有文件editExistingFile.ts
create_new_file创建新文件createNewFile.ts
run_terminal_command执行终端命令runTerminalCommand.ts
grep_search全文内容搜索grepSearch.ts
file_glob_search文件名模式匹配globSearch.ts
ls列出目录内容ls.ts
view_diff查看 Git 变更viewDiff.ts
view_repo_map查看项目结构图viewRepoMap.ts
view_subdirectory查看子目录结构viewSubdirectory.ts
search_web网络搜索searchWeb.ts
fetch_url_content抓取网页内容fetchUrlContent.ts
codebase代码库语义检索codebaseTool.ts
create_rule_block创建规则配置块createRuleBlock.ts
request_rule请求规则执行requestRule.ts
read_skill读取技能文件readSkill.ts

工具定义规范

每个工具由两部分组成:

  1. 定义definitions/):JSON Schema 描述工具的参数和返回值
  2. 实现implementations/):实际的执行函数
// 工具定义示例(伪代码)
const readFileTool: Tool = {
function: {
name: "read_file",
description: "Read the contents of a file",
parameters: {
type: "object",
properties: {
filepath: { type: "string", description: "The file path to read" },
},
required: ["filepath"],
},
},
// ... 元数据
};

统一调用管线

callTool.ts 是工具调用的中枢,支持三种来源:

LLM 工具调用请求
callTool(tool, toolCall, extras)
├─ 内置工具 → 调用 implementations/ 中的实现
├─ MCP 工具 → 解析 mcp:// URI → MCPManager.getConnection → client.callTool
└─ HTTP 工具 → POST 到外部服务
返回 ContextItem[] 结果

关键代码路径:

async function callTool(tool: Tool, toolCall: ToolCall, extras: ToolExtras) {
// 1. 解析和验证参数
const args = safeParseToolCallArgs(toolCall, tool);
// 2. 参数预处理(可选)
if (tool.preprocessArgs) {
args = await tool.preprocessArgs(args, { ide: extras.ide });
}
// 3. 根据工具类型执行
if (isMcpTool(tool)) {
return await callMcpTool(tool, args, extras);
} else if (isHttpTool(tool)) {
return await callHttpTool(tool, args, extras);
} else {
return await callBuiltInTool(tool, args, extras);
}
}

工具审批策略

Continue 支持在工具调用前进行策略评估:

// tools/evaluatePolicy 消息处理器
const evaluatedPolicy = tool.evaluateToolCallPolicy(
basePolicy, // 基础策略(如 always-ask)
parsedArgs, // 解析后的参数
processedArgs, // 预处理后的参数
);

审批策略决定:

  • 工具调用前是否需要用户确认
  • 哪些参数可以自动填充
  • 是否有权限限制(如禁止执行特定命令)

客户端工具 vs 服务端工具

CLIENT_TOOLS_IMPLS 标记了由客户端(IDE 扩展)而非服务端实现的工具:

const CLIENT_TOOLS_IMPLS = [
BuiltInToolNames.EditExistingFile,
BuiltInToolNames.SingleFindAndReplace,
BuiltInToolNames.MultiEdit,
];

这些工具的完整实现不在 core/ 中,而是在各个 IDE 扩展(VS Code、JetBrains)中,因为需要直接操作 IDE 的文件系统。


问题与规避

陷阱 1:工具参数解析失败

现象:LLM 返回的 tool call 参数格式不正确(如 JSON 字符串未解析),导致执行失败。

规避safeParseToolCallArgs 先尝试标准 JSON 解析,失败后回退到更宽松的解析策略(如将字符串整体作为参数)。coerceArgsToSchema 进一步将参数适配到工具定义所需的 JSON Schema。

陷阱 2:终端命令安全风险

现象:Agent 可能执行危险的终端命令(如 rm -rf)。

规避@continuedev/terminal-security 包提供终端命令的安全评估。runTerminalCommand 的实现会检查命令安全性,并根据 evaluateToolCallPolicy 的结果决定是否询问用户。

陷阱 3:工具调用超时

现象:某些工具(如大型文件的读取、慢速的网络请求)可能长时间不返回。

规避:工具调用使用 AbortController 管理。LLM 的 llm/streamChat 消息有对应的 abort 消息可中止整个管线,包括进行中的工具调用。


设计取舍

为什么用定义/实现分离

Continue 将工具的定义(JSON Schema)和实现(执行函数)分开存放。这带来几个好处:

  • 可组合:同一个定义可以有不同的实现(如本地 vs 远程)
  • 可序列化:定义是纯 JSON Schema,可以安全地发送给 LLM
  • 可测试:定义和实现可以独立测试

替代方案:将定义和实现在同一个文件中。Continue 选择分离是为了支持 MCP 工具的动态加载——MCP 工具只有定义(通过 MCP 协议获取),实现在远程服务器。

统一调用 vs 分派

Continue 的 callTool 在一个函数内分派三种来源(内置、MCP、HTTP),而非用独立的路由器。这是因为:

  • 工具来源的判断逻辑简单(URI 协议或名称检查)
  • 统一入口便于添加横切逻辑(日志、策略评估、错误处理)
  • 调用者只需知道一个入口点

参考来源