Usando a API de Pesquisa 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 do Ollama permite que você amplie LLMs locais com informações da web em tempo real. Este guia mostra como implementar capacidades de pesquisa web em Go, desde chamadas simples da API até agentes de pesquisa completos.

gopher biking

Começando

O Ollama tem uma biblioteca oficial para Go para pesquisa web? O Ollama fornece uma API REST para pesquisa web que funciona com qualquer cliente HTTP Go. Embora ainda não haja uma SDK oficial para Go para pesquisa web, você pode facilmente implementar as chamadas da 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 do Ollama, consulte a folha de dicas do 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 Básica

Como autenticar com a API de pesquisa web do 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 a 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 a 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 da API (status %d): %s", resp.StatusCode, string(body))
	}

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

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

	return &searchResp, nil
}

func main() {
	results, err := webSearch("O que é 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 Fetch Web

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

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

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("falha ao desserializar a 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 %d mais\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[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("erro da 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)
	}

	// Pesquisar
	results, err := client.WebSearch("novas funcionalidades do Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

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

	// Buscar
	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 do Ollama baseados em Go? Modelos com capacidades fortes 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ê estiver trabalhando com modelos Qwen3 e precisar processar ou reclassificar resultados de pesquisa, veja nosso guia sobre reclassificação de documentos de texto com Ollama e modelo Qwen3 Embedding 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: "Pesquisar a 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: "Recuperar o conteúdo completo de uma página da web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "A URL a ser recuperada",
						},
					},
					"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 ferramentas
		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("atingido o número máximo de iterações")
}

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("Quais são as últimas funcionalidades do Ollama?")
	if err != nil {
		fmt.Printf("Erro: %v\n", err)
		return
	}

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

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

Pesquisa Concorrente

O Go é excelente para operações concorrentes. Aqui está como realizar várias pesquisas em paralelo. Entender como o Ollama lida com solicitações paralelas pode ajudá-lo 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{
		"Funcionalidades mais recentes do Ollama",
		"implantação local de LLM",
		"agentes de pesquisa de AI em Go",
	}

	results := concurrentSearch(queries)

	for _, r := range results {
		fmt.Printf("\n🔍 Query: %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 ao contexto para temporizadores 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() {
	// Crie contexto com timeout de 10 segundos
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API de pesquisa web do Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Solicitação expirou")
		} 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 total do contexto, pois os resultados da pesquisa da web podem ser extensos. Se você precisar gerenciar seus modelos do Ollama ou movê-los para diferentes locais, veja nosso guia sobre como mover modelos do Ollama para diferentes unidades ou pastas.

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

Para aplicações avançadas de IA que combinam conteúdo de texto e visual, considere explorar embeddings multimodais para estender suas capacidades de pesquisa além de consultas de texto apenas.

Boas Práticas

  1. Tratamento de Erros: Sempre verifique por erros e trate falhas da API de forma gentil
  2. Temporizadores: Use contexto com temporizadores para solicitações de rede
  3. Limites de Taxa: Respeite os limites de taxa da API do Ollama. Esteja ciente das possíveis mudanças na API do Ollama, conforme discutido em primeiros sinais de enshittification do Ollama
  4. Truncamento de Resultados: Truncar resultados para ~8000 caracteres para limites de contexto
  5. Solicitações Concorrentes: Use goroutines para pesquisas paralelas
  6. Reutilização de Conexão: Reutilize o cliente HTTP para melhor desempenho
  7. Testes: Escreva testes unitários abrangentes para suas implementações de pesquisa. Siga melhores práticas de testes unitários em Go para garantir que seu código seja robusto e mantível