[KongchangAI]
· 4 min read· 2,320 words

Building an AI Agent from Scratch in Go: Architecture, the Agentic Loop, and Tool Calling

Building an AI Agent from Scratch in Go: Architecture, the Agentic Loop, and Tool Calling

Building the AI Agent Omega from scratch in Go, lecture one: architecture, domain modeling, and the Agentic Loop.

This article covers lecture one of building the AI Agent project Omega from scratch in Go. An LLM is just a token-generating "brain"; layering a tool layer and automated orchestration on top turns it into an Agent, with the Agentic Loop at its core. The architecture emphasizes interface-first plus the adapter pattern to keep Providers stateless and the domain isolated. It also tours Go fundamentals (struct/interface, goroutine/channel, context) and defines Omega's domain entities, ending with the seven-step Agentic Loop.

Building an AI Agent from Scratch in Go: Architecture, the Agentic Loop, and Tool Calling

The relationship between LLMs and Agents

A large model is essentially just a "brain"—it can understand language and generate text, but it cannot actively read files, execute commands, or access the network. What truly gives an LLM the ability to act is a layer of automated orchestration layered on top of it—this is an AI Agent. A YouTube tech creator is building such an Agent project from scratch in Go, codenamed Omega, and is publicly sharing the entire design process as a course series. This article distills the core content of the first lecture: architectural decisions, domain entity modeling, and the most critical piece—the design of the Agentic Loop.

The Essential Difference Between an LLM and an Agent

An LLM is a token-generation model. Claude Opus, Sonnet, and the GPT series all belong to this category. They can understand natural languages like English and Hindi, but that's all. When you ask an LLM to read a file, it cannot do it itself—it can only output some text saying "I want to use the file_read tool with the argument command/agent/main.go."

Connecting this kind of "intention" from the LLM to actual operations requires a tool layer. file_read is responsible for reading files, list_directory for listing directories, run_shell for executing commands, browser_use for online retrieval, and MCP (Model Context Protocol) for connecting to external services. When these tools are integrated on top of the LLM to form an automated execution chain, the result is an Agent—it can read files, run commands, and call APIs, behaving as if it had a mind of its own.

MCP (Model Context Protocol) is a standard protocol open-sourced by Anthropic at the end of 2024, designed to let LLM applications connect to external data sources and tool services in a unified way. Before MCP appeared, every Agent project had to write separate integration code for each external service—GitHub, databases, file systems, etc.—with inconsistent formats that were hard to reuse. MCP abstracts these integrations into "Servers"—each Server declaratively exposes its own tools, resources, and prompt templates, while the Agent acts as a "Client" communicating with it via the standard JSON-RPC protocol. This means an MCP Server written for the Claude desktop client can be directly reused by any Agent that supports MCP. In a self-built Agent like Omega, MCP is essentially upgrading the extension of the tool layer from "hardcoding" to "plug-in": without modifying the core loop, simply attaching a new MCP Server automatically gives the Agent new capabilities. This is the concrete implementation, at the tool level, of the design principle "whatever needs to be added in the future must be easy to add."

Core Design Principles: Extensibility and Domain Isolation

Extensible architecture design

The creator repeatedly emphasizes one engineering principle: whatever needs to be added in the future must be easy to add. Today you connect to Anthropic's API, tomorrow you want to switch to OpenAI or OpenRouter—you shouldn't have to rewrite the business logic.

The implementation path relies on two design patterns:

Interface First. Define a unified LLM interface. The Anthropic Provider implements it, and the OpenAI Provider also implements it. The business layer always calls only the interface and doesn't care who is underneath.

Adapter Pattern. Third-party APIs have different request/response formats, and the adapter is responsible for translating neutral domain requests into each vendor's wire format, then translating the responses back. Providers are stateless—they don't know whether this is the 10th or 11th request, don't care whether the token budget is about to be exceeded, and only care about "give me a request, and I'll return a response."

The value of this design is that the CLI, mobile app, and desktop are all just different consumers of the same event stream. Today you use a terminal, tomorrow you want to connect a web front end—the infrastructure doesn't need to change.

A Quick Tour of Core Go Concepts

