Go + Ollama 构建本地 LLM Agent 实战:ReAct 模式与 Function Calling

# Go + Ollama 构建本地 LLM Agent 实战:ReAct 模式与 Function Calling

在大语言模型应用火热的今天,很多开发者都会 Pyhon 搭配 LangChain 来构建 AI 应用,但 Go 生态这方面的资料相对较少。Ollama 作为一个轻量级的本地 LLM 运行框架,支持多种开源模型,但它官方提供的 SDK 主要是 Python 和 JavaScript。对于我们这些 Go 开发者来说,怎么在 Go 项目里用上 Ollama,怎么让 LLM 能够调用我们自己的工具,一直是个需要解决的问题。

这篇文章记录了我在项目中的一次实践,带你用 Go 语言基于 Ollama 构建一个完整的本地 LLM Agent,支持 ReAct 推理模式和 Function Calling 功能调用。

## 为什么要本地运行 LLM

说起为什么选择本地运行而不是调用 OpenAI API,个人体验有几个原因。首先是数据隐私,有些业务数据不方便上传到第三方,即使 API 服务商承诺安全,也不如自己掌控来得放心。其次是成本,尤其是需要大量调用或者进行长对话的场景,本地运行的费用几乎为零。最后是定制灵活,可以随意更换模型、调整参数,不用受制于第三方 API 的限制。

Ollama 正好满足了这些需求。安装简单,启动快,支持 llama3.1、qwen2.5、mistral 等主流开源模型,而且最新版本已经支持 function calling,这意味着我们有能力让模型调用外部工具。

## 环境准备与项目初始化

开始之前,确保本地已经安装好 Ollama。如果没有安装,一条命令搞定:

“`bash
curl -fsSL https://ollama.com/install.sh | sh
“`

然后拉取一个支持工具调用的模型。个人推荐 llama3.1 或者 qwen2.5,这两者在 function calling 方面表现都不错:

“`bash
ollama pull llama3.1:8b
“`

确认拉取成功后,启动服务:

“`bash
ollama serve
“`

服务默认监听 11434 端口,接下来我们会在 Go 代码里连接这个地址。

新建 Go 项目目录:

“`bash
mkdir -p ollama-agent && cd ollama-agent
go mod init ollama-agent
“`

准备就绪,开始写代码。

## 核心代码实现

### Ollama API 客户端封装

由于 Ollama 官方没有提供 Go SDK,我们需要自己封装 HTTP 请求。好在 Ollama 的 API 设计比较简洁,chat 接口就是一个 JSON 格式的 POST 请求,理解起来没什么难度。

核心结构体定义如下:

“`go
package main

import (
“bytes”
“encoding/json”
“fmt”
“net/http”
“time”
)

// Message 代表聊天消息
type Message struct {
Role string `json:”role”`
Content string `json:”content”`
}

// ToolCall 代表工具调用
type ToolCall struct {
Function Function `json:”function”`
}

// Function 代表函数定义
type Function struct {
Name string `json:”name”`
Arguments json.RawMessage `json:”arguments”`
}

// Tool 代表可用的工具
type Tool struct {
Type string `json:”type”`
Function struct {
Name string `json:”name”`
Description string `json:”description”`
Parameters struct {
Type string `json:”type”`
Properties map[string]struct {
Type string `json:”type”`
Description string `json:”description”`
} `json:”properties”`
Required []string `json:”required”`
} `json:”parameters”`
} `json:”function”`
}

// ChatRequest 代表聊天请求
type ChatRequest struct {
Model string `json:”model”`
Messages []Message `json:”messages”`
Tools []Tool `json:”tools,omitempty”`
Stream bool `json:”stream”`
}

// ChatResponse 代表聊天响应
type ChatResponse struct {
Message struct {
Content string `json:”content”`
ToolCalls []ToolCall `json:”tool_calls,omitempty”`
} `json:”message”`
Done bool `json:”done”`
}

// OllamaClient Ollama 客户端
type OllamaClient struct {
baseURL string
client *http.Client
}

// NewOllamaClient 创建新的客户端
func NewOllamaClient(baseURL string) *OllamaClient {
return &OllamaClient{
baseURL: baseURL,
client: &http.Client{
Timeout: 120 * time.Second,
},
}
}

// Chat 发送聊天请求
func (c *OllamaClient) Chat(req ChatRequest) (*ChatResponse, error) {
url := c.baseURL + “/api/chat”

jsonData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf(“failed to marshal request: %w”, err)
}

httpReq, err := http.NewRequest(“POST”, url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf(“failed to create request: %w”, err)
}

httpReq.Header.Set(“Content-Type”, “application/json”)

resp, err := c.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf(“failed to send request: %w”, err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(“unexpected status code: %d”, resp.StatusCode)
}

var chatResp ChatResponse
if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil {
return nil, fmt.Errorf(“failed to decode response: %w”, err)
}

return &chatResp, nil
}
“`

