Build a Local Go AI Agent with Ollama and DeepSeek – MVP Guide

This article walks you through creating a fully offline, extensible AI programming assistant in Go, using Ollama and DeepSeek‑R1, covering project layout, message formats, function calling, tool integration, a simple WebSocket UI, and future extension ideas.

Code Wrench
Code Wrench
Code Wrench
Build a Local Go AI Agent with Ollama and DeepSeek – MVP Guide

Overview

This article demonstrates how to build a locally‑run AI Agent in pure Go using Ollama with the DeepSeek‑R1 model. The agent can converse, invoke tools (code execution, file I/O, web search, etc.), and be extended further.

Project Structure (Minimal MVP)

easy-agent/
├── main.go
├── agent/
│   ├── agent.go
│   ├── agency.go
│   ├── memory.go
│   ├── ollama_client.go
│   └── tools.go
└── client/
    └── index.html

agent/ : core logic of the AI Agent

client/ : simple WebSocket front‑end for testing

ollama_client.go : wrapper for calling a local LLM via Ollama

agency.go : main agent loop that handles function calls

tools.go : definitions of tool functions

memory.go : lightweight in‑memory store

Message Format and Function Calling

The LLM exchanges a list of messages. Example messages:

{"role": "user", "content": "帮我解释下面代码"}
{"role": "assistant", "content": "…"}
{"role": "tool", "name": "run_code", "content": "执行结果…"}

When a tool is required the model returns a function_call object:

{
  "message": {
    "role": "assistant",
    "function_call": {
      "name": "run_code",
      "arguments": "{\"language\":\"go\",\"code\":\"...\"}"
    }
  }
}

Processing steps:

Parse the function_call field.

Execute the corresponding tool (e.g., run code).

Append a new message with role: tool and the tool output.

Call the model again with the updated message list.

Repeat until the model produces a final answer.

Ollama Client (High‑Quality Version)

File:

agent/ollama_client.go
type OllamaClient struct {
    Endpoint string
    Client   *http.Client
    Model    string
}

func NewOllamaClient(endpoint string, timeout time.Duration, model string) *OllamaClient {
    return &OllamaClient{Endpoint: endpoint, Client: &http.Client{Timeout: timeout}, Model: model}
}

func (c *OllamaClient) Call(ctx context.Context, messages []ChatMessage, tools any) (*ChatResponse, error) {
    reqBody := ChatRequest{Model: c.Model, Messages: messages, Tools: tools, ToolChoice: "auto"}
    b, _ := json.Marshal(reqBody)
    req, _ := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewReader(b))
    req.Header.Set("Content-Type", "application/json")
    resp, err := c.Client.Do(req)
    if err != nil { return nil, err }
    defer resp.Body.Close()
    var cr ChatResponse
    json.NewDecoder(resp.Body).Decode(&cr)
    return &cr, nil
}

This client can call any Ollama‑compatible model such as DeepSeek‑R1, Qwen, or LLaMA.

Agent Loop (Core Reasoning Engine)

File:

agent/agency.go
func (a *Agent) Run(prompt string) (string, error) {
    messages := []ChatMessage{{Role: "system", Content: "你是 AI 编程伙伴,遇到需要执行的任务请调用工具"}, {Role: "user", Content: prompt}}
    for i := 0; i < 6; i++ {
        cr, _ := a.client.Call(context.Background(), messages, toolsMetadata())
        msg := cr.Choices[0].Message
        if msg.FunctionCall != nil {
            // forward the function call to the tool system
            messages = append(messages, ChatMessage{Role: msg.Role, Name: msg.FunctionCall.Name})
            result := a.execTool(msg.FunctionCall)
            messages = append(messages, ChatMessage{Role: "tool", Name: msg.FunctionCall.Name, Content: result})
            continue
        }
        return msg.Content, nil
    }
    return "", fmt.Errorf("loop limit")
}

The loop adds system and user messages, lets the model think, executes any requested tool, feeds the result back, and repeats until a final answer is produced.

Tool System

File:

agent/tools.go
func (a *Agent) execTool(fc *FunctionCall) string {
    switch fc.Name {
    case "run_code":
        return RunCodeSandbox(fc.Arguments)
    case "read_file":
        return ReadFile(fc.Arguments)
    case "web_search":
        return WebSearch(fc.Arguments)
    default:
        return "unknown tool"
    }
}

Tool metadata is supplied to the model as a JSON schema:

func toolsMetadata() any {
    return []map[string]any{{
        "type": "function",
        "function": map[string]any{
            "name": "web_search",
            "description": "联网搜索相关信息",
            "parameters": map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "query": map[string]any{"type": "string"},
                    "num_results": map[string]any{"type": "integer"},
                },
                "required": []string{"query"},
            },
        },
    }}
}

Adding a Web Search Tool (Example Stub)

func WebSearch(args WebSearchArgs) ([]SearchResult, error) {
    // Use SerpAPI, DuckDuckGo, Bing API, or scrape search pages.
    return results, nil
}

When the model decides to call web_search, it sends a function call such as:

{"function_call": {"name": "web_search", "arguments": "{\"query\":\"Go map race condition\",\"num_results\":3}"}}

The Agent runs the search, feeds the results back, and the model returns a concise answer.

Frontend Debugging Interface (WebSocket Streaming)

Run the agent: go run main.go Open client/index.html in a browser. The UI streams text from the agent and allows you to submit queries such as “帮我分析这段 Go 程序”. The data flow is:

Agent → WebSocket → Model → Tool → Model → Browser

Agent UI
Agent UI
Agent UI
Agent UI

Source Code

GitHub: https://github.com/louis-xie-programmer/easy-agent

Gitee: https://gitee.com/louis_xie/easy-agent

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoAI agentTool CallingOllamalocal LLM
Code Wrench
Written by

Code Wrench

Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻

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.