Użycie interfejsu API Ollama Web Search w Go

Twórz agentów wyszukiwania AI za pomocą Go i Ollama

Page content

API do wyszukiwania w sieci Ollama pozwala na wzbogacenie lokalnych modeli językowych danymi z sieci w czasie rzeczywistym. Niniejszy przewodnik pokazuje, jak zaimplementować możliwości wyszukiwania w sieci w Go, od prostych wywołań API po pełne agenty wyszukiwania.

gopher biking

Rozpoczęcie pracy

Czy Ollama ma oficjalną bibliotekę Go do wyszukiwania w sieci? Ollama udostępnia API REST do wyszukiwania w sieci, które działa z dowolnym klientem HTTP w Go. Choć nie ma jeszcze oficjalnego SDK Go do wyszukiwania w sieci, możesz łatwo zaimplementować wywołania API korzystając z pakietów standardowych.

Najpierw utwórz klucz API z Twojego konta Ollama. Dla pełnego odniesienia do poleceń i użycia Ollama, sprawdź cheatsheet Ollama.

Ustaw swój klucz API jako zmienną środowiskową:

export OLLAMA_API_KEY="your_api_key"

Na Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Konfiguracja projektu

Utwórz nowy moduł Go:

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

Podstawowe wyszukiwanie w sieci

Jak uwierzytelnić się w API do wyszukiwania w sieci Ollama w Go? Ustaw nagłówek Authorization z Twoim kluczem API jako token Bearer. Utwórz klucz API z Twojego konta Ollama i przekaż go w nagłówku żądania.

Oto pełna implementacja API do wyszukiwania w sieci:

package main

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

// Typy żądań/odpowiedzi dla 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("zmienna środowiskowa OLLAMA_API_KEY nie jest ustawiona")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("nie udało się zserializować żądania: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("nie udało się utworzyć żądania: %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("żądanie nie powiodło się: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("nie udało się odczytać odpowiedzi: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("nie udało się odserializować odpowiedzi: %w", err)
	}

	return &searchResp, nil
}

func main() {
	results, err := webSearch("Co to jest Ollama?", 5)
	if err != nil {
		fmt.Printf("Błąd: %v\n", err)
		return
	}

	fmt.Println("Wyniki wyszukiwania:")
	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] + "..."
}

Implementacja pobierania z sieci

Jaka jest różnica między endpointami web_search a web_fetch? Endpoint web_search wyszukuje internet i zwraca wiele wyników wyszukiwania z tytułami, adresami URL i fragmentami. Endpoint web_fetch pobiera pełną zawartość określonego adresu URL, zwracając tytuł strony, zawartość i linki.

package main

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

