feat: add repetition detection fallback to orchestrator pipeline

Se añade un mecanismo de detección de repetición para cortar conversaciones
circulares entre agentes cuando hablan sin parar.

- Nuevo campo RepetitionThreshold en OrchestrationCfg (schema.go).
- Función detectRepetition() compara cada nueva respuesta con las anteriores
  usando similitud de bigramas (Dice coefficient).
- Si la similitud supera el umbral (default 0.6), el pipeline se detiene
  inmediatamente con un log de warning, antes de gastar una llamada LLM
  en la evaluación de calidad.
- Funciones auxiliares: similarity() y makeBigrams() para el cálculo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:11:00 +00:00
parent 485d6e86be
commit 55eff3389a
2 changed files with 91 additions and 4 deletions
+5 -4
View File
@@ -418,10 +418,11 @@ type SpecialMeta struct {
// OrchestrationCfg configures the multi-bot orchestrator.
type OrchestrationCfg struct {
MaxIterations int `yaml:"max_iterations"`
QualityThreshold float64 `yaml:"quality_threshold"`
DelegationTimeout time.Duration `yaml:"delegation_timeout"`
Rooms []OrchestratedRoomCfg `yaml:"rooms"`
MaxIterations int `yaml:"max_iterations"`
QualityThreshold float64 `yaml:"quality_threshold"`
DelegationTimeout time.Duration `yaml:"delegation_timeout"`
RepetitionThreshold float64 `yaml:"repetition_threshold"` // 0-1: similarity ratio to detect circular conversations
Rooms []OrchestratedRoomCfg `yaml:"rooms"`
}
// OrchestratedRoomCfg defines a room managed by the orchestrator.
+86
View File
@@ -331,6 +331,15 @@ func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext
"iteration", i,
)
// Fallback: detect circular conversations before quality evaluation
if o.detectRepetition(responses) {
o.logger.Warn("repetition detected, stopping pipeline to prevent circular conversation",
"iteration", i+1,
"total_responses", len(responses),
)
break
}
// Evaluate quality (Fase 3)
score := o.evaluate(ctx, msgCtx.Content, response)
o.logger.Info("quality evaluated",
@@ -462,6 +471,83 @@ func (o *Orchestrator) buildParticipantsList(botIDs []string, exclude string) st
return sb.String()
}
// detectRepetition checks if a new response is too similar to previous responses,
// indicating a circular conversation that should be stopped.
// Returns true if the conversation should be terminated.
func (o *Orchestrator) detectRepetition(responses []orchestration.BotResponse) bool {
if len(responses) < 2 {
return false
}
threshold := o.cfg.Orchestration.RepetitionThreshold
if threshold <= 0 {
threshold = 0.6 // default
}
latest := responses[len(responses)-1].Text
for i := 0; i < len(responses)-1; i++ {
if similarity(latest, responses[i].Text) >= threshold {
return true
}
}
return false
}
// similarity computes a simple bigram-based similarity ratio between two strings.
// Returns a value between 0.0 (completely different) and 1.0 (identical).
func similarity(a, b string) float64 {
if a == b {
return 1.0
}
a = strings.ToLower(strings.TrimSpace(a))
b = strings.ToLower(strings.TrimSpace(b))
if a == b {
return 1.0
}
if len(a) < 2 || len(b) < 2 {
return 0.0
}
bigramsA := makeBigrams(a)
bigramsB := makeBigrams(b)
// Count intersection
intersection := 0
for bg, countA := range bigramsA {
if countB, ok := bigramsB[bg]; ok {
if countA < countB {
intersection += countA
} else {
intersection += countB
}
}
}
totalA := 0
for _, c := range bigramsA {
totalA += c
}
totalB := 0
for _, c := range bigramsB {
totalB += c
}
if totalA+totalB == 0 {
return 0.0
}
return float64(2*intersection) / float64(totalA+totalB)
}
func makeBigrams(s string) map[string]int {
runes := []rune(s)
bgs := make(map[string]int, len(runes))
for i := 0; i < len(runes)-1; i++ {
bg := string(runes[i : i+2])
bgs[bg]++
}
return bgs
}
func truncate(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {