理解 LLM 与 API 请求
看懂消息、Token 和 HTTP,从离线模拟走到第一次真实模型调用。
学完这一课,你将能够
- 辨认请求和响应的组成
- 配置并运行真实模型客户端
- 为远程请求设置超时与错误分支
学习目标
- 读懂一次请求的地址、方法、请求头与请求体。
- 区分模型、应用状态和上下文窗口。
- 为远程调用设计超时、错误处理和预算。
前置要求:能运行上一课的 Python 脚本。 先运行离线模拟器,再按本课后半部分接入真实模型。 真实请求需要你自己的服务账户与凭据,并可能按所选服务的规则产生 API 费用。
一次模型请求是什么
应用把输入编码成服务要求的格式,发送到指定地址,服务返回生成结果或错误。 不同提供商的接口字段、认证方式和响应结构不同,不能把一种格式当成通用协议。 本课程会先把它们封装在自己的适配函数里。
| 部分 | 作用 | 常见例子 |
|---|---|---|
| URL | 请求发到哪里 | 服务提供的推理端点 |
| Method | 执行哪类操作 | POST |
| Headers | 元信息与认证 | Content-Type、认证头 |
| Body | 业务输入 | 消息、模型选择、输出配置 |
| Status | HTTP 处理结果 | 200、401、429、500 |
HTTP 200 只说明协议层请求成功。 返回的业务内容仍然需要检查,例如是否缺少字段、是否触发拒答、是否达到输出限制。
模型不等于一段永不丢失的对话
模型每次能看到哪些信息,取决于本次请求和服务的会话机制。 自己的应用仍然需要清楚保存什么、发送什么、删除什么。 聊天界面里完整可见的历史,并不意味着模型每次都收到了完整历史。
上下文窗口限制了单次可处理的信息量。 Token 是模型处理文本的单位,和字符数、单词数并不一一对应。 中文、代码和英文的切分方式可能不同,应使用提供商匹配的计数工具或服务返回的用量。
输入、工具描述、检索资料和输出都可能占用预算。 输出预算不能无限增加:响应越长,通常延迟和费用越高。 温度等采样参数影响输出变化,但不能替代事实核验或格式校验。
先做一个本地适配层
保存为 model_adapter.py,运行 python model_adapter.py:
import json
def generate(messages, max_output_chars=80):
"""离线教学替身,不调用模型,也不计算真实 Token。"""
if not isinstance(messages, list) or not messages:
raise ValueError("messages 必须是非空列表")
for message in messages:
if not isinstance(message, dict):
raise ValueError("每条消息必须是字典")
if not isinstance(message.get("content"), str):
raise ValueError("content 必须是字符串")
question = messages[-1]["content"]
answer = f"模拟收到:{question}"
return {
"text": answer[:max_output_chars],
"finish_reason": "limit" if len(answer) > max_output_chars else "stop",
"mode": "offline_fixture",
}
request = [{"role": "user", "content": "用一句话介绍 Agent"}]
wire_text = json.dumps(request, ensure_ascii=False)
response = generate(json.loads(wire_text))
print(json.dumps(response, ensure_ascii=False, indent=2))这里的字符上限仅用于演示截断分支,不能当作 Token 估计。
generate 是你的应用契约。以后替换内部实现时,业务代码仍然只读取 text 和结束原因。
真实接入时还可以返回请求 ID、实际 Token 用量和提供商名称。
真实接入时需要补齐什么
下面是流程说明,不是可以直接运行的 SDK 代码:
从环境变量读取凭据
按所选服务当前文档构造请求
设置连接与读取超时
发送请求
检查 HTTP 状态与服务错误
提取内容、结束原因和真实用量
转换为应用统一的返回结构凭据只应留在服务端环境,不要硬编码到前端或提交到代码仓库。 没有凭据时应明确提示未配置,不能展示一条模拟回答并声称来自真实模型。
哪些失败可以重试
认证失败通常需要修复配置,立即重复请求没有帮助。 速率限制可以按服务返回的等待建议重试,并增加次数上限和退避。 网络超时意味着结果未知;涉及付费或外部写入的操作,先检查是否已执行。 把异常分成配置错误、暂时错误和内容错误,比统一返回“出错了”更好排查。
练习与验收
把字符上限改成 6,观察结束原因从 stop 变成 limit。
分别传入空列表和缺少 content 的消息,检查错误是否清楚。
画出“你的应用 → 适配层 → 服务”的边界。
验收时能解释:本地替身验证了数据流,但没有验证真实模型质量、网络稳定性或 Token 成本。
常见误区
“接口通了就完成了”:还需要验证内容、结束原因与调用预算。
“字符数就是 Token 数”:这里只是演示长度限制,实际用量以对应模型的计数为准。
延伸阅读
动手接入真实模型
以下客户端使用 兼容 Chat Completions 格式的非流式接口,不是所有模型服务的通用客户端。
消息、认证和工具字段参考 DeepSeek 官方首次调用文档。
服务应支持 POST /chat/completions、Bearer 认证、messages 与 choices 响应。
其他接口格式需要独立适配;不要仅更换 URL 就假设兼容。
准备三个环境变量:
| 变量 | 应填写的内容 |
|---|---|
LLM_BASE_URL | 服务文档提供的 API 根地址,例如 https://api.deepseek.com |
LLM_API_KEY | 你自己创建的 API Key |
LLM_MODEL | 账户已开通且当前支持该接口的模型名称 |
模型名称以服务控制台和最新文档为准,不复制已经下线的旧名称。
在终端或开发环境的安全配置中设置变量;普通 Python 不会自动加载 .env 文件。
在网页应用中,Key 只保存在服务端,不能放入浏览器代码、公开环境变量或版本库。
账户没有额度或服务未开通时,按错误提示处理,不会自动回退到模拟回答。
把以下代码保存为 llm_client.py,它也会被第三模块的真实 Agent 示例复用。
import json
import os
import socket
import urllib.error
import urllib.parse
import urllib.request
def chat(messages, tools=None):
config = {name: os.environ.get(name, "").strip() for name in (
"LLM_BASE_URL", "LLM_API_KEY", "LLM_MODEL"
)}
missing = [name for name, value in config.items() if not value]
if missing:
raise RuntimeError("缺少环境变量:" + ", ".join(missing))
base = config["LLM_BASE_URL"].rstrip("/")
if urllib.parse.urlparse(base).scheme != "https":
raise RuntimeError("真实服务必须使用 HTTPS 地址")
payload = {
"model": config["LLM_MODEL"], "messages": messages,
"stream": False, "max_tokens": 1200,
}
if tools is not None:
payload["tools"] = tools
request = urllib.request.Request(
base + "/chat/completions",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + config["LLM_API_KEY"],
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
result = json.load(response)
except urllib.error.HTTPError as error:
hints = {401: "检查凭据", 402: "检查账户额度", 403: "检查权限", 429: "请求过快或额度受限"}
raise RuntimeError(f"HTTP {error.code}:{hints.get(error.code, '请检查服务状态和请求配置')}") from None
except (urllib.error.URLError, TimeoutError, socket.timeout):
raise RuntimeError("网络失败或请求超时;本例不会自动重试") from None
except (json.JSONDecodeError, UnicodeDecodeError):
raise RuntimeError("服务未返回合法 JSON") from None
if not isinstance(result, dict):
raise RuntimeError("响应根节点必须是对象")
choices = result.get("choices")
if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict):
raise RuntimeError("响应缺少有效 choices")
choice = choices[0]
if not isinstance(choice.get("message"), dict):
raise RuntimeError("响应缺少 assistant 消息")
if choice.get("finish_reason") not in ("stop", "tool_calls"):
raise RuntimeError("生成未正常完成:" + str(choice.get("finish_reason")))
return result
if __name__ == "__main__":
try:
result = chat([{"role": "user", "content": "用一句中文说明什么是 AI Agent。"}])
print(result["choices"][0]["message"].get("content"))
print("服务返回的实际用量:", result.get("usage", "服务未提供"))
except RuntimeError as error:
raise SystemExit(str(error))配置完成后执行 python llm_client.py。
成功时应看到真实模型回答和服务提供的用量;未配置时只显示缺少的变量名。
max_tokens 是这里限定的兼容接口字段,若服务不支持,应按对应文档调整适配层。
对于默认启用思考模式的模型,输出预算也可能覆盖推理内容;出现 length 时应检查该服务的预算规则。
本例设置单次网络超时,不自动重试;完整应用还应加总任务时限和费用预算。 先用一条短请求确认配置,再进入真实工具调用实验,避免一开始就运行无上限循环。 课程代码经过离线协议测试;真实响应、账户权限和实际费用需要在你的环境中验证。
让这一课,真正成为你的收获
完成练习后标记完成,也可以随时回来复习。
笔记与进度保存在当前浏览器,无需登录