Het gebruik van de Ollama Web Search API in Go

Maak AI zoekagents met Go en Ollama

Inhoud

Ollama’s Web Search API laat je lokale LLMs verrijken met real-time webinformatie. Deze gids laat je zien hoe je web zoekfunctionaliteiten in Go kunt implementeren, van eenvoudige API-aanroepen tot volledig uitgeruste zoekagenten.

gopher biking

Aan de slag

Heeft Ollama een officiële Go-bibliotheek voor webzoekfuncties? Ollama biedt een REST API voor webzoekfuncties die werkt met elke Go HTTP-client. Hoewel er nog geen officiële Go-SDK voor webzoekfuncties beschikbaar is, kun je de API-aanroepen eenvoudig implementeren met behulp van standaardbibliotheekpakketten.

Eerst, maak een API-sleutel aan vanuit je Ollama account. Voor een uitgebreid overzicht van Ollama-commands en gebruik, zie de Ollama cheatsheet.

Stel je API-sleutel in als een omgevingsvariabele:

export OLLAMA_API_KEY="your_api_key"

Op Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Projectopzet

Maak een nieuw Go-module aan:

mkdir ollama-search
cd ollama-search
go mod init ollama-search

Basiswebzoekfunctie

Hoe authenticatieer ik met de webzoek-API van Ollama in Go? Stel de Authorization-header in met je API-sleutel als een Bearer-token. Maak een API-sleutel aan vanuit je Ollama-account en geef deze door in de aanvraagheader.

Hieronder staat een volledige implementatie van de webzoek-API:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// Request/Response types voor web_search
type WebSearchRequest struct {
	Query      string `json:"query"`
	MaxResults int    `json:"max_results,omitempty"`
}

type WebSearchResult struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Content string `json:"content"`
}

type WebSearchResponse struct {
	Results []WebSearchResult `json:"results"`
}

func webSearch(query string, maxResults int) (*WebSearchResponse, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY omgevingsvariabele niet ingesteld")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om aanvraag te maken is mislukt: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om aanvraag te maken is mislukt: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("aanvraag is mislukt: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API-fout (status %d): %s", resp.StatusCode, string(body))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om antwoord te lezen is mislukt: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("mogelijkheid om antwoord te ontsleutelen is mislukt: %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("Wat is Ollama?", 5)
	if err != nil {
		fmt.Printf("Fout: %v\n", err)
		return
	}

	fmt.Println("Zoekresultaten:")
	fmt.Println("===============")
	for i, result := range results.Results {
		fmt.Printf("\n%d. %s\n", i+1, result.Title)
		fmt.Printf("   URL: %s\n", result.URL)
		fmt.Printf("   %s\n", truncate(result.Content, 150))
	}
}

func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}

Web Fetch Implementatie

Wat is het verschil tussen de web_search en web_fetch endpoints? Het web_search-eindpunt voert een internetzoekopdracht uit en retourneert meerdere zoekresultaten met titels, URLs en samenvattingen. Het web_fetch-eindpunt haalt de volledige inhoud van een specifieke URL op, en retourneert de paginatitel, inhoud en links.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// Request/Response types voor web_fetch
type WebFetchRequest struct {
	URL string `json:"url"`
}

type WebFetchResponse struct {
	Title   string   `json:"title"`
	Content string   `json:"content"`
	Links   []string `json:"links"`
}

func webFetch(url string) (*WebFetchResponse, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY omgevingsvariabele niet ingesteld")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om aanvraag te maken is mislukt: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om aanvraag te maken is mislukt: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("aanvraag is mislukt: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API-fout (status %d): %s", resp.StatusCode, string(body))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("mogelijkheid om antwoord te lezen is mislukt: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("mogelijkheid om antwoord te ontsleutelen is mislukt: %w", err)
	}

	return &fetchResp, nil
}