这段代码封装了最基础的 HTTP 调用逻辑,设置了 120 秒的超时时间,因为大模型推理有时会比较慢。

### 工具函数定义

为了让 LLM 能够调用我们自己写的工具,需要定义工具的结构和执行逻辑。这里实现了两个简单的工具作为示例:计算器获取当前时间。

“`go
package main

import (
“encoding/json”
“fmt”
“math”
“time”
)

// ToolResult 工具执行结果
type ToolResult struct {
Success bool `json:”success”`
Result string `json:”result”`
Error string `json:”error,omitempty”`
}

// Calculator 简单计算器工具
type Calculator struct{}

func NewCalculator() *Calculator {
return &Calculator{}
}

func (c *Calculator) Name() string {
return “calculator”
}

func (c *Calculator) Description() string {
return “执行数学计算,支持加、减、乘、除、幂运算”
}

func (c *Calculator) Execute(args json.RawMessage) ToolResult {
var params struct {
Operation string `json:”operation”`
A float64 `json:”a”`
B float64 `json:”b”`
}

if err := json.Unmarshal(args, &params); err != nil {
return ToolResult{Success: false, Error: fmt.Sprintf(“参数解析失败: %v”, err)}
}

var result float64
switch params.Operation {
case “add”:
result = params.A + params.B
case “subtract”:
result = params.A – params.B
case “multiply”:
result = params.A * params.B
case “divide”:
if params.B == 0 {
return ToolResult{Success: false, Error: “除数不能为零”}
}
result = params.A / params.B
case “power”:
result = math.Pow(params.A, params.B)
default:
return ToolResult{Success: false, Error: fmt.Sprintf(“未知运算: %s”, params.Operation)}
}

return ToolResult{Success: true, Result: fmt.Sprintf(“%.2f”, result)}
}

// TimeTool 获取当前时间
type TimeTool struct{}

func NewTimeTool() *TimeTool {
return &TimeTool{}
}

func (t *TimeTool) Name() string {
return “get_current_time”
}

func (t *TimeTool) Description() string {
return “获取当前日期和时间”
}

func (t *TimeTool) Execute(args json.RawMessage) ToolResult {
now := time.Now()
return ToolResult{
Success: true,
Result: now.Format(“2006-01-02 15:04:05”),
}
}
“`

工具的定义包含三个部分:名称、描述、以及执行逻辑。名称和描述会发送给 LLM,让它知道什么情况下应该调用这个工具。

### ReAct Agent 实现

ReAct(Reasoning + Acting)模式的核心思想是让 LLM 在推理过程中交替进行思考和行动。简单来说,就是让模型在每一步都考虑当前状态,需要调用工具时就调用工具,然后把工具返回的结果加入到对话历史中,继续推理,直到完成任务。

