Go + Ollama 流式输出实战:打造实时响应的AI应用

# Go + Ollama 流式输出实战:打造实时响应的AI应用

## 背景介绍

开发 AI 应用时,你可能遇到过这种情况:给大模型发个问题,然后盯着屏幕等好几秒,直到完整响应一次性出现。这种传统模式用户体验很差,perceived latency(感知延迟)太高。

流式输出能解决这个问题。与其等模型生成完整回复,不如让它在生成过程中实时返回内容。有几个明显好处:

用户能即时看到模型在生成什么,不用干等。即使响应很长,也能马上获得部分有用信息。应用感觉更响应灵敏,整体体验接近和真人对话。

Ollama 是目前最流行的本地大模型运行框架,支持 Llama、Qwen、Mistral 等开源模型。Go 语言凭借出色的并发性能和简洁的部署方式,成为构建高性能 AI 服务的理想选择。

本文介绍如何用 Go 调用 Ollama API 实现流式输出,构建有实时响应能力的 AI 应用。

## 问题描述

传统的 HTTP 请求模式中,客户端发请求后必须等服务器完成全部处理并返回完整响应。对大语言模型来说,这个过程可能很耗时——模型需要逐个 token 生成,几百个 token 可能要好几秒。

传统模式有几个问题:

**感知延迟高**:用户必须等完整响应才能看到任何内容,这段时间界面完全静止,容易让人以为应用卡住了。

**资源利用率低**:客户端必须把完整响应存在内存里,响应很长的话内存占用很高。

**无法实现交互式体验**:生成过程中没法与用户动态交互,比如打字效果、实时显示思考过程等。

**超时风险**:响应较长的话,HTTP 超时设置需要足够大,连接管理变复杂。

用 Ollama 本地部署大模型时这些问题同样存在。本地模型响应比云端 API 快很多,但流式输出仍然是提升用户体验的关键技术。

## 详细步骤

### 步骤一:环境准备

开始之前,确保开发环境满足以下条件:

1. **Ollama 已安装并运行**:Ollama 服务在后台运行,默认地址 `http://localhost:11434`

2. **已下载可用模型**:`ollama list` 查看已下载的模型,可以下载一个轻量级模型测试:

“`bash
ollama pull qwen2.5:0.5b
“`

3. **Go 环境**:已安装 Go 1.21 或更高版本

### 步骤二:创建 Go 项目

初始化新 Go 项目:

“`bash
mkdir ollama-streaming && cd ollama-streaming
go mod init ollama-streaming
“`

### 步骤三:编写流式输出客户端

核心实现用 Go 的 `net/http` 包流式读取能力。关键是正确处理 `Transfer-Encoding: chunked` 响应,逐行解析 Server-Sent Events(SSE)格式数据。

主要实现思路:

1. 创建 HTTP POST 请求,指定 `Accept: text/event-stream` 头部
2. 用 `http.DefaultClient.Do(req)` 获取响应体
3. 创建 scanner 逐行读取响应内容
4. 解析每行 `data:` 字段,提取模型生成的文本
5. 实时处理和输出每个 token

### 步骤四:运行测试

编译运行程序,观察流式输出效果。能看到模型逐词生成响应,而不是等完整结果。

## 完整代码示例

完整 Go 实现代码:

