简介:面向希望接入DeepSeek服务的软件工程师、科研人员与技术爱好者,这份文档系统梳理了从申请API访问权限、研读官方接口文档,到选择开发环境、安装依赖库、编写请求代码直至完成联调的完整链路。内容以Python为主要示例语言,覆盖密钥与令牌的申请流程、接口端点与参数格式的确认、请求头身份验证、JSON响应解析以及状态码异常处理等关键环节,并对测试调试、集成进业务项目时容易遇到的凭证错误、参数设置与网络连接问题给出排查思路。包内共1个docx文档,约16KB,篇幅精炼,示例代码框架与步骤清单便于检索复用。目前已有1255人学习,适合初次接触DeepSeek接口、需要一份可对照操作的中初级开发者,也可作为团队内部接入AI服务时的入门参考材料。
1. DeepSeek API 的价值:把一个模型能力变成工程接口
上个月一个做工单系统的朋友接到需求,客户要在一个迭代里看到问题单的自动摘要。他手上没有 GPU 集群,也没有标注团队,最省事的做法不是训模型,而是调接口。DeepSeek API 就是这类把模型能力直接暴露成 HTTP 端点的服务,一次 POST 换回一段文本,真正花时间的从来不是那一行请求,而是鉴权、超时、重试、限流、上下文裁剪这些工程细节。这份调用指南把从申请访问权限、读接口文档、装依赖、写第一行请求代码,到调试排错、集成进业务系统的链路完整串了一遍。它适合想在一个月内把 AI 能力塞进现有系统的后端、数据和算法同学,也适合第一次做第三方接口联调、想弄清鉴权与错误处理套路的开发者。
2. 接入前准备:权限申请、密钥管理与文档读法
在写任何代码之前,先把接入链路的层次想清楚,比急着pip install有用得多。一个能上生产的调用链路通常长这样:业务代码只负责「我要一段摘要」,中间有一层接入层负责拼请求、带鉴权头、超时控制、重试与限流,再往下才是真正的 HTTP 传输和 JSON 解析。很多人第一次接第三方 AI 接口时,直接把requests.post写在控制器里,跑通 demo 很快,等到要换模型、要统计 token 成本、要被限流的时候,才发现每一处调用都得改一遍。常见做法是把接入层单独做成一个 client 模块,业务侧只依赖它暴露的chat()方法。
2.1 权限不是点一下就有:申请链路与预期管理
拿到访问凭证这一步,流程本身不复杂,但有几个容易低估的地方。一般路径是:注册开放平台账号 → 完成实名或企业认证 → 创建应用 → 申请 API Key → 配置额度或计费方式。申请表单里通常要填使用场景和预估调用量,这一栏别随手糊弄,写清楚「工单摘要,日均 3000 次,单次输入 1500 token 左右」这类具体数字,审核侧判断额度会快很多。
预期管理上有两点血泪经验。第一,先用最低额度跑通 PoC,别一上来就申请大额度,审核周期和商务流程会把你的开发节奏拖住。第二,别用个人账号去跑生产流量,密钥一旦绑在个人身份上,人一走、号一换,整条链路就得重新接。
注意:不同平台对 API Key 的可见性策略不一样,有的只展示一次,创建完立刻存进密码管理器,别指望页面还能再翻出来。
申请通过后你拿到的通常是一串以固定前缀开头的密钥,加上一个 base_url。这两个东西构成全部的身份信息,后面所有请求都围绕它们展开。
2.2 文档先读四块:鉴权、端点、参数、错误码
拿到权限后第一件事是读文档,但别从头读到尾——AI 服务类接口的文档动辄几十页,全读一遍收益很低。我一般只先抠四块内容。
第一块是鉴权方式。绝大多数平台走Authorization: Bearer <key>的请求头,少数要求签名或临时 token,这个搞错了后面全是 401。
第二块是端点路径和请求方法。对话类接口基本是POST /chat/completions这种形态,注意区分「兼容 OpenAI 协议」和「私有协议」两套文档,同一个平台可能同时提供,混着看会把参数名搞乱。
第三块是请求体参数。下面这张表是我每次接新平台都会先填一遍的,填完基本就知道这个接口能干什么、不能干什么。
| 参数 | 作用 | 我一般怎么设 |
|---|---|---|
model | 指定调用的模型版本 | 先用文档里的主力模型,别乱试冷门版本 |
messages | 对话消息数组,含role与content | 系统提示单独一条,别和用户输入拼一起 |
temperature | 采样随机性 | 摘要类 0.2~0.3,创意类才往 0.8 以上调 |
max_tokens | 限制输出长度 | 按业务上限卡死,防止费用失控 |
stream | 是否流式返回 | 面向用户的界面开,后台批处理关 |
stop | 停止词 | 结构化输出时用,能在指定位置截断 |
第四块是错误码和限流策略。文档里那张状态码表最容易被跳过,而它恰恰是联调阶段最值钱的东西——后面第 5 章会专门拆。
2.3 密钥落地的三层:环境变量、配置中心、密钥管理服务
密钥写死在代码里是新手最常见的翻车点,而且往往不是被恶意利用,是被自己人误传到代码仓库。按项目规模分三层处理比较务实。
单人开发或 PoC 阶段,环境变量足够:
# 写入当前 shell 会话,重启终端就失效,适合临时调试 export DEEPSEEK_API_KEY="sk-xxxxxxxxxxxxxxxx" export DEEPSEEK_BASE_URL="https://<平台文档给出的域名>/v1"这里DEEPSEEK_BASE_URL一定要以官方文档为准,别抄网上博客里的旧地址,接口版本升级后老域名可能只保留很短的兼容期。本地开发更好的做法是用.env文件加python-dotenv,同时把.env写进.gitignore:
# .gitignore,务必确认这几行在里面 .env .env.* !.env.example再配一个.env.example提交到仓库,只留字段名不留值,新同事 clone 下来照着填,比口头传密钥靠谱得多。
到了多人协作阶段,环境变量就该换成配置中心或密钥管理服务了。团队里常见做法是把密钥存在配置中心的一个加密命名空间里,应用启动时拉取,配合「开发 / 预发 / 生产」三套不同的 key,这样任何一套泄露都能单独吊销而不影响其他环境。密钥轮换周期我一般设 90 天,到期前一周在两个 key 并存的状态下切流量,切完再下线旧的,整个过程业务无感。
提示:轮换期间同时保留新旧两个密钥,接入层按「新 key 优先、失败回落旧 key」的顺序取,这是成本最低的平滑切换方式。
3. 工程骨架:把一次调用拆成配置、传输、解析四层
3.1 目录结构与依赖清单
一个能撑住二次开发的最小工程,目录别搞太花,但职责要分清。下面这个结构是我在几个项目里反复用过的一版,改一改就能套:
deepseek-client/ ├── config/ │ ├── __init__.py │ └── settings.py # 读环境变量、给默认值、做类型校验 ├── client/ │ ├── __init__.py │ ├── http_client.py # 传输层:超时、重试、连接池 │ ├── chat.py # 业务语义层:chat / chat_stream / chat_json │ └── errors.py # 异常分类,别让业务层看见 requests 的异常 ├── service/ │ └── summarize.py # 具体业务:工单摘要 ├── tests/ │ └── test_chat.py # 用 mock 断言的回归用例 ├── .env.example └── requirements.txt依赖不需要多,三个就够起步:
| 包 | 用途 | 备注 |
|---|---|---|
requests | 同步 HTTP 调用 | 简单场景够用,不必上异步框架 |
httpx | 异步与流式支持 | 需要高并发或 SSE 时再加 |
python-dotenv | 本地读取.env | 生产环境走真实环境变量,不装也行 |
config/settings.py干的事很单一,但必须做类型和存在性校验,让「少配了一个变量」在启动阶段就炸,而不是等到第一条请求返回 401 才发现:
import os class Settings: def __init__(self): # 缺少密钥时直接抛错,把问题拦在启动阶段 self.api_key = os.environ.get("DEEPSEEK_API_KEY") if not self.api_key: raise RuntimeError("DEEPSEEK_API_KEY 未配置") # base_url 去掉末尾斜杠,避免拼路径时出现双斜杠 self.base_url = os.environ.get("DEEPSEEK_BASE_URL", "").rstrip("/") if not self.base_url: raise RuntimeError("DEEPSEEK_BASE_URL 未配置") self.timeout = float(os.environ.get("API_TIMEOUT", "30")) self.max_retries = int(os.environ.get("API_MAX_RETRIES", "3"))timeout和max_retries也走环境变量,是为了让你在不同环境用不同策略:本地调试给 60 秒方便打断点,生产给 15 秒快速失败。
3.2 最小可用请求:直连跑通第一次调用
在封装之前,先用最原始的方式跑通一次,确认密钥、网络、端点三者都通。这一步别偷懒跳过,封装层会把很多细节藏起来,出问题时排查成本反而更高。
import os import requests url = f"{os.environ['DEEPSEEK_BASE_URL']}/chat/completions" headers = { "Authorization": f"Bearer {os.environ['DEEPSEEK_API_KEY']}", "Content-Type": "application/json", } payload = { "model": "deepseek-chat", # 以文档里的模型名为准 "messages": [ {"role": "system", "content": "你是一名简洁的技术支持助手。"}, {"role": "user", "content": "用户反馈登录后白屏,请给出三条排查方向。"}, ], "temperature": 0.3, # 摘要/排障类任务压低随机性 "max_tokens": 512, } resp = requests.post(url, json=payload, headers=headers, timeout=30) if resp.status_code == 200: data = resp.json() print(data["choices"][0]["message"]["content"]) print("用量:", data.get("usage")) else: # 错误响应体通常带 code 和 message,原样打出来最省事 print("状态码:", resp.status_code, "响应:", resp.text[:500])这段代码里几个参数值得单独说。json=payload会自动序列化并带上Content-Type,别手动json.dumps再传data=,那样容易漏掉编码细节。timeout=30是硬性要求,不加的话默认无限等待,一个卡住的连接能把整个线程池拖死。错误分支里打印resp.text而不是只打状态码,因为同是 400,有的是参数名写错,有的是模型名不存在,只有响应体说得清。
3.3 封装 client:超时、重试与异常分类
跑通之后立刻封装,理由很实际:一旦有第二个地方要调模型,复制粘贴的代码就会开始分叉。封装的核心是干三件事——统一异常、区分可重试与不可重试、把连接复用起来。
import time import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry class ApiError(Exception): """业务层只感知这个异常,不感知 requests 的细节""" def __init__(self, status, code, message, retryable): super().__init__(f"[{status}] {code}: {message}") self.status = status self.retryable = retryable # 只对连接层错误做重试,HTTP 状态码的重试在下面手动控制 _adapter = HTTPAdapter( pool_connections=10, pool_maxsize=20, max_retries=Retry(total=0), ) class ChatClient: def __init__(self, settings): self.s = settings self.sess = requests.Session() self.sess.mount("https://", _adapter) self.sess.headers.update({ "Authorization": f"Bearer {settings.api_key}", "Content-Type": "application/json", }) def chat(self, messages, model="deepseek-chat", temperature=0.3, max_tokens=1024): payload = { "model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } delay = 1.0 for attempt in range(self.s.max_retries + 1): try: r = self.sess.post( f"{self.s.base_url}/chat/completions", json=payload, timeout=self.s.timeout, ) except requests.Timeout: # 超时说明请求可能已被处理,重试有重复计费风险,需业务侧权衡 if attempt == self.s.max_retries: raise ApiError(0, "timeout", "请求超时", True) time.sleep(delay); delay *= 2; continue except requests.ConnectionError as e: if attempt == self.s.max_retries: raise ApiError(0, "network", str(e), True) time.sleep(delay); delay *= 2; continue if r.status_code == 200: return r.json() err = self._parse_error(r) # 429 和 5xx 可重试,4xx 其他一律不重试,避免打爆配额 if err.retryable and attempt < self.s.max_retries: time.sleep(delay); delay *= 2; continue raise err这里的几个决策值得展开。Retry(total=0)是故意关掉 urllib3 的自动重试,因为它的重试条件不区分幂等性,而带max_tokens的生成请求重发一次就是一次费用。重试改为手动控制,退避策略用 1 秒、2 秒、4 秒的指数退避,最多三次,这样遇到 429 限流时不会形成「越限越重试、越重试越限」的雪崩。
_parse_error里做状态码分类,401、403 这类属于配置错误,重试一百次也没用,直接抛给上层终止;429 和 500、502、503 属于临时性故障,才值得等一等。这套分类逻辑看起来啰嗦,但在生产环境里能把「凌晨三点被告警叫醒」的概率降下来一大截。
还有一点,超时重试要谨慎。请求超时的时候,服务端其实可能已经跑完了生成,只是响应没回来,重试等于白白付两次费。我的做法是摘要类任务允许重试,因为成本可控;长文生成类任务超时不重试,改为把任务状态标记为「待人工确认」。
3.4 流式响应的解析与 token 计量
面向用户的界面不开流式,体验会差得像死机。流式的底层是 SSE,响应体是一行行data: {...},以data: [DONE]结束,解析的时候必须按行缓冲,不能想当然地按 chunk 切分——网络层不保证 chunk 边界对齐。
def chat_stream(self, messages, model="deepseek-chat"): payload = {"model": model, "messages": messages, "stream": True} with self.sess.post( f"{self.s.base_url}/chat/completions", json=payload, timeout=self.s.timeout, stream=True, ) as r: r.raise_for_status() buf = "" for raw in r.iter_lines(decode_unicode=True): if not raw: continue line = raw.strip() if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break # 累积到完整 JSON 再解析,避免半行被截断导致 json 报错 buf += data try: chunk = json.loads(buf) buf = "" # 解析成功才清空缓冲 except json.JSONDecodeError: continue # 半行,等下一行拼上 delta = chunk["choices"][0].get("delta", {}) piece = delta.get("content") if piece: yield pieceiter_lines已经把换行处理掉了,但真正的坑在于:有些网关会把多个data:行合并成一次推送,也有的会在中间插入心跳注释行。所以判断条件写成「先看是不是data:前缀,再看是不是[DONE],剩下的才尝试 JSON」,不符合的一律跳过。buf的累积与清空逻辑是这段代码的核心——只有json.loads成功才清空,解析失败说明这行还没拼完整,继续攒。
token 计量不能等到月底看账单才发现超支。正常响应里的usage字段会给出输入、输出和总 token 数,把它和业务 ID 一起落库,按天聚合就能画出成本曲线。流式场景下,如果平台在最后一个 chunk 里带usage,记得单独取;不带的话,用粗略估算兜底——中文大致按字符数乘 0.6 估,英文按单词数估,误差够用,至少能提前发现异常调用。
4. 核心链路拆解:从 messages 拼装到响应落库
4.1 messages 结构:角色分工与上下文裁剪
messages是个数组,每个元素带role和content。system用来定规矩,比如输出格式、语气、禁止事项;user是当前输入;assistant是历史回复,多轮对话里要把它们按顺序还原。新手最容易犯的错是把历史对话和系统提示拼成一大段字符串塞进一条user消息,模型会分不清哪句是自己说的,多轮下来逻辑就开始飘。
def build_messages(system_prompt, history, user_input, max_turns=8): """history: [{"role": "user"/"assistant", "content": "..."}, ...]""" msgs = [{"role": "system", "content": system_prompt}] # 只保留最近 N 轮,防止上下文无限膨胀 trimmed = history[-max_turns * 2:] if history else [] msgs.extend(trimmed) msgs.append({"role": "user", "content": user_input}) return msgsmax_turns这个参数按业务定。工单摘要类任务其实只有一轮,根本不需要历史;客服对话类任务保留 5 到 8 轮足够,再多的话输入 token 成本会涨得很快,而且模型对中间位置的细节记忆本身就弱。更精细的做法是按 token 数裁而不是按轮数裁,先估算总长度,超了就从头删最老的对话,保留system和最近两轮不动。
system_prompt的写法有个小技巧:把输出格式的约束放在最后一句。比如「只输出 JSON,不要包含任何解释文字」放在提示词末尾,模型遵守的概率明显更高。这是我在几个项目里反复验证过的经验,不算严格结论,但成本为零,值得试。
4.2 响应解析与结构化输出
响应体的主结构是choices[0].message.content,另外三个字段必须读:finish_reason告诉你输出是正常结束还是被max_tokens截断,usage给成本核算,id方便出问题时找平台侧对日志。finish_reason如果是length,说明输出被截断了,这时候拿到的 JSON 大概率是残缺的,直接解析会抛异常。
def parse_chat_response(data): choice = data["choices"][0] content = choice["message"]["content"] reason = choice.get("finish_reason") if reason == "length": # 被 max_tokens 截断,结构化输出不可信,走降级分支 raise ApiError(200, "truncated", "输出被截断,请提高 max_tokens", False) usage = data.get("usage", {}) return { "content": content, "prompt_tokens": usage.get("prompt_tokens", 0), "completion_tokens": usage.get("completion_tokens", 0), "request_id": data.get("id"), }需要结构化输出时,别指望模型一定吐出干净 JSON。哪怕提示词写得再严,它也可能在前后加一句「好的,以下是结果」,或者用单引号。稳健的解析方式是先用正则把第一个{到最后一个}之间的内容抠出来,再json.loads;失败就重试一次,并在提示词里更强调格式;连续失败两次就降级成纯文本,让人工兜底,别让整个流程卡死在一个格式问题上。
把结果落库的时候,建议加一张调用日志表,字段至少包含请求 ID、业务主键、模型名、输入输出 token 数、耗时、状态。这张表后期是你做成本分析、效果对比、问题回溯的唯一依据:
CREATE TABLE api_call_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id VARCHAR(64), -- 平台返回的 id,便于和对方日志对齐 biz_key VARCHAR(64), -- 业务主键,如工单号 model VARCHAR(32), prompt_tokens INT, completion_tokens INT, latency_ms INT, status VARCHAR(16), -- ok / truncated / error created_at DATETIME DEFAULT CURRENT_TIMESTAMP );4.3 集成进业务:服务层、异步队列与幂等
把调用写进控制器是能跑的,但跑不久。正确的落点是服务层,业务逻辑只负责「拿数据、组 prompt、调 client、存结果」,不关心 HTTP 细节。
同步调用的场景适合「用户点了按钮、等两三秒能接受」的交互,比如摘要生成。异步场景适合批量处理,比如夜间跑一遍历史工单打标。异步化的时候注意两点:任务必须幂等,同一个工单重复入队不能产生两条结果;队列消费端要有并发上限,别一次放出几百个请求把配额打满。
一个实用做法是给业务记录加一个状态字段,取值pending / processing / done / failed,消费者执行前先做一次「抢占式更新」——UPDATE ... SET status='processing' WHERE id=? AND status='pending',受影响行数为 0 就说明别人已经在跑了,直接跳过。这一招不用引入分布式锁,够用且不容易出问题。
限流放在服务层入口做,用令牌桶按分钟放行,比在 client 里做更直观,也方便按业务优先级分配额度——摘要任务可以给低优先级,用户实时问答给高优先级,配额紧张的时候先保后者。
5. 避坑与排查:联调阶段最容易翻车的五个点
5.1 一个「密钥明明对」的 401
现象:密钥在本地能跑通,部署到容器里就稳定返回 401,日志里打印出来的密钥长度也是对的。
原因:环境变量末尾带了换行符,或者值里混进了引号。export KEY="sk-xxx"在某些 CI 配置里会把引号一起读进去;另外.env文件被 Windows 编辑器存成带 BOM 或 CRLF 的格式,读出来的值尾部多一个\r,密钥长度看着没错,但内容就是不对。
解决:在配置层做一次清洗——value.strip().strip('"').strip("'"),并且在启动日志里打印密钥的「前 4 位 + 长度」而不是完整值,方便核对又不泄露。这个习惯我保持了几年,救过至少三次。
5.2 请求一直挂着不返回
现象:本地偶尔卡住几十秒,服务端监控显示 CPU 没有异常,也没有报错。
原因:没设timeout,requests默认无限等待。长文本生成本来就慢,网络抖动时连接可能半死不活,线程被长期占用,几个并发就把连接池耗光了。
解决:一定要显式设timeout,并且区分连接超时和读取超时:timeout=(5, 60)表示连不上 5 秒放弃、等响应最多 60 秒。别用单个数字,那会把两个阶段卡成同一个值。超过 30 秒的生成任务,考虑改成流式,至少能让前端感受到进度。
5.3 重试把配额打爆
现象:高峰期日志里 429 一片,重试几次之后错误率不降反升,账单还涨了。
原因:重试没有退避,或者把 4xx 也纳入了重试范围。参数错误的请求重试一万次也是错,而且每次都在消耗连接和配额。另一种情况是上游已经把任务重排了三次,下游又重试三次,指数级放大。
解决:按状态码分类,只重 429、500、502、503;重试用指数退避加随机抖动(jitter),避免多个实例同一时刻一起重试;再加一层全局并发信号量,把同时发出的请求数压住。抖动那个随机量别嫌麻烦,多实例部署下没它就是一个整齐的尖峰。
5.4 流式输出到一半断了
现象:前端已经显示了一部分文字,突然停住,控制台没有明显错误。
原因:一是 SSE 解析把半行当完整 JSON 解了,异常被吞掉;二是网关或负载均衡有响应超时,长连接被掐断;三是代码里raise_for_status之后没有处理流式响应体的ConnectionError。
解决:解析端用前面说的缓冲累积法,解析失败不抛异常只继续攒;在生成循环外裹一层 try,捕获连接中断后把已生成内容落库并把状态置为partial,让业务能清晰地知道「这是一段没说完的话」;网关侧把空闲超时调到大于最长生成时间,或者每 15 秒发一个注释行保活。
5.5 输出格式时好时坏
现象:同样的提示词,十次里有两次返回的 JSON 解析不了,导致整个流程报错。
原因:模型对格式的遵守不是确定性的,temperature越高越明显;另外max_tokens设得太小,输出在 JSON 中间被截断,finish_reason是length,但解析代码没检查这个字段就直接json.loads了。
解决:三层防护——temperature降到 0.2 以下;解析前先查finish_reason,等于length直接走重试;解析用正则抠出 JSON 主体再loads,失败重试一次并加强提示词约束。三层都加上之后,这类报错基本归零。另有一个偏门但有效的做法:在提示词里给出一个完整的输出样例,比只描述字段名有效得多。
6. 二次开发:把调用层做成可切换、可观测的模块
真到了要改的时候,你会发现最早那版「一个文件搞定」的代码是最难改的。二次开发的核心不在功能叠加,而在于把变化点收拢。三个方向值得优先做。
第一,把模型供应商抽象掉。定义一个LLMProvider协议,只暴露chat()和chat_stream()两个方法,具体实现分DeepSeekProvider、LocalProvider等。业务层依赖协议而不是具体类,将来要换模型、要 A/B 对比、要在主服务不可用时切到备选,都只是换一行工厂配置。这个抽象别做太早也别做太重,两个方法足够,别一上来就设计十层继承。
第二,把可观测性做进接入层。每次调用记录:模型名、输入输出 token、耗时、状态、finish_reason。用一张日志表和几个聚合查询就够了,不必急着上链路追踪。真正有用的是下面这个查询——按天看成本趋势和失败率,异常的时候一眼能看出来:
SELECT DATE(created_at) AS d, SUM(prompt_tokens + completion_tokens) AS tokens, SUM(status != 'ok') / COUNT(*) AS fail_rate FROM api_call_log GROUP BY d ORDER BY d DESC LIMIT 14;第三,把提示词版本化。提示词本质是配置,不是代码里的字符串常量。存到配置文件或数据库里,带上版本号和变更记录,每次调用日志里记下用的是哪版。这样当输出质量下降时,你能立刻判断是模型变了还是提示词改了——这个后悔药,早备早安心。
验证改造是否成功,别靠肉眼观察输出质量,那太主观。准备一套 30 到 50 条的固定测试集,输入固定、断言输出里必须包含的关键信息(比如摘要里必须出现工单号、错误码),每次改动跑一遍,通过率低于基线就不合入。这套回归用例一开始写起来有点枯燥,但它能挡住绝大多数「改了 A 结果 B 坏了」的意外。
还有个具体技巧值得单独说:把temperature和max_tokens做成可按调用传入的参数,但在 client 里设一个默认值兜底,并记录「本次调用是否显式传参」。团队里总有人会顺手传个temperature=1.0做实验,结果忘了删。有了这层记录,出问题时能快速定位到是谁的那次改动把输出搞飘了。
我自己踩过最深的一次坑,是在没做截断检查的情况下把结构化输出直接喂给了下游解析器。模型某次因为max_tokens不够,返回了半截 JSON,下游抛了个看不出所以然的解析异常,排查了两个小时才定位到finish_reason。从那以后我每次写解析函数,第一行必定先断言finish_reason,第二步才碰内容,宁可多写三行判断,也不愿再翻一次那个日志。希望这些经验能帮你少走几步弯路,早点把接口接稳、接顺。
本文还有配套的精品资源,点击获取