这是系列的第 12 篇。整个系列写给零基础、想入行 AI 的朋友,每天一篇,30 天后你会做出 3 个能写进简历的项目。
这篇解决什么问题
昨天加工具那个作业,交上来的代码我已经看了,功能基本都对。但有个共性问题:代码太脆了。
举几个真实场景。用户问一句话,转圈七八秒什么都不显示,以为程序卡死了;网络抖一下程序直接崩,用户一脸懵;跑了几天想看花了多少钱,完全不知道。
功能能用只是及格,能稳稳地交付出去才是合格。今天讲三件事:流式输出解决"等太久"、重试机制解决"太脆弱"、成本控制解决"不知道在烧多少钱"。这三件事做完,你的程序才像个正经产品。明天开工的项目一,会全程用上。
一、流式输出:让字一个个蹦出来
你现在用的写法,是等 AI 把整段话想完了再一次性给你。回答长了,用户要干等好几秒。
看看网页版大模型的体验:字是一个个往外蹦的。这不是特效,是流式输出(stream),AI 生成一个字就传一个字。
原理很好理解。像点外卖:非流式是把整桌菜做好了端上来,流式是开放式厨房,做一道上一道。后者等待感明显更小。
代码只改一个地方,加 stream=True:
python
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "讲个笑话"}],
stream=True # 关键:开启流式
)
for chunk in response: # 一段一段地接收
piece = chunk.choices[0].delta.content
if piece: # 有些片段是空的,跳过
print(piece, end="", flush=True) # end="" 不换行,flush 立刻输出
注意几个变化:返回值从"一个完整的响应"变成了"一段可以遍历的流";取内容从 message.content 变成了 delta.content。
有个坑要提前说:开了流式之后,你拿不到完整的结果文本了,因为它是碎片式给你的。所以要把碎片拼起来:
python
full_text = ""
for chunk in response:
piece = chunk.choices[0].delta.content
if piece:
full_text += piece # 一边显示一边拼接
print(piece, end="", flush=True)
# 循环结束后,full_text 才是完整回答,后面要用就用它
二、报错重试:别让程序死在网络抖动上
调 API 是网络请求,网络请求就一定会失败。三类最常见的错误:
- 网络抖动、超时(一时的问题,重试就好)
- 限流(请求太频繁,等一会儿重试)
- 余额不足、密钥错误(重试一万次也没用)
第三类要直接告诉用户,前两类应该自动重试。这是个通用套路,值得记住:
python
import time
def call_with_retry(messages, max_try=3):
for i in range(max_try):
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages
)
return response.choices[0].message.content # 成功就返回
except Exception as e:
print(f"第{i + 1}次失败了:{e}")
if i < max_try - 1:
time.sleep(2) # 等两秒再试,给服务器缓一缓
else:
return "抱歉,服务暂时不可用,请稍后再试" # 三次都不行,兜底
关键在最后那句兜底:程序不能因为一次网络问题就崩给用户看。错误要能被接住,并且翻译成人话。
三、成本控制:知道你花了多少钱
Day 6 说过,API 按 token 计费。但很多人跑了一个月都不知道自己花了多少,直到收到账单。
好消息是,每次 API 返回里都带着账单信息,就藏在响应对象里:
python
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "你好"}]
)
usage = response.usage
print("输入 token:", usage.prompt_tokens) # 你发过去的内容
print("输出 token:", usage.completion_tokens) # AI 回给你的内容
print("总计:", usage.total_tokens)
三件事顺手就能做:
第一,记日志。把每次调用的 token 数写进文件,月底一看就知道花在哪了。
第二,砍浪费。Day 10 讲的滑动窗口就是最简单的节流手段,别把几百轮对话全都发过去。
第三,主次分明。便宜的模型干粗活(分类、提取关键词),贵的模型干细活(写作、复杂推理)。这是行业里的普遍做法,成本能差出好几倍。
四、把三件事装在一起
实际项目里,这三件事是叠着用的。给你一个可以复用的骨架:
python
import time
def chat_safe(messages, max_try=3):
"""带重试和成本记录的对话函数"""
for i in range(max_try):
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True # 流式输出
)
full_text = ""
for chunk in response:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)
full_text += piece
print() # 结束后换行
return full_text
except Exception as e:
print(f"\n第{i + 1}次失败:{e}")
if i < max_try - 1:
time.sleep(2)
else:
return "服务暂时不可用,请稍后再试"
以后写任何 AI 程序,直接从这段改,能省下很多重复工作。
常见报错排查
报错一:开了 stream 之后取 content 报错或拿到 None。
流式要取 delta.content 而不是 message.content,并且要判断空值(有些片段没有内容)。
报错二:重试了三次还是失败。
先看报错原文。如果是余额或密钥问题,重试是无效的,要直接去控制台处理。
报错三:流式输出在 VSCode 里看着正常,打包成程序就不显示。
输出缓冲的问题,确认用了 flush=True。有些环境需要手动刷新缓冲区。
报错四:response.usage 是 None。
开了 stream 之后,部分平台的 usage 需要额外设置才会返回。想统计成本又用流式,可以自己用字符数粗略估算,或者不开流式单独计数。
今天的作业
把 chat_safe 这段拿去跑,打印一句话,观察两件事:字是不是一个个出来的;每次调用用了多少 token。把 token 数字贴到评论区,看看大家问同一句话,谁的花费高。
明天预告
Day 13:《项目一开工:AI 文件整理助手,几百个乱文件一键归类》。铺垫了十二天,明天正式开始做简历上的第一个项目。上午搭骨架,下午就能看到它真的帮你整理文件。我会把完整代码拆成几段讲,你跟着敲就行。
*系列目录:30天从零开始学AI应用 开发