我第一次调用大模型 API:先用 curl 跑通最小闭环,再把调用封装成一个可复用 client

我第一次调用大模型 API:先用 curl 跑通最小闭环,再把调用封装成一个可复用 client

上一篇我先把“AI 应用开发工程师在做什么”画了一张地图(避免我一上来就学偏)。这一篇我终于要动手做第一件真正“像工程”的事:把大模型 API 调用跑通

我给自己定了一个很小但很硬的目标:

> 10 分钟内,在命令行里得到一次有效的模型回复; > 然后把这次调用变成一个“未来我所有 demo 都能复用”的最小 client(集中管理 key、超时、错误处理和日志字段)。

我现在越来越相信:入门阶段最怕的不是“我不会写 prompt”,而是“我连调用链路都没跑通”,一切概念都会变成玄学。

---

0) 我先把调用链路拆成 4 个零件(否则我会不知道卡在哪里)

我把一次 LLM 调用拆成 4 个零件(这对排错特别有用):

1. 凭证:API key 是否正确、是否有权限 2. 请求:URL / headers / body 的格式是否对 3. 响应:是否拿到了可用输出(不只是 HTTP 200) 4. 工程化:超时、重试、日志、可观测性有没有最小形态

我这篇会按这个顺序走,先把“能跑通”解决,再谈“跑得稳”。

---

1) 10 分钟 `curl` 小实验:先让模型回一句话

我用 curl 的原因很简单:它绕开了 SDK、框架、依赖,最接近“裸 API”。只要 curl 能通,后面 Python/Node/框架怎么换都不慌。

Step 1:准备环境变量(我不把 key 写进命令历史)

export ANTHROPIC_API_KEY="你的_key_放这里"
export ANTHROPIC_BASE_URL="https://api.minimaxi.com/anthropic"

这次我用的是 MiniMax 的 Anthropic 兼容接口。这里的 ANTHROPIC_BASE_URL 只是“服务根地址”,真正发请求时还要拼到具体 endpoint。

如果我只是在当前终端里临时测试,上面的 export 就够了。但我现在是通过 SSH 登录机器,默认 shell 是 zsh,并且装了 oh-my-zsh,所以更适合把这两个变量写到远程机器的 ~/.zshrc 里。这样每次重新登录后都能直接使用,不需要手动再 export 一遍。

nano ~/.zshrc

在文件末尾加上:

export ANTHROPIC_API_KEY="你的真实 key"
export ANTHROPIC_BASE_URL="https://api.minimaxi.com/anthropic"

保存后让配置立即生效:

source ~/.zshrc

我一般不直接 echo $ANTHROPIC_API_KEY,避免把 key 打到屏幕或日志里。可以只检查长度:

echo $ANTHROPIC_BASE_URL
echo ${#ANTHROPIC_API_KEY}

如果是 SSH 场景,要确认自己改的是远程机器的 ~/.zshrc,不是本机的。登录远程机器后可以先看一下当前 shell:

echo $SHELL

看到类似 /bin/zsh/usr/bin/zsh,就说明把变量写进 ~/.zshrc 这个方向是对的。

还有一个 macOS 上常见的小坑:系统里可能已经有 Python,但命令叫 python3,不是 python。如果执行:

python basic_demo.py

看到:

zsh: command not found: python

先检查:

which python3
python3 --version

能看到类似 /opt/homebrew/bin/python3Python 3.x.x,就直接用:

python3 basic_demo.py

如果确实想让 python 也能用,可以在 ~/.zshrc 里加一行别名:

alias python=python3

然后执行 source ~/.zshrc。不过对入门练习来说,先直接写 python3 更清楚。

Step 2:发起一次最小请求(先把 endpoint、header、body 对齐)

我一开始直接请求了 https://api.minimaxi.com/anthropic,结果返回 404 page not found。原因很直接:它不是完整接口地址。Anthropic 兼容接口的消息调用路径是 /v1/messages,所以实际请求要发到:

https://api.minimaxi.com/anthropic/v1/messages

最小可用请求是这样:

curl --request POST \
  --url "$ANTHROPIC_BASE_URL/v1/messages" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $ANTHROPIC_API_KEY" \
  -d '{
    "model": "MiniMax-M2.7",
    "max_tokens": 2048,
    "messages": [
      {
        "role": "user",
        "content": "用一句话解释:AI 应用开发工程师主要在做什么?"
      }
    ]
  }'

这里有四个字段特别容易写错:

位置 这次实际写法 说明
URL $ANTHROPIC_BASE_URL/v1/messages base URL 后面还要有 /v1/messages
Header X-Api-Key: $ANTHROPIC_API_KEY 不是所有 provider 都用 Authorization: Bearer ...
Body messages: [{ role, content }] Anthropic 兼容接口不是 input 字段
Output budget max_tokens: 2048 太小可能只返回 thinking,还没生成最终 text

如果不确定模型名是否可用,可以先查模型列表:

curl --request GET \
  --url "$ANTHROPIC_BASE_URL/v1/models" \
  -H "X-Api-Key: $ANTHROPIC_API_KEY"

Step 3:我怎么判断“真的跑通了”

