LLM Agents in Google ADK: The Four Knobs You Actually Tune

description, instruction, generation params, and structured output — the dials on almost every agent you'll build

The smallest agent in Google’s Agent Development Kit is a name, a model, and one line of instruction. That gets you talking. But the moment you have more than one agent, or you need the output to be machine-readable instead of chatty prose, you start reaching for four fields that appear on nearly every LlmAgent you write: description, instruction, the generation parameters, and an output schema. This post walks each one, side by side in Python and Go, and lands on the sharpest contrast between the two SDKs: how they force typed JSON out of a model.

Knob 1 — description: how a parent decides to delegate

description is not the system prompt. It’s a one-liner written for other agents to read. When you build a multi-agent tree (a later concept), a parent looks at each child’s description to decide whether to route a request there. So write it as a routing hint, not as behavior:

description="Provides structured facts (capital, currency, population, languages) about a country."

If a top-level agent is the only one a human talks to, the description does nothing — but the habit of writing a crisp, capability-focused line pays off the instant you add a coordinator above it.

Knob 2 — instruction: the system prompt, with {state} templating

The instruction is the actual behavioral prompt. The feature worth internalizing early is that ADK treats it as a template: any {placeholder} is substituted from session state at run time.

instruction="You are helping {user_name}. Their preferred units are {units?}."

{user_name} is pulled from session state; {units?} — note the ? — is optional, so the substitution silently no-ops if the key is absent. This is how you personalize a static prompt without doing string concatenation in your own code. Both SDKs share the {key} / {key?} convention. Go adds one extra escape hatch: an InstructionProvider — a function that builds the prompt in code when {} templating isn’t expressive enough. Python’s equivalent is passing a callable as the instruction.

Knob 3 — generation params: temperature, tokens, top-p

Generation config controls the sampling behavior of the underlying model. For extraction and structured tasks you almost always want temperature low (0–0.2) so the output is consistent run to run.

from google.genai import types

generate_content_config=types.GenerateContentConfig(
    temperature=0.1,
    max_output_tokens=512,
    top_p=0.9,
)

Go carries the same fields, with one idiom difference that trips people up: temperature is a pointer (*float32), not a plain value. The API needs to distinguish “unset” from “explicitly zero”, and Go has no None, so ADK uses a pointer and gives you the genai.Ptr helper:

GenerateContentConfig: &genai.GenerateContentConfig{
    Temperature:     genai.Ptr(float32(0.1)),
    MaxOutputTokens: 512,
    TopP:            genai.Ptr(float32(0.9)),
},

Python takes the bare temperature=0.1 because None already carries the “unset” meaning. max_output_tokens is a plain int in both.

Knob 4 — structured output: force typed JSON

This is the reason most people reach past the smallest agent. Instead of parsing prose with regex, you declare the exact shape you want and the model is constrained to emit conforming JSON. This is where the two SDKs diverge the most.

Python — declare a Pydantic model and hand it over. ADK converts it to a schema for you, and you get a validating class for free:

from pydantic import BaseModel, Field

class CountryInfo(BaseModel):
    capital: str = Field(description="The capital city.")
    currency: str = Field(description="The official currency name.")
    population_millions: float = Field(description="Approximate population in millions.")
    languages: list[str] = Field(description="Official language(s).")

root_agent = LlmAgent(
    name="country_info_agent",
    model="gemini-flash-latest",
    description="Provides structured facts about a country.",
    instruction="You are a geography expert. Return the capital, currency, "
                "approximate population in millions, and official language(s).",
    generate_content_config=types.GenerateContentConfig(temperature=0.1),
    output_schema=CountryInfo,
    output_key="country_info",   # writes the result into session.state["country_info"]
)

Go — there’s no Pydantic, so you build the genai.Schema explicitly. More verbose, but dependency-free and the shape is right there in front of you:

func countrySchema() *genai.Schema {
    return &genai.Schema{
        Type: genai.TypeObject,
        Properties: map[string]*genai.Schema{
            "capital":            {Type: genai.TypeString},
            "currency":           {Type: genai.TypeString},
            "populationMillions": {Type: genai.TypeNumber},
            "languages":          {Type: genai.TypeArray, Items: &genai.Schema{Type: genai.TypeString}},
        },
        Required: []string{"capital", "currency", "populationMillions", "languages"},
    }
}

// llmagent.New(llmagent.Config{ ... OutputSchema: countrySchema(), OutputKey: "country_info" })

Watch the naming: Python’s population_millions (snake_case) vs. the JSON populationMillions (camelCase). Pick a convention and make your instruction tell the model the same one.

The output_key in both examples is the quiet workhorse: it stashes the final structured result into session.state["country_info"], which is how one agent hands data to the next in a pipeline.

Mental model: the schema is a contract on the final output

Setting an output schema guarantees the response parses into your type. But the two SDKs disagree on what an output-schema agent may also do — and this is not a universal rule, so don’t assume:

When you need both structure and tools on Go, the pattern is to split the work: one agent does the tool-calling, a second formats its result against the schema — the subject of the next concept.

Two bonus knobs worth knowing

Two more fields shape how the agent is fed, and they pair naturally with the four above:

The takeaway

Four knobs cover most of what you’ll tune: description for delegation, instruction for behavior (templated from state), generation params for sampling, and an output schema for typed results. The one place to slow down is structured output — Python’s Pydantic-to-schema convenience and Go’s explicit genai.Schema produce the same guarantee, but Go additionally forbids tools on a schema’d agent. Know that difference and you’ll design your agent boundaries correctly from the start.

Next in the series: composing these agents into Sequential, Parallel, and Loop workflows.

Sources & References

Configuring LLM agents in ADK
The Go SDK (google.golang.org/adk/v2)