从代码调用模型

上一章构建了一份消息列表。本章从 Python 将该列表发送到真实模型并获得回答。这种往返是使用这些模型构建的最小完整循环,你编写的几乎每个功能都建立在这个基础之上。
你输入一个提示词,你的代码将其发送到某处,然后文本返回来。感觉像模型就在你的程序中。它不在,看看它实际运行的地方可以解释成本、速度和你会遇到的怪癖。
所以想象一下物理上发生的事情。模型是提供商服务器上的一个巨大参数集,运行在你永远看不到的硬件上。当你调用它时,你的代码通过互联网发送一个普通的网络请求,携带你的消息。
提供商在他们的机器上运行预测循环,并将生成的令牌发送回给你。模型调用是租用他人计算机上的几个时刻。这个框架解释了很多事情:延迟是模型在他们的端生成令牌的时间,成本是他们为该计算收费,整个事情是一个网络调用,伴随着所有相应的含义。
示例使用官方 OpenAI 库。其他提供商有细节差异,但形状几乎到处都一样:你发送一个消息列表,你得到一条消息回来。这种相同性是你对供应商锁定的保险,因为相同的代码可以指向你自己托管或在其他地方租用的开放模型,通常只需改变基础 URL(开放和封闭模型涵盖了这个选择)。
请求
用 pip install openai 安装库,创建一个客户端,然后调用它。客户端从环境变量读取你的 API 密钥,所以密钥从不出现在你的代码中。
from openai import OpenAI
client = OpenAI() # 从环境中读取 OPENAI_API_KEY
MODEL = "gpt-4o-mini" # 这是你改变的唯一一行来切换模型
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a concise assistant. Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
)两部分是必需的:model,你想要运行的模型的名称,和 messages,来自上一章的角色标记消息列表。其他一切都有默认值。
注意模型名称位于单个常量中。在一个更好、更便宜的模型每几个月出现一次的领域中,这是故意的。将**模型名称**保留在一个地方,以便交换模型是一行改变。这是变化规则的实践:将不稳定的细节保留在你能找到的地方,而不是分散在你的代码中。
model 和 messages。模型名称属于单个常量,因为更便宜或更好的模型每隔几个月就会出现,你不想去寻找它。我以缓慢的方式学到的,在一个模型被重命名后,我发现它粘贴在九个文件中。 读取响应
回复返回包装在某个结构中。你想要的文本是里面的一条路径,但结构的其余部分值得知道,因为它告诉你发生了什么。
choice = response.choices[0]
print(choice.message.content) # 回复文本
print(choice.finish_reason) # "stop" -> 模型自己完成了
print(response.usage) # Usage(prompt_tokens=24, completion_tokens=18, total_tokens=42)三样东西很重要。message.content 是文本,坐在 choices[0] 内部,因为 API 可以返回多个选项,尽管你几乎总是要求一个并读取第一个。
finish_reason 告诉你模型为什么停了下来。"stop" 意味着它完成了它的思考,而 "length" 意味着它撞到了你的令牌上限并被中途中止。检查那个字段是你如何在代码中检测被截断回复而不是用眼睛。
而 usage 报告了调用花费的令牌,分为你发送的提示和它写的完成。那个**使用**对象是你的账单,分条列出。每个调用准确地告诉你它在令牌中花费了什么,这是你在构建时要注意的数字。
response.choices[0].message.content,但两个邻居也很重要。finish_reason 告诉你为什么它停止了:"stop" 意味着完成,"length" 意味着它撞到了你的令牌上限并被截断。而 response.usage 是你的令牌账单,提示和完成分开,所以你永远不用猜测调用花费了什么。 值得了解的参数
超越 model 和 messages,两个可选设置经常出现。
temperature 是来自 LLM 如何工作 的随机性拨盘,变成具体的。低值(接近 0)让模型保持接近它的最佳选择:专注且可重复,这是你对提取和分类想要的。更高的值(围绕 0.7 到 1)让它够得着不太可能的令牌:更多样化和创意,而且更容易漂移。你调节的是模型大胆采样的程度,仅此而已。
max_tokens 限制回复可以运行多少令牌。它保护你免受逃脱答案和逃脱账单。设置**max_tokens**太低,模型会被切断,这正是上一部分的 "length" 完成原因,所以留下真实的回旋余地。
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.2, # 专注且一致
max_tokens=300, # 限制回复长度
)temperature 调节模型大胆采样的程度:低于专注、可重复的答案,高于多样化、创意的答案。max_tokens 限制回复长度来保护你的账单,但设置太低会触发 "length" 切断,所以留下回旋余地。对于提取和分类,每次都选择低温度。 流式传输
默认情况下,你等待整个答案完成生成,然后它一下子到达。由于模型一次生成一个令牌,一个长回复意味着一个长等待盯着什么都没有。
**流式传输**将每个令牌在生成时立即发送给你,所以文本立即出现并实时填充,就像聊天应用显示回复在自己打字出来。
stream = client.chat.completions.create(
model=MODEL,
messages=messages,
stream=True,
)
for chunk in stream:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)总生成时间两种方式都是一样的;模型没有更快。流式传输只改变当你看到令牌,在他们被生成时表面他们而不是等到最后才拿回。
你通过迭代块并打印每个文本的 delta 来处理它。权衡是多一点代码而大大更好的等待。流式传输改变当你看到令牌,而不是他们到达多快。
delta 更早看到令牌。 每个调用都是独立的
这是章节中最重要的行为,它直接遵循 上下文窗口 课程。API 是**无状态的**:每个请求完全独立。提供商的服务器不记得你的之前调用。它在你发送的确切消息上运行预测,返回结果,并对下一次的交换保留任何东西。
所以对话不被模型或 API 存储任何地方。它活在你的代码中。如果你想第二轮知道第一轮发生了什么,你自己保持消息的运行列表并再次发送整个东西。一个来回聊天是你通过在每个调用上重新发送一个不断增长的历史创建的幻觉。
这也是为什么一个长对话随时间每条消息成本更多:每个调用重新发送所有先前的轮次作为输入令牌,所以提示不断增长,即使用户的新问题很短。
history = [
{"role": "system", "content": "You are a friendly travel assistant. Keep answers short."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# 整个历史每次都上升;服务器不记得任何东西
response = client.chat.completions.create(model=MODEL, messages=history)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply}) # 为下一轮保留回复
return reply
chat("I have a weekend in Beijing. What should I see?")
chat("Which of those is best for kids?") # 仅工作因为历史携带轮一第二个问题仅使意义因为第一个问题和它的答案仍在 history 中并一起被发送。你是内存。这一个想法,你组装并每次重新发送完整上下文,在对话下面,稍后在工具使用、检索和代理下面也下面。他们都是关于决定在每个独立调用之前什么进入那个消息列表的变化。
当事情出错
模型调用是到别人服务器的网络调用,所以它在所有网络调用失败的方式失败。从一开始就为它设计。
- 错误和速率限制。提供商限制你可以在一个窗口内发送多少请求。撞到限制,调用失败且速率限制错误。标准响应是等待并重试,在每个失败后加长等待(这被称为退避)所以你不会锤击一个忙碌的服务。
- **超时。**一个调用可能会挂起。设置一个超时,所以一个慢请求不会冻结你的应用。
- **密钥保留在服务器上。**你的 API 密钥是一个花费你的钱的密码。永远不要把 API 密钥放在浏览器代码中。任何人可以打开开发工具并读取它。浏览器与你的后端交谈,而你的后端,持有密钥,与模型交谈。
- **假设任何答案可能是错误的。**即使一个成功的调用可能返回一个 幻觉 或损坏的输出,所以下一章是关于使结果变成你的代码可以信任的东西。
def ask_model(messages):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
timeout=20, # 20 秒后放弃
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Sorry, something went wrong. Please try again."
