工具系统:内置工具 + 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 |
工具定义规范
每个工具由两部分组成:
- 定义(
definitions/):JSON Schema 描述工具的参数和返回值 - 实现(
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 协议或名称检查)
- 统一入口便于添加横切逻辑(日志、策略评估、错误处理)
- 调用者只需知道一个入口点