这次跑通后返回的是一个 JSON,结构大概是这样:

{
  "id": "06624edd66565b2aad903284ff269dd3",
  "type": "message",
  "role": "assistant",
  "model": "MiniMax-M2.7",
  "content": [
    {
      "thinking": "...",
      "type": "thinking"
    },
    {
      "text": "AI 应用开发工程师主要负责把机器学习或深度学习模型落地到实际产品中,包括模型选型、数据处理与特征工程、系统集成、性能调优以及上线后的监控与维护等工作。",
      "type": "text"
    }
  ]
}

真正给用户看的内容不是整个 JSON,而是 content 数组里 typetext 的那段。这个点很重要:后面封装 client 时,不能只把原始响应直接丢给页面。

我给自己设了三个“通过条件”,避免出现“HTTP 200 但我其实没拿到可用结果”的自欺:

  • 返回 JSON 能被解析(不是 HTML / 报错页)
  • content 里确实能提取出 type: "text" 的可读文本
  • 我能记录下这次请求的最小日志字段(见下文表格)

---

2) 我最先踩到的几类错误(以及我怎么快速定位)

这部分是我觉得最值钱的:新手卡住时,大概率不是“不会写 prompt”,而是这些基础错误。

现象(最常见) 可能原因 我怎么定位 我怎么修
404 page not found 只请求了 base URL,没有拼到具体 endpoint 看 URL 是否停在 /anthropic,而不是 /anthropic/v1/messages 把请求地址改成 $ANTHROPIC_BASE_URL/v1/messages
ping: cannot resolve https://... ping 只能 ping 域名/IP,不能 ping 带协议和路径的 URL https://api.minimaxi.com/anthropic 拆开看,主机名是 api.minimaxi.com ping api.minimaxi.comcurl -I https://api.minimaxi.com 做连通性检查
401 Unauthorized key 错了 / 过期 / header 写法不对 先检查环境变量是否真生效(echo $ANTHROPIC_API_KEY),再看是否带了 X-Api-Key header 重新生成 key;确认服务端/账号权限;别把空字符串当 key
400 Bad Request JSON body 格式错误 / 字段名不对 先把 -d 的 JSON 复制出来用 JSON 校验器过一遍;再对照 API reference 从最小请求体开始增量加字段,不要一次堆满
Python 脚本抛出 no text content in response HTTP 请求已经成功,但 max_tokens 太小,输出预算被 thinking 用完;或响应结构和解析逻辑不匹配 看脚本日志里的 status=200,再完整打印/保存原始 JSON,确认 content 里到底有哪些 type 增大 max_tokens,比如从 256 调到 2048;同时让解析逻辑只提取 type: "text"
429 Too Many Requests 限流 / 余额/配额问题 看响应体里的错误信息;看是否短时间并发太高 加退避重试(backoff);做缓存;降并发
5xx 服务端暂时异常 记录 request id(如果有)与时间点 做重试(带上限);避免无脑死循环
“返回了但内容很奇怪” 输入不清晰 / 输出解析方式不对 先把原始响应完整落盘(不要只打印一段文本) 给输入增加约束;下一篇我会专门写“结构化输出”

我对自己的要求是:任何错误都必须“可复现 + 可记录”。否则我下次一定会再踩一遍。

---

3) 把调用收敛成一个最小 client(我不想把 API 调用散落在业务里)

我现在的做法是:无论写什么 demo,我都先写一个 client 模块,把“调用 API 的细节”集中起来。这样未来要换 provider、换模型、换接口路径时,不需要全项目搜改。

下面我用 Python 写一个几乎没有依赖的最小封装(只用标准库)。你也可以用 Node/Go/Java 做同样的事,思路一致。

目标:这个最小 client 至少做到 5 件事

结构化清单(我贴在 TODO 里对照):

  • [ ] 从环境变量读取 ANTHROPIC_API_KEY,缺失就明确报错
  • [ ] 统一设置超时(避免卡死)
  • [ ] 对非 2xx 的响应抛出“可读错误”(包含状态码与响应体片段)
  • [ ] 输出最小日志字段(请求耗时、状态码、是否成功)
  • [ ] 从原始 JSON 里提取最终文本,而不是把整段响应直接交给业务层

Python 最小示例:Anthropic 兼容 `/v1/messages` 的“能用版”

import json
import os
import time
import urllib.request
import urllib.error


def extract_text(result: dict) -> str:
    chunks = []
    for item in result.get("content", []):
        if item.get("type") == "text" and item.get("text"):
            chunks.append(item["text"])
    return "\n".join(chunks)


