智能客服如何按客户类型切换话术?一套支持“渠道标签 + 用户自选 + 对话推断“的分类架构设计

写在前面

我长期专注于智能机器人客服软件(冰石)的开发。最近一个保险行业的客户提出了一个非常典型的需求:

同一个 AI 客服,要面对小企业主白领家庭主妇 等不同客户群,希望对每一类客户都使用不同的话术

客户的疑问是:到底是给每类客户用不同的提示词 ?还是写一个大提示词,让模型自己判断客户类型再切换话术?

这个问题在做大模型应用落地的同行里其实非常普遍------今天分享一下我们的完整设计思路、踩过的坑,以及一份可参考的工程实现。


一、先回答最初的问题:合并写还是拆开写?

我直接给结论:拆开写,再用一个分类决策层在外部路由

很多团队一开始会图省事,把所有话术塞进一个 system prompt:

复制代码
你是保险客服。如果用户是企业主,用 A 话术......
如果用户是白领,用 B 话术......
如果用户是家庭主妇,用 C 话术......

听起来很合理,但上线后会遇到三个问题:

  1. Token 浪费:每轮对话模型都要"读"完三套话术再挑一套,请求成本飙升。
  2. 话术串味:本该用主妇话术的回答里,突然冒出"现金流""税务筹划"这种企业主词汇------大模型在多套规则里很容易"漏风"。话术越细,串味越严重。
  3. 维护痛苦:后期合规要求改某一类话术,改动牵连其他两类,回归测试成本很高。

正确做法是把判定逻辑从提示词里剥离出来,放在外层的代码里完成;模型只专注于"用某一套话术服务这个用户"。


二、整体架构

我们设计的架构是这样的:

复制代码
                ┌──────────────────────┐
                │       用户进线       │
                └──────────┬───────────┘
                           │
              ┌────────────┴────────────┐
              │   分类决策层 Classifier │
              │                         │
              │  输入:                 │
              │  ① 渠道/标签            │
              │  ② 用户自选             │
              │  ③ 对话推断             │
              │                         │
              │  输出:user_type + 置信度│
              └────────────┬────────────┘
                           │
              ┌────────────┴────────────┐
              │   提示词路由 Router     │
              │  按 user_type 加载      │
              │  对应 system prompt     │
              └────────────┬────────────┘
                           │
              ┌────────────┴────────────┐
              │       LLM 调用          │
              └────────────┬────────────┘
                           │
              ┌────────────┴────────────┐
              │  回写:识别到的新信号   │
              │  更新用户画像           │
              └─────────────────────────┘

核心思路是:三种来源的信号都写入同一份"用户画像"对象,分类器每轮基于最新画像出结论。


三、数据结构:把"画像"明确下来

第一步先定义清楚每个会话需要维护什么状态。这一步看似简单,但我们经验是:会话状态设计不清晰,后期所有调试都会变成猜谜游戏

复制代码
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

class UserType(str, Enum):
    SMALL_BUSINESS_OWNER = "small_business_owner"
    WHITE_COLLAR = "white_collar"
    HOMEMAKER = "homemaker"
    UNKNOWN = "unknown"  # 兜底类型

class Source(str, Enum):
    CHANNEL_TAG = "channel_tag"       # 渠道带入
    USER_DECLARED = "user_declared"   # 用户自选
    INFERRED = "inferred"             # 对话推断

@dataclass
class TypeSignal:
    """一条分类信号"""
    user_type: UserType
    source: Source
    confidence: float          # 0.0 ~ 1.0
    evidence: str              # 依据,便于排查
    turn: int                  # 在第几轮产生

@dataclass
class UserProfile:
    """会话级用户画像"""
    user_id: str
    session_id: str
    signals: list[TypeSignal] = field(default_factory=list)
    current_type: UserType = UserType.UNKNOWN
    current_confidence: float = 0.0
    locked: bool = False       # 是否锁定(用户明确声明后可锁定)

几个关键点:

  • 每条信号独立保存,不要让新信号直接覆盖旧信号。所有历史信号都留下来,融合时再综合判断。
  • evidence 字段必填。生产环境出问题时,这是排查"为什么用了错的话术"的命根子。
  • locked 字段:用户明确声明身份后锁定,避免后续推断把它改飞。

四、三个分类信号源的实现