The course spends a lot of time laying out the fundamentals of Go, because the Agentic Loop that follows is built entirely on them.

Struct and Interface

Go has no classes, only structs and interfaces. A struct is a structure that holds data variables; a field whose first letter is uppercase is public, and a lowercase one is package-private. An interface only defines method signatures, and any struct that implements those methods automatically satisfies the interface. Provider is defined as an interface containing two methods: complete and stream.

Goroutine and Channel

A goroutine is a lightweight thread managed by the Go runtime, running in user space. A channel is a pipe for communication between goroutines—essentially a block of shared memory on the heap, and it's thread-safe, so reads and writes won't cause data races. The Agent's event stream (text deltas, tool calls, tool results, errors) is sent back from a background goroutine to the main program precisely through channels.

Context and Cancellation

Context is a data structure in Go for passing cancellation signals. The main thread creates a cancelContext and passes it to all of the Agent's runtime. When the user presses Ctrl+C, the terminal catches the signal, triggers context cancellation, and the Agent Session along with all open file handles and network connections inside it are cleanly closed. Context cannot be replaced in place; deriving a new context requires creating a child context.

How Go structs and interfaces are implemented

The key to understanding Context is that it is an immutable tree, not a mutable global state. context.Background() is the root, and each WithCancel, WithTimeout, or WithValue derives a child node that holds a reference to its parent. When a parent node is cancelled, all of its descendants receive the cancellation signal in a cascade; conversely, cancelling a child node does not affect the parent or sibling nodes. This one-way propagation mechanism is exactly what Agent scenarios need—when the user presses Ctrl+C and the top-level Session context is cancelled, then every round of LLM HTTP requests, every tool call, and every open database connection derived from it can immediately exit in their respective goroutines by listening to the ctx.Done() channel. Context is usually passed as the first argument to a function, and by convention it should not be stored in a struct field, because its lifecycle should strictly follow a single request. In the Agentic Loop, each iteration often derives a child context with a timeout from the Session context to constrain the maximum duration of a single tool call—this is exactly how max seconds in the Budget is implemented.

Domain Entity Modeling

The creator uses a system design style similar to Hello Interview: list requirements first, then define entities, and finally draw sequence diagrams. All entities belong to Omega's domain language and have nothing to do with Anthropic or OpenAI.

Role: Identifies the source of a message, user or assistant.

Message: Composed of a role and an array of content blocks. User messages and assistant replies are both Messages, only with different roles.

Content Block: Can be text (plain text), tool_use (tool call request), or tool_result (tool execution result). tool_use contains an id, name, and input (dynamic JSON); tool_result reuses the same id for tracking.

Completion Request / Response: These are Omega's "neutral request" and "neutral response." The Request contains the system prompt, the message history array, and tool definitions; the Response is a single content block and is not appended to the history.

Tool Definition: Each tool has a name, description, and input_schema (structured JSON) to ensure calls are deterministic.

Usage: Records input tokens, output tokens, and cache read tokens. Cache reads are metered separately because a transformer's KV vectors can be cached under the same prefix, making them cheaper.

Budget: Limits max turns (e.g., 100 turns), max tokens (e.g., 5 million), and max seconds (e.g., 60 seconds for a single tool call), and uses a check function to verify whether limits are exceeded on each turn.

Agent Event: Event types include text_delta (token-by-token streaming text), tool_call, tool_result, turn_end (end of turn), and error, passed to consumers through channels.

KV Cache (Key-Value Cache) is the key to understanding why cache read tokens in Usage are priced separately. When a transformer generates each token, it needs to compute attention over all previous tokens; once the Key and Value vectors there have been computed, they don't need to be recomputed as long as the prefix stays the same. Vendors like Anthropic cache these vectors on the server side, and if a subsequent request hits the same prefix, it skips recomputation and reuses them directly. This is crucial for an Agent: each round of the Agentic Loop resends the ever-growing message history, the prefixes overlap heavily, and the cache hit rate is extremely high. Cache writes are usually about 25% more expensive than normal input tokens, but cache reads are about 90% cheaper, so in long conversations the cache can significantly reduce both cost and latency. This also explains why, during domain modeling, usage is split into input, output, and cache_read categories rather than a vague total token count—only by metering them separately can you truly reflect costs and provide a data basis for subsequent optimizations like Compaction (context compression).

