Go에서 Ollama 웹 검색 API 사용
Go와 Ollama로 AI 검색 에이전트 만들기
Ollama의 웹 검색 API는 실시간 웹 정보를 활용하여 로컬 LLM을 보완할 수 있습니다. 이 가이드에서는 단순한 API 호출부터 전체 기능을 갖춘 검색 에이전트까지, Go에서 웹 검색 기능을 구현하는 방법을 보여드립니다.

시작하기
Ollama에 웹 검색을 위한 공식 Go 라이브러리가 있나요? Ollama는 모든 Go HTTP 클라이언트와 함께 작동하는 웹 검색용 REST API를 제공합니다. 아직 웹 검색을 위한 공식 Go SDK는 없지만, 표준 라이브러리 패키지를 사용하여 API 호출을 쉽게 구현할 수 있습니다.
먼저, Ollama 계정에서 API 키를 생성하세요. 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
기본 웹 검색
Go에서 Ollama의 웹 검색 API를 인증하는 방법은 무엇인가요? API 키를 Bearer 토큰으로 사용하여 Authorization 헤더를 설정하면 됩니다. Ollama 계정에서 API 키를 생성하고 요청 헤더에 전달하세요.
다음은 웹 검색 API의 완전한 구현 예시입니다:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Request/Response types for 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 environment variable not set")
}
reqBody := WebSearchRequest{
Query: query,
MaxResults: maxResults,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", "https://ollama.com/api/web_search", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %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("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var searchResp WebSearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &searchResp, nil
}
func main() {
results, err := webSearch("What is Ollama?", 5)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Search Results:")
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 엔드포인트는 인터넷을 조회하여 제목, URL, 스니펫이 포함된 여러 검색 결과를 반환합니다. web_fetch 엔드포인트는 특정 URL의 전체 내용을 가져와서 페이지 제목, 내용, 링크를 반환합니다.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Request/Response types for 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 environment variable not set")
}
reqBody := WebFetchRequest{URL: url}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", "https://ollama.com/api/web_fetch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %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("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var fetchResp WebFetchResponse
if err := json.Unmarshal(body, &fetchResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &fetchResp, nil
}
func main() {
result, err := webFetch("https://ollama.com")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Title: %s\n\n", result.Title)
fmt.Printf("Content:\n%s\n\n", result.Content)
fmt.Printf("Links found: %d\n", len(result.Links))
for i, link := range result.Links {
if i >= 5 {
fmt.Printf(" ... and %d more\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 environment 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 error (status %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)
}
// Search
results, err := client.WebSearch("Ollama new features", 5)
if err != nil {
log.Fatal(err)
}
for _, r := range results.Results {
fmt.Printf("- %s\n %s\n\n", r.Title, r.URL)
}
// Fetch
page, err := client.WebFetch("https://ollama.com")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Page: %s\n", page.Title)
}
검색 에이전트 구축
Go 기반 Ollama 검색 에이전트에 가장 적합한 모델은 무엇인가요? 도구 사용(tool-use) 기능이 강점을 지닌 모델이 가장 잘 작동하며, 여기에는 qwen3, gpt-oss, 그리고 qwen3:480b-cloud 및 deepseek-v3.1-cloud와 같은 클라우드 모델이 포함됩니다. Qwen3 모델을 사용하고 검색 결과를 처리하거나 다시 랭킹(rerank)해야 한다면, Go 언어로 Ollama와 Qwen3 임베딩 모델을 이용한 텍스트 문서 다시 랭킹 가이드를 참고하세요.
다음은 완전한 검색 에이전트 구현 예시입니다:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Chat types
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 orchestrates web search with 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: "Search the web for current information",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]string{
"type": "string",
"description": "The search query",
},
},
"required": []string{"query"},
},
},
},
{
Type: "function",
Function: ToolFunction{
Name: "web_fetch",
Description: "Fetch the full content of a web page",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]string{
"type": "string",
"description": "The URL to fetch",
},
},
"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("chat error: %w", err)
}
messages = append(messages, response.Message)
// No tool calls means we have a final answer
if len(response.Message.ToolCalls) == 0 {
return response.Message.Content, nil
}
// Execute tool calls
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("Error: %v", err)
}
// Truncate for context limits
if len(result) > 8000 {
result = result[:8000] + "... [truncated]"
}
messages = append(messages, Message{
Role: "tool",
Content: result,
ToolName: toolCall.Function.Name,
})
}
}
return "", fmt.Errorf("max iterations reached")
}
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("invalid query argument")
}
return a.webSearch(query)
case "web_fetch":
url, ok := toolCall.Function.Arguments["url"].(string)
if !ok {
return "", fmt.Errorf("invalid url argument")
}
return a.webFetch(url)
default:
return "", fmt.Errorf("unknown tool: %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("What are the latest features in Ollama?")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("\n📝 Answer:")
fmt.Println(answer)
}
Go에서 큰 웹 검색 응답을 처리하는 방법은 무엇인가요? 모델 컨텍스트에 전달하기 전에 응답 내용을 잘라내기(truncate)하세요. 컨텍스트 제한 내에서 작동하도록 콘텐츠를 약 8,000자로 제한하기 위해 문자열 슬라이싱을 사용하세요.
동시 검색
Go는 동시 운영(concurrency)에 뛰어난 언어입니다. 여러 검색을 병렬로 수행하는 방법은 다음과 같습니다. 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 latest features",
"local LLM deployment",
"AI search agents Go",
}
results := concurrentSearch(queries)
for _, r := range results {
fmt.Printf("\n🔍 Query: %s\n", r.Query)
if r.Error != nil {
fmt.Printf(" Error: %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() {
// Create context with 10 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := webSearchWithContext(ctx, "Ollama web search API")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Request timed out")
} else {
fmt.Printf("Error: %v\n", err)
}
return
}
fmt.Println(result)
}
추천 모델
Go 검색 에이전트에 사용할 컨텍스트 길이는 얼마로 설정해야 하나요? 합리적인 성능을 위해 컨텍스트 길이를 약 32,000 토큰으로 설정하는 것이 좋습니다. 웹 검색 결과는 방대할 수 있으므로 검색 에이전트는 전체 컨텍스트 길이와 함께 사용할 때 가장 잘 작동합니다. Ollama 모델을 관리하거나 다른 위치로 이동해야 한다면, Ollama 모델을 다른 드라이브나 폴더로 이동하는 방법 가이드를 참고하세요.
| 모델 | 파라미터 | 적합한 용도 |
|---|---|---|
qwen3:4b |
4B | 빠른 로컬 검색 |
qwen3 |
8B | 일반 목적 에이전트 |
gpt-oss |
다양 | 연구 과제 |
qwen3:480b-cloud |
480B | 복잡한 추론 (클라우드) |
gpt-oss:120b-cloud |
120B | 장문 연구 (클라우드) |
deepseek-v3.1-cloud |
- | 고급 분석 (클라우드) |
모범 사례
- 오류 처리: 항상 오류를 확인하고 API 실패를 적절하게 처리하세요.
- 타임아웃: 네트워크 요청에 대한 타임아웃을 컨텍스트와 함께 사용하세요.
- 레이트 리미팅: Ollama의 API 레이트 리미트를 준수하세요. Ollama의 품질 저하에 대한 초기 징후에서 논의된 Ollama API의 잠재적 변경 사항에 유의하세요.
- 결과 잘라내기: 컨텍스트 제한을 위해 결과를 약 8,000자로 잘라내세요.
- 동시 요청: 병렬 검색을 위해 고루틴(goroutine)을 사용하세요.
- 연결 재사용: 더 나은 성능을 위해 HTTP 클라이언트를 재사용하세요.
- 테스팅: 검색 구현에 대해 포괄적인 단위 테스트를 작성하세요. 코드가 강건하고 유지보수 가능하도록 Go 단위 테스트 모범 사례을 따르세요.