☰
Agent-Reach:AI智能体触达基础设施的架构设计与落地实践
2026/10/28 21:45:56 网站建设 项目流程

做完 Agent-Reach 这个项目之后,我最大的感受是:以前做 AI 应用,大家把精力都放在模型能力、Prompt 调优、知识库召回这些“大脑”层面的东西上,但真正把智能体放进生产环境时,最先被挑战的往往不是模型聪不聪明,而是“它怎么才能主动找到用户”这件事。

Agent-Reach 的定位很简单,它是一套面向智能体的触达与连接基础设施。它解决的核心问题是:当 AI Agent 需要主动发起对话、发送通知、执行提醒或进行用户触达时,如何做到有通道、有策略、有风控、有追踪。如果你正在做 AI 客服、营销助手、CRM 智能化升级,或者任何需要 Agent 主动“开口说话”的系统,这篇文章应该能给你一些可以直接落地的思路。

这套系统在产线里跑了大半年,接入了邮件、短信、IM 三类渠道,日均处理触达任务十万级,整体送达率稳定在 99.2% 以上。我会从项目拆解、架构设计、实操实现、问题排查四个维度完整复盘,尽量说人话,把踩过的坑和最后的选择都写清楚。

1. 项目定位:Agent-Reach 到底在解决什么问题

1.1 智能体为什么需要一张“触达网”

大多数 AI 应用目前的交互模式是“被动响应”的。用户发来消息,Agent 理解意图,调用工具,返回结果。这种模式在客服问答、知识查询、内容生成场景里没有问题,因为它天然是用户发起、Agent 响应的闭环。

但一旦进入业务自动化场景,事情就变了。典型例子包括:购物车里有商品但三天没下单,Agent 需要主动提醒用户;用户订阅了某个价格监控,Agent 需要在价格跌破阈值时主动推送;企业内部运维告警,Agent 需要主动拉起值班人员并跟进处理结果。这些场景的共同特点是,Agent 必须先于用户发起触达,而且要在正确的时机、通过正确的渠道、用正确的频率去触达。

传统做法是直接在业务代码里硬写调用逻辑,比如下单后调一次短信接口,超时后再调一次邮件接口。这套逻辑在规则固定的场景下能跑,但换成 AI Agent 来驱动就不够用了。因为 Agent 的触达行为是动态决策的,它可能在同一时间对同一个用户产生多种触达意图,也可能因为上下文变化而取消或延迟某个触达计划。

Agent-Reach 的核心就是把这个动态触达能力从业务系统里抽离出来,形成独立的“触达网”。它统一管理渠道连接、触达决策、频率控制、内容合规、结果追踪,让 Agent 只需要表达意图,由基础设施来保证触达被正确执行。

1.2 Agent-Reach 不是什么

这块我踩过不少认知上的坑,先说清楚边界,免得后面设计走偏。

Agent-Reach 不是消息推送 SDK 的套壳。常见的推送 SDK 面向的是“App 端推送”这种相对固定的通道,关注的是设备注册和厂商通道适配。而 Agent-Reach 面对的是多渠道、多形态的触达目标,统一抽象的是“目标对象”,不是“设备”。

Agent-Reach 也不是通知网关。通知网关的核心是“把消息从 A 点送到 B 点”,不关心谁触达、什么策略、是否合规。Agent-Reach 在通知层之上多了一层“策略层”,它要回答的是为什么在这个时间触达、为什么选这个渠道、为什么这个频率是安全的。

Agent-Reach 更不是内容生成引擎。它的职责是“把已经生成好的内容,以正确的方式、在正确的时间送给正确的人”。内容是否合规、是否有欺骗性、是否适合某个渠道,由安全层做规则校验,但生成动作本身不属于触达基础设施的范畴。

看清楚这三条边界之后,Agent-Reach 的职责就变得非常清晰:它只管“触达”这件事,从决策到执行到追踪,形成一个完整的工程闭环。

1.3 适用场景与边界

从我实际跑下来的经验看,以下场景用 Agent-Reach 收益最明显:

  • 电商场景的主动挽留和唤醒。比如用户加购未支付、领券未使用、收藏商品降价,Agent 根据业务规则主动触达。
  • 企业服务场景的智能跟进。比如销售线索分配后 Agent 自动发送资料、预约提醒、跟进回访。
  • 自动化运维场景的告警触达。运维 Agent 检测到异常指标,自动向责任人发出告警并持续跟进直到确认处理。
  • 营销自动化场景的个性化触达。基于用户行为数据,Agent 决定在特定时间点推送个性化内容。

