4. 推理接口调用:API服务搭建与调用实战

推理接口,说白了就是让大模型“开口说话”的通道。我见过太多团队模型训得不错,结果API一压就崩。今天咱们就把这块硬骨头啃下来。

4.1 选Flask还是FastAPI?我的建议

我个人习惯用FastAPI。为什么?因为它原生支持异步,对大模型的流式输出特别友好。Flask当然也能做,但需要额外配一堆东西。

来看个对比:

特性 Flask FastAPI
异步支持 需额外插件 原生支持
流式输出 较复杂 简单优雅
性能 一般 高并发优秀
学习成本 中等

嗯,这里要注意:如果你们团队对Flask特别熟,也不是不能用。但我建议新项目直接上FastAPI,省得后面重构。

4.2 请求与响应格式设计

接口设计这块,我踩过不少坑。最典型的就是请求体太复杂,前端同学骂娘。

我推荐的标准格式:

{
  "model": "law-llm-v1",
  "messages": [
    {"role": "user", "content": "请分析这个合同条款"}
  ],
  "temperature": 0.3,
  "max_tokens": 2048,
  "stream": true
}

响应格式:

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "根据《合同法》第XX条..."
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 45,
    "completion_tokens": 120,
    "total_tokens": 165
  }
}
避坑指南: 我曾经把usage字段放在最前面,结果前端解析时总报错。后来才明白——流式输出时,usage应该在最后一条消息里返回。

4.3 流式输出实现

大模型回答慢,用户等不了。流式输出就是解决方案——一个字一个字往外吐,用户体验好很多。

FastAPI实现流式输出,其实就几行代码:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def generate_stream(prompt):
    # 模拟大模型逐字输出
    response = "根据《民法典》第XXX条规定..."
    for char in response:
        yield f"data: {char}\n\n"
        await asyncio.sleep(0.05)
    yield "data: [DONE]\n\n"

@app.post("/v1/chat/completions")
async def chat_completion(request: dict):
    prompt = request.get("messages", [])[-1]["content"]
    return StreamingResponse(
        generate_stream(prompt),
        media_type="text/event-stream"
    )

你想想看,如果不用流式,用户得等十几秒才能看到第一个字。用了流式,200毫秒就开始出字了。这差距,用户感知非常明显。

注意: 流式输出时,一定要设置正确的Content-Type。我见过有人设成application/json,结果前端SSE解析不了,折腾了半天。

4.4 超时与重试机制

大模型推理不稳定,这是常态。我遇到过模型推理到一半卡住,也遇到过GPU显存不够直接OOM。所以超时和重试必须配好。

我的配置方案:

import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),  # 最多重试3次
    wait=wait_exponential(multiplier=1, min=2, max=10)  # 指数退避
)
async def call_llm(prompt: str):
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            "http://localhost:8000/v1/chat/completions",
            json={"messages": [{"role": "user", "content": prompt}]},
            timeout=httpx.Timeout(30.0, read=60.0)  # 连接超时30秒,读取超时60秒
        )
        return response.json()

这里有几个关键点:

  • 连接超时:一般设10-30秒,太久会拖垮整个服务
  • 读取超时:流式输出建议设60-120秒,因为大模型生成慢
  • 重试策略:用指数退避,别一失败就猛冲

我的经验: 重试次数别超过3次。超过3次还失败,说明模型或服务本身有问题,重试也没用。这时候应该返回503,让上游降级处理。

4.5 完整架构图

下面这张图,是我在实际项目中总结出来的推理接口调用流程:

客户端 HTTP请求 API网关 超时/重试/限流 路由 FastAPI服务 请求解析/流式输出 LLM 流式响应(SSE) 错误处理 超时/异常 重试队列 指数退避重试 降级处理 重试3次失败 监控告警 记录日志

这张图里,我特别想强调两点:

  • 流式路径:不走常规HTTP响应,而是走SSE通道。这样用户能实时看到模型在“思考”
  • 错误处理分支:超时后先重试,重试3次还不行就走降级。别让用户干等

4.6 实战避坑清单

最后,分享几个我踩过的坑:

  1. 连接池耗尽:高并发时,每个请求都新建连接,很快就把端口耗光了。一定要用连接池复用。
  2. 流式输出不flush:有些框架默认会缓冲输出,导致流式变成“块式”。记得每次yield后flush一下。
  3. 超时时间设太短:法律大模型要分析长文本,生成时间可能超过30秒。我一般设60秒起步。
  4. 重试导致重复:如果模型已经生成了部分内容,重试时要不要重新生成?我建议幂等设计,每次重试都重新开始。
小技巧: 可以在响应头里加个X-Request-Id,方便排查问题。我每次排查线上故障,第一件事就是看这个ID。

好了,推理接口这块就聊到这儿。API服务搭建不难,难的是把各种边界情况都处理好。记住一句话:用户感知到的稳定性,取决于系统最薄弱的那个环节


公众号:蓝海资料掘金营,微信deep3321