信号源 1:渠道标签(最早可用)

最便宜、最快的信号。会话刚创建时就能拿到:

复制代码
CHANNEL_TYPE_MAP = {
    "boss_community_wechat": UserType.SMALL_BUSINESS_OWNER,
    "linkedin_ad":           UserType.WHITE_COLLAR,
    "parenting_app":         UserType.HOMEMAKER,
    # ... 其他渠道
}

def init_profile_from_channel(user_id, session_id, channel) -> UserProfile:
    profile = UserProfile(user_id=user_id, session_id=session_id)
    if channel in CHANNEL_TYPE_MAP:
        profile.signals.append(TypeSignal(
            user_type=CHANNEL_TYPE_MAP[channel],
            source=Source.CHANNEL_TAG,
            confidence=0.6,        # 中等置信度
            evidence=f"channel={channel}",
            turn=0,
        ))
    return profile

踩坑提醒 :渠道标签的置信度建议给 0.5~0.7,别给到 0.9。原因很现实:企业主社群里也会混进来打工人,宝妈群里也有自己开店的。给太高会让推断信号没机会修正它。

信号源 2:对话推断(边聊边判断)

这是最重的一块,建议用一个独立的"分类小调用",与主对话解耦------主对话归主对话,分类归分类,互不干扰。

我们提供两套实现,按成本和精度选:

方式 A:规则 + 关键词(便宜,适合冷启动)
复制代码
KEYWORDS = {
    UserType.SMALL_BUSINESS_OWNER: [
        "我们公司", "我开了个", "员工", "现金流", "客户款", "营业执照", "老板"
    ],
    UserType.WHITE_COLLAR: [
        "我上班", "公司给上的", "五险一金", "公积金", "加班", "打工", "同事"
    ],
    UserType.HOMEMAKER: [
        "我家孩子", "我老公", "我老婆", "我不上班", "全职", "带娃", "家庭"
    ],
}

def rule_based_infer(message: str, turn: int) -> Optional[TypeSignal]:
    hits = {t: sum(kw in message for kw in kws) for t, kws in KEYWORDS.items()}
    best_type, count = max(hits.items(), key=lambda x: x[1])
    if count == 0:
        return None
    return TypeSignal(
        user_type=best_type,
        source=Source.INFERRED,
        confidence=min(0.4 + 0.15 * count, 0.85),
        evidence=f"keyword_hits={count}",
        turn=turn,
    )
方式 B:LLM 分类(精度高,几分钱一次)

每隔 N 轮跑一次(不必每轮都跑),或在规则没命中时跑:

复制代码
CLASSIFY_PROMPT = """你是用户分类助手。根据下面的对话历史,判断用户最可能属于哪一类:

A. small_business_owner(小企业主/个体经营者)
B. white_collar(公司白领/上班族)
C. homemaker(家庭主妇/全职照顾家庭)
D. unknown(信息不足)

只输出 JSON,不要任何解释:
{"type": "...", "confidence": 0.0-1.0, "evidence": "一句话依据"}

对话历史:
{history}
"""

def llm_infer(history: list[dict], turn: int) -> Optional[TypeSignal]:
    resp = small_llm.complete(CLASSIFY_PROMPT.format(history=format_history(history)))
    data = json.loads(resp)
    if data["type"] == "unknown":
        return None
    return TypeSignal(
        user_type=UserType(data["type"]),
        source=Source.INFERRED,
        confidence=data["confidence"],
        evidence=data["evidence"],
        turn=turn,
    )

调用时机建议 :第 1、3、5 轮各跑一次,之后如果置信度已经够高就停。不要每轮都跑,浪费钱。我们实测下来,前 5 轮里基本就能稳定分类,后面再跑性价比很低。

信号源 3:用户自选(最强信号)

在合适时机抛出选项,比如冷启动时或推断置信度长期上不去时:

复制代码
def ask_user_to_self_identify():
    return {
        "type": "quick_reply",
        "text": "为了给您更精准的建议,方便了解一下您目前的情况吗?",
        "options": [
            {"label": "我是企业主/自己做生意", "value": "small_business_owner"},
            {"label": "我是上班族",            "value": "white_collar"},
            {"label": "我主要照顾家庭",        "value": "homemaker"},
            {"label": "暂不方便说",            "value": "skip"},
        ]
    }

