Pruebas y resolución de problemas
Esta guía cubre estrategias de prueba para agentes Goa-AI y soluciones a problemas comunes.
Pruebas de agentes
Pruebas con el motor en memoria
El motor en memoria es ideal para pruebas porque:
- No requiere dependencias externas (sin Temporal)
- Se ejecuta de forma síncrona para un comportamiento predecible en las pruebas
- Proporciona retroalimentación rápida durante el desarrollo
func TestChatAgent(t *testing.T) {
// Create runtime with in-memory engine (default)
rt := runtime.New()
ctx := context.Background()
// Register agent with test planner
err := chat.RegisterChatAgent(ctx, rt, chat.ChatAgentConfig{
Planner: &TestPlanner{},
})
require.NoError(t, err)
_, err = rt.CreateSession(ctx, "test-session")
require.NoError(t, err)
// Run agent
client := chat.NewClient(rt)
out, err := client.Run(
ctx,
"test-session",
[]*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Hello"}},
}},
)
require.NoError(t, err)
// Assert on output
assert.NotEmpty(t, out.RunID)
assert.NotNil(t, out.Final)
}
Pruebas de planificadores con clientes de modelo simulados
Aísla la lógica del planificador simulando el cliente del modelo:
type MockModelClient struct {
responses []model.Message
callCount int
}
func (m *MockModelClient) Complete(ctx context.Context, req *model.Request) (*model.Response, error) {
if m.callCount >= len(m.responses) {
return nil, fmt.Errorf("no more mock responses")
}
resp := &model.Response{
Content: []model.Message{m.responses[m.callCount]},
}
m.callCount++
return resp, nil
}
func (m *MockModelClient) Stream(ctx context.Context, req *model.Request) (model.Streamer, error) {
// Return a mock streamer for streaming tests
return &MockStreamer{response: m.responses[m.callCount]}, nil
}
func TestPlannerWithMockClient(t *testing.T) {
mockClient := &MockModelClient{
responses: []model.Message{
{
Role: model.ConversationRoleAssistant,
Parts: []model.Part{
model.TextPart{Text: "I'll search for that."},
model.ToolUsePart{
ID: "call-1",
Name: "search",
Input: json.RawMessage(`{"query": "test"}`),
},
},
},
},
}
planner := &MyPlanner{client: mockClient}
input := &planner.PlanInput{
Messages: []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Search for test"}},
}},
}
result, err := planner.PlanStart(context.Background(), input)
require.NoError(t, err)
// Assert planner returned tool calls
assert.NotNil(t, result.ToolCalls)
assert.Len(t, result.ToolCalls, 1)
assert.Equal(t, "search", string(result.ToolCalls[0].Name))
}
Pruebas de herramientas de forma aislada
Prueba los ejecutores de herramientas de forma independiente del agente:
func TestSearchToolExecutor(t *testing.T) {
// Create executor with mock dependencies
mockSearchService := &MockSearchService{
results: []string{"doc1", "doc2", "doc3"},
}
executor := &SearchExecutor{searchService: mockSearchService}
// Create test tool call
meta := &runtime.ToolCallMeta{
RunID: "test-run",
SessionID: "test-session",
TurnID: "test-turn",
ToolCallID: "call-1",
}
call := &planner.ToolRequest{
Name: specs.Search,
Payload: json.RawMessage(`{"query": "test", "limit": 5}`),
}
// Execute tool
result, err := executor.Execute(context.Background(), meta, call)
require.NoError(t, err)
require.NotNil(t, result.ToolResult)
// Assert on result
assert.Nil(t, result.ToolResult.Error)
assert.NotNil(t, result.ToolResult.Result)
// Unmarshal and verify typed result
searchResult, ok := result.ToolResult.Result.(*specs.SearchResult)
require.True(t, ok)
assert.Len(t, searchResult.Documents, 3)
}
Pruebas de validación y sugerencias de reintento de herramientas
Verifica que las herramientas devuelven los errores y sugerencias adecuados ante una entrada no válida:
func TestToolValidationReturnsHint(t *testing.T) {
executor := &SearchExecutor{}
// Invalid payload - missing required field
call := &planner.ToolRequest{
Name: specs.Search,
Payload: json.RawMessage(`{"limit": 5}`), // missing "query"
}
result, err := executor.Execute(context.Background(), &runtime.ToolCallMeta{}, call)
require.NoError(t, err) // Executor should not return error
require.NotNil(t, result.ToolResult)
// Should return ToolError with RetryHint
assert.NotNil(t, result.ToolResult.Error)
assert.NotNil(t, result.ToolResult.RetryHint)
assert.Equal(t, planner.RetryReasonMissingFields, result.ToolResult.RetryHint.Reason)
assert.Contains(t, result.ToolResult.RetryHint.MissingFields, "query")
}
Pruebas de composición de agentes
Prueba escenarios de agente-como-herramienta:
func TestAgentComposition(t *testing.T) {
rt := runtime.New()
ctx := context.Background()
// Register provider agent
err := planner.RegisterPlannerAgent(ctx, rt, planner.PlannerAgentConfig{
Planner: &PlanningPlanner{},
})
require.NoError(t, err)
// Register consumer agent that uses provider's tools
err = orchestrator.RegisterOrchestratorAgent(ctx, rt, orchestrator.OrchestratorAgentConfig{
Planner: &OrchestratorPlanner{},
})
require.NoError(t, err)
_, err = rt.CreateSession(ctx, "test-session")
require.NoError(t, err)
// Run orchestrator - it should invoke planner agent as a tool
client := orchestrator.NewClient(rt)
out, err := client.Run(
ctx,
"test-session",
[]*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Create a plan for X"}},
}},
)
require.NoError(t, err)
// Verify child run was created
assert.Greater(t, out.ChildrenCount, 0)
}
Resolución de problemas
Errores comunes
Error “registration closed”
Síntoma:
error: registration closed: cannot register agent after runtime start
Causa: Intentar registrar un agente después de que el runtime haya comenzado a procesar ejecuciones.
Solución: Registra todos los agentes antes de iniciar cualquier ejecución:
rt := runtime.New()
// ✓ Register all agents first
chat.RegisterChatAgent(ctx, rt, chatConfig)
planner.RegisterPlannerAgent(ctx, rt, plannerConfig)
// ✓ Then create a session and start runs
client := chat.NewClient(rt)
if _, err := rt.CreateSession(ctx, "session-123"); err != nil {
panic(err)
}
out, err := client.Run(ctx, "session-123", messages, opts...)
Error “missing session ID”
Síntoma:
error: missing session ID: session ID is required for run
Causa: Iniciar una ejecución sin proporcionar un ID de sesión.
Solución: Proporciona siempre un ID de sesión como argumento posicional requerido:
// ✗ Wrong - no session ID
out, err := client.Run(ctx, "", messages)
// ✓ Correct - session ID provided
if _, err := rt.CreateSession(ctx, "session-123"); err != nil {
panic(err)
}
out, err := client.Run(ctx, "session-123", messages)
Consejo: Para pruebas, utiliza un ID de sesión fijo. En producción, genera IDs de sesión únicos por conversación.
Errores de violación de políticas
Síntoma:
error: policy violation: max tool calls exceeded (10/10)
Causa: El agente superó el límite MaxToolCalls configurado para las herramientas con presupuesto. Las herramientas declaradas Bookkeeping() no consumen este límite.
Soluciones:
- Aumenta el límite si el caso de uso requiere legítimamente más llamadas a herramientas:
RunPolicy(func() {
DefaultCaps(MaxToolCalls(20)) // Increase from default
})
Mejora la eficiencia del planificador para usar menos llamadas a herramientas:
- Agrupa operaciones por lotes cuando sea posible
- Utiliza llamadas a herramientas más específicas
- Mejora la ingeniería de prompts
Comprueba si hay bucles infinitos en la lógica del planificador que llamen repetidamente a la misma herramienta.
Exime del presupuesto las herramientas estructuradas de bookkeeping declarándolas
Bookkeeping()en el DSL. Las actualizaciones de estado, marcadores de progreso y herramientas de commit terminal suelen pertenecer a esta categoría; una vez exentas, nunca consumenRemainingToolCallsy siempre pueden ejecutarse. CombinaBookkeeping()conTerminalRun()para crear una herramienta de tipo “commit this run” con garantía de finalización incluso después de agotar el presupuesto de recuperación.
Síntoma:
error: bookkeeping-only tool batch requires a terminal tool or terminal planner payload
Causa: El planificador emitió únicamente herramientas de bookkeeping, pero ninguno de esos resultados era apto para impulsar otro turno del planificador. Por defecto, los resultados satisfactorios de bookkeeping permanecen ocultos a futuros turnos PlanResume, por lo que el mismo turno debe resolverse de forma terminal / quedar a la espera de entrada, o producir un resultado de bookkeeping visible para el planificador.
Soluciones:
- Termina en el mismo turno con
TerminalRun(),FinalResponseoFinalToolResultcuando el lote de bookkeeping ya sea terminal. - Pausa explícitamente con un handshake de espera/pausa si la ejecución está aguardando entrada humana o externa.
- Marca el resultado de bookkeeping como
PlannerVisible()cuando contenga un estado canónico sobre el que el siguiente turno del planificador deba razonar, por ejemplo, una instantánea estructurada de progreso. - No combines
PlannerVisible()conTerminalRun(). UsaTerminalRun()para una finalización atómica yPlannerVisible()para bookkeeping no terminal que deba reanudar la planificación.
Síntoma:
error: policy violation: max consecutive failed tool calls exceeded (3/3)
Causa: Fallaron múltiples llamadas consecutivas a herramientas.
Soluciones:
- Corrige los errores subyacentes de la herramienta - revisa los logs del ejecutor de la herramienta
- Mejora las sugerencias de reintento para que el planificador pueda autocorregirse
- Aumenta el límite si se esperan fallos transitorios:
RunPolicy(func() {
DefaultCaps(MaxConsecutiveFailedToolCalls(5))
})
Síntoma:
error: policy violation: time budget exceeded (2m0s)
Causa: La ejecución del agente superó el TimeBudget configurado.
Soluciones:
- Aumenta el presupuesto para operaciones de larga duración:
RunPolicy(func() {
TimeBudget("10m")
})
- Usa
Timingpara un control más preciso:
RunPolicy(func() {
Timing(func() {
Budget("10m") // Overall budget
Plan("1m") // Per-plan timeout
Tools("2m") // Per-tool timeout
})
})
- Optimiza la ejecución de herramientas para que finalicen más rápido.
Error “unknown tool”
Síntoma:
error: unknown tool: orchestrator.helpers.search
Causa: El planificador solicitó una herramienta que no está registrada.
Soluciones:
- Verifica el registro del toolset - asegúrate de que el toolset esté registrado en el agente:
Agent("chat", "Chat agent", func() {
Use(HelpersToolset) // Make sure this is included
})
Comprueba la ortografía del nombre de la herramienta - los nombres de herramientas distinguen mayúsculas/minúsculas y utilizan nombres cualificados.
Regenera el código tras cambios en el DSL:
goa gen example.com/project/design
Error “invalid payload”
Síntoma:
error: invalid payload: json: cannot unmarshal string into Go struct field SearchPayload.limit of type int
Causa: El LLM proporcionó una carga útil que no coincide con el esquema de la herramienta.
Soluciones:
- Devuelve un RetryHint desde el ejecutor para que el planificador pueda autocorregirse:
if err != nil {
return runtime.Executed(&planner.ToolResult{
Name: call.Name,
Error: planner.NewToolError("invalid payload"),
RetryHint: &planner.RetryHint{
Reason: planner.RetryReasonInvalidArguments,
Tool: call.Name,
ExampleInput: map[string]any{"query": "example", "limit": 10},
Message: "limit must be an integer",
},
}), nil
}
Mejora las descripciones de las herramientas para aclarar los tipos esperados.
Añade ejemplos al DSL:
Args(func() {
Attribute("limit", Int, "Maximum results", func() {
Example(10)
Minimum(1)
Maximum(100)
})
})
Consejos de depuración
Habilitar el registro de depuración
import "goa.design/goa-ai/runtime/agent/runtime"
rt := runtime.New(
runtime.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))),
)
Suscribirse a eventos para depurar
type DebugSink struct{}
func (s *DebugSink) Send(ctx context.Context, event stream.Event) error {
fmt.Printf("[%s] %s run=%s session=%s payload=%v\n",
time.Now().Format(time.RFC3339),
event.Type(),
event.RunID(),
event.SessionID(),
event.Payload(),
)
return nil
}
func (s *DebugSink) Close(ctx context.Context) error { return nil }
// Wire the sink into the runtime to observe all stream events.
rt := runtime.New(runtime.WithStream(&DebugSink{}))
Inspeccionar especificaciones de herramientas en tiempo de ejecución
// List all registered tools
for _, spec := range rt.ToolSpecsForAgent(chat.AgentID) {
fmt.Printf("Tool: %s\n", spec.Name)
fmt.Printf(" Description: %s\n", spec.Description)
fmt.Printf(" Payload Schema: %s\n", spec.Payload.Schema)
}
Próximos pasos
- Referencia del DSL - Referencia completa de las funciones del DSL
- Runtime - Comprende la arquitectura del runtime
- Producción - Despliega con Temporal y UI en streaming