智能客服系统实战:基于 RAG + LLM 搭建面向商家的多租户 AI 客服架构
智能客服系统实战:基于 RAG + LLM 搭建面向商家的多租户 AI 客服架构
摘要:本文针对中小商家在落地 AI 客服时面临的知识库搭建门槛高、多租户数据隔离难、用户不打分导致质量评估失真、并发响应延迟等痛点,基于 RAG(检索增强生成)+ LLM + SSE 流式协议,详解如何构建支持多租户隔离、流式应答、双 LLM 自动评估的智能客服系统。通过完整的架构设计、关键代码实现及生产环境压测数据,开发者可掌握向量检索、tenant 隔离、SSE 推流、自动评估等关键技术。该方案已在赛能saillm(面向商家的 AI 营销与智能客服平台)生产环境验证,支撑单租户日均 8 万次对话,P95 响应延迟 320 ms,意图识别准确率 96.4%。
1. 背景痛点:商家 AI 客服的"四座大山"
调研了 200+ 中小商家(零售/餐饮/本地服务)后,我们总结出传统客服方案在 AI 化过程中的核心痛点:
-
知识库搭建门槛高
传统 RAG 方案要求商家自己写 Markdown、做分块、调 embedding 模型。90% 的中小商家没有技术团队,这套流程直接劝退。 -
多租户数据串扰
SaaS 化客服产品最容易翻车的点。A 商家的客户问"你们家奶茶多少钱",AI 从 B 商家的资料里捞答案,直接变成商业事故。市面不少"AI 客服 SaaS"实际只在业务层做过滤,向量层并未隔离。 -
用户打分数据失真
主动要求用户打分的方案,实际回收率不到 5%,且集中在"非常不满意"和"非常满意"两极,中段数据缺失。无法准确评估 AI 客服的真实质量。 -
大模型响应延迟高
GPT-4 级模型平均响应 1.8-3.5 秒,直接同步返回,客户体验是"干等几秒蹦一坨字",流失率高。商家场景对响应速度极其敏感。
针对这四个痛点,我们设计了一套基于 RAG + LLM 的方案,下文逐节展开。
2. 技术对比:RAG + LLM vs 关键词匹配 vs 纯 LLM
| 维度 | RAG + LLM(本文方案) | 关键词 + 正则 | 纯 LLM(Fine-tune) |
|---|---|---|---|
| 准确率 | 96.4%(实测) | 60-70% | 92-95% |
| 知识更新 | 上传文档即时生效 | 需重写规则 | 需重新训练 |
| 多轮对话 | 原生支持 | 需自己实现状态机 | 原生支持 |
| 数据隔离 | namespace 级强隔离 | 业务层 if-else | 模型权重难拆 |
| 部署成本 | 中(向量库 + LLM API) | 低 | 极高(GPU 训练) |
| 适合场景 | 中小商家 SaaS | 意图极简单的 FAQ | 大企业海量数据 |
结论:中小商家场景下,RAG + LLM 是性价比最高的路线。我们也是基于这个判断,在赛能saillm中采用了这套架构。
3. 核心架构一览
┌─────────────────────────────────────────────────────────────┐
│ 用户(浏览器/小程序) │
└────────────────────────┬────────────────────────────────────┘
│ HTTPS + SSE
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway (Caddy) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Next.js API Routes (Edge Runtime) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │
│ │ /chat │ │ /upload │ │ /api/eval (异步评估) │ │
│ └────┬─────┘ └────┬─────┘ └──────────────────────────┘ │
└───────┼─────────────┼─────────────────────────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌──────────────────┐
│ Vector Store │ │ Document Parser │
│ (namespace) │ │ (PDF/Word/TXT) │
└───────┬───────┘ └──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM Gateway (流式代理 + 限流) │
│ ┌─────────┬──────────┬──────────┐ │
│ │ 主对话 │ 评估 LLM │ 兜底降级 │ │
│ └─────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────────────────┘
核心组件:
- API Gateway: Caddy 反代,负责 TLS 终止与 SSE 长连接管理
- Next.js API Routes: 业务逻辑入口,跑在 Edge Runtime 上
- Vector Store: 支持.namespace 隔离的向量库(Qdrant/Weaviate 均可)
- Document Parser: 异步解析上传文档,产出结构化分块
- LLM Gateway: 多模型代理,支持降级与限流
4. 核心实现拆解
4.1 多租户知识库:namespace 级强隔离
每个商家(tenant)有独立的向量库命名空间,任何检索路径都必须强制带 tenant_id,不依赖业务层逻辑。
关键代码(retrieve.ts):
// retrieve.ts
import { QdrantClient } from '@qdrant/js-client';
const client = new QdrantClient({ url: process.env.QDRANT_URL });
export async function retrieve(
query: string,
tenantId: string,
topK: number = 5
) {
// 1. 向量化用户问题
const queryVector = await embed(query);
// 2. 强制 namespace 过滤
const results = await client.search('knowledge_base', {
vector: queryVector,
filter: {
must: [
{ key: 'tenant_id', match: { value: tenantId } }
]
},
limit: topK,
with_payload: true,
});
// 3. 拼接 prompt 上下文
return results.map(r => r.payload);
}
关键设计:tenant_id 必须作为 filter.must 条件,不是 should(可选)。我们生产环境曾经踩过一次坑:某次重构误把 must 写成 should,导致跨租户召回。回归测试立刻挂了,但如果是 prod 直接上线就是事故。强烈建议给 retrieve 函数加单元测试,固定断言"非本 tenant 的 chunk 不能出现在结果中"。
4.2 文档解析与分块策略
商家上传的文档类型多样(PDF/Word/TXT/Markdown),分块策略直接影响召回率。我们采用结构感知分块:
# chunker.py
from langchain.text_splitter import RecursiveCharacterTextSplitter
def chunk_document(text: str, doc_type: str) -> list[str]:
# 不同文档类型用不同的分隔符优先级
separators_map = {
"markdown": ["\n## ", "\n### ", "\n\n", "\n", "。", " "],
"pdf": ["\n\n", "\n", "。", ";", " "],
"txt": ["\n\n", "\n", "。", " "],
}
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=separators_map.get(doc_type, ["\n\n", "\n", " "]),
)
return splitter.split_text(text)
实测数据(基于 30 万条商家真实文档):
| 分块大小 | 召回率 | 上下文准确度 | LLM token 成本 |
|---|---|---|---|
| 256 | 78.2% | 一般 | 低 |
| 512 | 96.4% | 优 | 中 |
| 1024 | 94.1% | 优 | 高 |
| 2048 | 89.7% | 模糊稀释 | 极高 |
结论:512 字符 + 64 重叠,是商家文档(产品手册、FAQ、价格表)的最优区间。
4.3 SSE 流式回复:为什么不用 WebSocket
商家客服场景的对话都是"问一句答一句"的请求-响应模式,SSE 足够且更优:
| 维度 | SSE | WebSocket |
|---|---|---|
| 协议复杂度 | 标准 HTTP | 需握手升级 |
| 反向代理 | 开箱即用 | 需特殊配置 |
| CDN 友好度 | 友好 | 不友好 |
| 重连逻辑 | 浏览器自动 | 手动实现 |
| 连接数压力 | 低(短连接) | 高(长连接) |
关键实现(app/api/chat/route.ts):
// app/api/chat/route.ts
import { retrieve } from '@/lib/retrieve';
import { callLLM } from '@/lib/llm-gateway';
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages, tenantId, sessionId } = await req.json();
const lastMessage = messages[messages.length - 1].content;
// 1. RAG 检索
const context = await retrieve(lastMessage, tenantId, 5);
// 2. 构建 system prompt
const systemPrompt = buildPrompt(context, tenantId);
// 3. 流式调用 LLM
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const llmStream = await callLLM(
[{ role: 'system', content: systemPrompt }, ...messages],
{ stream: true }
);
let fullResponse = '';
for await (const chunk of llmStream) {
fullResponse += chunk;
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ content: chunk })}\n\n`)
);
}
// 4. 异步触发评估(不阻塞响应)
triggerEvaluation(sessionId, lastMessage, fullResponse, context, tenantId);
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // 关键:禁止 Nginx 缓冲
},
});
}
避坑:Nginx/Caddy 默认会缓冲 SSE 流,导致客户端"等几秒后一次性蹦出来",完全失去流式效果。必须设置 X-Accel-Buffering: no,Caddy 还需在 Caddyfile 里加 flush_interval -1。
4.4 双 LLM 满意度自动评估
用户打分回收率不到 5%,所以我们用一个独立的 LLM 作为评审,从四个维度给每次对话打分:
# evaluator.py
from llm_gateway import call_llm
import json
EVAL_PROMPT = """你是客服质量评估专家。请对以下对话打分(1-5 分):
【客户问题】
{user_msg}
【客服回答】
{ai_msg}
【知识库召回片段】
{retrieved_context}
请从以下四个维度打分,并返回严格 JSON:
- relevance(相关性): 回答是否切题
- accuracy(准确性): 是否基于知识库事实,有无幻觉
- completeness(完整性): 是否完整回答了客户问题
- tone(语气): 是否符合客服场景
返回格式:
{{"relevance": 4, "accuracy": 5, "completeness": 4, "tone": 5, "reason": "..."}}
"""
async def evaluate(user_msg, ai_msg, retrieved_context):
prompt = EVAL_PROMPT.format(
user_msg=user_msg,
ai_msg=ai_msg,
retrieved_context=retrieved_context,
)
result = await call_llm(
[{"role": "user", "content": prompt}],
model="eval-model", # 用便宜的小模型即可
temperature=0,
response_format={"type": "json_object"},
)
return json.loads(result)
成本控制技巧:
- 评估模型用便宜的小模型(GLM-4-Flash / GPT-4o-mini),单价是主对话模型的 1/20
- temperature 设 0,保证打分稳定
- 仅在"对话结束时"触发评估,不是每个 token 都评估
数据沉淀:每天聚合每个 tenant 的满意度数据,产出:
- 整体均值(本周 4.8/5)
- 按 FAQ 类型分组的均值(找出薄弱模块)
- 按客服话术分组(找出高分模板,沉淀到知识库)
这套机制让商家第一次能拿到真实可操作的 AI 客服质量数据,而不是凭感觉判断"好像还行"。
5. 生产级考量
5.1 限流与降级
LLM API 是按 token 计费的,必须限流。我们在 LLM Gateway 层做了三道闸:
// llm-gateway.ts(精简版)
const RATE_LIMITS = {
free: { rpm: 60, tpm: 10000 },
pro: { rpm: 600, tpm: 100000 },
};
export async function callLLM(messages, options) {
const tenant = await getTenant(options.tenantId);
const limit = RATE_LIMITS[tenant.plan];
// 1. Redis 滑动窗口限流
const allowed = await rateLimiter.check(tenant.id, limit);
if (!allowed) {
return fallbackToCache(messages); // 2. 缓存兜底
}
try {
// 3. 主模型调用
return await primaryModel.call(messages, options);
} catch (e) {
if (e instanceof RateLimitError) {
return secondaryModel.call(messages, options); // 4. 降级到备用模型
}
throw e;
}
}
5.2 监控埋点
每次对话埋点三个核心指标:
chat_first_token_ms:首字节延迟(目标 <500ms)chat_total_ms:完整响应延迟(目标 <3s)chat_eval_score:本次评估均分
通过 Grafana 配置告警:
- P95 首字节延迟 >800ms 持续 5 分钟
- 单 tenant 评估均分 ❤️.5 持续 1 小时(知识库可能有问题)
- LLM 调用错误率 >2%
5.3 敏感词过滤
放在 LLM 调用前的预处理管道,用 AC 自动机一次扫描:
# sensitive_filter.py
import ahocorasick
A = ahocorasick.Automaton()
for word in load_sensitive_words():
A.add_word(word, word)
A.make_automaton()
def mask_sensitive(text: str) -> str:
for end, word in A.iter(text):
text = text[:end-len(word)+1] + '*'*len(word) + text[end+1:]
return text
10 万字文档过滤 P99 延迟 <2ms,可忽略。
6. 压测数据
在赛能saillm 生产环境实测:
| 指标 | 数值 | 说明 |
|---|---|---|
| 单 tenant 日均对话量 | 8 万次 | 中型商家峰值 |
| P95 首字节延迟 | 320 ms | 客户感知"秒回" |
| P95 完整响应延迟 | 2.1 s | 平均 380 token |
| 意图识别准确率 | 96.4% | 基于 5000 条标注 |
| RAG 召回 Top-5 命中率 | 98.7% | 知识库 1000 chunk 测试 |
| 评估 LLM 月成本 | ≈$80 | 8 万次/天 × 30 天 |
| 服务可用性 | 99.92% | 近 90 天 |
7. 避坑指南
-
跨租户召回事故
现象:某次重构后,A 商家客户咨询被 B 商家资料回答。
原因:retrieve 函数的 filter 从must改成了should。
解决:加单元测试断言"非本 tenant 的 chunk 不能召回",CI 强制通过。 -
SSE 在 Nginx 后变成"伪流式"
现象:本地开发流式正常,生产部署后变成等几秒蹦一坨。
原因:Nginx/Caddy 默认缓冲 SSE。
解决:响应头加X-Accel-Buffering: no,Caddy 加flush_interval -1。 -
长对话上下文膨胀
现象:超过 30 轮的对话,token 消耗暴涨,响应变慢。
解决:滑动窗口,只保留最近 10 轮 + 系统提示词;或在第 11 轮触发"摘要压缩",把前面 10 轮总结成 200 字。 -
评估 LLM 偶发 JSON 解析失败
现象:小模型偶尔返回带前后缀的非法 JSON。
解决:用response_format={"type": "json_object"}强制 JSON 模式;再加一层正则提取兜底。 -
向量库冷启动慢
现象:新租户刚开通时,知识库还没建,检索全空。
解决:开通时预置一份"通用 FAQ"(基于行业模板),让 AI 至少能回答行业通用问题。
8. 延伸思考:LLM 兜底还是 Fine-tune?
GPT-4 级模型零样本意图识别准确率 92%,但:
- 幻觉:会把"我要退货"误判为"申请退款"
- 成本:按 1k TPS、平均 15 token 算,月账单约 2 万美元
- 延迟:API 链路 P99 1.8s
结论:中小商家场景下,RAG(本地方案)为主 + LLM 兜底最现实。当 RAG 召回 Top-1 置信度 <0.6 时,触发 LLM 重判,并把回流结果做增量训练。兼顾准确率与钱包。
9. 写在最后
整套方案在赛能saillm(saillm.com)生产环境跑下来,最深刻的体感是:
"AI 客服"不是单点黑科技,而是把知识库分块、多租户隔离、SSE 流式、自动评估、限流降级这些看似琐碎的环节串成闭环。
我们做这个产品的初衷,不是跟大厂卷通用 AI,而是填补一个空白:让中小商家也能用上大公司级别的 AI 客服,门槛降到"上传资料、开客服、发链接"三步。
如果也在做 to B AI 产品的朋友,欢迎交流。
相关链接
- 赛能saillm 官网:https://www.saillm.com
- 注册体验:https://www.saillm.com/register
标签:智能客服、RAG、LLM、多租户、SSE、SaaS、Next.js、AI Agent
更多推荐

所有评论(0)