Tests et dépannage
Ce guide couvre les stratégies de test pour les agents Goa-AI et les solutions aux problèmes courants.
Agents de test
Tests avec le moteur en mémoire
Le moteur en mémoire est idéal pour les tests car il :
- Ne nécessite aucune dépendance externe (pas de Temporal)
- S’exécute de manière synchrone pour un comportement de test prévisible
- Fournit un retour rapide pendant le développement
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)
}
Planificateurs de tests avec des clients modèles simulés
Isolez la logique du planificateur en vous moquant du client modèle :
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))
}
Outils de test isolés
Testez les exécuteurs de l’outil indépendamment de l’agent :
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)
}
Conseils de validation et de nouvelle tentative de l’outil de test
Vérifiez que les outils renvoient des erreurs et des conseils appropriés en cas de saisie non valide :
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")
}
Composition de l’agent de test
Scénarios de test d’agent en tant qu’outil :
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)
}
Dépannage
Erreurs courantes
Erreur “inscription fermée”
Symptôme:
error: registration closed: cannot register agent after runtime start
Cause : Tentative d’enregistrement d’un agent après que le runtime a commencé à traiter les exécutions.
Solution : Enregistrez tous les agents avant de démarrer une exécution :
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...)
Erreur “ID de session manquant”
Symptôme:
error: missing session ID: session ID is required for run
Cause : Démarrage d’une exécution sans fournir d’ID de session.
Solution : Fournissez toujours un ID de session comme argument de position requis :
// ✗ 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)
Conseil : Pour les tests, utilisez un ID de session fixe. Pour la production, générez des identifiants de session uniques par conversation.
Erreurs de violation des règles
Symptôme:
error: policy violation: max tool calls exceeded (10/10)
Cause : L’agent a dépassé la limite MaxToolCalls configurée pour les outils budgétisés. Les outils déclarés Bookkeeping() ne comptent pas dans ce plafond.
Solutions :
- Augmentez la limite si le cas d’utilisation nécessite légitimement davantage d’appels d’outils :
RunPolicy(func() {
DefaultCaps(MaxToolCalls(20)) // Increase from default
})
Améliorez l’efficacité du planificateur pour utiliser moins d’appels d’outils :
- Opérations par lots lorsque cela est possible
- Utiliser des appels d’outils plus spécifiques
- Améliorer l’ingénierie rapide
Vérifiez les boucles infinies dans la logique du planificateur qui appelle à plusieurs reprises le même outil.
Exempter du budget les outils de comptabilité structurée en les déclarant
Bookkeeping()dans le DSL. Les mises à jour de statut, les marqueurs de progression et les outils de validation de terminal appartiennent généralement à cette catégorie ; une fois exonérés, ils ne consomment jamaisRemainingToolCallset peuvent toujours s’exécuter. AssociezBookkeeping()àTerminalRun()pour obtenir un outil de « validation de cette exécution » dont la finalisation est garantie même une fois le budget de récupération épuisé.
Symptôme:
error: bookkeeping-only tool batch requires a terminal tool or terminal planner payload
Cause : Le planificateur n’a émis que des outils de comptabilité, mais aucun de ces résultats n’était éligible pour déclencher un autre tour de planificateur. Par défaut, les résultats de comptabilité réussis restent cachés des futurs tours PlanResume, donc le même tour doit soit être résolu de manière terminale/attendre une entrée, soit produire un résultat de comptabilité visible par le planificateur.
Solutions :
- Terminez dans le même tour avec
TerminalRun(),FinalResponseouFinalToolResultlorsque le lot de comptabilité est déjà terminal. - Pause explicitement avec une poignée de main d’attente/pause si l’exécution attend une entrée humaine ou externe.
- Marquez le résultat de la comptabilité
PlannerVisible()lorsqu’il contient un état canonique sur lequel le prochain tour du planificateur doit raisonner, comme un instantané de progression structuré. - Ne combinez pas
PlannerVisible()avecTerminalRun(). UtilisezTerminalRun()pour l’achèvement atomique etPlannerVisible()pour la comptabilité non terminale qui devrait reprendre la planification.
Symptôme:
error: policy violation: max consecutive failed tool calls exceeded (3/3)
Cause : Plusieurs appels d’outils consécutifs ont échoué.
Solutions :
- Corrigez les erreurs sous-jacentes de l’outil - vérifiez les journaux de l’exécuteur de l’outil
- Améliorez les conseils de nouvelle tentative afin que le planificateur puisse s’auto-corriger
- Augmentez la limite si des pannes transitoires sont attendues :
RunPolicy(func() {
DefaultCaps(MaxConsecutiveFailedToolCalls(5))
})
Symptôme:
error: policy violation: time budget exceeded (2m0s)
Cause : L’exécution de l’agent a dépassé le TimeBudget configuré.
Solutions :
- Augmenter le budget pour les opérations de longue durée :
RunPolicy(func() {
TimeBudget("10m")
})
- Utilisez
Timingpour un contrôle précis :
RunPolicy(func() {
Timing(func() {
Budget("10m") // Overall budget
Plan("1m") // Per-plan timeout
Tools("2m") // Per-tool timeout
})
})
- Optimisez l’exécution des outils pour terminer plus rapidement.
Erreur “outil inconnu”
Symptôme:
error: unknown tool: orchestrator.helpers.search
Cause : Le planificateur a demandé un outil qui n’est pas enregistré.
Solutions :
- Vérifiez l’enregistrement de l’ensemble d’outils : assurez-vous que l’ensemble d’outils est enregistré auprès de l’agent :
Agent("chat", "Chat agent", func() {
Use(HelpersToolset) // Make sure this is included
})
Vérifiez l’orthographe du nom de l’outil : les noms d’outils sont sensibles à la casse et utilisent des noms qualifiés.
Régénérer le code après les modifications de DSL :
goa gen example.com/project/design
Erreur “charge utile invalide”
Symptôme:
error: invalid payload: json: cannot unmarshal string into Go struct field SearchPayload.limit of type int
Cause : Le LLM a fourni une charge utile qui ne correspond pas au schéma de l’outil.
Solutions :
- Renvoie un RetryHint de l’exécuteur afin que le planificateur puisse s’auto-corriger :
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
}
Améliorer les descriptions des outils pour clarifier les types attendus.
Ajoutez des exemples au DSL :
Args(func() {
Attribute("limit", Int, "Maximum results", func() {
Example(10)
Minimum(1)
Maximum(100)
})
})
Conseils de débogage
Activer la journalisation du débogage
import "goa.design/goa-ai/runtime/agent/runtime"
rt := runtime.New(
runtime.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))),
)
Abonnez-vous aux événements pour le débogage
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{}))
Inspecter les spécifications de l’outil au moment de l’exécution
// 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)
}
Prochaines étapes
- Référence DSL - Référence complète de la fonction DSL
- Runtime – Comprendre l’architecture d’exécution
- Production - Déployer avec Temporal et diffuser UI