func main() {
	result, err := webFetch("https://ollama.com")
	if err != nil {
		fmt.Printf("Fout: %v\n", err)
		return
	}

	fmt.Printf("Titel: %s\n\n", result.Title)
	fmt.Printf("Inhoud:\n%s\n\n", result.Content)
	fmt.Printf("Gevonden links: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... en %d meer\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Herbruikbare Clientbibliotheek

Maak een herbruikbare Ollama-clientbibliotheek aan voor schoner code:

// ollama/client.go
package ollama

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type Client struct {
	apiKey     string
	httpClient *http.Client
	baseURL    string
}

func NewClient() (*Client, error) {
	apiKey := os.Getenv("OLLAMA_API_KEY")
	if apiKey == "" {
		return nil, fmt.Errorf("OLLAMA_API_KEY omgevingsvariabele niet ingesteld")
	}

	return &Client{
		apiKey: apiKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
		baseURL: "https://ollama.com/api",
	}, nil
}

type SearchResult struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Content string `json:"content"`
}

type SearchResponse struct {
	Results []SearchResult `json:"results"`
}

type FetchResponse struct {
	Title   string   `json:"title"`
	Content string   `json:"content"`
	Links   []string `json:"links"`
}

func (c *Client) WebSearch(query string, maxResults int) (*SearchResponse, error) {
	payload := map[string]interface{}{
		"query":       query,
		"max_results": maxResults,
	}
	return doRequest[SearchResponse](c, "/web_search", payload)
}

func (c *Client) WebFetch(url string) (*FetchResponse, error) {
	payload := map[string]string{"url": url}
	return doRequest[FetchResponse](c, "/web_fetch", payload)
}

func doRequest[T any](c *Client, endpoint string, payload interface{}) (*T, error) {
	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", c.baseURL+endpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API-fout (status %d): %s", resp.StatusCode, string(body))
	}

	var result T
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, err
	}

	return &result, nil
}

Gebruik:

package main

import (
	"fmt"
	"log"
	"ollama-search/ollama"
)

func main() {
	client, err := ollama.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	// Zoek
	results, err := client.WebSearch("Nieuwe functies in Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

	for _, r := range results.Results {
		fmt.Printf("- %s\n  %s\n\n", r.Title, r.URL)
	}

	// Haal op
	page, err := client.WebFetch("https://ollama.com")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Pagina: %s\n", page.Title)
}

Zoekagent Bouwen

Welke modellen werken het beste voor Go-gebaseerde Ollama-zoekagenten? Modellen met sterke tool-gebruiksfaciliteiten werken het beste, waaronder qwen3, gpt-oss, en cloudmodellen zoals qwen3:480b-cloud en deepseek-v3.1-cloud. Als je werkt met Qwen3-modellen en moet je zoekresultaten verwerken of herschikken, zie dan onze gids over herordenen van tekstdocumenten met Ollama en Qwen3 Embedding model in Go.

Hieronder staat een volledige implementatie van een zoekagent:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

// Chat types
type Message struct {
	Role      string     `json:"role"`
	Content   string     `json:"content,omitempty"`
	ToolCalls []ToolCall `json:"tool_calls,omitempty"`
	ToolName  string     `json:"tool_name,omitempty"`
}

type ToolCall struct {
	Function FunctionCall `json:"function"`
}

type FunctionCall struct {
	Name      string                 `json:"name"`
	Arguments map[string]interface{} `json:"arguments"`
}

type Tool struct {
	Type     string       `json:"type"`
	Function ToolFunction `json:"function"`
}

type ToolFunction struct {
	Name        string                 `json:"name"`
	Description string                 `json:"description"`
	Parameters  map[string]interface{} `json:"parameters"`
}

type ChatRequest struct {
	Model    string    `json:"model"`
	Messages []Message `json:"messages"`
	Tools    []Tool    `json:"tools,omitempty"`
	Stream   bool      `json:"stream"`
}

type ChatResponse struct {
	Message Message `json:"message"`
}

// SearchAgent coördineert webzoekfuncties met LLM
type SearchAgent struct {
	model      string
	ollamaURL  string
	apiKey     string
	maxIter    int
	httpClient *http.Client
}

func NewSearchAgent(model string) *SearchAgent {
	return &SearchAgent{
		model:      model,
		ollamaURL:  "http://localhost:11434/api/chat",
		apiKey:     os.Getenv("OLLAMA_API_KEY"),
		maxIter:    10,
		httpClient: &http.Client{},
	}
}

func (a *SearchAgent) Query(question string) (string, error) {
	tools := []Tool{
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_search",
				Description: "Zoek op het internet naar actuele informatie",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "De zoekopdracht",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Haal de volledige inhoud van een webpagina op",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "De URL om op te halen",
						},
					},
					"required": []string{"url"},
				},
			},
		},
	}

	messages := []Message{
		{Role: "user", Content: question},
	}

	for i := 0; i < a.maxIter; i++ {
		response, err := a.chat(messages, tools)
		if err != nil {
			return "", fmt.Errorf("chatfout: %w", err)
		}

		messages = append(messages, response.Message)

		// Geen toolaansroepen betekent dat we een eindantwoord hebben
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Voer toolaansroepen uit
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Aanroepen: %s\n", toolCall.Function.Name)

			result, err := a.executeTool(toolCall)
			if err != nil {
				result = fmt.Sprintf("Fout: %v", err)
			}

			// Truncate voor contextlimieten
			if len(result) > 8000 {
				result = result[:8000] + "... [verkort]"
			}

			messages = append(messages, Message{
				Role:     "tool",
				Content:  result,
				ToolName: toolCall.Function.Name,
			})
		}
	}

	return "", fmt.Errorf("max aantal iteraties bereikt")
}

