profile: rebase concurrent conversation updates

This commit is contained in:
2026-08-03 17:58:58 +08:00
parent 94f303ec72
commit 74bcf0b8d6
5 changed files with 198 additions and 83 deletions
@@ -1,6 +1,8 @@
package top.jie65535.mirai.profile
object ConversationProfileReducer {
private const val MISSING_ITEM_REFERENCE = "P2147483647"
fun reduce(
profiles: Map<Long, UserProfileSnapshot>,
batch: ConversationProfileBatch,
@@ -33,4 +35,55 @@ object ConversationProfileReducer {
)
}
}
fun reduceRebased(
expectedProfiles: Map<Long, UserProfileSnapshot>,
latestProfiles: Map<Long, UserProfileSnapshot>,
batch: ConversationProfileBatch,
eligibleUserIds: Set<Long>,
response: ConversationProfileModelResponse,
model: String,
promptVersion: String,
summaryMaxLength: Int,
): List<ProfileReduction> {
val rebasedResponse = response.copy(
users = response.users.map { userResponse ->
val userId = batch.aliasToUserId[userResponse.userAlias]
val expected = userId?.let(expectedProfiles::get)
val latest = userId?.let(latestProfiles::get)
if (userId == null || userId !in eligibleUserIds || expected == null || latest == null) {
userResponse
} else {
userResponse.copy(
operations = userResponse.operations.map { operation ->
operation.rebaseItemReference(expected, latest)
}
)
}
}
)
return reduce(
profiles = latestProfiles,
batch = batch,
eligibleUserIds = eligibleUserIds,
response = rebasedResponse,
model = model,
promptVersion = promptVersion,
summaryMaxLength = summaryMaxLength,
)
}
private fun ProfileModelOperation.rebaseItemReference(
expected: UserProfileSnapshot,
latest: UserProfileSnapshot,
): ProfileModelOperation {
if (action == ProfileOperationAction.ADD) return this
val expectedItem = ProfileItemReferences.resolve(expected, itemRef)
?: return copy(itemRef = MISSING_ITEM_REFERENCE)
val latestReference = ProfileItemReferences.entries(latest)
.firstOrNull { entry -> entry.item.id == expectedItem.id }
?.reference
?: MISSING_ITEM_REFERENCE
return copy(itemRef = latestReference)
}
}
@@ -14,8 +14,6 @@ import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
object UserProfileAnalysisService {
private const val MAX_CONVERSATION_CONFLICT_RETRIES = 3
private val runningUsers = ConcurrentHashMap.newKeySet<Long>()
private val runningGroups = ConcurrentHashMap.newKeySet<Long>()
private val runningCompactions = ConcurrentHashMap.newKeySet<Long>()
@@ -433,75 +431,70 @@ object UserProfileAnalysisService {
.filterValues { it >= minAuthoredTextChars.coerceAtLeast(1) }
.keys
if (eligibleUserIds.isEmpty()) return null
var profiles = userLocks.withUserLocks(eligibleUserIds) {
val profiles = userLocks.withUserLocks(eligibleUserIds) {
withContext(Dispatchers.IO) {
if (UserProfileStore.isConversationProcessed(batch.inputHash)) null
else loadConversationProfiles(eligibleUserIds)
}
} ?: return null
var conflictRetries = 0
var totalUsage = ProfileTokenUsage()
while (true) {
val (result, reductions) = analyzeConversationWithRetry(
model = model,
profiles = profiles,
batch = batch,
eligibleUserIds = eligibleUserIds,
retryMax = retryMax,
summaryMaxLength = summaryMaxLength,
onRetryFailure = onRetryFailure,
)
totalUsage += result.usage
val commitOutcome = userLocks.withUserLocks(eligibleUserIds) {
withContext(Dispatchers.IO) {
if (UserProfileStore.isConversationProcessed(batch.inputHash)) {
ConversationCommitOutcome.AlreadyProcessed
val (result, reductions) = analyzeConversationWithRetry(
model = model,
profiles = profiles,
batch = batch,
eligibleUserIds = eligibleUserIds,
retryMax = retryMax,
summaryMaxLength = summaryMaxLength,
onRetryFailure = onRetryFailure,
)
val totalUsage = result.usage
val commitOutcome = userLocks.withUserLocks(eligibleUserIds) {
withContext(Dispatchers.IO) {
if (UserProfileStore.isConversationProcessed(batch.inputHash)) {
ConversationCommitOutcome.AlreadyProcessed
} else {
val latestProfiles = loadConversationProfiles(eligibleUserIds)
val committedReductions = if (hasProfileVersionConflict(profiles, latestProfiles)) {
ConversationProfileReducer.reduceRebased(
expectedProfiles = profiles,
latestProfiles = latestProfiles,
batch = batch,
eligibleUserIds = eligibleUserIds,
response = result.response,
model = model.modelName,
promptVersion = ProfilePromptStore.PROMPT_VERSION,
summaryMaxLength = summaryMaxLength.coerceAtLeast(100),
)
} else {
val latestProfiles = loadConversationProfiles(eligibleUserIds)
if (hasProfileVersionConflict(profiles, latestProfiles)) {
ConversationCommitOutcome.Conflict(latestProfiles)
} else {
UserProfileStore.commitConversation(
reductions = reductions.map { reduction ->
reduction to batch.forUser(reduction.profile.userId)
},
usage = totalUsage,
)
ConversationCommitOutcome.Committed
}
reductions
}
}
}
when (commitOutcome) {
ConversationCommitOutcome.AlreadyProcessed -> return null
ConversationCommitOutcome.Committed -> {
onCommittedOperations(
"source=CONVERSATION bot=${batch.botId} group=${batch.groupId} " +
"batch=[${batch.startTime},${batch.endTime})",
reductions,
)
return ConversationProfileAnalysisReport(
analyzedUsers = eligibleUserIds.size,
processedMessages = batch.messages.size,
appliedOperations = reductions.sumOf { it.operations.size },
skippedOperations = reductions.sumOf { it.skippedOperations.size },
UserProfileStore.commitConversation(
reductions = committedReductions.map { reduction ->
reduction to batch.forUser(reduction.profile.userId)
},
usage = totalUsage,
)
}
is ConversationCommitOutcome.Conflict -> {
conflictRetries++
if (conflictRetries > MAX_CONVERSATION_CONFLICT_RETRIES) {
throw IllegalStateException(
"${batch.groupId} 会话画像提交连续冲突 $conflictRetries 次,未提交结果"
)
}
profiles = commitOutcome.latestProfiles
ConversationCommitOutcome.Committed(committedReductions)
}
}
}
val committedReductions = when (commitOutcome) {
ConversationCommitOutcome.AlreadyProcessed -> return null
is ConversationCommitOutcome.Committed -> commitOutcome.reductions
}
onCommittedOperations(
"source=CONVERSATION bot=${batch.botId} group=${batch.groupId} " +
"batch=[${batch.startTime},${batch.endTime})",
committedReductions,
)
return ConversationProfileAnalysisReport(
analyzedUsers = eligibleUserIds.size,
processedMessages = batch.messages.size,
appliedOperations = committedReductions.sumOf { it.operations.size },
skippedOperations = committedReductions.sumOf { it.skippedOperations.size },
usage = totalUsage,
)
}
private fun loadConversationProfiles(userIds: Set<Long>): Map<Long, UserProfileSnapshot> =
@@ -755,11 +748,10 @@ object UserProfileAnalysisService {
}
private sealed class ConversationCommitOutcome {
object Committed : ConversationCommitOutcome()
object AlreadyProcessed : ConversationCommitOutcome()
data class Conflict(
val latestProfiles: Map<Long, UserProfileSnapshot>,
data class Committed(
val reductions: List<ProfileReduction>,
) : ConversationCommitOutcome()
object AlreadyProcessed : ConversationCommitOutcome()
}
}