背景痛点:传统多语言客服的“三座大山”
做跨境的同学都懂,客服群一响,血压就飙。
- 响应延迟:人工排班要盖 24h,小语种更是“等天亮”。实测平均首响 2.8 小时,丢单率 7%。
- 翻译误差:先机器翻、再人工改,SKU 名一错,退货比成交快。
- 人力成本:英法德西意日韩,7 组人 4 班倒,工资占 GMV 3.2%,旺季还要临时外包, onboarding 一周,上线当天就爆线。
一句话:慢、错、贵。LLM 能不能一次解决?我们踩了 4 个月坑,把 95% 的咨询压到 200 ms 内回完,人力降到 0.6%,方案拆给大家。
技术选型:GPT-4、Claude、Mistral 横向对比
先给结论:
- 意图识别:GPT-4 > Claude-3 > Mistral-7B
- 多语言生成:Claude-3 更“母语感”,GPT-4 更“销售感”
- 成本:Mistral 自部署 < GPT-4 API < Claude-3
我们最后走“混合”路线:
- 意图模型用 Mistral-7B 微调后自部署,单卡 A10 可抗 800 QPS,延迟 40 ms。
- 生成模型按语言路由:欧美市场 GPT-4,新兴市场 Mistral-13B,Claude-3 做兜底质检。
这样把 70% 流量留给便宜模型,30% 高客单走 GPT-4,整体成本降 52%。
核心架构:三板斧砍延迟
1. 动态语言路由层
用户消息先过“语言嗅探器”——fastText 176 语言分类,99 ms 内给出语种 + 置信度。
路由表按“延迟-成本”双权重动态刷新,每 30 s 拉一次 Prometheus 指标,自动把流量切到空闲节点。
负载均衡用 gRPC 自定义 Resolver,基于“连接池水位”做加权轮询,代码见下节。
2. 带 LRU 缓存的上下文管理
对话状态以session_id+lang做 key,value 存最近 5 轮用户消息 + 掩码后的 bot 回复。
缓存用 Go 写的线程安全 LRU,TTL 15 min,支持热 key 复制,命中率 68%,直接把 LLM 调用量砍一半。
3. 异步处理流水线
Kafka 按语种分区,Celery 任务粒度=“一次 LLM 请求”,超时 3 s 自动重试。
为防止雪崩,采用“令牌桶 + 退避”双限流:网关层 QPS 令牌桶,模型层并发退避,保证 GPU 不会一次性被冲爆。
代码实战:Python + Go 双剑合璧
Python:gRPC 多语言路由服务
# language_router.py Google Python Style Guide import grpc from concurrent import futures import router_pb2, router_pb2_grpc import fasttext import prometheus_client # 全局连接池,避免每次新建 channel _POOL_SIZE = 50 _pools = {} # key: endpoint -> grpc.Channel class RouterServicer(router_pb2_grpc.RouteServiceServicer): def __init__(self): self.lang_model = fasttext.load_model("lid.176.ftz") self.qps_counter = prometheus_client.Counter("router_qps", "Queries per second") def Detect(self, request, context): self.qps_counter.inc() text = request.text lang, score = self.lang_model.predict(text.replace("\n", " ")) lang = lang[0].replace("__label__", "") # 根据 lang 选 endpoint endpoint = self._choose_endpoint(lang) return router_pb2.DetectReply(lang=lang, endpoint=endpoint) def _choose_endpoint(self, lang: str) -> str: # 简化:查路由表 + 取最小延迟 table = {"en": "gpt4.api.com:50051", "es": "mistral.api.com:50051"} return table.get(lang, "claude.api.com:50051") def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=100)) router_pb2_grpc.add_RouteServiceServicer_to_server(RouterServicer(), server) server.add_insecure_port("[0.0.0.0:50051") server.start() server.wait_for_termination() if __name__ == "__main__": serve()Go:并发安全 LRU 缓存组件
// cache.go Google Go Style Guide package cache import ( "container/list" "sync" "time" ) type entry struct { key string value interface{} expiration int64 } type LRUCache struct { mu sync.Mutex cap int ttl time.Duration ll *list.List items map[string]*list.Element } func NewLRU(cap int, ttl time.Duration) *LRUCacheotom return &LRUCache{ cap: cap, ttl: ttl, ll: list.New(), items: make(map[string]*list.Element), } } func (c *LRUCache) Get(key string) (interface{}, bool) { c.mu.Lock() defer c.mu.Unlock() if ele, ok := c.items[key]; ok的单位 ent := ele.Value.(*entry) if time.Now().UnixNano() < ent.expiration { c.ll.MoveToFront(ele) return ent.value, true } // 过期淘汰 c.removeElement(ele) } return nil, false } func (c *LRUCache) Put(key string, value interface{}) { c.mu.Lock() defer c.mu.Unlock() if ele, ok := c.items[key]; ok { c.ll.MoveToFront(ele) ele.Value.(*entry).value = value ele.Value.(*entry).expiration = time.Now().Add(c.ttl).UnixNano() return } ent := &entry{key, value, time.Now().Add(c.ttl).UnixNano()} ele := c.ll.PushFront(ent) c.items[key] = ele if c.ll.Len() > c.cap { c.removeElement(c.ll.Back()) } } func (c *LRUCache) removeElement(ele *list.Element) { ent := ele.Value.(*entry) delete(c.items, ent.key) c.ll.Remove(ele) }生产考量:别让“小概率”变热搜
- 幂等性:Celery task 自带
task_id,网关层把用户消息哈希成request_id,重复提交直接返回缓存,防用户疯狂点“发送”。 - 敏感词过滤:DFA 双数组实现,10 万词库 2 ms 内跑完,支持热更新,每天凌晨拉一次新词表。
- 监控:Prometheus 拉取 gRPC latency、GPU util、Kafka lag,Grafana 配三色告警:黄线 200 ms、红线 500 ms、紫线 1 s,值班手机半夜响过两次,都是上游 CDN 抖动。
避坑指南:血泪换来的 3 个锦囊
- 冷启动延迟:模型容器放
preload脚本,启动即跑一条“Hello”热身,GPU 显存申请完毕再注册到 consul;上线 0 请求 502。 - 多时区上下文:时间戳统一存 UTC,展示层按用户
timezone转本地,缓存 key 里一定加lang,否则西班牙语用户看到葡萄牙语“Olá”直接投诉。 - GPU 动态分配:K8s 用
nvidia.com/gpu: 1整卡调度太浪费,改mig拆成 3 个 10 GB 实例,白天高峰 3 卡跑生成,夜里离线微调,资源利用率从 38% 提到 78%。
写在最后
系统上线 90 天,自动回复率 95%,平均响应 180 ms,客服团队缩编 40%,人效翻 3 倍。但新问题也来了:每新增一个语种,就要重新标 5 k 条样本做微调,标注成本直线上升。
如何平衡多语言支持与模型微调成本?是把所有语料全扔给一个大模型“零样本”,还是继续走“小模型+轻量 LoRA”?欢迎留言聊聊你的解法。