Preise

Architektur im Überblick

Quelldateien

Diese Seite wurde aus den folgenden Quelldateien erstellt:

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:

  1. Runner als zentraler Orchestrator: Der Runner (runner/runner.go:78-111) verwaltet die gesamte Agentenausführung und koordiniert Session-, Artifact- und Memory-Services.

  2. Hierarchische Agentenstruktur: Agenten können Sub-Agenten enthalten (agent/agent.go:84-87), wodurch komplexe Multi-Agent-Systeme mit delegierten Aufgaben entstehen.

  3. Service-Trennung: Session-, Artifact- und Memory-Services sind entkoppelt und können unabhängig implementiert werden (In-Memory oder persistent).

  4. LLM-Abstraktion: Das LLM-Interface (model/llm.go:26-29) ermöglicht die Integration verschiedener Modelle, primär Gemini.

  5. 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:

go
1type 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:

AspektVerantwortungNicht-Verantwortung
IdentitätName und Beschreibung verwaltenUI-Darstellung
HierarchieSub-Agenten-Beziehungen definierenParent-Referenzen (durch Runner gesetzt)
AusführungRun-Funktion bereitstellenSession-Management
LebenszyklusCallbacks auslösenService-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:

go
1subAgentSet := 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:

  1. 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).

  2. 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):

go
1type 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:

go
1type 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):

go
1if params.InvocationID == "" {
2    params.InvocationID = "e-" + uuid.NewString()
3}

Context-Hierarchie und Einschränkungen

Das ADK implementiert eine Context-Hierarchie mit abgestuften Zugriffsebenen:

  1. InvocationContext: Voller Zugriff auf alle Ressourcen
  2. CallbackContext: Schreibender Zugriff auf State und Artifacts, aber eingeschränkter Agent-Zugriff
  3. ReadonlyContext: Nur lesender Zugriff auf State und Metadaten

Die Tests in internal/context/context_test.go:27-46 verifizieren diese Hierarchie:

go
1func 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):

go
1func 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-Baums
  • SessionService: Verwaltung der Sitzungen

Optionale Felder:

  • ArtifactService: Speicherung binärer Daten
  • MemoryService: Benutzerübergreifender Kontext
  • AutoCreateSession: 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:

  1. Session-Auflösung: Der Runner versucht zuerst, eine existierende Session zu laden. Falls autoCreateSession aktiviert ist und die Session nicht existiert, wird eine neue erstellt (runner/runner.go:147-164).

  2. Agent-Selektion: findAgentToRun bestimmt basierend auf der Session-Historie und der aktuellen Nachricht, welcher Agent ausgeführt werden soll.

  3. Event-Streaming: Die Ergebnisse werden als Iterator (iter.Seq2[*session.Event, error]) zurückgegeben, was Streaming-Antworten ermöglicht.

  4. Event-Persistierung: Jedes Event wird durch AppendEvent in der Session gespeichert.

Session-Service-Schnittstelle

Der Session-Service definiert die CRUD-Operationen für Sitzungen (session/service.go:25-32):

go
1type 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:

go
1func 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 Events
  • After: 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:

go
1type 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):

go
1type 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):

go
1type 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 ist
  • TurnComplete: Signalisiert das Ende eines vollständigen Turn
  • Interrupted: Zeigt eine Benutzerunterbrechung bei bidi-Streaming an

Telemetry-Integration

Die Agenten-Integration zeigt agent/agent.go:160-190, wie Telemetry-Spans erstellt werden:

go
1func (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):

go
1func 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):

go
1type 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:

go
1eventActions := &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:

go
1func (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):

go
1type 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-TypState-ZugriffArtifact-ZugriffAgent-Zugriff
InvocationContextLesend/SchreibendLesend/SchreibendVollständig
CallbackContextLesend/SchreibendLesend/SchreibendNur Name
ReadonlyContextNur lesendKein ZugriffNur Name

Datenfluss und End-to-End-Ausführung

Vollständiger Ausführungsfluss

正在加载图表渲染器...

Datenfluss-Erklärungspunkte:

  1. 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.

  2. Callback-Kurzschluss: Wenn ein BeforeAgentCallback Content zurückgibt, wird die Run-Funktion übersprungen. Dies ermöglicht Vorverarbeitung oder vollständige Antwortgenerierung ohne LLM-Aufruf.

  3. Event-Persistierung: Jedes emittierte Event wird sofort an die Session angehängt, was eine konsistente Historie gewährleistet.

  4. 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:

go
1type 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

TechnologieVerwendungszweckAuswahlbegründungAlternative
Go 1.23+LaufzeitumgebungIterator-Unterstützung, PerformancePython ADK
Gemini APILLM-BackendNative Google-Integration, MultimodalOpenAI, Anthropic
genai SDKLLM-ClientTypsichere API, StreamingREST direkt
A2A ProtocolAgent-zu-AgentStandardisiertes ProtokollProprietär
MCPTool-IntegrationStandard für Tool-DiscoveryCustom Tool API
OpenTelemetryObservabilityStandard, Vendor-agnostischPrometheus
In-Memory StorageEntwicklung/TestsEinfachheit, keine DependenciesRedis, PostgreSQL
HTTP/RESTAPI-ExpositionUniversal kompatibelgRPC
SSEStreaming-ResponsesBrowser-kompatibelWebSockets

Modulabhängigkeiten und Schnittstellen

正在加载图表渲染器...

Abhängigkeits-Erklärungspunkte:

  1. Agent → Model: Agenten benötigen das LLM-Interface für Content-Generierung, sind aber nicht an eine spezifische Implementierung gebunden.

  2. Runner → Agent + Session: Der Runner orchestriert beide und stellt die Verbindung zwischen Ausführung und Persistenz her.

  3. Context-Hierarchie: Die internen Context-Implementierungen hängen von der Agent-Schnittstelle ab, nicht umgekehrt.

  4. 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:

go
1runner, 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:

go
1type 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:

go
1agent, 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: