去年我们把内部所有离散的机器人、通知服务和自动化任务统一收敛到一个叫 Agent-Reach 的触达层上,跑了将近八个月。这个项目名字起得很直白:既要让 Agent 具备推理和行动能力,也要确保它能真正“够得着”目标——无论是人、外部系统,还是那条迟迟没有回执的接口。这篇文章不打算写成产品说明书,而是把我从需求拆解、架构取舍、参数调试到线上排障的整个过程整理出来,包括那些文档里不会写的坑。
如果你是做客服机器人、工单自动化、IM 机器人这类方向,或者正在规划智能体平台的“执行/触达”环节,里面涉及的思路和配置值得直接抄走。
1. Agent-Reach 到底要解决什么问题
1.1 渠道碎片化是第一个敌人
我们最开始的状态和很多团队类似:企业微信机器人一套代码,钉钉机器人另一套代码,短信接口单独一个服务,邮件通知又一个定时任务。每个渠道都有自己的鉴权、重试、限流和数据格式。业务方提需求的时候经常是“帮我在企微上也发一份”,听上去很简单,实际上要把同一套逻辑复制粘贴到另一个项目里,改字段映射,重新测试,再处理一遍报错。
这种情况持续久了,有个更隐蔽的问题:Agent 决策出来了,但执行层接不住。比如 LLM 判断“这个客户需要回访”,下一步要发起外呼、推送站内信、同步 CRM 状态。如果这些动作散落在不同的服务里,Agent 只能靠写死多个 API 调用来实现,一旦某个渠道失败,没有统一的重试和补偿机制,整条业务链就断了。
1.2 触达层必须和 Agent 执行层解耦
做 Agent-Reach 时我们定了一条核心原则:Agent 只负责“决定做什么”,不负责“具体怎么送达”。这句话听起来像老生常谈,但真正落地时要顶住很多压力。业务方会问:不就是往企微推条消息吗,为什么要搞消息模板、路由规则、优先级队列这么多概念?
实际上拆分之后收益非常明显。Agent 侧只需要构建一个标准化的动作对象,告诉触达层“把什么内容、发给谁、走哪个渠道、期望什么反馈”。至于这个动作是调用接口,还是发 IM,还是生成一个待办工单,全部由触达层的适配器去处理。这样一来,新增一个渠道只需要写一个 adapter,业务方不需要改动任何 Agent 逻辑。
我还特意给 Agent-Reach 定了一个可量化的目标——触达成功率。所有渠道统一按这个指标考核:成功回执 + 内容有效送达 + 反馈回收达标。这个指标后来成了整个项目最核心的北极星,因为所有重试、降级、死信策略都在围绕它做优化。
1.3 适用场景与边界
Agent-Reach 适合这么几类场景:客服机器人需要主动外呼或消息提醒;工单系统需要让 Agent 创建任务并通知处理人;营销或运营场景需要批量触达用户并回收反馈;企业内部的自动化助手需要在多个 IM 渠道间做无缝衔接。
不适合的场景也得说清楚:它不适合用于高并发下大规模广告推送,那种场景需要专门的营销触达平台,Agent-Reach 的定位是“智能体动作的出口”,不是“流量分发器”。另外,如果想让它直接操作数据库写数据,请克制一点,建议只走它留的 action 接口,把 SQL 操作封装成业务动作。
2. 整体架构与关键组件
2.1 逻辑上的五级链路
Agent-Reach 的逻辑结构大致可以分为五层:
- 接入层:接收来自 Agent 的标准化动作请求,也接收来自业务系统的原始事件。
- 路由层:根据用户 Profile、消息类型、优先级和渠道偏好,决定触达通道。
- 编排层:负责组装消息内容、应用模板、挂载附件、执行 LLM 生成结果的格式化。
- 执行层:调用具体的 adapter 发出消息或调用接口,并注册回执期待。
- 反馈层:接收异步回执,维护消息状态机,回收后续的用户反馈或系统响应。
如果画一个简化版的数据流,大概是:
AgentRuntime 产生“action_request” → 路由层根据 recipient 的渠道映射表找到可用渠道 → 执行层通过 ChannelAdapter 发出消息 → 消息进入待回执队列 → 回执信息回来后更新 MessageState → 反馈回调通知给 AgentRuntime。
这样每个环节都可以单独扩容。曾经有一段时间短信渠道的 adapter 频繁超时,我们直接把该渠道的执行节点从主集群里拆出来做隔离,其他渠道完全不受影响。这就是分层设计在故障隔离上的直接收益。
2.2 ChannelAdapter 抽象
ChannelAdapter 是整个项目的插件化核心。每个渠道都需要实现六个方法:
- send(message):发送消息或执行动作
- callback(payload):处理异步回调
- status(message_id):查询消息状态
- quota():查询剩余配额或限流状态
- health():渠道健康检查
- retry_policy(error):根据错误类型返回重试策略
把这六个方法固定下来后,新增一个渠道基本变成了纯粹的协议对接工作。我们后来接了一个大客户自研的 App 内部推送通道,两个工程师一周搞定,包括联调和压测。封装得越规整,后续扩展就越省心。
2.3 路由与优先级策略
路由策略上我们一开始想得很复杂,试图用一套规则引擎解决所有情况:周末降级、非工作时间不走电话、高意向客户优先短信等等。但实践证明,规则太多会导致 Agent 的决策过程不可解释,出问题后难以定位是规则错了还是 Agent 错了。
后面简化为两级判断:
第一级,由 Agent 在生成动作时给出channel_hint,比如“priority_high”“general_notify”“urgent”,这相当于 Agent 对渠道有一个初步的预判。第二级,由触达层根据实际情况做最终裁决,参考用户在线状态、工作时段、渠道健康度、历史送达率。
最终裁决有四个硬规则,直接写在配置中心里:
- 用户设置过免打扰则禁止发送强提醒类消息。
- 渠道处于黄码状态则自动降级到备用渠道。
- 高优先级动作不允许走非实时渠道。
- 同一用户同一业务 ID 下同时最多存在 2 条待回执消息,超出则排队。
其中第四点非常重要,它防止了 Agent 在极端情况下重复轰炸同一个用户。我们线上遇到过 LLM 幻觉导致同一客户在两分钟内生成了 8 次触达动作,如果没有这条规则,对方大概率会直接投诉。
3. 核心流程实现与参数设计
3.1 一次完整触达的生命周期
一次触达的生命周期我用七个状态来跟踪:
CREATED → DEQUEUED → DISPATCHING → DISPATCHED → ACKED → FEEDBACK_RECEIVED ↘ DEAD_LETTER一条消息被 Agent 提交后,第一步进入消息缓存并落库。这里不是简单存一下,而是要生成全局唯一的reach_id,后续所有渠道、所有回调、所有日志都绑定这个 ID。然后进入分发队列,消费者根据路由结果选择 adapter 发出请求。
渠道回执分为两级:一级是“送答回执”(ACK),代表渠道侧确认接收;二级是“用户反馈”(FEEDBACK),代表目标用户真的看到了消息或者有后续操作。两级回执之间通常存在较大时间差,比如邮件渠道的 ACK 很快,但用户回复可能要隔几个小时甚至隔天。
这个时间差就是 Agent-Reach 的校准空间。我们用动态超时来解决:普通 IM 消息 60 秒内必须有回执,否则进入重试;邮件 5 分钟内判定是否投递成功;短信 30 秒内收到网关回执。每个渠道的超时都不一样,全部放在配置中心由后台可以动态调整,不需要发版。
3.2 重试与幂等设计
重试是触达层最容易翻车的地方。粗暴的重试会造成消息重复推送,完全不敢重试又会丢失关键触达。我们最终采用的是“阶梯重试 + 幂等键 + 人工死信”的组合方案。
先说重试策略。默认四次重试,间隔分别是 1 秒、3 秒、9 秒、27 秒,也就是指数退避加一点抖动。这个配置主要针对瞬时故障,比如网络抖动、服务重启、连接池打满。超过四次后消息进入死信队列,由值班人员人工处置。
幂等是防止重复的重中之重。每个动作请求在进入时就会生成一个dedup_key,通常由业务键 + 动作类型 + 接收者拼接后做哈希。例如订单完成提醒触发的触达动作,dedup_key 可能是order_20250101_12345:notify_customer:user_8899的 md5。这个键会存储在 Redis 里,有效期为 24 小时。一旦相同键的请求再次进来,直接返回上一次的处理状态,不会二次发送。
另外还有一层更精细的控制:对于支持带幂等字段的渠道(比如短信平台的sequence_id,企业微信机器人侧的msgid),我们会把reach_id直接透传过去。这样即使重试过程中上游协议出现了语义模糊,渠道侧也能识别出这是同一条消息。
3.3 超时、限流与配额管理
与重试配套的是超时与限流的参数设计。这块我踩过比较深的坑,早期把所有渠道的超时统一设成 5 秒,结果邮件渠道经常误判失败,短信渠道反而出现大面积阻塞。后来改成表格里的这套参数:
| 渠道类型 | 发送超时 | 回执等待 | 基线 QPS | 单日配额 |
|---|---|---|---|---|
| 企业微信机器人 | 3s | 60s | 50 | 20000 条 |
| 钉钉工作通知 | 3s | 60s | 50 | 20000 条 |
| 短信网关 | 5s | 30s | 20 | 5000 条 |
| 邮件 SMTP | 10s | 300s | 30 | 10000 封 |
| Webhook 回调 | 2s | 不适用 | 100 | 无配额 |
这套参数不是一拍脑袋定的。短信限流 20 QPS 是因为接入的那条通道一旦超过 25 QPS 会触发熔断,保险起见我们把基线压在 80% 以下。邮件回执等待 300 秒,是因为发信服务器经常出现队列积压,太短会导致大量误重试。
限流器用的是令牌桶。每个渠道独立的令牌桶,容量等于基线 QPS,补桶速度也等于基线 QPS。这样做的好处是,短时间内来了 200 条推送,系统会平滑地分批发出去,而不是一股脑打到渠道网关。如果令牌桶持续为空超过 30 秒,路由层会自动把部分低优先级消息降级到备用渠道。
4. 一个完整落地案例:销售线索触达 Agent
4.1 需求与流程设计
项目上线后的第一个重量级业务是销售线索触达。业务逻辑是这样的:客户在官网留资后,系统分配一个销售代表,Agent 需要判断线索质量并执行触达动作。之前的做法是纯规则的 DCM 系统,只会机械地发一条模板消息,转化率一般。
和业务方聊完后我们圈定了一个闭环流程:
留资事件触发 → Agent 根据线索来源、页面停留时长、IP 归属地等信号生成线索评分 → 评分高于 60 分则生成一个包含个性化话术的动作 → Agent-Reach 选择渠道(优先企微,其次短信) → 发送后回收“是否回复”作为反馈 → 如果 30 分钟内没有回复,自动升级为二次动作,换短信补充触达 → 如果二次也没有回复,生成一个销售认领任务。
这个流程里的关键点在于,Agent 不是一次性把所有触达动作全部生成,而是根据上一次的反馈逐步推进。触达层在中间起的作用,就是保证每一次动作都真正送达,并且把反馈结果实时推给 Agent 决策端。
4.2 配置示例
Agent-Reach 的配置采用 YAML。这个配置是简化后的示意,但结构上完全来自我们线上系统。
agent: name: lead_nudge_agent description: "用于销售线索的首轮触达与跟进" channels: preferred: - wecom_robot - sms fallback: - email routing: dedup_key: - ${lead_id} - ${action_type} - ${customer_id} priority: high: [wecom_robot, sms] normal: [wecom_robot, email] batch: [email] message: template: "tpl_lead_first_reach" enable_personalization: true max_length: 800 feedback: wait_time: 600s on_timeout: - action: escalate_to_second_reach channel: [sms] after: 1800s retry: max_attempts: 4 backoff: [1s, 3s, 9s, 27s] dead_letter: actions: - create_ticket_for_sales_manager字段并不复杂,但每一条都和业务流程有关系。feedback.wait_time设成 600 秒,是因为我们分析了历史数据,80% 的线索回复发生在商家首条触达后 10 分钟内。on_timeout里的escalate_to_second_reach是触达层内置的一个特殊动作——它不是简单重发,而是生成一个新的触达计划,换渠道、换话术、换时间点。
4.3 上线后的关键指标
这个场景上线后跑了三周,我们重点看四个指标:
触达成功率从系统替换前的 94.2% 提升到 98.7%。剩余的 1.3% 大多是用户手机号格式错误或企微已删除员工,这类问题必须靠人工清洗,系统再怎么重试也没用。
线索回复率提升了大约 5 个百分点。这个不完全归功于触达层,Agent 的个性化话术也贡献了一部分,但触达层保证了每个动作不是在用户手机里静默消失,而是真正推到了能引起注意的通道上。
平均首响时长从原来的 47 分钟降到 28 分钟。主要原因是此前纯 DCM 系统只在工作时间发消息,Agent-Reach 落地后通过判断用户活跃时段,把部分消息调整到晚上八点到十点之间发送,回复率明显改善。
最逗的是销售团队报障次数。以前每周都有销售反馈“客户说他没收到消息”,排查发现是系统掉了回执,实际上消息发了但库里状态没更新。Agent-Reach 把这两件事分开了——只要渠道返回 ACK,就说明送答成功,销售能直接看到“已送达”状态,争执直接减少了一大半。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
下面这几个问题是 Agent-Reach 上线以来被问得最多的,也是新手最容易处理的几个方向:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 消息发了两次 | 幂等键过期或未传 dedup_key | 查看 Redis 中的 dedup_key 有效期及请求链路 |
| 渠道显示成功但用户没收到 | ACK 不代表用户看到,只代表渠道受理 | 查看 FEEDBACK 状态,确认是否有用户反馈 |
| 重试次数耗尽仍失败 | 渠道长时间故障或参数不可用 | 进入死信队列,人工处理并修正配置 |
| 高优先级消息被排队 | 超出渠道 QPS 配额 | 查看令牌桶消费速率,考虑扩容或降级 |
| 邮件模板变量填充错误 | 模板变量和 payload 键名不一致 | 校验 message_meta 中字段映射 |
| 死信队列堆积 | 多个动作绑定同一失效渠道 | 临时调整路由规则,强制切到备用渠道 |
5.2 让我印象最深的一次线上事故
有一次凌晨,企微机器人渠道出现大面积回调延迟。正常的 ACK 应该在 60 秒内返回,但当晚大量消息卡在 DISPATCHED 状态超过 15 分钟。按正常逻辑,超过回执等待时间应该触发重试,但诡异的是重试机制没有生效。
查了很久才发现问题出在配置中心的一个开关上。当时为了测试新版本,把重试总开关短暂关闭了,测试结束后运维忘记恢复。也就是说,所有消息在发出去之后,即使确认失败也不会进入重试流程,全部原地等待直到超时。那次事故让我们养成了两个习惯:一是配置中心的所有开关都要有操作审计日志;二是重试配置必须有独立的健康检查,一旦检测到“长时间无任何重试动作”就自动告警。
5.3 安全审核与人工兜底
Agent-Reach 可以触达真实用户,所以有一层安全兜底逻辑是必不可少的。两个阈值很关键。
第一个是敏感动作确认阈值。比如 Agent 要向用户发送包含退款链接、转账信息、合同文件的内容,这类动作必须经过人工审批后才允许执行。系统会生成一个预操作详情,审批人在企微上点“确认”后动作才进入执行队列。
第二个是消息内容敏感词拦截。LLM 生成的内容不可控因素较多,虽然上线前有 prompt 约束,但仍然出现过误把竞品信息写进话术的情况。所以 Agent-Reach 在编排层做了内容合规检查,匹配到敏感词库时会把消息改为待人工确认状态。
这条设计花了额外两天做,实际救过我们至少三次。有一次 Agent 把邮件正文里的价格变量错误渲染成了负数,系统拦截后值班人员人工修正了模板映射,避免了一次面向 5000 个客户的错误推送。所以我想说的是,控制 Agent“做什么”很重要,控制 Agent 的“出口”同样重要。
6. 技术选型复盘与扩展经验
6.1 为什么选这些组件
Agent-Reach 的核心存储用了 Redis 加关系型数据库的组合。Redis 存储短时效数据,包括 dedup_key、令牌桶、待回执状态表;关系型数据库存储消息历史、模板配置、审计日志。
一开始有人提议全链路用纯消息队列,加一组消费者做分发。但我们最终保留了数据库存储,主要原因是触达数据的审计需求。这类数据涉及客户触达记录,业务部门会来追溯:某年某月某日对某客户做了什么操作,结果是什么。纯消息队列很难准确回答这类查询,而明细表的索引查询可以秒级返回。
队列选型上,我们用了 Redis Stream 而不是直接上 Kafka。原因很简单:这里的触达量级还没有大到需要分布式队列的水平,Redis Stream 的复杂度低、运维成本低,出了问题可以直接看 Redis 状态。当触达量增长到每秒数千条级别,再迁移到 Kafka 也不是难事,底层的 adapter 接口不需要改动。
6.2 还能怎么扩展
Agent-Reach 目前的动作类型主要是“发送消息”和“调用 action”。下一步值得做的扩展是接入更多的 IOT 类触达,比如智能硬件上的提醒、线下门店的现场通知。核心的 adapter 模式完全能覆盖,只是一层新的协议适配。
另一个值得扩展的是多渠道的归因分析。目前系统可以看到每条触达的动作记录与反馈结果,但如果同一用户通过多个渠道被反复触达,最终转化到底归因于哪次动作,没有现成的数据能力。这是我在观察数据时发现很遗憾的一块,如果未来纳入归因模型,运营团队能更精准地控制触达节奏。
6.3 实战中的观察与体会
项目跑下来之后,我最大的体会是:触达层的价值,不在于你把消息发得有多快,而在于系统能诚实地告诉你每条消息到底执行到了哪一步。Agent 管住“决策”两端——做什么、依据什么做;触达层管住“出口”——是否送达、是否被看见、是否引起反馈。二者互为犄角,缺一不可。
还有一个心得体会是,设计这类系统时不要把重试、超时、幂等这些底层机制藏在代码里。把它们做成配置项,甚至做成后台可操作的管理界面,能让后续维护轻松很多。因为线上的问题永远比测试环境复杂,没有动态调整的能力,每次故障都要发版解决,团队迟早会被拖垮。
最后分享一个小技巧:给每个触达动作设计一个结构化的reason_code,比如reason_no_answer、reason_channel_down、reason_quota_exceeded。这些代码不仅方便排查问题,还能反过来修正 Agent 的决策策略。我们已经开始把失败原因反馈给 Agent 的 prompt 上下文,让它在下次决策时避开最近故障率较高的渠道。这种“反馈回灌”的做法,才是触达层从被动执行工具进化为智能体能力一部分的关键一步。