Verwendung der Ollama Web Search API in Go

AI-Suchagenten mit Go und Ollama erstellen

Inhaltsverzeichnis

Ollamas Web-Search-API ermöglicht es Ihnen, lokale LLMs mit Echtzeit-Webinformationen zu erweitern. Diese Anleitung zeigt Ihnen, wie Sie Web-Suchfunktionen in Go implementieren, von einfachen API-Aufrufen bis hin zu vollwertigen Suchagenten.

gopher biking

Einstieg

Hat Ollama eine offizielle Go-Bibliothek für die Websuche? Ollama bietet eine REST-API für die Websuche, die mit jedem Go-HTTP-Client funktioniert. Obwohl es noch keine offizielle Go-SDK für die Websuche gibt, können Sie die API-Aufrufe problemlos mit Standardbibliotheks-Paketen implementieren.

Erstellen Sie zunächst einen API-Schlüssel von Ihrem Ollama-Konto. Für einen umfassenden Überblick über Ollama-Befehle und deren Verwendung, besuchen Sie den Ollama-Cheat-Sheet.

Legen Sie Ihren API-Schlüssel als Umgebungsvariable fest:

export OLLAMA_API_KEY="your_api_key"

Auf Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Projekt-Einrichtung

Erstellen Sie ein neues Go-Modul:

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

Grundlegende Websuche

Wie authentifiziere ich mich bei Ollamas Web-Search-API in Go? Setzen Sie den Authorization-Header mit Ihrem API-Schlüssel als Bearer-Token. Erstellen Sie einen API-Schlüssel von Ihrem Ollama-Konto und geben Sie ihn im Anfrage-Header an.

Hier ist eine vollständige Implementierung der Web-Search-API:

package main

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

// Anfrage/Antwort-Typen für 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-Umgebungsvariable nicht gesetzt")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Erstellen der Anfrage: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Lesen der Antwort: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("Fehler beim Entpacken der Antwort: %w", err)
	}

	return &searchResp, nil
}

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

	fmt.Println("Suchergebnisse:")
	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-Implementierung

Was ist der Unterschied zwischen den Endpunkten web_search und web_fetch? Der Endpunkt web_search durchsucht das Internet und gibt mehrere Suchergebnisse mit Titeln, URLs und Auszügen zurück. Der Endpunkt web_fetch 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"
)

