01 大模型 API 的真相

QuibblerAgentQuibblerAgent 2026-11-13 约 17 分钟 12 次阅读

SDK 三行代码就能调起大模型,这层便利同时也挡住了视线:请求体里有什么、响应结构长什么样、错误时服务器说了什么,很多写过一阵子大模型应用的人也答不上来。本文是「从零手写 Agent」系列第 1 篇,丢开 SDK,用 httpx 裸发一次请求,把调用大模型的真相摆在明面上:它就是一次 HTTPS POST,输入输出都是 JSON。第 0 篇路线图已把零件备齐,本篇开始拆第一站。

本文按"请求体解剖 → 响应体解剖 → 实战踩坑 → 总结"组织,文末给出把这套认知迁移到任意厂商 API 的方法。

1、概述:调用即 HTTP

所有大模型 API 剥掉外壳后是同一件事:客户端向固定地址 POST 一段 JSON,服务器在响应里返回一段 JSON。以 Anthropic 风格的接口为例,入口固定为 /v1/messages,认证靠请求头携带的 key。

请求的骨架如下:

import httpx

headers = {
    "x-api-key": API_KEY,               # 身份认证:服务器靠它识别你是谁
    "anthropic-version": "2023-06-01",  # API 版本号,写死这个日期即可
    "content-type": "application/json",
}
response = httpx.post(url, headers=headers, json=request_body, timeout=60)

解读:SDK 帮你做的第一件事就是拼这三个请求头并发送。所谓 anthropic、openai 这些官方库,本质是这段 HTTP 代码的包装——SDK 没有魔法,魔法在服务器上。各字段中 x-api-key 错了会得到 401,版本号缺失会得到 400,读错误响应体是排障的第一动作。

把骨架补全成能直接运行的最小脚本,依赖只有 httpx 与 python-dotenv:

import os
import httpx
from dotenv import load_dotenv

load_dotenv(override=True)   # 防预设变量偷换端点,第 4 节展开

body = {
    "model": "glm-4.6",
    "max_tokens": 128,
    "messages": [{"role": "user", "content": "用一句话解释什么是 HTTP API"}],
}
response = httpx.post(
    os.environ["ANTHROPIC_BASE_URL"] + "/v1/messages",
    headers={
        "x-api-key": os.environ["ANTHROPIC_API_KEY"],
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    },
    json=body,
    timeout=60,
)
for block in response.json()["content"]:
    print(block["type"], "->", block.get("text", "")[:50])

实测记录(一次真实运行)的输出如下:

thinking -> The user asks in Chinese what an HTTP API is...
text -> HTTP API 是通过 HTTP 协议暴露能力的一组接口,客

解读:全程没有 SDK 参与——一个 HTTP 库加十几行代码就完成了"调大模型"这件事。输出里先到的是思考块后到的是正文块,这个细节第 3 节解释。httpx 的用法可查 官方文档。

顺带把行业里的两大家族认个脸熟:Anthropic 风格走 /v1/messages,认证用 x-api-key 请求头;OpenAI 风格走 /v1/chat/completions,认证用 Authorization: Bearer 请求头,system 也不是顶层字段而是 messages 里的一条。两套协议的 messages 数组几乎同形,学会一套另一套半天就能上手——这是"先学协议再学 SDK"的复利所在,本系列用 Anthropic 风格讲,结论两边通用。厂商名单还在变长,但都逃不出这两族:选型时先问"哪族协议",比问"哪家 SDK"离本质更近。

2、请求体解剖

请求体是 JSON,描述"这次对话"的完整信息。最小可用的请求体与各字段职责如下:

request_body = {
    "model": "glm-4.6",                   # 用哪个模型,厂商不同名字不同
    "max_tokens": 1024,                   # 本次回答最多生成多少 token
    "system": "你是简洁的技术助教。",      # 系统提示词:模型的人设与规则
    "messages": [
        {"role": "user", "content": "用一句话解释什么是 HTTP API"},
    ],
}

各字段的职责如下:

字段 必填 作用
model 是 模型名,如 glm-4.6、claude-opus-5-5
max_tokens 是 输出预算,1 token 约等于 0.5 到 1.5 个汉字
messages 是 对话历史,每条含 role 与 content
system 否 人设与规则,优先级高于用户消息

