Avvio rapido

Build a working AI agent in 10 minutes. Start with a stub, add streaming, validation, then connect a real LLM.

Questa guida ti porta da un modulo vuoto a un agente Goa-AI generato ed eseguibile. L’esempio generato utilizza il motore in memoria, quindi non è necessario Temporal, MongoDB, Redis o una chiave API del modello per la prima esecuzione.

Costruirai:

  1. Un progetto Goa con un agente, uno strumento digitato e un completamento diretto digitato.
  2. Codice di cablaggio dell’agente, del set di strumenti, del completamento e del runtime generato.
  3. Un esempio di impalcatura eseguibile con un pianificatore stub che è possibile sostituire con un pianificatore supportato da modello.
  4. I primi hook di produzione: sessioni esplicite, esecutori di strumenti generati, streaming e registrazione del modello.

1. Crea un modulo

GOPROXY=direct go install goa.design/goa/v3/cmd/goa@fix/goa-generation-plan

mkdir quickstart && cd quickstart
go mod init example.com/quickstart
GOPROXY=direct go get goa.design/goa/v3@fix/goa-generation-plan goa.design/goa-ai@main
mkdir design

Questi nomi di branch selezionano il contratto di storage integrato del runtime usato dalla guida. L’impostazione del proxy diretto è necessaria perché il nome del branch preliminare di Goa contiene una barra. Go registra pseudoversioni esatte in go.mod, quindi gli aggiornamenti successivi dei branch non modificano una build esistente finché non esegui di nuovo go get.

Goa-AI attualmente prende di mira il moderno Go. Utilizza la versione Go dichiarata da Modulo goa.design/goa-ai o successivo.


2. Definire l’agente

Crea design/design.go:

package design

import (
	. "goa.design/goa/v3/dsl"
	. "goa.design/goa-ai/dsl"
)

var _ = API("orchestrator", func() {})

var AskPayload = Type("AskPayload", func() {
	Attribute("question", String, "User question to answer")
	Example(map[string]any{"question": "What is the capital of Japan?"})
	Required("question")
})

var Answer = Type("Answer", func() {
	Attribute("text", String, "Answer text")
	Example(map[string]any{"text": "Tokyo is the capital of Japan."})
	Required("text")
})

var TaskDraft = Type("TaskDraft", func() {
	Attribute("name", String, "Task name")
	Attribute("goal", String, "Outcome-style goal")
	Example(map[string]any{"name": "Prepare launch checklist", "goal": "Confirm the service is ready to launch."})
	Required("name", "goal")
})

var _ = Service("orchestrator", func() {
	Completion("draft_task", "Produce a task draft directly", func() {
		Return(TaskDraft)
	})

	Agent("chat", "Friendly Q&A assistant", func() {
		Use("helpers", func() {
			Tool("answer", "Answer a simple question", func() {
				Args(AskPayload)
				Return(Answer)
			})
		})
		RunPolicy(func() {
			DefaultCaps(MaxToolCalls(2), MaxRecoveryTurns(1))
			TimeBudget("15s")
		})
	})
})

Questa è la fonte della verità. Strumenti e completamenti riutilizzano i normali tipi Goa, descrizioni, esempi e validazioni. Gli schemi rivolti al modello, i codec tipizzati, e i contratti di runtime vengono generati da questo progetto.


3. Genera codice ed esempio

goa gen example.com/quickstart/design
goa example example.com/quickstart/design
go mod tidy
go run ./cmd/orchestrator

Forma prevista:

