From 445695c6e131d8f3871f8c4a142ad399e836ce2f Mon Sep 17 00:00:00 2001 From: thzxx Date: Sat, 18 Apr 2026 08:15:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Agent=20Engine=20=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=85=9C=E5=BA=95=20=E2=80=94=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8D=E6=94=AF=E6=8C=81=20function=20calling=20?= =?UTF-8?q?=E7=9A=84=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当模型没有通过 Ollama API 的 tool_calls 字段返回工具调用, 而是在回复文本中写出 Action/Action Input 时(如 gemma4 系列), 自动从文本中提取工具调用,确保 ReAct Loop 正常执行。 支持的文本格式: - **Action:** tool_name **Action Input:** {json} - Action: tool_name Action Input: {json} - 工具名白名单校验 + JSON 解析容错(单引号、尾逗号等) --- src/renderer/services/agent-engine.ts | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index ff3c5f6..4cfdb33 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -87,6 +87,89 @@ const TOOL_USAGE_GUIDE = `【工具使用规则】 9. 记忆工具:你可以使用 memory_search 搜索过去的记忆,使用 memory_add 添加新的重要记忆。 10. 会话工具:你可以使用 session_list 列出历史会话,使用 session_read 读取历史会话内容。`; +/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */ +const VALID_TOOL_NAMES = new Set([ + 'read_file', 'write_file', 'list_directory', 'search_files', 'create_directory', + 'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search', + 'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', + 'replace_in_files', 'read_multiple_files', 'git', 'compress', + 'memory_search', 'memory_add', 'session_list', 'session_read' +]); + +/** + * 文本解析兜底:当模型没有通过 tool_calls 字段返回工具调用, + * 而是在文本中写了 "Action: xxx" / "Action Input: {...}" 时, + * 从文本中提取工具调用。 + */ +function parseToolCallsFromText(content: string): ToolCall[] { + const calls: ToolCall[] = []; + + // 匹配模式1: **Action:** tool_name **Action Input:** {json} + // 匹配模式2: Action: tool_name Action Input: {json} + // 同时支持 Action 和 Action Input 写在同一段或多行 + const actionRegex = /\*?\*?Action:?\*?\*?\s*([\w]+)\s*[\r\n]+\s*\*?\*?Action\s*Input:?\*?\*?\s*(\{[\s\S]*?\})/gi; + + let match; + while ((match = actionRegex.exec(content)) !== null) { + const toolName = match[1].trim(); + const argsStr = match[2].trim(); + + if (!VALID_TOOL_NAMES.has(toolName)) continue; + + try { + // 清理 JSON:去除可能的 markdown 代码块包裹 + let cleaned = argsStr.replace(/```json?\s*/g, '').replace(/```/g, '').trim(); + const args = JSON.parse(cleaned); + calls.push({ + type: 'function', + function: { name: toolName, arguments: args } + }); + } catch { + // JSON 解析失败,尝试修复常见问题 + try { + // 处理单引号、尾逗号等 + let fixed = argsStr + .replace(/'/g, '"') + .replace(/,\s*}/g, '}') + .replace(/,\s*]/g, ']') + .replace(/```json?\s*/g, '') + .replace(/```/g, '') + .trim(); + const args = JSON.parse(fixed); + calls.push({ + type: 'function', + function: { name: toolName, arguments: args } + }); + } catch { + logWarn(`文本解析兜底: 工具 ${toolName} 的参数 JSON 解析失败`, argsStr.slice(0, 100)); + } + } + } + + // 备选模式:Action 和 Action Input 写在一行或用不同分隔 + if (calls.length === 0) { + const singleLineRegex = /Action:\s*(\w+)\s*.*?Action\s*Input:\s*(\{[^}]+\})/gi; + while ((match = singleLineRegex.exec(content)) !== null) { + const toolName = match[1].trim(); + const argsStr = match[2].trim(); + if (!VALID_TOOL_NAMES.has(toolName)) continue; + try { + const args = JSON.parse(argsStr); + calls.push({ + type: 'function', + function: { name: toolName, arguments: args } + }); + } catch { /* skip */ } + } + } + + if (calls.length > 0) { + logInfo(`文本解析兜底: 从回复中提取到 ${calls.length} 个工具调用`, calls.map(c => c.function.name).join(', ')); + } + + return calls; +} + /** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */ const toolResultCache = new Map(); @@ -340,6 +423,14 @@ export async function runAgentLoop( logModelResponse(content.length, toolCalls.length); + // 文本解析兜底:模型没通过 tool_calls 返回但文本中写了 Action + if (toolCalls.length === 0 && useTools) { + const parsedCalls = parseToolCallsFromText(content); + if (parsedCalls.length > 0) { + toolCalls.push(...parsedCalls); + } + } + // 检查是否是 Final Answer(没有工具调用且包含 Final Answer) const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:'); if (toolCalls.length === 0) {