这里有一个贯穿全部后续篇目的关键认知:服务器不保存任何对话状态。messages 就是这次请求携带的全部历史,模型只知道你发来的内容。所谓"多轮对话的记忆",是客户端每次把历史重新发一遍——这是系列第 2 篇的主题。

2.1、messages 数组

messages 是唯一承载对话的字段,规则不多但条条要命。角色在请求里只出现两种:user 与 assistant,system 走顶层字段不进数组;首条消息必须是 user;content 既可以是纯字符串,也可以是内容块列表——图文混排、工具块都靠块列表表达。一份两轮的 messages 长这样:

messages = [
    {"role": "user", "content": "1 加 2 等于几"},
    {"role": "assistant", "content": "1 加 2 等于 3。"},
    {"role": "user", "content": "再乘 10 呢"},
]

解读:第三条提问没有重复前面的算式,它默认模型读得到前两条。这份默认在服务器端并无对应物——三条消息每次请求都被完整重发,模型才"记得"。多轮记忆的全部机制就藏在这个数组里,第 2 篇专门拆它。

块列表形态提前举个例:让模型看一张图,content 就是一个数组,里面放一个 image 块加一个 text 块。第 4 篇的工具调用块、第 2 篇的工具结果块,都会以同样的方式进出这个数组——把 messages 当成"一切信息的容器"来理解,后面再遇到新块类型就不会慌。

2.2、常用可选参数

必填四要素之外还有几个高频可选参数,各端点的支持程度略有差异。常用项如下:

参数 类型 作用
temperature 数值 采样随机性,越低越稳定
stream 布尔 开启流式推送,第 3 篇主题
stop_sequences 字符串列表 命中任一串即停笔

收束一句:可选参数都服务于"控制输出",不确定时先不传,用默认值跑通再调。同名字段在不同厂商文档里的取值范围偶有出入,以所用端点的文档为准。

以 temperature 为例给个手感:抽取、分类、固定格式输出这类"要稳"的任务,设 0 到 0.3;创作、起名这类"要活"的任务再往上调;默认值 1 附近适合聊天,工程任务别偷懒用默认。顺手给 stop_sequences 一个场景:让模型输出到某个分隔符就停笔,省下它后面发散写的篇幅。

3、响应体解剖

响应同样是 JSON,三块信息各司其职。一次真实响应的骨架如下:

{
  "content": [
    {"type": "thinking", "thinking": "The user asks in Chinese..."},
    {"type": "text", "text": "HTTP API 是一种通过 HTTP 协议……"}
  ],
  "stop_reason": "end_turn",
  "usage": {"input_tokens": 34, "output_tokens": 215}
}

content 是内容块列表而非字符串,因为模型的回答由多种块拼成:上例来自一个推理模型,先返回思考块再返回正文块。列表结构为工具调用块预留了位置——模型"想用某个工具"时,同样以块的形式出现在这里,那是系列第 4 篇的大门。

响应里还有些次要字段扫一眼即可:id 是这次响应的编号,排障时拿它对日志;model 回显实际服务的模型,部分网关会做模型路由,这个字段是核对"你付的钱买了哪个模型"的依据;role 恒为 assistant。stop_reason 除表中三值外,还有命中自定义停止串的 stop_sequence 等少见值,遇到了查文档即可,不影响主流程。把 id 与 usage 一起打进日志,是成本最低的观测手段——第 2 篇的账单分析与第 10 篇的评估都从这里取数。

stop_reason 说明模型为什么停笔,有三个高频值。各值含义如下:

取值 含义
end_turn 说完了,正常结束
max_tokens 输出预算用完,被强行截断
tool_use 想调用工具,等待客户端执行

usage 是计费单:input_tokens 是你发过去的内容消耗,output_tokens 是模型生成的部分,费用等于输入单价乘输入量加输出单价乘输出量。一个容易被忽略的细节:推理模型的思考块也计入输出账单,上面这次"一句话回答"实付 215 个输出 token,其中大头是思考。

3.1、usage 与计费算术

计费公式展开是小学算术,但每个因子都值得盯紧。一个可复用的算账函数如下:

