API веб-поиска Ollama в Go
AI-поисковые агенты с использованием Go и Ollama
Ollama’s Web Search API позволяет дополнять локальные LLMs актуальной информацией из интернета. Это руководство показывает, как реализовать возможности веб-поиска на Go, от простых API-запросов до полнофункциональных поисковых агентов.

Начало работы
Есть ли у Ollama официальная библиотека для Go для веб-поиска? Ollama предоставляет REST API для веб-поиска, который работает с любым клиентом HTTP на Go. Хотя пока нет официального SDK для Go, вы легко можете реализовать API-запросы с использованием стандартных пакетов библиотеки.
Сначала создайте API ключ в вашем аккаунте Ollama. Для полной справки по командам и использованию Ollama ознакомьтесь с шпаргалкой Ollama.
Установите ваш API ключ как переменную окружения:
export OLLAMA_API_KEY="your_api_key"
В Windows PowerShell:
$env:OLLAMA_API_KEY = "your_api_key"
Настройка проекта
Создайте новый модуль Go:
mkdir ollama-search
cd ollama-search
go mod init ollama-search
Базовый веб-поиск
Как аутентифицироваться в API веб-поиска Ollama на Go? Установите заголовок Authorization с вашим API ключом как Bearer токен. Создайте API ключ в вашем аккаунте Ollama и передайте его в заголовке запроса.
Вот полная реализация API веб-поиска:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Типы запросов/ответов для 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("переменная окружения OLLAMA_API_KEY не установлена")
}
reqBody := WebSearchRequest{
Query: query,
MaxResults: maxResults,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("не удалось сериализовать запрос: %w", err)
}
req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("не удалось создать запрос: %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("запрос не удался: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка API (статус %d): %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать ответ: %w", err)
}
var searchResp WebSearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
return nil, fmt.Errorf("не удалось десериализовать ответ: %w", err)
}
return &searchResp, nil
}
func main() {
results, err := webSearch("Что такое Ollama?", 5)
if err != nil {
fmt.Printf("Ошибка: %v\n", err)
return
}
fmt.Println("Результаты поиска:")
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] + "..."
}
Реализация Web Fetch
В чем разница между эндпоинтами web_search и web_fetch? Эндпоинт web_search выполняет поиск в интернете и возвращает несколько результатов с заголовками, URL и выдержками. Эндпоинт web_fetch получает полное содержимое конкретного URL, возвращая заголовок страницы, содержимое и ссылки.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Типы запросов/ответов для 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("переменная окружения OLLAMA_API_KEY не установлена")
}
reqBody := WebFetchRequest{URL: url}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("не удалось сериализовать запрос: %w", err)
}
req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("не удалось создать запрос: %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("запрос не удался: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка API (статус %d): %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать ответ: %w", err)
}
var fetchResp WebFetchResponse
if err := json.Unmarshal(body, &fetchResp); err != nil {
return nil, fmt.Errorf("не удалось десериализовать ответ: %w", err)
}
return &fetchResp, nil
}
func main() {
result, err := webFetch("https://ollama.com")
if err != nil {
fmt.Printf("Ошибка: %v\n", err)
return
}
fmt.Printf("Заголовок: %s\n\n", result.Title)
fmt.Printf("Содержимое:\n%s\n\n", result.Content)
fmt.Printf("Найдено ссылок: %d\n", len(result.Links))
for i, link := range result.Links {
if i >= 5 {
fmt.Printf(" ... и еще %d\n", len(result.Links)-5)
break
}
fmt.Printf(" - %s\n", link)
}
}
Переиспользуемый клиентский пакет
Создайте переиспользуемый пакет клиента Ollama для более чистого кода:
// 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("переменная окружения OLLAMA_API_KEY не установлена")
}
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("ошибка API (статус %d): %s", resp.StatusCode, string(body))
}
var result T
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return &result, nil
}
Использование:
package main
import (
"fmt"
"log"
"ollama-search/ollama"
)
func main() {
client, err := ollama.NewClient()
if err != nil {
log.Fatal(err)
}
// Поиск
results, err := client.WebSearch("новые функции Ollama", 5)
if err != nil {
log.Fatal(err)
}
for _, r := range results.Results {
fmt.Printf("- %s\n %s\n\n", r.Title, r.URL)
}
// Получение
page, err := client.WebFetch("https://ollama.com")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Страница: %s\n", page.Title)
}
Построение поискового агента
Какие модели лучше всего подходят для поисковых агентов на Go с использованием Ollama? Лучше всего работают модели с сильными возможностями использования инструментов, включая qwen3, gpt-oss и облачные модели, такие как qwen3:480b-cloud и deepseek-v3.1-cloud. Если вы работаете с моделями Qwen3 и вам нужно обрабатывать или переранжировать результаты поиска, ознакомьтесь с нашим руководством по переранжировке текстовых документов с использованием Ollama и модели Qwen3 Embedding в Go.
Вот полная реализация поискового агента:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Типы чата
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 управляет веб-поиском с использованием 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: "Поиск в интернете актуальной информации",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]string{
"type": "string",
"description": "Поисковый запрос",
},
},
"required": []string{"query"},
},
},
},
{
Type: "function",
Function: ToolFunction{
Name: "web_fetch",
Description: "Получение полного содержимого веб-страницы",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]string{
"type": "string",
"description": "URL для получения",
},
},
"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("ошибка чата: %w", err)
}
messages = append(messages, response.Message)
// Отсутствие вызовов инструментов означает, что у нас есть окончательный ответ
if len(response.Message.ToolCalls) == 0 {
return response.Message.Content, nil
}
// Выполнение вызовов инструментов
for _, toolCall := range response.Message.ToolCalls {
fmt.Printf("🔧 Вызов: %s\n", toolCall.Function.Name)
result, err := a.executeTool(toolCall)
if err != nil {
result = fmt.Sprintf("Ошибка: %v", err)
}
// Обрезка для ограничений контекста
if len(result) > 8000 {
result = result[:8000] + "... [обрезано]"
}
messages = append(messages, Message{
Role: "tool",
Content: result,
ToolName: toolCall.Function.Name,
})
}
}
return "", fmt.Errorf("достигнуто максимальное количество итераций")
}
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("неверный аргумент запроса")
}
return a.webSearch(query)
case "web_fetch":
url, ok := toolCall.Function.Arguments["url"].(string)
if !ok {
return "", fmt.Errorf("неверный аргумент URL")
}
return a.webFetch(url)
default:
return "", fmt.Errorf("неизвестный инструмент: %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("Какие последние функции в Ollama?")
if err != nil {
fmt.Printf("Ошибка: %v\n", err)
return
}
fmt.Println("\n📝 Ответ:")
fmt.Println(answer)
}
Как обрабатывать большие ответы веб-поиска в Go? Обрезайте содержимое ответа перед передачей его в контекст модели. Используйте срезы строк для ограничения содержимого примерно до 8000 символов, чтобы оно помещалось в ограничения контекста.
Параллельный поиск
Go отлично подходит для параллельных операций. Вот как выполнять несколько поисков одновременно. Понимание как Ollama обрабатывает параллельные запросы может помочь вам оптимизировать свои параллельные реализации поиска.
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",
"Локальное развертывание LLM",
"AI поисковые агенты Go",
}
results := concurrentSearch(queries)
for _, r := range results {
fmt.Printf("\n🔍 Запрос: %s\n", r.Query)
if r.Error != nil {
fmt.Printf(" Ошибка: %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
}
Контекст и отмена
Добавьте поддержку контекста для таймаутов и отмены:
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() {
// Создание контекста с таймаутом 10 секунд
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := webSearchWithContext(ctx, "API веб-поиска Ollama")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Запрос истек по времени")
} else {
fmt.Printf("Ошибка: %v\n", err)
}
return
}
fmt.Println(result)
}
Рекомендуемые модели
Какую длину контекста следует использовать для поисковых агентов на Go? Установите длину контекста примерно на 32000 токенов для разумной производительности. Поисковые агенты работают лучше всего с полной длиной контекста, так как результаты веб-поиска могут быть обширными. Если вам нужно управлять вашими моделями Ollama или перемещать их в разные места, ознакомьтесь с нашим руководством по как переместить модели Ollama на другой диск или папку.
| Модель | Параметры | Лучше всего для |
|---|---|---|
qwen3:4b |
4B | Быстрые локальные поиски |
qwen3 |
8B | Общее назначение агента |
gpt-oss |
Разные | Исследовательские задачи |
qwen3:480b-cloud |
480B | Сложные рассуждения (облако) |
gpt-oss:120b-cloud |
120B | Долгосрочные исследования (облако) |
deepseek-v3.1-cloud |
- | Продвинутый анализ (облако) |
Для продвинутых приложений ИИ, которые сочетают текстовое и визуальное содержимое, рассмотрите возможность изучения кросс-модальных вложений для расширения ваших поисковых возможностей за рамками текстовых запросов.
Лучшие практики
- Обработка ошибок: Всегда проверяйте ошибки и обрабатывайте сбои API корректно
- Тайм-ауты: Используйте контекст с тайм-аутами для сетевых запросов
- Ограничение скорости: Уважайте ограничения скорости API Ollama. Будьте осведомлены о возможных изменениях в API Ollama, как обсуждалось в первые признаки деградации Ollama
- Обрезка результатов: Обрезайте результаты до ~8000 символов из-за ограничений контекста
- Параллельные запросы: Используйте горутины для параллельных поисков
- Переиспользование соединений: Переиспользуйте HTTP-клиент для лучшей производительности
- Тестирование: Пишите всесторонние модульные тесты для своих реализаций поиска. Следуйте лучшим практикам модульного тестирования на Go
Полезные ссылки
- Шпаргалка по Ollama
- Переранжирование текстовых документов с помощью Ollama и модели Qwen3 Embedding - на Go
- Кросс-модальные эмбеддинги: объединение модулей ИИ
- Модульное тестирование на Go: структура и лучшие практики
- Как переместить модели Ollama на другой диск или папку
- Как Ollama обрабатывает параллельные запросы
- Первые признаки деградации Ollama
- Блог Ollama о веб-поиске
- Официальная документация Ollama
- Примеры на Go для Ollama
- Репозиторий Ollama на GitHub