智能客服系统实战:基于 RAG + LLM 搭建面向商家的多租户 AI 客服架构

摘要:本文针对中小商家在落地 AI 客服时面临的知识库搭建门槛高、多租户数据隔离难、用户不打分导致质量评估失真、并发响应延迟等痛点,基于 RAG(检索增强生成)+ LLM + SSE 流式协议,详解如何构建支持多租户隔离、流式应答、双 LLM 自动评估的智能客服系统。通过完整的架构设计、关键代码实现及生产环境压测数据,开发者可掌握向量检索、tenant 隔离、SSE 推流、自动评估等关键技术。该方案已在赛能saillm(面向商家的 AI 营销与智能客服平台)生产环境验证,支撑单租户日均 8 万次对话,P95 响应延迟 320 ms,意图识别准确率 96.4%。


1. 背景痛点:商家 AI 客服的"四座大山"

调研了 200+ 中小商家(零售/餐饮/本地服务)后,我们总结出传统客服方案在 AI 化过程中的核心痛点:

  1. 知识库搭建门槛高
    传统 RAG 方案要求商家自己写 Markdown、做分块、调 embedding 模型。90% 的中小商家没有技术团队,这套流程直接劝退。

  2. 多租户数据串扰
    SaaS 化客服产品最容易翻车的点。A 商家的客户问"你们家奶茶多少钱",AI 从 B 商家的资料里捞答案,直接变成商业事故。市面不少"AI 客服 SaaS"实际只在业务层做过滤,向量层并未隔离。

  3. 用户打分数据失真
    主动要求用户打分的方案,实际回收率不到 5%,且集中在"非常不满意"和"非常满意"两极,中段数据缺失。无法准确评估 AI 客服的真实质量。

  4. 大模型响应延迟高
    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. 避坑指南

  1. 跨租户召回事故
    现象:某次重构后,A 商家客户咨询被 B 商家资料回答。
    原因:retrieve 函数的 filter 从 must 改成了 should
    解决:加单元测试断言"非本 tenant 的 chunk 不能召回",CI 强制通过。

  2. SSE 在 Nginx 后变成"伪流式"
    现象:本地开发流式正常,生产部署后变成等几秒蹦一坨。
    原因:Nginx/Caddy 默认缓冲 SSE。
    解决:响应头加 X-Accel-Buffering: no,Caddy 加 flush_interval -1

  3. 长对话上下文膨胀
    现象:超过 30 轮的对话,token 消耗暴涨,响应变慢。
    解决:滑动窗口,只保留最近 10 轮 + 系统提示词;或在第 11 轮触发"摘要压缩",把前面 10 轮总结成 200 字。

  4. 评估 LLM 偶发 JSON 解析失败
    现象:小模型偶尔返回带前后缀的非法 JSON。
    解决:用 response_format={"type": "json_object"} 强制 JSON 模式;再加一层正则提取兜底。

  5. 向量库冷启动慢
    现象:新租户刚开通时,知识库还没建,检索全空。
    解决:开通时预置一份"通用 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 产品的朋友,欢迎交流。


相关链接

标签:智能客服、RAG、LLM、多租户、SSE、SaaS、Next.js、AI Agent

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