The Core Logic of the Agentic Loop

This is the most important part of the entire course. After an Agent Session is created, the run method starts the loop:

  1. Check the budget. At the start of each turn, first verify whether turns, tokens, and time have exceeded their limits.
  2. Make the LLM call. Carry the cancelContext, system prompt, message history, and available tools, and call the provider's complete method.
  3. Transform the response. The Provider converts the Anthropic response into Omega's unified Completion Response.
  4. Append the message. Append the assistant's reply to the message array, forming an ever-growing context.
  5. Determine the stop reason. If it's end_turn, emit a run_end event, close the channel, and end the loop. If it's max_tokens, emit an error and exit. If it's tool_use, proceed to the next step.
  6. Execute the tools. Iterate over each tool_use block, emit a tool_call event, call the corresponding tool through the tool registry, emit a tool_result event after getting the result, and append the tool_result as a new message.
  7. Return to step 1 and continue the loop.

The essence of this loop is that the Agent has control over the loop. It decides when to wait for a tool to return, when to continue to the next turn, and when to end. Both the Provider and the tools are stateless service providers, and all state is concentrated in the Agent Session.

Why Design It This Way

The creator mentions his experience being interviewed: LLD has no standard answer, but your design must answer two questions. First, is it extensible—in the future, when adding a new provider or a new tool, can you do it without changing the core logic? Second, is the domain boundary clear—does your server speak only its own domain language, with third-party formats isolated inside the adapter?

"Omega is a black box, and the LLM is also a black box. I design for Omega, and Omega implements it." This sentence sums up the entire architectural philosophy: users send requests and receive responses, without caring whether the underlying system is Anthropic or OpenAI; the business logic doesn't care about technical details and depends only on interface contracts.

Subsequent lectures will move on to topics such as streaming responses (SSE, token-by-token push), MCP integration, Compaction (context compression), and stream isolation in multi-user scenarios. The value of the first lecture isn't in how many lines of code were written, but in laying a solid foundation—without understanding the Agentic Loop and domain modeling, there's no way to even begin tackling the production-grade problems that come later.

The Adapter Pattern is one of the 23 classic GoF design patterns, and its core intent is "to convert the interface of a class into another interface the client expects, so that classes that could not otherwise work together because of incompatible interfaces can work together." In Omega, it manifests as two layers of translation: in the request direction, it converts Omega's neutral domain model (Completion Request) into Anthropic's or OpenAI's wire format; in the response direction, it translates the vendor's raw JSON back into Omega's Completion Response. Adapters are usually stateless, interchangeable, purely functional components, which echoes the earlier emphasis on "stateless Providers." Complementing this is the principle of Interface First: define the abstraction that consumers truly need first, and let implementations adapt to the abstraction, rather than making the abstraction cater to a specific vendor. Taken together, the benefit is dependency inversion—high-level business logic depends on stable interfaces, and low-level implementations can be swapped at any time, so switching API vendors, integrating a self-developed model, or adding a Mock for testing all require no changes to the Agentic Loop itself.

One-line Summary

Building the AI Agent Omega from scratch in Go, lecture one: architectural decisions, domain modeling, and the core logic of the Agentic Loop.

Paragraph Summary

This article distills the core content of lecture one of building the AI Agent project Omega from scratch in Go. It points out that an LLM is just a "brain" that generates tokens, and that a tool layer and automated orchestration must be layered on top for it to become an Agent, whose essence lies in the Agentic Loop. Architecturally, it emphasizes "Interface First + Adapter Pattern," which keeps Providers stateless and business logic independent of any third-party API format, achieving extensibility and domain isolation. The article also tours the core Go concepts that support the loop: struct/interface, goroutine/channel, and context cancellation. It then defines Omega's domain entities in a system design style: Role, Message, ContentBlock, Completion Request/Response, Tool Definition, Usage, Budget, and Agent Event. Finally, it details the seven-step process of the Agentic Loop and explains its design philosophy.

#AI Agent#Golang#Agentic Loop#tool calling#adapter pattern#Model Context Protocol#domain modeling
Share:

Related articles