上一波《RAG 还是微调?用一个真实的多语言客服场景说清楚》里,我把客服知识库的技术选型聊完了,这篇往前走一步:知识库之外,客户跟进这个环节本身能不能交给 Agent 自动跑?本文基于一个真实的跨境客户管理场景做架构推演:业务是外贸团队跟进 WhatsApp 上的询盘客户,样本是近三个月约 4000 条会话的跟进节奏统计,看哪些环节适合自动化、哪些必须留在人手里。全文只谈架构与工程取舍,不涉及任何具体产品,所有 API 端点统一用 https://api.example.com/… 占位。
一、需求约束:自动跟进到底要解决什么
先把业务痛点翻译成工程语言,需求边界比想象中窄:
- 响应时效:外贸询盘的黄金回复窗口是 30 分钟内。时差是最大敌人,凌晨两点的询盘等到早上九点回复,客户多半已被同行截走。
- 跟进节奏:成交前平均需要 5~8 次触达。人肉跟容易漏——不是不想跟,是客户多了记不住谁该跟了。
- 语言门槛:同一个销售要面对英语、西语、阿拉伯语客户,回复质量随语言波动很大。
- 合规红线:绝对不能变成骚扰。客户明确拒绝后必须停止触达,频率要有硬性上限。
注意最后一条,它决定了整个架构的形态:这不是一个「生成内容」问题,而是一个「在严格约束下做决策」的问题。这直接排除了「一个大 prompt 解决一切」的做法。
二、整体架构:Agent 拆成四层
我在做的一个跨境客户管理产品里验证过的拆法(也是本文唯一的经验来源声明),是把 Agent 按职责切成四层,每层可以独立测试、独立降级:
┌─────────────────────────────────────────────┐
│ 触发层 Scheduler:定时扫描 + 事件驱动 │
├─────────────────────────────────────────────┤
│ 决策层 Agent Core:判断该不该跟、跟什么 │
├─────────────────────────────────────────────┤
│ 执行层 Tool Executor:发消息/查资料/记账 │
├─────────────────────────────────────────────┤
│ 护栏层 Guardrail:频率/黑名单/人工审批 │
└─────────────────────────────────────────────┘
四层的职责边界:
- 触发层:两种触发源——定时扫描(每天早八点跑一遍「哪些客户到了该跟进的节点」)和事件驱动(客户回复、询盘到来)。事件驱动走 Webhook,关于消息通道同步的重复投递问题,我在《Webhook 幂等设计:AI 消息通道同步的三类重复问题(附代码)》里踩过完整的坑,这里不展开,只强调一句:没有幂等键的事件驱动 Agent,迟早会重复跟进同一个客户两次。
- 决策层:LLM 参与的核心位置。输入是客户画像 + 最近会话摘要 + 跟进历史,输出是一个结构化决策:{action, channel, draft, urgency}。注意输出的是「决策 + 草稿」,不是「直接发送」。
- 执行层:纯工具调用,收发消息、查订单状态、写跟进日志。这层没有任何智能,就是普通的 API 编排。
- 护栏层:频率上限、拒绝名单、敏感词、金额超限转人工。这层是纯规则代码,一行 LLM 都不碰。
会话数据怎么存、怎么支撑百万级消息量,我在《AI 客服系统怎么做会话数据模型?支撑百万级会话的表设计》里给过完整的表结构,决策层读的「跟进历史」就是那套模型里的 follow_up_log,两篇可以对照着看。
三、决策层的三个设计取舍
取舍一:LLM 判断「该不该」,规则判断「能不能」。
最常见的错误设计是让 LLM 直接输出「发/不发」。实测下来(4000 条会话回放,统计三个版本的误判率):让 LLM 全权决策,客户已拒绝后仍被触达的比例约 2.1%;改成「规则先算出合法动作集 → LLM 只能在集合内选择」,这个数字降到 0,代价是约 8% 的本可以触达的场景被保守跳过。对 ToB 业务,这个代价完全值得——丢一个骚扰投诉比丢一次跟进机会贵得多。
取舍二:草稿生成与发送解耦。
LLM 生成的跟进草稿不直接进消息通道,而是先进「待发队列」,按 urgency 分级:高紧迫(客户刚回复)自动发,中紧迫进人工确认队列,低紧迫攒到次日批量审。这个设计让销售保留最终控制权,也让 Agent 的错误有拦截窗口。
取舍三:会话摘要而不是全量上下文。
不要把三个月聊天记录塞进 prompt。每次会话结束后异步生成一段结构化摘要(客户意向、卡点、承诺事项),决策时只喂摘要 + 最近 3 条消息。上下文 token 降了 90% 以上,决策质量实测没有下降——因为跟进决策依赖的是「历史脉络」,不是「逐字记录」。
四、方案对比:三种自动化深度怎么选
| 纯提醒(Agent 只算「该跟谁」) | 低 | 不省人力,只省记性;依赖销售自觉执行 | 团队小、客单价高、话术敏感 |
| 草稿+人工确认(Agent 写、人发) | 中 | 确认队列可能积压;高峰期人工吞吐跟不上 | 大多数外贸团队的起步形态 |
| 全自动发送(带护栏) | 高 | 护栏漏一条就是事故;需要完整回滚与审计能力 | 标准化程度高的售后/物流类跟进 |
三种方案不是递进关系,是按业务风险分流:话术越标准化、越远离承诺类内容,越可以往后选。首单报价、合同条款这类内容,写到全自动也别信——LLM 编造一个交付周期,销售是会用信誉买单的。
五、状态机:跟进流程不能让 LLM 自由发挥
决策层内部,客户跟进状态用显式状态机管理,LLM 只负责状态内的动作生成,不负责状态迁移:
NEW → CONTACTED → QUOTED → NEGOTIATING → (WON | LOST)
│ │ │
└── FOLLOWUP_WAITING ←──┘
│
└── CHURNED(客户明确拒绝,全渠道静默)
- 状态迁移只由代码触发:收到客户回复、报价发出、超时未回——这些事件的判定可以是 LLM 辅助的(比如从消息里识别「客户已拒绝」),但迁移动作本身是确定性代码。
- 每个状态有独立的 prompt 模板和护栏配置:NEGOTIATING 状态的草稿必须经过人工确认(金额敏感),FOLLOWUP_WAITING 的触达频率是 72 小时一次,CHURNED 状态下任何触达动作直接被护栏拦截。
- CHURNED 是吸收态:进去了就出不来,除非客户主动发消息。这一条是合规的生命线。
实测里 LLM 在这个架构里的真实角色是三件事:意图分类(这条回复算什么事件)、草稿生成、摘要更新。都不是「自由决策」。
六、核心代码:状态机 + 护栏的骨架
下面是可以直接改造复用的最小骨架(Python 3.10+,无第三方依赖):
# followup_agent.py — 跟进 Agent 状态机 + 护栏骨架
import json
import time
import urllib.request
from dataclasses import dataclass
from enum import Enum
API_BASE = "https://api.example.com/v1"
class State(str, Enum):
NEW = "NEW"
CONTACTED = "CONTACTED"
QUOTED = "QUOTED"
NEGOTIATING = "NEGOTIATING"
FOLLOWUP_WAITING = "FOLLOWUP_WAITING"
CHURNED = "CHURNED"
WON = "WON"
LOST = "LOST"
# 每个状态的触达约束:min_gap_hours=最小间隔, require_review=是否必须人工确认
GUARDRAILS = {
State.NEW: {"min_gap_hours": 24, "require_review": True},
State.CONTACTED: {"min_gap_hours": 72, "require_review": False},
State.FOLLOWUP_WAITING: {"min_gap_hours": 72, "require_review": False},
State.QUOTED: {"min_gap_hours": 48, "require_review": True},
State.NEGOTIATING: {"min_gap_hours": 72, "require_review": True},
State.CHURNED: None, # 吸收态:任何触达直接拦截
State.WON: None,
State.LOST: None,
}
@dataclass
class Customer:
cid: str
state: State
last_touched_at: float = 0.0
rejected: bool = False
def _post(path: str, payload: dict) –> dict:
req = urllib.request.Request(
f"{API_BASE}/{path}",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
def llm_decide(customer: Customer, summary: str) –> dict:
"""调用 LLM 生成决策。注意:只让它做「集合内选择」,不让它决定发不发。"""
allowed = ["draft_followup", "wait", "escalate"]
if customer.state is State.CHURNED:
allowed = [] # 护栏前置:吸收态连选择集都不给
prompt = (
f"客户状态: {customer.state.value}\\n跟进摘要: {summary}\\n"
f"可选动作: {allowed}\\n"
'只输出 JSON: {"action": "…", "draft": "…", "urgency": "high|mid|low"}'
)
# 此处替换为实际的 LLM 调用;示例返回固定结构
return {"action": "draft_followup", "draft": "…", "urgency": "mid"}
def try_followup(c: Customer, summary: str, now: float | None = None) –> str:
now = now or time.time()
rail = GUARDRAILS[c.state]
if rail is None:
return "BLOCKED_STATE"
if c.rejected:
return "BLOCKED_REJECTED"
if now – c.last_touched_at < rail["min_gap_hours"] * 3600:
return "BLOCKED_FREQUENCY"
decision = llm_decide(c, summary)
if decision["action"] != "draft_followup":
return f"SKIP_{decision['action'].upper()}"
if rail["require_review"] or decision["urgency"] == "low":
_post("followup/queue", {"cid": c.cid, "draft": decision["draft"],
"review": True})
return "QUEUED_FOR_REVIEW"
_post("messages/send", {"cid": c.cid, "body": decision["draft"]})
c.last_touched_at = now
_post("followup/log", {"cid": c.cid, "state": c.state.value,
"action": "auto_sent"})
return "SENT"
这段骨架刻意保持简单,但它涵盖了三个最容易做错的点:护栏前置(LLM 拿到的选择集本身已经被过滤)、吸收态拦截、触达后写日志。生产环境还要补:状态迁移事件的持久化、草稿队列的过期清理、以及决策的审计留痕。
七、踩坑实录:三个真实翻车点
坑一:时区导致触发层重复扫描。 定时任务按 UTC 部署、业务按当地时间看,结果某天凌晨触发层连跑两轮,同一批客户各收到一条跟进。修复很简单——触发层加幂等键(customer_id + date + slot),但这正是《Webhook 幂等设计》里那个教训的变体:分布式系统里,「只跑一次」永远要靠显式去重,不能靠调度器自觉。
坑二:LLM 把「客户已拒绝」翻译成了「需要换个话题再试」。 回放数据里发现几条会话,客户说了 “not interested”,Agent 下周又发了一条新角度的跟进。根因是意图分类的 prompt 里没有把拒绝信号列为吸收态触发器。修复:拒绝信号改成关键词 + LLM 双通道检测,任一命中即迁移到 CHURNED。这个坑让我把「LLM 判断」从迁移路径上彻底摘掉了。
坑三:摘要更新延迟导致决策失真。 摘要是异步生成的,高峰期延迟到 20 分钟。期间决策层拿到的是旧摘要,给一个刚下过单的客户又发了一封催单邮件。修复:摘要带版本号,决策时校验版本,低于最新版本就降级为「纯提醒」模式。教训是异步链路里的每一环都要回答「数据旧了怎么办」。
八、FAQ
Q1:这个架构必须用大参数模型吗? 不需要。意图分类和摘要生成用 7B 级别的小模型就够,草稿生成可以按语言路由到不同模型。整个架构里 LLM 是被约束在窄任务里的,模型能力要求比想象中低。
Q2:Agent 误发消息了怎么办? 消息通道层面做不了撤回(对方已收到),所以重点是事前拦截而不是事后补救:护栏层 + 人工确认队列 + 影子模式(先跑两周只记日志不发送,复盘误判率再放开)。
Q3:状态机会不会太僵硬,接不住灵活的销售流程? 状态机管的是「客户所处阶段」这个客观事实,不是销售动作本身。销售完全可以自由沟通,Agent 只在跟进触达时参考状态。实践中更常见的问题是状态定义太细——6~8 个状态足够,超过 10 个说明你把销售动作混进状态里了。
Q4:怎么评估 Agent 上线后的效果? 三个指标:响应时效(首响时间中位数)、跟进覆盖率(该跟未跟的客户占比)、事故数(护栏拦截外的异常触达)。前两个是收益,第三个是风险底线,缺一不可。
Q5:多语言草稿的质量怎么保证? 按语言维护独立的 prompt 模板和few-shot样例,高紧迫草稿走人工确认。非英语小语种建议先只在「提醒模式」下运行,积累到足够的人工修订样本再放开。
上一波《Webhook 幂等设计:AI 消息通道同步的三类重复问题(附代码)》解决的是这个架构里事件驱动的数据一致性,《RAG 还是微调?用一个真实的多语言客服场景说清楚》解决的是知识库选型,加上这篇的决策层,Agent 跟进的三大件就齐了。
这个系列还剩最后 2 篇,会一篇篇更完,关注不迷路。
你们团队的客户跟进现在是纯人工还是已经有自动化了?卡在哪一步,评论区聊聊。
专注 AI 工程化实践与出海外贸技术
网硕互联帮助中心





评论前必须登录!
注册