From 5ee23387c0234ac93463636e25ae874570b4693b Mon Sep 17 00:00:00 2001 From: thzxx Date: Fri, 17 Apr 2026 10:28:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DAgent=20Loop=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E9=87=8D=E5=A4=8D=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4web=5Ffetch=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增工具调用缓存机制,避免重复调用相同工具+参数 - 新增同轮内重复检测和跨轮次重复检测,连续两轮相同调用自动终止循环 - TOOLS_USAGE_GUIDE 新增第8条严禁重复调用规则 - web_fetch 默认不截断(max_chars=0),返回完整内容 - 移除 tool-registry.ts executeTool 中重复的 logToolStart 调用 --- src/main/tool-handlers.ts | 6 +-- src/renderer/services/agent-engine.ts | 75 +++++++++++++++++++++++++- src/renderer/services/tool-registry.ts | 4 +- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/main/tool-handlers.ts b/src/main/tool-handlers.ts index 2a8370b..b64cdaf 100644 --- a/src/main/tool-handlers.ts +++ b/src/main/tool-handlers.ts @@ -433,9 +433,9 @@ export async function handleWebFetch(params: { url: string; max_chars?: number; return { success: false, error: '仅支持 http/https 协议' }; } - const maxChars = params.max_chars || 10000; + const maxChars = params.max_chars || 0; // 0 = 不截断,返回全部内容 - sendLog('info', `🌐 web_fetch`, `${url} (${maxChars} chars)`); + sendLog('info', `🌐 web_fetch`, `${url} (${maxChars > 0 ? maxChars + ' chars' : '完整内容'})`); const resp = await fetch(url, { headers: { 'User-Agent': 'MetonaOllama/1.0' }, signal: AbortSignal.timeout(15000) @@ -466,7 +466,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number; .trim(); } - const truncated = text.length > maxChars; + const truncated = maxChars > 0 && text.length > maxChars; if (truncated) text = text.slice(0, maxChars); sendLog('success', `🌐 web_fetch 完成`, `${resp.status} | ${text.length} chars${truncated ? ' (已截断)' : ''}`); diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index ede33c8..3992abf 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -39,7 +39,30 @@ const TOOL_USAGE_GUIDE = `【工具使用规则】 - 用户让你查看/修改/分析文件 → read_file / list_directory / search_files - 用户问项目状态、代码内容 → git status / read_file - 用户让你执行操作 → run_command / write_file 等 - 宁可多调一次工具,也不要给出过时或编造的信息。`; + 宁可多调一次工具,也不要给出过时或编造的信息。 +8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。直接使用已有结果继续工作。例如:已经 web_fetch 抓取了页面内容,下一步应直接处理该内容(如 write_file 保存),而不是重新抓取。`; + +/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */ +const toolResultCache = new Map(); + +/** 生成工具调用缓存 key */ +function getToolCacheKey(name: string, args: Record): string { + try { + // 对 arguments 排序 key 以保证一致性 + return name + '::' + JSON.stringify(args, Object.keys(args).sort()); + } catch { + return name + '::' + String(args); + } +} + +/** 检测当前轮次是否存在重复工具调用 */ +function isDuplicateCall(call: ToolCall, loopCalls: ToolCall[]): boolean { + const callKey = getToolCacheKey(call.function.name, call.function.arguments); + return loopCalls.some((prev, idx) => { + if (idx === loopCalls.length - 1) return false; // 跳过自身 + return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey; + }); +} export interface AgentCallbacks { onThinking: (text: string) => void; @@ -65,6 +88,9 @@ export async function runAgentLoop( return; } + // 新一轮对话,清空工具缓存 + toolResultCache.clear(); + // 检查模型是否支持 Tool Calling const modelSupportsTools = state.get('modelSupportsTools', false); if (!modelSupportsTools) { @@ -126,6 +152,8 @@ export async function runAgentLoop( const loopStartTime = Date.now(); let content = ''; let totalEvalCount = 0; + /** 记录前一轮的工具调用 key,用于跨轮去重 */ + let prevLoopToolKeys: string[] = []; const makeStats = () => { const totalDuration = (Date.now() - loopStartTime) * 1e6; // 转为纳秒 @@ -263,6 +291,17 @@ export async function runAgentLoop( return; } + // 跨轮次完整重复检测:如果本轮所有工具调用与上一轮完全相同,终止循环 + const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort(); + const currentKeysStr = JSON.stringify(currentLoopKeys); + const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort()); + if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) { + logWarn('检测到连续两轮工具调用完全相同,终止 Agent Loop', currentKeysStr.slice(0, 200)); + callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats()); + return; + } + prevLoopToolKeys = currentLoopKeys; + for (const call of toolCalls) { // 检查是否已中止 if (abortController.signal.aborted) { @@ -270,6 +309,36 @@ export async function runAgentLoop( return; } + // 同一轮内重复调用检测 + if (isDuplicateCall(call, toolCalls)) { + logWarn(`跳过重复工具调用: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200)); + const cachedKey = getToolCacheKey(call.function.name, call.function.arguments); + const cached = toolResultCache.get(cachedKey); + if (cached) { + messages.push({ + role: 'tool', + tool_name: call.function.name, + content: JSON.stringify(cached) + }); + callbacks.onToolCallResult(call.function.name, cached, call); + } + continue; + } + + // 跨轮次缓存命中检测 + const cacheKey = getToolCacheKey(call.function.name, call.function.arguments); + const cachedResult = toolResultCache.get(cacheKey); + if (cachedResult) { + logInfo(`工具缓存命中: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200)); + messages.push({ + role: 'tool', + tool_name: call.function.name, + content: JSON.stringify(cachedResult) + }); + callbacks.onToolCallResult(call.function.name, cachedResult, call); + continue; + } + callbacks.onToolCallStart(call); logToolStart(call.function.name, JSON.stringify(call.function.arguments)); @@ -333,6 +402,10 @@ export async function runAgentLoop( tool_name: call.function.name, content: JSON.stringify(result) }); + // 缓存成功结果(非 run_command,因为命令执行可能有副作用) + if (result.success && call.function.name !== 'run_command') { + toolResultCache.set(cacheKey, result); + } callbacks.onToolCallResult(call.function.name, result, call); logToolResult(call.function.name, result.success, result.success ? undefined : result.error); } catch (err) { diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 218d13a..77a9227 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -160,7 +160,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ required: ['url'], properties: { url: { type: 'string', description: 'URL to fetch (http/https).' }, - max_chars: { type: 'integer', description: 'Max characters to return. Default: 10000.' }, + max_chars: { type: 'integer', description: 'Max characters to return. Default: 0 (no limit, return full content).' }, extract_mode: { type: 'string', enum: ['text', 'markdown'], description: 'Extraction mode. Default: text.' } } } @@ -409,8 +409,6 @@ export async function executeTool(toolName: string, args: Record