mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(wiki): bound LLM retry + http read timeout (RFC-012 M1 follow-up)
This commit is contained in:
parent
b9ed8219ba
commit
a369e8055d
@ -33,7 +33,9 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import java.time.Duration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Flux;
|
||||
@ -805,7 +807,8 @@ public class AgentGraphBuilder {
|
||||
Map<String, Object> kwargs = modelProviderService.readProviderGenerateKwargs(provider);
|
||||
MultiValueMap<String, String> headers = buildOpenAiHeaders(kwargs);
|
||||
String completionsPath = resolveOpenAiCompletionsPath(baseUrl, kwargs);
|
||||
RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
|
||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
|
||||
|
||||
// Spring AI OpenAiApi 构造函数会先 set User-Agent 为 "spring-ai",再 addAll 我们的 headers,
|
||||
@ -923,7 +926,8 @@ public class AgentGraphBuilder {
|
||||
throw new MateClawException("err.agent.anthropic_key_invalid", "Anthropic API Key 未配置或无效: " + provider.getProviderId());
|
||||
}
|
||||
String baseUrl = provider.getBaseUrl();
|
||||
RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
|
||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
|
||||
|
||||
AnthropicApi.Builder builder = AnthropicApi.builder()
|
||||
@ -1275,6 +1279,19 @@ public class AgentGraphBuilder {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-012 M1:给 LLM 调用走的 RestClient 显式配置超时,避免 socket 永久挂起等待。
|
||||
* <p>
|
||||
* connectTimeout=10s(任何 LLM 提供方都不该超过这个建立连接时间);
|
||||
* readTimeout=180s(覆盖 nginx 60s 网关超时 + 留足真实长响应余量;超时后由上层 retry 接管)。
|
||||
*/
|
||||
private RestClient.Builder applyHttpTimeouts(RestClient.Builder builder) {
|
||||
SimpleClientHttpRequestFactory rf = new SimpleClientHttpRequestFactory();
|
||||
rf.setConnectTimeout(Duration.ofSeconds(10));
|
||||
rf.setReadTimeout(Duration.ofSeconds(180));
|
||||
return builder.requestFactory(rf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 generateKwargs.headers 中提取需要强制覆盖的 headers。
|
||||
* 用于通过 RestClient/WebClient 拦截器绕过 Spring AI OpenAiApi 的默认 User-Agent。
|
||||
|
||||
@ -57,4 +57,28 @@ public class WikiProperties {
|
||||
|
||||
/** 扫描时跳过大于此大小的文件(字节),默认 50MB */
|
||||
private long maxScanFileSize = 50 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Wiki LLM 重试最大尝试次数(含首次)。
|
||||
* <p>
|
||||
* RFC-012 M1:旧实现无最大次数,遇到 nginx 504 这种"反复瞬时"错误会永远重试。
|
||||
* 设为 5 后单 chunk 最多走 5 轮,配合 llmMaxTotalDurationMs 共同保证有界停止。
|
||||
*/
|
||||
private int llmMaxAttempts = 5;
|
||||
|
||||
/**
|
||||
* Wiki LLM 重试总耗时上限(毫秒),从首次调用开始计时。
|
||||
* <p>
|
||||
* RFC-012 M1:单 chunk LLM 调用 + 重试的硬封顶,超过即放弃,让该 chunk 进入 failed 计数。
|
||||
* 默认 4 分钟。
|
||||
*/
|
||||
private long llmMaxTotalDurationMs = 240_000;
|
||||
|
||||
/**
|
||||
* 是否启用两阶段消化(路由 → 逐页 merge)。
|
||||
* <p>
|
||||
* RFC-012 M2:true 时单 chunk 的 LLM 输出量大幅缩减,避免 nginx 60s 网关超时。
|
||||
* 默认 false 保持向后兼容;M2 实现完成后切到 true。
|
||||
*/
|
||||
private boolean useTwoPhaseDigest = false;
|
||||
}
|
||||
|
||||
@ -454,16 +454,22 @@ public class WikiProcessingService {
|
||||
* 立即终止(模型不可用):401/403 认证失败、模型不存在、quota 用尽、非法 API key、
|
||||
* InterruptedException(优雅关停)。
|
||||
* <p>
|
||||
* 使用指数退避(1s → 2s → 4s → ... → 封顶 60s),无最大尝试次数。
|
||||
* 使用指数退避(1s → 2s → 4s → ... → 封顶 60s)。
|
||||
* <p>
|
||||
* RFC-012 M1:加入 maxAttempts 与 maxTotalDurationMs 双重上限,避免 nginx 504 这种
|
||||
* 反复瞬时错误把单 chunk 卡到永远;buildChatModel 提到循环外,所有重试复用同一实例。
|
||||
*/
|
||||
private String callLlmWithResilientRetry(Prompt prompt, String ctx) {
|
||||
long backoffMs = 1000;
|
||||
final long maxBackoffMs = 60_000;
|
||||
final int maxAttempts = Math.max(1, properties.getLlmMaxAttempts());
|
||||
final long maxTotalDurationMs = Math.max(1_000L, properties.getLlmMaxTotalDurationMs());
|
||||
final long startNanos = System.nanoTime();
|
||||
final ChatModel chatModel = buildChatModel();
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
attempt++;
|
||||
try {
|
||||
ChatModel chatModel = buildChatModel();
|
||||
ChatResponse response = chatModel.call(prompt);
|
||||
if (response == null || response.getResult() == null
|
||||
|| response.getResult().getOutput() == null
|
||||
@ -484,10 +490,17 @@ public class WikiProcessingService {
|
||||
ctx, attempt, t.getMessage());
|
||||
throw new RuntimeException("LLM unavailable: " + t.getMessage(), t);
|
||||
}
|
||||
log.warn("[Wiki] LLM transient failure for {} attempt={}, retrying in {}ms: {}",
|
||||
ctx, attempt, backoffMs, t.getMessage());
|
||||
long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L;
|
||||
if (attempt >= maxAttempts || elapsedMs >= maxTotalDurationMs) {
|
||||
log.error("[Wiki] LLM exhausted for {} after {} attempts in {}ms (limits: maxAttempts={}, maxTotalDurationMs={}): {}",
|
||||
ctx, attempt, elapsedMs, maxAttempts, maxTotalDurationMs, t.getMessage());
|
||||
throw new RuntimeException("LLM exhausted: " + t.getMessage(), t);
|
||||
}
|
||||
long sleepMs = Math.min(backoffMs, Math.max(0L, maxTotalDurationMs - elapsedMs));
|
||||
log.warn("[Wiki] LLM transient failure for {} attempt={}/{} elapsed={}ms, retrying in {}ms: {}",
|
||||
ctx, attempt, maxAttempts, elapsedMs, sleepMs, t.getMessage());
|
||||
try {
|
||||
Thread.sleep(backoffMs);
|
||||
Thread.sleep(sleepMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("LLM retry interrupted for " + ctx, ie);
|
||||
|
||||
@ -59,8 +59,10 @@ spring:
|
||||
jdbc:
|
||||
initialize-schema: embedded
|
||||
# Spring AI Retry 配置:429/503/529 归为可重试(TransientAiException),启用指数退避
|
||||
# RFC-012 M1:max-attempts 从 5 调到 2,避免与 wiki 层 callLlmWithResilientRetry 的 5×N 嵌套放大;
|
||||
# 真正的重试主控权交给业务层(wiki / agent),Spring AI 仅负责"一次性瞬时抖动"的快速重试
|
||||
retry:
|
||||
max-attempts: 5
|
||||
max-attempts: 2
|
||||
on-http-codes: 429, 503, 529
|
||||
backoff:
|
||||
initial-interval: 3000
|
||||
|
||||
Loading…
Reference in New Issue
Block a user