def call_llm(input_text: str, model: str = "MiniMax-M2.7", timeout_s: int = 30) -> str:
    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        raise RuntimeError("Missing ANTHROPIC_API_KEY in environment variables")

    base_url = os.environ.get("ANTHROPIC_BASE_URL", "https://api.minimaxi.com/anthropic")
    url = f"{base_url.rstrip('/')}/v1/messages"
    payload = json.dumps(
        {
            "model": model,
            "max_tokens": 2048,
            "messages": [{"role": "user", "content": input_text}],
        }
    ).encode("utf-8")

    req = urllib.request.Request(
        url,
        data=payload,
        headers={
            "X-Api-Key": api_key,
            "Content-Type": "application/json",
        },
        method="POST",
    )

    started = time.time()
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            body = resp.read().decode("utf-8")
            elapsed_ms = int((time.time() - started) * 1000)
            print(f"[llm] status={resp.status} elapsed_ms={elapsed_ms}")
            result = json.loads(body)
            text = extract_text(result)
            if not text:
                raise RuntimeError(f"[llm] no text content in response: {body[:500]}")
            return text
    except urllib.error.HTTPError as e:
        elapsed_ms = int((time.time() - started) * 1000)
        err_body = e.read().decode("utf-8", errors="replace")
        raise RuntimeError(
            f"[llm] status={e.code} elapsed_ms={elapsed_ms} body={err_body[:500]}"
        ) from e


if __name__ == "__main__":
    text = call_llm("用三条 bullet 总结:AI 应用开发入门最应该先学什么?")
    print(text)

这个脚本里还有一个细节:status=200 只说明 HTTP 调用成功,不代表 extract_text(result) 一定能取到最终文本。第一次我把 max_tokens 设成 256,终端里先打印了:

[llm] status=200 elapsed_ms=7423

但随后 Python 脚本在这一行抛错:

raise RuntimeError(f"[llm] no text content in response: {body[:500]}")

原因是返回的 content 里只有 type: "thinking",还没有 type: "text"。把 max_tokens 调到 2048 后,同样的脚本就能正常打印最终回答:

[llm] status=200 elapsed_ms=21771

- **掌握 Python 编程及数据科学库**(NumPy、Pandas、Matplotlib),能够快速实现数据清洗、可视化和脚本化开发。
- **弄懂机器学习/深度学习核心概念**(监督/非监督学习、损失函数、梯度下降、常用模型如线性回归、决策树、CNN、RNN),为后续建模奠定理论基础。
- **学会主流框架并完成端到端项目**(TensorFlow / PyTorch),从数据预处理、模型训练、调参到模型部署,积累完整的实战经验。

这个错误不是接口没通,而是 client 的文本提取逻辑没有拿到它期待的输出字段。调大 max_tokens 后,响应里出现了 type: "text"extract_text(result) 才能把最终回答取出来。

我把它写得“朴素”,是因为我现在在入门期要的不是优雅框架,而是:

  • 我能控制每一行发生了什么(便于 debug)
  • 未来我想换 SDK/框架,也有一套自己的“最低标准”
  • 业务代码拿到的是最终文本,不用知道 provider 返回了几层 JSON

---

4) 我给这个 client 预留的“升级路线”(下一篇、下下篇会用到)

跑通之后,我给自己列了一张“升级路线图”,避免我又去四处乱学。

升级点 为什么要做 我打算放到哪一期
流式输出(stream) 体验上差别巨大;也更贴近真实产品 入门后半(做最小聊天应用时)
结构化输出(schema) 让输出变成“可依赖接口”,更容易接 UI/落库/做回归 入门末尾 / 基础期开头
重试与退避 429/5xx 必然会遇到,不做就是不稳定 入门期就加最小版
tracing / request id 多步工作流时排错必备 第二期基础
provider 适配层 MiniMax、OpenAI、Anthropic 等接口字段不完全相同,业务层不能直接依赖原始 JSON 第二期基础

---

5) 参考素材

我刻意只留少量“能直接支撑这一篇”的权威资料:

  • OpenAI:迁移到 Responses API 的指南(统一调用心智)
  • https://developers.openai.com/api/docs/guides/migrate-to-responses

  • OpenAI:responses.create API reference(对照字段、理解最小请求体)
  • https://developers.openai.com/api/docs/api-reference/responses/create

  • MiniMax:Anthropic 兼容接口文档(本篇 curl 示例的 endpoint、header、body 来源)
  • https://platform.minimaxi.com/docs/api-reference/text-chat-anthropic

  • OpenAI:Structured Outputs(下一篇我会用它把输出变成工程契约)
  • https://platform.openai.com/docs/guides/structured-outputs

---

小结

我这篇最大的收获不是“我会调用大模型了”(这其实很容易),而是我终于把它变成了一个可复用的工程入口

  • 先用 curl 跑通最小闭环(确定网络/凭证/请求体没问题)
  • 再用最小 client 把调用细节收敛起来(超时、错误、日志)
  • 最后才考虑“更好的 prompt / 更强的模型 / 更复杂的框架”

这套顺序能极大降低我后面学习时的随机性:我不再被“看起来很酷的框架”牵着走,而是每次都能落到一条可验证的调用链路上。

下一步计划

1. 加一个最小重试策略:只对 429/5xx 做退避重试(最多 2 次),并把每次重试写进日志。 2. 把 provider 配置拆出来:base_urlapi_keymodel 不写死在业务代码里。 3. 写 Episode 03:Prompt 不是玄学——我把需求写成“可执行任务”,并做一个可复用 prompt 模板。

// Kai@CodeHubble

// 观测坐标:AI 应用开发/2026-05-24

上一篇