不太适合的场景也有。比如需要实时双向长连接的音视频通话,这样的场景需要的是传输层协议,不是触达基础设施;再比如需要极高实时性的系统级通知,消息必须在几十毫秒内到达,Agent-Reach 这种带策略编排和风控的链路反而会拖慢速度。

一句话总结:Agent-Reach 适合的是“AI 主动发起、需要策略编排、需要全链路追踪”的业务触达场景,不适合纯实时信令和底层传输场景。

2. 核心设计思路:连接、策略、安全三层架构

2.1 整体架构长什么样

Agent-Reach 的整体架构我把它分成三层:连接层、策略层、安全层。

连接层负责的是“渠道适配”。它屏蔽了邮件、短信、IM 之间的协议差异,向上提供统一的发送接口。不管是发邮件还是发短信,对上层而言都是调用同一个 Send 方法,传一个统一的触达任务对象进去就好。连接层内部管理每个渠道的连接池、超时时间、重试策略、回执解析。

策略层负责的是“触达决策”。它决定一条触达任务在什么时候发出、通过什么渠道发出、要不要延迟、失败后怎么重试。策略层从 Agent 或其他业务系统接收“触达意图”,结合用户偏好、历史触达记录、渠道压力、时间窗口这些因素,生成具体的执行计划。

安全层负责的是“触达防护”。它对所有出站消息做目标校验、内容合规检查、频率限制,对异常行为做熔断和告警。安全层是整个链路里的兜底,哪怕策略层判断失误或者上游系统逻辑出错,安全层也要能挡住问题消息。

三层之间的关系不是简单的前后调用,而是互相穿插的。安全层在策略层决策前做预检,在连接层发送前做最终验证。策略层生成计划后,实际执行时也可以动态调整。

2.2 为什么必须拆开

我做第一个版本的时候没有拆层,所有逻辑都堆在一个 Service 里。Agent 调一个方法,里面先查用户偏好,再判断渠道,然后拼模板,最后调渠道 SDK 发送。当时觉得挺顺的,等接入第二个渠道、第三个渠道的时候开始崩溃。

崩溃的点主要在三处。第一,渠道参数散落在各处。短信限流规则写在 A 文件里,邮件退订逻辑写在 B 文件里,导致每次加渠道都要改核心业务代码。第二,频率控制完全失效。因为没有独立的策略层做统一控制,不同业务线都在各自发消息,同一个用户一天被触达七八次,投诉率直线上升。第三,事故回溯困难。用户投诉收到垃圾消息时,根本查不出来是哪条业务链路发出来的,因为没有统一的 Trace ID 贯通全链路。

拆成三层之后,这些问题被结构性地解决了。渠道变了只改连接层,策略变了只改策略层,安全规则变了只改安全层。这个拆分让团队协作也顺了很多,负责渠道对接的人不需要理解业务策略,负责策略编排的人不需要关心渠道 SDK 怎么调。

2.3 关键设计取舍

几个核心设计取舍值得单独拿出来说。

第一个取舍是“同步转异步”。一开始我把触达设计成同步调用,Agent 发出请求后等着渠道返回结果。实测发现这是个大坑。短信网关在高峰期响应能到 5 秒以上,邮件服务偶尔会出现超时重连,同步调用不仅拖垮了 Agent 的响应速度,还把渠道的故障直接传导给了上游系统。后来改成异步模式:Agent 提交触达任务后立刻收到一个任务 ID,后续结果通过回调、轮询、投递状态查询去获取。这个改动让整个系统的稳定性和吞吐量都上一个台阶。

第二个取舍是消息幂等。很多触达任务在业务层面天然有重复触发的可能,比如 Agent 因为上下文丢失重发指令,或者定时任务因为负载高被多次调度。如果不做幂等,用户就会在短时间内收到多条一模一样的消息。我的做法是给每个触达任务生成一个全局唯一的触达 ID,这个 ID 由任务类型、目标对象 ID、业务事件 ID 共同哈希得到,在存储层面做唯一约束,重复请求直接丢弃。

第三个取舍是状态机驱动。每条触达任务从创建到结束,经历的状态包括 pending、scheduled、sending、delivered、failed、cancelled。状态之间的转移是严格受限的,比如 failed 状态只能转移到 rescheduled 或者终态,不能直接跳回 sending。这个模型让整个系统的行为变得可预期,也让排查问题变得非常简单,只要看一条任务当前在哪个状态,就能判断它接下来会怎么走。

2.4 可观测性设计

