The Loop workflow is the core pattern in an Agent Team for handling iterative optimization tasks. Unlike Sequential and Parallel workflows, which execute once, the Loop workflow uses a loop-evaluate-optimize closed-loop mechanism that lets an Agent autonomously improve output quality. However, looping brings risks—infinite loops, convergence oscillation, and state bloat—which require rigorous engineering controls. This post dives into the mathematical model, termination theory, state evolution, and production practice of Loop workflows.
Mathematical Model of Iterative Optimization
Convergence Analysis
A Loop workflow can be formalized as an iterative function system:
x_{n+1} = f(x_n, e_n)
Where:
x_n: output of the nth iterationf: Agent improvement functione_n: evaluation feedback of the nth iteration
Convergence condition: when there exists a quality metric Q(x) such that Q(x_{n+1}) >= Q(x_n) holds for all n, and Q(x) has an upper bound, then the sequence {x_n} must converge.
// Convergence analyzer
type ConvergenceAnalyzer struct {
qualityHistory []float64
windowSize int
threshold float64
}
func (ca *ConvergenceAnalyzer) IsConverged() (bool, float64) {
if len(ca.qualityHistory) < ca.windowSize {
return false, 0
}
// Take the most recent windowSize quality values
recent := ca.qualityHistory[len(ca.qualityHistory)-ca.windowSize:]
// Calculate variance
mean := calculateMean(recent)
variance := calculateVariance(recent, mean)
// Variance below threshold means converged
converged := variance < ca.threshold
return converged, variance
}
func (ca *ConvergenceAnalyzer) DetectOscillation() bool {
if len(ca.qualityHistory) < 4 {
return false
}
// Detect oscillation: quality values repeatedly rise and fall
last4 := ca.qualityHistory[len(ca.qualityHistory)-4:]
upDown := (last4[1] > last4[0]) && (last4[2] < last4[1]) && (last4[3] > last4[2])
downUp := (last4[1] < last4[0]) && (last4[2] > last4[1]) && (last4[3] < last4[2])
return upDown || downUp
}
Quality Metric Design
// Multi-dimensional quality evaluation system
type QualityMetrics struct {
// Content quality (0-1)
Relevance float64 // relevance
Coherence float64 // coherence
Completeness float64 // completeness
Accuracy float64 // accuracy
// Structural quality (0-1)
Structure float64 // structural soundness
Formatting float64 // formatting compliance
// Language quality (0-1)
Grammar float64 // grammatical correctness
Style float64 // style consistency
// Business quality (0-1)
Requirements float64 // requirement satisfaction
Constraints float64 // constraint satisfaction
}
func (qm *QualityMetrics) OverallScore() float64 {
// Weighted combined score
weights := map[string]float64{
"Relevance": 0.20,
"Coherence": 0.15,
"Completeness": 0.15,
"Accuracy": 0.20,
"Structure": 0.10,
"Formatting": 0.05,
"Grammar": 0.05,
"Style": 0.05,
"Requirements": 0.03,
"Constraints": 0.02,
}
score := 0.0
score += weights["Relevance"] * qm.Relevance
score += weights["Coherence"] * qm.Coherence
score += weights["Completeness"] * qm.Completeness
score += weights["Accuracy"] * qm.Accuracy
score += weights["Structure"] * qm.Structure
score += weights["Formatting"] * qm.Formatting
score += weights["Grammar"] * qm.Grammar
score += weights["Style"] * qm.Style
score += weights["Requirements"] * qm.Requirements
score += weights["Constraints"] * qm.Constraints
return score
}
// Evaluate quality via LLM
func evaluateWithLLM(ctx context.Context, model agent.Model, content string, criteria []string) (*QualityMetrics, error) {
prompt := fmt.Sprintf(`Please evaluate the quality of the following content, scoring each dimension (0-1):
%s
Evaluation dimensions:
%s
Please output the scores in JSON format.`, content, strings.Join(criteria, "\n"))
response, err := model.GenerateContent(ctx, prompt)
if err != nil {
return nil, err
}
var metrics QualityMetrics
if err := json.Unmarshal([]byte(response), &metrics); err != nil {
return nil, err
}
return &metrics, nil
}
Loop Workflow Architecture Design
Core Components
type LoopWorkflow struct {
agent *agent.Agent // iteration Agent
evaluator Evaluator // quality evaluator
exitCondition ExitCondition // exit condition
maxIterations int // maximum iterations
minIterations int // minimum iterations
convergenceCfg *ConvergenceConfig // convergence config
stateManager StateManager // state manager
feedbackBuilder FeedbackBuilder // feedback builder
strategy IterationStrategy // iteration strategy
}
// Evaluator interface
type Evaluator interface {
Evaluate(ctx context.Context, output string, target string) (*EvaluationResult, error)
}
// Exit condition interface
type ExitCondition interface {
ShouldExit(state *LoopState) (bool, string)
}
// Iteration state
type LoopState struct {
Iteration int // current iteration count
CurrentOutput string // current output
PreviousOutput string // previous output
QualityHistory []float64 // quality history
BestOutput string // best output
BestQuality float64 // best quality
Feedback string // current feedback
Context map[string]interface{} // context state
StartTime time.Time // start time
TokenUsed int // token consumption
}
// Evaluation result
type EvaluationResult struct {
Quality float64 // quality score
Feedback string // improvement feedback
Metrics *QualityMetrics // detailed metrics
Passed bool // passed
Criteria map[string]bool // per-criterion pass status
}
Execution Engine
func (lw *LoopWorkflow) Execute(ctx context.Context, input string) (*LoopResult, error) {
// 1. Initialize state
state := &LoopState{
Iteration: 0,
Context: make(map[string]interface{}),
StartTime: time.Now(),
BestQuality: -1,
}
state.Context["original_input"] = input
// 2. Initial execution
output, err := lw.agent.Run(ctx, input)
if err != nil {
return nil, fmt.Errorf("initial execution failed: %w", err)
}
state.CurrentOutput = output
state.BestOutput = output
// 3. Evaluate initial output
eval, err := lw.evaluator.Evaluate(ctx, output, input)
if err != nil {
return nil, fmt.Errorf("initial evaluation failed: %w", err)
}
state.QualityHistory = append(state.QualityHistory, eval.Quality)
state.BestQuality = eval.Quality
state.Feedback = eval.Feedback
// 4. Iterative optimization loop
for {
state.Iteration++
// Check exit condition
shouldExit, reason := lw.exitCondition.ShouldExit(state)
if shouldExit {
return lw.buildResult(state, reason), nil
}
// Check max iterations
if state.Iteration >= lw.maxIterations {
return lw.buildResult(state, "max_iterations_reached"), nil
}
// Build iteration input
iterationInput := lw.feedbackBuilder.Build(state, eval)
// Execute iteration
newOutput, err := lw.agent.Run(ctx, iterationInput)
if err != nil {
// Iteration failed, use best historical result
log.Printf("Iteration %d failed: %v", state.Iteration, err)
return lw.buildResult(state, "iteration_failed"), nil
}
state.PreviousOutput = state.CurrentOutput
state.CurrentOutput = newOutput
// Evaluate new output
eval, err = lw.evaluator.Evaluate(ctx, newOutput, input)
if err != nil {
log.Printf("Evaluation failed at iteration %d: %v", state.Iteration, err)
continue
}
state.QualityHistory = append(state.QualityHistory, eval.Quality)
state.Feedback = eval.Feedback
state.TokenUsed += estimateTokens(newOutput)
// Update best result
if eval.Quality > state.BestQuality {
state.BestQuality = eval.Quality
state.BestOutput = newOutput
}
// Detect convergence
if lw.convergenceCfg != nil {
analyzer := &ConvergenceAnalyzer{
qualityHistory: state.QualityHistory,
windowSize: lw.convergenceCfg.WindowSize,
threshold: lw.convergenceCfg.Threshold,
}
if converged, variance := analyzer.IsConverged(); converged {
state.Context["convergence_variance"] = variance
return lw.buildResult(state, "converged"), nil
}
if analyzer.DetectOscillation() {
return lw.buildResult(state, "oscillation_detected"), nil
}
}
// Check token budget
if lw.tokenBudget != nil && !lw.tokenBudget.CanAllocate(1000) {
return lw.buildResult(state, "token_budget_exhausted"), nil
}
}
}
func (lw *LoopWorkflow) buildResult(state *LoopState, reason string) *LoopResult {
return &LoopResult{
Output: state.BestOutput,
Quality: state.BestQuality,
Iterations: state.Iteration,
ExitReason: reason,
QualityHistory: state.QualityHistory,
TokenUsed: state.TokenUsed,
Duration: time.Since(state.StartTime),
}
}
Termination Condition Design
Composite Exit Conditions
// Combine multiple exit conditions
type CompositeExitCondition struct {
conditions []ExitCondition
mode CompositeMode // ANY | ALL
}
type CompositeMode int
const (
ModeAny CompositeMode = iota // exit when any condition is met
ModeAll // exit only when all conditions are met
)
func (c *CompositeExitCondition) ShouldExit(state *LoopState) (bool, string) {
if c.mode == ModeAny {
for _, cond := range c.conditions {
if exit, reason := cond.ShouldExit(state); exit {
return true, reason
}
}
return false, ""
}
// ModeAll
reasons := make([]string, 0)
for _, cond := range c.conditions {
exit, reason := cond.ShouldExit(state)
if !exit {
return false, ""
}
reasons = append(reasons, reason)
}
return true, strings.Join(reasons, " + ")
}
// Concrete exit condition implementations
// 1. Quality threshold condition
type QualityThresholdCondition struct {
Threshold float64
}
func (c *QualityThresholdCondition) ShouldExit(state *LoopState) (bool, string) {
if state.BestQuality >= c.Threshold {
return true, fmt.Sprintf("quality_threshold_reached: %.3f >= %.3f",
state.BestQuality, c.Threshold)
}
return false, ""
}
// 2. Maximum iteration condition
type MaxIterationCondition struct {
MaxIterations int
}
func (c *MaxIterationCondition) ShouldExit(state *LoopState) (bool, string) {
if state.Iteration >= c.MaxIterations {
return true, fmt.Sprintf("max_iterations: %d", c.MaxIterations)
}
return false, ""
}
// 3. Improvement stall condition
type ImprovementStallCondition struct {
WindowSize int
MinImprovement float64
}
func (c *ImprovementStallCondition) ShouldExit(state *LoopState) (bool, string) {
if len(state.QualityHistory) < c.WindowSize+1 {
return false, ""
}
recent := state.QualityHistory[len(state.QualityHistory)-c.WindowSize:]
best := state.QualityHistory[len(state.QualityHistory)-c.WindowSize-1]
for _, q := range recent {
if q-best >= c.MinImprovement {
return false, ""
}
}
return true, fmt.Sprintf("improvement_stalled: no improvement > %.3f in last %d iterations",
c.MinImprovement, c.WindowSize)
}
// 4. Time budget condition
type TimeBudgetCondition struct {
MaxDuration time.Duration
}
func (c *TimeBudgetCondition) ShouldExit(state *LoopState) (bool, string) {
if time.Since(state.StartTime) >= c.MaxDuration {
return true, fmt.Sprintf("time_budget_exhausted: %v", c.MaxDuration)
}
return false, ""
}
// 5. Quality regression condition (prevent over-optimization)
type QualityRegressionCondition struct {
Threshold float64
}
func (c *QualityRegressionCondition) ShouldExit(state *LoopState) (bool, string) {
if len(state.QualityHistory) < 2 {
return false, ""
}
last := state.QualityHistory[len(state.QualityHistory)-1]
prev := state.QualityHistory[len(state.QualityHistory)-2]
if prev-last > c.Threshold {
return true, fmt.Sprintf("quality_regression: %.3f -> %.3f", prev, last)
}
return false, ""
}
Iteration Strategy Design
Feedback Building Strategy
// Feedback builder interface
type FeedbackBuilder interface {
Build(state *LoopState, eval *EvaluationResult) string
}
// Detailed feedback builder
type DetailedFeedbackBuilder struct{}
func (b *DetailedFeedbackBuilder) Build(state *LoopState, eval *EvaluationResult) string {
var feedback strings.Builder
feedback.WriteString(fmt.Sprintf("Original requirement: %s\n\n", state.Context["original_input"]))
feedback.WriteString(fmt.Sprintf("Current iteration: %d\n", state.Iteration))
feedback.WriteString(fmt.Sprintf("Current quality score: %.3f\n", eval.Quality))
feedback.WriteString(fmt.Sprintf("Historical best: %.3f\n\n", state.BestQuality))
feedback.WriteString("Evaluation feedback:\n")
feedback.WriteString(eval.Feedback)
feedback.WriteString("\n\n")
// Add concrete improvement suggestions
feedback.WriteString("Aspects needing improvement:\n")
for criterion, passed := range eval.Criteria {
if !passed {
feedback.WriteString(fmt.Sprintf("- %s: not met\n", criterion))
}
}
feedback.WriteString("\nPlease improve the content based on the above feedback and output the optimized version.")
return feedback.String()
}
// Diff feedback builder (provides before-and-after comparison)
type DiffFeedbackBuilder struct{}
func (b *DiffFeedbackBuilder) Build(state *LoopState, eval *EvaluationResult) string {
var feedback strings.Builder
feedback.WriteString("Please optimize the following content.\n\n")
feedback.WriteString("[Current Version]\n")
feedback.WriteString(state.CurrentOutput)
feedback.WriteString("\n\n")
if state.PreviousOutput != "" {
feedback.WriteString("[Previous Version]\n")
feedback.WriteString(state.PreviousOutput)
feedback.WriteString("\n\n")
feedback.WriteString(fmt.Sprintf("Quality change: %.3f -> %.3f\n",
state.QualityHistory[len(state.QualityHistory)-2], eval.Quality))
}
feedback.WriteString("\nImprovement direction: ")
feedback.WriteString(eval.Feedback)
return feedback.String()
}
Adaptive Iteration Strategy
// Dynamically adjust iteration strategy based on current state
type AdaptiveIterationStrategy struct {
baseStrategy IterationStrategy
qualityTargets []float64 // stage-based quality targets
currentPhase int
}
func (s *AdaptiveIterationStrategy) GetNextInput(state *LoopState, eval *EvaluationResult) string {
// Select stage based on current quality
for i, target := range s.qualityTargets {
if state.BestQuality < target {
s.currentPhase = i
break
}
}
// Different strategies for different stages
switch s.currentPhase {
case 0:
// Phase 1: focus on structure and completeness
return s.buildStructureFocusPrompt(state, eval)
case 1:
// Phase 2: focus on content and accuracy
return s.buildContentFocusPrompt(state, eval)
case 2:
// Phase 3: focus on language and style
return s.buildStyleFocusPrompt(state, eval)
default:
return s.baseStrategy.GetNextInput(state, eval)
}
}
func (s *AdaptiveIterationStrategy) buildStructureFocusPrompt(state *LoopState, eval *EvaluationResult) string {
return fmt.Sprintf(`Current quality: %.3f, focus on structure optimization.
Current content:
%s
Please optimize the following:
1. Ensure clear headings and paragraph structure.
2. Add appropriate subheadings.
3. Ensure logical flow is smooth.
4. Check the completeness of the beginning and end.
Output the complete optimized version.`, state.BestQuality, state.CurrentOutput)
}
Hands-On Scenario: Copywriting Multi-Round Optimization System
Full Implementation
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/google/adk-go/agent"
"github.com/google/adk-go/team"
)
// CopywritingOptimizer copywriting optimization system
type CopywritingOptimizer struct {
workflow *team.LoopWorkflow
}
func NewCopywritingOptimizer(model agent.Model) (*CopywritingOptimizer, error) {
// Create the writing Agent
writerAgent, err := agent.New(agent.Config{
Name: "copywriter",
Model: model,
Instruction: `You are a senior copywriter. Create or optimize copy based on requirements and feedback.
Requirements:
1. Lively and infectious language.
2. Highlight core selling points.
3. Match the target audience's language habits.
4. Keep within the requested word count.`,
Timeout: 30 * time.Second,
})
if err != nil {
return nil, err
}
// Build evaluator
evaluator := &CopywritingEvaluator{
model: model,
criteria: []EvaluationCriterion{
{Name: "Appeal", Weight: 0.25, Prompt: "Evaluate the copy's appeal and hook strength"},
{Name: "Clarity", Weight: 0.20, Prompt: "Evaluate the clarity of message delivery"},
{Name: "Persuasion", Weight: 0.25, Prompt: "Evaluate persuasion and conversion potential"},
{Name: "BrandTone", Weight: 0.15, Prompt: "Evaluate consistency with brand tone"},
{Name: "Creativity", Weight: 0.15, Prompt: "Evaluate creativity and differentiation"},
},
}
// Build composite exit condition
exitCondition := team.NewCompositeExitCondition(team.ModeAny,
&team.QualityThresholdCondition{Threshold: 0.85}, // quality threshold reached
&team.MaxIterationCondition{MaxIterations: 5}, // max 5 rounds
&team.ImprovementStallCondition{ // improvement stalled
WindowSize: 2,
MinImprovement: 0.05,
},
&team.TimeBudgetCondition{MaxDuration: 2 * time.Minute}, // time budget
&team.QualityRegressionCondition{Threshold: 0.10}, // quality regression
)
// Build workflow
workflow := team.NewLoopWorkflow(
team.WithAgent(writerAgent),
team.WithEvaluator(evaluator),
team.WithExitCondition(exitCondition),
team.WithFeedbackBuilder(&team.DiffFeedbackBuilder{}),
team.WithConvergenceConfig(&team.ConvergenceConfig{
WindowSize: 3,
Threshold: 0.01,
}),
team.WithTokenBudget(30000),
)
return &CopywritingOptimizer{workflow: workflow}, nil
}
// CopywritingEvaluator copywriting evaluator
type CopywritingEvaluator struct {
model agent.Model
criteria []EvaluationCriterion
}
type EvaluationCriterion struct {
Name string
Weight float64
Prompt string
}
func (e *CopywritingEvaluator) Evaluate(ctx context.Context, output string, target string) (*team.EvaluationResult, error) {
// Build evaluation prompt
prompt := fmt.Sprintf(`Please evaluate the following copy, scoring each dimension (0-1, 3 decimal places).
Target: %s
Copy content:
%s
Evaluation dimensions:
`, target, output)
for _, c := range e.criteria {
prompt += fmt.Sprintf("- %s (weight %.0f%%): %s\n", c.Name, c.Weight*100, c.Prompt)
}
prompt += `
Please output in JSON format:
{
"scores": {"dimension name": score},
"overall": overall score,
"feedback": "specific improvement suggestions",
"passed": true/false
}`
response, err := e.model.GenerateContent(ctx, prompt)
if err != nil {
return nil, fmt.Errorf("evaluation failed: %w", err)
}
var evalData struct {
Scores map[string]float64 `json:"scores"`
Overall float64 `json:"overall"`
Feedback string `json:"feedback"`
Passed bool `json:"passed"`
}
if err := json.Unmarshal([]byte(response), &evalData); err != nil {
return nil, fmt.Errorf("parse evaluation: %w", err)
}
criteria := make(map[string]bool)
for name, score := range evalData.Scores {
criteria[name] = score >= 0.7
}
return &team.EvaluationResult{
Quality: evalData.Overall,
Feedback: evalData.Feedback,
Passed: evalData.Passed,
Criteria: criteria,
Metrics: &team.QualityMetrics{
// Map to generic metrics
},
}, nil
}
func main() {
ctx := context.Background()
optimizer, err := NewCopywritingOptimizer(model)
if err != nil {
log.Fatalf("Failed to create optimizer: %v", err)
}
result, err := optimizer.workflow.Execute(ctx,
`Create a social media promotional copy for a new smartwatch.
Target audience: urban professionals aged 25-35
Core selling points: 7-day battery, health monitoring, stylish design
Word count: 100-150 words`)
if err != nil {
log.Fatalf("Optimization failed: %v", err)
}
fmt.Printf("Final copy (quality: %.3f):\n%s\n\n", result.Quality, result.Output)
fmt.Printf("Iterations: %d\n", result.Iterations)
fmt.Printf("Exit reason: %s\n", result.ExitReason)
fmt.Printf("Quality history: %v\n", result.QualityHistory)
fmt.Printf("Token used: %d\n", result.TokenUsed)
fmt.Printf("Total duration: %v\n", result.Duration)
}
Anti-Infinite-Loop Mechanisms
Multi-Layer Protection
type AntiLoopProtection struct {
maxIterations int // hard limit
timeBudget time.Duration // time limit
tokenBudget int // token limit
similarityThreshold float64 // output similarity threshold
history []string // output history
}
func (p *AntiLoopProtection) Check(state *LoopState) (bool, string) {
// 1. Iteration count check
if state.Iteration >= p.maxIterations {
return true, "max_iterations"
}
// 2. Time check
if time.Since(state.StartTime) >= p.timeBudget {
return true, "time_budget"
}
// 3. Token check
if state.TokenUsed >= p.tokenBudget {
return true, "token_budget"
}
// 4. Similarity check (detect loops)
if len(state.QualityHistory) >= 3 {
current := state.CurrentOutput
for i, hist := range p.history {
similarity := calculateSimilarity(current, hist)
if similarity > p.similarityThreshold {
return true, fmt.Sprintf("repeated_output (similarity %.3f with iteration %d)",
similarity, i)
}
}
}
p.history = append(p.history, state.CurrentOutput)
// 5. Quality regression check
if len(state.QualityHistory) >= 2 {
last := state.QualityHistory[len(state.QualityHistory)-1]
prev := state.QualityHistory[len(state.QualityHistory)-2]
if prev-last > 0.2 {
return true, "significant_regression"
}
}
return false, ""
}
// Text similarity calculation (simplified Jaccard)
func calculateSimilarity(a, b string) float64 {
setA := tokenize(a)
setB := tokenize(b)
intersection := 0
for token := range setA {
if setB[token] {
intersection++
}
}
union := len(setA) + len(setB) - intersection
if union == 0 {
return 1.0
}
return float64(intersection) / float64(union)
}
func tokenize(text string) map[string]bool {
tokens := make(map[string]bool)
words := strings.Fields(text)
for _, word := range words {
tokens[strings.ToLower(word)] = true
}
return tokens
}
State Evolution and History Management
Compressed Storage
type CompressedLoopHistory struct {
iterations int
qualityTrend []float64
bestOutputs []OutputSnapshot
compressionCfg *CompressionConfig
}
type OutputSnapshot struct {
Iteration int
Quality float64
Hash string // content hash for deduplication
Summary string // summary replaces full content
}
func (h *CompressedLoopHistory) Add(output string, quality float64, iteration int) {
// Only keep results from critical iterations
if h.shouldKeep(iteration, quality) {
hash := sha256.Sum256([]byte(output))
h.bestOutputs = append(h.bestOutputs, OutputSnapshot{
Iteration: iteration,
Quality: quality,
Hash: fmt.Sprintf("%x", hash[:8]),
Summary: summarize(output, 200),
})
}
h.qualityTrend = append(h.qualityTrend, quality)
h.iterations = iteration
}
func (h *CompressedLoopHistory) shouldKeep(iteration int, quality float64) bool {
// Keep: first, best, and most recent
if iteration == 1 {
return true
}
if quality > h.getBestQuality() {
return true
}
if iteration > h.iterations-2 {
return true
}
return false
}
Common Questions in Depth
Q: How does a Loop workflow ensure convergence?
A: Convergence cannot be absolutely guaranteed, but the probability can be increased by:
- Quality monotonicity: ensure evaluator feedback actually points toward improvement.
- Feedback quality: use detailed, actionable feedback rather than vague “not good enough” comments.
- Iterative cooling: reduce the amplitude of changes in later iterations to avoid oscillation.
- Multi-start: run multiple Loops from different initial outputs and select the best result.
Q: What if the evaluator itself is inaccurate?
A: Three layers of protection:
- Multi-evaluator voting: use 3 different evaluators and take the median.
- Human-in-the-loop: introduce human review at critical nodes.
- Evaluator calibration: regularly calibrate evaluation criteria with labeled data.
Q: How is Loop token consumption controlled?
A: A combination of strategies:
- Set a hard budget cap.
- Use a lightweight model for evaluation.
- Compress historical context (keep only summaries).
- Early termination (exit early when quality improvement < 0.01).
Next Steps
You now have a deep understanding of Loop workflow iterative optimization and convergence control. Next, explore the Custom Workflow—flexible orchestration, dynamic scheduling, and complex scenario adaptation for custom workflows.
← Parallel Workflow | Custom Workflow →
Want to learn more Go ADK hands-on? Follow the “Mengshou Programming” channel for weekly updates on Go and AI programming.
