在 Go 中使用 Ollama Web Search API
使用 Go 和 Ollama 构建 AI 搜索代理
Ollama 的 Web 搜索 API 可以让您将本地 LLM 与实时网络信息相结合。本指南将向您展示如何在 Go 中实现 网络搜索功能,从简单的 API 调用到功能齐全的搜索代理。

入门
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 进行身份验证? 将 Authorization 头设置为您的 API 密钥作为 Bearer 令牌。从您的 Ollama 账户创建一个 API 密钥并在请求头中传递。
以下是网络搜索 API 的完整实现:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// WebSearch 的请求/响应类型
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_search 和 web_fetch 端点之间有什么区别? web_search 端点会查询互联网并返回多个搜索结果,包括标题、URL 和摘要。web_fetch 端点会检索特定 URL 的完整内容,返回页面标题、内容和链接。
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// WebFetch 的请求/响应类型
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[Fetch错误: %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 SearchResponse
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 {
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 嵌入模型在 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, "Ollama 网络搜索 API")
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 应用,请考虑探索 跨模态嵌入,以扩展您的搜索能力,超越仅文本查询。
最佳实践
- 错误处理:始终检查错误并优雅地处理 API 失败
- 超时:使用带有超时的上下文进行网络请求
- 速率限制:尊重 Ollama 的 API 速率限制。注意 Ollama API 的潜在变化,如 Ollama Enshittification 的最初迹象
- 结果截断:将结果截断至约 8000 个字符以适应上下文限制
- 并发请求:使用 goroutines 进行并行搜索
- 连接复用:复用 HTTP 客户端以提高性能
- 测试:为您的搜索实现编写全面的单元测试。遵循 Go 单元测试最佳实践 以确保您的代码健壮且可维护