Utilisation de l’API de recherche web Ollama en Go

Créer des agents de recherche IA avec Go et Ollama

Sommaire

L’API Web Search d’Ollama vous permet d’enrichir les LLM locaux avec des informations web en temps réel. Ce guide vous montre comment implémenter les fonctionnalités de recherche web en Go, depuis des appels API simples jusqu’à des agents de recherche complets.

gopher bicyclette

Démarrage

Ollama dispose-t-il d’une bibliothèque officielle Go pour la recherche web ? Ollama fournit une API REST pour la recherche web qui fonctionne avec n’importe quel client HTTP Go. Bien qu’il n’existe pas encore de SDK Go officiel pour la recherche web, vous pouvez facilement implémenter les appels API en utilisant des packages de la bibliothèque 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 le tableau de bord Ollama.

Définissez votre clé API comme 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 s’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 de la sérialisation 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("échec de la requête : %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 de la désérialisation 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 du Web Fetch

Quelle est la différence entre les points de terminaison web_search et web_fetch ? Le point de terminaison web_search interroge Internet et renvoie plusieurs résultats de recherche avec des titres, des URL et des extraits. Le point de terminaison web_fetch récupère le contenu complet d’une URL spécifique, en 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 de la sérialisation 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("échec de la requête : %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 de la désérialisation 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 de plus\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 d’utilisation d’outils solides fonctionnent le mieux, y compris 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 avez besoin de traiter ou de reclasser des résultats de recherche, consultez notre guide sur la reclassification de documents texte avec le modèle Ollama et Qwen3 Embedding en Go.

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

package main

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

// Types de discussion
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 un 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 des informations actuelles sur le web",
				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 discussion : %w", err)
		}

		messages = append(messages, response.Message)

		// L'absence d'appels d'outils 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("nombre maximal d'itérations atteint")
}

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 grandes réponses de recherche web en Go ? Tronquez le contenu de la réponse avant de le transmettre au contexte du modèle. Utilisez le tranchage de chaînes pour limiter le contenu à environ 8000 caractères pour qu’il s’adapte aux limites de contexte.

Recherche Concurrente

Go excelle dans les opérations concurrentes. 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 concurrente.

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{
		"Dernières fonctionnalités d'Ollama",
		"Déploiement de LLM locaux",
		"Agents de recherche IA 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 d’attente (timeouts) 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 dois-je utiliser pour les agents de recherche Go ? Définissez la longueur du contexte à environ 32 000 jetons pour une performance raisonnable. Les agents de recherche fonctionnent le mieux avec la longueur de contexte complète, car les résultats de la recherche web peuvent être étendus. Si vous avez besoin de gérer vos modèles Ollama ou de les déplacer vers des emplacements différents, consultez notre guide sur comment déplacer les modèles Ollama vers un disque ou un dossier différent.

Modèle Paramètres Idéal pour
qwen3:4b 4B Recherches locales rapides
qwen3 8B Agent à usage général
gpt-oss Divers Tâches de recherche
qwen3:480b-cloud 480B Raisonnement complexe (cloud)
gpt-oss:120b-cloud 120B Recherche en format long (cloud)
deepseek-v3.1-cloud - Analyse avancée (cloud)

Bonnes Pratiques

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

Liens Utiles

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.