Skip to content

第一次调用 API

大模型 API 真正跑起来,脚本这边其实就几步:读环境变量,准备请求,发给接口,打印返回。

第一次写这类脚本卡住的地方通常不是代码本身——SDK 装到哪个项目里了、API Key 是不是在当前终端里、模型名有没有写错、网络能不能连到外面,这几个有一个没对上,报错看起来都像”代码坏了”。

这里先拿待办清单当材料。材料越简单,越容易判断问题出在 API 调用本身,还是出在输入内容和提示词上。

一、项目目录

新建一个干净目录,专门放这组 API 练习代码。

Windows PowerShell:

powershell
uv init ai-api-demo
Set-Location ai-api-demo
uv add openai
New-Item -ItemType Directory -Force scripts

Linux / macOS:

bash
uv init ai-api-demo
cd ai-api-demo
uv add openai
mkdir -p scripts

openai 是官方 Python SDK。它帮脚本处理认证、请求发送、响应对象这些事情,不用自己手写 requests.post() 拼 HTTP 请求。

执行完以后,目录大概是这样:

text
ai-api-demo/
├── pyproject.toml
├── uv.lock
└── scripts/

pyproject.toml 记录项目依赖,uv.lock 锁住具体版本。换机器跑时,用 uv sync 能装回同一批依赖。目录里如果多了 .venv/ 也正常,那是 uv 给当前项目准备的虚拟环境。

如果 uv add openai 下载超时,本机又需要走代理,可以先在当前终端临时设置代理,再重新执行安装命令:

powershell
$env:all_proxy = "http://127.0.0.1:7890"
uv add openai

二、环境变量

API Key 不要写进 .py 文件。代码一旦传到仓库、截图、日志里,key 就泄露了。

PowerShell 里临时设置:

powershell
$env:OPENAI_API_KEY = "sk-..."

Linux / macOS:

bash
export OPENAI_API_KEY="sk-..."

再放一个模型名。<model-name> 是占位符,实际运行时换成账号里能用的模型名,尖括号也删掉。模型名写错时,接口一般会返回 400 类错误。

PowerShell:

powershell
$env:OPENAI_MODEL = "<model-name>"

Linux / macOS:

bash
export OPENAI_MODEL="<model-name>"

检查当前终端能不能读到这两个值:

bash
uv run python -c "import os; print('KEY=OK' if os.getenv('OPENAI_API_KEY') else 'KEY=MISSING'); print('MODEL=' + os.getenv('OPENAI_MODEL', 'MISSING'))"

输出类似这样才继续:

text
KEY=OK
MODEL=<model-name>

这里的检查只说明当前终端有变量。换一个终端、定时任务、systemd 服务或 CI 任务里跑,环境变量要重新配置。很多认证失败其实不是 key 真错了,而是脚本启动时根本没拿到这个变量。

三、基础脚本

新增文件:scripts/hello_ai.py

python
#!/usr/bin/env python3
"""调用一次大模型 API,把返回文本打印出来。"""

import os
import sys

from openai import OpenAI


def main():
    # API Key 和模型名都从环境变量读,避免写死在代码里。
    api_key = os.getenv("OPENAI_API_KEY")
    model = os.getenv("OPENAI_MODEL")

    if not api_key:
        print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
        return 2
    if not model:
        print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
        return 2

    client = OpenAI(api_key=api_key, timeout=60.0)

    response = client.responses.create(
        model=model,
        instructions="你负责整理文本。只输出待办清单,每行一条,不要解释。",
        input="今晚买菜,写周报,回复邮件。",
        max_output_tokens=200,
    )

    print(response.output_text.strip())
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行脚本。

PowerShell:

powershell
uv run python scripts/hello_ai.py
$LASTEXITCODE

Linux / macOS:

bash
uv run python scripts/hello_ai.py
echo $?

输出可能是项目符号,也可能是编号,意思接近就行:

text
- 买菜
- 写周报
- 回复邮件

OpenAI(api_key=api_key, timeout=60.0) 里有两个点。api_key 从环境变量来,不进代码;timeout=60.0 是请求超时,避免网络卡住时脚本一直挂着。AI 接口生成内容需要时间,超时太短容易误伤,完全不设超时又不好排查。

max_output_tokens=200 是限制输出长度。这里本来只想要三条待办,不需要让模型自由发挥一大段。

response.output_text 是响应对象里整理好的文本。响应里还有 id、usage、output 等字段,第一次调用先盯住这一项就够了。

四、拆分请求

刚才那段代码能跑,但 input 里把要求和材料揉在了一起。小例子没问题,脚本一旦开始读文件、读表单、读接口返回,这种写法就容易乱。

更清楚的写法是:instructions 放稳定要求,input 放这次要处理的材料。

scripts/hello_ai.py 里调用 API 的部分改成这样:

python
response = client.responses.create(
    model=model,
    instructions="你负责整理文本。只输出待办清单,每行一条,不要解释。",
    input="今晚买菜,写周报,回复邮件。",
    max_output_tokens=200,
)

现在材料如果来自文件,就只换 input 这一块。

新增文件:todo.txt

text
今晚买菜
写周报
回复邮件

再给 scripts/hello_ai.py 增加一行导入:

python
from pathlib import Path

然后把请求前后的几行改成这样:

python
text = Path("todo.txt").read_text(encoding="utf-8")

