背景痛点:传统客服为什么总“答非所问”
过去两年,我先后接手过三套“上一代”客服系统:一套基于正则+关键词,两套用 Bert+CRF 做意图分类。上线初期都跑得挺欢,可一旦对话超过三轮,用户就开始吐槽“机器人失忆”。总结下来,老架构的硬伤集中在四点:
- 多轮断层:对话状态靠 slot 填充,上下文长度超过 256 token 就丢槽位。
- 长尾失效:训练语料 80% 集中在 Top 200 意图,冷门问题(如“发票抬头写错了还能改吗?”)全部被归为“其它”。
- 知识更新慢:业务规则改一行,就要重训整个模型,发版窗口至少 24h。
- 响应延迟高:Bert 意图+FAQ 召回+规则排序,整条链路 1.2s,用户体验“ppt 式聊天”。
痛定思痛,团队决定把底座直接换成大模型,用生成式方案一次性解决“理解+回答+多轮”。
技术选型:微调还是 RAG?先算一笔账
在 8×A100(40G)集群上,我们对比了两种路线,数据如下(均值,单卡 FP16):
| 方案 | 显存占用 | 首 token 延迟 | 单卡 QPS | 更新成本 |
|---|---|---|---|---|
| 全参微调 13B | 26G | 320ms | 6 | 高(需重训) |
| LoRA 13B | 16G | 300ms | 7 | 中(只训 Adapter) |
| RAG + 6B 底座 | 10G | 180ms | 12 | 低(只更新知识库) |
结论很直观:
- 业务知识每天变 → RAG 更轻;
- 对话风格强定制 → LoRA 更稳;
- 预算卡脖子 → 6B+RAG 是性价比之王。
我们最终采用“6B 底座 + RAG + 轻量 Prompt 微调”的混合方案:通用对话让模型直接答,企业知识用召回补充,风格示例靠 5% 数据 LoRA 一把梭。
核心实现:30 行代码跑通对话链
下面给出最小可运行骨架,Python 3.10,依赖:langchain==0.1.0、fastchat、fastapi。
1. 对话链封装(带记忆 & 异常兜底)
from typing import List from langchain import ConversationChain, PromptTemplate from langchain.memory import ConversationTokenBufferMemory from langchain.chat_models import ChatOpenAI class CSChain: def __init__(self, model_path: str, max_token: int = 4096, memory_key: str = "history"): llm = ChatOpenAI( openai_api_base="http://localhost:8000/v1", # 本地 FastChat model_name=model_path, max_tokens=512, temperature=0.3, request_timeout=60, ) template = """你是客服助手,请根据上下文和知识库回答问题。 知识库:{context} 对话历史:{history} 用户:{input} 客服:""" prompt = PromptTemplate( input_variables=["context", "history", "input"], template=template, ) memory = ConversationTokenBufferMemory( llm=llm, max_token_limit=max_token, memory_key=memory_key ) self.chain = ConversationChain( llm=llm, prompt=prompt, memory=memory, verbose=False ) async def answer(self, query: str, context: str) -> str: try: return await self.chain.arun(input=query, context=context) except Exception as e: # 异常兜底:返回静态文案 + 人工入口 return f"系统繁忙,已转人工客服,稍等~(trace: {e})"2. RAG 召回器(基于 ES + 向量双路)
class Retriever: def __init__(self, es_index: str, emb_model: str): self.es = Elasticsearch() self.encoder = SentenceTransformer(emb_model) async def search(self, q: str, top_k: int = 3) -> List[str]: vec = self.encoder.encode(q) # 1. 向量检索 knn = {"field": "vector", "query_vector": vec, "k": top_k} # 2. 关键词检索 match = {"match": {"title": q}} res = self.es.search( index=self.es_index, knn=knn, query=match, size=top_k, rank="rrf", ) return [h["_source"]["text"] for h in res["hits"]["hits"]]3. FastAPI 异步入口(带限流 & 负载均衡)
app = FastAPI() chain = CSChain(model_path="cs-6b") retriever = Retriever(es_index="cs_kb", emb_model="bge-small") @app.post("/ask") async def ask(req: AskRequest, background: BackgroundTasks): # 1. 限流:单用户 10 qps if not limiter.allow(req.user_id): raise HTTPException(429, "请求过快") # 2. 召回知识 context = "\n".join(await retriever.search(req.query)) # 3. 异步生成 answer = await chain.answer(req.query, context) # 4. 后台记录 background.add_task(log_conv, req.user_id, req.query, answer) return {"answer": answer}部署时,起 4 个 replica,Nginx 轮询,单卡 QPS 12,整体 48,满足 2w 峰值并发。
性能优化:把 3090 榨出 50% 富余
1. Batch 大小 vs TPS
实验在 3090(24G)+ 6B 模型上完成,输入 512、输出 128 token:
| batch | 显存 | 首 token | TPS |
|---|---|---|---|
| 1 | 10G | 180ms | 12 |
| 4 | 16G | 220ms | 38 |
| 8 | OOM | — | — |
结论:在线服务 batch=4 是甜点,再大就 OOM;若离线批处理,可降到 FP8 量化再上 16。
2. 注意力窗口裁剪
我们将 RoPE 基频从 10k 提到 40k,再把窗口限制 2048 实验,结果:
- 知识型问答(单跳事实)准确率 92% → 90%,几乎无损;
- 多轮闲聊(>6 轮)准确率 85% → 76%,掉点明显。
因此,知识型场景可大胆裁剪窗口,节省 18% 显存;多轮场景建议开全窗或 4k 滑动。
3. KV Cache 预分配
提前根据最大长度 malloc 显存池,避免torch.cat动态拼接,延迟抖动从 ±60ms 降到 ±15ms,P99 毛刺消失。
避坑指南:上线前必须踩的三颗雷
对话状态丢失
症状:用户问“那我呢?”机器人答“您指什么?”
根因:- 记忆模块用
ConversationSummaryMemory,摘要算法把指代信息压丢; - 高并发下
InMemory被不同线程覆写。
解法: - 换成
ConversationTokenBufferMemory,保留原始 token; - 用 Redis 存
session_id→ 历史,API 无状态化。
- 记忆模块用
敏感词“漏杀”
大模型天生会“创造性”输出,曾把“退款”说成“退妈”。
最佳实践:- 双层过滤:①模型输出 ②正则+DFA 白名单;
- 业务侧维护动态敏感词表,5 分钟热更新;
- 对金融/医疗场景,加外部合规 API 二次校验。
知识库“幻觉”
症状:模型把过时的“7 天无理由”说成“15 天”。
解法:- 召回片段打时间戳标签,Prompt 里声明“仅使用 2024 版政策”;
- 对答案做“可溯源”标记,点击展开引用原文,降低投诉率 30%。
效果复盘与下一步
上线四周,核心指标:
- 首响时间 中位数 220 ms → 180 ms;
- 多轮解决率 63% → 81%;
- 人工转接率 28% → 15%;
- GPU 利用率 38% → 72%,电费持平。
不过,仍有几道开放题留给读者:
- 当知识库膨胀到 1 亿条,向量索引重建成本如何摊销?
- 在 100ms 延迟红线内,如何平衡 13B 效果与 6B 成本?
- 如果业务要求“可解释”,你会把 chain-of-thought 暴露给用户,还是另建一套摘要?
欢迎在评论区交换思路,一起把客服机器人做得既聪明又省钱。