还记得上篇结尾那个"简易智能客服"吗?我们用字典模拟了 LLM 的消息列表,build_messages 函数拼出 system 和 user 消息,假装有个模型在回答你。
说句实话——那是个"假模型"。它不会思考,只会查字典。今天带你真正调通一个大模型 API。
这篇学完,你能独立完成五件事:
这些不是零散的知识点,它们就是 AI Agent 开发最核心的"骨架"——你后面学 LangGraph、学 Agent 编排,底层全是今天这些东西。
📌 阅读建议:准备好电脑,跟着每节示例动手敲。上篇的语法你已经会了,这篇全是"怎么用",光看是学不会的,必须敲。
01 虚拟环境与依赖管理:给你的"厨房"装上独立灶台
上篇我们把 Python 环境比作厨房,锅灶一应俱全。但有个问题你可能还没遇到:如果你在系统里直接 pip install 各种包,就像在唯一的灶台上乱炖——今天装 A 项目要的 requests 1.0,明天装 B 项目要的 requests 2.0,两个版本打架,A 项目就崩了。
这就是全局污染和版本冲突。解决办法叫虚拟环境(venv):每个项目一个独立的小厨房,互不干扰。Python 自带这个功能,不用装任何东西。
# 创建虚拟环境:第一个 venv 是目录名,叫什么都行
python -m venv venv
创建好之后,目录里会多出一个 venv 文件夹,里面装着一份"独立的小 Python"。重点来了:你必须先激活它,pip 装的包才会进这个环境。
# Mac / Linux 激活
source venv/bin/activate
# Windows PowerShell 激活
venv\Scripts\activate
激活成功后,命令行前面会出现 (venv) 前缀,比如 (venv) mac@...$。看到它,就说明你现在用的是独立环境。想退出,输入 deactivate 即可。
激活之后,安装我们这篇文章要用的两个包:
pip install requests openai
requests 是发 HTTP 请求用的,openai 是官方 SDK——别被名字骗了,它不只是给 OpenAI 用的,几乎所有国产大模型都兼容它,后面你会看到。
装完后,把依赖清单存下来,方便换电脑、同事协作时一键还原:
pip freeze > requirements.txt
requirements.txt 里每一行是一个包加版本号,比如 requests==2.32.3。别人拿到这个文件,一条命令就能装齐:
pip install -r requirements.txt
再补充一个实用技巧:每次开始写新项目,第一步永远是建虚拟环境。养成这个肌肉记忆,能帮你避开 90% 的依赖地狱。如果项目已经建好了但忘了建环境,也可以随时补——venv 目录删掉重建就行,不影响任何代码。
另外,pip 本身也值得知道两个常用选项:pip list 查看当前环境装了哪些包,pip uninstall 包名 卸载不需要的包。排查问题时先 pip list 看看版本,往往能一眼发现"版本不对"。
✅ 运行前检查清单(01 节)
- [ ] 命令行出现
(venv) 前缀(虚拟环境已激活) - [ ]
pip install requests openai 安装成功,无红色报错 - [ ]
pip list 里能看到 openai 和 requests
⚠️ 新手常见坑:
最常见的情况是——明明 pip install openai 成功了,运行代码却报 ModuleNotFoundError: No module named 'openai'。十有八九是用错了解释器:VS Code 右下角选的是系统 Python,而包装进了 venv。检查方法:在终端 which python(Mac)或 where python(Windows),看路径里有没有 venv。VS Code 里按 Ctrl+Shift+P(Mac 是 Cmd+Shift+P),输入 "Python: Select Interpreter",选带 ('venv': venv) 的那个。记住一句话:装包的环境和跑代码的环境必须是同一个。另外,Mac 用户如果提示 pip 找不到,试试 python3 -m pip install xxx,这是最不容易出错的调用方式。
02 先学会调用 HTTP API:大模型其实是个"外卖电话"
在碰 SDK 之前,我想先带你扒开一层皮,看看 LLM API 到底长什么样。说穿了特别简单:大模型 API 就是一个网址(URL),你往它那儿发一个请求,它给你返回一段文字。
就像打电话叫外卖:你报需求(请求),商家做饭(服务器上的模型处理),骑手送餐(响应)。这个"打电话"的协议叫 HTTP。Python 里最常用的"电话机"就是 requests 库。先来个最简单的 GET 请求,试试水:
import requests
# GET 请求:从服务器"取"数据
resp = requests.get("https://httpbin.org/get")
print("状态码:", resp.status_code) # 200 表示成功
print("响应内容:", resp.text[:200]) # 打印前 200 个字符
status_code 是 HTTP 状态码,200 就是"一切正常"。resp.text 是原始响应文本,通常是一大段 JSON。
但调用大模型用的是 POST 请求——POST 和 GET 的区别,可以粗暴理解为:GET 是"你给我看看",POST 是"我交给你处理一下"。你要把消息发给模型让它生成回答,自然是 POST。请求长这样:
import requests
import os
# 从环境变量读取密钥(上篇学过 os.getenv,这里直接复用)
api_key = os.getenv("DEEPSEEK_API_KEY")
resp = requests.post(
"https://api.deepseek.com/chat/completions", # 接口地址
headers={
"Authorization": f"Bearer {api_key}", # 身份凭证:我是谁
"Content-Type": "application/json", # 我发送的是 JSON 格式
},
json={ # 请求体:我要干什么
"model": "deepseek-v4-flash",
"messages": [
{"role": "user", "content": "你好,用一句话介绍你自己"}
],
},
timeout=30, # 30 秒没响应就放弃,防止程序卡死
)
print("状态码:", resp.status_code)
data = resp.json() # 把 JSON 字符串解析成 Python 字典
print(data["choices"][0]["message"]["content"]) # 取出模型回复
✅ 运行前检查清单(02 节)
- [ ] 已设置环境变量:
export DEEPSEEK_API_KEY="sk-你的密钥"(或用 .env 方案) - [ ] 代码里
os.getenv("DEEPSEEK_API_KEY") 不是 None(可先 print 验证) - [ ] 模型名写的是
deepseek-v4-flash(旧名 deepseek-chat 已弃用)
这段代码就是 LLM 调用的"原始形态",几个关键点:
- Authorization 头:
Bearer 后面跟你的 API Key,相当于你的"工牌",服务器靠它认出你是谁、有没有钱。 - json 参数:requests 会自动帮你把字典转成 JSON 字符串,并设置好 Content-Type。
- resp.json():把服务器返回的 JSON 文本解析成 dict,然后一层层取数据——
choices[0].message.content 就是模型说的话。这套结构就是上篇我们模拟过的那个 JSON,现在见到真身了。
状态码是排查问题的第一线索,记住这三个就够用了:
再补一个生产级细节:超时设置。 上面代码里的 timeout=30 不是摆设。如果没有超时,网络卡住时程序会一直挂在那里,像等一个永远不来的外卖。生产环境里,建议把超时拆成两段:timeout=(连接超时, 读取超时),比如 timeout=(5, 60)——5 秒连不上就放弃,连上后 60 秒内没返回也放弃。这样既不怕网络慢,也不怕服务器卡死。
关于 API Key 的安全,这是底线问题:绝不允许把密钥直接写死在代码里。一旦代码上传到 GitHub,等于把钱包密码公开了,别人能用你的额度疯狂调用,账单直接爆掉。正确做法是放环境变量:
# 方式一:终端里临时设置(每次开新终端都要重新设)
export DEEPSEEK_API_KEY="sk-你的密钥"
# 方式二(推荐):写进 .env 文件,用 python-dotenv 自动加载
pip install python-dotenv
.env 文件内容就一行:
DEEPSEEK_API_KEY=sk-你的密钥
代码里这样加载:
from dotenv import load_dotenv
import os
load_dotenv() # 读取项目根目录的 .env 文件
api_key = os.getenv("DEEPSEEK_API_KEY")
千万别忘了把 .env 加进 .gitignore,否则一提交就把密钥交出去了。
⚠️ 新手常见坑:
① resp.json() 报错 JSONDecodeError,通常是状态码不是 200,返回的是错误文本,先打印 resp.status_code 和 resp.text 看真实内容;
② 密钥报 401,先确认环境变量真的读到了——在代码里 print(os.getenv("DEEPSEEK_API_KEY")) 看看是不是 None;
③ 请求体里 messages 必须是列表,少写一层中括号会直接 400 报错。
03 LLM API 核心概念:看懂"菜单"再点菜
学会了"打电话",现在要弄明白"怎么点菜"。LLM API 有五个核心概念,搞懂它们,任何一家厂商的接口你都能秒懂。
第一个概念:token(令牌)。 模型不看"字",它看的是 token——一种把文本切碎后的基本单位。粗略换算:英文大约 4 个字符算 1 个 token,中文 1 个字约等于 1~1.5 个 token。比如"你好世界"大概 4 个 token。
为什么重要?因为 token 就是钱:API 按 token 计费,你发出去的消息和模型吐出来的回复,都要按 token 数付钱。后面你会看到,每次调用的响应里都带着 usage 字段,告诉你这次花了多少 token。
怎么估算自己的一段话有多少 token? 最简单的办法:先看英文,一段 100 个英文单词的文本约等于 130~150 个 token;中文的话,100 个字大约 100~150 个 token。不用算得很准,心里有个数量级就行——你只需要知道"提示词越长越贵"这个方向感。很多厂商的控制台还提供 token 计算器,把文本贴进去就能看到精确数字,调试成本时很好用。
第二个概念:消息结构。 Chat 接口的请求体里,messages 是一个列表,里面每条消息有三个角色:
- system:给模型立"人设"的,比如"你是一个耐心的编程老师"。它不直接给用户看,是幕后指令。
- user:用户说的话。多轮对话中,每轮用户输入都追加一条。
- assistant:模型自己的回答。多轮对话中,模型每次回复也要追加回去,模型才能"记得"自己说过什么。
一个多轮消息列表长这样:
messages = [
{"role": "system", "content": "你是一个简洁的旅行助手"},
{"role": "user", "content": "我想去成都玩三天"},
{"role": "assistant", "content": "好的!建议安排:第一天宽窄巷子+锦里,第二天熊猫基地,第三天都江堰。需要我细化行程吗?"},
{"role": "user", "content": "帮我细化第二天的行程"},
]
注意看:user 和 assistant 消息交替出现,这就是模型的"记忆"。模型本身没有记忆,你发多少条消息它就"看"多少条。上篇我们用字典模拟这个结构,现在你知道了,那是一比一还原的真实协议。
第三个概念:关键参数。 点菜时你可以提要求,这些参数就是要求:
| | |
|---|
model | | |
messages | | |
temperature | | |
max_tokens | | |
top_p | | |
stream | | |
第四个概念:OpenAI 兼容协议。 这是 2026 年最值得庆幸的一件事:几乎所有主流厂商都兼容 OpenAI 的接口格式——DeepSeek、通义千问、智谱、Kimi、OpenRouter 都是。这意味着你只需要学会一套 SDK,换模型时只改两行:base_url(接口地址)和 api_key(密钥),模型名再改一下,完事。这就是本篇只用 openai 一个 SDK 打天下的原因。
第五个概念:2026 年主流 API 现状。 帮你快速建立市场认知(数据截至 2026 年 8 月,价格变动快,以官网为准):
| | | | |
|---|
| | | | |
| GPT-5.6 系列(Sol/Terra/Luna) | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
💡 补充:DeepSeek V4 Flash 的缓存命中输入价仅 $0.0028/M,约为正常输入的 1/50。这意味着"重复发相同前缀"几乎不花钱——具体怎么利用,看下面的深度洞察二。
新手入门,我强烈建议首选 DeepSeek:便宜(一次对话几分钱)、兼容 OpenAI 协议、中文效果好、注册就送额度。本篇所有示例都用 DeepSeek 的接口,base_url 是 https://api.deepseek.com。想体验别的模型?把 base_url 和 api_key 换成对应厂商的即可,代码一行都不用改。
顺便说下各家怎么拿密钥,免得你卡在第一步:DeepSeek 去 platform.deepseek.com 注册,控制台里"API Keys"页面创建;阿里云百炼去百炼控制台开通模型服务,北京地域有免费额度,适合学生党白嫖练手;OpenAI 去 platform.openai.com,但要绑卡。拿到密钥后统一放进 .env 文件,我们的代码只认环境变量,不认"复制粘贴进代码"。
深度洞察一:为什么 OpenAI 兼容协议成了"事实标准"?
这背后不是技术碾压,而是网络效应。OpenAI 是第一个把"对话式大模型"做成标准接口的厂商:/chat/completions + messages 结构 + 参数命名,这套设计足够简单、足够通用,于是 SDK 生态(openai-python 等)最先成熟。
后来者(DeepSeek、通义、智谱……)要做的不是另起炉灶,而是"兼容它"——因为兼容 = 免费获得整个开发者生态:开发者不用换 SDK 就能用你的模型,迁移成本趋近于零。这就是**事实标准(de facto standard)**的经典案例:不是官方强制,而是市场用脚投票。
对你这个学习者来说,这是天大的好事——学会一套 API,等于学会了所有厂商的 API。换模型只改三行:base_url、api_key、model。
深度洞察二:token 计费背后的经济学
为什么输出价比输入价贵一倍(0.28 vs 0.14)?因为生成是自回归的:模型要一个 token 一个 token 地"想"出来,每生成一个都要重新计算一遍,这是最贵的环节;而输入是并行"读"进去的,便宜得多。
那为什么缓存命中的输入只要 $0.0028/M(比正常输入便宜 50 倍)?因为服务端有 KV cache:如果这次请求的前缀和上次完全一样,服务端直接复用缓存,省掉了重新"读"的成本。这解释了一个省钱技巧:system 人设和固定前缀尽量保持一致,命中缓存,输入成本直接降两个数量级。
深度洞察三:"上下文 = 记忆"的边界问题
很多人以为"上下文窗口越大 = 模型记忆越好",这是误解。上下文窗口不是记忆,是注意力预算:模型没有真正的记忆,你塞多少条消息它就"看"多少条,看完就忘。窗口再大(DeepSeek V4 Flash 有 100 万 token),也有两个硬伤:
- 中间遗忘:研究(Lost in the Middle)表明,模型对长上下文的"中间部分"关注度最低,重要信息放开头和结尾最容易被记住。
- 成本线性增长:每轮对话都要把全部历史重发一遍,聊得越久,输入 token 越多,账单越贵——窗口是 100 万 token,但你的钱包不是。
所以真实产品从不"无脑塞上下文",而是做记忆管理:截断、摘要、RAG 检索。第 05 节你会亲手实现前两种。
⚠️ 新手常见坑
① 把 temperature 和 top_p 同时调到很高,输出会变成"胡言乱语",这两个参数控制的是同一件事(随机性),调一个就行;
② 以为换模型要重学一套 API——不用,认准"OpenAI 兼容"四个字,换 base_url 就行;
③ 看到网上教程写 deepseek-chat、deepseek-reasoner 这两个旧模型名——它们已在 2026-07-24 弃用并映射到 V4 Flash,直接用 deepseek-v4-flash 即可。
04 你的第一个 LLM 程序:从"模拟"到"真实"只差 30 行
理论讲完了,动手。上篇我们模拟的 LLMClient,今天换成真实的 API 调用。先确认你已经装好 SDK:
pip install openai
然后新建一个 first_llm.py,把下面代码完整敲进去:
"""我的第一个 LLM 程序:调用 DeepSeek"""
import os
from openai import OpenAI
# 1. 创建客户端:告诉 SDK "去哪家店、用谁的钥匙"
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"), # 密钥从环境变量读,绝不硬编码
base_url="https://api.deepseek.com", # 改成其他厂商的地址即可换模型
)
# 2. 组装消息:system 立人设,user 提问题
messages = [
{"role": "system", "content": "你是一个耐心的中文编程老师,回答要简洁。"},
{"role": "user", "content": "用一句话解释什么是 API?"},
]
# 3. 发起对话:核心就这一行
response = client.chat.completions.create(
model="deepseek-v4-flash", # 模型名
messages=messages, # 对话内容
temperature=0.7, # 适度创意
max_tokens=500, # 最多生成 500 个 token
)
# 4. 取出模型回复
reply = response.choices[0].message.content
print("模型回复:", reply)
# 5. 查看本次消耗(token 就是钱,养成看账单的习惯)
print(f"本次消耗 token:{response.usage.total_tokens}")
✅ 运行前检查清单(04 节)
- [ ] 已安装 openai:
pip install openai - [ ] 已设置密钥:终端
export DEEPSEEK_API_KEY="sk-你的密钥",或用 .env 方案(02 节讲过) - [ ] 代码里
os.getenv("DEEPSEEK_API_KEY") 不是 None - [ ] 模型名写的是
deepseek-v4-flash(旧名 deepseek-chat 已弃用)
逐行拆解,你就明白 SDK 帮我们做了什么:
OpenAI(...):创建客户端。SDK 内部会记住 base_url 和 api_key,后面所有请求都自动带上。想切换厂商,改这两行就行。client.chat.completions.create(...):发 POST 请求到 /chat/completions 接口。还记得 02 节我们用 requests 手写的请求体吗?SDK 帮你把 headers、JSON 序列化、状态码检查全包了。response.choices[0].message.content:从返回对象里取模型回复。choices 是列表,通常只有一个元素;.message.content 就是文本。response.usage.total_tokens:这次调用总共消耗的 token 数。这是你控制成本的第一手数据。
💡 小提示:如果你的密钥放在 .env 文件里,记得在代码开头加上 load_dotenv()(02 节讲过),否则 os.getenv 会读到 None。
运行:
python first_llm.py
看到模型回复的那一刻,恭喜你——从"模拟"到"真实",你正式迈过了 AI 开发的第一道门槛。
接下来升级:让模型输出 JSON。 真实开发中,你经常需要程序能直接解析的结果,而不是一段自然语言。方法很简单:在 system 提示词里明确格式要求,然后用 json.loads 解析:
import os
import json
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{"role": "system", "content": "你是一个信息提取器。只输出 JSON,格式:{\"name\": 姓名, \"age\": 年龄, \"city\": 城市}"},
{"role": "user", "content": "张三今年 28 岁,住在杭州"},
],
max_tokens=200,
)
raw = response.choices[0].message.content
print("模型原始输出:", raw)
data = json.loads(raw) # 字符串 -> 字典
print("姓名:", data["name"])
print("年龄:", data["age"])
加一层错误处理,让程序在 API 出问题时优雅地提示,而不是崩溃。上篇学的 try/except 正好派上用场:
import os
from openai import OpenAI, APIError, AuthenticationError, RateLimitError
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
try:
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": "你好"}],
)
print(response.choices[0].message.content)
except AuthenticationError:
print("❌ 密钥无效,请检查 DEEPSEEK_API_KEY 环境变量")
except RateLimitError:
print("❌ 请求太频繁被限流了,歇几秒再试")
except APIError as e:
print(f"❌ API 错误:{e}")
顺便算一笔账(衔接上篇的成本计算):DeepSeek V4 Flash 输入 0.28/百万 token。假设一次对话用了 1000 个输入 token + 500 个输出 token,成本是:
cost = 1000 / 1_000_000 * 0.14 + 500 / 1_000_000 * 0.28
print(f"这一次对话大约花费 ${cost:.6f}") # 0.00028 美元,约 2 厘人民币
没错,一次对话不到一分钱。这也是为什么建议新手用 DeepSeek 练手——可以放心大胆地写代码、跑实验,账单不会吓到你。
⚠️ 新手常见坑:
① response.choices[0].message.content 取出来是 None,多半是开启了流式但按非流式方式取值,或者模型返回了 tool_calls(第 07 节会讲);
② json.loads 报错,先 print(raw) 看模型到底输出了什么——有时模型会在 JSON 前后加解释文字,解决办法是 system 提示词里写死"只输出 JSON,不要任何其他文字";
③ 报 ModuleNotFoundError: openai,回到 01 节检查虚拟环境是否激活。
05 多轮对话与上下文管理:让模型"记住"你说过的话
上篇的简易客服有个致命缺陷:它没有记忆。你问"订单到哪了",它回答后,你再问"那退款呢",它不知道你在说同一个订单。
真实的多轮对话,靠的就是把每轮消息都追加进 messages 列表——这就是模型的"记忆"。还记得 03 节深度洞察三吗?"上下文 = 注意力预算",现在你来亲手管理这份预算:
"""多轮对话:messages 就是模型的记忆"""
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 记忆的起点:system 人设
messages = [
{"role": "system", "content": "你是一个贴心的旅行规划助手,回答要简洁实用。"},
]
print("旅行助手已上线(输入 quit 退出)")
whileTrue:
user_input = input("你:")
if user_input == "quit":
break
# 1. 把用户的话写进"记忆"
messages.append({"role": "user", "content": user_input})
# 2. 带着全部记忆去问模型
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
max_tokens=400,
)
reply = response.choices[0].message.content
# 3. 把模型的回答也写进"记忆"
messages.append({"role": "assistant", "content": reply})
print(f"AI:{reply}")
print(f"(当前记忆长度:{len(messages)} 条消息)")
跑起来试试:先问"我想去成都玩",再问"第二天吃什么",你会发现模型记得你们聊过成都。每一轮,messages 都在变长,模型"看"到的上下文越来越完整——这就是多轮对话的全部秘密。
但这里藏着一个大问题:上下文膨胀。每轮对话都要把全部历史发给模型,聊得越久,token 消耗越大,而且模型有上下文长度上限(虽然 DeepSeek V4 Flash 有 100 万 token 的窗口,但你的钱包没有)。聊到一定程度,要么报错,要么账单起飞。
最简单的截断策略:只保留最近 N 条消息。 像人一样,太久远的细节记不住就算了,记住最近聊的就行:
deftrim_messages(messages, max_len=10):
"""只保留 system 人设 + 最近 max_len 条消息"""
system_msg = messages[0] # 人设永远保留
recent = messages[1:] # 去掉 system,只看对话部分
if len(recent) > max_len:
recent = recent[-max_len:] # 只留最近 N 条
return [system_msg] + recent
注意:截断前先把 system 单独拎出来,否则人设会被一起丢掉,模型"人设崩塌"。
更高级的做法是"摘要压缩":聊久了,让模型把前面的对话总结成几句话,塞回 system 里,再接新对话。这个思路就是后面 RAG 和 Agent 记忆系统的雏形,先记住"有这回事"即可。
进阶需求:对话持久化。 程序关掉,记忆就没了。把 messages 存成 JSON 文件,下次启动再读回来:
import json
defsave_messages(messages, filename="history.json"):
"""把对话历史存成 JSON 文件"""
with open(filename, "w", encoding="utf-8") as f:
json.dump(messages, f, ensure_ascii=False, indent=2)
defload_messages(filename="history.json"):
"""从 JSON 文件恢复对话历史"""
try:
with open(filename, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return [{"role": "system", "content": "你是一个贴心的旅行规划助手。"}]
再聊聊"摘要压缩"这个更聪明的方案。 截断是"丢记忆",摘要压缩是"压缩记忆":当消息超过阈值时,让模型把前面的对话浓缩成几句话,然后把摘要塞进 system 消息,历史消息清空。这样既保留了关键信息,又控制了 token 消耗。
真实产品里,很多 Agent 的记忆系统就是这个思路的工程化版本——先记住概念,以后你写自己的记忆模块时,会回来感谢今天的自己。
⚠️ 新手常见坑:
① 忘记把 assistant 的回复 append 进 messages,模型就会"失忆",回答驴唇不对马嘴——记住:user 和 assistant 的消息都要追加;
② 截断时把 system 消息也截掉了,模型人设崩塌——截断前记得把第一条 system 单独保留;
③ 存 JSON 文件时忘记 ensure_ascii=False,存进去全是 \u4f60\u597d 这种转义字符,人没法看。
06 流式输出:让 AI 像真人一样"打字"
你有没有注意过,ChatGPT 的回答是一个字一个字蹦出来的?这就是流式输出(streaming)。
原理很简单:模型生成完整个回答再一次性返回,叫非流式;边生成边返回,一个字一个字传给你,叫流式。为什么要流式?两个理由:
- 用户体验:大模型生成一段 200 字的回答可能要几秒到十几秒。非流式模式下,用户盯着空白屏幕干等;流式模式下,文字实时蹦出来,用户感觉"AI 在打字",等待焦虑瞬间消失。
- 延迟感知:流式模式下第一个字往往 1 秒内就出来了,用户感知到的延迟大幅降低。
代码差异极小——加一个 stream=True,然后遍历响应:
"""流式输出:打字机效果"""
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 关键:stream=True
stream = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{"role": "system", "content": "你是一个诗人。"},
{"role": "user", "content": "写一首关于夏天的五言绝句"},
],
stream=True, # 开启流式
)
# 遍历每一个"碎片",逐字打印
print("AI:", end="")
for chunk in stream:
delta = chunk.choices[0].delta.content # 这一小片文字
if delta: # 有的碎片是空的,要跳过
print(delta, end="", flush=True) # flush=True 强制立即输出,不打进缓冲区
print()
对比一下非流式和流式的差异,就两处:
| | |
|---|
| | stream=True |
| response.choices[0].message.content | 遍历 for chunk in response,取 chunk.choices[0].delta.content |
| | |
| | |
新手最容易踩的坑:流式响应里,chunk.choices[0].delta.content 经常是 None——尤其是第一个碎片(可能只包含 role 信息)和最后一个碎片(结束标记)。如果你直接 print(delta),会打出 None;如果 delta + "x" 会直接 TypeError 崩溃。所以必须判空:if delta: 再处理。
另外注意:流式模式下默认不返回 usage 统计。如果一定要统计成本,在请求里加 stream_options={"include_usage": True},最后一个碎片就会带上 chunk.usage 字段(第 08 节的助手就是这么做的)。
⚠️ 新手常见坑:
① 忘了 flush=True,文字会攒在缓冲区里一次性蹦出来,打字机效果变"憋大招";
② 把 delta.content 当成 message.content 取——流式里没有 message,只有 delta;
③ 流式循环里做耗时的打印格式化,会拖慢"打字"节奏,保持循环体轻量。
07 结构化输出与 Function Calling:让模型听程序的话
前面几节,模型一直在"说人话"。但真实的 Agent 系统里,程序需要的是机器能直接解析的结果——比如一个 JSON,而不是一段可能带废话的自然语言。这就是结构化输出。
第一种方式:JSON Mode。 在请求里加 response_format={"type": "json_object"},强制模型输出合法 JSON:
"""情感分析:让模型输出结构化 JSON"""
import os
import json
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{"role": "system", "content": "你是情感分析器。只输出 JSON,格式:{\"sentiment\": 情感, \"score\": 分数(0-1), \"reason\": 理由}"},
{"role": "user", "content": "分析这句话的情感:加班到半夜终于上线了,成就感爆棚!"},
],
response_format={"type": "json_object"}, # 强制 JSON 输出
max_tokens=200,
)
result = json.loads(response.choices[0].message.content)
print("情感:", result["sentiment"])
print("分数:", result["score"])
print("理由:", result["reason"])
跑一下,你会拿到类似 {"sentiment": "正面", "score": 0.95, "reason": "..."} 的结构化结果。程序拿到它,就能做判断、进分支、存数据库——这就是 Agent 能"自动化"的基础。
第二种方式(也是 Agent 的灵魂):Function Calling(函数调用)。 这是 2026 年 AI 开发必须掌握的能力。概念一句话:你告诉模型"你有哪些工具可用",模型判断该用哪个工具、传什么参数,然后把工具结果交给你,你再把结果回传给模型生成最终回答。
听起来绕,跑一遍就懂了。经典的"查天气"例子:
"""Function Calling:让模型调用你的工具函数"""
import os
import json
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 1. 定义一个"工具函数":真实项目中这里会调用天气 API
defget_weather(city: str) -> dict:
"""模拟查询天气"""
weather_map = {
"北京": "晴,25℃",
"上海": "多云,28℃",
"广州": "小雨,30℃",
}
return {"city": city, "weather": weather_map.get(city, "数据未知")}
# 2. 用 tools 参数告诉模型:你有一个叫 get_weather 的工具,长这样
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如:北京"}
},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
# 3. 第一轮调用:模型看到问题,决定"我需要查天气工具"
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
tools=tools, # 告诉模型有哪些工具
)
msg = response.choices[0].message
if msg.tool_calls:
# 4. 模型要求调用工具:取出工具名和参数
tool_call = msg.tool_calls[0]
print("模型想调用工具:", tool_call.function.name)
print("参数:", tool_call.function.arguments)
args = json.loads(tool_call.function.arguments) # 参数是 JSON 字符串
result = get_weather(args["city"]) # 5. 程序执行工具函数
# 6. 把"模型的工具调用请求"和"工具执行结果"都追加进消息
messages.append(msg) # assistant 的 tool_calls 消息
messages.append({
"role": "tool", # 工具结果用 role="tool"
"tool_call_id": tool_call.id, # 与调用请求对应
"content": json.dumps(result, ensure_ascii=False),
})
# 7. 第二轮调用:模型"看到"了工具结果,生成最终回答
final = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
)
print("最终回答:", final.choices[0].message.content)
跑起来,你会看到完整闭环:模型说"我想调用 get_weather,参数是北京"→ 你的代码执行工具拿到天气 → 模型基于天气数据回答"北京今天晴,25℃"。
注意:模型本身不知道北京天气,它只是学会了"用你的工具"——这就是 Agent 能联网、能查数据库、能操作系统的底层机制。
再补一个控制参数:tool_choice。 默认情况下(tool_choice="auto"),模型自己判断要不要用工具、用哪个;你也可以强制它必须用某个工具(tool_choice={"type": "function", "function": {"name": "get_weather"}}),或者强制不许用(tool_choice="none")。调试阶段,如果你发现模型该调工具却不调,先试试强制指定,能帮你快速确认是"模型没识别到意图"还是"工具定义有问题"。
整个流程就是 Agent 循环的雏形:模型决策 → 程序执行 → 结果回传 → 模型再决策。你后面学 LangGraph,学的就是把这种循环编排成复杂的工作流。今天先把这个闭环跑通,地基就打牢了。
⚠️ 新手常见坑:
① 忘了把 msg(含 tool_calls 的 assistant 消息)追加回 messages,第二轮调用会报错"tool_call_id 不存在"——tool_calls 消息和 tool 结果必须成对出现;
② tool_call.function.arguments 是 JSON 字符串,不是字典,必须 json.loads 解析;
③ 模型有时会"幻觉"工具名或参数,防御办法是在 description 里写清楚每个参数的含义和示例值。
08 实战:命令行 AI 助手,把今天学的全串起来
现在,把前面所有知识点组装成一件完整的作品:一个命令行 AI 助手。它具备:
- 三个命令:
/quit 退出、/save 保存对话、/cost 查看花费
新建 assistant.py,完整代码如下:
"""命令行 AI 助手:多轮对话 + 流式输出 + 命令系统"""
import os
import json
from openai import OpenAI, AuthenticationError, RateLimitError, APIError
from dotenv import load_dotenv
load_dotenv() # 读取 .env 文件中的密钥
# 价格常量(美元/百万 token,DeepSeek V4 Flash,以官网为准)
PRICE_IN = 0.14
PRICE_OUT = 0.28
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 对话记忆 + 成本统计
messages = [{"role": "system", "content": "你是一个友好的全能助手,回答简洁实用。"}]
total_cost = 0.0# 累计花费(美元)
defstream_chat(messages):
"""流式调用模型,返回完整回复和本次 token 消耗"""
global total_cost
reply_parts = []
usage = None# 用量统计:开启 include_usage 后,最后一个碎片会带上
stream = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
stream=True,
max_tokens=800,
stream_options={"include_usage": True}, # 让最后一个碎片带上 token 统计
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) # 打字机效果
reply_parts.append(delta)
if chunk.usage: # 最后一个碎片带用量统计
usage = chunk.usage
full_reply = "".join(reply_parts)
# 估算成本:输入约等于历史全部 token,这里简化按输出 + 预估输入算
if usage:
cost = usage.prompt_tokens / 1_000_000 * PRICE_IN + \
usage.completion_tokens / 1_000_000 * PRICE_OUT
else:
cost = 0.0# 拿不到统计时按 0 处理,不影响主流程
total_cost += cost
return full_reply, cost
defsave_history():
"""把对话保存成 JSON 文件"""
filename = "chat_history.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(messages, f, ensure_ascii=False, indent=2)
print(f"\n💾 对话已保存到 {filename}")
print("🤖 AI 助手已上线!输入 /quit 退出,/save 保存对话,/cost 查看花费")
whileTrue:
user_input = input("\n你:").strip()
ifnot user_input:
continue
# -------- 命令系统 --------
if user_input == "/quit":
print("👋 再见!")
break
if user_input == "/save":
save_history()
continue
if user_input == "/cost":
print(f"💰 本次会话累计花费:${total_cost:.4f}(约 {total_cost * 7.2:.2f} 元)")
continue
# -------- 正常对话 --------
messages.append({"role": "user", "content": user_input})
print("AI:", end="")
try:
reply, cost = stream_chat(messages)
messages.append({"role": "assistant", "content": reply})
print(f"\n(本次花费 ${cost:.5f})")
except AuthenticationError:
print("\n❌ 密钥无效,请检查 .env 文件里的 DEEPSEEK_API_KEY")
except RateLimitError:
print("\n❌ 请求太频繁,休息几秒再试")
except APIError as e:
print(f"\n❌ 出错了:{e}")
# 简单防膨胀:记忆超过 20 条就截断(保留 system + 最近 18 条)
if len(messages) > 20:
system_msg = messages[0]
messages = [system_msg] + messages[-18:]
print("(已自动精简历史记忆)")
✅ 运行前检查清单(08 节)
- [ ] 已安装 openai 和 python-dotenv:
pip install openai python-dotenv - [ ] 项目根目录有
.env 文件,内容为 DEEPSEEK_API_KEY=sk-你的密钥 - [ ]
.env 已加入 .gitignore(防止密钥泄露) - [ ] 虚拟环境已激活(命令行有
(venv) 前缀)
功能点讲解:
- 命令系统:
if/elif 分支判断输入是否以 / 开头,实现 /quit、/save、/cost 三个命令——这就是最朴素的"意图路由",上篇学的控制流直接复用。 - stream_chat 函数:封装了流式调用。注意
chunk.usage 只在最后一个碎片出现,并且需要 stream_options={"include_usage": True} 才会返回,用它拿 token 统计。 - 成本统计:每次调用后按价格常量换算美元,
total_cost 累加。/cost 命令随时查账。 - 记忆截断:messages 超过 20 条就保留 system + 最近 18 条,防止无限膨胀(05 节学的,直接用上)。
- 异常处理:密钥错、限流、网络错误都被捕获,程序不崩,用户还能继续聊。
运行效果预览(真实运行时的样子):
🤖 AI 助手已上线!输入 /quit 退出,/save 保存对话,/cost 查看花费
你:帮我写一段 Python 代码,计算 1 到 100 的和
AI:最简单的方式是用内置函数 sum:
total = sum(range(1, 101))
print(total) # 5050
也可以用循环:total = 0
for i in range(1, 101):
total += i
print(total)
(本次花费 $0.00008)
你:/cost
💰 本次会话累计花费:$0.0002(约 0.00 元)
你:/save
💾 对话已保存到 chat_history.json
你:/quit
👋 再见!
一次完整会话花费不到一分钱,这就是 DeepSeek 入门练手的底气。这个程序虽然只有 80 行,但它已经是一个"最小可用 Agent 外壳"了——有记忆、有工具(命令)、有成本控制、有异常兜底。把它跑通,你就真正拥有了"从零到跑通大模型程序"的能力。
动手改造建议,给你三个由易到难的方向:① 给 /save 加上时间戳文件名,每次保存不覆盖(chat_20260818.json);② 加一个 /clear 命令一键清空历史(保留 system 人设);③ 把 07 节的 get_weather 工具接进来,让助手能回答"北京天气怎么样"——这一步做完,你的助手就从"聊天机器人"升级成"会干活的 Agent"了。别小看这三个练习,它们正好覆盖了文件操作、状态管理、工具调用三个 Agent 开发的核心能力。
⚠️ 新手常见坑:
① stream_chat 里 chunk.usage 可能不存在,直接访问会 AttributeError——用 if chunk.usage: 判空;
② /save 保存的是整个 messages 列表(含 system 人设),下次加载后注意别重复加 system;
③ 输入空内容直接回车,input().strip() 后是空字符串,记得 continue 跳过,否则白花一次调用费。
09 进阶方向与最佳实践:从"跑通"到"专业"
跑通助手只是起点。真实生产环境里,还有四个问题绕不开:并发、重试、缓存、成本。每个给你一个最小方案,够你用一阵子。
1. 并发调用:一次问模型十个问题。 上篇学过 asyncio,openai SDK 也提供异步接口。想在单线程里并行发多个请求,用 asyncio.gather:
"""并发调用:同时问模型多个问题"""
import os
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
asyncdefask(question: str) -> str:
resp = await client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": question}],
max_tokens=200,
)
return resp.choices[0].message.content
asyncdefmain():
questions = ["1+1=?", "中国的首都是?", "Python 是谁发明的?"]
# 三个请求同时发出,总耗时约等于最慢的一个
results = await asyncio.gather(*[ask(q) for q in questions])
for q, r in zip(questions, results):
print(f"问:{q}\n答:{r}\n")
asyncio.run(main())
2. 重试与退避:网络抖动不慌。 429 限流、500 服务器错误,重试几次通常能成。先装库,再用 tenacity 三行搞定:
pip install tenacity
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
defcall_with_retry(client, messages):
"""最多重试 3 次,每次等待时间指数增长(1s、2s、4s...)"""
return client.chat.completions.create(
model="deepseek-v4-flash", messages=messages
)
3. 缓存:相同的请求别花两次钱。 用字典做最简单的缓存——相同的消息列表直接返回上次结果:
import json
cache = {}
defcached_chat(client, messages):
key = json.dumps(messages, ensure_ascii=False) # 消息列表转字符串当键
if key in cache:
print("(命中缓存,未调用 API)")
return cache[key]
resp = client.chat.completions.create(model="deepseek-v4-flash", messages=messages)
cache[key] = resp.choices[0].message.content
return cache[key]
4. 成本控制三板斧:① 提示词精简——能 50 字说清的事别写 200 字,输入 token 直接省一半;② 利用缓存命中价——DeepSeek V4 Flash 缓存命中的输入价仅 $0.0028/M,约为未命中的 1/50(以官网为准),固定前缀(如 system 人设)尽量保持一致,命中缓存直接省钱;③ 批量任务放闲时跑——DeepSeek 2026 年 8 月起实行峰谷定价,非实时任务(日志分析、批量摘要)挪到晚上跑,成本立省一半。
5. API Key 安全加固:环境变量 + .gitignore 里加 .env + 定期轮换密钥。记住:密钥进了 Git 历史,就等于泄露了,只能作废重建。另外,如果项目要部署到服务器,别用 .env 文件,直接用平台的环境变量配置功能(Vercel、阿里云、AWS 都支持),更安全也更方便。
6. 日志与可观测性:生产环境里,每次 API 调用都要留痕——时间、模型、token 数、耗时、是否重试。别小看这件事,排查线上问题时,日志就是你的"黑匣子"。最简单的做法是每轮调用后打印一行结构化日志,或者用 Python 自带的 logging 模块输出到文件。等你开始写真正的 Agent 应用,会庆幸从第一天就养成了记日志的习惯。
下一步学习路径,按顺序走:
- FastAPI:把命令行助手包成 Web 服务,别人就能通过浏览器/接口访问你的 AI 应用——这是从"玩具"到"产品"的第一步。
- LangChain / LangGraph:学 Agent 编排。你已经在 07 节亲手实现了 tool 调用闭环,LangGraph 就是把这个循环工程化、可视化。
- RAG(检索增强):让 Agent 能"读"你自己的文档,回答基于你的知识库。
- 微调:用你的业务数据定制模型行为——这是最后才需要碰的,前面的路还长。
给你的行动清单(建议一周内完成):
- [ ] 跑通 04 节的第一个 LLM 程序,看到模型回复
- [ ] 把 05 节多轮对话扩成"旅行助手",聊 10 轮以上
- [ ] 把 06 节流式输出加进你的助手,体验打字机效果
- [ ] 跑通 07 节 Function Calling 闭环,把 get_weather 换成真实天气 API
- [ ] 完成 08 节命令行助手,并加上
/clear(清空历史)命令
完成这五步,你就不再是"看过教程的人",而是"跑通过真实 Agent 的人"。
结语:从"模拟"到"真实",只差一次 API 调用
回到开头那个"简易智能客服"。上篇我们用字典模拟 LLM,假装模型会回答;这篇你亲手调通了真实的大模型 API——从"模拟"到"真实",中间隔的只是一次 client.chat.completions.create() 调用,但跨过去之后,你看到的世界完全不同了。
你现在的工具箱里,有 HTTP 与 API 的底层认知、token 与成本的算账能力、多轮对话的记忆管理、流式输出的体验优化、结构化输出与工具调用的 Agent 核心机制,还有一个能跑的命令行 AI 助手。这套东西,就是 2026 年 AI Agent 开发者的入门标配。
最后送你一句话:学 API 调用,最忌讳"收藏了就等于会了"。今天这篇的所有代码,加起来约 400 行,但每一行都值得你亲手敲一遍。敲的过程中你会遇到报错、遇到 None、遇到状态码 429——这些"坑"才是真正的老师。把 08 节的助手跑通,再按动手建议加上一两个功能,你就完成了从"看教程的人"到"写代码的人"的转变,这个转变,谁也替不了你。
接下来,把 08 节的助手改造一下,加一个你自己的功能——比如让它读你的待办清单、帮你总结文章。改通的那一刻,你就不再是"学习者",而是"开发者"了。
跑通第一个程序了吗?评论区晒出你的运行结果
参考来源
- DeepSeek 官方 API 文档(Chat Completions) - https://api-docs.deepseek.com/api/create-chat-completion
- DeepSeek 官方 API 定价页 - https://api-docs.deepseek.com/quick_start/pricing
- OpenAI Python SDK 官方文档 - https://github.com/openai/openai-python
- 阿里云百炼:DeepSeek-V4 上线公告 - https://developer.aliyun.com/article/1731293
- 2026 年主流 LLM API 价格对比(DevTk.AI / Neodrop AI,数据截至 2026 年 8 月,以官网为准) - https://devtk.ai/zh/news/llm-api-pricing-2026
注:文中价格与模型信息截至 2026 年 8 月,厂商调价频繁,请以各官网实时价格为准。