def on_user_declared(profile: UserProfile, choice: str, turn: int):
    if choice == "skip":
        return
    profile.signals.append(TypeSignal(
        user_type=UserType(choice),
        source=Source.USER_DECLARED,
        confidence=0.95,
        evidence="user_self_declared",
        turn=turn,
    ))
    profile.locked = True   # 锁定,不再被推断覆盖

五、融合逻辑:决策层的核心

收到多个信号后,怎么综合得出 current_type?我们用优先级 + 加权 + 时间衰减的混合策略:

复制代码
SOURCE_PRIORITY = {
    Source.USER_DECLARED: 3,
    Source.INFERRED:      2,
    Source.CHANNEL_TAG:   1,
}

def decide(profile: UserProfile) -> tuple[UserType, float]:
    # 1. 已锁定,直接返回最近一次用户声明
    if profile.locked:
        latest = next(
            (s for s in reversed(profile.signals) if s.source == Source.USER_DECLARED),
            None
        )
        if latest:
            return latest.user_type, latest.confidence

    # 2. 按类型聚合分数 = sum(置信度 × 来源权重 × 时间衰减)
    scores: dict[UserType, float] = {}
    current_turn = max((s.turn for s in profile.signals), default=0)

    for sig in profile.signals:
        weight = SOURCE_PRIORITY[sig.source]
        decay = 0.9 ** (current_turn - sig.turn)   # 越新的信号权重越高
        scores[sig.user_type] = scores.get(sig.user_type, 0) + \
                                sig.confidence * weight * decay

    if not scores:
        return UserType.UNKNOWN, 0.0

    best_type = max(scores, key=scores.get)
    total = sum(scores.values())
    confidence = scores[best_type] / total if total else 0.0
    return best_type, confidence

设计要点:

  • 用户自选最强:权重 3 + 置信度 0.95,几乎一定胜出。
  • 推断中等:权重 2,多次一致的推断也能压过渠道标签。
  • 渠道最弱:权重 1,只在没其他信息时起作用。
  • 时间衰减0.9^Δturn 让最近的信号占更大比重,旧信号会被自然稀释------这一点对动态切换很关键。

六、动态切换:避免"话术变脸"翻车

光自动切还不够。生产环境里有一个非常容易翻车的场景:

用户聊到第 4 轮,AI 推断出他是企业主,于是下一轮回复突然换了一套口吻------用户感受是"客服怎么变了一个人",体验崩坏。

我们加了两条规则:

规则 1:切换前先确认(软切换)

如果当前类型变了且新类型置信度 ≥ 0.8,但用户没明确声明过,不要直接换话术,而是插一句确认:

"听您这么说,您是自己开公司是吗?这样我可以从经营保障的角度给您一些建议。"

用户回"对"再切。这一步可以在主提示词里加一段指令:

复制代码
当系统标注 <pending_type_switch>X</pending_type_switch> 时,
在回复中自然地确认用户身份,而非直接套用 X 类话术。

规则 2:硬冲突时回退到通用版

如果两种类型分数接近(差距 < 20%),用 UNKNOWN 的中性话术,别勉强选一个。这一点尤其重要:选错的代价远大于选"中性"。


七、完整的对话主循环

把上面的部件拼起来,每轮对话长这样:

复制代码
def handle_turn(session_id: str, user_message: str) -> str:
    profile = load_profile(session_id)
    turn = profile.current_turn + 1

    # 1. 收集本轮信号(推断)
    if should_run_inference(turn, profile):
        sig = rule_based_infer(user_message, turn) \
              or llm_infer(profile.history, turn)
        if sig:
            profile.signals.append(sig)

    # 2. 决策当前类型
    new_type, new_conf = decide(profile)

    # 3. 判断是否需要软切换
    pending_switch = None
    if (new_type != profile.current_type
        and new_conf >= 0.8
        and not profile.locked):
        pending_switch = new_type

    # 4. 加载对应提示词(共享部分 + 类型专属部分)
    prompt_type = profile.current_type if pending_switch else new_type
    system_prompt = build_prompt(
        base=COMMON_PROMPT,
        type_specific=PROMPTS[prompt_type],
        pending_switch=pending_switch,
    )

    # 5. 调用 LLM
    reply = llm.chat(system_prompt, profile.history + [user_message])

    # 6. 更新画像
    profile.current_type = new_type
    profile.current_confidence = new_conf
    save_profile(profile)

    return reply

