消费者权益保护机构近期点名AI客服时,用户说得最多的一句话是“被客服当猴耍”。按字面能力看,AI客服已经能完成欢迎语、关键词识别、常见问题检索和自动生成回答;但按实际体验看,很多系统仍会在用户着急处理订单、投诉、退款、改地址时,给出无用的重复话术,甚至找不到人工入口。真正的问题不是大模型不会说话,而是客服系统没有设计出“知道自己不能做什么”的边界,也没有把“转人工”和“问题闭环”当成核心路由。
这篇文章不讨论具体企业案例,而是把客服投诉中最常见的问题翻译成一套工程改进思路:如何设计一个既能保留大模型自然语言能力、又不会把用户困在死循环里的AI客服系统。文章会从根因分析开始,逐步拆解能力边界、会话上下文、转人工链路、可观测性和回归测试,最后给出一个最小可运行的示例。
1. AI客服被点名:问题到底出在模型,还是出在系统设计?
1.1 用户投诉里的三类高频现象
用户对AI客服不满,通常不是因为它回答得不够“像人”,而是因为它没有解决实际问题。整理公开投诉和低分评价后,大概可以归成三类。
第一类是答非所问。用户输入“我要退货”,系统返回的是优惠券领取方法;输入“我的订单怎么还没发货”,系统返回的是“您可以点击右下角物流查询”。这属于语义理解不到位或知识库检索范围选择错误。
第二类是循环套娃。用户无论输入什么,系统都回复同一句“您可以描述更多问题哦”,或者在几个固定卡片之间来回跳转。反复几次之后,用户既没有得到答案,也没有退出路径。
第三类是转人工困难。用户已经明确输入“人工客服”“投诉”“我要找真人”,系统仍然用FAQ话术试图拦截,或者提示“当前人工繁忙”,然后让用户继续等待。这已经不是模型能力问题,而是服务流程设计问题。
这三类现象放在技术架构里,指向同一个结论:AI客服的对外表现,不是单个模型的输出决定的,而是由意图识别、知识检索、会话管理、业务接口、转人工策略共同决定的。模型只负责最后一个生成环节。
1.2 模型能力不等于客服系统能力
很多人测试大模型时会觉得它足够聪明。随便丢一句“帮我写一篇退货说明”,模型能写出完整内容,于是直接把模型接到客服后端,结果线上体验立刻变形。
原因是客服场景和创作场景完全不同。用户来客服系统的目标是办理业务,不是看模型发挥。用户期望回答能够对应到订单状态、退换货政策、账户问题、发票信息等真实数据。模型如果没有拿到订单上下文,再会写话术也回答不准确。
更麻烦的是,模型天然倾向于给出流畅、可信、完整的句子,而不是直接承认“我不知道”。在缺少知识库、缺少数据库读取权限、缺少转人工策略时,系统仍然会生成看起来正确但实际无效的内容,并让用户误以为业务已经处理完成。
客服系统设计必须补上这层缺口。要让模型在能确定的范围内回答,在不能确定时明确说不知道,知道要转人工时立刻转交,而不是继续拖延用户时间。
1.3 客服系统的责任边界
一个合格的客服系统,至少要回答清楚三个问题:能处理哪些请求,不能处理哪些请求,不能处理时怎么退出。
能处理的请求,指的是系统有对应知识页、业务流程或业务接口。比如物流查询、优惠券使用规则、售后时效、地址变更条件。这部分是AI客服的正常工作范围。
不能处理的请求,包括用户表达强烈不满、要求人工介入、遇到账号敏感操作、问题超出设定知识范围等。此时系统必须提供人工或工单出口。
退出的路径必须始终存在,并且不能把“退出”藏到三级菜单后面。很多被诟病的AI客服,不是在回答上出错,而是根本不提供有效出口。架构上应当把“退出或转人工”当作核心路由,和“大模型回复”放在同等重要的位置。
可以用一张表来罗列系统边界:
| 请求类型 | 系统应做的事 | 缺了会怎样 |
|---|---|---|
| 政策咨询 | 用知识库检索回答,附内容来源 | 答案看着合理,实际无依据 |
| 订单查询 | 调用订单系统,返回真实状态 | 模型猜测单号状态,制造假故障 |
| 地址修改 | 核验身份再执行,并要求用户确认 | 权限越权或误操作 |
| 投诉 | 记录事件、转人工或创建工单 | 用户情绪升级,无法追踪 |
| 超范围问题 | 拒绝回答,并推荐人工 | 模型幻觉扩大,用户走投无路 |
2. 构建AI客服前,先定义“服务底线”和“能力边界”
2.1 先列意图清单和动作清单
很多客服项目仓促上马,让模型自由发挥,美其名曰“智能”。但落地时应该先做两件看起来很传统的工作:定义意图,定义动作。
意图是用户这句话想解决什么问题,例如“查物流”“催发货”“申请退款”“改地址”“开发票”“投诉”。每个意图都要配一个处理策略。
动作是系统为了完成这个意图需要的操作,例如调用订单查询接口、创建售后单、推送给CRM系统。对于动作型的处理,不能只靠模型生成一句“已经帮您申请”,必须有后端真实操作作为依据。
一份最小意图-动作表可以是这样的结构:
| 意图 | 动作 | 是否需要二次确认 | 失败兜底 |
|---|---|---|---|
| 查物流 | 调用物流接口 | 否 | 返回接口报错,提示稍后再试或转人工 |
| 催发货 | 创建催发货任务 | 是 | 转工单,通知仓储人员 |
| 申请退款 | 进入退款引导并校验订单 | 是 | 若用户订单异常,转人工 |
| 修改收货地址 | 校验订单状态,调用修改接口 | 是 | 拒绝并说明原因 |
| 投诉 | 生成投诉工单 | 否 | 若系统不可用,马上留电话回拨 |
这一步可以帮助团队确认哪些功能可以做强交互闭环,哪些知识类问题只需要“检索并回复”。最怕的是把所有问题都交给一个万能Prompt,用户遇到问题时不是拿到明确结果,而是被要求重新描述。
2.2 给每个高风险动作配二次校验
客服系统里最常见的风险不是模型说错一句话,而是系统根据模型的输出直接执行了业务操作。
比如模型从用户对话中抽取了“退款金额”,然后拼成一个JSON,调用退款接口。如果抽取结果有误,或者用户说“我要退昨天买的那个”,模型没有明确关联到具体订单,退款操作就可能出错。
设计原则是:凡是涉及资金、优惠券、隐私资料、订单状态变更的动作,都必须在动作执行前加入二次确认页面或确认话术。系统可以先给用户展示“您确认要提交以下申请吗”的确认信息,等用户选择“确认”后再真正提交。
这看起来只是产品交互规则,但工程上必须做到:动作确认和动作执行之间保持状态一致性。不能出现用户已经确认,请求头还是旧参数;也不能出现用户点了一次提交,网络超时后系统自动重试导致重复创建售后单。幂等键和重试机制要在后端提前设计好。
2.3 把“转人工”当作一等路由,而不是附加功能
转人工不应该由模型自己决定心情好坏,也不应该藏在某个意图分支后面。更合理的做法是,把“用户是否要求转人工”和“系统判断自己无法处理”当成路由入口条件,放在所有逻辑的最前面。
这样做的原因很简单。用户说“转人工”时,意图已经非常明确,继续让AI拦截只会制造更多投诉。系统在处理用户输入前,可以先经过一轮规则、分类器或语义模型,命中“人工意愿”时直接进入人工队列,携带会话摘要和用户已描述的问题,而不是把之前所有上下文丢掉。
转人工还应该具备优先级。投诉、隐私泄露、资金异常、长时间未解决等场景,可以标记高优先级。普通业务咨询转人工时,可以减少排队等待。
2.4 定一个可验收标准
上线前后需要设置几个指标,让团队知道系统到底是变好了还是变差了。基础指标建议包含首答有用率、转人工成功率、人工接通率、会话重复转交率、用户重新进入率。
首答有用率指第一次回答是否解决了用户问题。这个指标不能只靠模型自己评分,需要用户在每条回答后给出“有帮助”或“没帮助”的轻量反馈,并由人工质检抽样确认。
转人工成功率和人工接通率反映的是出口质量。如果用户转人工后等待时间过长,或者转人工后要重新描述历史问题,即使AI回答正确率很高,整体体验也无法改善。
还需要监控“用户放弃率”。当用户已经开始投诉或输入明确业务诉求,却在一段时间后直接关闭会话,说明系统没能解决问题。这个信号比任何满意度问卷都真实。
3. 最小实现:支持“转人工”的大模型客服后端
3.1 技术选型和目录结构
为了把前面的设计思路落地,这里用FastAPI实现一个最小后端,重点不在于所有生产功能,而在于演示完整链路:接收用户消息、管理会话历史、判断是否转人工、调用大模型、返回结构化响应。
示例使用大模型服务提供的OpenAI兼容接口,地址通过环境变量注入。你可以把它替换成自己部署或购买的任意兼容服务;如果本地只有vLLM、Ollama、模型网关,则只需要改地址和模型名。
目录结构可以简化成:
ai-customer-service/ ├── app.py ├── requirements.txt ├── .env └── README.mdrequirements.txt:
fastapi uvicorn[standard] httpx pydantic.env应包含大模型地址、模型名称、密钥等信息。这里只列占位项,真实项目请通过密钥管理或K8s Secret加载:
LLM_BASE_URL=http://127.0.0.1:8001/v1/chat/completions LLM_API_KEY=sk-your-key LLM_MODEL=your-chat-model如果不方便填写真实key,可以先使用本地兼容服务做联调。下面代码不做任何版本绑定,重点看逻辑结构。
3.2 会话消息管理
会话历史是避免“用户重复描述问题”的基础。最简单的方式是使用内存字典保存消息,demo里已经够用;生产系统要换成Redis或数据库,并加上过期时间。
app.py核心代码:
from typing import Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel import httpx import os app = FastAPI() MEMORY: dict[str, list[dict]] = {} MAX_HISTORY = 10 MAX_AGE_SECONDS = 60 * 30 HUMAN_WORDS = [ "转人工", "人工客服", "真人", "我要投诉", "必须要人处理", "人工", ] SYSTEM_PROMPT = """你是一个电商平台的在线客服助手。 你的工作范围包括:售前咨询、订单查询、退换货政策、发票说明、优惠活动规则。 如果用户需要人工介入,请明确告知将转接人工客服,不要重复询问用户问题。 回答时只能依据给定业务上下文,不要编造订单号和金额。 如果无法确认,请回答“当前问题需要人工协助”。""" def get_history(session_id: str) -> list[dict]: data = MEMORY.get(session_id, []) # 简单过期处理 return [item for item in data if item.get("ts", 0) + MAX_AGE_SECONDS > int(__import__("time").time())] def append_message(session_id: str, role: str, content: str) -> None: data = get_history(session_id) data.append({"_session_id": session_id, "role": role, "content": content}) if len(data) > MAX_HISTORY: data = data[-MAX_HISTORY:] MEMORY[session_id] = data def remove_sys_fields(messages: list[dict]) -> list[dict]: return [{"role": item["role"], "content": item["content"]} for item in messages]这段代码中最容易被忽视的是两个函数:append_message负责写入,remove_sys_fields负责在调用模型前清理内部字段。不要把标记时间、会话ID等字段直接传给大模型接口,否则模型可能学到异常结构。
3.3 转人工检测与会话路由
在调用大模型之前,先做一次转人工判断。这样能让“我要投诉”这类请求立即进入人工入口,不走模型生成流程。
class ChatRequest(BaseModel): session_id: str message: str user_id: Optional[str] = None class ChatResponse(BaseModel): reply: str need_human: bool reason: str = "" def should_request_human(text: str) -> bool: t = text.strip().lower() for word in HUMAN_WORDS: if word in t: return True return False def is_obvious_failure(text: str) -> bool: failure_mark = ["系统繁忙", "查询失败", "接口异常", "无法处理"] return any(word in text for word in failure_mark) async def call_llm(session_id: str, user_message: str) -> str: url = os.getenv("LLM_BASE_URL", "http://127.0.0.1:8001/v1/chat/completions") api_key = os.getenv("LLM_API_KEY", "") model = os.getenv("LLM_MODEL", "chat-model") history = remove_sys_fields(get_history(session_id)) payload = { "model": model, "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + history, "temperature": 0.2, "top_p": 0.8, "max_tokens": 600, } headers = {"Authorization": f"Bearer {api_key}"} async with httpx.AsyncClient(timeout=20) as client: resp = await client.post(url, headers=headers, json=payload) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] @app.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code=400, detail="消息不能为空") append_message(req.session_id, "user", req.message) if should_request_human(req.message): append_message( req.session_id, "assistant", "好的,正在为您转接人工客服,请稍候。", ) return ChatResponse( reply="好的,正在为您转接人工客服,请稍候。", need_human=True, reason="user_request_human", ) try: reply = await call_llm(req.session_id, req.message) except Exception: append_message( req.session_id, "assistant", "系统正在处理中,请留下联系方式,我们会安排人工回访。", ) return ChatResponse( reply="系统正在处理中,请留下联系方式,我们会安排人工回访。", need_human=True, reason="llm_call_failed", ) if is_obvious_failure(reply) or "需要人工协助" in reply: append_message(req.session_id, "assistant", reply) return ChatResponse( reply=reply, need_human=True, reason="llm_says_need_human", ) append_message(req.session_id, "assistant", reply) return ChatResponse(reply=reply, need_human=False, reason="")这段代码体现了两个关键判断:用户主动要求人工时,直接路由到人工;大模型或底层接口出现异常时,不允许系统沉默,而是转人工或留电回访。
这里的转人工还不是真正的队列写入,只是返回了一个结构化标记。生产实现中,reason字段需要被下游链路解析,用来创建工单或推送到坐席工作台。
3.4 启动与本地验证
安装依赖并启动:
pip install -r requirements.txt uvicorn app:app --reload --port 8000用两个请求验证转人工链路是否奏效:
curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{"session_id":"s1001","message":"我要投诉"}'预期返回:
{ "reply": "好的,正在为您转接人工客服,请稍候。", "need_human": true, "reason": "user_request_human" }再试一个政策咨询请求:
curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{"session_id":"s1001","message":"7天无理由退货的时限怎么算?"}'如果大模型服务可用,会返回一段知识型话术。这里要注意:本地代码中并没有接入真正的知识库,所以线上环境还需要在SYSTEM_PROMPT里注入检索后的知识片段,或者把回答逻辑改成RAG链路。
4. 多轮对话为什么经常让用户重复描述问题?
4.1 上下文丢失的三种具体表现
客服对话最常见的一种差评是:用户在第一轮说了订单号,AI回复不相关;用户第二轮又提了一次订单号,AI还是像第一次一样问“请问您的订单号是多少”。原因通常是以下几个。
第一,后端没有保存会话历史,每一轮请求都被当成新对话。第二,有保存历史,但历史没有被拼进模型请求。第三,历史被拼进去了,但内容超过模型上下文窗口,早先的关键信息被截断。
工程上不仅要把消息保存下来,还要在每次调用前明确把当前用户的诉求、订单号、问题类型整理成结构化的状态信息,而不是只塞一堆聊天记录。
4.2 会话结构的最小模型
推荐从“消息列表”升级为“用户状态”结构。一个完整会话包含以下内容:
| 字段 | 示例 | 用途 |
|---|---|---|
| session_id | s1001 | 会话唯一标识 |
| user_id | u881 | 已登录用户标识 |
| context | {"order_id": "A123", "issue": "改地址"} | 多轮抽取的实体 |
| history | [{"role":"user","content":"..."}] | 原始消息 |
| state | WAITING_CONFIRM | 当前会话状态 |
| created_at | 2025-07-01 10:00:00 | 创建时间 |
| last_active_at | 2025-07-01 10:05:00 | 最后活跃时间 |
当用户说“我要改订单A123的地址”,系统把order_id和issue存到context。下一轮用户说“改成北京朝阳区”,模型只需要结合当前上下文,就能知道用户指的是“订单A123改地址到北京朝阳区”,而不是重新询问。
所以上下文管理不只是把聊天记录拼起来,还要做字段抽取、状态更新和过期处理。
下面是简化版状态记录逻辑:
SESSION_CONTEXT: dict[str, dict] = {} def update_context(session_id: str, order_id: str = None, issue: str = None) -> None: ctx = SESSION_CONTEXT.setdefault( session_id, {"order_id": None, "issue": None, "need_confirm": False}, ) if order_id: ctx["order_id"] = order_id if issue: ctx["issue"] = issue4.3 上下文超过长度后怎么办
模型上下文窗口再大,也扛不住客服会话长期运行。推荐两种处理方式:裁剪短期历史和生成会话摘要。
短期消息历史可以只保留最近几轮完整原文,用于理解用户刚刚的表达。再往前的消息,用一段JSON格式的摘要代替。摘要需要包含用户ID、核心诉求、已确认订单号、已提供的解决方案、未解决点。
{ "summary": "用户想修改订单A123的收货地址,要求改成北京市朝阳区", "order_id": "A123", "question_type": "modify_address", "status": "wait_confirm", "already_tried": [] }这样既压缩了token,又保留了对坐席交接最有用的信息。
4.4 多轮确认的体验设计
当系统识别到用户有业务操作意图后,不要每次都让用户从头表达。比如先问“您要处理哪个订单”,用户回答“A123”,下一个分支直接问“是否把地址修改为北京市朝阳区?”。
这类交互本质上是在状态机里前进,每一步都把前一步结果带进下一步。和自由多轮对话不同,客服业务动作必须用确定性状态约束,避免模型自由发散的潜风险。
5. 避免大模型“一本正经胡说八道”的工程化措施
5.1 给模型画出“会”与“不会”的范围
AI客服被投诉的很大一部分原因是幻觉。用户问“我能不能全额退款”,模型直接回答“可以退款”,但实际业务规则是“仅7天内且商品未拆封才支持全额退款”。
避免幻觉的第一道关卡不是更好的幻觉检测模型,而是给模型提供结构化的知识片段。调用模型前,将和用户问题最相关的政策、订单、FAQ片段检索出来,放入context,并让模型只能基于这些片段回答。
系统提示词中应当明确写清楚:
你只能基于提供给你的知识片段回答。 如果知识片段里没有对应内容,不要推测。 回答时列出引用编号,方便用户核实。示例注入知识片段的Prompt结构:
{ "role": "system", "content": "请基于以下知识回答用户问题。\n[1] 7天无理由退货时限:自签收次日起7日内可以申请。\n[2] 已拆封商品若影响二次销售,不在无理由退货范围。\n用户问题:我昨天拆了包装,还能申请7天无理由退货吗?" }5.2 不要用“自由自然语言”直接执行业务动作
很多大模型应用喜欢让模型输出JSON,然后程序解析JSON去执行动作。这种方式在开发阶段很方便,但风险在于:模型输出不是稳定的程序字段,可能存在字段遗漏、动作误判、金额乱写、订单号幻觉。
更稳妥的做法是给模型提供固定的工具声明或函数列表。模型只能选择其中某个动作,并输出动作参数。
{ "function_name": "modify_delivery_address", "parameters": { "order_id": "A123", "new_address": "北京市朝阳区" } }服务端在真正执行前需要校验三件事:
- order_id是否存在且属于当前用户。
- 用户是否已经完成身份认证。
- new_address是否完整,且需用户再次确认后才能提交。
即使模型给出了JSON,服务端也不能直接拿它操作数据库,而要解析成内部结构并走正规业务校验逻辑。
5.3 外部工具调用要白名单化
如果客服系统需要调用订单、售后、发票等外部接口,建议把所有动作集中在一个工具调度层,并对每个工具定义权限、超时、失败返回。
| 工具 | 可调用的角色 | 是否高级风险 | 失败策略 |
|---|---|---|---|
| 查询订单 | 已登录用户,且只能查本人订单 | 否 | 返回错误并提示重试 |
| 发起退款申请 | 已登录用户 + 二次确认 | 是 | 记录操作日志,转人工 |
| 修改地址 | 已登录用户 + 订单状态校验 + 二次确认 | 是 | 失败保留上下文,允许重试 |
| 下发短信验证码 | 需风控策略 | 是 | 频率限制,失败转人工 |
模型永远不能直接拥有数据库连接、文件系统或服务器命令执行权限。系统生成的工具调用,需要经过权限过滤和参数校验。
5.4 RAG检索时要设置置信下限
接入RAG方案时,很多团队把相似度阈值设置得过低,导致检索结果和当前问题完全无关,模型最后只能靠复杂话术凑出一个回答。
建议为知识库检索设置“有答案才回答”的下限。如果检索结果top1相似度低于阈值,就应承认“暂未找到相关答案”,并转人工或引导用户换一个问法。
同时要记录检索出来的知识标题和相似度分数。这样出现问题后,运营人员可以快速定位是不是知识库缺少某个主题,而不是直接怀疑大模型能力有问题。
6. 转人工链路设计:不是“排队”,而是“交接”
6.1 交接对象和交接材料
“转人工”如果只是把用户放进一个等待队列,本质上就是把问题从AI的烂摊子丢给人。更完整的做法是“交接”,而交接的前提是交接材料。
人工坐席接手时,应当看到用户的服务档案,至少包含:
- 用户基础信息,昵称、会员等级、是否已完成实名。
- 历史对话摘要,标题式列出用户已经说过的内容。
- 问题类型和严重等级,例如“退款失败”“投诉物流”“政策咨询”。
- 当前会话已尝试过的方案。
- 系统连接的业务上下文,例如已识别的订单号。
会话摘要可以由大模型自动生成,但必须在转人工发生后立即落库。下面是交接数据示例:
{ "ticket_id": "TK20250701001", "user_id": "u881", "session_id": "s1001", "category": "complaint", "priority": "high", "reason": "user_request_human", "summary": "用户投诉物流超过3天未更新,要求客服跟进派送并说明赔付规则。", "context": { "order_id": "A123", "carrier": "示例快递" }, "callback_phone_masked": "138****1234" }交接数据里的手机号、地址等个人信息必须做脱敏处理和权限隔离,只允许有服务权限的坐席查看。客服页面不能显示明文敏感信息。
6.2 人工入口在会话中的位置
可以这样设计聊天流程:
| 用户动作 | 系统动作 | 预期结果 |
|---|---|---|
| 输入“人工” | 检测到人工意图 | 不问原因,直达人工队列 |
| AI连续2次未能回答 | 自动创建优先工单 | 坐席主动回拨或在线接入 |
| 用户点击“不满意” | 记录失败原因 | 作为后续质检数据 |
| 夜间无人坐席 | 引导用户留联系方式 | 生成次日回访任务 |
夜间值班是重要场景。很多用户时间因为深夜网购问题找客服,人工队列却无人接听。此时不能只显示“当前非工作时间”,而是应自动提交工单并明确告知用户回访时间,让用户感受到问题没有被丢弃。
6.3 排队过程的体验控制
如果人工确实繁忙,需要给用户排队预期。这里不应该让用户一直干等,只显示“正在排队,前面还有10人”,而应该提供三种选择:等待在线接入、选择离线留言、留下电话等回拨。
排队过程中,AI状态也需要同步给用户。不要出现用户已经转人工,AI还在按照机器人逻辑自动回复知识库内容的情况。这会让用户认为自己又被丢回了机器人循环。
7. 日志、评测与后续迭代
7.1 不要只记录问题和答案
很多客服项目上线后,日志只记录了“好”或“不好”的最终评价,缺少过程信息。要定位失误,必须记录每一次请求的完整上下文。
建议记录字段包括:
| 字段 | 说明 |
|---|---|
| session_id | 每次会话唯一标识 |
| user_text | 用户原始输入 |
| model_input | 发送给模型的最终消息片段 |
| model_output | 模型生成的结果 |
| retrieval_scores | 知识检索分数 |
| intent_name | 命中的意图或分类结果 |
| route_result | 是否转人工,原因是哪个 |
| error_code | 接口异常或业务异常码 |
| latency_ms | 响应时长 |
| user_feedback | 用户对回答的有用性评价 |
| timestamp | 完整时间戳 |
这些数据要能够按会话ID串起来,也要支持按意图、按模型版本、按时间范围做统计。
7.2 用户点“转人工”和“不满意”是最重要的标注信号
AI客服经常会遇到用户输入“你回答的根本不是我要的”然后点转人工。这类信号可以直接作为负面样本。
工程做法是把这类事件异步写回标注队列。运营人员对样本进行人工复核后,可以选择加入回归测试集或调整Prompt。不要直接让模型拿单挑样本做微调,那样容易过拟合,最好的方式是先积累一批质量稳定的评估集。
一个实用的反馈闭环是:
- 用户点“不满意”
- 系统记录当前问题、回答、上下文
- 运营人员分类失败原因
- 修复知识库或调整提示
- 启动回归测试
- 发布并观察指标变化
7.3 用历史会话做回归测试
大模型应用不能只靠上线时的一次冒烟测试。Prompt或知识库变更后,需要先跑一批历史真实会话,检查哪些会话从正确变为错误。
最小回归集可以包含100到200条会话。每条会话由运营和研发共同标注期望结果,标注内容包括:应该走人工、应该附知识链接、应该拒绝回答、应该返回订单状态等。
建立方便的工具:
test_cases = [ { "id": "case_001", "messages": ["我要退货", "我的订单是A123"], "expect": "query_refund_policy", }, { "id": "case_002", "messages": ["人工客服", "快一点"], "expect": "human_handoff", }, ]跑回归时,用同一条Prompt和同一组测试数据在不同模型版本下对比输出,观察哪些版本更稳定。
7.4 流量峰值下的防护
客服系统可能在大促、活动日出现流量和情绪集中上升的情况。底层模型服务在被大量请求压垮时,系统不能把每个请求都发往模型,然后静默等待超时。
建议增加限流、降级和队列三层能力。高峰期可以对知识类问题启用缩减模型或快速检索回复;对高风险动作继续限制为人工处理;对模型服务超时立刻降级为常见问题和转人工入口。
用户在大促期间最需要的不是长篇大论,而是“有人接住问题,确认正在处理”。宁可给一句简短真实回复,也不让用户看见一个转圈后崩溃的页面。
8. 从“被戳脊梁骨”到“习惯性被信任”的落地清单
8.1 把常见失败模式整理成运维手册
不同团队踩过的坑大致相同。建议在项目启动时,就把下面这些常见现象写进排查手册,遇到同类问题可以按表处理。
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| 用户说“人工”,AI继续答FAQ | 人工意图没有在路由最前面命中 | 将人工关键词检测放到LLM调用之前 |
| 转人工成功,但坐席不知道用户说过什么 | 没有生成交接摘要 | 在转人工时保存分类、实体、历史摘要 |
| 用户问了3次同样问题,AI仍像首次聊天 | 会话历史未保存或未传给模型 | 持久化历史,并检查模型请求是否包含历史 |
| 答案像真的,但业务规则是反的 | 没有注入知识库片段 | 引入RAG,并限制模型只能基于知识片段回答 |
| 系统偶尔读不到订单,询问两次后自己编造 | 缺少接口错误兜底 | 校验失败时直接声明无法处理并转人工 |
| 用户点确认却被重复扣款或重复提交 | 缺少幂等键和状态校验 | 接入幂等ID,动作状态做幂等控制 |
| 人工不在线,系统还是在假装解决问题 | 没有夜间/无人值守策略 | 非工作时间自动转工单并承诺回访时间 |
8.2 安全和隐私是客服系统绕不开的红线
AI客服处理的大部分是个人信息和订单数据。给模型传入user_id、order_id时要确保调用方做了身份认证;管理员接口要使用鉴权,不能让任意用户通过绑定其他人ID去查询订单。
日志里的手机号、地址、银行卡号等敏感信息要脱敏。模型服务端日志也会记录请求内容,如果不做脱敏,敏感数据会进入模型厂商日志,属于高风险行为。
客服系统还需要支持“清除会话”能力。用户可以要求删除历史对话和个人信息。系统层面应该记录数据删除请求,并能从存储和日志中移除对应内容。
8.3 上线前检查清单
在正式发布前,建议逐条核对这些点:
- [ ] 是否给模型限定回答范围。
- [ ] 是否设置“不知道”时的拒绝话术。
- [ ] 是否把“用户主动转人工”放在路由最前。
- [ ] 是否实现转人工时的会话摘要传递。
- [ ] 是否设置夜间、无坐席、服务异常时的处理路径。
- [ ] 是否为高风险业务动作增加二次确认和幂等控制。
- [ ] 是否记录模型输入输出和检索分数。
- [ ] 是否设置请求超时与降级策略。
- [ ] 是否对日志中的个人信息脱敏。
- [ ] 是否建立人工客服抽检的评估集。
- [ ] 是否对模型输出做了不允许执行外部操作的隔离。
- [ ] 是否提供会话删除和数据清除机制。
8.4 后续可以扩展的方向
当基础链路稳定后,可以继续提升两件事:第一,扩大确定性业务闭环的比例,让更多订单查询、售后申请能在AI侧完成,而不是把所有动作都丢给人工;第二,建立持续的对话质量评估闭环,把线上用户的负面反馈转化为知识库或模型版本迭代依据。
更复杂的项目可以进一步引入语音客服、异步外呼回访、多模态工单、实时坐席辅助。每个扩展都要回到同一个原则:无论技术多么先进,用户被卡住时,一定有出口,有人负责,有结果反馈。
AI客服愿意承认“不知道”,同时能让用户快速找到真人,这不是模型退步,而是客服系统真正具备了服务意识。这样的系统即使偶尔犯错,用户也会觉得它是一个可靠的入口,而不是一个被推出来敷衍人的机器人。