触达这件事有一个容易忽略的问题:消息"发出去了"不等于"到了"。邮件进了垃圾箱,短信被运营商拦截,IM 消息被折叠,这些从我们系统视角看都是"送达成功",但实际用户根本没有看到。所以 Agent-Reach 在可观测性上做了两层设计。

第一层是链路追踪。每条触达任务从创建开始就带有一个 Trace ID,贯通策略决策、安全检查、渠道发送、回执解析全过程。排查问题时,只需要拿着 Trace ID 查一遍,就知道每条消息在哪个环节卡住了。

第二层是回执分析。我们和渠道方约定了一套回执标准,渠道除了返回"已受理"之外,还会异步返回真实的送达结果,比如邮件可能返回 hard_bounce 或 spam_report,短信可能返回 delivered 或 undelivered。这些回执会被归一化后写入状态存储,用来计算真实的送达率数据。

这里打个比方。触达就像寄快递,系统只看到"揽收成功"还远远不够,得能看到包裹到了哪个中转站、是否被签收、为啥退回来,才能真正算清楚服务质量。

3. 实操过程与核心实现

3.1 基础组件选型

Agent-Reach 的实现用了几个比较常见的基础组件,选型理由如下表:

组件选择理由
核心网关服务Go高并发连接处理能力强,部署简单,GC 开销可控,适合这种连接密集型服务
消息队列RabbitMQ触达任务异步化的核心依托,支持延迟队列、优先级队列,语义清晰直接
状态存储Redis + PostgreSQLRedis 存热状态和计数,PostgreSQL 存触达任务持久化记录和历史归档
监控体系Prometheus + Grafana指标采集和仪表盘展示,有一套现成生态,接入成本低
配置管理YAML + 配置中心触达计划、渠道参数、安全规则全部外部化配置,不随代码发布

Go 在连接层这块有天然优势,因为它对并发模型支持得特别自然。每条渠道连接都是独立的 goroutine,连接池管理、心跳保活、超时控制写起来很顺手。之前用过 Java 写过一版,连接数上来之后线程调度压力不小,换成 Go 之后资源占用直接降了一半。

队列层用 RabbitMQ 是看中了它的延迟队列插件。触达任务经常需要"5 分钟后提醒"、"明天上午 10 点触达"这类定时能力,延迟队列可以直接支持,不需要额外写一堆定时调度的代码。

存储这块要分开说。Redis 主要存两类数据:一类是频率控制的计数器,对性能要求极高,用单线程原子操作最稳;另一类是触达任务的热状态,需要支持高频状态更新。PostgreSQL 存的是全量触达记录,用于审计、分析、重放。两者互补,别用错场景。

3.2 统一渠道接入的实现

连接层最重要的一个接口是 Channel,它把所有渠道的能力抽象成一个统一入口:

type Channel interface { Send(ctx context.Context, task *Task) (*Receipt, error) Name() string Capabilities() ChannelCapabilities } type ChannelCapabilities struct { SupportTemplate bool SupportReceipt bool MaxContentLength int AllowedMIMETypes []string }

每个渠道只需要实现这个接口,就能接入 Agent-Reach。邮件渠道的 Send 方法会调用 SMTP 服务,同时生成退订链接和邮件头信息;短信渠道的 Send 方法会调用运营商网关,同时做短信签名和内容长度校验;IM 渠道的 Send 方法会调用 Bot API,同时处理消息类型和回调地址。

这个设计带来的最大好处是接入新渠道的成本极低。我们后来接入一个新的 IM 渠道,只需要写一个不到 200 行的适配器,不需要动任何上层的策略和业务逻辑。

模板渲染也是在这层做的。不同渠道对内容格式的要求不同,比如短信要求纯文本且不超过 70 个汉字,邮件支持 HTML 且可以带附件,IM 支持 Markdown 和交互式按钮。连接层维护了一个模板映射表,每条模板在创建时绑定渠道类型,渲染时自动选择对应版本。

这里有个很重要的细节:模板渲染必须做变量校验。如果业务系统传进来的变量值包含跳转链接、骗赞话术、敏感词,必须在渲染前就拦截掉。这个校验逻辑可以在安全层做,但渲染动作本身在连接层。

3.3 触达策略编排的全流程

策略层接收的是"触达意图",它本身不关心模板怎么拼,而是关心任务怎么排期、频率怎么控制、渠道怎么选。

一个触达计划的典型配置长这样:

