Menggunakan Ollama Web Search API dalam Go

Bangun agen pencarian AI dengan Go dan Ollama

Konten Halaman

API Pencarian Web Ollama memungkinkan Anda memperluas LLM lokal dengan informasi web secara real-time. Panduan ini menunjukkan cara mengimplementasikan kemampuan pencarian web dalam Go, dari panggilan API sederhana hingga agen pencarian berfitur lengkap.

gopher bersepeda

Memulai

Apakah Ollama memiliki perpustakaan Go resmi untuk pencarian web? Ollama menyediakan API REST untuk pencarian web yang berfungsi dengan klien HTTP Go apa pun. Meskipun belum ada SDK Go resmi untuk pencarian web, Anda dapat dengan mudah mengimplementasikan panggilan API menggunakan paket standar.

Pertama, buat kunci API dari akun Ollama Anda. Untuk referensi menyeluruh mengenai perintah dan penggunaan Ollama, lihat lembar cepat Ollama.

Atur kunci API Anda sebagai variabel lingkungan:

export OLLAMA_API_KEY="your_api_key"

Di Windows PowerShell:

$env:OLLAMA_API_KEY = "your_api_key"

Pengaturan Proyek

Buat modul Go baru:

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

Pencarian Web Dasar

Bagaimana cara saya mengotentikasi dengan API pencarian web Ollama dalam Go? Tetapkan header Authorization dengan kunci API Anda sebagai token Bearer. Buat kunci API dari akun Ollama Anda dan masukkan ke dalam header permintaan.

Berikut adalah implementasi lengkap dari API pencarian web:

package main

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

// Tipe permintaan/Respons untuk 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("variabel lingkungan OLLAMA_API_KEY tidak disetel")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("gagal mengubah permintaan: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("gagal membaca respons: %w", err)
	}

	var searchResp WebSearchResponse
	if err := json.Unmarshal(body, &searchResp); err != nil {
		return nil, fmt.Errorf("gagal mengubah respons: %w", err)
	}

	return &searchResp, nil
}

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

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

Implementasi Fetch Web

Apa perbedaan antara endpoint web_search dan web_fetch? Endpoint web_search memanggil internet dan mengembalikan beberapa hasil pencarian dengan judul, URL, dan cuplikan. Endpoint web_fetch mengambil konten penuh dari URL tertentu, mengembalikan judul halaman, konten, dan tautan.

package main

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

// Tipe permintaan/Respons untuk 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("variabel lingkungan OLLAMA_API_KEY tidak disetel")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("gagal mengubah permintaan: %w", err)
	}

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

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("gagal membaca respons: %w", err)
	}

	var fetchResp WebFetchResponse
	if err := json.Unmarshal(body, &fetchResp); err != nil {
		return nil, fmt.Errorf("gagal mengubah respons: %w", err)
	}

	return &fetchResp, nil
}

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

	fmt.Printf("Judul: %s\n\n", result.Title)
	fmt.Printf("Konten:\n%s\n\n", result.Content)
	fmt.Printf("Tautan yang ditemukan: %d\n", len(result.Links))
	for i, link := range result.Links {
		if i >= 5 {
			fmt.Printf("  ... dan %d lebih\n", len(result.Links)-5)
			break
		}
		fmt.Printf("  - %s\n", link)
	}
}

Paket Klien yang Dapat Digunakan Ulang

Buat paket klien Ollama yang dapat digunakan ulang untuk kode yang lebih bersih:

// 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("variabel lingkungan OLLAMA_API_KEY tidak disetel")
	}

	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("kesalahan 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
}

Penggunaan:

package main

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

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

	// Pencarian
	results, err := client.WebSearch("fitur baru Ollama", 5)
	if err != nil {
		log.Fatal(err)
	}

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

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

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

Membangun Agen Pencarian

Model mana yang bekerja terbaik untuk agen pencarian Ollama berbasis Go? Model dengan kemampuan penggunaan alat yang kuat bekerja terbaik, termasuk qwen3, gpt-oss, dan model cloud seperti qwen3:480b-cloud dan deepseek-v3.1-cloud. Jika Anda bekerja dengan model Qwen3 dan perlu memproses atau mengevaluasi ulang hasil pencarian, lihat panduan kami mengenai penilaian ulang dokumen teks dengan Ollama dan model embedding Qwen3 dalam Go.

Berikut adalah implementasi agen pencarian lengkap:

package main

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

