裸辞半年后我靠这套机器学习部署方案拿到了Offer

黄军
2026-07-11 17:54
阅读 614

说实话,写这篇文章的时候我正戴着降噪耳机,Spotify里循环着Lo-Fi Beats,手边是一杯已经凉透的美式。Gap了整整半年,简历投出去像石沉大海,好不容易最近捞到几个面试机会,边刷题边复习,头发是一把一把地掉。

但今天不聊八股文,想跟大家唠唠我最近在项目里折腾的机器学习部署那点事儿。为啥要聊这个?因为面试被问麻了。现在大厂面试,光会训模型已经不够看了,人家更关心你怎么把模型又快又稳地怼到线上去跑。

事情是这么开始的

上个月一个前同事内推我,说他们团队在搞一个智能体开发的项目,需要有人懂模型部署和推理优化。我一听,这不正好嘛,之前在大厂那会儿天天跟K8s打交道,云原生这块我还是有点底气的。

结果面试的时候,面试官上来就甩了个面试题挑战

"假设你有一个基于LangChain搭建的多智能体系统,底层挂了三个不同的大模型,QPS要求500,P99延迟要控制在2秒以内,你怎么设计部署架构?"

好家伙,我当时脑子里就一个字:麻。

不是不会,是这个问题太综合了。你得懂模型推理优化、懂服务编排、懂资源调度、还得懂LangChain那套东西的坑。我面完出来就在地铁上拿手机记了一堆笔记,回家打开电脑就开始整理,想着干脆写成博客,也算给自己做个复盘。

先说说LangChain智能体部署的那些坑

用过LangChain的朋友都知道,这玩意儿写Demo是真的爽,几行代码就能搭一个Agent出来。但你要真往生产环境上搬,那坑是一个接一个。

我之前的项目里,我们用LangChain搭了一个客服智能体,接入了知识库检索、工具调用、多轮对话这些能力。本地跑得飞起,一上预发环境直接拉胯。

问题出在哪?

第一,LangChain的调用链太深了。 一个用户请求进来,经过Agent规划、Tool选择、LLM调用、结果解析,中间可能套了四五层嵌套。每一层都有序列化反序列化的开销,Python的GIL在这里简直是噩梦。

第二,模型推理没有做批处理。 最开始我们就是一个请求一个请求地发给模型服务,GPU利用率连30%都不到。后来上了Continuous Batching,直接干到80%以上。

第三,没做缓存。 很多相似的Query反复打过来,每次都重新跑一遍推理,这不是纯纯的浪费算力嘛。

我的部署方案长什么样

先说整体架构,画个文字版的图:

                    ┌─────────────┐
                    │  API Gateway │
                    │  (限流/鉴权) │
                    └──────┬──────┘
                           │
                    ┌──────▼──────┐
                    │  Agent服务   │
                    │  (FastAPI)   │
                    └──────┬──────┘
                           │
           ┌───────────────┼───────────────┐
           │               │               │
    ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
    │ 模型推理服务1 │ │ 模型推理服务2 │ │ 模型推理服务3 │
    │ (vLLM)      │ │ (TGI)       │ │ (TensorRT)  │
    └─────────────┘ └─────────────┘ └─────────────┘
           │               │               │
           └───────────────┼───────────────┘
                           │
                    ┌──────▼──────┐
                    │  Redis缓存   │
                    │  + 向量数据库 │
                    └─────────────┘

核心思路就是把推理和Agent逻辑解耦。Agent服务本身不跑模型,只做编排和调度。模型推理全部交给专门的推理服务,用vLLM或者TGI这种高性能框架来跑。

推理服务优化

这块我花的时间最多。以vLLM为例,分享几个关键配置:

# vllm_serving.py
from vllm import AsyncLLMEngine
from vllm.engine.arg_utils import AsyncEngineArgs

engine_args = AsyncEngineArgs(
    model="/models/qwen-72b-chat",
    tensor_parallel_size=4,          # 4卡并行
    gpu_memory_utilization=0.92,     # 显存利用率拉满
    max_num_batched_tokens=32768,    # 批处理token上限
    max_num_seqs=256,                # 最大并发序列数
    enable_chunked_prefill=True,     # 开启分块预填充
    max_paddings=256,                # padding容忍度
)

