Feedback Loop em Orquestradores de Agentes: Aprendizado sem Machine Learning
Toda vez que o orquestrador seleciona um agente e o resultado é bom, essa informação se perde. Na próxima requisição similar, ele toma a mesma decisão do zero, sem considerar que já acertou antes. E quando erra, erra de novo pelo mesmo motivo. O sistema não aprende. A solução é registrar cada decisão com seu resultado (sucesso ou falha, rating do usuário), acumular padrões ao longo do tempo, e usar esse histórico para melhorar decisões futuras. Não é machine learning pesado. É um sistema de feedback simples que calcula confiança por agente e sugere ações baseado em padrões temporais.
Guia de tópicos:
- O Problema: Decisões sem Memória
- Estrutura do Feedback
- Métricas por Agente: Confiança Calculada
- Padrões Temporais: Quando o Sistema Funciona Melhor
- Sugestões Proativas Baseadas em Histórico
- Maturidade do Sistema: De Nascente a Expert
- Insights Agregados
- Considerações Finais
- Links Indicativos
O Problema: Decisões sem Memória
O orquestrador toma dezenas de decisões por sessão: qual agente usar, se precisa coordenar múltiplos agentes, qual workflow executar. Cada decisão pode ser boa ou ruim. Se o agente de "auth" foi selecionado para uma requisição sobre "validação de token" e o resultado foi bom, isso é informação valiosa. Na próxima vez que alguém pedir algo sobre tokens, o sistema deveria ter mais confiança em delegar para "auth".
Sem feedback loop, o sistema é stateless. Cada requisição é processada como se fosse a primeira. A seleção de agente depende apenas do input atual, sem considerar histórico. Isso significa que erros se repetem e acertos não são reforçados.
O que eu queria era simples: o sistema ficar melhor com o uso. Não precisa de treinamento, não precisa de fine-tuning, não precisa de GPU. Só precisa de um registro de "essa decisão funcionou" ou "essa decisão não funcionou" e um cálculo que use essa informação.
Estrutura do Feedback
Cada decisão gera um registro de feedback com contexto suficiente para análise posterior:
type DecisionFeedback struct {
DecisionID string `json:"decision_id"`
Success bool `json:"success"`
Timestamp time.Time `json:"timestamp"`
Context string `json:"context"`
AgentUsed string `json:"agent_used"`
UserRating int `json:"user_rating"` // 1-5
}
O DecisionID é o identificador único da decisão (geralmente o span ID do sistema de observabilidade). Success é binário: o workflow completou sem erro ou não. Context é o input original do usuário. AgentUsed é qual agente foi selecionado. UserRating é opcional e vai de 1 a 5.
O rating é o sinal mais forte mas também o mais raro. A maioria dos usuários não vai dar rating explícito para cada interação. Por isso o Success — que é automático: se não deu erro, é sucesso — serve como proxy. Não é perfeito (um resultado sem erro pode ser ruim), mas é melhor que nada e não exige ação do usuário.
Métricas por Agente: Confiança Calculada
Cada agente acumula métricas de performance ao longo do tempo:
type AgentMetrics struct {
TotalUses int
SuccessCount int
AvgRating float64
LastUsed time.Time
Confidence float64
}
func (al *AdvancedLearning) updateAgentMetrics(agent string, success bool, rating int) {
metrics := al.agentPerformance[agent]
metrics.TotalUses++
metrics.LastUsed = time.Now()
if success {
metrics.SuccessCount++
}
// Média móvel do rating
oldAvg := metrics.AvgRating
metrics.AvgRating = (oldAvg*float64(metrics.TotalUses-1) + float64(rating)) / float64(metrics.TotalUses)
// Confiança = success rate * usage weight
successRate := float64(metrics.SuccessCount) / float64(metrics.TotalUses)
usageWeight := math.Min(float64(metrics.TotalUses)/10.0, 1.0)
metrics.Confidence = successRate * usageWeight
}
A confiança é o produto de dois fatores: taxa de sucesso e peso de uso. A taxa de sucesso é direta: se o agente acertou 9 de 10 vezes, tem 90% de success rate. O peso de uso evita que um agente com 1 uso e 100% de sucesso tenha confiança máxima. Ele precisa de pelo menos 10 usos para que o peso chegue a 1.0. Com 5 usos e 100% de sucesso, a confiança é 0.5 (100% × 5/10). Com 10 usos e 90% de sucesso, a confiança é 0.9 (90% × 10/10).
Isso resolve o problema de cold start: agentes novos começam com confiança baixa independente dos primeiros resultados. Conforme acumulam histórico, a confiança converge para o valor real de performance.
Padrões Temporais: Quando o Sistema Funciona Melhor
Uma observação interessante é que a performance do sistema varia com o horário e o dia da semana. Isso acontece porque serviços externos de LLM têm padrões de carga. De madrugada, menos gente usando, respostas mais rápidas e precisas. Em horário comercial, mais carga, mais timeouts.
O sistema detecta esses padrões automaticamente:
type TemporalPattern struct {
Hour int
DayOfWeek int
Frequency int
SuccessRate float64
}
func (al *AdvancedLearning) analyzeTemporalPatterns() {
patternMap := make(map[string]*TemporalPattern)
for _, fb := range al.feedback {
hour := fb.Timestamp.Hour()
dayOfWeek := int(fb.Timestamp.Weekday())
key := fmt.Sprintf("%d_%d", hour, dayOfWeek)
if patternMap[key] == nil {
patternMap[key] = &TemporalPattern{
Hour: hour,
DayOfWeek: dayOfWeek,
}
}
pattern := patternMap[key]
pattern.Frequency++
if fb.Success {
pattern.SuccessRate = (pattern.SuccessRate*float64(pattern.Frequency-1) + 1.0) / float64(pattern.Frequency)
} else {
pattern.SuccessRate = (pattern.SuccessRate * float64(pattern.Frequency-1)) / float64(pattern.Frequency)
}
}
}
O sistema agrupa feedback por hora e dia da semana, e calcula a taxa de sucesso para cada slot temporal. Depois de algumas semanas de uso, ele sabe que segunda-feira às 14h tem 95% de sucesso mas sexta-feira às 18h tem 70%. Essa informação pode ser usada para ajustar timeouts (mais generoso em horários de pico) ou para avisar o usuário que respostas podem demorar mais em determinados períodos.
Sugestões Proativas Baseadas em Histórico
Com padrões temporais e métricas de agente, o sistema pode fazer sugestões antes do usuário pedir:
func (al *AdvancedLearning) GetProactiveSuggestions(context string) []string {
now := time.Now()
currentHour := now.Hour()
currentDay := int(now.Weekday())
suggestions := make([]string, 0)
// Sugestões baseadas em padrões temporais
for _, pattern := range al.patterns {
if pattern.Hour == currentHour && pattern.DayOfWeek == currentDay && pattern.SuccessRate > 0.8 {
suggestions = append(suggestions, fmt.Sprintf(
"Based on patterns, this is a good time for %s operations", context))
}
}
// Sugestões baseadas em performance de agentes
scores := []agentScore{}
for agent, metrics := range al.agentPerformance {
score := metrics.Confidence * metrics.AvgRating / 5.0
scores = append(scores, agentScore{agent, score})
}
sort.Slice(scores, func(i, j int) bool {
return scores[i].score > scores[j].score
})
if len(scores) > 0 {
suggestions = append(suggestions, fmt.Sprintf(
"Consider using agent '%s' (confidence: %.2f)", scores[0].name, scores[0].score))
}
return suggestions
}
As sugestões aparecem antes do processamento começar. Se o sistema sabe que o agente "auth" tem confiança 0.95 para o tipo de requisição atual, ele sugere. Se sabe que o horário atual tem alta taxa de sucesso, ele informa. Não é intrusivo: são dicas opcionais que o usuário pode ignorar.
Maturidade do Sistema: De Nascente a Expert
O sistema classifica sua própria maturidade baseado na quantidade de feedback acumulado:
func (al *AdvancedLearning) calculateMaturity() string {
if len(al.feedback) < 10 {
return "nascent"
} else if len(al.feedback) < 50 {
return "developing"
} else if len(al.feedback) < 200 {
return "mature"
}
return "expert"
}
Isso é útil para calibrar expectativas. Um sistema "nascent" com 5 feedbacks não tem dados suficientes para sugestões confiáveis. Um sistema "expert" com 200+ feedbacks tem padrões estatisticamente significativos. A maturidade pode ser exibida num painel de insights para que o usuário saiba o quanto pode confiar nas sugestões.
Na prática, um usuário ativo atinge "developing" em uma semana e "mature" em um mês. "Expert" leva alguns meses de uso regular. Os thresholds são conservadores de propósito: é melhor o sistema dizer "ainda estou aprendendo" do que dar sugestões ruins com pouco dado.
Insights Agregados
O comando insights combina todas as métricas em um resumo:
func (al *AdvancedLearning) GetInsights() map[string]interface{} {
totalFeedback := len(al.feedback)
successCount := 0
totalRating := 0.0
for _, fb := range al.feedback {
if fb.Success {
successCount++
}
totalRating += float64(fb.UserRating)
}
return map[string]interface{}{
"total_decisions": totalFeedback,
"success_rate": float64(successCount) / float64(totalFeedback),
"average_rating": totalRating / float64(totalFeedback),
"agent_performance": al.agentPerformance,
"temporal_patterns": len(al.patterns),
"learning_maturity": al.calculateMaturity(),
}
}
O output para o usuário fica algo como: "21 decisões totais, 95.2% de sucesso, rating médio 4.2/5, maturidade: developing, 8 padrões temporais identificados". É um snapshot rápido de como o sistema está performando e evoluindo.
Considerações Finais
O sistema de aprendizado descrito aqui é deliberadamente simples. Não tem rede neural, não tem gradient descent, não tem nada que precise de GPU. É aritmética básica: contadores, médias e proporções. E funciona. A confiança por agente melhora a seleção. Os padrões temporais ajustam expectativas. As sugestões proativas economizam tempo.
O que importa levar deste artigo: "aprendizado" em software não precisa ser machine learning. Um sistema que registra decisões, calcula métricas simples, e usa essas métricas para informar decisões futuras já é um sistema que aprende. A barreira de entrada é zero: um slice de structs e algumas divisões.
A limitação principal é que tudo vive em memória. Se o processo reinicia, o aprendizado se perde. A evolução natural é persistir o feedback em um arquivo JSON e carregar no startup. São 10 linhas de código a mais e o sistema mantém seu conhecimento entre sessões.
Links indicativos: