摘要:很多企业在企业微信上服务客户的标准姿势是——每个客户一个专属群:客户在群里提需求,企业的销售、售后、技术各司其职地在群里响应。本文继续以 冰石机器人(官网 icestonebot.com) 为例,拆解两组能力的实现:一是好友通过即自动建专属群(含一个很有味道的工程问题:建群请求超时后怎么"找回"这个群);二是让机器人说话更像真人的两个机制——多条消息合并回复与无回复自动续聊。

本系列共三篇:
① 人机协同——机器人客服与真人客服如何共处一个会话
②(本篇)专属客户群与拟人化体验
③ 知识库与 AI 自主学习——让机器人越用越聪明


一、为什么是"每个客户一个群"

企业客户和 C 端消费者不一样:一个客户背后往往对应企业内部多个角色的服务——

  • 销售对接商务和续费;
  • 售后处理报修和退换;
  • 技术支持解决产品使用问题;
  • 机器人 7×24 小时兜底常见问题。

如果客户只加了销售一个人的微信,那么每次售后问题都要销售转述,效率低且容易失真。所以企业微信生态里演化出了一种标准服务形态:客户通过好友后,立刻拉一个专属群——客户 + 销售 + 售后 + 技术 + 机器人,所有沟通都沉淀在这个群里。

好处很实在:

  1. 服务信息集中:客户所有历史问题都在一个群里,任何同事进来都能看到上下文;
  2. 责任清晰:@谁谁负责,不存在"客户消息躺在某个同事的私聊里没人知道";
  3. 机器人天然融入:机器人挂在群里,配合上一篇讲的人机协同机制,和真人同事并肩服务;
  4. 对客户来说是尊贵感:一进来就有一个以自己命名的服务群,好几个人为他服务。

手工建群显然不现实——客户通过好友的高峰期,运营根本忙不过来,而且建群、改名、拉人、发欢迎语这套动作重复且机械。这正是机器人该干的活。


二、自动建群:配置与流程

冰石机器人的自动拉群配置在「企业微信管理 → 账号配置 → 编辑」里:

账号配置中的自动拉群配置

核心配置项:

配置项说明示例
启用自动拉群总开关,启用后添加好友即自动建群开
群名称模板支持占位符 {用户名}、{日期}(MM-DD)客户群-{用户名}-{日期}
选择联系人建群时要拉入的企业内部/外部成员销售张三、售后李四
第一/二/三条群消息建群后依次发送的欢迎消息,支持文本/图片/视频,{_member_} 指代新客户名字“欢迎 {member}!这是您的专属服务群”

整体流程:

企业微信 冰石机器人 客户 企业微信 冰石机器人 客户 好友申请 好友申请事件 自动同意(可选)+ 发好友欢迎消息 好友添加成功事件 渲染群名模板 客户群-王总-08-27 创建群(同事们 + 新客户) 返回群 ID 修改群名称 依次发送 3 条群欢迎消息

流程里有三个不太起眼、但都是踩过坑之后才有的实现细节:

细节 1:建群和起名是两步。
企业微信的建群接口只负责"把这些人拉到一个群里",返回群 ID,不能在创建时直接指定群名。所以必须先建群、拿到群 ID,再调改名接口。两步之间还要留缓冲时间,等群在服务端真正就绪。

细节 2:一律创建"外部群"。
企业微信区分内部群(仅企业成员)和外部群(可含企业外联系人)。客户是外部联系人,进不了内部群——所以自动建群硬性规定创建外部群,避免"群建好了客户进不来"的乌龙。

细节 3:群名要过滤特殊字符。
群名模板里的 {用户名} 来自客户的微信昵称,而昵称里什么字符都可能有(表情、特殊符号)。企微对群名字符集有限制,所以渲染完模板后还要做一次白名单过滤,不合法字符替换成下划线,否则改名接口会失败。

def create_auto_group(new_friend, config):
    cfg = config["auto_group_config"]
    if not cfg["enabled"]:
        return
    members = cfg["internal_contact_ids"] + cfg["external_contact_ids"]
    members.append(new_friend.id)
    if len(members) < 2:
        return                                    # 只有客户自己,没必要建群

    name = (cfg["group_name_template"] or "{用户名}-{日期}") \
        .replace("{用户名}", new_friend.name) \
        .replace("{日期}", today("MM-DD"))
    name = sanitize(name)                         # 白名单字符过滤

    room_id = wecom.create_room(members, outer=True)   # 第一步:建群(外部群)
    wait_room_ready()
    wecom.modify_room_name(room_id, name)              # 第二步:改名
    wait_a_moment()
    for msg in cfg["group_messages"]:                  # 第三步:欢迎语三连
        send(room_id, msg.replace("{_member_}", new_friend.name))

三、幂等的艺术:建群超时之后,怎么"找回"那个群

这是本篇最有工程味道的部分。

问题:建群请求发出去之后,偶尔会超时——接口没有在时限内返回群 ID。但坑爹的是:群往往已经建成功了,只是响应没来得及回来。这时候系统面临一个两难:

  • 当作失败重试?→ 客户会被拉进两个一模一样的群,非常尴尬;
  • 当作成功放弃?→ 拿不到群 ID,改名和欢迎语都没法发,客户进了一个连名字都没有的裸群。

冰石机器人的解法分三步,本质是一个异步事件找回模式:

第一步:把"没下文的建群请求"登记在案。
超时发生时,把这次请求的关键信息(哪个账号、拉了哪些人)存进一张 pending 表。请求 ID 的生成方式很讲究:

member_key = ";".join(sorted(member_list))       # 成员列表排序 → 顺序无关
request_id = md5(f"{account}_{customer}_{member_key}")
pending[request_id] = {
    "members": set(member_list),
    "group_name": name,
    "welcome_msgs": msgs,
    "created_at": now(),
}

成员列表先排序再哈希——同一批人无论以什么顺序发起,生成的 request_id 都一样。这一步天然保证了幂等:就算上层因为超时又发起了一次登记,也只会命中同一条 pending 记录,不会重复。

第二步:等群自己"冒出来"。
群建成功后,企业微信侧会推送一个"群成员变更"事件,事件里携带新群的群 ID 和成员列表。系统拿这个事件去 pending 表里匹配:

def on_group_created_event(event):
    callback_members = set(event.member_list)
    for req_id, req in pending.items():
        if req["members"].issubset(callback_members):   # 子集匹配!
            finish_group_setup(event.room_id, req)      # 补改名 + 补欢迎语
            del pending[req_id]
            return

匹配规则用的是子集匹配而不是全等匹配:请求时的成员集合 ⊆ 事件里的成员集合即算命中。为什么?因为事件里的成员列表可能多带一个建群者自己(也就是机器人所在的账号),全等匹配会永远匹配不上。

第三步:过期清理。
pending 记录带 5 分钟超时——如果 5 分钟内始终没等到对应的群事件,说明群大概率真的没建成,清掉记录,避免内存里堆积僵尸请求。

正常返回

超时

成员子集匹配命中

超时未命中

建群请求

拿到群ID
改名+欢迎语

成员集排序+MD5
登记 pending

5分钟内收到
群成员变更事件?

清理记录
视为失败

这套"同步超时 → 登记待办 → 异步事件找回 → 子集匹配 → 过期兜底"的模式,不只适用于建群——任何"请求方超时但操作实际可能已生效"的异步系统(下单、开票、开通服务)都可以套用。


四、新群零配置:缺省配置继承

自动建出来的群,马上就要开始接待客户了。但问题来了:这个群是全新的,后台的聊天配置表里根本没有它的记录——它的提示词是什么?FAQ 开不开?真人客服名单是谁?

如果每建一个群都要人工去后台配一遍,"自动"就名存实亡了。

冰石机器人的方案是缺省群配置:

  1. 管理员预先配置一条特殊的聊天配置,作为该企微账号下所有"无主"会话的模板;
  2. 消息从某个群进来时,先按群 ID 查配置;
  3. 查不到 → 取缺省配置逐字段深拷贝一份(内存中,不落库),当场作为这个群的配置使用;
  4. 后续如果管理员想对某个群精细化调整,再单独为它建配置覆盖缺省值。
def get_group_config(account, group_id):
    cfg = db.query(group_id=group_id, status="active")
    if cfg:
        return cfg
    default = db.query(group_id=account.wxid)      # 约定:群ID=账号自身ID 的那条即缺省配置
    if default:
        cfg = deep_copy_all_columns(default)       # 逐列拷贝,不写库
        cfg.group_id = group_id
        return cfg
    return None

于是每个自动建出来的专属群,从诞生那一刻起就自动继承了完整的机器人能力:提示词、FAQ 开关、真人客服名单、等待秒数……建群 → 接客,全程零人工配置。


五、像真人一样"看完再说":多条消息合并

场景切到日常对话。真实的客户很少一条消息把话说完,更常见的是:

客户:打印机连不上云盒
客户:一直闪红灯
客户:昨天还是好的

普通机器人是一问一答式的:三条消息触发三次回复,机器人连珠炮一样回三段话,而且第一段回复只看到"连不上云盒"、完全没等到"闪红灯"这个关键信息——既蠢又吵。

真人客服的做法是:看对方把话说完,合起来理解,回一条。

冰石机器人用合并等待窗口模拟这个行为,配置项在聊天配置的 AI 大模型配置区:

多条消息合并与无回复续聊配置

LLM 多条消息合并等待秒数(默认 3 秒,0 表示不合并):同一个人在窗口期内连续发的多条需要大模型回复的消息,会合并成一次调用。

实现是一个滑动窗口缓冲:

buffers = {}   # (账号, 会话, 发送者) -> {"messages": [...], "timer": Timer}