RunID: demo-chat-run
Assistant: Tool helpers.answer returned {"text":"Tokyo is the capital of Japan."}
Completion draft_task: &{Name:Prepare launch checklist Goal:Confirm the service is ready to launch.}
Completion delta draft_task: {"goal":"Confirm the ser
Completion stream draft_task: &{Name:Prepare launch checklist Goal:Confirm the service is ready to launch.}

La riga Completion delta è un prefisso JSON trasmesso in streaming. Il punto esatto in cui si interrompe può variare, ma il valore finale trasmesso è completo e corrisponde all’esempio dichiarato.

goa gen crea i contratti generati. goa example crea proprietà dell’applicazione impalcatura:

  • gen/: codice generato. Non modificare questa directory manualmente.
  • cmd/orchestrator/main.go: punto di ingresso di esempio eseguibile.
  • internal/agents/orchestrator/bootstrap/bootstrap.go: costruzione del runtime e registrazione dell’agente.
  • internal/agents/chat/planner/planner.go: pianificatore stub da sostituire.
  • gen/orchestrator/completions/: helper digitati per il completamento diretto.

Rigenerare dopo le modifiche DSL. Esegui nuovamente goa example quando vuoi l’impalcatura aggiornamenti, quindi mantenere le modifiche dell’applicazione in cmd/ e internal/.


4. Comprendere il ciclo di runtime

Il ciclo di pianificazione/esecuzione:

  1. PlanStart riceve i messaggi utente iniziali.
  2. Il pianificatore restituisce un FinalResponse, chiamate strumento o una richiesta di attesa.
  3. Il runtime convalida ed esegue le chiamate agli strumenti ammessi utilizzando le specifiche generate e gli esecutori registrati.
  4. PlanResume riceve gli output dello strumento visibili dal pianificatore.
  5. Il ciclo si ripete finché il pianificatore non restituisce una risposta finale, un risultato dello strumento terminale o il runtime non impone limiti/budget di tempo.

Quando limiti o deadline forzano la finalizzazione, un planner può comunque chiudere il run restituendo strumenti terminali di bookkeeping. Il runtime esegue solo strumenti TerminalRun() in quel percorso (TerminalRun() implica bookkeeping) e richiede che abbiano successo prima di considerare chiuso il run.

L’esempio generato inizia con uno stub planner in modo che questo flusso sia visibile prima colleghi un modello. Un vero pianificatore segue lo stesso contratto; delega semplicemente la decisione a un cliente modello.


5. Chiama l’agente dal codice

I pacchetti di agenti generati espongono client tipizzati. Le esecuzioni a sessione richiedono un sessione esplicita; le esecuzioni one-shot sono intenzionalmente senza sessioni.

store := storageinmem.New()
if _, err := store.CreateSession(ctx, "session-1", time.Now().UTC()); err != nil {
	log.Fatal(err)
}

rt, cleanup, err := bootstrap.New(ctx, store)
if err != nil {
	log.Fatal(err)
}
defer cleanup()

client := chat.NewClient(rt)
out, err := client.Run(ctx, "session-1", []*model.Message{{
	Role:  model.ConversationRoleUser,
	Parts: []model.Part{model.TextPart{Text: "Hello"}},
}})
if err != nil {
	log.Fatal(err)
}
fmt.Println(out.RunID)

out, err = client.OneShotRun(ctx, []*model.Message{{
	Role:  model.ConversationRoleUser,
	Parts: []model.Part{model.TextPart{Text: "Summarize this document"}},
}})

Utilizzare Run o Start per il lavoro conversazionale/sessionale. Utilizzare OneShotRun o StartOneShot per processi di richiesta/risposta che dovrebbero essere osservabili da RunID ma non dovrebbe appartenere a una sessione.

Lo scaffold locale generato accetta uno storage.Store e il comando di esempio usa runtime/agent/storage/inmem. In produzione, l’applicazione passa un adattatore per il servizio proprietario del database del runtime. Quel servizio, non un worker dell’agente, crea e termina le sessioni.


6. Implementa un esecutore di strumenti

I package generati includono RegisterUsedToolsets per i toolset locali. Gli executor ricevono metadati espliciti e restituiscono un risultato posseduto dal runtime. Ogni package delle specifiche espone inoltre un descrittore tipizzato per strumento, qui helpers.AnswerTool, che associa ID, codec del payload e codec del risultato senza ripetere manualmente il collegamento:

type HelpersExecutor struct{}

func (e *HelpersExecutor) Execute(
	ctx context.Context,
	meta *runtime.ToolCallMeta,
	call *runtime.ToolCall,
) (*runtime.ToolExecutionResult, error) {
	switch call.Name {
	case helpers.Answer:
		args, err := helpers.AnswerTool().Payload.FromJSON(call.Payload)
		if err != nil {
			return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
		}
		return runtime.Executed(&planner.ToolResult{
			Name:   call.Name,
			Result: &helpers.AnswerResult{Text: "Answer: " + args.Question},
		}), nil
	default:
		return runtime.Executed(&planner.ToolResult{
			Name: call.Name,
			Failure: &planner.ToolFailure{
				Kind:     planner.FailureInvalidCall,
				Error:    planner.NewToolError("unknown tool"),
				Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
			},
		}), nil
	}
}

if err := chat.RegisterUsedToolsets(ctx, rt, chat.WithHelpersExecutor(&HelpersExecutor{})); err != nil {
	log.Fatal(err)
}

Il runtime convalida il payload JSON con i codec generati prima dell’esecuzione, codifica i risultati positivi con i codec dei risultati generati, registra l’esecuzione canonica eventi e passa gli output visibili dal pianificatore a PlanResume.


7. Connetti un modello

Registra i clienti del fornitore con il runtime, quindi accedi ad essi dai pianificatori tramite ID. Per i pianificatori di streaming, preferisci PlannerModelClient; possiede assistente/pensiero e l’emissione di eventi di utilizzo. Questo esempio usa OpenAI; vedi Runtime → Integrazione LLM per gli equivalenti AWS Bedrock e Google Vertex AI (Gemini e Claude-on-Vertex).

modelClient, err := rt.NewOpenAIModelClient(runtime.OpenAIConfig{
	APIKey:       os.Getenv("OPENAI_API_KEY"),
	DefaultModel: "gpt-5-mini",
	HighModel:    "gpt-5",
	SmallModel:   "gpt-5-nano",
})
if err != nil {
	log.Fatal(err)
}
if err := rt.RegisterModel("default", modelClient); err != nil {
	log.Fatal(err)
}

Schizzo del pianificatore:

func (p *Planner) PlanStart(ctx context.Context, in *planner.PlanInput) (*planner.PlanResult, error) {
	mc, ok := in.Agent.PlannerModelClient("default")
	if !ok {
		return nil, errors.New("model client default is not registered")
	}

	summary, err := mc.Stream(ctx, &model.Request{
		Messages: in.Messages,
		Tools:    in.Agent.AdvertisedToolDefinitions(),
		Stream:   true,
	})
	if err != nil {
		return nil, err
	}
	if len(summary.ToolCalls) > 0 {
		return &planner.PlanResult{ToolCalls: summary.ToolCalls}, nil
	}
	final := summary.FinalResponse()
	if final == nil {
		return nil, errors.New("model stream ended without a canonical response")
	}
	return &planner.PlanResult{
		FinalResponse: final,
		Streamed: true,
	}, nil
}

Utilizza in.Agent.ModelClient("default") quando hai bisogno di controllo e associazione del flusso non elaborato con planner.ConsumeStream. Scegli un proprietario dello stream per turno del pianificatore.


8. Aggiungi streaming

Goa-AI emette eventi di flusso digitati per il testo dell’assistente, l’avvio/fine dello strumento, il flusso di lavoro stato, attesa, utilizzo e collegamenti di esecuzione figlio. Cablare qualsiasi stream.Sink:

type ConsoleSink struct{}

func (s *ConsoleSink) Send(ctx context.Context, event stream.Event) error {
	switch e := event.(type) {
	case stream.AssistantReply:
		fmt.Print(e.Data.Text)
	case stream.ToolStart:
		fmt.Printf("tool_start: %s\n", e.Data.ToolName)
	case stream.ToolEnd:
		fmt.Printf("tool_end: %s\n", e.Data.ToolName)
	case stream.Workflow:
		fmt.Printf("workflow: %s\n", e.Data.Phase)
	}
	return nil
}

func (s *ConsoleSink) Close(ctx context.Context) error { return nil }

rt := runtime.New(runtimeStore, runtime.WithStream(&ConsoleSink{}))

Per le UI di produzione, pubblica su Pulse e iscriviti al flusso della sessione (session/<session_id>). Chiudi la connessione utente quando osservi run_stream_end per la corsa attiva.


9. Utilizza i completamenti diretti digitati

Completion(...) è per l’output dell’assistente strutturato che non è una chiamata allo strumento. Gli helper generati richiedono l’output strutturato imposto dal provider e lo decodificano codec generati:

resp, err := completions.CompleteDraftTask(ctx, modelClient, &model.Request{
	Messages: []*model.Message{{
		Role:  model.ConversationRoleUser,
		Parts: []model.Part{model.TextPart{Text: "Draft a task for launch readiness."}},
	}},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(resp.Value.Name)

I nomi di completamento fanno parte del contratto di output strutturato: 1-64 ASCII caratteri, lettere/cifre/_/-, che iniziano con una lettera o una cifra. Streaming gli aiutanti di completamento espongono i blocchi di anteprima completion_delta e decodificano solo i file pezzo canonico finale completion.


10. Comporre agenti

Gli agenti possono esportare set di strumenti utilizzati da altri agenti. Gli agenti nidificati vengono eseguiti come secondari flussi di lavoro con il proprio RunID e i flussi emettono child_run_linked in modo che le interfacce utente possano eseguire il rendering degli alberi.

Agent("researcher", "Research specialist", func() {
	Export("research", func() {
		Tool("deep_search", "Perform deep research", func() {
			Args(ResearchRequest)
			Return(ResearchReport)
		})
	})
})

Agent("coordinator", "Delegates specialist work", func() {
	Use(AgentToolset("orchestrator", "researcher", "research"))
})

Ogni agente mantiene il proprio pianificatore, gli strumenti e la policy. L’archivio del runtime fornito dall’applicazione registra ogni esecuzione radice e figlia, incluso il collegamento al padre. Il padre riceve un normale risultato dello strumento con un RunLink all’esecuzione figlia.


Cosa hai costruito

  • Un agente incentrato sulla progettazione con strumenti convalidati dallo schema.
  • Codec di payload/risultati generati e schemi JSON rivolti al modello.
  • Un contratto tipizzato a completamento diretto.
  • Un client runtime generato con esecuzione in sessione e one-shot.
  • Un percorso verso la pianificazione supportata da modelli, le interfacce utente in streaming e la composizione degli agenti.

Per la produzione, aggiungi il motore Temporal per la durabilità, un unico archivio del runtime di proprietà dell’applicazione, un archivio di memoria di proprietà del prodotto quando necessario, Pulse per lo streaming distribuito e middleware del modello per i limiti di velocità del provider. Il design di Goa rimane la fonte della verità.


Passaggi successivi

| Guida | Cosa imparerai ||——-|——————-| | DSL Reference | Tutte le funzioni DSL: policy, MCP, registri || Runtime | Pianifica/esegui loop, motori, archivi di memoria || Toolsets | Strumenti supportati da servizi, trasformazioni, esecutori || Agent Composition | Approfondimento sui modelli di agente come strumento || Production | Configurazione temporale, streaming alle interfacce utente, limitazione della velocità |