Usando la API de búsqueda web de Ollama en Go

Construya agentes de búsqueda de IA con Go y Ollama

Índice

La API de búsqueda web de Ollama le permite mejorar los LLM locales con información en tiempo real de la web. Esta guía le muestra cómo implementar capacidades de búsqueda web en Go, desde llamadas simples a la API hasta agentes de búsqueda completos.

gopher biking

Comenzando

¿Tiene Ollama una biblioteca oficial de Go para la búsqueda web? Ollama proporciona una API REST para la búsqueda web que funciona con cualquier cliente HTTP de Go. Aunque no hay una SDK oficial de Go para la búsqueda web todavía, puede implementar fácilmente las llamadas a la API usando paquetes de la biblioteca estándar.

Primero, cree una clave API desde su cuenta de Ollama. Para una referencia completa sobre los comandos y el uso de Ollama, consulte la hoja de trucos de Ollama.

Establezca su clave API como una variable de entorno:

export OLLAMA_API_KEY="your_api_key"

En PowerShell de Windows:

$env:OLLAMA_API_KEY = "your_api_key"

Configuración del Proyecto

Cree un nuevo módulo de Go:

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

Búsqueda Web Básica

¿Cómo autenticarse con la API de búsqueda web de Ollama en Go? Establezca el encabezado Authorization con su clave API como token Bearer. Cree una clave API desde su cuenta de Ollama y pásela en el encabezado de la solicitud.

Aquí hay una implementación completa de la API de búsqueda web:

package main

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

// Tipos de solicitud/respuesta para 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 de entorno OLLAMA_API_KEY no establecida")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("no se pudo serializar la solicitud: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("no se pudo crear la solicitud: %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("solicitud fallida: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("no se pudo leer la respuesta: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("no se pudo deserializar la respuesta: %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("¿Qué es Ollama?", 5)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Println("Resultados de búsqueda:")
	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] + "..."
}

Implementación de Web Fetch

¿Cuál es la diferencia entre los endpoints web_search y web_fetch? El endpoint web_search consulta la internet y devuelve varios resultados de búsqueda con títulos, URLs y fragmentos. El endpoint web_fetch recupera el contenido completo de una URL específica, devolviendo el título de la página, el contenido y los enlaces.

package main

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

// Tipos de solicitud/respuesta para 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 de entorno OLLAMA_API_KEY no establecida")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("no se pudo serializar la solicitud: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("no se pudo crear la solicitud: %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("solicitud fallida: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("no se pudo leer la respuesta: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("no se pudo deserializar la respuesta: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Título: %s\n\n", result.Title)
	fmt.Printf("Contenido:\n%s\n\n", result.Content)
	fmt.Printf("Enlaces encontrados: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... y %d más\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Paquete de Cliente Reutilizable

Cree un paquete de cliente reutilizable de Ollama para código más limpio:

// 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 de entorno OLLAMA_API_KEY no establecida")
	}

	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[Fetch端](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("error de API (estado %d): %s", resp.StatusCode, string(body))
	}

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

	return &result, nil
}

Uso:

package main

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

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

	// Buscar
	results, err := client.WebSearch("nuevas características de Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

	fmt.Printf("Página: %s\n", page.Title)
}

Construyendo un Agente de Búsqueda

¿Cuáles son los modelos que funcionan mejor para agentes de búsqueda de Ollama basados en Go? Los modelos con fuertes capacidades de uso de herramientas funcionan mejor, incluyendo qwen3, gpt-oss y modelos en la nube como qwen3:480b-cloud y deepseek-v3.1-cloud. Si está trabajando con modelos Qwen3 y necesita procesar o reordenar resultados de búsqueda, consulte nuestra guía sobre reordenamiento de documentos de texto con Ollama y modelo de incrustación Qwen3 en Go.

Aquí hay una implementación completa de un agente de búsqueda:

package main

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

// Tipos 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 orquesta la búsqueda 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: "Buscar en la web información actual",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "La consulta de búsqueda",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Obtener el contenido completo de una página web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "La URL a obtener",
						},
					},
					"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("error de chat: %w", err)
		}

		messages = append(messages, response.Message)

		// No hay llamadas a herramientas significa que tenemos una respuesta final
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Ejecutar llamadas a herramientas
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Llamando: %s\n", toolCall.Function.Name)

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

			// Recortar para límites de contexto
			if len(result) > 8000 {
				result = result[:8000] + "... [recortado]"
			}

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

	return "", fmt.Errorf("se alcanzó el número máximo de iteraciones")
}

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("argumento de consulta inválido")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("argumento de URL inválido")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("herramienta desconocida: %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("¿Cuáles son las últimas características de Ollama?")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

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

¿Cómo manejar grandes respuestas de búsqueda web en Go? Recorte el contenido de la respuesta antes de pasarlo al contexto del modelo. Use la rebanada de cadena para limitar el contenido a aproximadamente 8000 caracteres para ajustarse a los límites de contexto.

Búsqueda Concurrente

Go destaca en operaciones concurrentes. Aquí hay cómo realizar múltiples búsquedas en paralelo. Entender cómo Ollama maneja las solicitudes paralelas puede ayudarle a optimizar sus implementaciones de búsqueda 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{
		"últimas características de Ollama",
		"implementación local de LLM",
		"agentes de búsqueda de AI en Go",
	}

	results := concurrentSearch(queries)

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

Contexto y Cancelación

Agregue soporte de contexto para tiempos de espera y cancelación:

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() {
	// Crear contexto con tiempo de espera de 10 segundos
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API de búsqueda web de Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Solicitud agotó el tiempo")
		} else {
			fmt.Printf("Error: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Modelos Recomendados

¿Qué longitud de contexto debo usar para agentes de búsqueda de Go? Establezca la longitud de contexto a aproximadamente 32000 tokens para un rendimiento razonable. Los agentes de búsqueda funcionan mejor con la longitud completa de contexto ya que los resultados de búsqueda pueden ser extensos. Si necesita administrar sus modelos de Ollama o moverlos a diferentes ubicaciones, consulte nuestra guía sobre cómo mover modelos de Ollama a diferentes unidades o carpetas.

Modelo Parámetros Mejor Para
qwen3:4b 4B Búsquedas locales rápidas
qwen3 8B Agente general
gpt-oss Varios Tareas de investigación
qwen3:480b-cloud 480B Razonamiento complejo (en la nube)
gpt-oss:120b-cloud 120B Investigación de larga forma (en la nube)
deepseek-v3.1-cloud - Análisis avanzado (en la nube)

Para aplicaciones de IA avanzadas que combinen contenido de texto y visual, considere explorar incrustaciones de modalidad cruzada para extender sus capacidades de búsqueda más allá de las consultas de texto solo.

Buenas Prácticas

  1. Manejo de Errores: Siempre verifique errores y maneje fallas de API de manera amable
  2. Tiempo de espera: Use contexto con tiempos de espera para solicitudes de red
  3. Límites de tasa: Respete los límites de tasa de API de Ollama. Esté atento a posibles cambios en la API de Ollama, como se discute en primeras señales de enshittificación de Ollama
  4. Recorte de resultados: Recorte los resultados a ~8000 caracteres para límites de contexto
  5. Solicitudes concurrentes: Use goroutines para búsquedas paralelas
  6. Reutilización de conexión: Reutilice el cliente HTTP para un mejor rendimiento
  7. Pruebas: Escriba pruebas unitarias exhaustivas para sus implementaciones de búsqueda. Siga mejores prácticas para pruebas unitarias en Go para asegurar que su código sea robusto y mantenible

Enlaces Útiles