- 从 MCP 到 Tool Use
MCP (Model Context Protocol) 是一种用于标准化与各种模型交互的协议。在本项目中,MCP工具定义通过McpClient类统一管理,为不同LLM提供商提供一致的工具调用接口。
MCP 工具的基本结构定义如下(官方额外还有一些注释字段,目前没用到):
{
name: string; // Unique identifier for the tool
description?: string; // Human-readable description
inputSchema: { // JSON Schema for the tool's parameters
type: "object",
properties: { ... } // Tool-specific parameters
}
}通过mcpClient.ts的callTool方法,可以实现跨提供商的工具调用:
async callTool(toolName: string, args: Record<string, unknown>): Promise<ToolCallResult>工具调用结果遵循统一格式:
interface ToolCallResult {
isError?: boolean;
content: Array<{
type: string;
text: string;
}>;
}当需要将MCP工具映射到不同提供商时,会通过以下流程:
- 使用
presenter.mcpPresenter.mcpToolsToOpenAITools、presenter.mcpPresenter.mcpToolsToAnthropicTools或presenter.mcpPresenter.mcpToolsToGeminiTools等方法进行转换 - 这些方法会将MCP工具的
inputSchema转换为各提供商期望的参数格式 - 确保工具名称和描述在转换过程中保持一致
Anthropic的Tool API是通过AnthropicProvider类实现的,支持Claude 3系列中具备tool use能力的模型。
Anthropic要求工具定义通过tools参数传递,格式遵循以下结构:
{
tools: [
{
name: string;
description: string;
input_schema: object; // JSON Schema格式
}
]
}Anthropic对消息格式有特殊要求,特别是工具调用相关的消息结构:
- 系统消息(system):独立于对话消息,通过
system参数传递 - 用户消息(user):包含
content数组,可以包含文本和图像 - 助手消息(assistant):可以包含工具调用,使用
tool_use类型的内容块 - 工具响应:作为用户消息的一部分,使用
tool_result类型的内容块
formatMessages方法负责将标准聊天消息转换为Anthropic格式:
private formatMessages(messages: ChatMessage[]): {
system?: string;
messages: Anthropic.MessageParam[];
}Claude API返回的工具调用事件包括:
content_block_start(类型为tool_use):工具调用开始content_block_delta(带有input_json_delta):工具参数流式更新content_block_stop:工具调用结束message_delta(带有stop_reason: 'tool_use'):因工具调用而停止生成
这些事件被转换为标准化的LLMCoreStreamEvent事件:
{
type: 'tool_call_start' | 'tool_call_chunk' | 'tool_call_end';
tool_call_id?: string;
tool_call_name?: string;
tool_call_arguments_chunk?: string;
tool_call_arguments_complete?: string;
}首先,定义一个getTime工具:
{
"name": "getTime",
"description": "获取特定时间偏移量的时间戳(毫秒)。可用于获取过去或未来的时间。正数表示未来时间,负数表示过去时间。例如,要获取昨天的时间戳,使用-86400000作为偏移量(一天的毫秒数)。",
"input_schema": {
"type": "object",
"properties": {
"offset_ms": {
"type": "number",
"description": "相对于当前时间的毫秒数偏移量。负值表示过去时间,正值表示未来时间。"
}
},
"required": ["offset_ms"]
}
}{
"role": "user",
"content": [
{
"type": "text",
"text": "请告诉我昨天的日期是什么时候?"
}
]
}{
"role": "assistant",
"content": [
{
"type": "text",
"text": "为了告诉您昨天的日期,我需要获取昨天的时间戳。"
},
{
"type": "tool_use",
"id": "toolu_01ABCDEFGHIJKLMNOPQRST",
"name": "getTime",
"input": {"offset_ms": -86400000}
}
]
}{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABCDEFGHIJKLMNOPQRST",
"result": "1684713600000"
}
]
}{
"role": "assistant",
"content": [
{
"type": "text",
"text": "根据获取的时间戳1684713600000,昨天的日期是2023年5月22日。这个时间戳表示从1970年1月1日至昨天的毫秒数。"
}
]
}Gemini通过GeminiProvider类实现工具调用功能,支持Gemini Pro及更新版本的模型。
Gemini要求工具定义传递为以下格式:
{
tools: [
{
functionDeclarations: [
{
name: string,
description: string,
parameters: object // OpenAPI格式的JSON Schema
}
]
}
]
}Gemini的消息结构相对简单,但有一些特殊处理:
- 系统指令(systemInstruction):作为独立参数传递
- 内容数组(contents):包含用户和模型消息
- 工具调用:通过
functionCall对象表示 - 工具响应:通过
functionResponse对象表示
Gemini的流式响应需要处理以下特殊情况:
functionCall对象表示工具调用开始- 函数参数通过
functionCall.args对象传递 functionCallResult事件表示工具响应
这些事件同样被转换为标准的LLMCoreStreamEvent格式,方便统一处理。
{
"tools": [
{
"functionDeclarations": [
{
"name": "getTime",
"description": "获取特定时间偏移量的时间戳(毫秒)。",
"parameters": {
"type": "object",
"properties": {
"offset_ms": {
"type": "number",
"description": "相对于当前时间的毫秒偏移量,负数表示过去,正数表示未来。"
}
},
"required": ["offset_ms"]
}
}
]
}
]
}{
"role": "user",
"parts": [
{
"text": "请告诉我昨天的日期是什么时候?"
}
]
}{
"role": "model",
"parts": [
{
"functionCall": {
"name": "getTime",
"args": {
"offset_ms": -86400000
}
}
}
]
}{
"role": "user",
"parts": [
{
"functionResponse": {
"name": "getTime",
"response": 1684713600000
}
}
]
}{
"role": "model",
"parts": [
{
"text": "根据获取的时间戳1684713600000,昨天的日期是2023年5月22日。"
}
]
}OpenAI的工具调用实现在OpenAICompatibleProvider类中,支持GPT-3.5-Turbo和GPT-4系列模型。
OpenAI的函数调用格式最为广泛使用:
{
tools: [
{
type: "function",
function: {
name: string,
description: string,
parameters: object // JSON Schema格式
}
}
]
}OpenAI的消息格式比较标准化:
- 消息数组(messages):包含role和content
- 工具调用:记录在assistant消息中的
tool_calls数组 - 工具响应:作为单独的
tool角色消息,包含tool_call_id引用
OpenAI的流式事件包括:
tool_calls数组表示工具调用- 流式API返回
delta.tool_calls表示工具调用的增量更新 - 流式工具参数通过
tool_calls[i].function.arguments传递
这些事件同样被标准化为通用的LLMCoreStreamEvent格式。
{
"tools": [
{
"type": "function",
"function": {
"name": "getTime",
"description": "获取特定时间偏移量的时间戳(毫秒)。",
"parameters": {
"type": "object",
"properties": {
"offset_ms": {
"type": "number",
"description": "相对于当前时间的毫秒偏移量,负数表示过去,正数表示未来。"
}
},
"required": ["offset_ms"]
}
}
}
]
}[
{
"role": "user",
"content": "请告诉我昨天的日期是什么时候?"
}
][
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "getTime",
"arguments": "{ \"offset_ms\": -86400000 }"
}
}
]
}
][
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "1684713600000"
}
][
{
"role": "assistant",
"content": "根据获取的时间戳1684713600000,昨天的日期是2023年5月22日。"
}
]对于不支持原生工具调用的模型,项目实现了基于提示词工程的替代方案:
在OpenAICompatibleProvider中的prepareFunctionCallPrompt方法实现了这一功能:
private prepareFunctionCallPrompt(
messages: ChatCompletionMessageParam[],
mcpTools: MCPToolDefinition[]
): ChatCompletionMessageParam[]该方法将工具定义作为指令添加到系统消息中,包括:
- 工具调用格式说明(通常使用XML风格标签如
<function_call>) - 工具定义的JSON Schema
- 使用示例和格式要求
从流式文本中解析函数调用通过正则表达式和状态机实现:
protected parseFunctionCalls(
response: string,
fallbackIdPrefix: string = 'tool-call'
): Array<{ id: string; type: string; function: { name: string; arguments: string } }>解析过程处理以下挑战:
- 检测函数调用的开始和结束标记
- 处理嵌套的JSON结构
- 处理不完整或格式错误的函数调用
- 为函数调用分配唯一ID
流式解析通过状态机(TagState)跟踪标签状态:
type TagState = 'none' | 'start' | 'inside' | 'end'这使得即使在复杂的流式生成中,也能准确识别和提取函数调用信息。
- 添加函数描述到系统提示中:
你是一个有用的AI助手。当需要时,你可以使用以下工具帮助回答问题:
function getTime(offset_ms: number): number
描述: 获取当前时间偏移后的毫秒数时间戳
参数:
- offset_ms: 时间偏移量(毫秒)
使用工具时,请使用以下格式:
<function_call>
{
"name": "getTime",
"arguments": {
"offset_ms": -86400000
}
}
</function_call>
- 模型生成带有函数调用标记的回复:
我需要获取昨天的日期。我将调用getTime函数获取昨天的时间戳。
<function_call>
{
"name": "getTime",
"arguments": {
"offset_ms": -86400000
}
}
</function_call>
- 通过正则表达式解析函数调用:
// 使用状态机和正则匹配提取<function_call>标签内容
const functionCallMatch = response.match(/<function_call>([\s\S]*?)<\/function_call>/);
if (functionCallMatch) {
try {
const parsedCall = JSON.parse(functionCallMatch[1]);
// 调用函数并获取结果
} catch (error) {
// 处理解析错误
}
}- 将函数结果添加到上下文中:
函数结果: 1684713600000
根据获取的时间戳,昨天是5月22日。
这种方法通过精心设计的提示词和文本解析技术,使不支持原生工具调用的模型也能模拟工具调用功能。