从模型到用户:我在新东家踩过的机器学习部署坑

产品说很简单
2026-03-27 14:37
阅读 2049

入职这家中型 SaaS 公司刚两个月,我这个“AI原住民”就被塞进了一个听起来高大上、实则焦头烂额的项目——把团队内部的智能问答模块上线给客户用。说是“智能问答”,其实就是个基于 LangChain 的 RAG(Retrieval-Augmented Generation)系统,底层跑着一个经过 Fine-tuning 的开源 LLM。听起来很酷?现实是:开发环境跑得飞起,一到部署就翻车。

作为一个重度 Cursor 用户,平时写 CRUD 都靠它生成,连注释都懒得打。但这次不一样——这玩意儿要直接面对前端、面对用户、面对产品经理那双“怎么还没好”的死亡凝视。上周五晚上九点,测试群里弹出消息:“线上问答返回空白,客户急了”。那一刻,我真想拔掉网线假装断电。

于是,痛定思痛,写下这篇血泪总结。不为炫技,只为下次别再被运维拉去背锅。

为什么部署比训练更让人头秃?

很多人以为,Fine-tuning 完模型、LangChain 编排好流程,就万事大吉了。天真!模型在 Jupyter Notebook 里输出完美答案,和它在生产环境扛住并发请求、快速响应、不崩不卡,完全是两码事。

我们最初犯的错,就是把“能跑”当成“能用”。本地用一台 32G 内存的笔记本跑 LangChain + 向量数据库 + LLM,延迟 800ms,觉得“还行”。结果一上 K8s,前端调用超时设的是 2 秒,用户刷三次没反应就骂娘。更惨的是,某次高峰时段 QPS 突然飙升,Pod 直接 OOMKilled,整个服务雪崩。

这时候我才意识到:机器学习部署不是技术终点,而是工程起点

从 Fine-tuning 到轻量化:别让模型成为累赘

我们的业务场景是帮客户自动解析他们上传的 PDF 合同,提取关键条款并回答问题。原始数据是几千份法律文档,噪声大、格式杂。一开始我们直接拿 Llama-3-8B 做全参数微调,效果确实不错,准确率 85%+。但部署时傻眼了——单个推理请求吃掉 6GB 显存,还得配 A10G,成本直接爆炸。

后来在 Cursor 里敲了个 prompt:“如何在保持性能的前提下减小 LLM 部署体积?” 它建议试试 QLoRA 微调 + GGUF 量化。我半信半疑试了下,用 Unsloth 库做 QLoRA,只更新 0.1% 的参数,然后导出为 GGUF 格式,用 llama.cpp 推理。

结果惊人:模型从 14GB 压缩到 4.7GB(4-bit),推理速度提升 3 倍,显存占用降到 2GB 以下。虽然准确率掉了 2 个百分点(83% → 81%),但在业务容忍范围内。关键是——终于能在普通云服务器上跑了,不用求着运维申请 GPU 资源。

# 示例:用 llama.cpp 加载 GGUF 模型
./main -m ./models/contract-qa.Q4_K_M.gguf \
       -p "根据合同第5条,违约金是多少?" \
       -n 256 --temp 0.7

教训:Fine-tuning 不是为了追求 SOTA,而是为了在约束条件下找到性价比最优解。有时候,少即是多。

LangChain:强大但危险的双刃剑

LangChain 让我们快速搭起了 RAG 流程:用户问 → 检索相关段落 → 注入 prompt → 调 LLM → 返回。但默认配置简直是“内存泄漏制造机”。

问题出在两个地方:

  1. 向量检索未缓存:每次请求都重新查 FAISS,高频词反复计算。
  2. LLM 调用阻塞主线程:Python 的同步 I/O 在高并发下直接卡死。

我们改了三处:

  • 用 Redis 缓存 top-k 检索结果,TTL 设 5 分钟(合同内容不会秒变)
  • 把 LangChain 的 RunnableSequence 改成异步模式(感谢 FastAPI)
  • 限制最大上下文长度,防止用户问“把整本合同总结一下”这种魔鬼问题
# 异步 LangChain 调用示例
from langchain_core.runnables import RunnableLambda
import asyncio

async def async_retrieve(query: str):
    return await retriever.ainvoke(query)

async def async_llm_call(context: str, question: str):
    prompt = f"Context: {context}\n\nQ: {question}\nA:"
    return await llm.ainvoke(prompt)