PRICE_IN = 2.0 / 1e6    # 每百万输入 token 单价,按你的牌价改
PRICE_OUT = 8.0 / 1e6   # 输出普遍贵数倍,此处以四倍为例

def cost(usage):
    return (usage["input_tokens"] * PRICE_IN
            + usage["output_tokens"] * PRICE_OUT)

usage = {"input_tokens": 34, "output_tokens": 215}
print(cost(usage))   # 单次调用成本,单位随你的单价

解读:换上实际单价即可当成本计算器用。两个工程直觉由此而来:其一,输出 token 普遍比输入贵数倍,"让模型少说废话"比"让用户少打字"省钱得多;其二,把每次响应的 usage 落库,账单对不上时才有得回查。还有一个口径要记牢:max_tokens 截断时输出照常计费,但内容是被拦腰砍断的——计费与完整是两回事,别等用户报"答案没头没尾"才发现。部分端点的 usage 还会返回缓存命中等扩展字段,第 2 篇实测它的作用。

4、实战踩坑实录

裸写 HTTP 的过程会撞上 SDK 帮你挡掉的问题,下面三个坑都来自真实排障记录。

第一是环境变量覆盖:load_dotenv() 默认不覆盖已存在的同名环境变量,而机器上可能预设有指向其他地址的 ANTHROPIC_BASE_URL,导致请求悄悄发去了别处。修复是 load_dotenv(override=True),调试口诀是把实际用到的值打印出来,别假设。

第二是 Windows 编码:中文 Windows 控制台默认 GBK,打印特殊符号会抛 UnicodeDecodeError,需要在脚本开头执行 sys.stdout.reconfigure(encoding="utf-8")。

第三是企业网络:办公机访问外网通常必须走公司代理,直连会卡死。用 curl 逐个测试目标域名的连通性,是定位这类问题最快的路径;pip 的代理可以写入用户级 pip.ini 一劳永逸。三个坑的共同教训是:读懂错误信息再动手,比重启重试快十倍。

4.1、错误响应体解剖

三个坑都发生在请求发出前后,还有一类信息新手最容易整段扔掉——错误响应体。服务器报错时返回的同样是结构化 JSON,一次 401 的响应体如下:

{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "invalid x-api-key"
  }
}

解读:外层 type 标明这是错误响应,error.type 是给程序看的类别,message 是给人看的原因。实测记录:把 key 故意改错一位,得到 401 与上体;删掉 anthropic-version 头,得到 400 与 invalid_request_error,提示缺少版本号。把这些信息打印出来再动手,只要三行:

if response.status_code != 200:
    err = response.json().get("error", {})
    print(response.status_code, err.get("type"), err.get("message"))
    raise SystemExit(1)   # 先读体,再决定重试还是修代码

常见状态码与第一动作对照如下:

状态码 含义 第一动作
401 认证失败 核对 key 与请求头
400 请求不合法 按报错字段改参数
429 触发限流 按响应头等待后重试
529 服务过载 稍等再发

重试只对 429 与 5xx 明智,400 重试一百次也还是 400——错误类别决定动作,这是排障的基本功。

再补一层重试纪律:自动重试要配指数退避——第一次等 1 秒、第二次等 2 秒、第三次等 4 秒——并在重试前打印状态码与 error.type,避免把 400 当 429 无限重试。官方 SDK 默认对 429 与 5xx 自动重试两次,裸写 HTTP 时这份体贴需要自己补上,第 5 篇的循环里会用到。

5、总结

调用大模型是一次普通的 HTTP 请求:请求体四要素是 model、max_tokens、messages、system;响应三件宝是 content 块列表、stop_reason、usage;模型没有状态,历史全靠客户端携带。对于后续学习,建议用任意兼容 Anthropic 协议的端点亲手发一次裸请求,并故意制造一次 401 和一次 max_tokens 截断,观察响应差异。把这套认知迁移到任意厂商 API,只需对照三件事:端点与认证头、必填字段名、错误体结构——messages 的语义各家族高度一致,字段名对齐就能跑。下一篇让 messages 列表长起来,看看多轮对话的记忆到底怎么实现。API 细节可查 Anthropic Messages 文档与智谱开放平台。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

144