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;
// 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_CAP_MS = 60_000;
@ -540,11 +554,22 @@ public class NodeStreamingChatHelper {
broadcastDelta(conversationId, "warning",
buildDeltaJson("⏱️ 请求频率受限,等待 " + (delay / 1000) + " 秒后重试(第 " + attempt + "/" + MAX_RETRIES + " 次)..."));
}
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return buildErrorResult("LLM 调用被中断", conversationId, phase);
// Poll stop flag every 100ms so user Stop is honored mid-backoff.
long remaining = delay;
while (remaining > 0) {
if (streamTracker != null && streamTracker.isStopRequested(conversationId)) {
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);
}
// Rate limit / Server error: 重试
if (attempt < MAX_RETRIES && (errorType == ErrorType.RATE_LIMIT || errorType == ErrorType.SERVER_ERROR)) {
// Rate limit / Server error: retry with reduced limit for rate-limit/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={}): {}",
phase, attempt, MAX_RETRIES, errorType, error.getMessage());
phase, attempt, effectiveMaxRetries, errorType, error.getMessage());
return null; // 返回 null 触发重试
}

View File

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

View File

@ -26,8 +26,8 @@ import java.util.Set;
* agent:
* tool-result:
* enabled: true
* per-result-threshold-chars: 4000
* per-turn-budget-chars: 16000
* per-result-threshold-chars: 16000
* per-turn-budget-chars: 32000
* preview-head-chars: 800
* storage-base-dir:
* </pre>
@ -39,10 +39,10 @@ public class ToolResultProperties {
private boolean enabled = true;
/** 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. */
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. */
private int previewHeadChars = 800;

View File

@ -165,6 +165,12 @@ public class PlanGenerationNode implements NodeAction {
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.
NodeStreamingChatHelper.StreamResult result = streamingHelper.streamCallSilent(
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).
tool-result:
enabled: true
per-result-threshold-chars: 4000
per-turn-budget-chars: 16000
per-result-threshold-chars: 16000 # was 4000 — prevents WebSearch spill-to-disk
per-turn-budget-chars: 32000 # was 16000 — headroom for multi-tool turns
preview-head-chars: 800
storage-base-dir: ""
# Retrieval-style tools that must NEVER be spilled. Spilling read_file's