L'uso dell'Ollama Web Search API in Go

Costruisci agenti di ricerca AI con Go e Ollama

Indice

L’API di ricerca web di Ollama ti permette di integrare LLM locali con informazioni in tempo reale dal web. Questa guida ti mostra come implementare le capacità di ricerca web in Go, dal semplice utilizzo dell’API alle funzionalità complete degli agenti di ricerca.

gopher biking

Getting Started

Ollama ha una libreria ufficiale per la ricerca web in Go? Ollama fornisce un’API REST per la ricerca web che funziona con qualsiasi client HTTP in Go. Sebbene non esista ancora una libreria ufficiale Go per la ricerca web, puoi facilmente implementare le chiamate API utilizzando i pacchetti della libreria standard.

Per prima cosa, crea una chiave API dal tuo account Ollama. Per un riferimento completo sui comandi e sull’utilizzo di Ollama, consulta la guida rapida di Ollama.

Imposta la tua chiave API come variabile di ambiente:

export OLLAMA_API_KEY="your_api_key"

Su Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Setup del Progetto

Crea un nuovo modulo Go:

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

Ricerca Web Base

Come autenticarsi con l’API di ricerca web di Ollama in Go? Imposta l’intestazione Authorization con la tua chiave API come token Bearer. Crea una chiave API dal tuo account Ollama e passala nell’intestazione della richiesta.

Ecco un’implementazione completa dell’API di ricerca web:

package main

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

// Tipi di richiesta/risposta per 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("variabile di ambiente OLLAMA_API_KEY non impostata")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("fallito nel marshalling della richiesta: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("fallito nel leggere la risposta: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("fallito nel unmarshalling della risposta: %w", err)
	}

	return &searchResp, nil
}

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

	fmt.Println("Risultati della ricerca:")
	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] + "..."
}

Implementazione di Fetch Web

Qual è la differenza tra gli endpoint web_search e web_fetch? L’endpoint web_search effettua una query sul web e restituisce diversi risultati di ricerca con titoli, URL e snippet. L’endpoint web_fetch recupera il contenuto completo di un URL specifico, restituendo il titolo della pagina, il contenuto e i collegamenti.

package main

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

// Tipi di richiesta/risposta per 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("variabile di ambiente OLLAMA_API_KEY non impostata")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("fallito nel marshalling della richiesta: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("fallito nel leggere la risposta: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("fallito nel unmarshalling della risposta: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Titolo: %s\n\n", result.Title)
	fmt.Printf("Contenuto:\n%s\n\n", result.Content)
	fmt.Printf("Collegamenti trovati: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... e %d altri\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Pacchetto Client Riutilizzabile

Crea un pacchetto client riutilizzabile per Ollama per un codice più pulito:

// 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("variabile di ambiente OLLAMA_API_KEY non impostata")
	}

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

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

	return &result, nil
}

Utilizzo:

package main

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

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

	// Ricerca
	results, err := client.WebSearch("Nuove funzionalità di Ollama", 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("Pagina: %s\n", page.Title)
}

Costruzione di un Agente di Ricerca

Quali modelli funzionano meglio per agenti di ricerca basati su Go per Ollama? I modelli con forti capacità di utilizzo degli strumenti funzionano meglio, tra cui qwen3, gpt-oss e modelli cloud come qwen3:480b-cloud e deepseek-v3.1-cloud. Se stai lavorando con modelli Qwen3 e devi elaborare o riorientare i risultati della ricerca, consulta la nostra guida su riordinare documenti di testo con Ollama e modello Qwen3 Embedding in Go.

Ecco un’implementazione completa di un agente di ricerca:

package main

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

// Tipi di chat
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 coordina la ricerca web con 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: "Cerca sul web informazioni attuali",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "La query di ricerca",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Recupera il contenuto completo di una pagina web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "L'URL da recuperare",
						},
					},
					"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("errore nella chat: %w", err)
		}

		messages = append(messages, response.Message)

		// Nessun richiamo degli strumenti significa che abbiamo una risposta finale
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Esegui i richiami degli strumenti
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Richiamo: %s\n", toolCall.Function.Name)

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

			// Tronca per i limiti del contesto
			if len(result) > 8000 {
				result = result[:8000] + "... [troncato]"
			}

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

	return "", fmt.Errorf("raggiunto il numero massimo di iterazioni")
}

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("argomento query non valido")
		}
		return a.webSearch(query)

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

	default:
		return "", fmt.Errorf("strumento sconosciuto: %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("Quali sono le ultime funzionalità in Ollama?")
	if err != nil {
		fmt.Printf("Errore: %v\n", err)
		return
	}

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

Come gestire risposte di ricerca web molto grandi in Go? Tronca il contenuto della risposta prima di passarlo al contesto del modello. Utilizza la slicing delle stringhe per limitare il contenuto a circa 8000 caratteri per adattarlo ai limiti del contesto.

Ricerca Concurrente

Go eccelle nelle operazioni concorrenti. Ecco come eseguire diverse ricerche in parallelo. Comprendere come Ollama gestisce le richieste parallele può aiutarti a ottimizzare le tue implementazioni di ricerca concorrente.

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 ricercaConcorrente(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{
		"Le ultime funzionalità di Ollama",
		"deployamento LLM locale",
		"agenti di ricerca AI in Go",
	}

	results := ricercaConcorrente(queries)

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

Contesto e Annullamento

Aggiungi il supporto al contesto per i timeout e l’annullamento:

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() {
	// Crea contesto con timeout di 10 secondi
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API di ricerca web di Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Richiesta scaduta")
		} else {
			fmt.Printf("Errore: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Modelli Consigliati

Qual è la lunghezza del contesto che dovrei utilizzare per gli agenti di ricerca in Go? Imposta la lunghezza del contesto a circa 32000 token per un’adeguata prestazione. Gli agenti di ricerca funzionano meglio con la lunghezza completa del contesto poiché i risultati della ricerca web possono essere estesi. Se devi gestire i tuoi modelli Ollama o spostarli in posizioni diverse, consulta la nostra guida su come spostare i modelli Ollama in un diverso disco o cartella.

Modello Parametri Migliore Per
qwen3:4b 4B Ricerche locali rapide
qwen3 8B Agente generico
gpt-oss Vari Compiti di ricerca
qwen3:480b-cloud 480B Ragionamento complesso (cloud)
gpt-oss:120b-cloud 120B Ricerca lunga (cloud)
deepseek-v3.1-cloud - Analisi avanzata (cloud)

Per applicazioni AI avanzate che combinano contenuti testuali e visivi, considera l’explorazione di embedding cross-modal per estendere le tue capacità di ricerca oltre le query testuali.

Linee Guida per la Migliore Pratica

  1. Gestione degli errori: Controlla sempre per gli errori e gestisci le fallite delle API in modo gentile
  2. Timeout: Utilizza il contesto con i timeout per le richieste di rete
  3. Limiti di velocità: Rispetta i limiti di velocità dell’API di Ollama. Sii consapevole di potenziali modifiche all’API di Ollama, come discusso in primi segni di Ollama enshittification
  4. Troncamento dei risultati: Tronca i risultati a circa 8000 caratteri per i limiti del contesto
  5. Richieste concorrenti: Utilizza le goroutine per le ricerche parallele
  6. Riuso delle connessioni: Riusa il client HTTP per una migliore prestazione
  7. Test: Scrivi test unitari completi per le tue implementazioni di ricerca. Segui le migliori pratiche per i test unitari in Go per assicurarti che il tuo codice sia robusto e mantenibile