Why Choose Go for AI Agents? Inside agent-web’s Zero‑Dependency Architecture

This article explains how the agent-web framework builds a powerful AI agent orchestration system in Go without external libraries, detailing the reasons for choosing Go, the use of go:embed for single‑binary deployment, core data structures, planning and execution logic, and the interaction handling that enables both CLI and web interfaces.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
Why Choose Go for AI Agents? Inside agent-web’s Zero‑Dependency Architecture

Why Go for AI agents

Go provides native concurrency via goroutines and channels, static type safety that catches errors at compile time, and single‑binary deployment that removes dependency management.

Native concurrency : goroutine and channel model enables many search and analysis tasks to run simultaneously without the complexity of Python's asyncio or thread locks.

Type safety : the static type system catches many potential errors during compilation, which is crucial for complex agent state flows.

Simple deployment : compiling to a single binary eliminates the need for pip install or virtual environments.

Embedding static assets with go:embed

agent-web embeds its front‑end build output directly into the Go binary using the go:embed feature introduced in Go 1.16. The snippet below shows how the static files are embedded and served:

//go:embed dist/*
var distFS embed.FS

func main() {
    // Serve the embedded file system as static resources
    fs := http.FileServer(http.FS(distFS))
    http.Handle("/", fs)
}

Benefits of this approach include:

Eliminate missing assets : front‑end resources are bundled with the back‑end, so 404 errors cannot occur.

Extremely easy distribution : copy the compiled agent-web binary to any server, chmod +x, and run—no Nginx reverse proxy or Docker volume needed.

Version consistency : the front‑end and back‑end are forced to share the same version, avoiding compatibility problems.

Performance comparison: Go vs Python

Go’s mature M:N scheduler runs thousands of lightweight goroutines across multiple cores, avoiding Python’s Global Interpreter Lock (GIL) limitation for CPU‑bound work. As a compiled language, Go typically executes an order of magnitude faster than interpreted Python, reducing latency and resource consumption for large‑scale text analysis and JSON parsing.

Additional architectural advantages include clearer code readability thanks to static typing and gofmt, and simpler dependency management via Go modules, which avoids the “dependency hell” common in Python projects.

Core data structure

The heart of agent‑web is the PlanningAgent struct defined in agent/agent.go:

type PlanningAgent struct {
    client             *openai.Client
    config             AgentConfig
    messages           []openai.ChatCompletionMessage
    subagents          map[TaskType]Subagent
    interactionHandler InteractionHandler
}

This structure is deliberately minimal—no hidden graph objects or implicit context passing; everything is explicit. TaskTypeSearch: calls the Tavily API. TaskTypeAnalyze: processes text. TaskTypeReport: generates the final output. TaskTypePPT / TaskTypePodcast: multimodal generation.

PlanningAgent workflow

1. Initialization (NewPlanningAgent)

Creates an OpenAI‑compatible client and registers each sub‑agent (Search, Analyze, Report, etc.) in the subagents map, forming an expert team where each member has a specific skill.

2. Planning (Plan)

Builds a system prompt that lists available sub‑agents and requires the LLM to output a strict JSON task list. Example JSON schema:

{
  "tasks": [
    {"type": "SEARCH", "description": "...", "parameters": {...}},
    {"type": "ANALYZE", "description": "...", "parameters": {...}},
    {"type": "REPORT", "description": "...", "parameters": {...}}
  ]
}

The prompt forces structured output without relying on LLM function calling, ensuring consistent behavior across models such as GPT‑4, Claude 3, and DeepSeek.

3. Execution (Execute)

Iterates over the task list with three key steps.

Context injection

Injects the original user request and any previously collected outputs into each task’s parameters map so later tasks can see earlier results.

// Inject global conversation history
task.Parameters["global_context"] = globalContextBuilder.String()

// Append previous task outputs if any
if len(contextData) > 0 {
    if task.Parameters == nil {
        task.Parameters = make(map[string]interface{})
    }
    if existing, ok := task.Parameters["context"].([]string); ok {
        task.Parameters["context"] = append(existing, contextData...)
    } else {
        task.Parameters["context"] = contextData
    }
}

Task dispatch

Looks up the appropriate sub‑agent from the subagents map and executes it. Unknown task types produce an error.

subagent, ok := a.subagents[task.Type]
if !ok {
    return nil, fmt.Errorf("unknown task type: %s", task.Type)
}
result, err := subagent.Execute(ctx, task)

Dynamic adjustment

If a sub‑agent determines that more information is needed, it returns NewTasks. The main loop inserts these new tasks right after the current one, creating a micro‑loop of “Plan → Execute → Re‑Plan”.

if result.Success && len(result.NewTasks) > 0 {
    rear := append([]Task{}, plan.Tasks[i+1:]...)
    plan.Tasks = append(plan.Tasks[:i+1], append(result.NewTasks, rear...)...)
}
contextData = append(contextData,
    fmt.Sprintf("Output from %s task:
%s", task.Type, result.Output))

4. Run (public entry point)

The Run method composes planning, execution, and final result extraction (preferring RENDER or REPORT tasks).

func (a *PlanningAgent) Run(ctx context.Context, userRequest string) (string, error) {
    plan, err := a.Plan(ctx, userRequest)
    if err != nil { return "", err }
    results, err := a.Execute(ctx, plan)
    if err != nil { return "", err }
    // Extract final output, prioritizing RENDER/REPORT tasks
    // ...
}

InteractionHandler interface

Provides UI‑agnostic logging and plan review, enabling both CLI and web implementations (the web version pushes logs via Server‑Sent Events).

type InteractionHandler interface {
    ReviewPlan(plan *Plan) (string, error)
    Log(message string)
}

Example usage:

if s.interactionHandler != nil {
    s.interactionHandler.Log(fmt.Sprintf("  🔄 LLM requests more info. New query: %q", newQuery))
}
Architecture diagram
Architecture diagram
backend architectureAI agentsconcurrencyGoagent-webgo:embed
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.