استخدام واجهة برمجة التطبيقات الخاصة ببحث Ollama على الويب في Go

أنشئ وكلاء بحث ذكاء اصطناعي باستخدام Go وOllama

Page content

واجهة بحث الويب في Ollama تتيح لك تحسين نماذج LLM المحلية بمعلومات الويب في الوقت الفعلي. توضح هذه المقالة لك كيفية تنفيذ قدرات البحث عبر الويب في Go، من مكالمات API بسيطة إلى وكلاء البحث المتكاملين.

غفر يركض

البدء

هل لدى Ollama مكتبة Go رسمية لبحث الويب؟ يوفر Ollama واجهة برمجة تطبيقات REST لبحث الويب تعمل مع أي عميل HTTP في Go. على الرغم من عدم وجود مكتبة Go رسمية لبحث الويب بعد، يمكنك تنفيذ مكالمات API بسهولة باستخدام حزم المكتبة القياسية.

أولاً، أنشئ مفتاح API من حسابك في Ollama. للمراجعة الشاملة حول أوامر Ollama واستخدامها، تحقق من قائمة مصطلحات Ollama.

قم بتعيين مفتاح API كمتغير بيئة:

export OLLAMA_API_KEY="your_api_key"

على PowerShell في Windows:

$env:OLLAMA_API_KEY = "your_api_key"

إعداد المشروع

أنشئ وحدة Go جديدة:

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

البحث عبر الويب الأساسي

كيف أقوم بالتوقيع مع واجهة بحث الويب في Ollama في Go؟ ضع رأس “Authorization” مع مفتاح API الخاص بك كرمز Bearer. أنشئ مفتاح API من حسابك في Ollama وارسله في رأس الطلب.

إليك تنفيذ كامل لواجهة بحث الويب:

package main

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

// أنواع الطلب/الاستجابة لبحث الويب
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 variable بيئة not set")
	}

	reqBody := WebSearchRequest{
		Query:      query,
		MaxResults: maxResults,
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("فشل marshaling الطلب: %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("فشل unmarshal الاستجابة: %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_search و web_fetch؟ يسأل طرف web_search الإنترنت ويُرجع نتائج بحث متعددة مع العناوين، الروابط، والملخصات. يحصل طرف 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 variable بيئة not set")
	}

	reqBody := WebFetchRequest{URL: url}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("فشل marshaling الطلب: %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("فشل unmarshal الاستجابة: %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 variable بيئة not set")
	}

	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": "الرابط للاسترجاع",
						},
					},
					"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("🔧 Calling: %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("حجة الرابط غير صحيحة")
		}
		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, "واجهة بحث الويب في 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 - التحليل المتقدم (السحابة)

لتطبيقات AI المتقدمة التي تدمج المحتوى النصي والمرئي، يُرجى النظر في استكشاف التجريدات عبر الأوضاع لتوسيع قدرات البحث خارج الاستعلامات النصية فقط.

الممارسات الموصى بها

  1. إدارة الأخطاء: تحقق دائمًا من الأخطاء وتعامل مع فشل واجهة برمجة التطبيقات بسلاسة
  2. الوقت المحدد: استخدم السياق مع وقتي للطلبات الشبكية
  3. الحد من السرعة: احترم حدود واجهة برمجة التطبيقات لـ Ollama. تأكد من التغييرات المحتملة في واجهة برمجة التطبيقات لـ Ollama، كما تم مناقشته في العلامات الأولى لـ Ollama Enshittification
  4. الحد من النتائج: قطع النتائج إلى ~8000 حرف لحدود السياق
  5. الطلبات المتزامنة: استخدم go routines للبحث المتزامن
  6. إعادة استخدام الاتصالات: استخدم عميل HTTP لإعادة استخدامه لتحسين الأداء
  7. الاختبار: اكتب اختبارات وحدة شاملة لتنفيذات بحثك. اتبع أفضل الممارسات لاختبار الوحدات في Go

الروابط المفيدة