Usando a API de Busca Web do Ollama em Go

Construa agentes de busca com IA usando Go e Ollama

Conteúdo da página

A API de Pesquisa Web da Ollama permite aprimorar LLMs locais com informações da web em tempo real. Este guia mostra como implementar capacidades de pesquisa web em Go, desde chamadas simples de API até agentes de pesquisa completos.

gopher de bicicleta

Introdução

A Ollama possui uma biblioteca oficial em Go para pesquisa web? A Ollama oferece uma API REST para pesquisa web que funciona com qualquer cliente HTTP em Go. Embora não haja um SDK oficial em Go para pesquisa web ainda, você pode implementar facilmente as chamadas de API usando pacotes da biblioteca padrão.

Primeiro, crie uma chave de API a partir da sua conta Ollama. Para uma referência abrangente sobre comandos e uso da Ollama, consulte a folha de dicas da Ollama.

Defina sua chave de API como uma variável de ambiente:

export OLLAMA_API_KEY="your_api_key"

No PowerShell do Windows:

$env:OLLAMA_API_KEY = "your_api_key"

Configuração do Projeto

Crie um novo módulo Go:

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

Pesquisa Web Básica

Como faço para autenticar na API de pesquisa web da Ollama em Go? Defina o cabeçalho Authorization com sua chave de API como um token Bearer. Crie uma chave de API a partir da sua conta Ollama e passe-a no cabeçalho da solicitação.

Aqui está uma implementação completa da API de pesquisa web:

package main

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

// Tipos de Solicitação/Resposta 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("variável de ambiente OLLAMA_API_KEY não definida")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("falha ao serializar solicitação: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("falha ao criar solicitação: %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("solicitação falhou: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("falha ao ler resposta: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("falha ao desserializar resposta: %w", err)
	}

	return &searchResp, nil
}

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

	fmt.Println("Resultados da Pesquisa:")
	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] + "..."
}

Implementação de Web Fetch

Qual é a diferença entre os endpoints web_search e web_fetch? O endpoint web_search consulta a internet e retorna vários resultados de pesquisa com títulos, URLs e trechos. O endpoint web_fetch recupera o conteúdo completo de uma URL específica, retornando o título da página, o conteúdo e os links.

package main

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

// Tipos de Solicitação/Resposta 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("variável de ambiente OLLAMA_API_KEY não definida")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("falha ao serializar solicitação: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("falha ao criar solicitação: %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("solicitação falhou: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("falha ao ler resposta: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("falha ao desserializar resposta: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Título: %s\n\n", result.Title)
	fmt.Printf("Conteúdo:\n%s\n\n", result.Content)
	fmt.Printf("Links encontrados: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... e mais %d\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Pacote de Cliente Reutilizável

Crie um pacote de cliente Ollama reutilizável para um código mais limpo:

// 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("variável de ambiente OLLAMA_API_KEY não definida")
	}

	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("erro na 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
}

Uso:

package main

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

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

	// Pesquisa
	results, err := client.WebSearch("Ollama new features", 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("Página: %s\n", page.Title)
}

Construindo um Agente de Pesquisa

Quais modelos funcionam melhor para agentes de pesquisa Ollama baseados em Go? Modelos com fortes capacidades de uso de ferramentas funcionam melhor, incluindo qwen3, gpt-oss e modelos em nuvem como qwen3:480b-cloud e deepseek-v3.1-cloud. Se você está trabalhando com modelos Qwen3 e precisa processar ou reordenar resultados de pesquisa, veja nosso guia sobre reordenação de documentos de texto com Ollama e o modelo de incorporação Qwen3 em Go.

Aqui está uma implementação completa de um agente de pesquisa:

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 orquestra a pesquisa web com 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: "Pesquise na web por informações atuais",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "A consulta de pesquisa",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Recupere o conteúdo completo de uma página web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "A URL para recuperar",
						},
					},
					"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("erro no chat: %w", err)
		}

		messages = append(messages, response.Message)

		// Nenhuma chamada de ferramenta significa que temos uma resposta final
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Executar chamadas de ferramenta
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Chamando: %s\n", toolCall.Function.Name)

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

			// Truncar para limites de contexto
			if len(result) > 8000 {
				result = result[:8000] + "... [truncado]"
			}

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

	return "", fmt.Errorf("máximo de iterações atingido")
}

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("ferramenta desconhecida: %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("What are the latest features in Ollama?")
	if err != nil {
		fmt.Printf("Erro: %v\n", err)
		return
	}

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

Como faço para lidar com grandes respostas de pesquisa web em Go? Trunque o conteúdo da resposta antes de passá-lo ao contexto do modelo. Use fatiamento de string para limitar o conteúdo a aproximadamente 8000 caracteres para caber dentro dos limites de contexto.

Pesquisa Concorrente

O Go é excelente em operações concorrentes. Veja como executar várias pesquisas em paralelo. Compreender como a Ollama lida com solicitações paralelas pode ajudar a otimizar suas implementações de pesquisa 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 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 latest features",
		"local LLM deployment",
		"AI search agents Go",
	}

	results := concurrentSearch(queries)

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

Adicione suporte a contexto para tempos limite e cancelamento:

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() {
	// Criar contexto com tempo limite de 10 segundos
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "Ollama web search API")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Tempo limite da solicitação excedido")
		} else {
			fmt.Printf("Erro: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Modelos Recomendados

Qual comprimento de contexto devo usar para agentes de pesquisa em Go? Defina o comprimento do contexto para aproximadamente 32000 tokens para um desempenho razoável. Agentes de pesquisa funcionam melhor com o comprimento de contexto total, pois os resultados de pesquisa web podem ser extensos. Se você precisa gerenciar seus modelos Ollama ou movê-los para diferentes localizações, veja nosso guia sobre como mover modelos Ollama para outro drive ou pasta.

Modelo Parâmetros Melhor Para
qwen3:4b 4B Pesquisas locais rápidas
qwen3 8B Agente de uso geral
gpt-oss Vários Tarefas de pesquisa
qwen3:480b-cloud 480B Raciocínio complexo (nuvem)
gpt-oss:120b-cloud 120B Pesquisa extensa (nuvem)
deepseek-v3.1-cloud - Análise avançada (nuvem)

Boas Práticas

  1. Tratamento de Erros: Sempre verifique por erros e lidere com falhas da API graciosamente
  2. Tempos Limite: Use contexto com tempos limite para solicitações de rede
  3. Limitação de Taxa: Respeite os limites de taxa da API da Ollama. Esteja ciente de possíveis mudanças na API da Ollama, como discutido em primeiros sinais de enshittification da Ollama
  4. Truncamento de Resultados: Trunque os resultados para ~8000 caracteres devido aos limites de contexto
  5. Solicitações Concorrentes: Use goroutines para pesquisas paralelas
  6. Reuso de Conexão: Reutilize o cliente HTTP para melhor desempenho
  7. Testes: Escreva testes unitários abrangentes para suas implementações de pesquisa. Siga as boas práticas de teste unitário em Go para garantir que seu código seja robusto e manutenível

Subscrever

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.