Utiliser l'API de recherche web d'Ollama en Go

Construisez des agents de recherche IA avec Go et Ollama

Sommaire

L’API de recherche web d’Ollama vous permet d’augmenter les LLM locaux avec des informations en temps réel du web. Ce guide vous montre comment implémenter des capacités de recherche web en Go, des appels d’API simples aux agents de recherche complets.

gopher biking

Getting Started

Ollama a-t-il une bibliothèque Go officielle pour la recherche web ? Ollama fournit une API REST pour la recherche web qui fonctionne avec tout client HTTP Go. Bien qu’il n’y ait pas encore de SDK Go officiel pour la recherche web, vous pouvez facilement implémenter les appels d’API à l’aide des packages standard.

Tout d’abord, créez une clé API à partir de votre compte Ollama. Pour une référence complète sur les commandes et l’utilisation d’Ollama, consultez la feuille de rappel Ollama.

Définissez votre clé API en tant que variable d’environnement :

export OLLAMA_API_KEY="your_api_key"

Sur Windows PowerShell :

$env:OLLAMA_API_KEY = "your_api_key"

Configuration du projet

Créez un nouveau module Go :

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

Recherche web de base

Comment m’authentifier avec l’API de recherche web d’Ollama en Go ? Définissez l’en-tête Authorization avec votre clé API en tant que jeton Bearer. Créez une clé API à partir de votre compte Ollama et passez-la dans l’en-tête de la requête.

Voici une implémentation complète de l’API de recherche web :

package main

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

// Types de requête/réponse pour 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("variable d'environnement OLLAMA_API_KEY non définie")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("échec du marshalling de la requête : %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("échec de la création de la requête : %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("requête échouée : %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("échec de la lecture de la réponse : %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("échec du démarshalling de la réponse : %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("Qu'est-ce que Ollama ?", 5)
	if err != nil {
		fmt.Printf("Erreur : %v\n", err)
		return
	}

	fmt.Println("Résultats de la recherche :")
	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] + ". .."
}

Implémentation de la récupération web

Quelle est la différence entre les points de terminaison web_search et web_fetch ? Le point de terminaison web_search interroge l’internet et renvoie plusieurs résultats de recherche avec des titres, des URLs et des extraits. Le point de terminaison web_fetch récupère le contenu complet d’une URL spécifique, renvoyant le titre de la page, le contenu et les liens.

package main

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

// Types de requête/réponse pour 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("variable d'environnement OLLAMA_API_KEY non définie")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("échec du marshalling de la requête : %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("échec de la création de la requête : %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("requête échouée : %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("échec de la lecture de la réponse : %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("échec du démarshalling de la réponse : %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Titre : %s\n\n", result.Title)
	fmt.Printf("Contenu : \n%s\n\n", result.Content)
	fmt.Printf("Liens trouvés : %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... et %d autres\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Package client réutilisable

Créez un package client Ollama réutilisable pour un code plus propre :

// 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("variable d'environnement OLLAMA_API_KEY non définie")
	}

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

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

	return &result, nil
}

Utilisation :

