DeepSeek API 客户端封装
本文介绍了如何封装DeepSeek API客户端以构建稳定可靠的大模型通信桥梁。主要内容包括: 客户端核心结构设计:通过DeepSeekClient结构体封装API密钥、URL和HTTP客户端,类比私人助理处理通信细节。 消息数据结构:定义了Message和ChatRequest结构体,规范与大模型交互的数据格式。 同步生成模式实现:详细展示了从构造请求、发送到解析响应的完整流程,强调错误处理和资
DeepSeek API 客户端封装:构建稳定可靠的大模型通信桥梁
本文是《从零构建墨言博客助手》系列的第13章。完整源码请访问:https://github.com/2692341798/InkWords
引言:为什么需要封装API客户端?
想象一下,你要和一位远方的专家(DeepSeek模型)对话。每次对话都需要:
- 找到正确的地址(API端点)
- 说清楚你的问题(构造请求)
- 等待专家回答(发送请求)
- 理解专家的回复(解析响应)
如果每次对话都从头开始做这些步骤,不仅效率低下,而且容易出错。这就是我们需要封装API客户端的原因——创建一个"专业翻译官",帮我们处理所有通信细节。
一、客户端核心结构设计
让我们先看看DeepSeekClient的结构设计:
// DeepSeekClient 是与DeepSeek API通信的核心客户端
type DeepSeekClient struct {
APIKey string // 身份验证密钥,相当于"门禁卡"
APIURL string // API地址,相当于"专家办公室地址"
Client *http.Client // HTTP客户端,相当于"快递员"
}
生活化比喻:这就像你有一个私人助理(DeepSeekClient),他知道专家的地址(APIURL),有门禁卡(APIKey),并且有自己的交通工具(http.Client)。
创建客户端的工厂函数:
// NewDeepSeekClient 创建并初始化一个新的DeepSeek客户端
func NewDeepSeekClient(apiKey string) *DeepSeekClient {
return &DeepSeekClient{
APIKey: apiKey, // 保存API密钥
APIURL: defaultDeepSeekAPIURL, // 使用默认API地址
Client: &http.Client{}, // 创建新的HTTP客户端
}
}
二、消息数据结构定义
在与大模型对话时,我们需要定义清晰的数据结构。这就像写信要有固定的格式:
// Message 表示聊天中的一条消息
type Message struct {
Role string `json:"role"` // 角色:user(用户)或assistant(助手)
Content string `json:"content"` // 消息内容
}
// ChatRequest 表示发送给DeepSeek API的请求
type ChatRequest struct {
Model string `json:"model"` // 使用的模型名称
Messages []Message `json:"messages"` // 对话历史
Stream bool `json:"stream"` // 是否使用流式响应
}
代码解释:
json:"role"是Go语言的标签(tag),告诉JSON编码器/解码器字段在JSON中的名称Stream字段控制返回方式:false表示一次性返回完整结果,true表示流式返回
三、同步生成模式实现
同步模式就像传统的邮件通信:发送完整请求,等待完整响应。
// Generate 调用DeepSeek API并返回完整响应
func (c *DeepSeekClient) Generate(ctx context.Context, model string, messages []Message) (string, error) {
// 1. 构造请求体
reqBody := ChatRequest{
Model: model,
Messages: messages,
Stream: false, // 关键:设置为非流式
}
// 2. 将结构体转换为JSON字符串
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request body: %w", err)
}
// 3. 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.APIURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// 4. 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
// 5. 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close() // 确保响应体被关闭
// 6. 读取响应
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
// 7. 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API request failed with status %d: %s",
resp.StatusCode, string(bodyBytes))
}
// 8. 解析JSON响应
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(bodyBytes, &result); err != nil {
return "", fmt.Errorf("failed to unmarshal response: %w", err)
}
// 9. 提取内容
if len(result.Choices) == 0 {
return "", fmt.Errorf("API returned empty choices")
}
return result.Choices[0].Message.Content, nil
}
关键点解析:
- 错误处理:每一步都有详细的错误信息,使用
fmt.Errorf的%w包装错误 - 资源管理:使用
defer resp.Body.Close()确保网络连接被正确关闭 - 上下文传递:
ctx参数允许调用者取消长时间运行的请求
四、流式生成模式实现
流式模式就像实时对话:对方一边思考一边回答,你也能一边听一边理解。
下面是流式生成的核心实现:
// GenerateStream 调用流式API并实时推送数据块
func (c *DeepSeekClient) GenerateStream(ctx context.Context, model string,
messages []Message, chunkChan chan<- string) (string, error) {
defer close(chunkChan) // 确保函数退出时关闭通道
// 1. 构造流式请求体
reqBody := ChatRequest{
Model: model,
Messages: messages,
Stream: true, // 关键:设置为流式
}
// 2-5步与同步模式相同(序列化、创建请求、设置头部、发送请求)
// ...
// 6. 创建缓冲读取器(高效读取流数据)
reader := bufio.NewReader(resp.Body)
var finalFinishReason string
// 7. 循环读取流数据
for {
// 检查上下文是否被取消
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
// 读取一行数据(以\n分隔)
line, err := reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return finalFinishReason, nil
}
return "", fmt.Errorf("failed to read stream: %w", err)
}
lineStr := strings.TrimSpace(string(line))
if lineStr == "" {
continue // 跳过空行
}
// 8. 解析SSE格式:data: {json}
if !strings.HasPrefix(lineStr, "data: ") {
continue // 跳过非数据行
}
data := strings.TrimPrefix(lineStr, "data: ")
if data == "[DONE]" {
return finalFinishReason, nil // 流结束
}
// 9. 解析JSON数据块
var chunk ChatCompletionChunk
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue // 跳过解析失败的数据
}
// 10. 处理有效数据
if len(chunk.Choices) > 0 {
content := chunk.Choices[0].Delta.Content
if content != "" {
chunkChan <- content // 发送到通道
}
// 检查是否结束
if chunk.Choices[0].FinishReason != nil {
finalFinishReason = *chunk.Choices[0].FinishReason
if finalFinishReason == "stop" || finalFinishReason == "length" {
return finalFinishReason, nil
}
}
}
}
}
流式处理的核心机制:
- SSE协议:Server-Sent Events,服务器推送事件的标准格式
- 数据格式:每行以
data:开头,[DONE]表示结束 - 通道通信:使用Go的channel实现生产者-消费者模式
- 增量更新:每次只发送变化的部分(
delta.content)
五、实战:如何使用客户端
场景1:同步生成(适合短文本)
package main
import (
"context"
"fmt"
"log"
"your-project/internal/llm"
)
func main() {
// 1. 创建客户端
client := llm.NewDeepSeekClient("your-api-key-here")
// 2. 准备对话
messages := []llm.Message{
{Role: "user", Content: "请用Go语言写一个Hello World程序"},
}
// 3. 调用生成
ctx := context.Background()
response, err := client.Generate(ctx, "deepseek-chat", messages)
if err != nil {
log.Fatalf("生成失败: %v", err)
}
// 4. 输出结果
fmt.Println("生成的代码:")
fmt.Println(response)
}
场景2:流式生成(适合长文本)
package main
import (
"context"
"fmt"
"log"
"time"
"your-project/internal/llm"
)
func main() {
// 1. 创建客户端和通道
client := llm.NewDeepSeekClient("your-api-key-here")
chunkChan := make(chan string, 100) // 缓冲通道
// 2. 准备对话
messages := []llm.Message{
{Role: "user", Content: "写一篇关于人工智能的短文"},
}
// 3. 启动goroutine接收数据
go func() {
for chunk := range chunkChan {
fmt.Print(chunk) // 实时打印
}
fmt.Println("\n--- 生成完成 ---")
}()
// 4. 调用流式生成
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := client.GenerateStream(ctx, "deepseek-chat", messages, chunkChan)
if err != nil {
log.Fatalf("流式生成失败: %v", err)
}
}
六、错误处理最佳实践
在我们的实现中,错误处理遵循以下原则:
- 尽早失败:发现问题立即返回错误
- 提供上下文:使用
fmt.Errorf("failed to ...: %w", err)包装错误 - 资源清理:使用
defer确保资源释放 - 状态检查:检查HTTP状态码和响应结构
// 示例:完整的错误处理链
func process() error {
data, err := getData()
if err != nil {
return fmt.Errorf("获取数据失败: %w", err)
}
result, err := parseData(data)
if err != nil {
return fmt.Errorf("解析数据失败: %w", err)
}
return saveResult(result)
}
七、性能优化建议
- 连接复用:
http.Client默认启用连接池 - 缓冲读取:使用
bufio.Reader提高读取效率 - 合理超时:为长时间请求设置超时
- 通道缓冲:根据数据量设置合适的通道缓冲区大小
// 优化后的客户端配置
func NewOptimizedClient(apiKey string) *DeepSeekClient {
return &DeepSeekClient{
APIKey: apiKey,
APIURL: defaultDeepSeekAPIURL,
Client: &http.Client{
Timeout: 60 * time.Second, // 设置超时
Transport: &http.Transport{
MaxIdleConns: 100, // 最大空闲连接
IdleConnTimeout: 90 * time.Second, // 空闲连接超时
TLSHandshakeTimeout: 10 * time.Second, // TLS握手超时
},
},
}
}
总结
通过本章的学习,我们完成了DeepSeek API客户端的完整封装:
- 结构设计:定义了清晰的数据结构和客户端接口
- 同步模式:实现了一次性获取完整响应的
Generate方法 - 流式模式:实现了实时推送的
GenerateStream方法 - 错误处理:建立了完善的错误处理机制
- 实战应用:提供了两种使用场景的示例代码
这个客户端封装不仅解决了基本的API调用问题,还提供了:
- 灵活性:支持同步和流式两种模式
- 可靠性:完善的错误处理和资源管理
- 可扩展性:清晰的结构便于后续功能扩展
核心价值:通过封装,我们将复杂的HTTP通信、JSON解析、流处理等细节隐藏起来,让业务代码可以专注于核心逻辑,大大提高了开发效率和代码质量。
下期预告:文档解析器:支持 PDF、DOCX、Markdown
在下一篇文章中,我们将探讨如何构建一个多功能文档解析器。你将学习到:
- 如何解析不同格式的文档(PDF、DOCX、Markdown)
- 提取文本内容的策略和技巧
- 处理复杂文档结构(如表格、图片、样式)
- 构建统一的文档处理接口
无论你是处理用户上传的文档,还是需要从多种来源收集内容,一个强大的文档解析器都是不可或缺的。敬请期待!
更多推荐



所有评论(0)