mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
conversation: separate retry and loop budgets
This commit is contained in:
@@ -259,8 +259,8 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
||||
@ValueDescription("达到需要合并转发消息的阈值")
|
||||
val messageMergeThreshold by value(150)
|
||||
|
||||
@ValueDescription("最大循环次数,至少2次")
|
||||
val retryMax: Int by value(5)
|
||||
@ValueDescription("单次对话正常调用模型的最大循环轮数,至少2轮;失败重试不占用此轮数")
|
||||
val retryMax: Int by value(10)
|
||||
|
||||
@ValueDescription("关键字呼叫,支持正则表达式")
|
||||
val callKeyword by value("[小筱][林淋月玥]")
|
||||
|
||||
@@ -50,8 +50,6 @@ import top.jie65535.mirai.tools.WebSearch
|
||||
import top.jie65535.mirai.util.RetryBackoff
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -129,11 +127,12 @@ internal object ConversationEngine {
|
||||
if (endpoints.isEmpty()) error("OpenAI Token 未设置,无法开始")
|
||||
var endpointIndex = 0
|
||||
var done: Boolean
|
||||
var retry = max(PluginConfig.retryMax, 3)
|
||||
val maxRounds = PluginConfig.retryMax.coerceAtLeast(2)
|
||||
var completedRounds = 0
|
||||
val retryBackoff = RetryBackoff.fromConfig()
|
||||
var consecutiveFailures = 0
|
||||
do {
|
||||
val endpoint = endpoints[min(endpointIndex, endpoints.lastIndex)]
|
||||
val endpoint = endpoints[endpointIndex]
|
||||
var streamingOk = false
|
||||
try {
|
||||
val startedAt = OffsetDateTime.now().toEpochSecond().toInt()
|
||||
@@ -197,6 +196,7 @@ internal object ConversationEngine {
|
||||
reasoningContent = if (responseToolCalls.isNotEmpty()) reasoningContent?.toString() else null,
|
||||
)
|
||||
recordUsage(event, lastTokenUsage, lastCacheUsage)
|
||||
completedRounds++
|
||||
|
||||
if (responseToolCalls.size > toolCallTasks.size) {
|
||||
val finalToolResult = responseToolCalls.last().toResultMessage(event)
|
||||
@@ -208,7 +208,9 @@ internal object ConversationEngine {
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
history += ChatMessage.User(buildContinuationPrompt(retry, startedAt, event))
|
||||
history += ChatMessage.User(
|
||||
buildContinuationPrompt(maxRounds - completedRounds, startedAt, event)
|
||||
)
|
||||
} else {
|
||||
if (PluginConfig.enableContextCache) {
|
||||
ConversationContext.saveCache(
|
||||
@@ -223,31 +225,40 @@ internal object ConversationEngine {
|
||||
}
|
||||
} catch (cause: Exception) {
|
||||
if (cause is CancellationException) throw cause
|
||||
val failureMessage = if (!streamingOk) {
|
||||
LargeLanguageModels.reportFailure(endpoint)
|
||||
if (endpointIndex < endpoints.lastIndex) {
|
||||
endpointIndex++
|
||||
"接入点[${endpoint.label}]调用失败,将切换备用接入点[${endpoints[endpointIndex].label}]"
|
||||
} else {
|
||||
"接入点[${endpoint.label}]调用失败,无更多备用接入点"
|
||||
}
|
||||
} else {
|
||||
"调用llm后处理时发生异常"
|
||||
}
|
||||
if (retry <= 1) {
|
||||
JChatGPT.logger.warning("$failureMessage,已无剩余尝试", cause)
|
||||
if (streamingOk) {
|
||||
JChatGPT.logger.warning("调用llm后处理时发生异常,不再重试模型请求", cause)
|
||||
throw cause
|
||||
}
|
||||
LargeLanguageModels.reportFailure(endpoint)
|
||||
consecutiveFailures++
|
||||
val nextEndpointIndex = nextChatEndpointIndex(
|
||||
endpointCount = endpoints.size,
|
||||
currentIndex = endpointIndex,
|
||||
failureCount = consecutiveFailures,
|
||||
)
|
||||
if (nextEndpointIndex == null) {
|
||||
JChatGPT.logger.warning(
|
||||
"接入点[${endpoint.label}]调用失败,已无剩余接入点或重试次数",
|
||||
cause,
|
||||
)
|
||||
throw cause
|
||||
}
|
||||
val nextEndpoint = endpoints[nextEndpointIndex]
|
||||
val retryDelayMillis = retryBackoff.delayMillis(consecutiveFailures)
|
||||
val retryMessage = if (nextEndpointIndex == endpointIndex) {
|
||||
"接入点[${endpoint.label}]调用失败,将重试一次"
|
||||
} else {
|
||||
"接入点[${endpoint.label}]调用失败,将切换备用接入点[${nextEndpoint.label}]"
|
||||
}
|
||||
JChatGPT.logger.warning(
|
||||
"$failureMessage,将在 ${retryDelayMillis}ms 后重试",
|
||||
"$retryMessage,将在 ${retryDelayMillis}ms 后重试",
|
||||
cause,
|
||||
)
|
||||
endpointIndex = nextEndpointIndex
|
||||
if (retryDelayMillis > 0) delay(retryDelayMillis)
|
||||
done = false
|
||||
}
|
||||
} while (!done && 0 < --retry)
|
||||
} while (!done && completedRounds < maxRounds)
|
||||
} catch (cause: CancellationException) {
|
||||
throw cause
|
||||
} catch (cause: Throwable) {
|
||||
@@ -323,9 +334,9 @@ internal object ConversationEngine {
|
||||
return truncated
|
||||
}
|
||||
|
||||
private fun buildContinuationPrompt(retry: Int, startedAt: Int, event: MessageEvent): String = buildString {
|
||||
private fun buildContinuationPrompt(remainingRounds: Int, startedAt: Int, event: MessageEvent): String = buildString {
|
||||
appendLine("## 系统提示")
|
||||
append("本次运行最多还剩").append(retry - 1).appendLine("轮。")
|
||||
append("本次运行最多还剩").append(remainingRounds).appendLine("轮。")
|
||||
appendLine("如果要多次发言,可以一次性调用多次发言工具。")
|
||||
appendLine("如果没有什么要做的,可以提前结束。")
|
||||
appendLine("当前时间:${dateTimeFormatter.format(OffsetDateTime.now())}")
|
||||
@@ -359,3 +370,14 @@ internal object ConversationEngine {
|
||||
else content.take(maxLength) + "\n\n[系统提示:因内容过长,部分内容已被省略]"
|
||||
}
|
||||
}
|
||||
|
||||
internal fun nextChatEndpointIndex(endpointCount: Int, currentIndex: Int, failureCount: Int): Int? {
|
||||
require(endpointCount > 0) { "endpointCount must be positive" }
|
||||
require(currentIndex in 0 until endpointCount) { "currentIndex must reference an endpoint" }
|
||||
require(failureCount > 0) { "failureCount must be positive" }
|
||||
return when {
|
||||
endpointCount == 1 && failureCount == 1 -> currentIndex
|
||||
currentIndex < endpointCount - 1 -> currentIndex + 1
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package top.jie65535.mirai.conversation
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ConversationRetryPolicyTest {
|
||||
@Test
|
||||
fun singleEndpointIsRetriedOnce() {
|
||||
assertEquals(0, nextChatEndpointIndex(endpointCount = 1, currentIndex = 0, failureCount = 1))
|
||||
assertEquals(null, nextChatEndpointIndex(endpointCount = 1, currentIndex = 0, failureCount = 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleEndpointsAreEachAttemptedOnce() {
|
||||
assertEquals(1, nextChatEndpointIndex(endpointCount = 3, currentIndex = 0, failureCount = 1))
|
||||
assertEquals(2, nextChatEndpointIndex(endpointCount = 3, currentIndex = 1, failureCount = 2))
|
||||
assertEquals(null, nextChatEndpointIndex(endpointCount = 3, currentIndex = 2, failureCount = 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successfulFallbackRemainsTheStartingEndpointForLaterRounds() {
|
||||
assertEquals(2, nextChatEndpointIndex(endpointCount = 3, currentIndex = 1, failureCount = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsInvalidEndpointState() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
nextChatEndpointIndex(endpointCount = 0, currentIndex = 0, failureCount = 1)
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
nextChatEndpointIndex(endpointCount = 2, currentIndex = 2, failureCount = 1)
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
nextChatEndpointIndex(endpointCount = 2, currentIndex = 0, failureCount = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user