news 2026/10/22 13:12:44

Go AI Agent 实战 09|用 net/http 真实调用 DeepSeek

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go AI Agent 实战 09|用 net/http 真实调用 DeepSeek

本系列共 10 阶段,面向有 Go 后端基础的开发者,全程用 Go 手写实现。
当前进度:阶段二 Go 调用大模型 · 第 2 课(09)
上一篇:HTTP 模型 API 的工作原理与请求流程

上一课已经讲清了模型 API 的请求与响应结构。这一课把骨架补成完整程序:使用 Go 标准库net/http读取环境变量、发送真实请求、解析响应,并把失败原因转换成可处理的错误。

课程仍然不依赖第三方 SDK。你会看到 HTTP 客户端如何一步步完成模型调用,也会知道为什么不能把“拿到了一个 HTTP 响应”直接当成调用成功。

一、本课学习目标

学完本课,你应该能够:

  1. 使用http.NewRequest和http.Client.Do完成一次模型调用。
  2. 使用 Go 结构体描述请求和响应。
  3. 通过环境变量读取 API Key 与模型标识。
  4. 检查 HTTP 状态码并限制错误响应体的读取大小。
  5. 校验choices和回答内容,避免把异常响应当成成功。
  6. 用可复用的函数组织模型调用,而不是把所有逻辑塞进main。

二、从接口约定到 Go 程序

一次最小模型调用可以对应五个阶段:

读取配置 ↓ 构造请求 JSON ↓ 创建并发送 HTTP 请求 ↓ 检查状态码并解析响应 ↓ 校验最终回答

每个阶段都可能失败。例如:

  • 没有配置 API Key,程序不应该继续发送请求。
  • JSON 编码失败,说明本地数据结构有问题。
  • client.Do返回错误,通常意味着请求没有正常完成。
  • 服务端返回 401 或 429,说明请求已经到达服务端,但被拒绝。
  • HTTP 返回成功,响应缺少choices,仍然不能视为业务成功。

把这些阶段拆开,报错时才能快速定位问题。

三、准备项目

在 PowerShell 中执行:

mkdir go-deepseek-demoSet-Locationgo-deepseek-demo go mod init go-deepseek-demo

本课只使用 Go 标准库,不需要额外安装依赖。

四、完整示例

新建main.go,写入以下代码:

packagemainimport("bytes""encoding/json""errors""fmt""io""net/http""os""strings""time")const(apiURL="https://api.deepseek.com/chat/completions"defaultModel="deepseek-flash")typeChatMessagestruct{Rolestring`json:"role"`Contentstring`json:"content"`}typeChatRequeststruct{Modelstring`json:"model"`Messages[]ChatMessage`json:"messages"`}typeChatResponsestruct{IDstring`json:"id"`Modelstring`json:"model"`Choices[]struct{Message ChatMessage`json:"message"`FinishReasonstring`json:"finish_reason"`}`json:"choices"`}funccallModel(apiKey,model,promptstring)(string,error){ifstrings.TrimSpace(apiKey)==""{return"",errors.New("DEEPSEEK_API_KEY 未设置")}requestData:=ChatRequest{Model:model,Messages:[]ChatMessage{{Role:"user",Content:prompt,},},}body,err:=json.Marshal(requestData)iferr!=nil{return"",fmt.Errorf("序列化请求失败: %w",err)}req,err:=http.NewRequest(http.MethodPost,apiURL,bytes.NewReader(body),)iferr!=nil{return"",fmt.Errorf("创建 HTTP 请求失败: %w",err)}req.Header.Set("Authorization","Bearer "+apiKey)req.Header.Set("Content-Type","application/json")client:=&http.Client{Timeout:90*time.Second,}resp,err:=client.Do(req)iferr!=nil{return"",fmt.Errorf("发送模型请求失败: %w",err)}deferresp.Body.Close()ifresp.StatusCode<200||resp.StatusCode>=300{errorBody,readErr:=io.ReadAll(io.LimitReader(resp.Body,1<<20),)ifreadErr!=nil{return"",fmt.Errorf("HTTP %d,读取错误响应失败: %w",resp.StatusCode,readErr,)}return"",fmt.Errorf("模型 API 返回 HTTP %d: %s",resp.StatusCode,strings.TrimSpace(string(errorBody)),)}varresult ChatResponseiferr:=json.NewDecoder(resp.Body).Decode(&result);err!=nil{return"",fmt.Errorf("解析模型响应失败: %w",err)}iflen(result.Choices)==0{return"",errors.New("模型响应中 choices 为空")}answer:=strings.TrimSpace(result.Choices[0].Message.Content)ifanswer==""{return"",errors.New("模型返回了空回答")}returnanswer,nil}funcrun()error{apiKey:=strings.TrimSpace(os.Getenv("DEEPSEEK_API_KEY"))model:=strings.TrimSpace(os.Getenv("DEEPSEEK_MODEL"))ifmodel==""{model=defaultModel}answer,err:=callModel(apiKey,model,"请用 Go 代码举例解释什么是 goroutine。",)iferr!=nil{returnerr}fmt.Println("模型回答:")fmt.Println(answer)returnnil}funcmain(){iferr:=run();err!=nil{fmt.Fprintln(os.Stderr,"调用失败:",err)os.Exit(1)}}

