From 1dd24ca84d4c549b65b48f1925f22a960d287178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=B5=B7?= <7836246@qq.com> Date: Thu, 19 Mar 2026 09:23:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E7=B1=BB=E5=88=AB=20few-shot?= =?UTF-8?q?=20=E7=A4=BA=E8=8C=83=EF=BC=8C=E6=8F=90=E5=8D=87=20MCP/Skills/P?= =?UTF-8?q?lugins=20=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=8E=87=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:模型只模仿 few-shot 中见过的工具(Read/Bash), 导致 MCP 工具、Skills、Plugins 等第三方工具从不被调用。 修复: - 按命名空间/来源自动分组第三方工具(MCP 双下划线、 驼峰前缀、蛇形前缀等规则) - 每个命名空间选一个代表性工具加入 few-shot 示范 - 核心工具(Read/Bash) + 最多 4 个第三方工具代表 - 在单个 assistant 回复中展示多工具调用格式 - 第三方工具的示例参数从 schema 中自动提取 示例:如果用户有 mcp__context7、SuperPowers、claude_mem 等不同来源的工具,few-shot 会各选一个代表进行示范, 让模型知道所有类别的工具都可以调用。 --- src/converter.ts | 127 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 18 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index d605fd1..5ff64c8 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -248,21 +248,113 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise CORE_TOOL_PATTERNS.some(p => p.test(name)); + + // 分类:核心编程工具 vs 第三方工具(MCP/Skills/Plugins) + const coreTools = tools.filter(t => isCoreToolName(t.name)); + const thirdPartyTools = tools.filter(t => !isCoreToolName(t.name)); + + // 为工具生成示例参数 + const makeExampleParams = (tool: AnthropicTool): Record => { + if (/^(Read|read_file|ReadFile)$/i.test(tool.name)) return { file_path: 'src/index.ts' }; + if (/^(Bash|execute_command|RunCommand|run_command)$/i.test(tool.name)) return { command: 'ls -la' }; + if (/^(Write|write_to_file|WriteFile|write_file)$/i.test(tool.name)) return { file_path: 'output.txt', content: '...' }; + if (/^(ListDir|list_dir|list_directory|ListDirectory|list_files)$/i.test(tool.name)) return { path: '.' }; + if (/^(Search|search_files|SearchFiles|grep_search|codebase_search)$/i.test(tool.name)) return { query: 'TODO' }; + if (/^(Edit|edit_file|EditFile|replace_in_file)$/i.test(tool.name)) return { file_path: 'src/main.ts', old_text: 'old', new_text: 'new' }; + // 第三方工具:从 schema 中提取前 2 个参数名 + if (tool.input_schema?.properties) { + return Object.fromEntries( + Object.entries(tool.input_schema.properties as Record) + .slice(0, 2) + .map(([k, v]) => [k, v.type === 'boolean' ? true : v.type === 'number' ? 1 : 'value']) + ); + } + return { input: 'value' }; + }; + + // 选取 few-shot 工具集:按工具来源/命名空间分组,每个组选一个代表 + // 确保 MCP 工具、Skills、Plugins 等不同类别各有代表 (#67) + const fewShotTools: AnthropicTool[] = []; + + // 1) 核心工具:优先 Read,其次 Bash const readTool = tools.find(t => /^(Read|read_file|ReadFile)$/i.test(t.name)); - const bashTool = tools.find(t => /^(Bash|execute_command|RunCommand)$/i.test(t.name)); - const fewShotTool = readTool || bashTool || tools[0]; - const fewShotParams = fewShotTool.name.match(/^(Read|read_file|ReadFile)$/i) - ? { file_path: 'src/index.ts' } - : fewShotTool.name.match(/^(Bash|execute_command|RunCommand)$/i) - ? { command: 'ls -la' } - : fewShotTool.input_schema?.properties - ? Object.fromEntries( - Object.entries(fewShotTool.input_schema.properties as Record) - .slice(0, 2) - .map(([k]) => [k, 'value']) - ) - : { input: 'value' }; + const bashTool = tools.find(t => /^(Bash|execute_command|RunCommand|run_command)$/i.test(t.name)); + if (readTool) fewShotTools.push(readTool); + else if (bashTool) fewShotTools.push(bashTool); + else if (coreTools.length > 0) fewShotTools.push(coreTools[0]); + + // 2) 第三方工具:按命名空间/来源分组,每组取一个代表 + // 命名空间提取规则: + // - MCP 工具: "mcp__server__tool" → namespace = "mcp__server" + // - 双下划线分隔: "prefix__action" → namespace = "prefix" + // - 蛇形命名: "prefix_action_name" → namespace = 第一段 + // - 驼峰命名: "PrefixActionName" → namespace = 驼峰前缀 + // - 其他: 用工具名自身 + const getToolNamespace = (name: string): string => { + // MCP 双下划线格式: mcp__server__tool → "mcp__server" + const mcpMatch = name.match(/^(mcp__[^_]+)/); + if (mcpMatch) return mcpMatch[1]; + // 通用双下划线: prefix__tool → "prefix" + const doubleUnder = name.match(/^([^_]+)__/); + if (doubleUnder) return doubleUnder[1]; + // 蛇形命名前缀: some_prefix_action → "some"(至少要有2段才取前缀) + const snakeParts = name.split('_'); + if (snakeParts.length >= 3) return snakeParts[0]; + // 驼峰命名前缀: SuperPowersDoSomething → "SuperPowers" + const camelMatch = name.match(/^([A-Z][a-z]+(?:[A-Z][a-z]+)?)/); + if (camelMatch && camelMatch[1] !== name) return camelMatch[1]; + // 兜底:整个工具名就是 namespace + return name; + }; + + // 按 namespace 分组 + const namespaceGroups = new Map(); + for (const tp of thirdPartyTools) { + const ns = getToolNamespace(tp.name); + if (!namespaceGroups.has(ns)) namespaceGroups.set(ns, []); + namespaceGroups.get(ns)!.push(tp); + } + + // 每个 namespace 选一个代表(优先选有描述的) + const MAX_THIRDPARTY_FEWSHOT = 4; // 最多 4 个第三方工具代表 + const namespaceEntries = [...namespaceGroups.entries()] + .sort((a, b) => b[1].length - a[1].length); // 工具多的 namespace 优先 + + for (const [ns, nsTools] of namespaceEntries) { + if (fewShotTools.length >= 1 + MAX_THIRDPARTY_FEWSHOT) break; // 1 核心 + N 第三方 + // 选该 namespace 中描述最长的工具作为代表 + const representative = nsTools.sort((a, b) => + (b.description?.length || 0) - (a.description?.length || 0) + )[0]; + fewShotTools.push(representative); + } + + // 如果连一个都没选到,用 tools[0] + if (fewShotTools.length === 0 && tools.length > 0) { + fewShotTools.push(tools[0]); + } + + if (thirdPartyTools.length > 0) { + console.log(`[Converter] Few-shot 工具选择: ${fewShotTools.map(t => t.name).join(', ')} (${namespaceGroups.size} 个命名空间, ${thirdPartyTools.length} 个第三方工具)`); + } + + // 构建多工具 few-shot 回复 + const fewShotActions = fewShotTools.map(t => + `\`\`\`json action\n${JSON.stringify({ tool: t.name, parameters: makeExampleParams(t) }, null, 2)}\n\`\`\`` + ).join('\n\n'); // 自然的 few-shot:模拟一次真实的 IDE 交互 messages.push({ @@ -272,10 +364,9 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise 标签 // few-shot 是让模型遵循输出格式最强力的手段 - const fewShotAction = `\`\`\`json action\n${JSON.stringify({ tool: fewShotTool.name, parameters: fewShotParams }, null, 2)}\n\`\`\``; const fewShotResponse = thinkingEnabled - ? `\nThe user wants me to help with their project. I should start by examining the project structure to understand what we're working with.\n\n\nLet me start by examining the project structure.\n\n${fewShotAction}` - : `Understood. I'll use the structured format for actions. Here's how I'll respond:\n\n${fewShotAction}`; + ? `\nThe user wants me to help with their project. I should start by examining the project structure and using the available tools to understand what we're working with.\n\n\nLet me start by using multiple tools to gather information.\n\n${fewShotActions}` + : `Understood. I'll use all available actions as appropriate. Here are my first steps:\n\n${fewShotActions}`; messages.push({ parts: [{ type: 'text', text: fewShotResponse }], id: shortId(), @@ -293,7 +384,7 @@ export async function convertToCursorRequest(req: AnthropicRequest): Promise