perf(agent): implement Lane D performance fixes

This commit is contained in:
matevip 2026-04-22 10:13:07 +08:00
parent 1a00b9276c
commit 23133ea45d
5 changed files with 51 additions and 21 deletions

View File

@ -256,9 +256,23 @@ public class NodeStreamingChatHelper {
} }
} }
/**
* Broadcast a lightweight progress event so the frontend shows activity
* during silent LLM calls (e.g. triage). Sent as a "progress" SSE event.
*/
public void broadcastProgress(String conversationId, String message) {
if (streamTracker == null || conversationId == null || conversationId.isEmpty()) {
return;
}
streamTracker.broadcastObject(conversationId, "progress",
Map.of("message", message != null ? message : ""));
}
// ==================== 重试配置 ==================== // ==================== 重试配置 ====================
private static final int MAX_RETRIES = 5; private static final int MAX_RETRIES = 5;
// For rate-limit and server-error: fail fast to failover chain.
private static final int MAX_RETRIES_RATE_LIMIT = 2;
private static final long BACKOFF_BASE_MS = 3000; private static final long BACKOFF_BASE_MS = 3000;
private static final long BACKOFF_CAP_MS = 60_000; private static final long BACKOFF_CAP_MS = 60_000;
@ -540,11 +554,22 @@ public class NodeStreamingChatHelper {
broadcastDelta(conversationId, "warning", broadcastDelta(conversationId, "warning",
buildDeltaJson("⏱️ 请求频率受限,等待 " + (delay / 1000) + " 秒后重试(第 " + attempt + "/" + MAX_RETRIES + " 次)...")); buildDeltaJson("⏱️ 请求频率受限,等待 " + (delay / 1000) + " 秒后重试(第 " + attempt + "/" + MAX_RETRIES + " 次)..."));
} }
try { // Poll stop flag every 100ms so user Stop is honored mid-backoff.
Thread.sleep(delay); long remaining = delay;
} catch (InterruptedException ie) { while (remaining > 0) {
Thread.currentThread().interrupt(); if (streamTracker != null && streamTracker.isStopRequested(conversationId)) {
return buildErrorResult("LLM 调用被中断", conversationId, phase); log.info("[{}] Stop requested during backoff — aborting retry: conversationId={}",
phase, conversationId);
throw new CancellationException("Stream stopped by user");
}
long slice = Math.min(100, remaining);
try {
Thread.sleep(slice);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return buildErrorResult("LLM 调用被中断", conversationId, phase);
}
remaining -= slice;
} }
} }
@ -731,10 +756,12 @@ public class NodeStreamingChatHelper {
conversationId, phase, errorType); conversationId, phase, errorType);
} }
// Rate limit / Server error: 重试 // Rate limit / Server error: retry with reduced limit for rate-limit/server-error
if (attempt < MAX_RETRIES && (errorType == ErrorType.RATE_LIMIT || errorType == ErrorType.SERVER_ERROR)) { boolean isRateLimitOrServerError = errorType == ErrorType.RATE_LIMIT || errorType == ErrorType.SERVER_ERROR;
int effectiveMaxRetries = isRateLimitOrServerError ? MAX_RETRIES_RATE_LIMIT : MAX_RETRIES;
if (attempt < effectiveMaxRetries && isRateLimitOrServerError) {
log.warn("[{}] Retryable error (attempt {}/{}, type={}): {}", log.warn("[{}] Retryable error (attempt {}/{}, type={}): {}",
phase, attempt, MAX_RETRIES, errorType, error.getMessage()); phase, attempt, effectiveMaxRetries, errorType, error.getMessage());
return null; // 返回 null 触发重试 return null; // 返回 null 触发重试
} }

View File

@ -38,13 +38,10 @@ import java.util.concurrent.*;
public class ToolExecutionExecutor { public class ToolExecutionExecutor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ExecutorService TOOL_EXECUTOR = Executors.newFixedThreadPool( // JDK 21 virtual threads: no blocking stall for I/O-bound tools.
Math.max(4, Runtime.getRuntime().availableProcessors()), // Each tool invocation gets its own lightweight carrier thread.
r -> { private static final ExecutorService TOOL_EXECUTOR =
Thread t = new Thread(r, "tool-executor"); Executors.newVirtualThreadPerTaskExecutor();
t.setDaemon(true);
return t;
});
/** /**
* Legacy hardcoded unsafe set, kept as a fallback when no * Legacy hardcoded unsafe set, kept as a fallback when no

View File

@ -26,8 +26,8 @@ import java.util.Set;
* agent: * agent:
* tool-result: * tool-result:
* enabled: true * enabled: true
* per-result-threshold-chars: 4000 * per-result-threshold-chars: 16000
* per-turn-budget-chars: 16000 * per-turn-budget-chars: 32000
* preview-head-chars: 800 * preview-head-chars: 800
* storage-base-dir: * storage-base-dir:
* </pre> * </pre>
@ -39,10 +39,10 @@ public class ToolResultProperties {
private boolean enabled = true; private boolean enabled = true;
/** A single tool result larger than this is spilled to disk. */ /** A single tool result larger than this is spilled to disk. */
private int perResultThresholdChars = 4000; private int perResultThresholdChars = 16000; // was 4000 prevents WebSearch spill-to-disk
/** Aggregate cap on combined response size in one tool turn. */ /** Aggregate cap on combined response size in one tool turn. */
private int perTurnBudgetChars = 16000; private int perTurnBudgetChars = 32000; // was 16000 headroom for multi-tool turns
/** Number of leading characters kept inline as a preview after spilling. */ /** Number of leading characters kept inline as a preview after spilling. */
private int previewHeadChars = 800; private int previewHeadChars = 800;

View File

@ -165,6 +165,12 @@ public class PlanGenerationNode implements NodeAction {
Prompt prompt = new Prompt(promptMessages); Prompt prompt = new Prompt(promptMessages);
// Broadcast a lightweight progress token so the frontend shows activity
// during the silent triage call (typically 1-3 s).
if (streamingHelper != null) {
streamingHelper.broadcastProgress(conversationId, "分析中...");
}
// Silent streaming call structured JSON is parsed below; tokens are not forwarded to the client. // Silent streaming call structured JSON is parsed below; tokens are not forwarded to the client.
NodeStreamingChatHelper.StreamResult result = streamingHelper.streamCallSilent( NodeStreamingChatHelper.StreamResult result = streamingHelper.streamCallSilent(
chatModel, prompt, conversationId, "plan_generation"); chatModel, prompt, conversationId, "plan_generation");

View File

@ -167,8 +167,8 @@ mate:
# the in-context preview points the agent at the spill file (read_file tool). # the in-context preview points the agent at the spill file (read_file tool).
tool-result: tool-result:
enabled: true enabled: true
per-result-threshold-chars: 4000 per-result-threshold-chars: 16000 # was 4000 — prevents WebSearch spill-to-disk
per-turn-budget-chars: 16000 per-turn-budget-chars: 32000 # was 16000 — headroom for multi-tool turns
preview-head-chars: 800 preview-head-chars: 800
storage-base-dir: "" storage-base-dir: ""
# Retrieval-style tools that must NEVER be spilled. Spilling read_file's # Retrieval-style tools that must NEVER be spilled. Spilling read_file's