Ollama Web Search API in Go verwenden

KI-Suchagenten mit Go und Ollama erstellen

Inhaltsverzeichnis

Mit der Websuche-API von Ollama können Sie lokale LLMs mit Echtzeit-Webinformationen anreichern. Dieser Leitfaden zeigt Ihnen, wie Sie Websuchefunktionen in Go umsetzen, von einfachen API-Aufrufen bis hin zu vollausgestatteten Such-Agenten.

gopher biking

Erste Schritte

Hat Ollama eine offizielle Go-Bibliothek für die Websuche? Ollama stellt eine REST-API für die Websuche bereit, die mit jedem Go-HTTP-Client funktioniert. Obwohl es noch kein offizielles Go-SDK für die Websuche gibt, können Sie die API-Aufrufe leicht mit Standardbibliotheken implementieren.

Erstellen Sie zunächst einen API-Schlüssel aus Ihrem Ollama-Konto. Für einen umfassenden Referenzleitfaden zu Ollama-Befehlen und ihrer Verwendung werfen Sie einen Blick auf das Ollama-Spickzettel.

Setzen Sie Ihren API-Schlüssel als Umgebungsvariable:

export OLLAMA_API_KEY="your_api_key"

Unter Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Projektaufbau

Erstellen Sie ein neues Go-Modul:

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

Grundlegende Websuche

Wie authentifiziere ich mich mit Ollamas Websuche-API in Go? Setzen Sie den Authorization-Header mit Ihrem API-Schlüssel als Bearer-Token. Erstellen Sie einen API-Schlüssel aus Ihrem Ollama-Konto und übergeben Sie ihn im Anfrage-Header.

Hier ist eine vollständige Implementierung der Websuche-API:

package main

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

// Request/Response types for 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 environment variable not set")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %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("request failed: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return &searchResp, nil
}

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

	fmt.Println("Search Results:")
	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] + "..."
}

Implementierung von Web Fetch

Was ist der Unterschied zwischen den Endpunkten web_search und web_fetch? Der web_search-Endpunkt durchsucht das Internet und gibt mehrere Suchergebnisse mit Titeln, URLs und Textauszügen zurück. Der web_fetch-Endpunkt ruft den vollständigen Inhalt einer bestimmten URL ab und gibt den Seitentitel, den Inhalt und die Links zurück.

package main

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

// Request/Response types for 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 environment variable not set")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %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("request failed: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Title: %s\n\n", result.Title)
	fmt.Printf("Content:\n%s\n\n", result.Content)
	fmt.Printf("Links found: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... and %d more\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Wiederverwendbares Client-Paket

Erstellen Sie ein wiederverwendbares Ollama-Client-Paket für sauberen 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 environment variable not set")
	}

	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 error (status %d): %s", resp.StatusCode, string(body))
	}

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

	return &result, nil
}

Verwendung:

package main

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

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

	// Search
	results, err := client.WebSearch("Ollama new features", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

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

Aufbau eines Such-Agenten

Welche Modelle eignen sich am besten für Go-basierte Ollama-Such-Agenten? Modelle mit starken Tool-Nutzungsfähigkeiten funktionieren am besten, darunter qwen3, gpt-oss und Cloud-Modelle wie qwen3:480b-cloud und deepseek-v3.1-cloud. Wenn Sie mit Qwen3-Modellen arbeiten und Suchergebnisse verarbeiten oder neu sortieren müssen, sehen Sie sich unseren Leitfaden zur Neusortierung von Textdokumenten mit dem Ollama- und Qwen3-Embedding-Modell in Go an.

Hier ist eine vollständige Implementierung eines Such-Agenten:

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 orchestrates web search with 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: "Search the web for current information",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "The search query",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Fetch the full content of a web page",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "The URL to fetch",
						},
					},
					"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("chat error: %w", err)
		}

		messages = append(messages, response.Message)

		// No tool calls means we have a final answer
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Execute tool calls
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Calling: %s\n", toolCall.Function.Name)

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

			// Truncate for context limits
			if len(result) > 8000 {
				result = result[:8000] + "... [truncated]"
			}

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

	return "", fmt.Errorf("max iterations reached")
}

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("invalid query argument")
		}
		return a.webSearch(query)

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

	default:
		return "", fmt.Errorf("unknown 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("What are the latest features in Ollama?")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

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

Wie gehe ich mit großen Websuchantworten in Go um? Kürzen Sie den Antwortinhalt, bevor Sie ihn in den Modellkontext übergeben. Verwenden Sie Stringslicing, um den Inhalt auf ungefähr 8000 Zeichen zu begrenzen, damit er innerhalb der Kontextgrenzen bleibt.

Concurrent Search (Parallele Suche)

Go exceliert bei parallelen Operationen. So führen Sie mehrere Suchen parallel durch. Das Verständnis dafür, wie Ollama parallele Anfragen verarbeitet, kann Ihnen helfen, Ihre parallelen Suchimplementierungen zu optimieren.

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 latest features",
		"local LLM deployment",
		"AI search agents Go",
	}

	results := concurrentSearch(queries)

	for _, r := range results {
		fmt.Printf("\n🔍 Query: %s\n", r.Query)
		if r.Error != nil {
			fmt.Printf("   Error: %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
}

Kontext und Abbruch

Fügen Sie Kontextunterstützung für Timeouts und Abbrüche hinzu:

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() {
	// Create context with 10 second timeout
	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("Request timed out")
		} else {
			fmt.Printf("Error: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Empfohlene Modelle

Welche Kontextlänge sollte ich für Go-Such-Agenten verwenden? Stellen Sie die Kontextlänge auf etwa 32000 Token ein, um eine angemessene Leistung zu erzielen. Such-Agenten funktionieren am besten mit voller Kontextlänge, da Websuchergebnisse umfangreich sein können. Wenn Sie Ihre Ollama-Modelle verwalten oder an andere Speicherorte verschieben müssen, sehen Sie sich unseren Leitfaden dazu an, wie Sie Ollama-Modelle auf ein anderes Laufwerk oder in einen anderen Ordner verschieben.

Modell Parameter Am besten geeignet für
qwen3:4b 4B Schnelle lokale Suchen
qwen3 8B Allzweck-Agent
gpt-oss Verschiedene Recherchetasks
qwen3:480b-cloud 480B Komplexes Reasoning (Cloud)
gpt-oss:120b-cloud 120B Langlebige Recherche (Cloud)
deepseek-v3.1-cloud - Fortgeschrittene Analyse (Cloud)

Best Practices

  1. Fehlerbehandlung: Prüfen Sie immer auf Fehler und behandeln Sie API-Fehler elegant.
  2. Timeouts: Verwenden Sie Kontexte mit Timeouts für Netzwerk Anfragen.
  3. Ratenbegrenzung: Beachten Sie die API-Ratenbegrenzungen von Ollama. Seien Sie sich möglicher Änderungen an Ollamas API bewusst, wie sie in ersten Anzeichen der Ollama-Enshittification diskutiert werden.
  4. Ergebniskürzung: Kürzen Sie Ergebnisse auf ca. 8000 Zeichen für Kontextgrenzen.
  5. Parallele Anfragen: Verwenden Sie Goroutinen für parallele Suchen.
  6. Verbindungswiederverwendung: Wiederverwenden Sie den HTTP-Client für bessere Leistung.
  7. Testen: Schreiben Sie umfassende Unit-Tests für Ihre Suchimplementierungen. Befolgen Sie die Go-Unit-Testing-Best-Practices, um sicherzustellen, dass Ihr Code robust und wartbar ist.

Abonnieren

Neue Beiträge zu Systemen, Infrastruktur und KI-Engineering.