reach_plan: name: cart_recovery_3h trigger: event: cart_abandoned delay: 3h target: object: user preference_field: reach_channel scheduling: time_window: [10:00, 21:00] timezone: Asia/Shanghai max_reach_per_day: 1 channel_priority: - im - email - sms fallback: next_channel_delay: 15m

这个配置表达的意思是:当用户把商品加入购物车后 3 小时仍未支付,Agent 发起一条触达计划。目标对象是用户,触达时间必须在用户所在时区的 10 点到 21 点之间,每天最多触达 1 次。渠道优先级是 IM 首选、邮件次选、短信兜底。如果首选渠道发送失败,15 分钟后自动切换到下一个渠道。

这里最值得强调的是时间窗口和时区处理。最初版本用服务器时间做窗口判断,结果给海外用户发触达时,要么凌晨收到消息,要么被时间窗口挡住一直发不出去。改成按用户时区判断之后,体验立刻改善了。这个细节看起来简单,但涉及所有渠道和所有计划的配置项,属于那种一开始就要设计好、后期很难补的地方。

频率控制是策略层的另一个关键点。它的目标是保证两个维度:单个用户每天收到的触达总数不超限,以及同一个用户在一个触达计划下重复触达的间隔不少于最小间隔。第一个维度靠全局计数器,第二个维度靠每个计划的去重键。

控制频率的代码简化版如下:

-- 基于 Redis 的 Lua 脚本,原子完成频率检查和计数累加 local key = KEYS[1] local limit = tonumber(ARGV[1]) local horizon = tonumber(ARGV[2]) -- 时间窗口秒数 local now = tonumber(ARGV[3]) local current = redis.call('GET', key) if current and tonumber(current) >= limit then return 0 end local pipe = redis.call('PIPELINE') redis.call('INCR', key) redis.call('EXPIRE', key, horizon) local new = redis.call('GET', key) return new

这个脚本按用户维度加锁,保证多实例部署下不会出现同一个用户在同一秒被两个节点同时放行的情况。我在生产环境实测过,QPS 到两千左右依然稳定,Redis 的单线程模型在这一场景下反而是优势。

3.4 安全风控层的配置与实现

安全层的职责是接住所有不该发出去的消息。它看起来不如策略层花哨,但真正守住了整个系统的底线。

第一道是目标校验。发送之前必须校验目标对象的基本信息,比如手机号是 11 位数字、邮箱格式合法、IM 账号状态正常。这条校验放在最早的位置,因为后面所有操作都要基于目标的合法性展开。

第二道是内容合规检查。所有发送内容经过关键词过滤和模板变量检查。我们维护了一份动态更新的敏感词表,同时做了两个用户可感知的功能:邮件必须带退订链接,短信必须支持回复退订。这两个功能不只是合规要求,更是降低投诉率的关键。

第三道是速率限制和熔断。每个渠道都有独立的速率上限,比如短信渠道每分钟最多 300 条。当触达任务量激增时,连接层会把超出的任务缓存在队列里排队发送,而不是直接打到渠道网关。如果连续多次发送失败,熔断器会直接打开,暂停该渠道的流量,等技术确认原因后再恢复。

风控层的一个关键经验是:不要把所有拦截逻辑都放在代码里硬编码。我们把风控规则做成可配置项,运营同学可以在不改代码的情况下调整关键词表、调整限流阈值。真正需要写代码的只是规则的执行引擎,规则本身的变更成本要降到最低。

3.5 效果度量与回执处理

触达做完不算完,还得知道效果怎么样。Agent-Reach 定义了四个核心指标:送达率(delivered / sent)、打开率(opened / delivered)、响应率(replied / opened)、投诉率(complaints / delivered)。

这四个指标中,前两个相对客观,后两个主观性更强。打开率的计算依赖渠道回执,比如邮件服务支持像素跟踪,IM 渠道会返回 read 状态。响应率则需要业务层配合,识别用户是否对触达发起了行为。

回执处理有一个容易踩坑的点:很多渠道的回执是延迟到达的,甚至可能在几天后才到。我们的做法是双通道并行:实时回执走回调接口,逾期未收到的任务由定时扫描补偿。每天早上八点扫描一次所有处于已发送状态但超过 24 小时没有回执的任务,主动到渠道方查询一次投递状态。

延迟重试算法用的指数退避,规则如下:第一次失败 5 分钟后重试,第二次失败 25 分钟后重试,第三次失败 2 小时后重试,最多重试 4 次。这个节奏是从实践里调出来的,太密集容易赶上渠道方的限流窗口,太稀疏又会让用户等待过久。

4. 常见问题与排查技巧实录

