限时福利领取


背景痛点:传统客服系统为何总被吐槽“答非所问”

很多开发者第一次做客服机器人时,都会掉进同一个坑:用关键词+正则硬怼用户问题。
这种方式在意图识别、上下文保持、异常处理三方面几乎全线崩溃:

  • 意图识别:用户说“我订的咖啡还没送到”,关键词匹配到“咖啡”就返回商品介绍,完全不管“没送到”的投诉意图。
  • 上下文保持:用户先问“订单能取消吗”,机器人答“可以”,用户追问“那手续费呢”,机器人却忘了前面聊的是取消,直接给出退货手续费。
  • 异常处理:输入“@#¥%”或超长文本,系统要么 500,要么沉默,日志里只剩一句“pattern not matched”。

LLM 看似能“一把梭”,但纯生成方案又贵又慢,还不可控;于是混合架构(LLM+规则引擎)成了性价比最高的折中路线。

技术选型:纯 LLM vs 混合架构

维度 纯 LLM LLM+有限状态机
意图准确率 85% 左右,受 prompt 波动 规则兜底后稳定 ≥93%
响应延迟 1.2 s~2 s(逐字流式) 0.3 s 内完成状态跳转,LLM 仅用于生成答案
成本 按 token 计费,高并发爆炸 80% 走规则,0 成本
可控性 黑盒,可能“胡言乱语” 状态节点可白名单约束
扩展性 加业务需重训或改 prompt 新增状态节点即可

结论:Demo 阶段用混合架构,先把“能跑”做成“能扛”,再逐步把规则节点换成更精细的模型决策。

核心实现:FastAPI+状态机+Prompt 调优

1. 系统架构

用户问句 → FastAPI → 上下文管理器 → 状态机 → 规则或 LLM 生成答案 → 返回

2. 对话状态机设计

状态转移图(文字描述):

[START]
   │
   ├─关键词“订单”→ [ORDER] ──“取消”→ [ORDER_CANCEL]
   │                      │
   │                      └─“物流”→ [ORDER_LOGISTICS]
   │
   └─关键词“人工”→ [HUMAN] → 直接返回转人工提示

任何状态 3 轮无结果或超时 30 s,自动回到 [START] 并清空 history。

3. Prompt Engineering 小技巧

  • 在 system prompt 里先给 5 组 Few-shot,再让模型输出 JSON:
    {"intent":"ORDER_CANCEL","slot":{"order_id":"123"}}
  • 温度 0.1,top_p 0.3,把随机性压到最低。
  • 返回格式不符直接走异常分支,不让脏数据进入下游。

代码示例:可直接跑的 Python 源码

以下代码均符合 PEP8,核心算法时间复杂度 O(1)(字典跳转)。

对话上下文管理器

# context.py
from typing import Dict, List, Optional
import time

class DialogueContext:
    """带 TTL 的上下文容器,支持 callable 接口"""
    def __init__(self, ttl: int = 30):
        self.ttl = ttl
        self._cache: Dict[str, List[Dict]] = {}  # sid -> messages

    def __call__(self, sid: str) -> List[Dict]:
        """获取或初始化对话历史"""
        if sid not in self._cache:
            self._cache[sid] = []
        return self._cache[sid]

    def append(self, sid: str, role: str, text: str):
        hist = self.__call__(sid)
        hist.append({"role": role, "content": text, "ts": time.time()})

    def expire(self, sid: str) -> bool:
        hist = self.__call__(sid)
        if not hist:
            return False
        return time.time() - hist[-1]["ts"] > self.ttl

    def clear(self, sid: str):
        self._cache.pop(sid, None)

异常处理中间件

# middleware.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import traceback

def register_exception(app: FastAPI):
    @app.exception_handler(Exception)
    async def unified_handler(request: Request, exc: Exception):
        traceback.print_exc()
        return JSONResponse(
            status_code=200,  # 客服场景优先保可用
            content {"code": 500, "msg": "系统开小差,已通知工程师"}
        )

单元测试用例

# test_context.py
import pytest, time
from context import DialogueContext

def test_ttl_expire():
    ctx = DialogueContext(ttl=0.1)
    ctx.append("s1", "user", "hi")
    time.sleep(0.15)
()
    assert ctx.expire("s1") is True

运行 pytest 即可通过。

生产考量:让 Demo 像产品一样抗揍

1. 对话超时处理

  • 在 Redis 里给每个 sid 写 TTL=30 s,API 网关层统一拦截,超时返回“会话已过期,请重试”。
  • 上下文管理器同步清掉内存,防止脏数据。

2. 敏感词过滤

  • 采用 Double-Array Trie 预加载 3 万条敏感词库,复杂度 O(len(text)),单条 1 ms 内完成。
  • 命中即返回“亲亲,换个词试试~”,不计入 LLM 额度。

3. 性能压测数据

本地 4 核 8 G 容器,100 并发持续 60 s:

  • 纯规则路径:平均 QPS 1 180,P99 延迟 28 ms
  • 走 LLM 路径:平均 QPS 42,P99 延迟 1 450 ms
    结论:80% 流量走规则即可把账单打 2 折。

避坑指南:三次血泪总结

  1. 会话 ID 重复
    早期用时间戳+随机数,高并发碰撞 0.3%,切到 Snowflake 后归零。

  2. 内存泄漏
    上下文用 dict 缓存但忘了清,运行 3 天吃掉 4 G。加上“请求结束”事件监听,主动 ctx.clear(sid)

  3. LLM 返回格式异常
    直接抛给前端会 500。加一层 pydantic 模型校验,失败就降级到“转人工”兜底文案。

延伸思考:下一步往哪走

  • 结合知识图谱:把商品、订单、优惠券实体写入 Neo4j,状态机节点里先查图谱再决定回复,能回答“我买两杯咖啡用券后多少钱”这类需要计算的多跳问题。
  • 增加语音接口:接入 WebRTC + VAD,把用户语音先转文本再走现有流程,返回文本后用 TTS 流式播放,实现“边说边回”。

把这两块补齐,Demo 就能直接进化成 MVP,上线内测。

限时福利领取


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