// Jenis 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 mengatur pencarian web dengan 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: "Cari web untuk informasi terkini",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"query": map[string]string{
							"type":        "string",
							"description": "Pertanyaan pencarian",
						},
					},
					"required": []string{"query"},
				},
			},
		},
		{
			Type: "function",
			Function: ToolFunction{
				Name:        "web_fetch",
				Description: "Mengambil konten penuh dari halaman web",
				Parameters: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"url": map[string]string{
							"type":        "string",
							"description": "URL yang akan diambil",
						},
					},
					"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("kesalahan chat: %w", err)
		}

		messages = append(messages, response.Message)

		// Tidak ada panggilan alat berarti kita memiliki jawaban akhir
		if len(response.Message.ToolCalls) == 0 {
			return response.Message.Content, nil
		}

		// Eksekusi panggilan alat
		for _, toolCall := range response.Message.ToolCalls {
			fmt.Printf("šŸ”§ Memanggil: %s\n", toolCall.Function.Name)

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

			// Potong untuk batas konteks
			if len(result) > 8000 {
				result = result[:8000] + "... [dipotong]"
			}

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

	return "", fmt.Errorf("batas iterasi maksimal tercapai")
}

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("argumen query tidak valid")
		}
		return a.webSearch(query)

	case "web_fetch":
		url, ok := toolCall.Function.Arguments["url"].(string)
		if !ok {
			return "", fmt.Errorf("argumen url tidak valid")
		}
		return a.webFetch(url)

	default:
		return "", fmt.Errorf("alat tidak diketahui: %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("Apa fitur terbaru dalam Ollama?")
	if err != nil {
		fmt.Printf("Kesalahan: %v\n", err)
		return
	}

	fmt.Println("\nšŸ“ Jawaban:")
	fmt.Println(answer)
}

Bagaimana cara saya menangani respons pencarian web besar dalam Go? Potong konten respons sebelum mengirimkannya ke konteks model. Gunakan pemotongan string untuk membatasi konten hingga sekitar 8000 karakter untuk memenuhi batas konteks.

Pencarian Bersamaan

Go sangat baik dalam operasi bersamaan. Berikut cara melakukan beberapa pencarian sekaligus. Memahami cara Ollama menangani permintaan bersamaan dapat membantu Anda mengoptimalkan implementasi pencarian bersamaan Anda.

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{
		"Fitur terbaru Ollama",
		"penempatan LLM lokal",
		"agen pencarian AI Go",
	}

	results := concurrentSearch(queries)

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

Konteks dan Pembatalan

Tambahkan dukungan konteks untuk timeout dan pembatalan:

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() {
	// Membuat konteks dengan timeout 10 detik
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	result, err := webSearchWithContext(ctx, "API pencarian web Ollama")
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Permintaan timeout")
		} else {
			fmt.Printf("Kesalahan: %v\n", err)
		}
		return
	}

	fmt.Println(result)
}

Model yang Direkomendasikan

Panjang konteks apa yang harus saya gunakan untuk agen pencarian Go? Atur panjang konteks sekitar 32000 token untuk kinerja yang wajar. Agen pencarian bekerja terbaik dengan panjang konteks penuh karena hasil pencarian web dapat luas. Jika Anda perlu mengelola model Ollama Anda atau memindahkannya ke lokasi lain, lihat panduan kami mengenai cara memindahkan model Ollama ke drive atau folder berbeda.

Model Parameter Terbaik Untuk
qwen3:4b 4B Pencarian lokal cepat
qwen3 8B Agen tujuan umum
gpt-oss Berbagai Tugas penelitian
qwen3:480b-cloud 480B Penalaran kompleks (cloud)
gpt-oss:120b-cloud 120B Penelitian panjang (cloud)
deepseek-v3.1-cloud - Analisis lanjutan (cloud)

Untuk aplikasi AI lanjutan yang menggabungkan konten teks dan visual, pertimbangkan untuk mengeksplorasi embedding lintas-modal untuk memperluas kemampuan pencarian Anda di luar pertanyaan teks saja.

Praktik Terbaik

  1. Pengelolaan Kesalahan: Selalu periksa kesalahan dan tangani kegagalan API dengan baik
  2. Timeout: Gunakan konteks dengan timeout untuk permintaan jaringan
  3. Pembatasan Tingkat: Hormati batas tingkat API Ollama. Sadari potensi perubahan API Ollama, seperti yang dibahas dalam tanda-tanda awal Ollama enshittification
  4. Pemotongan Hasil: Potong hasil hingga sekitar 8000 karakter untuk batas konteks
  5. Permintaan Bersamaan: Gunakan goroutine untuk pencarian bersamaan
  6. Pemakaian Koneksi: Ulangi klien HTTP untuk kinerja yang lebih baik
  7. Pengujian: Tulis pengujian unit menyeluruh untuk implementasi pencarian Anda. Ikuti praktik terbaik pengujian unit Go untuk memastikan kode Anda kuat dan dapat dipelihara

Tautan Berguna