Architektur im Überblick
Quelldateien
Diese Seite wurde aus den folgenden Quelldateien erstellt:
- agent/agent.go
- agent/context.go
- runner/runner.go
- model/llm.go
- session/service.go
- internal/context/context_test.go
- internal/context/callback_context.go
- internal/context/readonly_context.go
- internal/context/invocation_context.go
- agent/agent_test.go
- examples/a2a/main.go
- examples/mcp/main.go
- examples/web/main.go
- examples/rest/main.go
- examples/telemetry/main.go
- examples/quickstart/main.go
- examples/toolconfirmation/main.go
- cmd/internal/adkcli/main.go
- examples/tools/loadmemory/main.go
- examples/tools/loadartifacts/main.go
Das Google Agent Development Kit (ADK) für Go ist ein Framework zur Entwicklung von KI-Agenten, das auf einer modularen Architektur basiert. Es trennt die Agentenlogik, Ausführungsorchestrierung, Sitzungsverwaltung und LLM-Integration in klar definierte Komponenten. Diese Struktur ermöglicht die Entwicklung von Agenten, die sowohl eigenständig als auch in hierarchischen Strukturen mit Sub-Agenten operieren können.
Systemarchitektur-Überblick
Die ADK-Architektur besteht aus vier Hauptschichten: der Agentenschicht, der Runner-Schicht, der Service-Schicht und der Modell-Schicht. Die Agentenschicht definiert die Kernlogik und Hierarchiebeziehungen, während die Runner-Schicht die Ausführung orchestriert und die Services koordiniert.
正在加载图表渲染器...
Architektur-Erklärungspunkte:
-
Runner als zentraler Orchestrator: Der Runner (runner/runner.go:78-111) verwaltet die gesamte Agentenausführung und koordiniert Session-, Artifact- und Memory-Services.
-
Hierarchische Agentenstruktur: Agenten können Sub-Agenten enthalten (agent/agent.go:84-87), wodurch komplexe Multi-Agent-Systeme mit delegierten Aufgaben entstehen.
-
Service-Trennung: Session-, Artifact- und Memory-Services sind entkoppelt und können unabhängig implementiert werden (In-Memory oder persistent).
-
LLM-Abstraktion: Das LLM-Interface (model/llm.go:26-29) ermöglicht die Integration verschiedener Modelle, primär Gemini.
-
Callback-Lebenszyklus: BeforeAgent- und AfterAgent-Callbacks (agent/agent.go:89-92) ermöglichen Eingriffe vor und nach der Agentenausführung.
Agenten-Kernarchitektur und Konfiguration
Kernkomponenten des Agenten
Der Agent ist die zentrale Ausführungseinheit im ADK. Er wird durch die Config-Struktur konfiguriert, die alle wesentlichen Eigenschaften definiert:
go1type Config struct { 2 Name string // Eindeutiger Name im Agenten-Baum 3 Description string // Für LLM-basierte Delegation verwendet 4 SubAgents []Agent // Hierarchiebeziehungen 5 BeforeAgentCallbacks []BeforeAgentCallback 6 AfterAgentCallbacks []AfterAgentCallback 7 Run func(ctx InvocationContext) iter.Seq2[*session.Event, error] 8}
Die Konstruktion erfolgt über die New()-Funktion (agent/agent.go:53-72), die Duplikate in der Sub-Agenten-Liste validiert und den internen Zustand initialisiert.
Verantwortlichkeiten und Grenzen:
| Aspekt | Verantwortung | Nicht-Verantwortung |
|---|---|---|
| Identität | Name und Beschreibung verwalten | UI-Darstellung |
| Hierarchie | Sub-Agenten-Beziehungen definieren | Parent-Referenzen (durch Runner gesetzt) |
| Ausführung | Run-Funktion bereitstellen | Session-Management |
| Lebenszyklus | Callbacks auslösen | Service-Koordination |
Sub-Agenten-Hierarchie
Die Sub-Agenten-Struktur ermöglicht die Delegation von Aufgaben an spezialisierte Agenten. Bei der Initialisierung (agent/agent.go:55-60) wird validiert, dass jeder Sub-Agent nur einmal referenziert wird:
go1subAgentSet := make(map[Agent]bool) 2for _, subAgent := range cfg.SubAgents { 3 if _, ok := subAgentSet[subAgent]; ok { 4 return nil, fmt.Errorf("error creating agent: subagent %q appears multiple times", subAgent.Name()) 5 } 6 subAgentSet[subAgent] = true 7}
Die Beschreibung (Description) ist kritisch für die LLM-basierte Delegation – das LLM verwendet diese Einzeiler-Beschreibung, um zu entscheiden, ob die Steuerung an einen Sub-Agenten übergeben werden soll (agent/agent.go:79-83).
Callback-Lebenszyklus
Der Callback-Mechanismus bietet Erweiterungspunkte im Ausführungslebenszyklus:
-
BeforeAgentCallbacks: Werden sequenziell vor der
Run-Funktion aufgerufen. Wenn ein Callback non-nil Content zurückgibt, wird die Hauptausführung übersprungen (agent/agent.go:89-92). -
AfterAgentCallbacks: Werden nach Abschluss der
Run-Funktion ausgeführt und können die Ergebnisse modifizieren oder ergänzen.
Die Testfälle in agent/agent_test.go:40-59 demonstrieren, dass BeforeAgent-Callbacks die LLM-Ausführung vollständig ersetzen können, wenn sie Content zurückgeben.
InvocationContext und Ausführungskontext
InvocationContext-Schnittstelle
Der InvocationContext ist die zentrale Schnittstelle für die Agentenausführung und kombiniert context.Context mit ADK-spezifischen Methoden (agent/context.go:60-98):
go1type InvocationContext interface { 2 context.Context 3 4 Agent() Agent 5 Artifacts() Artifacts 6 Memory() Memory 7 Session() session.Session 8 InvocationID() string 9 Branch() string 10 UserContent() *genai.Content 11 RunConfig() *RunConfig 12 EndInvocation() 13 Ended() bool 14 WithContext(ctx context.Context) InvocationContext 15}
Kernmethoden-Erklärung:
-
Branch(): Gibt den Pfad im Agenten-Baum zurück (z.B.
agent_1.agent_2.agent_3). Dies ist wichtig für parallele Agenten, deren Sub-Agenten die Konversationshistorie ihrer Peers nicht sehen sollten (agent/context.go:77-85). -
EndInvocation(): Beendet die aktuelle Invocation und stoppt geplante Agentenaufrufe. Der Zustand wird über
Ended()abgefragt (agent/context.go:93-97).
Interne Implementierung
Die interne Implementierung in internal/context/invocation_context.go:40-99 zeigt die Struktur:
go1type InvocationContext struct { 2 context.Context 3 params InvocationContextParams 4} 5 6func (c *InvocationContext) Agent() agent.Agent { 7 return c.params.Agent 8} 9 10func (c *InvocationContext) EndInvocation() { 11 c.params.EndInvocation = true 12}
Die InvocationID wird automatisch generiert, falls nicht angegeben (internal/context/invocation_context.go:42-44):
go1if params.InvocationID == "" { 2 params.InvocationID = "e-" + uuid.NewString() 3}
Context-Hierarchie und Einschränkungen
Das ADK implementiert eine Context-Hierarchie mit abgestuften Zugriffsebenen:
- InvocationContext: Voller Zugriff auf alle Ressourcen
- CallbackContext: Schreibender Zugriff auf State und Artifacts, aber eingeschränkter Agent-Zugriff
- ReadonlyContext: Nur lesender Zugriff auf State und Metadaten
Die Tests in internal/context/context_test.go:27-46 verifizieren diese Hierarchie:
go1func TestReadonlyContext(t *testing.T) { 2 inv := NewInvocationContext(t.Context(), InvocationContextParams{}) 3 readonly := NewReadonlyContext(inv) 4 5 if got, ok := readonly.(agent.InvocationContext); ok { 6 t.Errorf("ReadonlyContext is unexpectedly an InvocationContext") 7 } 8}
Der ReadonlyContext (internal/context/readonly_context.go:25-72) delegiert alle Aufrufe an den zugrundeliegenden InvocationContext, bietet aber nur die eingeschränkte Schnittstelle an.
Runner und Session-Management
Runner-Initialisierung und Konfiguration
Der Runner ist der zentrale Orchestrator, der die Agentenausführung mit den verschiedenen Services koordiniert. Die Initialisierung erfolgt über die New()-Funktion (runner/runner.go:78-111):
go1func New(cfg Config) (*Runner, error) { 2 if cfg.Agent == nil { 3 return nil, fmt.Errorf("root agent is required") 4 } 5 if cfg.SessionService == nil { 6 return nil, fmt.Errorf("session service is required") 7 } 8 9 parents, err := parentmap.New(cfg.Agent) 10 if err != nil { 11 return nil, fmt.Errorf("failed to create agent tree: %w", err) 12 } 13 14 pluginManager, err := plugininternal.NewPluginManager(...) 15 16 return &Runner{ 17 appName: cfg.AppName, 18 rootAgent: cfg.Agent, 19 sessionService: cfg.SessionService, 20 artifactService: cfg.ArtifactService, 21 memoryService: cfg.MemoryService, 22 parents: parents, 23 pluginManager: pluginManager, 24 autoCreateSession: cfg.AutoCreateSession, 25 }, nil 26}
Pflichtfelder:
Agent: Der Root-Agent des Agenten-BaumsSessionService: Verwaltung der Sitzungen
Optionale Felder:
ArtifactService: Speicherung binärer DatenMemoryService: Benutzerübergreifender KontextAutoCreateSession: Automatische Session-Erstellung
Run-Methode und Ausführungsfluss
Die Run-Methode (runner/runner.go:131-170) implementiert den vollständigen Ausführungsfluss:
正在加载图表渲染器...
Ausführungsfluss-Erklärungspunkte:
-
Session-Auflösung: Der Runner versucht zuerst, eine existierende Session zu laden. Falls
autoCreateSessionaktiviert ist und die Session nicht existiert, wird eine neue erstellt (runner/runner.go:147-164). -
Agent-Selektion:
findAgentToRunbestimmt basierend auf der Session-Historie und der aktuellen Nachricht, welcher Agent ausgeführt werden soll. -
Event-Streaming: Die Ergebnisse werden als Iterator (
iter.Seq2[*session.Event, error]) zurückgegeben, was Streaming-Antworten ermöglicht. -
Event-Persistierung: Jedes Event wird durch
AppendEventin der Session gespeichert.
Session-Service-Schnittstelle
Der Session-Service definiert die CRUD-Operationen für Sitzungen (session/service.go:25-32):
go1type Service interface { 2 Create(context.Context, *CreateRequest) (*CreateResponse, error) 3 Get(context.Context, *GetRequest) (*GetResponse, error) 4 List(context.Context, *ListRequest) (*ListResponse, error) 5 Delete(context.Context, *DeleteRequest) error 6 AppendEvent(context.Context, Session, *Event) error 7}
Die InMemoryService-Implementierung (session/service.go:34-40) bietet eine einfache In-Memory-Speicherung für Entwicklung und Tests:
go1func InMemoryService() Service { 2 return &inMemoryService{ 3 appState: make(map[string]stateMap), 4 userState: make(map[string]map[string]stateMap), 5 } 6}
GetRequest-Filteroptionen (session/service.go:58-70):
NumRecentEvents: Begrenzt auf die N neuesten EventsAfter: Filtert Events mit Timestamp >= gegebener Zeit
LLM-Integration und Modell-Schnittstelle
LLM-Schnittstelle
Die LLM-Schnittstelle (model/llm.go:26-29) abstrahiert die Kommunikation mit Sprachmodellen:
go1type LLM interface { 2 Name() string 3 GenerateContent(ctx context.Context, req *LLMRequest, stream bool) iter.Seq2[*LLMResponse, error] 4}
Die Schnittstelle unterstützt sowohl Streaming- als auch Nicht-Streaming-Modi über den stream-Parameter. Die Rückgabe als Iterator ermöglicht die progressive Verarbeitung von Teilantworten.
Request/Response-Strukturen
LLMRequest (model/llm.go:31-38):
go1type LLMRequest struct { 2 Model string 3 Contents []*genai.Content 4 Config *genai.GenerateContentConfig 5 Tools map[string]any `json:"-"` 6}
LLMResponse (model/llm.go:40-65):
go1type LLMResponse struct { 2 Content *genai.Content 3 CitationMetadata *genai.CitationMetadata 4 GroundingMetadata *genai.GroundingMetadata 5 UsageMetadata *genai.GenerateContentResponseUsageMetadata 6 CustomMetadata map[string]any 7 LogprobsResult *genai.LogprobsResult 8 ModelVersion string 9 Partial bool // Streaming: Teilinhalt 10 TurnComplete bool // Streaming: Runde abgeschlossen 11 Interrupted bool // Benutzerunterbrechung 12 ErrorCode string 13 ErrorMessage string 14 FinishReason genai.FinishReason 15 AvgLogprobs float64 16}
Wichtige Felder für Streaming:
Partial: Gibt an, ob dies ein unvollständiger Stream-Chunk istTurnComplete: Signalisiert das Ende eines vollständigen TurnInterrupted: Zeigt eine Benutzerunterbrechung bei bidi-Streaming an
Telemetry-Integration
Die Agenten-Integration zeigt agent/agent.go:160-190, wie Telemetry-Spans erstellt werden:
go1func (a *agent) Run(ctx InvocationContext) iter.Seq2[*session.Event, error] { 2 return func(yield func(*session.Event, error) bool) { 3 spanCtx, span := telemetry.StartInvokeAgentSpan(ctx, a, ctx.Session().ID(), ctx.InvocationID()) 4 yield, endSpan := telemetry.WrapYield(span, yield, func(span trace.Span, event *session.Event, err error) { 5 telemetry.TraceAgentResult(span, telemetry.TraceAgentResultParams{ 6 ResponseEvent: event, 7 Error: err, 8 }) 9 }) 10 defer endSpan() 11 // ... 12 } 13}
Callback-Mechanismen und Context-Hierarchie
CallbackContext-Implementierung
Der CallbackContext wird an Benutzer-Callbacks übergeben und bietet kontrollierten Zugriff auf Agent-Ressourcen. Die Erstellung erfolgt über Factory-Funktionen (internal/context/callback_context.go:48-68):
go1func NewCallbackContext(ctx agent.InvocationContext) agent.CallbackContext { 2 return newCallbackContext(ctx, make(map[string]any), make(map[string]int64)) 3} 4 5func NewCallbackContextWithDelta(ctx agent.InvocationContext, stateDelta map[string]any, artifactDelta map[string]int64) agent.CallbackContext { 6 return newCallbackContext(ctx, stateDelta, artifactDelta) 7}
Die interne Struktur (internal/context/callback_context.go:72-77):
go1type callbackContext struct { 2 agent.ReadonlyContext 3 artifacts *internalArtifacts 4 invocationCtx agent.InvocationContext 5 eventActions *session.EventActions 6}
State-Delta und Artifact-Delta
Der CallbackContext verwaltet Änderungen an State und Artifacts über EventActions:
go1eventActions := &session.EventActions{ 2 StateDelta: stateDelta, 3 ArtifactDelta: artifactDelta 4}
Beim Speichern eines Artifacts (internal/context/callback_context.go:33-46) wird die Version im Delta erfasst:
go1func (ia *internalArtifacts) Save(ctx context.Context, name string, data *genai.Part) (*artifact.SaveResponse, error) { 2 resp, err := ia.Artifacts.Save(ctx, name, data) 3 if err != nil { 4 return resp, err 5 } 6 if ia.eventActions != nil { 7 if ia.eventActions.ArtifactDelta == nil { 8 ia.eventActions.ArtifactDelta = make(map[string]int64) 9 } 10 ia.eventActions.ArtifactDelta[name] = resp.Version 11 } 12 return resp, nil 13}
Schnittstellen-Hierarchie
Die Context-Hierarchie wird durch Schnittstellen-Embedding definiert (agent/context.go:106-128):
go1type ReadonlyContext interface { 2 context.Context 3 UserContent() *genai.Content 4 InvocationID() string 5 AgentName() string 6 ReadonlyState() session.ReadonlyState 7 UserID() string 8 AppName() string 9 SessionID() string 10 Branch() string 11} 12 13type CallbackContext interface { 14 ReadonlyContext 15 Artifacts() Artifacts 16 State() session.State 17}
Zugriffsebenen:
| Context-Typ | State-Zugriff | Artifact-Zugriff | Agent-Zugriff |
|---|---|---|---|
| InvocationContext | Lesend/Schreibend | Lesend/Schreibend | Vollständig |
| CallbackContext | Lesend/Schreibend | Lesend/Schreibend | Nur Name |
| ReadonlyContext | Nur lesend | Kein Zugriff | Nur Name |
Datenfluss und End-to-End-Ausführung
Vollständiger Ausführungsfluss
正在加载图表渲染器...
Datenfluss-Erklärungspunkte:
-
Session-First-Ansatz: Die Session wird immer zuerst geladen oder erstellt, bevor ein Agent ausgewählt wird. Dies stellt sicher, dass der Kontext für die Agent-Selektion verfügbar ist.
-
Callback-Kurzschluss: Wenn ein BeforeAgentCallback Content zurückgibt, wird die Run-Funktion übersprungen. Dies ermöglicht Vorverarbeitung oder vollständige Antwortgenerierung ohne LLM-Aufruf.
-
Event-Persistierung: Jedes emittierte Event wird sofort an die Session angehängt, was eine konsistente Historie gewährleistet.
-
EndInvocation-Kontrolle: Sowohl Callbacks als auch die Run-Funktion können
EndInvocation()aufrufen, um nachfolgende Verarbeitung zu stoppen.
Event-Struktur und -Verarbeitung
Events kapseln alle Informationen über einen Ausführungsschritt:
go1type Event struct { 2 Author string 3 Content *genai.Content 4 LLMResponse model.LLMResponse 5 Actions EventActions 6 Timestamp time.Time 7 InvocationID string 8} 9 10type EventActions struct { 11 StateDelta map[string]any 12 ArtifactDelta map[string]int64 13}
Die EventActions erfassen alle Nebenwirkungen der Ausführung, wie State-Änderungen und Artifact-Updates.
Kern-Design-Entscheidungen
1. Iterator-basierte Streaming-Architektur
Entscheidung: Verwendung von Go 1.23+ iter.Seq2 für alle Streaming-Operationen.
Begründung: Ermöglicht lazy Evaluation und natürliche Streaming-Unterstützung ohne explizite Channel-Verwaltung. Die yield-Funktion erlaubt frühen Abbruch durch den Consumer.
Nachteil: Erfordert Go 1.23+ und gewöhnungsbedürftige Syntax für Entwickler, die mit Iteratoren nicht vertraut sind.
2. Context-Hierarchie für Zugriffskontrolle
Entscheidung: Drei-stufige Context-Hierarchie (Invocation → Callback → Readonly).
Begründung: Ermöglicht feingranulare Zugriffskontrolle für Callbacks und Tools. Tools erhalten nur minimale Zugriffsrechte, während der Haupt-Agent vollen Zugriff hat.
Implementierung: internal/context/context_test.go:36-46 zeigt die Typprüfungen, die sicherstellen, dass eingeschränkte Contexts nicht versehentlich als vollwertige InvocationContexts verwendet werden.
3. Service-Interface-Abstraktion
Entscheidung: Alle Services (Session, Artifact, Memory) als Interfaces definiert.
Begründung: Ermöglicht verschiedene Backend-Implementierungen (In-Memory für Tests, Datenbank für Produktion) ohne Code-Änderungen.
Beispiel: session/service.go:25-32 definiert das Interface, während session/service.go:34-40 die In-Memory-Implementierung bereitstellt.
4. Branch-basierte Historien-Trennung
Entscheidung: Der Branch-String im InvocationContext steuert die Sichtbarkeit von Konversationshistorie.
Begründung: Bei parallelen Agenten sollen Sub-Agenten nicht die Historie ihrer Peers sehen. Der Branch-Pfad (z.B. root.parallel.child1) ermöglicht diese Isolierung.
Quelle: agent/context.go:77-85 beschreibt das Format und die Verwendung.
5. Callback-gesteuerte Erweiterbarkeit
Entscheidung: BeforeAgent- und AfterAgent-Callbacks statt Middleware-Pattern.
Begründung: Einfachere Semantik für gängige Anwendungsfälle (Logging, Validierung, Antwortmodifikation). Die sequenzielle Ausführung mit Kurzschluss-Möglichkeit ist intuitiver als verkettete Middleware.
Testabdeckung: agent/agent_test.go:40-68 demonstriert die verschiedenen Callback-Szenarien.
6. Parent-Map für Agent-Hierarchie
Entscheidung: Separate Parent-Map statt Parent-Referenz im Agent.
Begründung: Vermeidet zirkuläre Referenzen und ermöglicht dynamische Umstrukturierung des Agenten-Baums. Die Parent-Map wird vom Runner verwaltet (runner/runner.go:88-91).
7. Telemetry-Integration durch Span-Wrapping
Entscheidung: Telemetry-Spans werden um die yield-Funktion gewrapped.
Begründung: Ermöglicht automatische Span-Beendigung und Fehler-Erfassung auch bei vorzeitigem Abbruch. Die WrapYield-Funktion in agent/agent.go:163-169 implementiert dieses Pattern.
Technologie-Stack und Auswahlkriterien
| Technologie | Verwendungszweck | Auswahlbegründung | Alternative |
|---|---|---|---|
| Go 1.23+ | Laufzeitumgebung | Iterator-Unterstützung, Performance | Python ADK |
| Gemini API | LLM-Backend | Native Google-Integration, Multimodal | OpenAI, Anthropic |
| genai SDK | LLM-Client | Typsichere API, Streaming | REST direkt |
| A2A Protocol | Agent-zu-Agent | Standardisiertes Protokoll | Proprietär |
| MCP | Tool-Integration | Standard für Tool-Discovery | Custom Tool API |
| OpenTelemetry | Observability | Standard, Vendor-agnostisch | Prometheus |
| In-Memory Storage | Entwicklung/Tests | Einfachheit, keine Dependencies | Redis, PostgreSQL |
| HTTP/REST | API-Exposition | Universal kompatibel | gRPC |
| SSE | Streaming-Responses | Browser-kompatibel | WebSockets |
Modulabhängigkeiten und Schnittstellen
正在加载图表渲染器...
Abhängigkeits-Erklärungspunkte:
-
Agent → Model: Agenten benötigen das LLM-Interface für Content-Generierung, sind aber nicht an eine spezifische Implementierung gebunden.
-
Runner → Agent + Session: Der Runner orchestriert beide und stellt die Verbindung zwischen Ausführung und Persistenz her.
-
Context-Hierarchie: Die internen Context-Implementierungen hängen von der Agent-Schnittstelle ab, nicht umgekehrt.
-
Tool-Integration: Tools sind optionale Erweiterungen, die unabhängig vom Kern funktionieren.
Konfiguration und Start-up
Runner-Konfiguration
Die minimale Runner-Konfiguration erfordert nur einen Root-Agenten und einen Session-Service:
go1runner, err := runner.New(runner.Config{ 2 AppName: "my-app", 3 Agent: rootAgent, 4 SessionService: session.InMemoryService(), 5})
Die vollständige Konfiguration (runner/runner.go:78-111) umfasst:
go1type Config struct { 2 AppName string 3 Agent agent.Agent 4 SessionService session.Service 5 ArtifactService artifact.Service // Optional 6 MemoryService memory.Service // Optional 7 AutoCreateSession bool // Default: false 8 PluginConfig PluginConfig // Für MCP etc. 9}
Agent-Konfiguration
Die Agent-Konfiguration (agent/agent.go:74-90) definiert Struktur und Verhalten:
go1agent, err := agent.New(agent.Config{ 2 Name: "weather_agent", 3 Description: "Answers weather questions", 4 SubAgents: []agent.Agent{subAgent1, subAgent2}, 5 BeforeAgentCallbacks: []agent.BeforeAgentCallback{validateInput}, 6 AfterAgentCallbacks: []agent.AfterAgentCallback{logResult}, 7 Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { 8 // Agent-Logik hier 9 }, 10})
Beispiel-Integrationen
Die Examples demonstrieren verschiedene Integrationsmuster:
- examples/rest/main.go:61-76: REST-API-Server mit Health-Check
- examples/web/main.go:103-115: Web-UI mit Multi-Agent-Loader
- examples/a2a/main.go:92-100: A2A-Protokoll-Exposition
- examples/mcp/main.go:107-115: MCP-Tool-Integration