deepseek-flash是系列代码中的默认示例。模型名称可能随账号和供应商配置变化,可以通过DEEPSEEK_MODEL覆盖,也可以直接修改适配器的默认值。

五、配置并运行

在 PowerShell 中设置当前会话的密钥:

$env:DEEPSEEK_API_KEY ="你的真实API_KEY"

如果账号使用的模型标识与示例不同,再设置:

$env:DEEPSEEK_MODEL ="控制台中的模型ID"

然后运行:

go run.

成功时,终端会输出一段模型回答。请求 ID、耗时、Token 用量等诊断信息还没有在这里打印,后续课程会逐步加入。

这类环境变量通常只对当前 PowerShell 会话及其子进程有效。关闭终端后可能需要重新设置。不要把真实密钥写进截图、聊天消息或 Git 仓库。

六、逐段理解关键代码

1. 用结构体表达协议

typeChatMessagestruct{Rolestring`json:"role"`Contentstring`json:"content"`}

JSON 标签把 Go 字段名和 API 字段名连接起来:

  • Go 中的Role序列化为 JSON 中的role。
  • Go 中的Content序列化为 JSON 中的content。

直接拼接 JSON 字符串容易漏掉转义、逗号或引号。结构体加encoding/json的做法更可靠,也方便后续加入多轮消息、temperature和stream等字段。

2.json.Marshal只负责编码

body,err:=json.Marshal(requestData)

它把 Go 值转换成[]byte,不会自动发送网络请求。

如果更新到json.Marshal这一步就失败,问题范围通常已经缩小到输入数据或字段定义。

3.NewRequest与Client.Do分工不同

req,err:=http.NewRequest(http.MethodPost,apiURL,bytes.NewReader(body))

NewRequest只是创建请求对象。真正建立网络连接并等待响应的是:

resp,err:=client.Do(req)

也要区分两个错误来源:

  • err != nil:HTTP 请求没有正常完成,例如 DNS、TLS、连接失败或超时。
  • err == nil:拿到了 HTTP 响应,但响应仍然可能是 401、429 或 500。

4.defer resp.Body.Close()释放连接资源

读取响应体后必须关闭resp.Body。否则在持续发送请求的服务中,底层连接和相关资源可能无法及时释放。

示例把主逻辑放进run(),由main()统一处理退出状态。这能保证函数返回时先执行defer,再决定是否结束进程。

5. 限制错误响应的读取量

io.LimitReader(resp.Body,1<<20)

1 << 20等于 1 MiB。异常服务或代理可能返回很大的 HTML 错误页,直接读入内存并不安全。

生产日志仍需要对错误内容做脱敏和截断,不能因为加了读取上限就无差别记录全部响应。

6. 解析成功不代表业务成功

iflen(result.Choices)==0{return"",errors.New("模型响应中 choices 为空")}

空切片不包含索引0。先检查长度,再读取result.Choices[0],可以避免运行时 panic。

之后还要检查回答去掉空白后是否为空。只有协议解析和业务校验都通过,调用才算真正成功。

七、常见失败如何排查