engine = AsyncLLMEngine.from_engine_args(engine_args)

这里有个细节,gpu_memory_utilization我设成了0.92而不是默认的0.9。别小看这0.02,在72B模型上能多塞进去好几个KV Cache的slot,并发能力直接提升了一截。当然你得确保不会OOM,这个需要压测来调。

还有一个大招是Prefix Caching。如果你的业务场景里System Prompt很长且固定,开启这个能省掉大量重复的prefill计算:

engine_args = AsyncEngineArgs(
    model="/models/qwen-72b-chat",
    enable_prefix_caching=True,      # 开启前缀缓存
    # ... 其他配置
)

实测下来,在System Prompt有2000+ token的场景下,首token延迟从1.8秒降到了0.6秒,效果非常显著。

Agent服务的异步化改造

LangChain默认是同步调用,这在生产环境完全不能忍。我把它整个改成了异步模式:

# agent_service.py
import asyncio
from fastapi import FastAPI
from langchain.agents import AgentExecutor
from langchain.callbacks import AsyncIteratorCallbackHandler

app = FastAPI()

# 全局连接池,避免每次请求都创建新连接
from aiohttp import ClientSession
session = ClientSession()

@app.post("/chat")
async def chat(request: ChatRequest):
    callback = AsyncIteratorCallbackHandler()
    
    # 异步执行Agent
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        callbacks=[callback],
        max_iterations=5,            # 限制最大迭代次数,防止死循环
        return_intermediate_steps=True,
    )
    
    # 设置超时,保护下游服务
    try:
        result = await asyncio.wait_for(
            agent_executor.ainvoke(
                {"input": request.query},
                config={"callbacks": [callback]}
            ),
            timeout=10.0
        )
    except asyncio.TimeoutError:
        return {"error": "Agent执行超时", "code": 408}
    
    return {"response": result["output"]}

这里我加了max_iterations=5asyncio.wait_for的双重保护。为啥?因为之前线上出过事故,Agent在两个Tool之间反复横跳,死循环了,直接把下游模型服务打挂了。那天晚上运维大哥给我打电话的时候,我正在家打游戏,接完电话手都是抖的。

缓存策略

缓存这块我做了两级:

缓存层级 存储介质 缓存内容 TTL 命中率
L1 本地LRU Cache 高频Query的完整响应 5min ~35%
L2 Redis Cluster Semantic Embedding缓存 30min ~25%

L1用的是Python的cachetools,简单粗暴但有效:

from cachetools import LRUCache

response_cache = LRUCache(maxsize=10000)

async def get_response(query: str) -> Optional[str]:
    cache_key = hashlib.md5(query.encode()).hexdigest()
    if cache_key in response_cache:
        return response_cache[cache_key]
    # ... 调用Agent获取响应
    response_cache[cache_key] = response
    return response

L2的语义缓存更有意思。我把Query转成embedding之后,不是做精确匹配,而是算余弦相似度。相似度超过0.95的,直接返回缓存结果。这个在客服场景里特别好使,因为用户问法虽然不一样,但意思经常是一样的。

在K8s上部署的一些心得

既然咱对K8s比较熟,这块就多聊两句。

模型服务的Pod配置,我一般这么写:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-serving
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        resources:
          requests:
            memory: "64Gi"
            cpu: "16"
            nvidia.com/gpu: 4
          limits:
            memory: "64Gi"
            cpu: "16"
            nvidia.com/gpu: 4
        env:
        - name: VLLM_ATTENTION_BACKEND
          value: "FLASHINFER"
        - name: NCCL_DEBUG
          value: "WARN"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 300    # 模型加载需要时间
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 10

几个注意点:

  1. initialDelaySeconds一定要设够。72B模型加载到4张A100上,怎么也得四五分钟。设短了K8s以为Pod挂了,反复重启,那场面别提多壮观了。

  2. GPU资源要用nvidia.com/gpu这种标准资源名,别自己瞎定义。

  3. 如果用的是多实例推理,建议加上Pod Disruption Budget,保证滚动更新的时候至少有一半的Pod可用:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: vllm-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: vllm-serving

