From 875d04d1e1da917058bd225f08d4b6cc909e425d Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 20 Apr 2026 20:07:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Agent=20Loop=20=E5=A4=9A=E8=BD=AE?= =?UTF-8?q?=E8=BF=AD=E4=BB=A3=E6=B6=88=E6=81=AF=E4=BA=92=E7=9B=B8=E8=A6=86?= =?UTF-8?q?=E7=9B=96=20=E2=80=94=20=E6=AF=8F=E8=BD=AE=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=B6=88=E6=81=AF=E6=B0=94=E6=B3=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:Agent Loop 多轮迭代(thought→action→observation→循环)时,每轮 onContent 回调都通过 updateLastAssistantMessage 替换同一个 placeholder 的 innerHTML,导致前一轮的回复内容被后一轮覆盖,只有最后一轮保留。 修复: - agent-engine.ts: AgentCallbacks 新增 onNewIteration 回调,每轮迭代 开始前通知 UI 层 - input-area.ts: sendMessageWithAgentLoop + handleRetry 添加 onNewIteration 处理——保存当前轮内容为独立消息、重置状态、创建新 placeholder。onDone 根据 agentModeIterations 判断是否需要重复保存 --- src/renderer/components/input-area.ts | 134 ++++++++++++++++++++------ src/renderer/services/agent-engine.ts | 7 ++ 2 files changed, 112 insertions(+), 29 deletions(-) diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts index a03696c..23c17a5 100644 --- a/src/renderer/components/input-area.ts +++ b/src/renderer/components/input-area.ts @@ -291,10 +291,28 @@ async function handleRetry(): Promise { }); try { + let retryContent = ''; let retryThinkContent = ''; + let retryIterations = 0; await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, { - onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage('', thinking, null); }, - onContent: (content) => updateLastAssistantMessage(content, retryThinkContent || null, null), + onNewIteration: () => { + if (retryContent) { + const prevMsg: ChatMessage = { + role: 'assistant', content: retryContent, timestamp: Date.now(), + ...(retryThinkContent && { think: retryThinkContent }), + }; + state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({ + ...s, messages: [...s.messages, prevMsg], updatedAt: Date.now() + })); + renderMessages(); + } + appendAssistantPlaceholder(); + retryContent = ''; + retryThinkContent = ''; + retryIterations++; + }, + onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage(retryContent, thinking, null); }, + onContent: (content) => { retryContent = content; updateLastAssistantMessage(content, retryThinkContent || null, null); }, onToolCallStart: (call) => { addToolCard({ name: call.function.name, arguments: call.function.arguments, @@ -315,17 +333,34 @@ async function handleRetry(): Promise { }, onConfirmTool: async (call) => showToolConfirm(call), onDone: async (finalContent, toolRecords, loopStats) => { - const assistantMsg: ChatMessage = { - role: 'assistant', content: finalContent || '', timestamp: Date.now(), - ...(retryThinkContent && { think: retryThinkContent }), - ...(toolRecords?.length && { toolCalls: toolRecords }), - ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), - ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), - }; - state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({ - ...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now() - })); - updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null); + retryContent = finalContent; + if (retryIterations > 0) { + updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null); + state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => { + const msgs = [...s.messages]; + for (let i = msgs.length - 1; i >= 0; i--) { + if (msgs[i].role === 'assistant') { + if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords }; + if (loopStats?.eval_count) msgs[i] = { ...msgs[i], eval_count: loopStats.eval_count }; + if (loopStats?.total_duration) msgs[i] = { ...msgs[i], total_duration: loopStats.total_duration }; + break; + } + } + return { ...s, messages: msgs, updatedAt: Date.now() }; + }); + } else { + const assistantMsg: ChatMessage = { + role: 'assistant', content: finalContent || '', timestamp: Date.now(), + ...(retryThinkContent && { think: retryThinkContent }), + ...(toolRecords?.length && { toolCalls: toolRecords }), + ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), + ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), + }; + state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({ + ...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now() + })); + updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null); + } renderMessages(); await saveCurrentSession(); updateTotalTokens(); @@ -891,9 +926,32 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio let assistantContent = ''; let thinkContent = ''; + let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数 try { await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, { + onNewIteration: () => { + // Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder + if (assistantContent) { + const prevMsg: ChatMessage = { + role: 'assistant', + content: assistantContent, + timestamp: Date.now(), + ...(thinkContent && { think: thinkContent }), + }; + state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({ + ...session, + messages: [...session.messages, prevMsg], + updatedAt: Date.now() + })); + renderMessages(); + } + // 创建新 placeholder,重置内容 + appendAssistantPlaceholder(); + assistantContent = ''; + thinkContent = ''; + agentModeIterations++; + }, onThinking: (thinking) => { thinkContent = thinking; updateLastAssistantMessage(assistantContent, thinkContent || null, null); @@ -926,22 +984,40 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio onDone: async (finalContent, toolRecords, loopStats) => { assistantContent = finalContent; - const assistantMsg: ChatMessage = { - role: 'assistant', - content: assistantContent || '', - timestamp: Date.now(), - ...(thinkContent && { think: thinkContent }), - ...(toolRecords?.length && { toolCalls: toolRecords }), - ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), - ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), - }; - state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({ - ...session, - messages: [...session.messages, assistantMsg], - updatedAt: Date.now() - })); - - updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null); + // 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里只更新 placeholder + if (agentModeIterations > 0) { + updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null); + // 更新最后一条消息的工具记录(onNewIteration 保存时没有 toolCalls) + state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => { + const msgs = [...session.messages]; + for (let i = msgs.length - 1; i >= 0; i--) { + if (msgs[i].role === 'assistant') { + if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords }; + if (loopStats?.eval_count) msgs[i] = { ...msgs[i], eval_count: loopStats.eval_count }; + if (loopStats?.total_duration) msgs[i] = { ...msgs[i], total_duration: loopStats.total_duration }; + break; + } + } + return { ...session, messages: msgs, updatedAt: Date.now() }; + }); + } else { + // 单迭代模式:正常保存 + const assistantMsg: ChatMessage = { + role: 'assistant', + content: assistantContent || '', + timestamp: Date.now(), + ...(thinkContent && { think: thinkContent }), + ...(toolRecords?.length && { toolCalls: toolRecords }), + ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), + ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), + }; + state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({ + ...session, + messages: [...session.messages, assistantMsg], + updatedAt: Date.now() + })); + updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null); + } renderMessages(); await saveCurrentSession(); updateTotalTokens(); diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index d36ee43..1fdf010 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -357,6 +357,8 @@ export interface AgentCallbacks { onToolCallError: (name: string, error: string, call: ToolCall) => void; onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void; onConfirmTool: (call: ToolCall) => Promise; + /** Agent Loop 新迭代开始(前一轮工具执行完毕,下一轮流式输出即将开始) */ + onNewIteration?: () => void; } /** 保存执行轨迹到 SQLite */ @@ -530,6 +532,11 @@ export async function runAgentLoop( throw new DOMException('Aborted', 'AbortError'); } + // 非首轮迭代:通知 UI 创建新的消息气泡(防止每轮内容互相覆盖) + if (loopCount > 1 && callbacks.onNewIteration) { + callbacks.onNewIteration(); + } + logAgentLoop(loopCount, maxLoops); let thinking = '';