def on_llm_message(key, msg, delay):
    if delay <= 0:
        llm_reply([msg]); return
    buf = buffers.setdefault(key, {"messages": [], "timer": None})
    buf["messages"].append(msg)
    if buf["timer"]:
        buf["timer"].cancel()            # 又来一条 → 重置倒计时(滑动窗口)
    buf["timer"] = Timer(delay, lambda: flush(key))
    buf["timer"].start()

def flush(key):
    buf = buffers.pop(key)
    with conversation_lock(key):          # 同会话串行,避免并发双回
        if batch_is_stale(key, buf):      # 等锁期间已被别的批次答过 → 丢弃
            return
        combined = "\n\n".join(buf["messages"])
        llm_reply(combined)

三个关键设计:

  • 缓冲的 key 精确到"发送者":群里 A 和 B 同时说话,各自有各自的缓冲,不会把两个人的话合并成一段发给大模型;
  • 滑动窗口:每来一条新消息就重置倒计时——客户打字慢、边想边发的场景下,窗口会一直顺延到他把话说完;
  • 过期批次丢弃:flush 时要拿会话锁排队,如果等锁期间客户的这批消息已经被更晚的批次连带回答过了,当前批次直接丢弃——避免"合并是为了少回话,结果反而连回两条"的自相矛盾。

效果上,开头那三条消息会在最后一条发出 3 秒后,合并成一段完整上下文交给大模型,机器人回一条同时考虑了三个信息点的回复——观感上和真人"看完再说"几乎一致。


六、客户不说话了怎么办:无回复自动续聊

还有一个更进阶的拟人化机制,同样在上面那张截图里:无回复续聊。

场景:机器人回答了客户的问题,客户没再说话。可能是问题解决了,也可能是他没看到、被别的事岔开了。真人销售遇到这种情况,往往会隔几分钟跟一句:“刚才的方法试了吗?还有问题随时说哈。”

冰石机器人的对应配置:

配置项默认值说明
无回复续聊等待分钟数0(关闭)机器人发出回复后开始计时,客户 N 分钟没有新消息则主动跟进一句
无回复续聊最大次数1客户持续无回复时最多自动续聊几条,防止变成骚扰

机制要点:

  1. 机器人每发出一条非空回复,就为该会话挂一个倒计时;
  2. 倒计时期间客户来新消息、真人客服发言、或会话被人工接管,倒计时立即取消——续聊只发生在"真的没人说话"时;
  3. 到点后,由大模型根据聊天历史生成跟进话术,不是固定模板——上文聊的是安装问题,跟进的就是"安装还顺利吗",而不是干巴巴的"在吗";
  4. 发送成功后计数 +1,未达上限则再挂一轮。

最有意思的是第 3 步的提示词设计。让大模型"决定要不要续聊"有一个经典难题:模型总想说点什么,哪怕不该说。冰石机器人在续聊提示词里用了一个哨兵串协议:

判断当前是否适合跟进。如果客户已明确表示"不用了"“先不用”"我先看看"等婉拒含义,或对话已自然结束,只输出 (__0__),不要输出任何其他内容。禁止发送活动、试用、优惠等推销内容,禁止反问推销。

系统侧拿到回复后先检查哨兵串:

followup = llm(build_followup_prompt(history))
if followup.strip() == "(__0__)" or not followup.strip():
    return          # 模型判断不该跟进 → 静默放弃,且不计入续聊次数
send(chat_id, followup)

给模型一个明确的"闭嘴出口",比在提示词里反复强调"不要过度打扰"有效得多——这是大模型应用里很值得推广的一个小技巧。


七、小结

本篇拆解了企微客服的两组能力:

专属客户群自动化:

  • 群名模板 + 成员配置 + 欢迎语三连,好友通过即建群;
  • 建群/改名两步走、强制外部群、群名字符过滤——都是实战踩出来的细节;
  • 建群超时的异步事件找回:成员集排序 MD5 幂等登记 → 群事件子集匹配 → 5 分钟过期兜底;
  • 缺省配置深拷贝继承,新群零配置即可接客。

拟人化体验:

  • 多条消息合并:按发送者隔离的滑动窗口缓冲 + 会话锁 + 过期批次丢弃,“看完再说”;
  • 无回复续聊:大模型生成跟进话术 + 哨兵串闭嘴协议 + 次数上限,“跟进而不骚扰”。

配合第一篇的人机协同机制,一个企微客服群里的完整图景是:客户进来就有专属群,群里真人同事各司其职,机器人安静地兜底常见问题、说话像真人、还会看时机跟进——整体感受是"一个团队在服务我",而不是"我在和机器人对话"。

下一篇(完结篇)我们深入机器人的"大脑":三级应答链路、图文并茂的知识库导入,以及解决"知识库越用越旧"这一行业痛点的 AI 自主学习机制。


冰石机器人官网:icestonebot.com
本文界面截图来自冰石机器人企业微信版实际部署环境(版本 20260827),配置项名称与默认值以实际部署版本为准。请在遵守企业微信平台规范的前提下合规使用。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