// Anfrage/Antwort-Typen für 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-Umgebungsvariable nicht gesetzt")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Erstellen der Anfrage: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("Fehler beim Lesen der Antwort: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("Fehler beim Entpacken der Antwort: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Titel: %s\n\n", result.Title)
	fmt.Printf("Inhalt:\n%s\n\n", result.Content)
	fmt.Printf("Gefundene Links: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... und %d weitere\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Wiederverwendbares Client-Paket

Erstellen Sie ein wiederverwendbares Ollama-Client-Paket für saubereren 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-Umgebungsvariable nicht gesetzt")
	}

	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-Fehler (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)
	}

	// Suche
	results, err := client.WebSearch("Ollama neue Funktionen", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

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

Aufbau eines Suchagenten

Welche Modelle eignen sich am besten für Go-basierte Ollama-Suchagenten? Modelle mit starken Tool-Use-Fä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 rangieren müssen, sehen Sie sich unsere Anleitung zum Neu-Rangieren von Textdokumenten mit Ollama und dem Qwen3-Embedding-Modell in Go.

Hier ist eine vollständige Implementierung eines Suchagenten:

package main

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

// Chat-Typen
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 orchestriert Websuche mit 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: "Suche im Web nach aktuellen Informationen",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "Die Suchanfrage",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Holt den vollständigen Inhalt einer Webseite",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "Die URL, die abgerufen werden soll",
						},
					},
					"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-Fehler: %w", err)
		}

		messages = append(messages, response.Message)

		// Keine Tool-Aufrufe bedeuten, dass wir eine endgültige Antwort haben
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Führe Tool-Aufrufe aus
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Aufruf: %s\n", toolCall.Function.Name)

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

			// Kürze für Kontextgrenzen
			if len(result) > 8000 {
				result = result[:8000] + "... [gekürzt]"
			}

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

	return "", fmt.Errorf("Maximale Iterationen erreicht")
}

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("ungültiges Abfrageargument")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("ungültiges URL-Argument")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("unbekanntes 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("Was sind die neuesten Funktionen in Ollama?")
	if err != nil {
		fmt.Printf("Fehler: %v\n", err)
		return
	}

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

Wie kann ich große Websuchergebnisse in Go verarbeiten? Kürzen Sie den Antwortinhalt, bevor Sie ihn an den Modellkontext übergeben. Verwenden Sie String-Slicing, um den Inhalt auf etwa 8000 Zeichen zu begrenzen, um innerhalb der Kontextgrenzen zu bleiben.

Parallelsuche

Go eignet sich hervorragend für parallele Operationen. Hier ist, wie Sie mehrere Suchen parallel durchführen können. Das Verständnis 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 neueste Funktionen",
		"Lokale LLM-Implementierung",
		"AI-Suchagenten Go",
	}

	results := concurrentSearch(queries)

	for _, r := range results {
		fmt.Printf("\n🔍 Abfrage: %s\n", r.Query)
		if r.Error != nil {
			fmt.Printf("   Fehler: %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 Abbruch 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() {
	// Erstellen Sie einen Kontext mit 10-Sekunden-Timeout
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "Ollama Web-Such-API")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Anfrage ist abgelaufen")
		} else {
			fmt.Printf("Fehler: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Empfohlene Modelle

Welche Kontextlänge sollte ich für Go-Suchagenten verwenden? Setzen Sie die Kontextlänge auf etwa 32000 Tokens für eine vernünftige Leistung. Suchagenten funktionieren am besten mit voller Kontextlänge, da Websuchergebnisse umfangreich sein können. Wenn Sie Ihre Ollama-Modelle verwalten oder an verschiedene Orte verschieben müssen, sehen Sie sich unsere Anleitung zum Ollama-Modelle auf eine andere Festplatte oder einen anderen Ordner verschieben.

Modell Parameter Beste Verwendung
qwen3:4b 4B Schnelle lokale Suchen
qwen3 8B Allgemeiner Agent
gpt-oss Verschieden Forschungsaufgaben
qwen3:480b-cloud 480B Komplexe Schlussfolgerungen (Cloud)
gpt-oss:120b-cloud 120B Langform-Forschung (Cloud)
deepseek-v3.1-cloud - Fortgeschrittene Analyse (Cloud)

Für fortgeschrittene KI-Anwendungen, die Text- und Bildinhalte kombinieren, sollten Sie die cross-modale Einbettungen in Betracht ziehen, um Ihre Suchfähigkeiten über textbasierte Abfragen hinaus zu erweitern.

Beste Praktiken

  1. Fehlerbehandlung: Überprüfen Sie immer auf Fehler und behandeln Sie API-Fehlschläge elegant
  2. Timeouts: Verwenden Sie Kontext mit Timeouts für Netzwerkanfragen
  3. Rate Limiting: Respektieren Sie die API-Rate-Limits von Ollama. Seien Sie sich möglicher Änderungen der Ollama-API bewusst, wie in erste Anzeichen der Ollama-Verschlechterung diskutiert
  4. Ergebnis-Trunkierung: Trunkieren Sie Ergebnisse auf ~8000 Zeichen für Kontextlimits
  5. Parallelanfragen: Verwenden Sie Goroutines für parallele Suchen
  6. Verbindungswiederverwendung: Wiederverwenden Sie HTTP-Clients für bessere Leistung
  7. Testen: Schreiben Sie umfassende Unit-Tests für Ihre Suchimplementierungen. Folgen Sie Go Unit-Testing Best Practices, um sicherzustellen, dass Ihr Code robust und wartbar ist