response = client.responses.create(
    model=model,
    instructions="你负责整理文本。只输出待办清单,每行一条,不要解释。",
    input=text,
    max_output_tokens=200,
)

这个变化不大,但很关键。模型不会自己去读本地文件,Python 先把 todo.txt 读出来,再把内容塞进请求里,模型才能看到。

五、错误处理

第一次调 API,出错经常集中在这几类:

现象更像是哪一层的问题
AuthenticationErrorAPI Key 没配、写错、被撤销
BadRequestError模型名不对、参数缺失、输入格式不对
RateLimitError请求太快、额度不足、触发限流
APITimeoutError请求等太久,模型生成慢或网络卡住
APIConnectionError代理、证书、防火墙、网络出口问题

如果这些错误全部包成一句“请求失败”,认证失败、网络失败、限流和模型名写错就混在一起了。分开打印,排查时至少知道从哪一层开始看。

scripts/hello_ai.py 改成带错误处理的版本:

python
#!/usr/bin/env python3
"""调用一次大模型 API,带基础错误处理。"""

import os
import sys
from pathlib import Path

from openai import (
    APIConnectionError,
    APIStatusError,
    APITimeoutError,
    AuthenticationError,
    BadRequestError,
    OpenAI,
    RateLimitError,
)


def main():
    api_key = os.getenv("OPENAI_API_KEY")
    model = os.getenv("OPENAI_MODEL")

    if not api_key:
        print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
        return 2
    if not model:
        print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
        return 2

    text = Path("todo.txt").read_text(encoding="utf-8")
    client = OpenAI(api_key=api_key, timeout=60.0)

    try:
        response = client.responses.create(
            model=model,
            instructions="你负责整理文本。只输出待办清单,每行一条,不要解释。",
            input=text,
            max_output_tokens=200,
        )
    except AuthenticationError as exc:
        print(f"认证失败: {exc}", file=sys.stderr)
        return 2
    except BadRequestError as exc:
        print(f"请求参数有问题: status={exc.status_code} {exc}", file=sys.stderr)
        return 2
    except RateLimitError as exc:
        print(f"触发限流或额度不足: {exc}", file=sys.stderr)
        return 3
    except APITimeoutError as exc:
        print(f"请求超时: {exc}", file=sys.stderr)
        return 4
    except APIConnectionError as exc:
        print(f"网络连接失败: {exc}", file=sys.stderr)
        return 4
    except APIStatusError as exc:
        # 其他非 2xx 状态码,例如服务端临时错误。
        print(f"API 返回错误: status={exc.status_code} {exc}", file=sys.stderr)
        return 5

    print(response.output_text.strip())
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

退出码也顺手分开:

退出码含义
0调用成功
2本地配置、认证或请求参数问题
3限流或额度问题
4网络或超时问题
5API 返回了其他错误状态

这个习惯和普通运维脚本一样。屏幕上打印了“失败”,但退出码还是 0,定时任务、CI 或监控就会把失败当成成功。

六、保存结果

请求通了以后,可以把输入和输出落到文件里。改提示词、换模型、换材料时,至少能回头看当时到底发了什么、收到了什么。

新增文件:scripts/save_result.py

python
#!/usr/bin/env python3
"""调用模型,并把输入和输出保存成 JSON 文件。"""

import json
import os
import sys
from datetime import datetime
from pathlib import Path

from openai import OpenAI


def main():
    api_key = os.getenv("OPENAI_API_KEY")
    model = os.getenv("OPENAI_MODEL")

    if not api_key:
        print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
        return 2
    if not model:
        print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
        return 2

    text = Path("todo.txt").read_text(encoding="utf-8")
    client = OpenAI(api_key=api_key, timeout=60.0)

    response = client.responses.create(
        model=model,
        instructions="你负责整理文本。只输出待办清单,每行一条,不要解释。",
        input=text,
        max_output_tokens=200,
    )

    output_dir = Path("outputs")
    output_dir.mkdir(exist_ok=True)

    result = {
        "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "model": model,
        "input": text,
        "output": response.output_text.strip(),
    }

    output_path = output_dir / "first-response.json"
    output_path.write_text(
        json.dumps(result, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

    print(response.output_text.strip())
    print(f"结果已保存: {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行:

bash
uv run python scripts/save_result.py

生成的文件在这里:

text
outputs/first-response.json

ensure_ascii=False 是为了让中文正常写进 JSON,不变成一串 \u4e2d\u6587。这里保存的是整理后的文本,不是完整响应对象。完整响应字段很多,结果如果要继续交给程序处理,再专门拆字段。

七、代理网络

本地访问国外 API 时,如果网络要走代理,PowerShell 里可以临时设置:

powershell
$env:all_proxy = "http://127.0.0.1:7890"
uv run python scripts/hello_ai.py

Linux / macOS:

bash
export all_proxy="http://127.0.0.1:7890"
uv run python scripts/hello_ai.py

代理只影响当前终端。换一个终端、定时任务、systemd 服务或 CI 任务里跑,代理变量也要重新配置。

如果脚本报 APIConnectionError,代码未必是问题。最直接的检查是看同一个终端里 uv add openai 能不能访问 PyPI,再看浏览器或命令行能不能访问 API 域名。包能下载但 API 不能访问,多半是代理规则、公司网络出口或证书链的问题;包也下载不了,就先处理本机网络和代理。