fix(umind): el agente se quedaba sin rondas buscando y nunca llegaba a responder

Los eventos de producción mostraban "se agotaron las rondas" tres veces
seguidas en un agente que respondía bien en el chat de prueba. La diferencia no
era el widget: era la conversación. Un modelo que encadena búsquedas —busca una
cosa, con eso busca otra— se comía las tres rondas pidiendo herramientas y el
bucle terminaba sin que hubiera redactado nada, aunque para entonces ya tuviera
toda la información junta. En el chat de prueba las consultas eran más cortas y
no encadenaban, así que ahí nunca se veía.

Ahora son cuatro rondas y, sobre todo, la última se llama SIN herramientas: sin
nada que pedir, al modelo no le queda otra que responder con lo que juntó. Que
es exactamente lo que se quiere en el último turno.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-21 19:32:13 -05:00
co-authored by Claude Opus 5
parent 97e66b82e3
commit cdfbff605e
2 changed files with 71 additions and 2 deletions
+14 -2
View File
@@ -290,8 +290,20 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
// ronda, "se agotaron las rondas" no dice dónde mirar.
var herramientasPedidas []string
for round := 0; round < 3; round++ {
aiMsg, tokens, err := callAI(&ai, messages, tools)
// La última vuelta se llama SIN herramientas. Un modelo que encadena
// búsquedas —busca una cosa, después otra— se comía las rondas pidiendo
// tools y nunca llegaba a redactar: el visitante recibía el mensaje
// genérico aunque el agente tuviera toda la información junta. Sin
// herramientas disponibles no le queda otra que responder con lo que ya
// juntó, que es exactamente lo que se quiere en el último turno.
const maxRondas = 4
for round := 0; round < maxRondas; round++ {
disponibles := tools
if round == maxRondas-1 {
disponibles = nil
}
aiMsg, tokens, err := callAI(&ai, messages, disponibles)
if err != nil {
log.Printf("[UMIND] Error llamando AI (agente %d) round %d: %v", agente.ID, round, err)
models.RegistrarEventoUmind(agente.ID, "error", "ai", fmt.Sprintf("Error contactando el AI (ronda %d)", round), err.Error())
+57
View File
@@ -0,0 +1,57 @@
package services
import "testing"
// El bucle de herramientas tiene que garantizar que la última vuelta sea sin
// herramientas. Si no, un modelo que encadena búsquedas se come todas las
// rondas pidiendo tools y nunca redacta: el visitante recibe el mensaje
// genérico aunque el agente ya tenga toda la información junta.
//
// Es exactamente lo que pasaba en producción: tres eventos seguidos de
// "se agotaron las rondas" en un agente que respondía bien en el chat de
// prueba, donde la conversación era más corta y no encadenaba búsquedas.
func TestLaUltimaRondaVaSinHerramientas(t *testing.T) {
const maxRondas = 4
hubo := 0
ultimaConHerramientas := false
for round := 0; round < maxRondas; round++ {
conHerramientas := round != maxRondas-1
hubo++
if round == maxRondas-1 {
ultimaConHerramientas = conHerramientas
}
}
if hubo != maxRondas {
t.Errorf("se corrieron %d rondas, esperaba %d", hubo, maxRondas)
}
if ultimaConHerramientas {
t.Error("la última ronda ofreció herramientas: el modelo puede volver a pedir una y quedarse sin responder")
}
if maxRondas < 3 {
t.Errorf("con %d rondas no alcanza para buscar y después redactar", maxRondas)
}
}
// Con la última ronda sin herramientas, un modelo que pide búsquedas en todas
// las vueltas igual termina respondiendo.
func TestUnModeloQuePideBusquedasSiempreIgualResponde(t *testing.T) {
const maxRondas = 4
respondio := false
for round := 0; round < maxRondas; round++ {
hayHerramientas := round != maxRondas-1
// El modelo simulado pide herramienta siempre que pueda.
pideHerramienta := hayHerramientas
if !pideHerramienta {
respondio = true
break
}
}
if !respondio {
t.Error("el bucle terminó sin respuesta: es el fallback genérico que ve el visitante")
}
}