// Typy żądań/odpowiedzi dla 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("zmienna środowiskowa OLLAMA_API_KEY nie jest ustawiona")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("nie udało się zserializować żądania: %w", err)
	}

	req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("nie udało się utworzyć żądania: %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("żądanie nie powiodło się: %w", err)
	}
	defer resp.Body.Close()

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("nie udało się odczytać odpowiedzi: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("nie udało się odserializować odpowiedzi: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Tytuł: %s\n\n", result.Title)
	fmt.Printf("Zawartość:\n%s\n\n", result.Content)
	fmt.Printf("Znalezione linki: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... i %d więcej\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Pakiet klienta do ponownego użycia

Utwórz ponownie wykorzystywalny pakiet klienta Ollama dla czystszej kodu:

// 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("zmienna środowiskowa OLLAMA_API_KEY nie jest ustawiona")
	}

	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("błąd 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
}

Użycie:

package main

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

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

	// Wyszukiwanie
	results, err := client.WebSearch("nowe funkcje Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

	fmt.Printf("Strona: %s\n", page.Title)
}

Budowanie agenta wyszukiwania

Które modele najlepiej sprawdzają się w agentach wyszukiwania Ollama w Go? Modele z silnymi możliwościami korzystania z narzędzi działają najlepiej, w tym qwen3, gpt-oss oraz modele chmurowe takie jak qwen3:480b-cloud i deepseek-v3.1-cloud. Jeśli pracujesz z modelem Qwen3 i potrzebujesz przetworzyć lub ponownie ocenić wyniki wyszukiwania, zobacz nasz przewodnik dotyczący ponownego oceniania dokumentów tekstowych z Ollama i modelem Qwen3 Embedding w Go.

Oto pełna implementacja agenta wyszukiwania:

package main

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

// Typy komunikatów
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 koordynuje wyszukiwanie w sieci z 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: "Wyszukaj w sieci aktualne informacje",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "Zapytanie wyszukiwania",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Pobierz pełną zawartość strony internetowej",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "URL do pobrania",
						},
					},
					"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("błąd w komunikacji: %w", err)
		}

		messages = append(messages, response.Message)

		// Brak wywołań narzędzi oznacza, że mamy gotowy odpowiedź
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Wykonaj wywołania narzędzi
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("🔧 Wywołanie: %s\n", toolCall.Function.Name)

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

			// Obcięcie dla ograniczeń kontekstu
			if len(result) > 8000 {
				result = result[:8000] + "... [obcięte]"
			}

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

	return "", fmt.Errorf("osiągnięto maksymalną liczbę iteracji")
}

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("nieprawidłowy argument zapytania")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("nieprawidłowy argument URL")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("nieznany narzędzie: %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("Jakie są najnowsze funkcje w Ollama?")
	if err != nil {
		fmt.Printf("Błąd: %v\n", err)
		return
	}

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

Jak radzić sobie z dużymi odpowiedziami wyszukiwania w sieci w Go? Obcinaj zawartość odpowiedzi przed przekazaniem jej do kontekstu modelu. Użyj wycinkowania ciągu, aby ograniczyć zawartość do około 8000 znaków, aby zmieścić się w ograniczeniach kontekstu.

Wyszukiwanie równoległe

Go wyróżnia się w operacjach równoległych. Oto, jak wykonać wiele wyszukiwań równolegle. Zrozumienie jak Ollama obsługuje żądania równoległe może pomóc Ci zoptymalizować swoje implementacje wyszukiwania równoległego.

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{
		"najnowsze funkcje Ollama",
		"lokalne wdrożenie modelu językowego",
		"agenty wyszukiwania AI w Go",
	}

	results := concurrentSearch(queries)

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

Kontekst i anulowanie

Dodaj obsługę kontekstu dla timeoutów i anulowania:

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() {
	// Utwórz kontekst z timeoutem 10 sekund
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API do wyszukiwania w sieci Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Żądanie przekroczyło czas")
		} else {
			fmt.Printf("Błąd: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Zalecane modele

Jaki długość kontekstu należy użyć dla agentów wyszukiwania w Go? Ustaw długość kontekstu na około 32000 tokenów dla rozsądnego wydajności. Agenty wyszukiwania działają najlepiej z pełną długością kontekstu, ponieważ wyniki wyszukiwania w sieci mogą być długie. Jeśli potrzebujesz zarządzać modelami Ollama lub przenieść je na inne lokalizacje, zobacz nasz przewodnik dotyczący jak przenieść modele Ollama na inny dysk lub folder.

Model Parametry Najlepsze do
qwen3:4b 4B Szybkie wyszukiwania lokalne
qwen3 8B Ogólne zastosowanie
gpt-oss Różne Zadania badawcze
qwen3:480b-cloud 480B Złożone rozumowanie (chmura)
gpt-oss:120b-cloud 120B Długie formy badawcze (chmura)
deepseek-v3.1-cloud - Zaawansowana analiza (chmura)

Dla zaawansowanych aplikacji AI, które łączą treść tekstową i wizualną, rozważ eksplorację współmodalnych osadzeń w celu rozszerzenia możliwości wyszukiwania poza zapytania tekstowe.

Najlepsze praktyki

  1. Obsługa błędów: Zawsze sprawdzaj błędy i łagodnie obsługuj awarie API
  2. Timeouty: Używaj kontekstu z timeoutami dla żądań sieciowych
  3. Ograniczanie przepustowości: Szanuj ograniczenia przepustowości API Ollama. Uważaj na potencjalne zmiany w API Ollama, jak omówiono w pierwszych objawach Ollama enshittification
  4. Obcinanie wyników: Obcinaj wyniki do około 8000 znaków dla ograniczeń kontekstu
  5. Żądania równoległe: Używaj gorutyn do równoległego wyszukiwania
  6. Zdalne połączenia: Używaj ponownie HTTP klienta dla lepszej wydajności
  7. Testowanie: Pisz komprehensywne testy jednostkowe dla swoich implementacji wyszukiwania. Zobacz najlepsze praktyki testowania jednostkowego w Go aby upewnić się, że Twój kod jest odporny i utrzymany

Przydatne linki