package main

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

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

	// Recherche
	results, err := client.WebSearch("Nouvelles fonctionnalités d'Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

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

Construction d’un agent de recherche

Quels modèles fonctionnent le mieux pour les agents de recherche Ollama basés sur Go ? Les modèles avec des capacités fortes d’utilisation d’outils fonctionnent le mieux, notamment qwen3, gpt-oss, et les modèles cloud comme qwen3:480b-cloud et deepseek-v3.1-cloud. Si vous travaillez avec des modèles Qwen3 et que vous avez besoin de traiter ou de reclasser les résultats de recherche, consultez notre guide sur le reclassage de documents textuels avec Ollama et le modèle d’embedding Qwen3 en Go.

Voici une implémentation complète d’agent de recherche :

package main

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

// Types de 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 orchestre la recherche web avec 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: "Rechercher sur le web des informations actuelles",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "La requête de recherche",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Récupérer le contenu complet d'une page web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "L'URL à récupérer",
						},
					},
					"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("erreur de chat : %w", err)
		}

		messages = append(messages, response.Message)

		// Aucun appel d'outil signifie que nous avons une réponse finale
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Exécuter les appels d'outils
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Appel : %s\n", toolCall.Function.Name)

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

			// Tronquer pour les limites de contexte
			if len(result) > 8000 {
				result = result[:8000] + "... [tronqué]"
			}

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

	return "", fmt.Errorf("itérations maximales atteintes")
}

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("argument de requête invalide")
		}
		return a.webSearch(query)

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

	default:
		return "", fmt.Errorf("outil inconnu : %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("Quelles sont les dernières fonctionnalités d'Ollama ?")
	if err != nil {
		fmt.Printf("Erreur : %v\n", err)
		return
	}

	fmt.Println("\n📝 Réponse :")
	fmt.Println(answer)
}

Comment gérer les réponses de recherche web volumineuses en Go ? Tronquer le contenu de la réponse avant de le passer au contexte du modèle. Utilisez la troncature de chaîne pour limiter le contenu à environ 8000 caractères pour s’adapter aux limites de contexte.

Recherche concurrente

Go excelle dans les opérations concourantes. Voici comment effectuer plusieurs recherches en parallèle. Comprendre comment Ollama gère les requêtes parallèles peut vous aider à optimiser vos implémentations de recherche concourante.

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 dernières fonctionnalités",
		"déploiement LLM local",
		"agents de recherche AI Go",
	}

	results := concurrentSearch(queries)

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

Contexte et annulation

Ajoutez le support du contexte pour les délais et l’annulation :

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() {
	// Créer un contexte avec un délai de 10 secondes
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API de recherche web d'Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("La requête a expiré")
		} else {
			fmt.Printf("Erreur : %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Modèles recommandés

Quelle longueur de contexte devrais-je utiliser pour les agents de recherche Go ? Définissez la longueur de contexte à environ 32000 tokens pour une performance raisonnable. Les agents de recherche fonctionnent le mieux avec une longueur de contexte complète car les résultats de recherche web peuvent être étendus. Si vous avez besoin de gérer vos modèles Ollama ou de les déplacer vers différents emplacements, consultez notre guide sur comment déplacer les modèles Ollama vers un autre disque ou dossier.

Modèle Paramètres Meilleur pour
qwen3:4b 4B Recherches locales rapides
qwen3 8B Agent général
gpt-oss Variés Tâches de recherche
qwen3:480b-cloud 480B Raisonnement complexe (cloud)
gpt-oss:120b-cloud 120B Recherche longue forme (cloud)
deepseek-v3.1-cloud - Analyse avancée (cloud)

Pour des applications avancées d’IA qui combinent du contenu textuel et visuel, envisagez d’explorer les embeddings multimodaux pour étendre vos capacités de recherche au-delà des requêtes textuelles uniquement.

Bonnes pratiques

  1. Gestion des erreurs : Vérifiez toujours les erreurs et gérez les échecs d’API de manière gracieuse
  2. Délais : Utilisez le contexte avec des délais pour les requêtes réseau
  3. Limitation de débit : Respectez les limites de débit de l’API Ollama. Soyez conscient des changements potentiels de l’API Ollama, comme discuté dans les premiers signes de l’enshittification d’Ollama
  4. Troncature des résultats : Tronquez les résultats à ~8000 caractères pour les limites de contexte
  5. Requêtes concourantes : Utilisez des goroutines pour des recherches parallèles
  6. Réutilisation des connexions : Réutilisez le client HTTP pour une meilleure performance
  7. Tests : Écrivez des tests unitaires complets pour vos implémentations de recherche. Suivez les meilleures pratiques de tests unitaires en Go pour vous assurer que votre code est robuste et maintenable

Liens utiles