func (a *SearchAgent) chat(messages []Message, tools []Tool) (*ChatResponse, error) {
	reqBody := ChatRequest{
		Model:    a.model,
		Messages: messages,
		Tools:    tools,
		Stream:   false,
	}

	jsonData, _ := json.Marshal(reqBody)

	resp, err := a.httpClient.Post(a.ollamaURL, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var chatResp ChatResponse
	if err := json.Unmarshal(body, &chatResp); err != nil {
		return nil, err
	}

	return &chatResp, nil
}

func (a *SearchAgent) executeTool(toolCall ToolCall) (string, error) {
	switch toolCall.Function.Name {
	case "web_search":
		query, ok := toolCall.Function.Arguments["query"].(string)
		if !ok {
			return "", fmt.Errorf("ongeldige queryargument")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("ongeldige urlargument")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("onbekende tool: %s", toolCall.Function.Name)
	}
}

func (a *SearchAgent) webSearch(query string) (string, error) {
	payload := map[string]interface{}{"query": query, "max_results": 5}
	jsonData, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	req.Header.Set("Authorization", "Bearer "+a.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := a.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	return string(body), nil
}

func (a *SearchAgent) webFetch(url string) (string, error) {
	payload := map[string]string{"url": url}
	jsonData, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	req.Header.Set("Authorization", "Bearer "+a.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := a.httpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	return string(body), nil
}

func main() {
	agent := NewSearchAgent("qwen3:4b")

	answer, err := agent.Query("Wat zijn de nieuwste functies in Ollama?")
	if err != nil {
		fmt.Printf("Fout: %v\n", err)
		return
	}

	fmt.Println("\n📝 Antwoord:")
	fmt.Println(answer)
}

Hoe verwerk ik grote webzoekantwoorden in Go? Truncate de antwoordinhoud voordat je deze doorgeeft aan de modelcontext. Gebruik string slicing om de inhoud te beperken tot ongeveer 8000 tekens om binnen de contextlimieten te blijven.

Concurrente zoekfunctie

Go is goed in concurrente bewerkingen. Hieronder staat hoe je meerdere zoekacties parallel kunt uitvoeren. Het begrijpen van hoe Ollama parallelle aanvragen verwerkt kan je helpen bij het optimaliseren van je concurrente zoekimplementaties.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
)

type SearchResult struct {
	Query   string
	Results []struct {
		Title   string `json:"title"`
		URL     string `json:"url"`
		Content string `json:"content"`
	} `json:"results"`
	Error error
}

func concurrentSearch(queries []string) []SearchResult {
	results := make([]SearchResult, len(queries))
	var wg sync.WaitGroup

	for i, query := range queries {
		wg.Add(1)
		go func(idx int, q string) {
			defer wg.Done()

			result := SearchResult{Query: q}

			payload := map[string]string{"query": q}
			jsonData, _ := json.Marshal(payload)

			req, _ := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
			req.Header.Set("Authorization", "Bearer "+os.Getenv("OLLAMA_API_KEY"))
			req.Header.Set("Content-Type", "application/json")

			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				result.Error = err
				results[idx] = result
				return
			}
			defer resp.Body.Close()

			body, _ := io.ReadAll(resp.Body)
			json.Unmarshal(body, &result)
			results[idx] = result
		}(i, query)
	}

	wg.Wait()
	return results
}

func main() {
	queries := []string{
		"Ollama nieuwste functies",
		"lokale LLM implementatie",
		"AI zoekagenten Go",
	}

	results := concurrentSearch(queries)

	for _, r := range results {
		fmt.Printf("\n🔍 Query: %s\n", r.Query)
		if r.Error != nil {
			fmt.Printf("   Fout: %v\n", r.Error)
			continue
		}
		for _, item := range r.Results[:min(3, len(r.Results))] {
			fmt.Printf("   • %s\n", item.Title)
		}
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

Context en Annulering

Voeg contextondersteuning toe voor time-outs en annulering:

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func webSearchWithContext(ctx context.Context, query string) (string, error) {
	payload := map[string]string{"query": query}
	jsonData, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", err
	}

	req.Header.Set("Authorization", "Bearer "+os.Getenv("OLLAMA_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func main() {
	// Maak context met 10 seconden time-out
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "Ollama web search API")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Aanvraag is verlopen")
		} else {
			fmt.Printf("Fout: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Aanbevolen modellen

Welke contextlengte moet ik gebruiken voor Go-zoekagenten? Stel de contextlengte in op ongeveer 32000 tokens voor redelijke prestaties. Zoekagenten werken het beste met de volledige contextlengte, omdat webzoekresultaten uitgebreid kunnen zijn. Als je je Ollama-modellen moet beheren of naar andere locaties moet verplaatsen, zie dan onze gids over hoe je Ollama-modellen verplaatst naar een andere schijf of map.

Model Parameters Bestemd voor
qwen3:4b 4B Snel lokale zoekacties
qwen3 8B Algemene doeleinden
gpt-oss Verschillend Onderzoekstaken
qwen3:480b-cloud 480B Complexere redenering (cloud)
gpt-oss:120b-cloud 120B Langere onderzoeksrapporten (cloud)
deepseek-v3.1-cloud - Geavanceerde analyse (cloud)

Voor geavanceerde AI-toepassingen die tekst en visuele inhoud combineren, overweeg dan cross-modal embeddings om je zoekfunctionaliteiten uit te breiden tot meer dan alleen tekstvragen.

Beste praktijken

  1. Foutafhandeling: Controleer altijd op fouten en behandel API-fouten op een beleefde manier
  2. Time-outs: Gebruik context met time-outs voor netwerkverzoeken
  3. Beperkingen: Respecteer Ollama’s API-beperkingen. Wees bewust van mogelijke veranderingen in de Ollama API, zoals besproken in de eerste tekenen van Ollama enshittification
  4. Resultaatverkorting: Verkort resultaten tot ongeveer 8000 tekens voor contextlimieten
  5. Concurrente aanvragen: Gebruik goroutines voor parallelle zoekacties
  6. Verbindingsherbruik: Herbruik HTTP-client voor betere prestaties
  7. Testen: Schrijf uitgebreide eenheidstests voor je zoekimplementaties. Volg de beste praktijken voor Go-eenheidstesten om ervoor te zorgen dat je code robuust en onderhoudbaar is