Använda Ollama Web Search API i Go

Bygg AI-söklägenheter med Go och Ollama

Sidinnehåll

Ollamas webbsöknings-API låter dig komplettera lokala LLMs med realtidsinformation från webben. Den här guiden visar hur du implementerar webbsökningsfunktioner i Go, från enkla API-anrop till fullständiga sökningsagenter.

gopher biking

Kom igång

Har Ollama en officiell Go-bibliotek för webbsökning? Ollama erbjuder ett REST-API för webbsökning som fungerar med vilken Go HTTP-klient som helst. Även om det inte finns någon officiell Go-SDK för webbsökning ännu, kan du enkelt implementera API-anropen med standardbibliotekspaket.

Skapa först ett API-nyckel från ditt Ollama-konto. För en omfattande referens om Ollama-kommandon och användning, kolla in Ollama cheatsheet.

Ange ditt API-nyckel som en miljövariabel:

export OLLAMA_API_KEY="din_api_nyckel"

På Windows PowerShell:

$env:OLLAMA_API_KEY = "din_api_nyckel"

Projektinställning

Skapa ett nytt Go-modul:

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

Grundläggande webbsökning

Hur autentiserar jag mig med Ollamas webbsöknings-API i Go? Ange Authorization-rubriken med ditt API-nyckel som en Bearer-token. Skapa ett API-nyckel från ditt Ollama-konto och skicka det i begäransrubriken.

Här är en fullständig implementering av webbsöknings-API:

package main

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

// Request/Response typer 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 miljövariabel inte inställd")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att marshala begäran: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att skapa begäran: %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("begäran misslyckades: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att läsa svar: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("misslyckades med att unmarshala svar: %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("Vad är Ollama?", 5)
	if err != nil {
		fmt.Printf("Fel: %v\n", err)
		return
	}

	fmt.Println("Sökresultat:")
	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-implementering

Vad är skillnaden mellan web_search och web_fetch-endpunkter? web_search-endpunkten frågar internet och returnerar flera sökträffar med titlar, URL:er och utdrag. web_fetch-endpunkten hämtar fullständigt innehåll från en specifik URL, returnerar sidtiteln, innehållet och länkarna.

package main

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

// Request/Response typer 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 miljövariabel inte inställd")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att marshala begäran: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att skapa begäran: %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("begäran misslyckades: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("misslyckades med att läsa svar: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("misslyckades med att unmarshala svar: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Titel: %s\n\n", result.Title)
	fmt.Printf("Innehåll:\n%s\n\n", result.Content)
	fmt.Printf("Hittade länkar: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... och %d till\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Återanvändbar klientpaket

Skapa ett återanvändbart Ollama-klientpaket för renare kod:

// 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 miljövariabel inte inställd")
	}

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

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

	return &result, nil
}

Användning:

package main

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

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

	// Sök
	results, err := client.WebSearch("Ollama nya funktioner", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

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

Bygga en söksagent

Vilka modeller fungerar bäst för Go-baserade Ollama-söksagenter? Modeller med starka verktygsanvändningsförmågor fungerar bäst, inklusive qwen3, gpt-oss, och molnmodeller som qwen3:480b-cloud och deepseek-v3.1-cloud. Om du arbetar med Qwen3-modeller och behöver bearbeta eller rangordna sökträffar, se vår guide om att rangordna textdokument med Ollama och Qwen3 Embedding-modell i Go.

Här är en komplett implementering av en söksagent:

package main

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

// Chat typer
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 organiserar webbsökning med 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: "Sök på webben för aktuell information",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "Sökfrågan",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Hämta fullständigt innehåll från en webbsida",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "URL:n att hämta",
						},
					},
					"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)

		// Inga verktygsanrop betyder att vi har ett slutgiltigt svar
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Utför verktygsanrop
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Anropar: %s\n", toolCall.Function.Name)

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

			// Avkorta för kontextbegränsningar
			if len(result) > 8000 {
				result = result[:8000] + "... [avkortat]"
			}

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

	return "", fmt.Errorf("max antal iterationer nådd")
}

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("ogiltigt frågeargument")
		}
		return a.webSearch(query)

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

	default:
		return "", fmt.Errorf("okänt verktyg: %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("Vad är de senaste funktionerna i Ollama?")
	if err != nil {
		fmt.Printf("Fel: %v\n", err)
		return
	}

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

Hur hanterar jag stora webbsöksrespons i Go? Avkorta innehållet innan du skickar det till modellens kontext. Använd strängskärning för att begränsa innehållet till cirka 8000 tecken för att passa inom kontextbegränsningarna.

Samtidig sökning

Go är utmärkt för samtidiga operationer. Här är hur du utför flera sökningar parallellt. Att förstå hur Ollama hanterar parallella begäranden kan hjälpa dig att optimera dina samtidiga sökimplementeringar.

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 senaste funktioner",
		"lokal LLM-distribution",
		"AI-söksagenter Go",
	}

	results := concurrentSearch(queries)

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

Lägg till kontextstöd för tidsgränser och avbrytning:

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() {
	// Skapa kontext med 10 sekunders tidsgräns
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "Ollama webbsöks-API")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Begäran tog för lång tid")
		} else {
			fmt.Printf("Fel: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Rekommenderade modeller

Vilken kontextlängd bör jag använda för Go-söksagenter? Använd en kontextlängd på cirka 32000 tokens för rimlig prestanda. Söksagenter fungerar bäst med full kontextlängd eftersom webbsökträffar kan vara omfattande. Om du behöver hantera dina Ollama-modeller eller flytta dem till olika platser, se vår guide om hur man flyttar Ollama-modeller till olika disk eller mapp.

Modell Parametrar Bäst för
qwen3:4b 4B Snabba lokala sökningar
qwen3 8B Allmänt ändamål
gpt-oss Olika Forskningsuppgifter
qwen3:480b-cloud 480B Komplex resonemang (moln)
gpt-oss:120b-cloud 120B Långformad forskning (moln)
deepseek-v3.1-cloud - Avancerad analys (moln)

För avancerade AI-applikationer som kombinerar text och visuellt innehåll, överväg att utforska korsmodalitetskodningar för att utöka dina sökmöjligheter bortom textbaserade frågor.

Bäst praxis

  1. Felhantering: Kontrollera alltid efter fel och hantera API-fel på ett smidigt sätt
  2. Tidbegränsningar: Använd kontext med tidbegränsningar för nätverksförfrågningar
  3. Hastighetsbegränsningar: Respektera Ollamas API-hastighetsbegränsningar. Var medveten om potentiella förändringar i Ollamas API, som diskuteras i första tecknen på Ollama enshittification
  4. Resultattrunkering: Trunkera resultat till ~8000 tecken för kontextbegränsningar
  5. Samverkande förfrågningar: Använd goroutines för parallella sökningar
  6. Anslutningsåteranvändning: Återanvänd HTTP-klient för bättre prestanda
  7. Testning: Skriv omfattande enhetstester för dina sökimplementeringar. Följ Go enhetstestning bäst praxis

Användbara länkar