HPA的配置

自动扩缩容这块,我用的不是CPU利用率,而是请求队列长度。因为模型推理是GPU密集型任务,CPU利用率根本反映不了真实的负载情况。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vllm-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-serving
  minReplicas: 2
  maxReplicas: 8
  metrics:
  - type: Pods
    pods:
      metric:
        name: vllm_queue_size
      target:
        type: AverageValue
        averageValue: "20"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60    # 快速扩容
      policies:
      - type: Pods
        value: 2
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300   # 慢速缩容,避免抖动
      policies:
      - type: Pods
        value: 1
        periodSeconds: 120

扩容快、缩容慢,这是模型服务的基本策略。因为GPU实例启动本身就慢,你要是缩容也很快,流量一抖动就来回折腾,GPU资源白白浪费。

性能数据对比

折腾了两周,最终的效果还是相当可观的。贴一下压测数据,给大家一个参考:

指标 优化前 优化后 提升幅度
QPS 120 530 341%
P50延迟 3.2s 0.8s 75%
P99延迟 8.5s 1.9s 78%
GPU利用率 28% 82% 193%
单请求成本 ¥0.15 ¥0.04 73%

P99延迟从8.5秒降到1.9秒,这个是最让我满意的。因为面试的时候面试官就问了P99怎么优化,我能结合实际数据来答,底气就足很多。

关于模型选择和调优

这块也顺带提一嘴,毕竟部署方案跟模型选择是强相关的。

我们项目里用了三个模型,各司其职:

  • Qwen-72B:主力模型,处理复杂推理和多轮对话
  • Qwen-7B:轻量模型,处理简单的意图识别和分类
  • BGE-Large:Embedding模型,用于知识库检索和语义缓存

这里有个小技巧:模型路由。不是所有请求都需要72B的大模型。我写了一个简单的路由器,根据Query的复杂度来分发:

async def route_request(query: str) -> str:
    # 简单规则 + 轻量模型判断
    if len(query) < 20 and not contains_keywords(query, ["分析", "比较", "为什么"]):
        return "qwen-7b"  # 简单问题走小模型
    
    # 用7B模型做一个快速分类
    intent = await classify_intent(query, model="qwen-7b")
    
    if intent in ["simple_qa", "chitchat"]:
        return "qwen-7b"
    elif intent in ["complex_reasoning", "multi_hop"]:
        return "qwen-72b"
    else:
        return "qwen-7b"  # 默认走小模型

这个路由上线后,72B模型的调用量直接降了40%,但用户满意度几乎没变。因为大部分用户的问题其实并不需要72B来回答。

一些碎碎念

写到这里,咖啡已经喝了三杯了,耳机里的歌单也换了两轮。

说实话,裸辞这半年,焦虑肯定是有的。看着银行卡余额一天天变少,有时候半夜醒来会想,是不是不应该这么冲动。但每当搞定一个技术难题,或者面试的时候能把一个知识点讲得明明白白,那种成就感又让我觉得,嗯,这波不亏。

机器学习部署这个方向,我觉得未来几年都会很吃香。现在会训模型的人一抓一大把,但能把模型又快又省地部署到线上、还能保证稳定性的,真的是稀缺人才。

如果你也在准备这方面的面试,我的建议是:

  1. 不要只停留在调API的层面。去了解一下vLLM、TGI这些推理框架的底层原理,知道Continuous Batching是怎么回事,知道KV Cache是怎么管理的。

  2. 一定要有实际的性能优化经验。面试官问"你怎么优化推理延迟",你说"我用了xxx技术,P99从多少降到了多少",这比说一百句"我了解xxx"都管用。

  3. 关注成本。大厂现在都在降本增效,你能从成本角度来聊部署方案,绝对是加分项。

好了,不说了,LeetCode的每日一题还没刷,明天还有一场面试。祝我好运吧,也祝看到这篇文章的你,面试顺利,Offer拿到手软。

我们下个Gap...啊不,下个工位见。

评论 0

最热最新
暂无评论
黄军Lv.1
0
影响力
0
文章
0
粉丝