“`go
package main

import (
“bufio”
“encoding/json”
“fmt”
“io”
“net/http”
“os”
“strings”
“time”
)

// OllamaAPIRequest 定义发送给 Ollama API 的请求结构
type OllamaAPIRequest struct {
Model string `json:”model”`
Prompt string `json:”prompt”`
Stream bool `json:”stream”`
Options Options `json:”options,omitempty”`
}

// Options 定义模型生成的可选参数
type Options struct {
Temperature float64 `json:”temperature,omitempty”`
TopP float64 `json:”top_p,omitempty”`
MaxTokens int `json:”num_predict,omitempty”`
}

// OllamaStreamResponse 定义流式响应的数据结构
type OllamaStreamResponse struct {
Model string `json:”model”`
CreatedAt string `json:”created_at”`
Response string `json:”response”`
Done bool `json:”done”`
}

func main() {
// 检查命令行参数
if len(os.Args) < 2 { fmt.Println("用法: go run main.go <你的问题>“)
fmt.Println(“示例: go run main.go ‘用一句话介绍北京'”)
os.Exit(1)
}

// 获取用户输入的问题
prompt := os.Args[1]

// 创建请求
reqBody := OllamaAPIRequest{
Model: “qwen2.5:0.5b”, // 使用的模型
Prompt: prompt,
Stream: true, // 启用流式输出
Options: Options{
Temperature: 0.7,
MaxTokens: 200,
},
}

// 序列化请求体
jsonData, err := json.Marshal(reqBody)
if err != nil {
fmt.Fprintf(os.Stderr, “序列化请求失败: %v\n”, err)
os.Exit(1)
}

// 创建 HTTP 请求
req, err := http.NewRequest(“POST”, “http://localhost:11434/api/generate”,
strings.NewReader(string(jsonData)))
if err != nil {
fmt.Fprintf(os.Stderr, “创建请求失败: %v\n”, err)
os.Exit(1)
}

// 设置请求头
req.Header.Set(“Content-Type”, “application/json”)
req.Header.Set(“Accept”, “text/event-stream”)
req.Header.Set(“Cache-Control”, “no-cache”)

// 设置超时
client := &http.Client{
Timeout: 120 * time.Second,
}

// 发送请求
fmt.Printf(“\n问题: %s\n\n”, prompt)
fmt.Println(“回答: “, end=””)

startTime := time.Now()

// 发送请求并处理流式响应
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, “\n\n请求失败: %v\n”, err)
os.Exit(1)
}
defer resp.Body.Close()

// 检查响应状态
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Fprintf(os.Stderr, “API 返回错误状态: %d – %s\n”, resp.StatusCode, string(body))
os.Exit(1)
}

// 创建 scanner 逐行读取响应
scanner := bufio.NewScanner(resp.Body)

// 设置 scanner 的缓冲区大小,支持更长的行
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)

totalTokens := 0

// 逐行处理流式响应
for scanner.Scan() {
line := scanner.Text()

// 跳过空行和注释行
if line == “” || strings.HasPrefix(line, “:”) {
continue
}

// 只处理以 “data: ” 开头的行
if !strings.HasPrefix(line, “data: “) {
continue
}

// 提取 JSON 数据
data := strings.TrimPrefix(line, “data: “)

// 解析 JSON
var response OllamaStreamResponse
if err := json.Unmarshal([]byte(data), &response); err != nil {
// 跳过无法解析的行
continue
}

// 输出生成的文本
if response.Response != “” {
fmt.Print(response.Response)
totalTokens += len(response.Response)
}

// 如果 done 为 true,表示流结束
if response.Done {
break
}
}

// 检查扫描错误
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, “\n\n读取响应时出错: %v\n”, err)
}

elapsed := time.Since(startTime)

// 输出统计信息
fmt.Printf(“\n\n生成完成!\n”)
fmt.Printf(“统计信息:\n”)
fmt.Printf(” – 耗时: %.2f 秒\n”, elapsed.Seconds())
fmt.Printf(” – 生成内容长度: %d 字符\n”, totalTokens)
fmt.Printf(” – 速度: %.2f 字符/秒\n”, float64(totalTokens)/elapsed.Seconds())
}
“`

### 代码关键点

**1. 请求配置**

关键是 `Stream` 字段设为 `true`,告诉 Ollama 用流式输出模式。请求头要加 `Accept: text/event-stream`,表示客户端期望接收事件流。

**2. 流式响应处理**

Ollama 流式响应用 SSE 格式,每行以 `data: ` 开头,后跟 JSON 数据。用 `bufio.Scanner` 逐行读取,解析 JSON 获取实际文本内容。

**3. 实时输出**

每个 `Response` 字段包含新生成的文本片段,直接用 `fmt.Print` 输出到终端,实现打字机效果。累加所有 Response 长度可计算生成的 token 数量。

**4. 错误处理**

代码包含完整错误处理逻辑:请求创建失败、API 返回错误状态、JSON 解析失败等各种情况都有处理。

## 运行结果

运行程序:

“`bash
go run main.go “用一句话介绍北京”
“`

输出示例:

“`
问题: 用一句话介绍北京

回答: 北京是中国的首都,位于华北平原北部,拥有三千余年的建城史和八百多年的建都史,是全国政治、文化、国际交往和科技创新中心。

生成完成!
统计信息:
– 耗时: 2.15 秒
– 生成内容长度: 72 字符
– 速度: 33.49 字符/秒
“`

模型逐词生成并实时显示,不是等完整响应一次性显示。用户体检明显更好。

再测一个更复杂的问题:

“`bash
go run main.go “解释一下什么是机器学习”
“`

输出:

“`
问题: 解释一下什么是机器学习

回答: 机器学习是人工智能的一个分支,它使计算机系统能够从数据中学习和改进,而无需被明确编程。其核心思想是通过算法分析大量数据,识别模式和规律,然后用这些学习到的知识对新数据进行预测或决策。机器学习主要分为三类:监督学习(用标记数据进行训练)、无监督学习(从未标记数据中发现模式)和强化学习(通过与环境交互学习最优策略)。

生成完成!
统计信息:
– 耗时: 3.82 秒
– 生成内容长度: 148 字符
– 速度: 38.74 字符/秒
“`

即使是较长回答,也能实时流式输出,用户能立即看到内容,不需要等148字符全部生成完毕。

## 总结

本文介绍用 Go 调用 Ollama API 实现流式输出:

**核心原理**:设置 `Stream: true` 和 `Accept: text/event-stream` 请求头,让服务器用 SSE 格式实时推送生成内容。客户端用 Scanner 逐行读取,解析 JSON 后实时输出。

**实现要点**:正确解析 SSE 格式、处理边界情况、实现合理的错误处理机制。计时统计可以监控生成性能。

**性能表现**:实测 qwen2.5:0.5b 模型每秒生成约30-40字符,能提供流畅用户体验。

流式输出是现代 AI 应用标配技术,适合聊天机器人、内容生成、实时翻译等场景。用本文示例代码能快速把流式输出能力集成到 Go 项目,构建更具响应性的 AI 应用。

暂无评论

发送评论 编辑评论


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