本文价值:这篇文章保留的是 AI 工程化主线:网络、Key、并发、失败、成本、Agent 编排和运行治理。
写在前面
很多人觉得”会用 AI”就是会写 Prompt。但真正把 AI 落地到生产环境,你会发现 Prompt 只是冰山一角。网络怎么通?Key 怎么管?并发怎么扛?失败怎么重试?成本怎么控?这些才是 AI 工程化的核心问题。
这篇文章记录我从”调 API 玩玩”到”搭建完整 AI 基础设施”的过程,踩过的坑和总结的方法论。
第一阶段:直连 API 的天真时代
最开始接触大模型 API,思路很简单:拿到 Key,curl 一下,拿到结果。
$response = Http::post('https://api.openai.com/v1/chat/completions', [ 'model' => 'gpt-4', 'messages' => [['role' => 'user', 'content' => $prompt]],]);很快就遇到了现实问题:
- 国内网络不稳定,直连经常超时
- 单个 Key 有 RPM/TPM 限制,并发一上来就 429
- Key 硬编码在代码里,泄露风险大
- 不同场景想用不同模型,但切换很麻烦
这些问题逼着我开始思考:怎么把 AI 调用从”写死的代码”变成”可管理的基础设施”。
第二阶段:反向代理 — 解决网络问题
第一个要解决的是网络问题。方案很直接:在海外服务器上搭一层反向代理。
server { listen 443 ssl; server_name ai-proxy.example.com;
location /v1/ { proxy_pass https://api.openai.com/v1/; proxy_set_header Authorization $http_authorization; proxy_set_header Content-Type $http_content_type; proxy_buffering off; # SSE 流式输出必须关闭缓冲 proxy_read_timeout 300s; }}关键细节:
proxy_buffering off是必须的,不然 SSE 流式输出会被 Nginx 缓冲,用户看到的就不是逐字输出而是一坨一坨的proxy_read_timeout要设长一点,大模型生成长文本可能需要几十秒- SSL 证书要配好,API 调用走 HTTPS 是基本要求
这一层代理不仅解决了网络问题,还带来了一个额外好处:所有 AI 请求都经过我的服务器,可以做日志记录、流量统计、异常监控。
但很快又遇到新问题:代理服务器的带宽和连接数也是有限的。当多个用户同时发起长对话,代理服务器的连接数会飙升。
解决方案是引入连接池和请求队列:
// 使用 Guzzle 连接池,复用 TCP 连接$client = new Client([ 'base_uri' => 'https://ai-proxy.example.com', 'timeout' => 120, 'connect_timeout' => 5, 'http_errors' => false, 'curl' => [ CURLOPT_TCP_KEEPALIVE => 1, CURLOPT_TCP_KEEPIDLE => 30, ],]);第三阶段:号池管理 — 解决并发和成本问题
单个 API Key 的限制是硬伤。OpenAI 的 Tier 1 账号 RPM 只有 500,GPT-4 更低。业务量一上来,429 错误满天飞。
我的方案是搭建一个 Key 号池系统:
class AiKeyPool{ /** * 从号池中获取可用的 Key * 策略:轮询 + 权重 + 冷却 */ public function getAvailableKey(string $model): ?AiKeyEntity { $keys = $this->getActiveKeys($model);
foreach ($keys as $key) { // 检查是否在冷却期(被 429 后冷却 60s) if ($this->isInCooldown($key->id)) { continue; }
// 检查当前分钟的请求数是否超限 $currentRpm = $this->getCurrentRpm($key->id); if ($currentRpm >= $key->rpm_limit) { continue; }
// 记录使用次数 $this->incrementUsage($key->id); return $key; }
return null; // 所有 Key 都不可用 }
/** * Key 被限流后进入冷却期 */ public function markCooldown(int $keyId, int $seconds = 60): void { Redis::setex("ai_key_cooldown:{$keyId}", $seconds, 1); }
/** * 记录每个 Key 的使用量,用于监控和计费 */ private function incrementUsage(int $keyId): void { $minuteKey = "ai_key_rpm:{$keyId}:" . date('YmdHi'); $dayKey = "ai_key_daily:{$keyId}:" . date('Ymd');
Redis::incr($minuteKey); Redis::expire($minuteKey, 120);
Redis::incr($dayKey); Redis::expire($dayKey, 86400 * 2); }}号池的核心设计思路:
- 轮询分发:请求均匀分配到不同 Key,避免单点压力
- 冷却机制:某个 Key 被 429 后自动冷却,不再分配请求
- 用量统计:每个 Key 的 RPM、日用量都有记录,方便监控成本
- 动态权重:可以给不同 Key 设置权重,比如付费 Key 权重高、免费 Key 权重低
这套号池系统上线后,429 错误率从 15% 降到了 0.3%。
多供应商统一接入
号池不仅管理同一个供应商的多个 Key,还要管理不同供应商的 Key。OpenAI、DeepSeek、Qwen、GLM 的 API 格式大同小异,但有细微差别。
我设计了一个模型管理表来抽象这些差异:
CREATE TABLE ai_models ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '模型名称', provider VARCHAR(30) NOT NULL COMMENT '供应商: openai/deepseek/qwen/glm', model_name VARCHAR(50) NOT NULL COMMENT 'API 模型标识', base_url VARCHAR(200) NOT NULL COMMENT 'API 地址', api_key TEXT NOT NULL COMMENT 'API Key(加密存储)', rpm_limit INT DEFAULT 500 COMMENT 'RPM 限制', tpm_limit INT DEFAULT 100000 COMMENT 'TPM 限制', input_price DECIMAL(10,6) COMMENT '输入价格 $/1K tokens', output_price DECIMAL(10,6) COMMENT '输出价格 $/1K tokens', status TINYINT DEFAULT 1, created_at DATETIME);关键设计:base_url 字段让每个模型可以指向不同的 API 地址。OpenAI 走反向代理,DeepSeek 直连国内 API,Qwen 走阿里云。这样切换模型只需要改数据库配置,不需要改代码。
class AiChatService{ public function chat(array $messages, AiModels $model): ChatResult { $client = new Client([ 'base_uri' => $model->base_url, 'timeout' => 120, ]);
// 所有供应商都兼容 OpenAI 格式 $response = $client->post('/v1/chat/completions', [ 'headers' => [ 'Authorization' => 'Bearer ' . decrypt($model->api_key), 'Content-Type' => 'application/json', ], 'json' => [ 'model' => $model->model_name, 'messages' => $messages, 'temperature' => $model->temperature ?? 0.7, 'max_tokens' => $model->max_tokens ?? 4096, ], ]);
return ChatResult::fromResponse(json_decode($response->getBody(), true)); }}为什么所有供应商都用 OpenAI 格式?因为 DeepSeek、Qwen、GLM 都兼容 OpenAI 的 API 格式。这是行业事实标准,一套代码接入所有模型。
成本控制:预算告警
号池不仅管理并发,还管理成本。每个 Key 都有日预算限制:
public function checkBudget(int $keyId): bool{ $key = $this->getKey($keyId); $todayUsage = $this->getTodayUsage($keyId);
// 计算今日花费 $cost = ($todayUsage['input_tokens'] / 1000) * $key->input_price + ($todayUsage['output_tokens'] / 1000) * $key->output_price;
if ($cost >= $key->daily_budget) { Log::warning("Key {$keyId} 已达日预算上限: \${$cost}"); return false; }
// 80% 预警 if ($cost >= $key->daily_budget * 0.8) { $this->sendBudgetAlert($keyId, $cost, $key->daily_budget); }
return true;}这套预算系统避免了一个真实发生过的事故:某次 Prompt 写错导致死循环调用,如果没有预算限制,一晚上能烧掉几百美元。
第四阶段:智能体系统 — 从 API 调用到 Agent 编排
解决了基础设施问题后,下一步是让 AI 调用变得更灵活。不同业务场景需要不同的 Prompt、不同的模型、不同的参数。
我设计了一套智能体(Agent)管理系统:
CREATE TABLE ai_agents ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '智能体名称', system_prompt TEXT COMMENT '系统提示词', model_id INT NOT NULL COMMENT '关联的模型', scene VARCHAR(30) NOT NULL DEFAULT 'chat' COMMENT '使用场景', mode VARCHAR(20) NOT NULL DEFAULT 'chat' COMMENT '对话模式', temperature DECIMAL(3,2) DEFAULT 0.7, max_tokens INT DEFAULT 4096, status TINYINT DEFAULT 1, created_at DATETIME);scene 字段是关键设计。同一个智能体系统,通过 scene 区分不同的使用场景:
chat:日常对话,温度高一点,回答更有创意goods_script:电商口播词生成,温度低一点,输出更稳定
class AiAgentsDep extends BaseDep{ /** * 根据场景获取激活的智能体 */ public function getActiveByScene(string $scene): ?Model { return $this->model ->where('scene', $scene) ->where('status', CommonEnum::STATUS_ACTIVE) ->orderBy('id', 'desc') ->first(); }}智能体 + 模型的组合让 AI 调用变得非常灵活。运营人员可以在后台直接调整 Prompt 和参数,不需要改代码、不需要发版。
第五阶段:AI 运行监控 — 让每次调用都可追溯
AI 调用不像普通接口,它有不确定性。同样的输入可能产生不同的输出,而且成本不低(GPT-4 一次调用可能几毛钱)。所以必须有完善的监控。
我设计了两张表来记录每次 AI 调用:
ai_runs(运行记录)├── agent_id → 用了哪个智能体├── model_id → 用了哪个模型├── input_tokens → 输入 Token 数├── output_tokens → 输出 Token 数├── duration_ms → 耗时├── status → 成功/失败└── ai_run_steps(运行步骤) ├── step: PROMPT → 构建提示词 ├── step: LLM → 模型调用 └── step: FINALIZE → 结果处理每次 AI 调用都会记录完整的链路:用了什么 Prompt、调了什么模型、花了多少 Token、耗时多久。这些数据不仅用于排查问题,还能用于成本分析和效果优化。
// 记录一次完整的 AI 调用$run = AiRun::create([ 'agent_id' => $agent->id, 'model_id' => $agent->model_id, 'scene' => 'goods_script', 'status' => 'running',]);
// Step 1: 构建 PromptAiRunStep::create([ 'run_id' => $run->id, 'step' => 'PROMPT', 'input' => json_encode($messages), 'started_at' => now(),]);
// Step 2: 调用模型$result = $aiChatService->chat($messages, $model);
// Step 3: 记录结果AiRunStep::create([ 'run_id' => $run->id, 'step' => 'LLM', 'output' => $result->content, 'tokens_in' => $result->usage->prompt_tokens, 'tokens_out' => $result->usage->completion_tokens,]);有了这套监控,我可以清楚地知道:
- 每天花了多少钱在 AI 上
- 哪个智能体的效果最好
- 哪些调用失败了,失败原因是什么
- 平均响应时间是多少,有没有变慢的趋势
监控面板数据示例
┌─────────────────────────────────────────────────┐│ AI 调用监控面板 │├─────────────────────────────────────────────────┤│ 今日调用次数:347 ││ 成功率:98.6% ││ 平均响应时间:3.2s ││ 今日 Token 消耗:1,247,832 ││ 今日费用:$4.73 │├─────────────────────────────────────────────────┤│ 按场景统计: ││ chat: 218 次 $2.89 avg 2.8s ││ goods_script: 129 次 $1.84 avg 3.9s │├─────────────────────────────────────────────────┤│ 按模型统计: ││ deepseek-v4: 201 次 $0.42 avg 2.1s ││ gpt-5.3: 89 次 $3.15 avg 4.7s ││ qwen-3.5: 57 次 $1.16 avg 3.4s │└─────────────────────────────────────────────────┘这些数据直接驱动了模型选择的决策。比如发现 DeepSeek V4 在口播词生成场景的效果和 GPT-5.3 差不多,但成本只有 1/7,于是把 goods_script 场景的默认模型从 GPT 切到了 DeepSeek。
第六阶段:错误处理与优雅降级
AI 服务不是 100% 可靠的。网络波动、服务降级、Token 超限都可能导致调用失败。一个生产级的 AI 系统必须有完善的错误处理。
重试策略
class AiRetryHandler{ private const RETRYABLE_CODES = [429, 500, 502, 503, 504]; private const MAX_RETRIES = 3;
public function callWithRetry(callable $fn): mixed { $lastException = null;
for ($attempt = 0; $attempt <= self::MAX_RETRIES; $attempt++) { try { return $fn(); } catch (AiApiException $e) { $lastException = $e;
if (!in_array($e->getCode(), self::RETRYABLE_CODES)) { throw $e; // 不可重试的错误直接抛出 }
if ($attempt === self::MAX_RETRIES) { throw $e; // 重试次数用完 }
// 指数退避:1s, 2s, 4s $delay = pow(2, $attempt);
// 429 特殊处理:读取 Retry-After 头 if ($e->getCode() === 429 && $e->retryAfter) { $delay = max($delay, $e->retryAfter); }
Log::info("AI 调用重试 #{$attempt}, 等待 {$delay}s", [ 'error' => $e->getMessage(), ]);
sleep($delay); } }
throw $lastException; }}模型降级
当主模型不可用时,自动切换到备用模型:
public function chatWithFallback(array $messages, string $scene): ChatResult{ // 获取场景对应的智能体(可能有多个,按优先级排序) $agents = $this->dep(AiAgentsDep::class)->getActiveByScene($scene);
foreach ($agents as $agent) { $model = AiModels::find($agent->model_id);
try { return $this->retryHandler->callWithRetry( fn() => $this->aiChatService->chat($messages, $model) ); } catch (AiApiException $e) { Log::warning("模型 {$model->name} 不可用,尝试降级", [ 'error' => $e->getMessage(), ]); continue; // 尝试下一个模型 } }
throw new BusinessException('所有 AI 模型均不可用,请稍后再试');}这套降级机制在实际运行中救过好几次。有一次 OpenAI 服务降级了 2 小时,系统自动切到 DeepSeek,用户完全无感知。
总结:AI 工程化的核心能力
回顾这段经历,我觉得 AI 工程化的核心不是”会写 Prompt”,而是:
- 基础设施能力:网络代理、Key 管理、连接池
- 系统设计能力:智能体抽象、场景隔离、配置化管理
- 可观测性:调用链路追踪、成本监控、效果评估
- 工程化思维:异步队列、错误重试、优雅降级
AI 只是一个能力,怎么把这个能力稳定、高效、可控地集成到业务系统中,才是 Agent 工程师真正要解决的问题。
在我看来,未来不会有”前端工程师”和”后端工程师”的严格区分,而是”能不能用最合适的技术解决问题”。语言和框架只是工具,选择合适的工具、设计合理的架构、让 AI 真正产生业务价值,这才是核心竞争力。