现象优先检查
提示 API Key 未设置是否在同一个终端窗口中设置环境变量
401API Key 是否有效,Bearer请求头是否完整
400 或 422模型标识、消息结构和请求 JSON 是否符合要求
429请求频率、并发量和账号配额
Client.Do返回错误DNS、网络、TLS、代理和超时设置
JSON 解析失败响应是否真的是 JSON,结构是否与代码一致
choices为空服务端是否返回了异常或非预期的成功响应

排查时优先保留状态码、请求 ID、错误类型和安全截断后的错误消息,不要打印完整Authorization请求头。

八、本课总结

  1. 一次模型调用需要依次完成配置读取、请求编码、HTTP 发送、状态检查、JSON 解析和业务校验。
  2. NewRequest构造请求,Client.Do才真正发送请求。
  3. err == nil只表示拿到了 HTTP 响应,不代表状态码成功。
  4. HTTP 成功也不代表回答字段一定有效。
  5. 环境变量可以把代码与密钥分离,但不能代替权限控制和日志脱敏。
  6. 可复用函数应返回error,由程序入口统一决定如何退出。

九、课后练习

练习 1:请求为什么使用 POST

为什么不把问题、API Key 和模型参数全部拼到 URL 中?

参考答案:聊天接口使用 POST 接收结构化 JSON 请求体,更适合传输长文本和复杂参数。API Key 应放在 HTTPS 认证请求头中,放进 URL 容易出现在访问日志、代理日志和浏览器历史里,增加泄露风险。

练习 2:为什么先检查choices

为什么不能直接访问result.Choices[0].Message.Content?

参考答案:choices可能是空数组。直接访问索引0会触发 panic。应先检查长度,再检查回答内容是否为空,并把异常响应转换为普通错误。

练习 3:什么情况下不应继续重试

如果程序收到 HTTP 401,是否应该立即再次发送相同请求?

参考思路:通常不应该。401 表示认证失败,重复同一个无效密钥不会修复问题。应先检查密钥、请求头和账号状态。只有明确属于暂时性故障的错误,才适合在限制次数的前提下重试。


下一篇:Go AI Agent 实战 10|环境变量、API Key 与密钥安全

专栏:Go 开发者学 AI Agent:从大模型到工程落地

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 13:10:15

星途天城玄女怎么搭配阵容 星途天城玄女配队思路

星途天城玄女作为雷系群攻核心输出&#xff0c;不少玩家一直在寻找适配她输出节奏的队伍。星途天城玄女依靠连锁雷伤压低全体血线&#xff0c;敌人残血时可触发额外连击&#xff0c;降神成型之后清场能力尤为突出。想要尝试多种搭配方案&#xff0c;反复测试玄女配队与龙纹组合…

作者头像 李华
网站建设 2026/10/22 13:10:13

python基础(六)-- JSON输入与文件读写

具体步骤 1、保存json文件&#xff0c;格式示例如下 [{"id": "S001", "model": "A", "passed": true, "latency_ms": 1999},{"id": "S002", "model": "A", "passed…

作者头像 李华
网站建设 2026/10/22 13:09:59

高频必考!辗转相除:从gcd到裴蜀定理,两道经典建模题

给你一副牌 [1,2,3,4,4,3,2,1]&#xff0c;问能否按 X ≥ 2张一组分组&#xff0c;且每组内数字相同。 第一反应可能是搜索、回溯、枚举 X。但正解只有&#xff1a; gcd(所有数字的计数) > 2为什么&#xff1f;因为“能分组”这件事&#xff0c;本质上是在问“这些计数有没有…

作者头像 李华
网站建设 2026/10/22 13:09:59

【RAG开发】LangChain消息的简写形式

SystemMessage、HumanMessage、AIMessage可以有如下的简写形式 SystemMessage(content "") HumanMessage(content "") AIMessage(content "")简写&#xff1a; ("system",""), ("human",""), ("…

作者头像 李华
网站建设 2026/10/22 13:09:57

LangChain入门 -- 记忆

前面我们讲的智能体都是没有记忆的&#xff0c;也就是前面问的话&#xff0c;后面第二次聊天调用就忘了&#xff0c;不能连接Agent的记忆&#xff1a; 短期记忆&#xff08;short-term memory&#xff09;&#xff1a;当前任务或会话的上下文 长期记忆&#xff08;long-term me…

作者头像 李华