3. 构建你的第一个流式 CLI 聊天
在第 1 章中,你完成了第一次 LLM 调用,并看到一次性出现的完整响应。在第 2 章中,你学习了代理式 AI(agentic AI)的概念基础,以及 LangChain 为什么存在。现在是时候构建一些实用的东西了:一个让人感觉响应迅速且专业的流式聊天应用。
为什么流式输出很重要:当你向 LLM 提出一个复杂问题时,等待 10–30 秒才能看到完整响应会让人感觉程序“坏了”。流式输出让 token 在生成时就逐步显示出来,从而形成自然的对话节奏。本章将构建一个带流式输出的 CLI 聊天应用,包含正确的配置管理、调试能力,以及健壮的错误处理。
你将构建什么:在本章结束时,你将拥有一个可用的 chat.py 脚本,它能够:
- 将 LLM 响应按 token 流式输出到终端
- 从环境变量中安全加载 API Key
- 使用合适的参数处理不同模型类型(聊天模型 vs 推理模型)
- 提供调试工具,用于检查实际发送给 LLM 的内容
- 优雅处理常见错误(缺少 API Key、网络故障、无效输入)
3.1) 创建工作文件夹并安装包
在编写任何代码之前,你需要一个干净的项目结构和正确的依赖。本节将为一个可维护的 Python 项目打下基础。
项目结构
为你的聊天应用创建一个新目录:
mkdir langchain-chat
cd langchain-chatPython 环境设置
创建虚拟环境以隔离依赖:
# 创建虚拟环境
python -m venv venv
# 激活(macOS/Linux)
source venv/bin/activate
# 激活(Windows)
venv\Scripts\activate为什么要使用虚拟环境? LangChain 有很多依赖(例如 OpenAI SDK、Pydantic、异步库)。虚拟环境可以确保:
- 你的系统 Python 保持干净
- 不同项目可以使用不同版本的 LangChain
- 依赖可复现(通过
requirements.txt)
激活后,你会在终端提示符中看到 (venv)。
安装 LangChain
安装 LangChain 的核心包:
pip install langchain-core==1.2.7 langchain-openai==1.1.7 python-dotenv包拆解:
langchain-core:核心抽象(消息、提示词、链、runnables)langchain-openai:OpenAI 专用实现(ChatOpenAI、嵌入)python-dotenv:从.env文件加载环境变量
版本说明:本书以 2026 年 1 月的 LangChain 1.2.x 为准。如果你在未来阅读本书,请查看 LangChain 文档 获取最新版本。
验证安装
创建一个简单测试以确认一切正常:
# test_install.py
try:
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
print("✓ langchain-core: OK")
print("✓ langchain-openai: OK")
print("\n安装成功!")
except ImportError as e:
print(f"✗ 导入失败: {e}")
print("请确保你的虚拟环境已激活。")运行它:
python test_install.py期望输出:
✓ langchain-core: OK
✓ langchain-openai: OK
安装成功!如果你看到“安装成功!”,就可以继续了。如果你遇到导入错误,请再次检查:
- 你的虚拟环境已激活(提示符中应有
(venv)) - 这些包已成功安装(尝试运行
pip list)
创建 requirements.txt
你刚刚用 pip install 命令安装了包。虽然这对学习来说可行,但有一种更好的方式:requirements.txt 文件。这是 Python 项目中的标准做法,原因有很多:
为什么使用 requirements.txt?
- 可复现性:其他人(或 6 个月后的你)可以安装完全相同的包版本
- 清晰的依赖管理:一眼就能看到项目需要哪些包
- 团队协作:团队成员使用完全一致的版本,避免“在我机器上能跑”的问题
- 自动化:服务器或 CI/CD 流水线可以用一行命令完成环境搭建:
pip install -r requirements.txt
在项目根目录创建一个 requirements.txt 文件:
# requirements.txt
langchain-core==1.2.7
langchain-openai==1.1.7
python-dotenv注意语法:
==1.2.7固定到精确版本(推荐用于可复现)- 不写版本号(如
python-dotenv)会安装最新的稳定版本 - 以
#开头的行是注释
现在任何人都可以用一条命令安装所有依赖:
pip install -r requirements.txt这通常比逐个输入包名更高效,也更符合团队协作的常见实践。如果队友克隆了你的项目,他们只需要:
- 创建虚拟环境
- 运行
pip install -r requirements.txt
无需记住包名或版本——文件里都写好了。
你的项目结构
完成本节后,你的文件夹应如下所示:
langchain-chat/
├── venv/ # 虚拟环境(不要提交到 git)
├── requirements.txt # 依赖列表
└── test_install.py # 安装验证脚本下一步:第 3.2 节将展示如何使用 .env 文件安全加载 API Key。
3.2) 使用 .env 的环境变量
API Key(密钥)属于机密。把它们硬编码到代码里存在安全风险(尤其是当你提交到 git 时)。本节将展示标准做法:将机密写入 .env 文件,并在运行时加载到环境变量中。
为什么使用环境变量?
硬编码 API Key 的问题:
# ❌ 千万不要这么做
llm = ChatOpenAI(api_key="sk-proj-abc123...")如果你把这段代码提交到 GitHub,你的 API Key 就公开了。任何人都能使用它、在你的账号上产生费用,或者导致你的 API Key 被吊销。
解决方案:把机密存放在环境变量中,并在运行时加载。
创建 .env 文件
在项目根目录创建一个 .env 文件:
# .env
OPENAI_API_KEY=sk-proj-your-actual-key-here获取你的 API Key:
- 前往 platform.openai.com/api-keys
- 创建一个新的 secret key
- 立即复制(之后无法再次查看)
- 将其粘贴到你的
.env文件中,替换sk-proj-your-actual-key-here
关键安全步骤:在做任何其他事情之前,先保护你的 API Key 不被提交到 git。
在项目根目录创建一个 .gitignore 文件,并添加以下几行:
# .gitignore
venv/
__pycache__/
*.pyc
.env.env 这一行会告诉 git 忽略你的 API Key 文件,从而防止你不小心把机密提交到版本控制中。
你现在的项目结构:
langchain-chat/
├── venv/
├── .env # 你的 API Key(被 git 忽略)
├── .gitignore # 包含:.env、venv/ 等
├── requirements.txt
└── test_install.py加载环境变量
python-dotenv 包会把 .env 文件加载到 os.environ 中:
# chat.py
import os
from dotenv import load_dotenv
# 加载 .env 文件
load_dotenv()
# 访问环境变量
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("在环境中未找到 OPENAI_API_KEY")
print(f"已加载 API Key:{api_key[:8]}...") # 只显示前 8 个字符load_dotenv() 如何工作:
- 从你运行脚本的位置开始查找
.env文件 - 读取每一行
KEY=value格式的内容 - 将每个变量写入
os.environ - 如果变量已被设置(例如由托管平台设置),它不会覆盖——将保留现有值
在 LangChain 中使用 API Key
LangChain 的 OpenAI 集成(ChatOpenAI 等)会自动在 os.environ 中查找 OPENAI_API_KEY:
from langchain_openai import ChatOpenAI
load_dotenv()
# 这会自动使用 os.environ["OPENAI_API_KEY"]
llm = ChatOpenAI(model="gpt-4o-mini")LangChain 的约定:当你创建 ChatOpenAI() 且不传入 api_key 参数时,它会自动在环境中查找 OPENAI_API_KEY。这是 LangChain 各类集成中常见的标准模式。
显式传入 API Key(用于测试或多个 API Key 场景):
llm = ChatOpenAI(
model="gpt-4o-mini",
api_key=os.environ.get("OPENAI_API_KEY")
)当你有多个 API Key(开发 vs 生产)或希望明确指定使用哪个 API Key 时,这会很有用。
生产环境中的环境变量
在生产环境(云平台、Docker 容器)中,你不会使用 .env 文件。相反,你会通过平台设置来配置环境变量:
- Docker:运行容器时使用
-e参数 - 云平台:在配置面板中设置环境变量
- CI/CD:使用机密管理工具
重要的是:你的代码不需要改变。无论变量来自 .env 文件还是云平台,os.environ.get("OPENAI_API_KEY") 的工作方式都一样。我们将在后续章节详细介绍部署。
验证你的设置
为确认一切正常,你可以测试前面展示的环境变量加载代码。如果你的 .env 文件配置正确,os.environ.get("OPENAI_API_KEY") 将返回你的 API Key。
如果 os.environ.get("OPENAI_API_KEY") 返回 None,请确认:
- 你在访问环境变量之前调用了
load_dotenv() .env存在于项目根目录.env中正确写入了OPENAI_API_KEY=sk-proj-...- 你是在项目根目录运行脚本
下一步:第 3.3 节将实现带流式输出的实际聊天循环。
3.3) 用流式输出实现聊天循环
现在你将构建核心聊天循环。本节将介绍流式输出(streaming)——它是“卡顿的聊天机器人”和“响应迅速的聊天机器人”的关键区别。
理解流式输出
不使用流式输出(第 1 章的方法):
response = llm.invoke("写一篇关于 AI 的 500 字短文")
print(response.content) # 等 20 秒,然后整篇文章一次性出现使用流式输出:
for chunk in llm.stream("写一篇关于 AI 的 500 字短文"):
print(chunk.content, end="", flush=True) # token 在生成时逐步出现为什么流式输出很重要:
- 即时反馈:不是盯着空白屏幕等 20 秒,而是立刻就能看到文字出现
- 更自然的对话感:就像和人聊天——回答是渐进出现的,而不是一次性给出
- 节省时间和成本:如果 LLM 开始给出错误答案,你可以提前停止,而不是等到得到完整(但无用)的响应
- 更好的调试:在构建应用时,你可以在问题发生时(例如格式错误)立刻发现,而不是长时间等待后才看到结果
流式输出到底是什么:流式输出是对同一段响应文本的增量传输。它不会暴露隐藏推理或模型内部过程——只是让你在 API 逐步返回部分输出时就能看到。把它想象成下载文件:你会随着分块到达看到进度,但无论一次性下载还是分块下载,文件内容都是一样的。
关于 chunk 边界的说明:chunk 不保证与单词或句子对齐。API 为了效率会以小批量发送 token,因此一个 chunk 可能是 "Hel"、"lo! How"、" can I"、" help you"、"?"。这很正常且符合预期——不要尝试从单个 chunk 中解析语义。
基本聊天循环
下面是一个最小的流式聊天循环:
# chat.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
def main():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
print("聊天已开始。输入 'quit' 或 'exit' 结束。\n")
while True:
user_input = input("You: ")
if user_input.lower() in ["quit", "exit"]:
print("再见!")
break
print("Assistant: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
print("\n")
if __name__ == "__main__":
main()它是如何工作的:
while True::用于持续对话的无限循环input("你: "):从终端获取用户输入llm.stream([HumanMessage(...)]):流式获取 LLM 响应- 带特殊参数的流式输出:
end="":每个 chunk 输出后不加换行(保持同一行输出)flush=True:强制立即输出到终端,不进行缓冲
为什么是 [HumanMessage(content=user_input)]?
LangChain 的聊天模型期望输入是消息列表,而不是原始字符串。每条消息都有一个角色:
- HumanMessage:用户输入
- AIMessage:LLM 响应
- SystemMessage:给 LLM 的指令(第 4 章将介绍)
即使只有一条用户消息,你也要传入列表:[HumanMessage(content="Hello")]。
关键限制——单轮对话:这个聊天循环刻意不保存状态。每次请求只发送当前消息,不发送之前的对话历史。这意味着:
- LLM 不会记得你之前问过什么
- 在你问了“法国的首都是什么?”之后,再问“它的人口是多少?”这类追问将无法工作
- 这是 LLM 的基本特性——除非你显式提供上下文,否则它们没有记忆
该限制的示例:
你: 法国的首都是什么?
助手: 巴黎。
你: 它的人口是多少?
助手: 我没有足够的上下文。你在问哪个城市?while True 循环提供了 UX 的连续性(你可以一直聊天),但每一轮都是独立的。第 8 章将介绍:我们会通过存储并在每次请求时重新发送消息历史来实现对话记忆。
运行聊天循环
python chat.py示例交互:
聊天已开始。输入 'quit' 或 'exit' 结束。
You: LangChain 是什么?
Assistant: LangChain 是一个用于开发由语言模型驱动的应用的框架。它提供了提示词管理、链、代理和记忆等工具。
You: 给我一个简单示例
Assistant: 这里有一个基础示例:...
You: quit
再见!理解流式 API
什么是 “chunk”?
每个 chunk 是一个 AIMessageChunk 对象,包含:
content:生成的文本 tokenresponse_metadata:模型信息、token 计数等
for chunk in llm.stream([HumanMessage(content="Hello")]):
print(f"Chunk: {chunk}")
print(f"Content: {chunk.content}")
print(f"Type: {type(chunk)}")输出:
Chunk: content='Hello' response_metadata={'model_provider': 'openai', ...}
Content: Hello
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
Chunk: content='!' response_metadata={...}
Content: !
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
Chunk: content=' How' response_metadata={...}
Content: How
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>累积完整响应
有时你需要完整响应(用于日志、测试或进一步处理):
def chat_with_accumulation():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
user_input = input("你: ")
full_response = ""
print("助手: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
full_response += chunk.content
print("\n")
# 现在你已经拥有完整响应
print(f"[DEBUG] 完整响应长度: {len(full_response)} 字符")
return full_response当你需要做到以下事情时,这种模式很常见:
- 将对话保存到数据库
- 将响应解析为结构化数据
- 计算 token 使用量或成本
完成本节后的项目结构:
langchain-chat/
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── test_install.py
└── chat.py # 流式聊天循环(新增!)下一步:第 3.4 节将展示如何通过智能参数配置来处理不同模型类型。
3.4) 智能配置:处理推理模型与聊天模型的参数差异
OpenAI 提供两类模型,它们的能力和控制机制不同:
聊天模型(gpt-4o、gpt-4o-mini):
- 快速、对话感强
- 支持
temperature来控制随机性与创造性 - 适合通用任务、创意写作、日常编码
推理模型(o1、o3、GPT-5):
- 更慢,但逻辑性与一致性更强
- 不支持
temperature(改用内部推理) - 适合复杂数学、多步规划、正式分析
关键差异:聊天模型使用概率采样(你来控制随机性),而推理模型使用确定性的内部逻辑(模型控制自身的推理过程)。
理解 Temperature(仅聊天模型)
什么是 temperature?
Temperature 是一个介于 0.0 到 2.0 的数字,用于控制模型回答的创造性。值越低(接近 0),回答越一致、可预测;值越高(接近 2.0),回答越富有创意、变化更大。你可以把它理解为一个“创造力旋钮”。
它如何工作:在生成每个词时,模型会看到多个可能的下一个词,并带有不同概率。Temperature 会影响模型如何选择:
- 低 temperature(0.0):几乎总是选择概率最高的词 → 回答一致、聚焦
- 高 temperature(2.0):更可能选择低概率词 → 回答多样、富有创意
重要:Temperature 只适用于聊天模型(gpt-4o、gpt-4o-mini)。 它不适用于推理模型(GPT-5、o1、o3),后者使用内部逻辑而非概率采样。
Temperature 取值指南:
-
0.0:高度确定、聚焦且一致
- 适用:事实问答、常规代码生成、结构化输出
- 同样输入 → 每次输出几乎一致
- 示例:“2+2 等于多少?” → 总是 “4”
-
0.7–1.0:标准采样行为(默认值为 1.0)
- 适用:一般对话、解释说明、平衡型回答
- 措辞与示例有适度变化
- 示例:“解释光合作用” → 每次措辞不同,但核心信息一致
-
1.2–2.0:更有创意、更发散、可预测性更低
- 适用:创意写作、头脑风暴、构思
- 语气、结构、用词差异更大
- 示例:“写一首关于月亮的诗” → 每次风格都很不同
注意:超过 1.0 会提升创造性,但可能降低事实准确性与连贯性。最大值为 2.0。
示例:Temperature 的影响(仅聊天模型)
# Temperature 0.0 - 确定性强,每次答案都一样
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
response = llm.invoke([HumanMessage(content="2+2 等于多少?")])
print(response.content) # 输出: 4
# Temperature 1.0 - 默认行为,可能有轻微措辞变化
llm = ChatOpenAI(model="gpt-4o-mini", temperature=1.0)
response = llm.invoke([HumanMessage(content="2+2 等于多少?")])
print(response.content) # 输出: 4(可能会带简短解释)对于封闭的事实问题,temperature 对正确性影响不大。
对于开放式或创意任务,temperature 会显著影响多样性、语气与风格。
如果在推理模型上使用聊天模型参数会怎样?
取决于模型——有的会拒绝,有的会悄悄忽略:
# ❌ 这会在 o3 模型上失败
llm = ChatOpenAI(model="o3-mini", temperature=0.7)错误:
BadRequestError: Temperature is not supported with this model不同模型,不同策略:
- o1 / o3 模型:会显式拒绝不支持的参数。如果包含 temperature,API 会立即返回 400 BadRequest 错误。
- GPT-5 模型:更宽松——参数会被接受但会被静默忽略。请求会成功,但 temperature 没有效果。
为什么这很重要:始终确认你正在使用哪种模型,并据此配置参数。错误的参数要么直接导致报错,要么静默失效,浪费你的调试时间。
如何控制推理模型行为
现在你知道聊天模型使用 temperature,而推理模型不使用。那么你如何控制推理模型?
推理模型通过提示词设计来调优,而不是参数:
- 推理模型不暴露
temperature或类似控制项 - 你需要通过提示词写法来引导行为:
- 明确指令:“逐步思考”“展示你的过程”
- 将约束写成规则:“你不能假设……”“务必验证……”
- 结构化要求:“以 JSON 格式输出”“在答案前包含推理”
- 决策逻辑:“如果条件 A,则做 X,否则做 Y”
示例:聊天参数 vs 推理提示词
# ❌ 聊天方式 - 对推理模型不适用
llm = ChatOpenAI(model="o3-mini", temperature=0.5)
# 错误: BadRequestError: Temperature is not supported
# ✅ 推理方式 - 通过提示词结构引导
prompt = """
逐步解决这个问题:
1. 说明你已知的信息
2. 展示你的计算过程
3. 验证你的答案
问题:如果 x + 5 = 12,x 等于多少?
"""
llm = ChatOpenAI(model="o3-mini")
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)输出:
1. 我已知的信息:x + 5 = 12
2. 计算过程:x = 12 - 5 = 7
3. 验证:7 + 5 = 12 ✓
答案:x = 7关键洞察:聊天模型通过参数控制,推理模型通过提示词控制。
模型选择决策表
既然你已经理解了如何控制两类模型,这里给出何时使用各类模型:
| 任务类型 | 推荐模型 | 原因 |
|---|---|---|
| 通用对话 | gpt-4o-mini | 快速、低成本、对话感强 |
| 简单问答 | gpt-4o-mini | 足以用于事实性查询 |
| 创意写作 | gpt-4o-mini(temp 0.8–1.0) | Temperature 带来创造性 |
| 代码生成 | GPT-5 | 更好的逻辑规划 |
| 复杂推理 | GPT-5 | 针对多步逻辑优化 |
| 数学问题 | o3 / o1 | 专用推理模型 |
| 多步规划 | GPT-5 | 擅长长时程规划 |
| 正式分析(法律/政策) | o3 | 严格确定性 |
成本与延迟的权衡
理解实际权衡有助于你为用例选择合适模型:
| 模型类型 | 速度(典型延迟) | 成本(相对) | 最适合 |
|---|---|---|---|
| gpt-4o-mini | 非常快(<2s) | 非常低 | 通用对话、简单任务 |
| gpt-4o | 快(1–4s) | 中等 | 更高质量聊天、多模态任务 |
| GPT-5 | 中等(3–8s) | 高 | 复杂推理、规划 |
| o1 / o3 | 最慢(5–15s+) | 最高 | 确定性推理、形式逻辑 |
说明:
- 速度反映典型响应延迟(会随提示词长度与复杂度变化)
- 成本是相对比较——请以 OpenAI 官网最新定价为准
- 推理模型用速度与成本换取一致性与正确性
- 聊天模型优先保证响应速度与效率
何时使用推理模型(GPT-5、o1、o3):
- 需要正确中间步骤的多步数学与 STEM(Science, Technology, Engineering, Mathematics) 问题
- 带有依赖关系与约束条件的复杂逻辑分析
- 多原因交互导致的代码调试
- 规则多、边界情况多、权衡多的规划任务
- 需要一致性与长时程思考的代理工作流
何时使用聊天模型(gpt-4o、gpt-4o-mini):
- 通用对话与交互式聊天
- 推理深度有限的简单问答
- 内容生成(博客、摘要、创意写作)
- 日常代码生成与样板任务
- 速度与成本比深度推理更重要的应用
下一步:第 3.5 节将展示调试技巧,用于检查实际发送给 LLM 的内容。
3.5) 调试:检查响应与 Token 用量
当你的 LLM 行为不符合预期时,你需要看到到底发送了什么、接收了什么。本节将展示如何检查 LLM 调用并调试问题。
为什么调试很重要
常见调试场景:
- “为什么 LLM 给了这个答案?” → 检查精确提示词
- “这次请求花了多少钱?” → 检查 token 用量
- “为什么这么慢?” → 测量延迟
- “我的消息格式正确吗?” → 检查消息结构
挑战:当你调用 llm.invoke() 时,你会拿到一个响应对象。但里面到底有什么?可用于调试的信息有哪些?
理解响应对象
在调试之前,你需要理解 llm.invoke() 返回的是什么。
基本结构:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])
# 响应里有什么?
print(type(response)) # AIMessage
print(response.content) # 实际文本
print(response.response_metadata) # token 用量、模型信息等输出:
<class 'langchain_core.messages.ai.AIMessage'>
Hello! How can I assist you today?
{
'token_usage': {
'completion_tokens': 9,
'prompt_tokens': 8,
'total_tokens': 17
},
'model_name': 'gpt-4o-mini-2024-07-18',
'finish_reason': 'stop',
...
}响应的关键部分:
response.content:LLM 生成的文本response.response_metadata:一个字典,包含:token_usage:使用了多少 token(用于成本计算)model_name:实际响应的精确模型版本finish_reason:生成为什么停止(详见“调试模式”小节)
访问 token 用量:
token_usage = response.response_metadata['token_usage']
print(f"Prompt tokens: {token_usage['prompt_tokens']}")
print(f"Response tokens: {token_usage['completion_tokens']}")
print(f"Total: {token_usage['total_tokens']}")输出:
Prompt tokens: 8
Response tokens: 9
Total: 17为什么这很重要:你需要这些值来调试、追踪成本并优化提示词。
根据 Token 用量计算成本
token 用量决定成本。不同模型有不同定价:
GPT-4o-mini(截至 2026 年 1 月):
- 输入:每 100 万 token $0.15
- 输出:每 100 万 token $0.60
GPT-4o:
- 输入:每 100 万 token $2.50
- 输出:每 100 万 token $10.00
成本计算函数:
def calculate_cost(token_usage, model_name):
"""根据 token 用量计算成本。"""
prompt_tokens = token_usage.get('prompt_tokens', 0)
completion_tokens = token_usage.get('completion_tokens', 0)
# 每 100 万 token 的定价(截至 2026 年 1 月)
pricing = {
'gpt-4o-mini': {'input': 0.15, 'output': 0.60},
'gpt-4o': {'input': 2.50, 'output': 10.00},
'gpt-5': {'input': 1.25, 'output': 10.00},
}
if model_name not in pricing:
return None
input_cost = (prompt_tokens / 1_000_000) * pricing[model_name]['input']
output_cost = (completion_tokens / 1_000_000) * pricing[model_name]['output']
return input_cost + output_cost
# 示例
response = llm.invoke([HumanMessage(content="Explain quantum computing")])
token_usage = response.response_metadata['token_usage']
cost = calculate_cost(token_usage, "gpt-4o-mini")
print(f"Cost: ${cost:.6f}")输出:
Cost: $0.000123为什么这很重要:生产应用每天可以处理 50,000+ 次请求。按每次请求 $0.002 计算,就是 $3,000/月。用错模型或提示词冗长,成本会飙升到 $30,000/月。一个重试循环的 bug 可能一夜烧掉数千美元。从第一天起就追踪 token 用量。
启用调试模式(当你需要原始 API 细节时)
响应对象与自定义封装可以处理大多数调试需求。但有时你需要看到LangChain 发送给 OpenAI 的确切内容——原始 JSON 请求与响应。
你可能需要它的场景:
- 调试 LangChain 的消息格式化
- 验证 API 参数是否正确设置
- 调查意外的 API 错误
- 理解确切的 API payload
LangChain 通过 langchain_core.globals 提供内置调试日志:
from langchain_core.globals import set_debug
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
set_debug(True)
# 现在所有 LLM 调用都会打印调试信息
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])输出:
[llm/start] [llm:ChatOpenAI] Entering LLM run with input:
{
"prompts": [
"Human: Hello"
]
}
[llm/end] [llm:ChatOpenAI] [1.45s] Exiting LLM run with output:
{
"generations": [
[
{
"text": "Hello! How can I assist you today?",
"generation_info": {
"finish_reason": "stop",
"logprobs": null
},
"type": "ChatGeneration",
...
}
]
],
"llm_output": {
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
...
},
"model_provider": "openai",
"model_name": "gpt-4o-mini-2024-07-18",
...
},
}注意:输出格式会因 LLM provider 而异。此示例展示了 OpenAI 的结构。
调试输出揭示了什么:
调试模式展示了完整的 LangChain → OpenAI 通信流程:
1. 消息格式转换:
# 你的代码
[HumanMessage(content="Hello")]
# 你在调试输出中看到的内容
{
"prompts": ["Human: Hello"]
}调试模式展示了 LangChain 在发送到 LLM 之前如何在内部表示你的消息。
2. 生成完成状态:
"finish_reason": "stop"生成为什么结束:
"stop":模型自然完成响应"length":响应因达到 max_tokens 限制而被截断"tool_calls":模型通过生成工具调用指令来结束生成,而不是给出最终文本响应(第 12 章)"content_filter":响应因安全或内容审核规则而被屏蔽或抑制
如果你看到 "length",请提高 max_tokens 以获取完整响应。
3. Token 用量明细:
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
"completion_tokens_details": {
"reasoning_tokens": 0 # 推理模型专用(o1/o3 等)
},
"prompt_tokens_details": {
"cached_tokens": 0 # 提示词缓存(节省成本)
}
}除了基础计数之外,你还可以看到:
- reasoning_tokens:内部推理步骤(仅推理模型)
- cached_tokens:有多少提示词 token 命中缓存(降低成本)
4. 模型版本与指纹:
"model_name": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_8bbc38b4db"- model_name:精确的快照版本(解释了为什么响应会随时间变化)
- system_fingerprint:OpenAI 后端配置 ID(当系统更新时会变化)
5. 请求耗时:
[llm/end] [llm:ChatOpenAI] [1.56s][1.45s] 展示了总请求耗时——用于识别慢查询很有用。
下一步:第 3.6 节将展示如何优雅处理常见错误。
3.6) 处理故障(模拟并修复常见错误)
生产级 LLM 应用会遇到可预测的故障模式:缺少凭证、网络超时、速率限制与无效输入。本节将展示如何优雅地处理这些错误,并从第一天起构建健壮的应用。
六种常见错误
1. 缺少 API Key
发生场景:你尝试创建 ChatOpenAI 实例,但环境中未设置 OPENAI_API_KEY。
示例:
# .env 文件不存在,或 OPENAI_API_KEY 未定义
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])你会看到的错误:
OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable如何修复:
- 检查项目根目录中是否存在
.env文件 - 验证 API Key 名称是否严格为
OPENAI_API_KEY(常见拼写错误:OPENAPI_KEY) - 确保在创建 LLM 之前调用了
load_dotenv()
2. 错误的 API Key
发生场景:你的 .env 文件包含无效、过期或复制错误的 API Key。
示例:
# .env 中是: OPENAI_API_KEY=sk-invalid-key-12345
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])你会看到的错误:
AuthenticationError: Incorrect API key provided如何修复:
- 前往 https://platform.openai.com/api-keys
- 验证你的 API Key 仍处于激活状态(未被吊销或过期)
- 如有需要,生成一个新 API Key
- 仔细复制完整 API Key(常见错误:缺少首/尾字符)
- 粘贴到
.env,不要带多余空格:
OPENAI_API_KEY=sk-proj-exactkeyhere3. 网络故障
发生场景:你的网络连接中断,或 OpenAI 服务器在请求期间暂时不可达。
示例:
# 请求过程中 WiFi 断开,或 OpenAI API 宕机
response = llm.invoke([HumanMessage(content="Hello")])你会看到的错误:
APIConnectionError: Connection error如何修复:
- 检查你的网络连接
- 在 https://status.openai.com 查看 OpenAI 状态
4. 速率限制
发生场景:你在短时间内发送过多请求,超过 API 配额。
示例:
# 立即发送 1000 个请求
for i in range(1000):
llm.invoke([HumanMessage(content=f"Request {i}")])你会看到的错误:
RateLimitError: Rate limit reached for requests如何修复:
- 在 https://platform.openai.com/account/limits 查看你的速率限制
- 如果需要更高限制,升级你的套餐
- 对大规模工作负载使用批处理(第 6 章将介绍)
5. 无效的模型名称
发生场景:你指定了一个不存在的模型名,或你的套餐无法使用该模型。
示例:
llm = ChatOpenAI(model="gpt-99-ultra") # 不存在
response = llm.invoke([HumanMessage(content="Hello")])你会看到的错误:
NotFoundError: The model `gpt-99-ultra` does not exist or you do not have access to it如何修复:
- 在 https://platform.openai.com/docs/models 查看你套餐中可用的模型
6. 超出 Token 限制
发生场景:你的提示词太长,超过了模型的最大上下文窗口。
示例:
# 创建一个 100 万字符的提示词
huge_prompt = "x" * 1_000_000
response = llm.invoke([HumanMessage(content=huge_prompt)])你会看到的错误:
BadRequestError: This model's maximum context length is 128000 tokens. However, your messages resulted in 250000 tokens.如何修复:
- 在发送前检查输入长度
- 了解你的模型限制:
- gpt-4o-mini: 128K tokens
- gpt-4o: 128K tokens
- gpt-5: 400K tokens
- 对长文档使用分块或摘要(第 9 章将介绍)
下一步:第 4 章将展示如何设计可复用的提示词模板,把提示词工程与应用代码解耦。