# 在 FastAPI 路由中
@app.post("/ask")
async def ask(request: AskRequest):
    docs = await async_retrieve(request.question)
    context = "\n".join([d.page_content for d in docs])
    answer = await async_llm_call(context, request.question)
    return {"answer": answer}

另外,LangChain 的日志默认关得太死。上线前务必打开 LANGCHAIN_TRACING_V2=true,配合 LangSmith,不然出问题根本不知道是检索错了还是 LLM 幻觉了。

前端不是终点,而是用户体验的第一道防线

别以为部署完 API 就完事了。前端同学才是直面用户的勇士。我们最初给前端的接口是“同步阻塞式”:发请求 → 等 2 秒 → 返回答案。结果用户疯狂点“发送”,后端瞬间被打爆。

后来和前端老哥对齐,改成流式响应 + 加载态反馈

  • 用 SSE(Server-Sent Events)逐字返回 LLM 输出
  • 前端显示“正在思考…” + 动画
  • 设置 5 秒超时,超时后提示“稍后再试”

前端代码也做了兜底:

// 前端防抖 + 超时控制
let abortController;

const askQuestion = async (question) => {
  if (abortController) abortController.abort();
  abortController = new AbortController();

  try {
    const response = await fetch('/stream-ask', {
      method: 'POST',
      body: JSON.stringify({ question }),
      signal: abortController.signal,
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value);
      // 实时追加到 UI
      appendToAnswer(chunk);
    }
  } catch (e) {
    if (e.name !== 'AbortError') {
      showError("服务繁忙,请稍后重试");
    }
  }
};

产品经理看了 demo 后居然说“有 ChatGPT 那味儿了”,难得夸一次,感动哭。

部署架构:别把所有鸡蛋放一个 Pod 里

早期我们图省事,把 LangChain、向量库、LLM 全塞进一个 FastAPI 服务。结果就是:一崩全崩。

现在拆成三层:

组件 技术栈 部署方式 扩缩容策略
前端 API 网关 FastAPI + Uvicorn K8s Deployment HPA based on CPU
检索服务 FAISS + Redis 独立 Pod 固定副本数
LLM 推理服务 llama.cpp + GGUF K8s Deployment + GPU node selector 手动扩缩(成本敏感)

关键改进:

  • LLM 服务独立:即使 LLM 挂了,前端还能返回“服务暂时不可用”,而不是 500
  • 向量库预加载:启动时加载 FAISS 索引到内存,避免冷启动延迟
  • 健康检查:每个服务暴露 /health,K8s 自动剔除异常实例

顺便吐槽一句:运维大哥看到我们从“一个 Pod”变成“五个服务”,表情一度非常复杂。但自从没再半夜被 PagerDuty 叫醒,他现在见我都笑。

监控与回滚:别等用户投诉才行动

上线第一周,我们靠人工刷页面测功能。第二周,客户群里开始有人问“为什么昨天能答的问题今天不行了?”

赶紧补监控:

  • Prometheus + Grafana:跟踪 QPS、P99 延迟、错误率
  • LangSmith:记录每条用户 query 的完整 trace
  • 日志告警:LLM 输出包含“我不知道”或“根据我的知识”超过阈值,自动告警(可能检索失效)

最救命的是一键回滚机制。我们把模型版本、LangChain prompt 模板、检索索引都打 tag 存 Git。一旦线上出问题,5 分钟内切回上一版。上周就有一次,因为新合同模板没入库,导致检索召回率暴跌,靠回滚保住了 SLA。

写在最后:AI 工程化,本质是“克制的艺术”

刚入职时,我满脑子都是“上更大模型”、“搞更复杂 pipeline”。现在明白了:在真实业务场景里,稳定 > 新颖,可维护 > 黑科技,成本可控 > 性能极致

Fine-tuning 不是为了秀技术,而是解决特定问题;LangChain 是脚手架,不是银弹;前端不是调用方,而是体验共建者。

现在,我依然每天开着 Cursor 写代码,但它更多帮我生成 boilerplate、写单元测试、解释报错。真正的架构决策、部署取舍、性能权衡,还得靠人脑——毕竟 AI 还不能替我挨产品经理的骂。

如果你也在搞 ML 部署,记住:别追求一步到位。先让服务跑起来,再让它跑得稳,最后才考虑跑得快。毕竟,在互联网公司,活下来比赢漂亮更重要。

(完)

P.S. 上周五那个线上事故,最后发现是 FAISS 索引文件在 CI/CD 中没正确复制……运维请我喝了杯冰美式,算扯平了。

评论 0

最热最新
暂无评论
产品说很简单Lv.1
0
影响力
0
文章
0
粉丝