curl 不止是下载工具:AI 与 Agent 开发中的十二类实战场景
多数人学 curl 的第一课是"下载文件",此后便把它归类为 wget 的同类。但在 AI 与 Agent 工程中,curl 的角色完全不同:它是唯一一个在协议层完全透明、零依赖、可在任何环境逐字复现的 HTTP 客户端。当需要回答"这个模型接口到底返回了什么"“延迟究竟卡在哪一段"“Agent 的工具调用为什么静默失败"时,curl 往往比任何 SDK 都更快给出答案——因为它不隐藏任何东西。
本文以 curl 8.22.0(2026-09-02 发布,第 276 个版本,命令行选项累计 278 个)为基线,梳理十二类高频场景。所有命令均可直接粘贴执行,只需替换端点与凭据。
一、LLM 接口调试的四个基本动作
1.1 最小可复现调用
--json(7.82.0 引入)是单次调用最省事的写法,它等价于同时做三件事:把数据作为 --data-binary 发送、设置 Content-Type: application/json、设置 Accept: application/json。
curl -sS https://api.deepseek.com/chat/completions \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" \
--json '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "只回复 OK"}],
"temperature": 0
}' \
-w '\n[%{http_code} | %{time_total}s | %{size_download}B]\n'
三点值得强调:
-sS是固定搭配。-s静音进度条,-S保留错误输出;只用-s会让失败无声发生,这在脚本里是灾难。- 既然
--json已负责Content-Type,就不要再手写一次该头,重复会造成难以察觉的调试噪音。 --json同时会把Accept锁定为application/json。这一点在调试 MCP 与 SSE 接口时正是 bug 来源,见第四节。
1.2 流式响应:-N 与"假慢"的鉴别
LLM 接口最容易被误判的性能问题,是缓冲伪装成慢。当逐字输出变成"卡很久然后一次性全出来”,问题几乎从不在模型,而在中间某一跳做了缓冲。
curl -N -sS https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
--json '{"model":"m","stream":true,"messages":[{"role":"user","content":"从 1 数到 30"}]}'
-N(--no-buffer,6.5 引入)关闭 curl 自身的输出缓冲。没有它,你测到的是终端的缓冲行为,而不是服务端行为——大量"接口不支持流式"的结论都源于此。
鉴别方法:若首字节很快到达但总量耗时很长,服务端流式正常,只是模型生成慢;若长时间无输出然后整体到达,则链路中存在缓冲点。缓冲的常见来源依次为:反向代理(nginx 默认 proxy_buffering on)、gzip 压缩中间件(其按构造即缓冲)、CDN 的内容类型策略、以及应用层把生成器写成"先收集后返回”。
1.3 多模态与文件上传:用 -F,不要手工编码
上传图片、音频到视觉模型或语音识别接口时,使用 multipart 表单:
curl -sS https://api.openai.com/v1/audio/transcriptions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F file=@meeting.m4a \
-F model=whisper-1 \
-F response_format=verbose_json
请求体包含大量 JSON 时,一律用文件引用而非命令内联,既避开 shell 转义,也让请求体可被独立审阅与版本管理:
# 推荐:请求体落在文件里,--json 读文件
curl -sS https://api.example.com/v1/chat/completions --json @payload.json
# 避免:多层转义、引号地狱、无法审查
curl -sS https://api.example.com/v1/chat/completions -d "{\"model\":\"m\",...}"
1.4 本地与自建推理端点速查
调试自建推理服务时,以下端点覆盖了 90% 的日常需求:
| 服务 | 关键端点 | 用途 |
|---|---|---|
| Ollama | /api/tags、/api/ps、/api/chat | 列出模型、查看已加载模型、对话 |
llama.cpp llama-server | /health、/props、/slots、/metrics | 存活探针、运行时参数、槽位状态、吞吐指标 |
| vLLM | /v1/models、/metrics、/health | 模型清单、Prometheus 指标、探针 |
| TGI | /generate_stream | 原生流式生成 |
# 确认本地推理服务是否真的加载了模型,而非仅仅进程存活
curl -sS http://127.0.0.1:11434/api/ps
curl -sS http://127.0.0.1:8080/health -o /dev/null -w '%{http_code}\n'
二、用 -w 把"慢"拆成六段
--write-out / -w(6.5 引入)是 curl 中最被低估的能力。它把一次请求拆成可归因的阶段耗时,让"慢"从形容词变成数字。
curl -sS -o /dev/null -w '
DNS : %{time_namelookup}s
TCP : %{time_connect}s
TLS : %{time_appconnect}s
预处理 : %{time_pretransfer}s
TTFB : %{time_starttransfer}s
总计 : %{time_total}s
连接数 : %{num_connects} 重定向: %{num_redirects}
远端 IP : %{remote_ip} HTTP: %{http_version} %{http_code}
下载 : %{size_download}B @ %{speed_download}B/s
' https://api.example.com/v1/models
关键字段与派生结论:
| 字段 | 含义 | 派生诊断 |
|---|---|---|
time_namelookup | DNS 解析完成 | 偏大 → DNS 或 DoH 问题 |
time_connect | TCP 握手完成 | 减去 namelookup 即 TCP 耗时 |
time_appconnect | TLS 握手完成 | 减去 connect 即 TLS 耗时,>300ms 需查证书链与 TLS 版本 |
time_starttransfer | 首字节到达(TTFB) | 减去 pretransfer 即服务端处理时间 |
time_total | 总耗时 | 减去 starttransfer 即下行传输时间 |
num_connects | 本次新建连接数 | 持续 >0 说明连接池未复用,是隐性延迟来源 |
一个实用技巧:把 -w 模板存成文件(-w @timing.txt),在 CI 中固化性能基线,用 time_total 与 time_starttransfer 的回归来拦截性能退化。
三、Agent 场景:把 curl 封装成可靠执行器
Agent 调用外部服务时,最大的风险不是"调用失败",而是失败得无声无息:模型把错误响应当作正常结果继续推理。以下四组参数是让 curl 变成可靠工具的必要条件。
3.1 退出码即控制流
curl -sS --fail-with-body "$URL" || handle_error $?
--fail-with-body(7.76.0 引入)在 HTTP 状态码 ≥400 时返回非零退出码,同时仍输出响应体。这比老式 -f(--fail)更实用:既有明确的失败信号,又保留了服务端的错误详情——恰恰是 Agent 自我修正所需的信息。
3.2 韧性四件套
| 参数 | 作用 | 引入版本 |
|---|---|---|
-m / --max-time | 整体超时,避免 Agent 永久挂起 | 4.0 |
--connect-timeout | 连接阶段独立超时 | 7.7 |
--retry-all-errors | 对所有错误重试(默认仅瞬时错误) | 7.71.0 |
--retry-connrefused | 连接被拒也重试 | 7.52.0 |
--rate | 限制请求速率,避免触发上游限流 | 7.84.0 |
curl -sS --fail-with-body \
--connect-timeout 5 --max-time 120 \
--retry 4 --retry-all-errors --retry-delay 2 --retry-max-time 180 \
-H "Authorization: Bearer $KEY" \
--json @payload.json "$ENDPOINT"
--retry-all-errors 与 -m 必须同时使用:只有重试没有超时,会让 Agent 在故障上游上耗尽整个任务预算。
3.3 参数防注入:--variable 与 --expand-*
当 URL、Header 或请求体由模型生成时,字符串拼接就是注入面。curl 8.3.0 引入的命令行变量机制,把"数据"与"结构"分离:
curl -sS --variable %USER_ID \
--variable token@/run/secrets/api_token \
--expand-url 'https://api.example.com/v1/users/{{USER_ID}}/profile' \
--expand-header 'Authorization: Bearer {{token:trim}}' \
--expand-url-query '{{USER_ID}}'
变量展开支持 trim(去首尾空白)、json(按 JSON 字符串规则转义)、url(百分号编码)、b64 等函数,以冒号分隔链式调用。模型输出的值经 url 或 json 处理后,即使包含 ../、&、引号也无法越出预定结构。
3.4 凭据不进命令行
进程命令行在多数系统上对同机其他用户可见(ps aux),把 API Key 写在 -H 里等于泄漏。两条正路:
# 方式一:配置文件(-K / --config,4.10 引入),权限 600
cat > /run/secrets/api.curlrc <<'EOF'
header = "Authorization: Bearer sk-xxxxxxxx"
silent
show-error
EOF
chmod 600 /run/secrets/api.curlrc
curl -K /run/secrets/api.curlrc --json @payload.json "$ENDPOINT"
# 方式二:netrc 文件(--netrc-file,7.21.5 引入)
curl -sS --netrc-file /run/secrets/netrc "$ENDPOINT"
补充两个与 AI 服务直接相关的认证参数:--oauth2-bearer(7.33.0)与 --aws-sigv4(7.75.0)——后者让 curl 可直接签名调用 Amazon Bedrock,无需额外 SDK。
四、MCP 服务器调试:Streamable HTTP 完整握手
MCP 已从早期的双端点 SSE 传输收敛到单端点 Streamable HTTP:所有交互走同一个 /mcp 路径,用 HTTP 方法区分语义(POST 发请求、GET 建事件流、DELETE 结束会话),会话状态由 Mcp-Session-Id 头承载。这意味着,一个纯 curl 脚本就能完整验证一个 MCP 服务器,无需安装任何客户端。
第一步,初始化会话并抓取会话 ID:
curl -sS -D - -X POST "$MCP_ENDPOINT" \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-03-26","capabilities":{},
"clientInfo":{"name":"curl-client","version":"1.0.0"}}}'
响应头中会出现 mcp-session-id。第二步发送初始化确认——注意它的响应是 202 Accepted 且无响应体,这不是错误:
SESSION_ID="从上一步响应头中提取"
curl -sS -X POST "$MCP_ENDPOINT" \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
第三、四步列出并调用工具:
# 列出工具
curl -sS -X POST "$MCP_ENDPOINT" \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq .
# 调用工具
curl -sS -X POST "$MCP_ENDPOINT" \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"add_numbers","arguments":{"a":15.5,"b":24.3}}}' | jq .
本节最重要的一个坑:MCP 规范要求客户端 Accept 头同时包含 application/json 与 text/event-stream。而第一节介绍的 --json 会把 Accept 覆盖成纯 application/json——于是本来好用的快捷参数,在 MCP 场景下会直接导致协商失败。这类"参数太好用以致掩盖了它做了什么"的情况,是 curl 调试中最常见的认知陷阱。
排障对照表:
| 现象 | 成因 |
|---|---|
| HTTP 400 | 缺失或非法的 Mcp-Session-Id |
| HTTP 404 | 会话已过期或被终止,需重新 initialize |
| 406 或空响应 | Accept 未同时声明两种内容类型 |
JSON-RPC -32602 | 参数非法,通常是工具名或字段名写错 |
| 返回 202 且无 body | 正常——这是通知类消息的预期响应 |
会话结束后清理:
curl -sS -X DELETE "$MCP_ENDPOINT" \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID"
五、批量与并发:--parallel + --rate
批量拉取模型列表、批量跑评测集、批量抓取上游文档时,不要用 shell 循环串行堆 &。curl 自带连接复用与并发调度:
curl -sS --parallel --parallel-max 8 --rate 4/s \
-o 'model-#1.json' \
'https://api.example.com/v1/models?page=[1-20]' \
'https://api.example.com/v1/pricing'
--parallel/-Z(7.66.0):单进程内并发多传输,并在同一主机上复用连接,省去重复的 DNS 与 TLS 握手。--rate(7.84.0):限制请求发起速率。对 LLM 接口而言,这是避免触发上游 429 的礼貌做法。--parallel-max-host(8.16.0):单独限制单主机并发数,防止把某个上游打爆。- 输出文件用
-o 'model-#1.json':#1是 glob 计数器占位符;8.21.0 起还支持命名 glob,可在文件名中按名字引用各段。
对需要长期增量同步的数据源,配合条件请求可大幅降低流量——--etag-compare / --etag-save(7.68.0)让 curl 自己维护 ETag,--skip-existing(8.10.0)跳过已完成项:
curl -sS -L --etag-save etags.json --etag-compare etags.json \
--skip-existing -O 'https://datasets.example.com/corpus-[001-100].jsonl'
六、Linux 网络诊断:curl 是第一把手术刀
在 AI 基础设施排障中,curl 承担了"分层归因"的核心职责——因为一次 HTTP 请求本身就是一份完整的协议栈体检报告。
6.1 逐层剥离
# 看完整交互过程(DNS → TCP → TLS → HTTP)
curl -v https://api.example.com/health
# 只看协议流量且带时间戳,输出更干净
curl --trace-ascii /tmp/trace.txt --trace-time https://api.example.com/health
# 校验证书链路与吊销状态
curl -v --cert-status https://api.example.com/health
6.2 绕过 DNS 与灰度验证
--resolve(7.21.3)把指定主机名强制解析到给定 IP,用于在 DNS 生效前验证新节点:
curl -sS --resolve api.example.com:443:10.0.1.7 \
https://api.example.com/v1/models
--connect-to(7.49.0)更灵活,可按 主机:端口:目标主机:目标端口 重定向连接,同时保留原始 Host 头与 SNI——这是做灰度对比与故障切流的利器:
# 把生产域名指向预发集群,请求头与 TLS 校验仍按生产域名进行
curl -sS --connect-to api.example.com:443:staging.internal:443 \
https://api.example.com/health
6.3 本地服务与容器
# 直接访问 Unix Socket,绕过 TCP 与防火墙
curl -sS --unix-socket /var/run/docker.sock http://localhost/version
# 指定网卡与地址族,验证多网卡环境的路由选择
curl -sS --interface eth1 -4 https://api.example.com/health
6.4 代理链
curl -sS -x http://proxy.corp:8080 --proxy-user u:p https://api.example.com
curl -sS --socks5-hostname 127.0.0.1:1080 https://api.example.com # 代理侧解析 DNS
curl -sS --noproxy localhost,127.0.0.1 https://api.example.com
--socks5-hostname 与 --socks5 的差别在于 DNS 解析发生在哪一端——这在排查 DNS 污染与 Split-Horizon 解析时是决定性变量。
6.5 退出码速查
curl 的退出码是诊断的第一手证据,把它写进排障手册:
| 退出码 | 含义 | 首查方向 |
|---|---|---|
| 6 | 无法解析主机名 | DNS、/etc/resolv.conf、DoH |
| 7 | 无法连接 | 端口、防火墙、安全组 |
| 22 | HTTP ≥400(配合 --fail-with-body) | 业务错误,需看响应体 |
| 28 | 操作超时 | --max-time / --connect-timeout 触发 |
| 35 | TLS 握手失败 | 协议版本、密码套件、中间证书 |
| 47 | 重定向次数过多 | 循环跳转 |
| 52 | 服务器无内容返回 | 上游进程崩溃或反代配置错误 |
| 56 | 接收数据失败 | 连接被中途重置 |
| 60 | 证书校验失败 | CA 信任链、过期、域名不匹配 |
七、陷阱清单
以下八条是实际排障中反复出现的认知偏差。
--json会覆盖Accept。调试 MCP、SSE 等依赖内容协商的接口时,必须手写两个头。-N不是可选项。测流式接口时省略它,测的是客户端缓冲。-d @file里的@是文件引用。请求体若真要发送以@开头的字面量,需用--data-raw(7.43.0)。-d不编码。需要 URL 编码时用--data-urlencode;只想把数据拼进查询串则用-G或--url-query(7.87.0)。--json永远发 POST。需要 GET 或自定义方法时,用-X显式指定。-k的代价是静默的。-k/--insecure跳过证书校验,会让中间人攻击无声通过;CI 中应禁用。- Windows PowerShell 里
curl不是 curl。Windows PowerShell 5.1 把curl别名到Invoke-WebRequest,参数语义完全不同。必须显式写curl.exe;JSON 正文用单引号在 PowerShell 中无效,应使用 here-string 或--json @payload.json。 - 只重试不限时。
--retry缺省的触发条件很窄,与--retry-all-errors配合-m才能构成完整韧性策略。
八、能力版本考古
curl 的开发持续了二十余年,很多能力有明确的版本门槛。跨机器部署脚本前,先对照这张表——在生产环境里用新参数而客户端版本过旧,是静默失败的常见来源。
| 能力 | 选项 | 引入版本 |
|---|---|---|
--write-out / --no-buffer / --form | -w / -N / -F | 6.5 / 6.5 / 5.0 |
--retry 系列 | --retry 等 | 7.12.3 |
--resolve / --netrc-file | 7.21.3 / 7.21.5 | |
--connect-to | 7.49.0 | |
--unix-socket | 7.40.0 | |
--parallel 并发 | -Z | 7.66.0 |
--etag-compare / --etag-save | 7.68.0 | |
--retry-all-errors | 7.71.0 | |
--aws-sigv4 | 7.75.0 | |
--fail-with-body | 7.76.0 | |
--json | 7.82.0 | |
--rate | 7.84.0 | |
--url-query | 7.87.0 | |
--variable / --expand-* | 8.3.0 | |
--ech(加密 SNI) | 8.8.0 | |
--mptcp | 8.9.0 | |
--skip-existing | 8.10.0 | |
--parallel-max-host / --follow | 8.16.0 | |
--knownhosts | 8.17.0 | |
--proxy-http3 | 8.21.0 | |
--httpsig-*(RFC 9421 消息签名) | 8.22.0 |
另需留意一个现实:curl 已成为自动化漏洞挖掘的重点目标。8.21.0 单次修复 18 个 CVE,8.22.0 修复 9 个,其中多数由 AI 模型发现。把 curl 纳入常规升级清单,是低成本的安全实践。
总结
curl 在 AI 与 Agent 工程中的价值,可以归纳为三条:
- 它是可复现性的载体。任何"SDK 里调不通"的问题,降维成一条 curl 命令后,就变成了可以直接粘贴给同事、贴进 issue、写进测试用例的证据。
- 它把黑盒拆成阶段。
-w给出耗时分段,-v给出协议交互,退出码给出归因,三者组合即构成最小的 HTTP 诊断闭环。 - 它天然适配 Agent 的工具化需求。退出码可判定、输出可约束、凭据可外置、参数可防注入——这些特性恰好是让"模型生成的调用"变得安全可控的前提。
三条建议:把常用调用固化成 -K 配置文件而非一长串参数;把 -w 的耗时模板纳入 CI 作为性能基线;以及在调试任何"接口很慢"的问题之前,先加 -N 再下结论。
延伸阅读:别把 2B 当小号 GPT-4:llama.cpp + Qwen 2B 的六种实战玩法——其中涉及的本地推理端点,正是本文第一节速查表的调试对象。