“`go
package main

import (
“encoding/json”
“fmt”
“log”
)

// Tool 定义工具接口
type Tool interface {
Name() string
Description() string
Execute(args json.RawMessage) ToolResult
}

// Agent LLM Agent
type Agent struct {
client *OllamaClient
tools map[string]Tool
model string
maxSteps int
}

// NewAgent 创建新的 Agent
func NewAgent(client *OllamaClient, model string, maxSteps int) *Agent {
return &Agent{
client: client,
tools: make(map[string]Tool),
model: model,
maxSteps: maxSteps,
}
}

// RegisterTool 注册工具
func (a *Agent) RegisterTool(tool Tool) {
a.tools[tool.Name()] = tool
}

// BuildToolsMessage 构建工具定义消息
func (a *Agent) BuildToolsMessage() []Tool {
var tools []Tool
for _, tool := range a.tools {
tools = append(tools, Tool{
Type: “function”,
Function: struct {
Name string `json:”name”`
Description string `json:”description”`
Parameters struct {
Type string `json:”type”`
Properties map[string]struct {
Type string `json:”type”`
Description string `json:”description”`
} `json:”properties”`
Required []string `json:”required”`
} `json:”parameters”`
}{
Name: tool.Name(),
Description: tool.Description(),
Parameters: struct {
Type string `json:”type”`
Properties map[string]struct {
Type string `json:”type”`
Description string `json:”description”`
} `json:”properties”`
Required []string `json:”required”`
}{
Type: “object”,
Properties: map[string]struct {
Type string `json:”type”`
Description string `json:”description”`
}{},
Required: []string{},
},
},
})
}
return tools
}

// Run 运行 Agent
func (a *Agent) Run(prompt string) error {
messages := []Message{
{Role: “user”, Content: prompt},
}

for step := 0; step < a.maxSteps; step++ { fmt.Printf("\n--- Step %d ---\n", step+1) req := ChatRequest{ Model: a.model, Messages: messages, Tools: a.BuildToolsMessage(), Stream: false, } resp, err := a.client.Chat(req) if err != nil { return fmt.Errorf("chat request failed: %w", err) } // 打印 LLM 回复 if resp.Message.Content != "" { fmt.Printf("LLM: %s\n", resp.Message.Content) } // ���查���否有工具调用 if len(resp.Message.ToolCalls) == 0 { // 没有工具调用,任务完成 return nil } // 处理工具调用 for _, tc := range resp.Message.ToolCalls { toolName := tc.Function.Name args := tc.Function.Arguments fmt.Printf("调用工具: %s\n", toolName) fmt.Printf("参数: %s\n", string(args)) tool, exists := a.tools[toolName] if !exists { messages = append(messages, Message{ Role: "tool", Content: fmt.Sprintf("错误: 未知工具 %s", toolName), }) continue } result := tool.Execute(args) var resultContent string if result.Success { resultContent = result.Result } else { resultContent = fmt.Sprintf("错误: %s", result.Error) } fmt.Printf("结果: %s\n", resultContent) // 将工具结果添加到消息历史 messages = append(messages, Message{ Role: "assistant", Content: "", }) messages = append(messages, Message{ Role: "tool", Content: resultContent, }) } } return fmt.Errorf("达到最大迭代次数 %d", a.maxSteps) } ``` 这段代码是整个 Agent 的核心。运行时会进行多轮对话:发送请求、检查响应中是否有工具调用、有的话执行工具并把结果加到历史消息中、继续发送请求,直到不需要调用工具为止。 ### 主函数与测试运行 把以上各个部分组合在一起: ```go func main() { // 创建 Ollama 客户端 client := NewOllamaClient("http://localhost:11434") // 创建 Agent agent := NewAgent(client, "llama3.1:8b", 10) // 注册工具 agent.RegisterTool(NewCalculator()) agent.RegisterTool(NewTimeTool()) // 运行测试 prompt := "请计算 2 的 10 次方,然后告诉我当前时间" log.Printf("开始执行: %s\n", prompt) if err := agent.Run(prompt); err != nil { log.Fatalf("Agent 执行失败: %v", err) } } ``` 运行测试: ```bash go run main.go ``` 输出结果差不多是这样的: ``` 2026/05/04 09:00:00 开始执行: 请计算 2 的 10 次方,然后告诉我当前时间 --- Step 1 --- LLM: 让我帮你完成这个任务。我需要先计算 2 的 10 次方。 调用工具: calculator 参数: {"operation":"power","a":2,"b":10} 结果: 1024.00 --- Step 2 --- LLM: 2 的 10 次方等于 1024。现在让我获取当前时间。 调用工具: get_current_time 参数: {} 结果: 2026-05-04 09:00:15 --- Step 3 --- LLM: 计算结果是 1024,当前时间是 2026-05-04 09:00:15。 ``` 可以看到 Agent 完成了两次工具调用:先计算幂运算,然后获取时间,最后把两个结果汇总回复给我们。 ## 实际应用案例:智能客服系统 上面的示例展示了基础功能,让我们看一个更接近实际业务场景的案例:智能客服系统。 在这个案例中,我们需要让 Agent 具备以下能力:查询订单状态、查询商品信息、计算退款金额。这几个功能在实际电商场景中非常常见。 首先定义这几个业务工具: ```go // OrderTool 订单查询工具 type OrderTool struct{} func NewOrderTool() *OrderTool { return &OrderTool{} } func (t *OrderTool) Name() string { return "get_order_status" } func (t *OrderTool) Description() string { return "查询订单状态,支持订单号查询,返回订单当前状态、金额、收货地址等信息" } func (t *OrderTool) Execute(args json.RawMessage) ToolResult { var params struct { OrderID string `json:"order_id"` } if err := json.Unmarshal(args, &params); err != nil { return ToolResult{Success: false, Error: fmt.Sprintf("参数解析失败: %v", err)} } // 模拟查询数据库 orders := map[string]struct { Status string Amount float64 Address string }{ "ORD001": {Status: "已发货", Amount: 299.00, Address: "北京市朝阳区xxx"}, "ORD002": {Status: "待付款", Amount: 599.00, Address: "上海市浦东新区xxx"}, "ORD003": {Status: "已完成", Amount: 1299.00, Address: "广州市天河区xxx"}, } order, exists := orders[params.OrderID] if !exists { return ToolResult{Success: false, Error: fmt.Sprintf("订单 %s 不存在", params.OrderID)} } result := fmt.Sprintf("订单号: %s\\n状态: %s\\n金额: %.2f\\n收货地址: %s", params.OrderID, order.Status, order.Amount, order.Address) return ToolResult{Success: true, Result: result} } // RefundTool 退款计算工具 type RefundTool struct{} func NewRefundTool() *RefundTool { return &RefundTool{} } func (t *RefundTool) Name() string { return "calculate_refund" } func (t *RefundTool) Description() string { return "计算退款金额,支持根据订单金额和退款比例计算实际退款金额" } func (t *RefundTool) Execute(args json.RawMessage) ToolResult { var params struct { OrderID string `json:"order_id"` RefundPercent float64 `json:"refund_percent"` } if err := json.Unmarshal(args, &params); err != nil { return ToolResult{Success: false, Error: fmt.Sprintf("参数解析失败: %v", err)} } // 模拟订单金额 orderAmounts := map[string]float64{ "ORD001": 299.00, "ORD002": 599.00, "ORD003": 1299.00, } amount, exists := orderAmounts[params.OrderID] if !exists { return ToolResult{Success: false, Error: fmt.Sprintf("订单 %s 不存在", params.OrderID)} } refundAmount := amount * params.RefundPercent / 100 result := fmt.Sprintf("订单金额: %.2f\\n退款比例: %.0f%%\\n实际退款金额: %.2f", amount, params.RefundPercent, refundAmount) return ToolResult{Success: true, Result: result} } ``` 然后修改主函数,注册这几个新工具: ```go func main() { client := NewOllamaClient("http://localhost:11434") agent := NewAgent(client, "llama3.1:8b", 10) // 注册原有的两个工具 agent.RegisterTool(NewCalculator()) agent.RegisterTool(NewTimeTool()) // 注册新的业务工具 agent.RegisterTool(NewOrderTool()) agent.RegisterTool(NewRefundTool()) // 测试智能客服场景 prompt := "我的订单 ORD001 已发货但还没收到货,请问现在是什么状态?如果申请退款的话能退多少?" log.Printf("智能客服测试: %s\n", prompt) if err := agent.Run(prompt); err != nil { log.Fatalf("Agent 执行失败: %v", err) } } ``` 运行这个测试,Agent 会先查询订单状态,发现是已发货状态,然后根据状态判断是否可以退款,并计算退款金额。这就是典型的高校智能客服场景。 从测试结果可以看到,整个对话过程是完全自然的多轮交互:用户提出问题,Agent 分析需求,调用相应工具,获取结果,最终给出回复。整个过程不需要人工干预。 ## 常见问题与解决方案 在实际应用过程中,可能会遇到一些常见问题,这里记录一下解决思路。 第一个问题是模型不调用工具。这通常是因为模型对工具的理解不够。这种情况下有几个调整思路:优化工具描述,让模型更容易理解工具的用途;减少工具数量,一次性提供太多工具会增加模型选择的难度;尝试更换模型,某些模型在 function calling 方面的表现会更好。 第二个问题是参数解析失败。工具返回的参数可能不符合预期格式。这种情况下可以在工具的 Execute 方法中添加更完善的参数验证和错误处理。 第三个问题是循环调用。如果 Agent 一直重复调用同一个工具,可以设置最大迭代次数来避免这个问题,这在上面的代码中已经通过 maxSteps 参数实现了。 第四个问题是响应速度。Ollama 在本地运行虽然免费,但推理速度取决于硬件配置。如果觉得慢,可以选择更小的模型,或者考虑使用 GPU 加速。 ## 总结与扩展 这篇文章记录了如何用 Go 语言基于 Ollama 构建本地 LLM Agent 的完整过程。从环境准备开始,到封装 HTTP 客户端、定义工具、执行 ReAct 推理模式,实现了让大模型调用外部工具的能力。 这个架构的优势在于纯 Go 实现,不需要 Python 环境,部署和维护都比较方便。工具部分用了接口设计,扩展新工具只需要实现接口方法就行,不需要改动核心逻辑。 后续可以尝试的方向包括:接入向量数据库实现 RAG(检索增强生成),让 Agent 能够查询自己的知识库;添加更多工具比如调用 HTTP API、查询数据库;实现更复杂的推理链,比如多步骤规划。 ## 性能优化建议 在实际生产环境中部署时,有一些性能优化建议值得参考。 首先是模型选择。如果对响应速度要求较高,可以选择较小的模型如 qwen2.5:7b,在配置一般的服务器上也能有不错的响应速度。如果对质量要求更高,可以选择更大的模型如 llama3.1:70b,但需要更强大的硬件支持。 其次是工具执行超时。设置合理的超时时间可以避免单个工具执行时间过长导致整个 Agent 阻塞。可以在工具实现中添加超时机制。 最后是结果缓存。对于一些查询频率高的工具,可以添加缓存层,避免重复查询数据库或外部 API。 代码已经上传到 GitHub,有需要的可以参考。遇到问题欢迎交流讨论。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