实操 01 LLM API 实操入门:从第一条请求到生产级调用
所属:AI 知识图谱 → 实操系列(practice/)。实操系列与正文分离:正文(00~11)写不变的原理与判断,实操篇给可运行代码,可随时随版本重写。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 把"知道大模型是什么"变成"能在代码里稳定调用它",补上正文与动手之间的最后一公里 |
| 核心内容 | 首次调用、核心参数、流式输出、多轮会话、结构化输出、Function Calling、Token 计量与成本估算、错误重试 |
| 前置知识 | 00 基础概念(Token/温度/上下文窗口)、任意一门编程语言基础 |
| 读完能做到 | 独立写一个:流式输出的多轮对话工具,带 JSON 结构化输出与工具调用,并能估算月成本 |
| 配套正文 | 参数选择原理见 00;提示词写法见 02;工具调用设计原则见 03;成本监控体系见 05 |
版本约定:本篇以 OpenAI Python SDK 为载体(国内外绝大多数服务商都兼容其 API 格式,换厂商通常只改
base_url和api_key)。具体模型名与价格一律标[易变],以各厂商官方文档为准。
一、环境准备与首次调用
1.1 安装与鉴权
# Python 3.10+ 建议用虚拟环境
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install openai密钥永远不进代码库,用环境变量:
export LLM_API_KEY="sk-..." # 你的密钥
export LLM_BASE_URL="https://api.openai.com/v1" # 换厂商只改这一行# hello_llm.py —— 你的第一个大模型调用
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL"), # None 时用 SDK 默认值
)
resp = client.chat.completions.create(
model="gpt-4o-mini", # [易变] 以厂商当前文档为准
messages=[
{"role": "system", "content": "你是一个严谨的技术助手。"},
{"role": "user", "content": "用一句话解释什么是 Token。"},
],
)
print(resp.choices[0].message.content)三种角色,一次说清(这是所有对话 API 的通用结构,三五年内不会变):
| 角色 | 谁在说 | 作用 |
|---|---|---|
system | 应用开发者 | 设定身份、规则、边界。用户看不到,权重最高 |
user | 终端用户 | 用户输入 |
assistant | 模型 | 模型回复。多轮对话时要把历史轮次传回去(见第三节) |
1.2 一次调用发生了什么
你的代码 ──HTTP POST /chat/completions──▶ 服务商服务器
{model, messages, temperature...}
│ 模型逐 Token 生成
你的代码 ◀──完整 JSON 响应────────────────── ┘响应里除了正文,还有 usage(本次消耗的 Token 数)——每次都看一眼,它是成本核算的原始数据(第五节)。
二、核心参数:只需要真正掌握这四个
| 参数 | 作用 | 什么时候调 | 不调的默认行为 |
|---|---|---|---|
temperature | 随机性。0≈每次都一样,1+≈更发散 | 抽取/分类/代码 → 0~0.3;文案/头脑风暴 → 0.7~1.0 | 多数服务默认 1.0,做工程必须显式设置 |
max_tokens | 生成上限(防止失控账单) | 生产环境永远设置 | 不设可能生成到上限 |
stop | 停止序列 | 结构化场景防越界 | 无 |
top_p | 核采样的截断 | 与 temperature 二选一调,不要同时调 | 1.0 |
原理与更多采样细节见 00 篇 · 温度与 CoT。
三、流式输出与多轮会话
3.1 流式(打字机效果 + 首字延迟优化)
stream = client.chat.completions.create(
model="gpt-4o-mini", # [易变]
messages=[{"role": "user", "content": "解释一下 RAG 的九个环节"}],
stream=True, # 关键开关
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta: # 末尾可能为 None
print(delta, end="", flush=True)流式不减少总 Token、不省成本,但把首字延迟从"整段生成完"降到"第一个 Token 生成完"——对交互体验是质变。延迟优化全景见 05 篇。
3.2 多轮会话:模型没有记忆,记忆就是你把历史传回去
最常见的初学者误区是以为服务端记住了上下文。每一轮你都要把完整历史传过去:
class Conversation:
def __init__(self, system_prompt: str):
self.messages = [{"role": "system", "content": system_prompt}]
def ask(self, client, user_input: str) -> str:
self.messages.append({"role": "user", "content": user_input})
resp = client.chat.completions.create(
model="gpt-4o-mini", # [易变]
messages=self.messages,
temperature=0.3,
)
answer = resp.choices[0].message.content
self.messages.append({"role": "assistant", "content": answer})
return answer
conv = Conversation("你是一个严谨的技术助手。")
conv.ask(client, "Flutter 和 React Native 的渲染差异?")
conv.ask(client, "那性能问题一般出在哪?") # 模型能理解"那"指什么,因为历史在 messages 里上下文会越来越长,成本线性上涨。工程上必须做截断/压缩策略:
- 滑动窗口:只保留最近 N 轮 + system
- 摘要压缩:把更早的历史让模型自己总结成一段
- 硬上限:超过
max context直接报错,不要等到服务端报
上下文工程的方法论见 02 篇。
四、结构化输出:让模型输出能被程序解析
对话式输出是给人看的;程序需要 JSON。三档方案,可靠性递增:
4.1 档位一:提示词约束 + 代码校验(最通用,任何模型可用)
import json
from pydantic import BaseModel, ValidationError
class BookReview(BaseModel):
title: str
rating: int # 1~5
tags: list[str]
SYS = """从书评中抽取信息,严格输出 JSON,schema:
{"title": string, "rating": 1-5的整数, "tags": string[]}
不要输出任何 JSON 以外的内容。"""
def extract(review_text: str, client) -> BookReview:
resp = client.chat.completions.create(
model="gpt-4o-mini", # [易变]
messages=[
{"role": "system", "content": SYS},
{"role": "user", "content": review_text},
],
temperature=0, # 抽取任务要确定性
)
for _ in range(3): # 校验失败自动重试
try:
return BookReview.model_validate_json(resp.choices[0].message.content)
except ValidationError:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYS},
{"role": "user", "content": review_text},
{"role": "assistant", "content": resp.choices[0].message.content},
{"role": "user", "content": "上面的输出不符合 schema,重新严格按 schema 输出 JSON。"},
],
temperature=0,
)
raise RuntimeError("连续 3 次输出不合法,转人工处理")要点:temperature=0 + 显式 schema + "不要输出多余内容" + pydantic 校验 + 失败带错误上下文重试。这套骨架可以直接搬去生产。
4.2 档位二:API 级 JSON Mode / Schema 约束
主流服务普遍提供开关(OpenAI 系为 response_format),让服务端在解码层约束 JSON 合法性,比纯提示词可靠得多。注意两点:
json_object模式仍需要你在提示词里给出字段说明json_schema(严格模式)最可靠,但支持的字段类型有限、部分模型不支持 → 查当期文档[易变]
4.3 怎么选
| 场景 | 方案 |
|---|---|
| 内部工具、允许偶发重试 | 档位一(最通用) |
| 面向用户、失败影响体验 | 档位二 + 档位一兜底重试 |
| 抽取结果直接入库 | 档位二严格模式 + pydantic 双保险 |
五、Function Calling:让模型能"动手"
模型不会执行任何东西——它只会输出"我想调用 X 工具、参数是 Y",执行永远在你的代码里。这就是 03 篇工具调用 5 原则的代码化:
import json
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前天气", # description 写清楚,模型靠它决定何时调用
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如 北京"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
},
}]
def get_weather(city: str, unit: str = "celsius") -> dict:
# 你的真实实现:查数据库 / 调第三方 API……
return {"city": city, "temp_c": 26, "condition": "多云"}
messages = [{"role": "user", "content": "北京今天适合跑步吗?"}]
resp = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools, temperature=0,
)
# 第一步:模型决定调用工具(也可能不调,要判空)
msg = resp.choices[0].message
if msg.tool_calls:
messages.append(msg) # 助手的"调用意图"也要进历史
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = get_weather(**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False),
})
# 第二步:把工具结果交回模型,生成最终自然语言回答
final = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools, temperature=0,
)
print(final.choices[0].message.content)
else:
print(msg.content) # 模型认为不需要工具,直接回答两个必然踩的坑:
- 工具描述写得太含糊 → 模型该调不调、不该调乱调。
description要说清"什么时候用我、参数什么格式"。 - 忘记把
tool角色的结果传回第二轮 → 模型瞎编工具结果。工具执行结果必须回填messages。
多工具、循环执行、步数上限的完整 Agent 循环,见实操系列后续 Agent 篇(规划中)。
六、Token 计量与成本估算
6.1 先会数 Token
pip install tiktokenimport tiktoken
enc = tiktoken.get_encoding("cl100k_base") # GPT 系;国产模型用厂商自带计数器更准
n = len(enc.encode("解释一下 RAG 的九个环节"))
print(n) # 输入 Token 数(估算)粗算规则(中文):1 个汉字 ≈ 1~2 个 Token。精确值以响应里的 usage 为准。
6.2 会算钱(方法不变,单价查官方 [易变])
单次成本 = 输入 Token 数 × 输入单价 + 输出 Token 数 × 输出单价关键事实:输出单价通常是输入的 3~4 倍 → 控制生成长度比精简提示词更省钱。
月成本估算模板(把数填进去就能用):
日请求量 R × 平均输入 Token I × 输入单价 Pi
+ 日请求量 R × 平均输出 Token O × 输出单价 Po
= 日成本;× 30 = 月成本示例:R=10,000,I=2,000,O=500,输入 $0.15/百万,输出 $0.60/百万([易变] 仅示意量级):
10,000 × 2,000 × 0.15/10⁶ + 10,000 × 500 × 0.60/10⁶
= 3.0 + 3.0 = $6/天 ≈ $180/月6.3 生产环境必须做的三件事
- 每次调用记录
usage(时间、模型、输入/输出 Token、业务场景标签)→ 这是 05 篇成本监控的数据底座 - 设预算熔断:按天/按用户累计,超阈值降级或拒绝
- 缓存重复请求:相同输入直接返回缓存结果,重复率高的场景能省 30%+
降本手段全景见 05 篇(按性价比排序)。
七、错误处理与重试:生产与 Demo 的分水岭
四类必然遇到的错误与对策:
| 错误 | 典型原因 | 对策 |
|---|---|---|
401 / 403 | 密钥错、欠费、无权限 | 不重试,立即告警 |
429 | 限流 | 指数退避重试(见下) |
5xx / 超时 | 服务端抖动 | 重试 1~2 次,加超时上限 |
400 上下文超限 | 历史太长 | 截断/压缩历史后重试一次 |
import time
def call_with_retry(fn, max_retries=3, base_delay=1.0):
"""指数退避重试:1s → 2s → 4s。只对可重试错误生效。"""
for attempt in range(max_retries):
try:
return fn()
except Exception as e:
status = getattr(e, "status_code", None)
if status in (401, 403):
raise # 认证类错误不重试
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
time.sleep(delay)生产环境直接用 SDK 自带的重试参数 + 超时:
client = OpenAI(api_key=..., max_retries=3, timeout=30.0)更完整的高可用设计(多模型降级、网关、熔断)见 05 篇网关与稳定性。
八、动手任务
按 10 篇路线 B 阶段一的要求,本篇读完后完成:
- [ ] 任务 1:跑通
hello_llm.py,打印usage,手动算一次这次调用花了多少钱 - [ ] 任务 2:把
Conversation类加上"最多保留最近 6 轮"的滑动窗口,并打印每轮结束后的累计 Token 数 - [ ] 任务 3:用档位一的结构化输出,写一个"把任意商品描述抽成
{名称, 价格区间, 三个卖点}"的函数,故意喂 10 条脏数据,统计校验重试的触发率 - [ ] 任务 4(进阶):给天气工具再加一个
get_time工具,构造一个"需要连续调用两个工具"的问题(如"现在适合跑步吗"需要时间+天气),体验多轮工具调用 - [ ] 自检:能不看代码说出——三种角色分别是什么?为什么多轮要传完整历史?工具调用的结果怎么回到模型手里?
延伸方向
- 输入知识库文档再回答 → 实操 02 RAG 实战
- 不想花钱调 API,本地跑一个开源模型 → 实操 03 模型本地部署与推理优化
- 提示词怎么写得让输出更稳 → 02 篇 · 上下文工程
- 请求量上来后的评测、监控、网关 → 05 篇 · 工程化与 LLMOps