fix: 修复多迭代 Agent Loop 的两个UI bug

Bug 1: 正在思考占位符作为独立消息残留
- 根因: renderMessages() 将 loading placeholder 计入 existingCount,
  导致新保存的消息不被渲染,旧 placeholder 永远留在页面上
- 修复: onNewIteration 保存消息前先移除旧 placeholder

Bug 2: 只有最后一条AI消息显示 token 统计
- 根因: onNewIteration 保存中间消息时不附带 token/duration 统计,
  只有 onDone 对最后一条才添加 loopStats(总累计值)
- 修复: 追踪每轮迭代的 eval_count 增量和耗时,
  中间消息和最后消息均保存 per-iteration 统计,
  确保 updateTotalTokens 求和不重复计数
This commit is contained in:
thzxx
2026-04-21 09:05:07 +08:00
parent 2496e04685
commit e5acf646c8
2 changed files with 33 additions and 6 deletions
+32 -5
View File
@@ -927,17 +927,32 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
let assistantContent = ''; let assistantContent = '';
let thinkContent = ''; let thinkContent = '';
let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数 let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数
state.set('_currentEvalCount', 0); // 重置累计 token 计数,防止跨会话残留
let lastIterationEvalCount = 0; // 上一轮迭代结束时的累计 token 数
let lastIterationStartTime = Date.now(); // 当前迭代开始时间
try { try {
await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, { await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, {
onNewIteration: () => { onNewIteration: () => {
// Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder // Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder
// 先移除旧 placeholder(防止被 renderMessages 当作已渲染消息计数)
const oldPlaceholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
if (oldPlaceholder) oldPlaceholder.remove();
if (assistantContent) { if (assistantContent) {
const now = Date.now();
// 计算本轮迭代的 token 和时长
const currentEvalCount = state.get<number>('_currentEvalCount', 0);
const iterationEvalCount = Math.max(0, currentEvalCount - lastIterationEvalCount);
const iterationDuration = (now - lastIterationStartTime) * 1e6;
const prevMsg: ChatMessage = { const prevMsg: ChatMessage = {
role: 'assistant', role: 'assistant',
content: assistantContent, content: assistantContent,
timestamp: Date.now(), timestamp: now,
...(thinkContent && { think: thinkContent }), ...(thinkContent && { think: thinkContent }),
...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }),
...(iterationDuration > 0 && { total_duration: iterationDuration }),
}; };
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({ state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
...session, ...session,
@@ -945,6 +960,9 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updatedAt: Date.now() updatedAt: Date.now()
})); }));
renderMessages(); renderMessages();
lastIterationEvalCount = currentEvalCount;
lastIterationStartTime = Date.now();
} }
// 创建新 placeholder,重置内容 // 创建新 placeholder,重置内容
appendAssistantPlaceholder(); appendAssistantPlaceholder();
@@ -986,15 +1004,24 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
// 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里只更新 placeholder // 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里只更新 placeholder
if (agentModeIterations > 0) { if (agentModeIterations > 0) {
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null); // 最后一轮迭代的 per-iteration stats(保证每条消息统计一致,不重复计数)
// 更新最后一条消息的工具记录(onNewIteration 保存时没有 toolCalls const finalEvalCount = loopStats?.eval_count || 0;
const lastIterTokens = Math.max(0, finalEvalCount - lastIterationEvalCount);
const lastIterDuration = (Date.now() - lastIterationStartTime) * 1e6;
const perIterStats = {
eval_count: lastIterTokens > 0 ? lastIterTokens : undefined,
total_duration: lastIterDuration > 0 ? lastIterDuration : undefined,
};
updateLastAssistantMessage(assistantContent, thinkContent || null, perIterStats);
// 更新最后一条消息的工具记录和 per-iteration statsonNewIteration 保存时没有 toolCalls
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => { state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => {
const msgs = [...session.messages]; const msgs = [...session.messages];
for (let i = msgs.length - 1; i >= 0; i--) { for (let i = msgs.length - 1; i >= 0; i--) {
if (msgs[i].role === 'assistant') { if (msgs[i].role === 'assistant') {
if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords }; if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords };
if (loopStats?.eval_count) msgs[i] = { ...msgs[i], eval_count: loopStats.eval_count }; if (lastIterTokens > 0) msgs[i] = { ...msgs[i], eval_count: lastIterTokens };
if (loopStats?.total_duration) msgs[i] = { ...msgs[i], total_duration: loopStats.total_duration }; if (lastIterDuration > 0) msgs[i] = { ...msgs[i], total_duration: lastIterDuration };
break; break;
} }
} }
+1 -1
View File
@@ -581,7 +581,7 @@ export async function runAgentLoop(
content += chunk.message.content; content += chunk.message.content;
callbacks.onContent(content); callbacks.onContent(content);
} }
if (chunk.eval_count) totalEvalCount = chunk.eval_count; if (chunk.eval_count) { totalEvalCount = chunk.eval_count; state.set('_currentEvalCount', totalEvalCount); }
if (chunk.message?.tool_calls?.length) { if (chunk.message?.tool_calls?.length) {
for (const tc of chunk.message.tool_calls) { for (const tc of chunk.message.tool_calls) {
if (tc.function?.name) { if (tc.function?.name) {