返回学习路线/阶段 01 · 认识 Agent,打好基础
LESSON 03 / 18

理解 LLM 与 API 请求

看懂消息、Token 和 HTTP,从离线模拟走到第一次真实模型调用。

40 分钟 · 含动手练习入门LLMHTTPToken

学完这一课,你将能够

  • 辨认请求和响应的组成
  • 配置并运行真实模型客户端
  • 为远程请求设置超时与错误分支

学习目标

  • 读懂一次请求的地址、方法、请求头与请求体。
  • 区分模型、应用状态和上下文窗口。
  • 为远程调用设计超时、错误处理和预算。

前置要求:能运行上一课的 Python 脚本。 先运行离线模拟器,再按本课后半部分接入真实模型。 真实请求需要你自己的服务账户与凭据,并可能按所选服务的规则产生 API 费用。

一次模型请求是什么

应用把输入编码成服务要求的格式,发送到指定地址,服务返回生成结果或错误。 不同提供商的接口字段、认证方式和响应结构不同,不能把一种格式当成通用协议。 本课程会先把它们封装在自己的适配函数里。

部分作用常见例子
URL请求发到哪里服务提供的推理端点
Method执行哪类操作POST
Headers元信息与认证Content-Type、认证头
Body业务输入消息、模型选择、输出配置
StatusHTTP 处理结果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 时应检查该服务的预算规则。

本例设置单次网络超时,不自动重试;完整应用还应加总任务时限和费用预算。 先用一条短请求确认配置,再进入真实工具调用实验,避免一开始就运行无上限循环。 课程代码经过离线协议测试;真实响应、账户权限和实际费用需要在你的环境中验证。

让这一课,真正成为你的收获

完成练习后标记完成,也可以随时回来复习。

笔记与进度保存在当前浏览器,无需登录

AgentStudy · Learn by building.以理解为起点,以作品为答案