Usando la API de búsqueda web de Ollama en Go
Construya agentes de búsqueda de IA con Go y Ollama
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.

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
- Manejo de Errores: Siempre verifique errores y maneje fallas de API de manera amable
- Tiempo de espera: Use contexto con tiempos de espera para solicitudes de red
- 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
- Recorte de resultados: Recorte los resultados a ~8000 caracteres para límites de contexto
- Solicitudes concurrentes: Use goroutines para búsquedas paralelas
- Reutilización de conexión: Reutilice el cliente HTTP para un mejor rendimiento
- 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
- Hoja de trucos de Ollama
- Reordenamiento de documentos de texto con Ollama y modelo de incrustación Qwen3 - en Go
- Incrustaciones de Modalidad Cruzada: Conectando Modalidades de IA
- Pruebas Unitarias en Go: Estructura y Mejores Prácticas
- Cómo Mover Modelos de Ollama a Diferente Unidad o Carpeta
- Cómo Ollama Maneja Solicitudes Paralelas
- Primeras Señales de Enshittificación de Ollama
- Blog de Búsqueda Web de Ollama
- Documentación Oficial de Ollama
- Ejemplos de Ollama en Go
- Repositorio de GitHub de Ollama