标签体系烂掉的样子都差不多:三百多个标签,一半是意思重复的近义词,销售不敢按标签筛人,运营做分群时还是回去翻聊天记录。问题几乎从来不出在分类不够细 —— 出在没人规定过每个标签由谁写、给谁读。这篇讲怎么把标签切成四层、哪几层该同步到企业微信侧、以及标签和自有 CRM 字段之间该建立什么关系。同步和写入这部分,按 wecomapi 的接入方式来写。
先回答「谁写谁读」,分类才有意义
大多数团队设计标签体系的第一步是画分类树 —— 行业、地域、规模、意向度、来源渠道,一口气列三十个维度。这一步很爽,但它跳过了唯一真正决定标签能不能用的问题:这个标签的值,是谁写进去的。
一个标签只要存在两个写入方,它就会在三个月内失去可信度。典型场景:「高意向」既允许销售手动打,也由行为评分自动打。销售看到系统打上的「高意向」,发现客户其实只点开过一次链接,于是手动摘掉;系统第二天又打回去。几轮之后没人再看这个标签,运营做分群时改用「聊天记录里出现过报价」这种土办法。
- 每个标签有且只有一个写入方:人,或者程序,不能都是。
- 人写的标签,程序只读不改 —— 包括不做「数据清洗」式的批量修正。
- 程序写的标签,在一线界面上以只读形态展示,不给手动摘除的入口;要改就去改产生它的规则。
- 需要人机协同的场景用两个标签,不要用一个:程序写「行为分高」,人写「已确认商机」,两者不一致本身就是有价值的信号。
「谁写谁读」这张表该在写第一行代码之前就填完,并且落进文档。它比分类树重要得多 —— 分类可以后补,写入权限一旦混乱,回收成本是整套标签作废重来。
四层标签:来源、阶段、意向、行为
分层的依据不是业务语义,而是四条技术特征:变不变、互不互斥、有没有时效、频率多高。按这几条切下来,标签自然落成四层,每层的存储方式和消费方式都不一样。
- 1来源层:客户从哪来(渠道、活动、承接员工、二维码批次)。写一次就不再变,值域封闭。它是所有归因分析的地基,必须在加好友通过的那一刻就写进去 —— 事后补不回来。
- 2阶段层:客户在流程里的位置(新客、跟进中、已报价、已成交、已流失)。互斥且单值,本质是状态机而不是标签。写新值之前必须撤掉旧值,否则很快会出现「已成交」和「已流失」同时挂着的客户。
- 3意向层:客户当前的热度分层(A/B/C 或高中低)。特征是会衰减。没有 TTL 的意向标签是最会骗人的东西 —— 四个月前打的「高意向」和今天打的长得一模一样。
- 4行为层:客户做过什么(看过价格页、七天内回复过、进过某个群)。高频、机器产出、数量可以很大。它不是给人看的,是给规则引擎当输入的。
这四层里只有阶段层需要严格的写入串行化和变更留痕,来源层写一次,意向层和行为层允许被覆盖重算。把它们塞进同一张表、用同一套写入逻辑对待,是很多标签系统后来跑不动的直接原因。
只有前三层该同步到企业微信侧
常见做法是把所有标签都同步到企业微信的客户标签里,理由是「一线在聊天窗口就能看到」。这条路走到几十个标签就开始难受,走到几百个就废了。
- 聊天侧边栏是给人扫一眼用的,能承载的信息量大概十个标签。行为层一个客户就能产出几十条,塞进去等于全都看不见。
- 行为标签变化频繁,每次变化都是一次写入调用。批量刷标签本来就是最容易撞上频率限制的操作之一,而它换来的业务价值接近于零。
- 你的分群、圈选和报表全在自己库里做,这些查询根本不需要标签存在企业微信侧。
所以判断是:把企业微信侧的客户标签当成「给一线看的展示层」,不要当成你的特征存储。来源、阶段、意向三层用 wecomapi 的客户标签接口做投影同步,行为层留在自己库里,只在需要时聚合成一个意向层的结论再往下投。
这条同时决定了同步方向只能是「自己的库 → 企业微信侧」的单向下行。一旦允许反向回写,就得处理「一线手动摘掉了一个由程序投影下去的标签」这类问题,而这个问题没有干净的解法。
和自有 CRM 对齐:投影,不是双向同步
「和 CRM 打通」经常被默认理解成双向同步。双向同步在这个场景里几乎必然出问题,因为两边的数据模型根本不对等。
- CRM 字段有类型、值域、必填约束、变更历史和操作人。
- 客户标签在会话侧是分组下的一批文本项,没有版本,也没有「这个值是谁在什么时候改的」。
结论很直接:system of record 只能在 CRM 或你自己的客户库这一侧。企业微信侧的标签是它的一次渲染结果,和页面上的一个徽章没有本质区别 —— 徽章不会反过来定义数据。
对齐之前还有一件更容易返工的事:身份映射。同一个真实客户可能被三个销售各自添加过,也可能同时出现在两个外部群里。你在会话侧拿到的客户身份,是「某个成员视角下的这个联系人」,它和 CRM 里的一条联系人记录不是一对一的。
所以映射表要从一开始就建成多对一:(客户身份, 归属成员) 指向一个 CRM 联系人 ID。合并依据按可信度排序 —— 手机号 > 一线人工确认 > 昵称头像的模糊匹配,最后一条只能用来生成待确认队列,绝不能自动合并。把这层做成一对一,等第二个销售也加上同一个客户时就得停下来重构,而这通常发生在数据已经攒了半年之后。
落地:事件驱动写入,合并后批量投影
写入侧用事件驱动:加好友通过、进群、收到消息这几类从 wecomapi 的事件回调里拿,成交回写来自你自己的业务系统,都是标签变更的触发点。投影侧不要跟着每次变更实时下发 —— 按客户维度合并成待投影队列,定时取差集下发。同一个客户在一分钟里被改了五次,只该产生一次写入。
// 示意逻辑,精确字段、接口与频率约束以 wecomapi 文档为准
const LAYER = { SOURCE: "source", STAGE: "stage", INTENT: "intent", BEHAVIOR: "behavior" };
const PROJECTED = [LAYER.SOURCE, LAYER.STAGE, LAYER.INTENT]; // 行为层不下行
// 1) 事件驱动写自己的库,写入方在这一层就被约束死
async function onEvent(evt) {
const patch = rules.derive(evt); // 只产出机器可写的层
if (patch.layer === LAYER.STAGE) {
await store.transitStage(patch.customerId, patch.value, evt.id); // 互斥 + 留痕
} else {
await store.upsertTag(patch.customerId, patch, evt.id); // evt.id 作幂等键
}
await projectQueue.push(patch.customerId); // 合并去重,不立即下发
}
// 2) 定时把可见层投影到会话侧,只发差集
async function flushProjection(batch) {
for (const id of batch) {
const want = await store.visibleTags(id, PROJECTED);
const have = await mirror.load(id); // 上一次投影的结果
const { add, remove } = diff(have, want);
if (!add.length && !remove.length) continue;
await limiter.run(() => api.customerTag(id, { add, remove }));
await mirror.save(id, want);
}
}两个细节值得单独说。mirror 那张「上次投影结果」表不能省,没有它就只能每次全量下发,写入量翻十倍;limiter 要按账号维度限速而不是全局限速,因为频率约束本来就是按账号算的。
让标签体系不腐烂的三条规矩
- 1新增标签必须先声明消费方。说不出这个标签会被哪个分群、哪条自动化或哪张报表用到,就不建。绝大多数垃圾标签都是「先建着以后可能有用」来的。
- 2每季度跑一次消费统计,连续两个季度零消费的标签下线。下线前把历史值归档,别直接删。
- 3阶段层的每次流转都写审计记录:谁、何时、从哪个值到哪个值、由哪个事件触发。这份记录在做转化分析和排查数据打架时的价值,远超它占的那点存储。
本文讲的是分层方式与工程取舍。标签相关接口的精确字段、写入约束与频率限制以 wecomapi 线上接口文档为准,不要照抄示意代码上生产。
常见问题
- 标签到底该存在企业微信侧还是自己库里?
- 自己库里是主,企业微信侧只放给一线看的那几层。前者有类型约束、变更历史和聚合查询能力,后者本质是聊天窗口旁边的展示位。按「来源、阶段、意向下行投影,行为层不下行」来切,通常够用。下行那一步用 wecomapi 的客户标签接口做,配一张上次投影结果表只发差集。
- 销售手打的标签和系统自动打的冲突了怎么办?
- 正确做法是不让它们冲突 —— 拆成两个标签,各自只有一个写入方。销售写的表示人工判断,系统写的表示行为事实。两者不一致本身就是值得关注的信号,把它做成一个待复核队列,比强行合并成一个值有用得多。
- 已经攒了几百个乱标签,怎么治理?
- 不要从合并近义词开始。先统计每个标签近三个月被哪些分群、自动化和报表消费过,零消费的直接冻结为只读、不再新增,剩下的按四层归位并补上写入方声明。一轮下来通常能砍掉一半以上,再谈重命名和合并。治理这段时间先把投影任务停掉,等分层归位了再用 wecomapi 重跑一次全量下发。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