4.1 问题速查表

把我在 Agent-Reach 的运维过程中遇到的典型问题整理成了一张速查表,直接照着排查能省很多时间:

问题现象可能原因排查思路
消息发出去了但用户没收到被渠道方判定为垃圾内容、进了垃圾箱查看回执中的 spam_report 标志;检查短信签名和邮件发信域名信誉
同一用户被重复触达任务幂等键冲突、状态机重复流转检查任务唯一约束是否生效,查相同 Trace ID 的重复执行记录
回调丢失导致状态卡死渠道回执接口不稳定、回调地址超时补上定时扫描逻辑,主动查询投递状态,超时重置状态
高峰期渠道限流全局速率超过渠道配额查看连接层限流器配置,调低速率上限,启用队列缓冲
触达计划一直未执行时区窗口设置错误、延迟队列消息丢失检查计划的时间窗口配置,查 RabbitMQ 延迟队列积压情况

4.2 排查实战案例

第一个典型案例是用户重复触达问题。上线第二周就爆了,有个用户在一个小时内收到三条“你有一张优惠券即将过期”的短信。查了半天才发现触达任务的幂等键没有加唯一约束,Agent 在上下文丢失后重新提交了同一个任务,系统没有识别出来,直接重复执行了。

修复方案是在 PostgreSQL 表上加唯一索引,字段是(plan_id, target_id, event_id),重复插入直接返回冲突。从那之后这个问题的发生频率降为零。

第二个案例是回调丢失导致状态卡死。当时用了一个 IM 渠道的回调接口,渠道方偶尔会延迟几小时才返回送达回执。我们的同步逻辑只依赖回调,结果大量任务卡在 sending 状态,既不重试也不终态。

后来补了一个 Reconcile 组件,每隔 30 分钟扫描一次超过 30 分钟未更新状态的任务,主动向渠道方发起状态查询请求,拿到真实投递状态后更新任务状态。这个补偿机制很土,但非常有效,解决了所有渠道回调不稳定的问题。

第三个案例是限流配置失效。多实例部署后发现短信渠道的发送频率超过了我们设定的阈值。排查发现限流器用的是单机版本的令牌桶,每个实例维护自己的桶,加起来自然超了全局限额。

改成 Redis + Lua 的全局计数方案后,这个问题彻底解决。这也验证了一句话:分布式系统里,任何依赖本机内存状态的组件都是天然不可靠的。

4.3 独家避坑经验整理

除了上面这些系统级问题,还有几条经验比较碎,但每一条都来自真实事故。

模板变量必须白名单化。业务侧接入时提交了一堆模板变量,有两个模板里带了用户输入的原始文本,结果被别有用心的人塞了营销文案进去,通过我们的系统批量发垃圾消息。现在所有模板变量都要预先声明类型和长度,用户输入型变量强制走安全层过滤。

时区是个隐形炸弹。系统刚上线时只用了服务器时区,结果一个面向北美用户的触达计划在服务器时间凌晨触发,用户收到的全是夜间推送。现在所有触达计划都强制要求声明时区,默认值都不给。

测试环境和生产环境的渠道必须隔离。曾经有一次测试环境的触达任务误加了一个真实手机号收件人,导致测试短信直接发到了真实用户手机上。后来的方案是测试环境统一使用一个固定的测试接收列表,任何不在列表里的目标都会被安全层拦截。

"送达"的语义要跟渠道方确认清楚。不同渠道对送达的定义完全不同。邮件渠道的 delivered 只代表进入用户邮箱系统,不代表用户看没看;短信渠道的 delivered 只代表网关接收成功,不代表用户手机一定收到了。在做数据考核时,这个语义差会导致指标完全失真。

最后一点个人体会

Agent-Reach 这个项目做完之后,我心里最踏实的一点是,“触达”这个动作终于从一个靠不住的体力活,变成了一条有状态、有策略、能追踪的工程链路。以前每次给用户发消息都像开盲盒,发完不知道到没到、有没有效果、有没有引发投诉。现在所有环节都可观测,出了问题有迹可循,效率指标也能持续复盘中不断优化。

如果让我给后来者一个建议,那就是别想着一次就把所有渠道接完。先挑一个核心渠道,把从 Agent 意图到触达成功再到效果回传的整个闭环跑通,然后再接第二个渠道、第三个渠道。这套架构的边际成本很低,每增加一个渠道,连接层多写一个适配器就行,真正的精力应该花在上面那层策略和风控,因为那才是 Agent-Reach 真正的价值所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询