八、提示词组织:共享部分一定要抽出来

三套话术里,肯定有大量重合内容(公司介绍、合规话术、不能承诺收益、转人工规则等)。我们的做法:

复制代码
COMMON_PROMPT(共享)
├── 角色定位
├── 合规底线(不能承诺收益、不能误导销售......)
├── 转人工规则
└── 通用礼貌话术

PROMPTS[small_business_owner]
└── 关注点:现金流保障、关键人风险、税务筹划
└── 用语:经营、老板、公司、员工
└── 案例库:......

PROMPTS[white_collar]
└── 关注点:房贷、收入中断、子女教育、补充医疗
└── 用语:上班、加班、同事、职场
└── 案例库:......

PROMPTS[homemaker]
└── 关注点:家庭整体保障、孩子、配偶意外
└── 用语:家人、孩子、老公/老婆
└── 案例库:......

最终 system prompt = COMMON_PROMPT + PROMPTS[user_type]

好处:合规要求改一次就够了,话术调整也不会牵一发而动全身。


九、上线前的几条工程经验

最后总结几条我们在保险客户项目里踩过的实操经验:

1. 全量记录信号和决策。 signals 列表每条都留存,每次切换都打日志(哪轮、什么依据、从 X 切到 Y)。这是后期调优和 badcase 排查的基础设施,省不得。

2. 先跑数据再调权重。 文章里给的置信度和权重是经验初值,真正的最优解必须等线上数据跑出来。重点关注:渠道标签的实际准确率?规则关键词的误判率?哪类用户最容易误分?

3. 别让分类阻塞主对话。 如果 LLM 分类调用慢或失败,主对话照常用上一轮的类型继续。分类是辅助系统,不能成为关键路径

4. 给运营留个后门。 后台界面要能手动改某个会话的 current_type 并锁定,应对客服介入或紧急 badcase 兜底。

5. 类型先少后多。 一开始 3 类够用,跑一段时间看数据再细化。一上来分 10 类,话术写不完、效果还不一定更好。

6. 留 UNKNOWN 兜底。 信息不足时用中性版本,别强行套某一类话术,否则用户体验会非常奇怪。


写在最后

回到最初的问题:"是用不同提示词,还是在同一个提示词里判断?"

经过这套架构的拆解,答案其实已经很清楚了------

判断 这件事让代码做,话术这件事让提示词做。 把"我是谁"和"我该说什么"这两件事解耦,整个系统才能扩展和维护。

这套架构我们已经在多个客户项目里验证过,覆盖保险、教育、医美等不同行业的多类客户分群场景。如果你也在做类似的智能客服 / 大模型应用落地,希望这篇文章能给你一些启发。

欢迎在评论区交流你的方案与踩过的坑。


关于作者:冰石智能机器人客服软件开发者,长期专注于智能客服领域的产品研发与大模型落地实践。

相关推荐
有个人神神叨叨1 小时前
Ontology-Driven Agents(本体驱动智能体)
人工智能
John_ToDebug1 小时前
拆解AI的“五大基础设施”:算力、网络、存储、电力、软件,谁在驱动千亿市值?
网络·人工智能
Pushkin.1 小时前
Symphony:大模型之后的系统范式——从“写代码”到“编排工作”
人工智能
风落无尘1 小时前
我用 LangChain 写了一个带“定速巡航”的向量化工具,发布到 PyPI 了!
人工智能·python·langchain
AI技术控1 小时前
RAG 效果差不是模型问题:10 个检索增强失败原因总结
人工智能·python·自然语言处理
xier_ran2 小时前
【BUG问题】5060Ti显卡Windows配置Anaconda中的CUDA及Pytorch,sm_120问题
人工智能·pytorch·windows
nix.gnehc2 小时前
AI Coding 演进史:从代码补全到智能体军团的四次范式革命
人工智能
前端之虎陈随易2 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
大数据·linux·javascript·人工智能·算法·microsoft·typescript
python零基础入门小白2 小时前
Transformer、Token、RAG全解析,一篇读懂大模型核心机制!
人工智能·深度学习·学习·语言模型·大模型·transformer·产品经理