🧠 大模型入门与实战 · 第 6 篇

大模型应用上线的 8 个坑:都是踩过才知道的

作者:VillaB · 发布:2026-10-27 · 更新:2026-10-27 · 分类:大模型

坑 1:没有超时和重试,一个慢请求拖垮整个队列

现象:监控显示偶发 60 秒响应,用户以为页面卡死。

原因:默认 SDK 的 timeout 往往很长甚至没有,模型侧偶发排队时,请求会一直挂着,占住你的 worker。

client = OpenAI(
    api_key=KEY,
    timeout=30.0,        # 总超时,必须设
    max_retries=2,       # 只重试「可重试」的错误
)

要点:

  • 超时要小于你上游(网关/前端)的超时,否则上游先断开,你白花钱
  • 只重试 429(限流)、5xx、超时;不要重试 400(参数错,重试还是错)
  • 重试要指数退避 + 抖动,否则限流时会形成雪崩
def backoff(i):
    return min(2 ** i, 8) + random.uniform(0, 0.5)

坑 2:把幻觉当"偶发问题"

现象:「模型有时候会编造不存在的信息,概率不高,先上线吧。」

问题:幻觉不是随机噪声,它集中在模型不确定的地方——也就是你最需要准确的地方(罕见实体、数字、日期、引用)。上线后用户信任一次崩塌就很难挽回。

三条硬要求:

  1. 结构化任务必须 schema 校验,不合法就重试或降级
  2. 给"我不知道"的合法出口(见第 3 篇专栏文章)
  3. 要求引用来源,没有来源的结论在 UI 上标注「未核实」
resp = client.chat.completions.create(
    model=MODEL,
    messages=msgs,
    response_format={"type": "json_object"},   # 强制 JSON
)
try:
    data = json.loads(resp.choices[0].message.content)
except json.JSONDecodeError:
    data = fallback()      # 降级,而不是把脏数据塞给用户

坑 3:JSON 输出不合法,只在 5% 的请求上

现象:json.loads 偶尔失败,错误日志里是 Expecting value: line 1 column 1 或输出被 ```json 包裹。

原因:模型可能加解释性文字、加 Markdown 代码块、或截断(max_tokens 不够)。

处理顺序:

  1. 用 response_format / JSON schema(首选,最可靠)
  2. prompt 里给完整示例而不是描述
  3. 容错解析:剥掉 ``` 包裹、找第一个 { 到最后一个 }、修复尾逗号
  4. 仍失败 → 重试一次(带"上次输出不合法"的提示)→ 再失败就降级
  5. 监控重试率:超过 5% 说明 prompt 或 schema 有问题,别当正常波动

坑 4:流式输出没处理"半截失败"

现象:开了流式,用户看到一半文字突然停住,或者收到一段乱码。

原因:

  • 流式过程中连接断了,已发送的内容无法撤回
  • 没处理 SSE 的 data: [DONE] 与心跳
  • 把 chunk 的增量当完整句子处理
stream = client.chat.completions.create(model=MODEL, messages=msgs, stream=True)
buf = []
try:
    for chunk in stream:
        delta = chunk.choices[0].delta.content or ""
        if delta:
            buf.append(delta)
            yield delta
except Exception:
    # 半截失败:要么补一句提示,要么标记本条回答不完整
    yield "

[回答中断,请重试]"

要点:流式必须在前端支持"中断重试",并且记录是否完整,否则统计数据会失真。

坑 5:撞限流(429)才发现没做队列

现象:上线当天调用量上来,大量 429,用户看到报错。

原因:厂商的 RPM/TPM 限制,免费额度和正式配额差别巨大;并发一上来就打满。

做法:

  • 服务端加令牌桶 / 信号量控制并发,别指望客户端自觉
  • 429 时排队而不是直接失败(可以接受延迟的请求)
  • 关键路径准备备选模型(同厂不同型号 / 另一家厂商)
sem = asyncio.Semaphore(8)      # 按你的配额设

async def call(msgs):
    async with sem:
        return await do_request(msgs)

坑 6:模型版本悄悄变了,效果跟着变

现象:「上周还好好的一天,这周准确率掉了 10%,代码一行没改。」

原因:厂商灰度更新了底层模型(同名不同版本),或你的代码用了"最新版本"别名而不是固定版本号。

做法:

  • 固定版本号(能用具体版本就别用 -latest)
  • 每周跑一次固定评测集,准确率下降就告警
  • 关键业务保留回滚路径(能切回旧版本或旧厂商)

坑 7:成本失控,月底账单吓人

现象:测试环境被脚本刷了几百万 token;或生产上某个循环调用把成本放大 100 倍。

做法:

  • 每个环境独立密钥,测试密钥设低额度
  • 服务端设单用户/单 IP 的日调用上限
  • 监控每小时的 token 消耗,异常陡增就告警
  • 别把密钥放前端(哪怕只读)——永远不要

坑 8:没有固定评测集,改动全靠感觉

现象:「我觉得这版更好」「我觉得变差了」,团队无法对齐。

做法(最低成本版):

# 20~50 条真实样本,覆盖边界:空输入、超长、歧义、恶意注入
EVAL = [{"input": "...", "expect_contains": ["..."], "must_json": True}, ...]

def run_eval(build_prompt):
    passed = 0
    for c in EVAL:
        out = call_model(build_prompt(c["input"]))
        if c["must_json"] and not is_json(out):
            continue
        if all(k in out for k in c["expect_contains"]):
            passed += 1
    return passed / len(EVAL)

每次改 prompt、换模型、升版本,都跑一遍。没有评测集,所有"优化"都是赌博。

上线前的一张检查表

  • [ ] 超时已设,且小于上游超时
  • [ ] 重试只针对 429/5xx/超时,带指数退避 + 抖动
  • [ ] 结构化输出用 schema 校验,有降级路径
  • [ ] 流式有中断处理,且记录完整性
  • [ ] 服务端有并发/限流控制
  • [ ] 模型版本号固定,有回滚路径
  • [ ] 密钥分环境、不在前端、有额度上限
  • [ ] 有固定评测集,改动前后都跑
  • [ ] 监控:错误率、P95 延迟、重试率、token 消耗

相关工具

返回文章列表