TL;DR
ADK (Agent Development Kit) is the highest-level abstraction in the Eino framework, providing ready-to-use patterns for building AI agents. This article starts with the simplest ChatModelAgent, progressively covers the Tool Use loop, DeepAgent's deep reasoning capabilities, interrupt/resume mechanisms, and state management. A complete data analysis agent example demonstrates how to build production-grade agents in Go.
Table of Contents
- Key Takeaways
- What is ADK
- ChatModelAgent Deep Dive
- Tool Use Complete Flow
- DeepAgent: Deep Reasoning
- Human-in-the-Loop: Interrupt & Resume
- Agent State Management
- Built-in Agent Patterns
- Practice: Build a Data Analysis Agent
- Best Practices
- FAQ
- Summary & Resources
Key Takeaways
- ADK positioning: High-level Agent abstraction on top of Eino's components and orchestration layers, encapsulating Tool Use loops, conversation management, and state persistence
- ChatModelAgent: The simplest Agent pattern — one ChatModel + a set of Tools, automatically handling multi-turn tool calls
- DeepAgent: Supports planning, reflection, and subtask decomposition for complex multi-step tasks
- Interrupt/Resume: Native Human-in-the-Loop support — agents can pause at any step for human review
- State Management: Built-in conversation history maintenance and checkpoint mechanism for cross-session persistence
What is ADK
ADK (Agent Development Kit) is the third capability layer in the Eino framework, sitting above Components and Composition:
The core problem ADK solves is: elevating LLM capabilities from "one-shot Q&A" to "continuous reasoning and action". It encapsulates these key capabilities:
- Tool Use loop: Automatically executes multi-turn LLM ↔ Tool interactions until the task is complete
- Conversation memory: Automatically maintains multi-turn dialogue context history
- State management: Supports checkpoint serialization for interrupt-and-resume workflows
- Built-in patterns: Provides ChatModelAgent, DeepAgent, and other ready-to-use Agent patterns
ChatModelAgent Deep Dive
ChatModelAgent is the simplest and most commonly used Agent pattern in ADK. Its core composition is one ChatModel plus a set of Tools:
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
func main() {
ctx := context.Background()
// Initialize ChatModel (OpenAI example)
chatModel := initOpenAIChatModel()
// Define tools
searchTool := createSearchTool()
calculatorTool := createCalculatorTool()
// Create ChatModelAgent
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: chatModel,
Tools: []tool.BaseTool{searchTool, calculatorTool},
SystemPrompt: "You are a helpful assistant that can search for information and perform calculations.",
MaxSteps: 10,
})
if err != nil {
panic(err)
}
// Run the Agent
result, err := agent.Run(ctx, "What's the temperature in Beijing today? Convert it to Fahrenheit.")
if err != nil {
panic(err)
}
fmt.Println(result.Content)
}
Configuration parameters:
| Parameter | Type | Description |
|---|---|---|
Model |
ChatModel |
The underlying LLM instance |
Tools |
[]tool.BaseTool |
List of available tools |
SystemPrompt |
string |
System prompt defining agent behavior |
MaxSteps |
int |
Maximum reasoning steps to prevent infinite loops |
Tool Use Complete Flow
Tool Use is the core execution mechanism of an Agent. Here's the complete interaction flow:
Step-by-step breakdown:
- User input: The user sends a message to the Agent
- LLM decision: The Agent's internal LLM decides the next action based on the system prompt and conversation history
- Tool call: If the LLM returns a
tool_call, the framework automatically parses and executes the corresponding tool - Result feedback: The tool execution result is fed back to the LLM as a
tool_resultmessage - Loop evaluation: The LLM may call another tool or generate the final response
- Termination: The loop ends when MaxSteps is reached or the LLM returns a message without
tool_call
// Custom tool implementation example
func createSearchTool() tool.BaseTool {
return tool.NewSimpleTool(
"web_search",
"Search the internet for real-time information",
func(ctx context.Context, params map[string]interface{}) (string, error) {
query := params["query"].(string)
// Call search API
results, err := searchAPI.Search(ctx, query)
if err != nil {
return "", err
}
return formatResults(results), nil
},
tool.WithStringParam("query", "Search keywords", true),
)
}
DeepAgent: Deep Reasoning
DeepAgent adds planning, reflection, and subtask decomposition capabilities on top of ChatModelAgent, making it suitable for complex multi-step tasks:
deepAgent, err := adk.NewDeepAgent(ctx, &adk.DeepAgentConfig{
Model: chatModel,
Tools: tools,
SystemPrompt: "You are a data analysis expert skilled at breaking down complex problems and solving them step by step.",
MaxSteps: 25,
Planning: true, // Enable task planning
Reflection: true, // Enable execution reflection
})
DeepAgent reasoning flow:
- Task understanding: Analyze the user request, identify goals and constraints
- Plan creation: Decompose the complex task into an ordered list of subtasks
- Step-by-step execution: Execute subtasks sequentially, each step may invoke tools
- Intermediate reflection: Check if intermediate results meet expectations, adjust plan if necessary
- Result synthesis: Aggregate all subtask results into the final response
Human-in-the-Loop: Interrupt & Resume
ADK natively supports Human-in-the-Loop (HITL) mode, allowing agents to pause execution at critical steps and wait for human review before resuming:
// Register a tool call interceptor
agent.OnToolCall(func(ctx context.Context, call *schema.ToolCall) (*schema.ToolResult, error) {
// Intercept sensitive operations (e.g., sending emails)
if call.Name == "send_email" {
recipient := call.Args["to"].(string)
subject := call.Args["subject"].(string)
// Return Interrupt to pause Agent execution
return nil, adk.Interrupt(fmt.Sprintf(
"Please confirm sending email:\nTo: %s\nSubject: %s",
recipient, subject,
))
}
return nil, nil // Other tools proceed normally
})
// Run the Agent (may interrupt at some step)
result, err := agent.Run(ctx, "Send a meeting notification email to John")
if errors.Is(err, adk.ErrInterrupted) {
// Agent has been interrupted, get Checkpoint
checkpoint := agent.GetCheckpoint()
// Persist checkpoint (e.g., store in Redis)
saveCheckpoint(checkpoint)
// Show interrupt message to user
fmt.Println("Awaiting review:", checkpoint.InterruptMessage)
}
// --- After human review ---
// Load checkpoint
checkpoint := loadCheckpoint()
// Construct human-approved result
approvedResult := &schema.ToolResult{
Name: "send_email",
Content: "Email confirmed and sent",
}
// Resume Agent execution
result, err := agent.Resume(ctx, checkpoint, approvedResult)
fmt.Println(result.Content) // "Email has been successfully sent to John"
Typical interrupt/resume scenarios:
- Sensitive operation confirmation (sending emails, transfers, data deletion)
- Intermediate result review (report generation, data modifications)
- Multi-phase long-running tasks (requiring human input to continue)
Agent State Management
The Agent maintains the following state information during execution:
// Checkpoint structure overview
type AgentCheckpoint struct {
ConversationHistory []schema.Message // Complete conversation history
CurrentStep int // Current execution step
PendingToolCalls []schema.ToolCall // Pending tool calls
InterruptMessage string // Interrupt reason
Metadata map[string]any // Custom metadata
}
// State persistence example
func saveCheckpoint(cp *adk.AgentCheckpoint) error {
data, err := json.Marshal(cp)
if err != nil {
return err
}
return redis.Set(ctx, "agent:checkpoint:"+sessionID, data, 24*time.Hour).Err()
}
// State restoration
func loadCheckpoint(sessionID string) (*adk.AgentCheckpoint, error) {
data, err := redis.Get(ctx, "agent:checkpoint:"+sessionID).Bytes()
if err != nil {
return nil, err
}
var cp adk.AgentCheckpoint
return &cp, json.Unmarshal(data, &cp)
}
State management best practices:
- Conversation history is maintained automatically — no need to manually append messages
- Checkpoints support JSON serialization and can be stored in any persistence layer
- Set a TTL on checkpoints to prevent long-term accumulation
- Inject business context via Metadata (e.g., user ID, session ID)
Built-in Agent Patterns
| Pattern | Use Case | Reasoning Capability | Complexity | Tool Use |
|---|---|---|---|---|
| ChatModelAgent | Simple tool calls, Q&A enhancement | Single-step reasoning | Low | Supported |
| DeepAgent | Complex analysis, research | Multi-step reasoning + reflection | Medium | Supported |
| Multi-Agent | Multi-role collaboration, task division | Coordinated reasoning | High | Supported |
Selection guidance:
- ChatModelAgent handles 80% of scenarios
- Upgrade to DeepAgent when you need task decomposition and reflection
- Use Multi-Agent when multiple specialized roles need to collaborate (see next article)
Practice: Build a Data Analysis Agent
Here's a complete data analysis Agent example integrating database queries, JSON processing, and report generation:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/model/openai"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
// Database query tool
func createQueryTool() tool.BaseTool {
return tool.NewSimpleTool(
"query_database",
"Execute SQL queries to retrieve data",
func(ctx context.Context, params map[string]interface{}) (string, error) {
sql := params["sql"].(string)
rows, err := db.QueryContext(ctx, sql)
if err != nil {
return "", fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
results := scanRows(rows)
data, _ := json.Marshal(results)
return string(data), nil
},
tool.WithStringParam("sql", "SQL query statement", true),
)
}
// Data analysis tool
func createAnalysisTool() tool.BaseTool {
return tool.NewSimpleTool(
"analyze_data",
"Perform statistical analysis on data (mean, median, trend, etc.)",
func(ctx context.Context, params map[string]interface{}) (string, error) {
data := params["data"].(string)
method := params["method"].(string)
result := performAnalysis(data, method)
return result, nil
},
tool.WithStringParam("data", "Data in JSON format", true),
tool.WithStringParam("method", "Analysis method: mean/median/trend/distribution", true),
)
}
// Report generation tool
func createReportTool() tool.BaseTool {
return tool.NewSimpleTool(
"generate_report",
"Generate a Markdown-formatted report from analysis results",
func(ctx context.Context, params map[string]interface{}) (string, error) {
findings := params["findings"].(string)
title := params["title"].(string)
report := fmt.Sprintf("# %s\n\n%s\n", title, findings)
return report, nil
},
tool.WithStringParam("title", "Report title", true),
tool.WithStringParam("findings", "Analysis findings content", true),
)
}
func main() {
ctx := context.Background()
// Initialize model
chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
Model: "gpt-4o",
APIKey: os.Getenv("OPENAI_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
// Create data analysis Agent
agent, err := adk.NewDeepAgent(ctx, &adk.DeepAgentConfig{
Model: chatModel,
Tools: []tool.BaseTool{
createQueryTool(),
createAnalysisTool(),
createReportTool(),
},
SystemPrompt: `You are a professional data analyst. Your workflow is:
1. Understand the user's analysis requirements
2. Write SQL queries to fetch data
3. Perform statistical analysis on the data
4. Generate a structured analysis report
Note: SQL queries must include a LIMIT clause to prevent excessive data retrieval.`,
MaxSteps: 20,
Planning: true,
Reflection: true,
})
if err != nil {
log.Fatal(err)
}
// Add sensitive operation interception
agent.OnToolCall(func(ctx context.Context, call *schema.ToolCall) (*schema.ToolResult, error) {
if call.Name == "query_database" {
sql := call.Args["sql"].(string)
if containsDangerousSQL(sql) {
return nil, adk.Interrupt("Dangerous SQL detected, please review: " + sql)
}
}
return nil, nil
})
// Execute analysis task
result, err := agent.Run(ctx, "Analyze user registration trends over the past 30 days, with daily stats and anomaly detection")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Content)
}
func containsDangerousSQL(sql string) bool {
dangerousKeywords := []string{"DROP", "DELETE", "UPDATE", "TRUNCATE", "ALTER"}
upperSQL := strings.ToUpper(sql)
for _, keyword := range dangerousKeywords {
if strings.Contains(upperSQL, keyword) {
return true
}
}
return false
}
This example demonstrates how to combine JSON data structures with an Agent tool chain to build an end-to-end data analysis pipeline.
Best Practices
Tool design principles:
- Each tool should have a single responsibility with a clear, accurate description (LLMs rely on descriptions to decide when to call tools)
- Tool return values should use structured formats (JSON) for easy LLM parsing
- Add context timeout controls for long-running tools
Agent configuration recommendations:
- SystemPrompt should clearly specify the agent's role, capability boundaries, and workflow
- Set MaxSteps based on task complexity (simple: 5-10, complex: 15-25)
- Production environments must configure Callbacks to monitor per-step execution time and token consumption
Error handling:
- Return meaningful error messages when tool execution fails (LLMs can use these to adjust strategy)
- Add a global timeout to the Agent to prevent infinite execution under abnormal conditions
- Use Eino's callback system to record execution traces
FAQ
Q: What's the difference between ADK and using Eino components directly?
ADK is a high-level abstraction built on top of Eino's components and orchestration layers. Using components directly requires you to manually handle the Tool Use loop, conversation history management, and error retries. ADK encapsulates all of this into ready-to-use Agent patterns so you can focus on business logic rather than infrastructure.
Q: Should I use ChatModelAgent or DeepAgent?
ChatModelAgent is ideal for simple tool-calling scenarios (search, calculation, API queries). DeepAgent is better for tasks requiring multi-step reasoning, task decomposition, and reflection (data analysis, report generation, research). Choose based on the reasoning depth your task requires.
Q: How do I persist the interrupt/resume checkpoint in production?
Checkpoint data can be serialized to JSON and stored in Redis, a database, or a file system. When resuming, deserialize the checkpoint and pass it to agent.Resume(). We recommend setting a TTL on checkpoints to prevent indefinite accumulation.
Q: What's the recommended MaxSteps setting?
For simple tool-calling scenarios, 5-10 steps is recommended. For complex multi-tool workflows, 15-25 steps works well. Too low and the agent can't complete tasks; too high risks infinite loops burning tokens. Monitor actual step distributions via Callbacks and tune accordingly.
Q: Does Eino ADK support streaming output?
Yes. Both ChatModelAgent and DeepAgent support streaming via the StreamRun method. Combined with Eino's Callback system, you can track each reasoning step in real-time. See the previous article on streaming and callbacks in this series for details.
Summary & Resources
ADK is the critical layer in the Eino framework that transforms LLM capabilities into real-world action. Build tool-enhanced agents quickly with ChatModelAgent, handle complex reasoning tasks with DeepAgent, and implement safe human-agent collaboration with the interrupt/resume mechanism — this combination covers the vast majority of production agent scenarios.
Next up: Eino Multi-Agent Coordination Patterns will dive deep into how multiple agents can collaborate to accomplish even more complex tasks.
Related Resources:
- Eino GitHub Repository
- Eino Streaming and Callback System (previous in series)
- AI Agent Glossary
- Agent Memory Explained
- JSON Formatter Tool — Debug agent tool return values
- JSON to Go Struct — Quickly generate tool parameter type definitions