做智能体这几年我最大的体会是模型想明白和事情办成了之间隔着一整套工程。Agent-Reach 这个词最早出现在我们内部的一份需求文档里当时团队要解决的事情很具体——让智能体不只是会聊天还能主动把消息、提醒、任务、通知送到对的人手上并且在送不到的时候清楚知道自己送不到、为什么送不到。后来我们把这一层单独拆出来做内部叫它 Agent-Reach也就是智能体的触达层。它要解决的问题其实一点都不新鲜甚至可以说很老土怎么保证一条消息在正确的时机、通过正确的通道、以正确的频次、送到正确的人那里并且结果可追溯。新鲜的地方在于发起这条消息的从运营同学点一下按钮变成了一个会自己判断的智能体。判断可以交给模型但触达本身必须是确定性的——这就是 Agent-Reach 存在的全部理由。这篇内容适合三类人看正在给智能体补手脚的工程师、做增长和用户运营的产品同学、以及被重复打扰和漏发问题折磨过的运维同学。下面我按架构、参数、代码、排查四个角度把这一层从零到一讲透。1. 先想清楚 Agent-Reach 到底解决什么问题1.1 从会思考到能落地之间的那段断头路很多人做智能体的第一步是接一个模型让它能理解意图、生成内容、调用几个工具。跑通 demo 那一刻很爽但上线一周就会发现问题全在后面模型说我已经通知用户了实际上什么都没发出去或者发出去三条用户收到六条再或者凌晨两点给用户发了一条您本周的账单已生成。这些都不是智能问题而是触达问题。我习惯用一个比喻来解释模型是大脑Agent-Reach 是手和嘴。大脑负责决定要不要说、说什么手和嘴负责什么时候说、用什么方式说、说不出去怎么办。把这两件事混在一个函数里写前期快后期一定会痛。因为它们的变更频率完全不同——业务话术一天改五版而通道协议可能一年才动一次。混在一起写每次改文案都可能碰坏发送逻辑每次通道升级都可能改到业务规则最后没人敢动这块代码。Agent-Reach 的边界就划在这儿它不做内容生成不做意图判断只负责把一份已经决定好的触达意图可靠地执行下去。输入是给谁、发什么、走哪条通道、什么时间、什么优先级输出是成功、失败、被抑制、还是待重试中间所有的脏活累活都在这一层消化掉。1.2 谁需要这套东西三类典型场景画像不是所有项目都需要单独做一层。如果一天只发几十条消息一个定时任务加几张表就够了。但只要出现下面三种场景中的任意一种就该考虑把触达抽出来了。场景类型触发源常用通道频率量级最要命的约束客户服务类工单状态变更、会话超时站内信、邮件单用户低频身份核验、隐私边界增长运营类定时批量、行为事件短信、推送、邮件单用户中频、全局高频频控、退订、封禁内部协同类审批流、告警事件群机器人、私聊机器人单用户高频权限隔离、信息泄露第一类是客服和售后。用户在等一个结果智能体判断出需要主动同步进度这种触达的价值极高但风险也高——发错人就是把别人的订单信息发给了另一个人这是事故级的。所以这类场景里收件人必须由系统按主键查出来绝不能让模型自由填。第二类是增长运营。批量、高频、有配额、有退订是典型的工程问题。这类场景最容易踩的坑是活动峰值打满通道配额导致当天后面所有正常通知都发不出去。第三类是内部协同。频率最高对延迟最敏感但用户容忍度也最高因为你发的是工作消息。这里的核心矛盾是权限一个智能体能不能把 A 项目群的消息转发到 B 项目群必须有明确的判定规则。我自己的经验是先把你的场景往这三类里对一下再决定架构做多重。内部协同类可以做得糙一点客服类必须做严增长类必须在频控上做到极致。1.3 三个岔路口自研、低代码编排、还是买托管定了要做接下来就是选型。这三条路我都走过说点实际的。维度全自研低代码编排托管服务上手速度慢约 2-4 周出第一版快1-3 天可跑通最快当天可用定制能力完全可控受限于平台能力基本无法改通道扩展自己写适配器平台支持的通道才能用平台支持什么就是什么数据归属自己掌握混合依赖服务商长期成本前期高后期低中等随量线性增长适合阶段有稳定团队、量大验证期、尝鲜极早期验证我给的建议是分阶段验证期用最轻的方式跑通策略到通道的完整链路证明这件事有价值量起来了再自研。最怕的是反过来——早期就上重架构结果业务方向变了一堆代码白写。不过有一个例外如果涉及用户隐私数据比如订单信息、健康数据我倾向于尽早自研哪怕功能少一点。因为这类数据一旦进到第三方系统后面想撤出来非常麻烦。2. 整体架构拆解一条消息从策略到用户手里要走几步2.1 五层结构策略、编排、通道、状态、观测Agent-Reach 我一般拆成五层每层职责清晰层与层之间只通过明确定义的数据结构通信。层级核心职责不该做的事典型产出策略层判断该不该触达不关心怎么发触达意图对象编排层决定怎么发、何时发、失败了怎么办不生成内容任务实例、调度计划通道层把标准请求翻译成通道协议不做业务判断发送结果、回执状态层持久化任务与结果保证幂等不含业务逻辑状态机、幂等索引观测层采集指标、日志、链路不参与决策指标、告警、归因报表这个拆法的核心价值在于每一层都可以单独测试、单独替换、单独扩容。策略层是纯函数输入上下文输出决策很好做单元测试通道层是适配器换一个通道就多写一个实现编排层是有状态的但状态全部落在状态层进程重启不丢。我见过很多团队把这五层压成一个服务代码量确实少了但一遇到我想把短信换成推送这种需求就要动到核心逻辑测试回归成本极高。2.2 为什么要把通道适配和业务策略强行拆开这一条我想单独说因为它是我踩过最深的坑之一。早期我们把如果是重要通知就发短信这条规则写在了发送函数里后来通道方调整了接口字段从msg_content改成了content我们改了一处结果发现另一个副本还在另一个文件里线上出现了一半成功一半失败的情况。拆开之后规则变成两件事策略层输出一个抽象的优先级字段通道层根据优先级和用户偏好映射到具体通道。通道接口变了只改通道层的一个文件业务规则变了只改策略层的配置。两者互不干扰。这里有个技术手段值得用依赖倒置。编排层只依赖一个抽象的发送接口不依赖任何具体通道。这个接口只有三个方法——发送、查询状态、取消。所有通道实现这个接口注册到一张表里。这样新增通道的成本就是写一个类 注册一行配置。顺带说一句通道层最容易被低估的是回执这件事。短信和推送的成功回执是异步来的可能是几秒后也可能是几分钟后而且不一定来。所以通道层必须支持无回执时的兜底判断通常是超时后按未知处理而不是按失败处理。按失败处理会导致重试重试就会重复触达这个后面细讲。2.3 状态机设计任务生命周期与幂等键状态层最怕的不是并发而是状态含义模糊。我建议一开始就把状态收敛成有限几个并且规定状态只能单向流转绝不回退。状态含义是否终态可流转到PENDING已创建待调度否SCHEDULED、SUPPRESSED、CANCELLEDSCHEDULED已排期待执行否DISPATCHING、CANCELLEDDISPATCHING已提交通道否SENT、FAILED、UNKNOWNSENT通道已受理否DELIVERED、FAILED、UNKNOWNDELIVERED用户已收到是无FAILED确定失败是无UNKNOWN回执超时是无SUPPRESSED被频控/规则抑制是无CANCELLED主动取消是无UNKNOWN这个状态是很多人不设的但它极其重要。它承认了一个现实有些发送出去的消息你永远不知道用户收到没有。如果把它算作失败去重试用户就可能收到两条如果算作成功统计上就不准。单独设一个状态业务方处理时可以选择发送前查一次状态或者接受不确定性。幂等键的设计我推荐用一个组合字符串业务ID 通道 收件人哈希 触达窗口。业务ID 保证同一件事不重复发通道保证换了通道可以再发一次收件人哈希保证换人不冲突触达窗口保证今天发过、明天还能发。这个组合上建唯一索引数据库层面就挡住了重复写入。注意幂等键里不要放时间戳到秒级。放秒级等于没有幂等因为两次调用时间戳必然不同。窗口应该用业务语义定义比如同一个账单周期同一天。3. 核心环节实现细节与参数怎么定3.1 触发时机定时、事件、混合怎么选触发方式决定了整套系统的形态。定时任务最简单一段 cron 扫一批任务实现快缺点是延迟不可控批量大时容易堆积。事件驱动实时性好但需要一套可靠的事件投递机制否则事件丢了消息就永远不发。我的做法是混合事件驱动为主定时兜底。事件来了立即处理同时有一个每五分钟跑一次的扫描任务找出所有超过预期时间还没进入终态的任务重新投递给编排层。这个兜底扫描的意义在于它能救回所有因进程崩溃、消息丢失、部署重启而卡住的任务。时间窗这块一定要做。用户本地时间的深夜和清晨不要发非紧急消息这个规则不是可选项。我在配置里一般设两个窗口普通消息允许 09:00 到 20:00紧急消息全天允许。判断时用用户所在时区计算不要用服务器时区——这一点在有多地区用户时特别容易出错我曾经因为没处理时区导致一批用户在北京时间凌晨三点收到消息投诉量直接翻倍。事件驱动还有一个容易忽略的点出站事件必须和业务事务一起提交。做法是在业务库里写一条待投递事件记录和业务数据在同一个事务里再由独立的投递进程读出来发到消息队列。这样业务提交成功事件一定不丢业务回滚事件一定不发。直接在一个事务里调消息队列两边不一致的概率非常高。3.2 频控与配额把参数算出来不要拍脑袋频控是 Agent-Reach 里最考验工程功底的地方。它要同时管四层全局总量、通道总量、用户维度、设备维度。任何一层没管住都会出问题。先解决参数怎么定。假设某通道每天可用配额是 100 万条历史数据显示流量的 40% 集中在上午 9 点到 11 点这两个小时。那么峰值时段的平均速率是100万 × 0.4 ÷ (2 × 3600秒) ≈ 55.6 条/秒这是平均值实际会有瞬时尖峰。按经验尖峰系数取 1.5 到 2 倍比较稳妥所以目标限流值定在 80 到 110 条/秒之间。定完这个数再往下分配给用户维度的限流单独设一条更严格的规则比如同一用户同一通道 24 小时内最多 3 条1 小时内最多 1 条。实现上我推荐令牌桶做全局和通道级限流滑动窗口做用户级限流。原因是全局限流允许一定程度突发令牌桶能积累令牌而用户级限流必须严格按时间窗计算不能有突发。两者都可以用原子脚本实现避免并发下的计数漂移。-- 用户级滑动窗口限流KEYS[1] 是用户通道维度的键 -- ARGV[1] 是窗口秒数ARGV[2] 是窗口内允许次数ARGV[3] 是当前时间戳毫秒 local key KEYS[1] local window tonumber(ARGV[1]) local limit tonumber(ARGV[2]) local now tonumber(ARGV[3]) local clearBefore now - window * 1000 redis.call(ZREMRANGEBYSCORE, key, 0, clearBefore) local count redis.call(ZCARD, key) if count limit then redis.call(ZADD, key, now, now .. - .. math.random(100000)) redis.call(PEXPIRE, key, window * 1000) return 1 end return 0这段脚本的关键点有三个用有序集合按时间戳裁剪过期记录、成员值加随机后缀避免同一毫秒覆盖、每次操作都续期防止键提前过期。返回 1 表示放行返回 0 表示被抑制。注意被抑制的任务不要直接标记成失败要标记成SUPPRESSED并记录抑制原因。很多团队把限流和失败混在一起统计结果告警一直响实际是限流在正常工作。除了硬性频控还有一类软性抑制用户明确表达过不想被打扰或者近期已经收到过同类消息。这类判断放在策略层做输出结果直接就是不触达不要走到编排层再拦能省掉大量无用计算。3.3 内容个性化与模板渲染的安全边界个性化是智能体的价值所在也是最容易出事的地方。我的原则很明确模板骨架由人定义模型只负责填槽位绝对不让模型自由生成整段内容。理由有三个。第一合规上自由生成的内容无法预先审核一旦出现不当表述责任在你自己。第二稳定性上模型每次生成的结果都不一样用户收到的消息风格漂移体验反而更差。第三可测试性上模板化的内容可以做快照测试生成的内容不行。模板设计要定几条硬规则变量必须来自白名单变量值渲染前做转义渲染后做长度截断。转义是为了防止变量里出现模板语法导致渲染异常长度截断是因为某些通道有严格字数限制超长会整条发送失败。from string import Template ALLOWED_VARS {user_name, order_no, amount, deadline} def render(template_str: str, payload: dict, max_len: int 300) - str: safe {k: str(v)[:100] for k, v in payload.items() if k in ALLOWED_VARS} for k in ALLOWED_VARS: safe.setdefault(k, 用户) # 变量缺失时的兜底值 text Template(template_str).safe_substitute(safe) text text.replace(\n, ).strip() if len(text) max_len: text text[: max_len - 3] ... return text这段代码里最值得说的是setdefault那一行。变量缺失是线上最常见的问题之一一个拼错的字段名会让整条消息变成尊敬的 None。给每个允许的变量都准备一个兜底值出错时至少文案是通顺的。另外一个细节渲染后的文本要去掉换行符再交给通道。因为很多通道对换行的处理不一致有的当分隔符有的直接截断统一成空格最安全。3.4 通道适配器的统一接口设计通道层的目标是新增一个通道只写一个文件。要做到这点接口必须足够抽象只包含所有通道都有的共性。from dataclasses import dataclass from typing import Protocol dataclass class SendRequest: task_id: str recipient: str title: str content: str priority: int # 1 低 2 普通 3 高 4 紧急 idempotency_key: str extra: dict # 通道私有参数业务层不感知 dataclass class SendResult: accepted: bool channel_msg_id: str | None error_code: str | None retryable: bool # 明确告诉编排层能不能重试 class Channel(Protocol): name: str def send(self, req: SendRequest) - SendResult: ... def query(self, channel_msg_id: str) - SendResult: ... def cancel(self, channel_msg_id: str) - bool: ...retryable这个字段是设计里最关键的一处。它把能不能重试的判断权交给了最了解通道特性的那层而不是让编排层去猜。比如参数错误不该重试通道限流应该重试网络超时应该谨慎重试。编排层只认这个布尔值逻辑就变得非常简单。4. 实操从零搭一个最小可用的 Agent-Reach4.1 环境准备与目录结构先说我用的技术栈都是很常规的东西Python 3.11、PostgreSQL 存任务状态、Redis 做限流和调度锁、一个消息队列做异步投递。这个组合在小规模下完全够用日均百万条以内不需要换。目录结构我一般这样组织agent_reach/ ├── core/ │ ├── models.py # 触达意图、任务、结果的数据结构 │ ├── state.py # 状态机与状态流转校验 │ └── idempotency.py # 幂等键生成与校验 ├── policy/ │ ├── rules.py # 该不该发、走哪条通道 │ └── frequency.py # 频控与抑制判断 ├── scheduler/ │ ├── dispatcher.py # 调度主循环 │ └── retry.py # 重试策略 ├── channels/ │ ├── base.py # 抽象接口 │ ├── mock_channel.py # 本地联调用假通道 │ └── sms_channel.py # 真实通道实现 ├── observability/ │ ├── metrics.py │ └── tracing.py └── config/ └── settings.yaml把假通道和真通道放在同一个目录、实现同一个接口是本地联调的关键。我在开发阶段完全不接真通道全部走 mock它按配置的概率返回成功、限流、超时三种结果能在本地把重试和幂等逻辑跑透。4.2 任务模型与幂等落库建表这块我给出核心字段重点是唯一索引和状态字段的长度冗余。CREATE TABLE reach_task ( id BIGSERIAL PRIMARY KEY, biz_id VARCHAR(64) NOT NULL, channel VARCHAR(32) NOT NULL, recipient_hash CHAR(64) NOT NULL, window_key VARCHAR(32) NOT NULL, idem_key VARCHAR(200) NOT NULL, priority SMALLINT NOT NULL DEFAULT 2, content TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT PENDING, attempt_count INT NOT NULL DEFAULT 0, next_retry_at TIMESTAMPTZ, scheduled_at TIMESTAMPTZ NOT NULL, channel_msg_id VARCHAR(128), last_error_code VARCHAR(64), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX uk_reach_task_idem ON reach_task (idem_key); CREATE INDEX idx_reach_task_due ON reach_task (status, scheduled_at) WHERE status IN (SCHEDULED, DISPATCHING);最后那个部分索引是性能的关键。调度器只关心到点该发的任务加上WHERE条件后索引大小只有全表的很小一部分扫描速度快很多。我实测过去掉这个条件后同样数据量下扫描耗时增加了六倍以上。写入时用ON CONFLICT (idem_key) DO NOTHING配合检查影响行数判断是新建还是命中已有任务。命中已有任务时不要报错直接返回已有任务ID让上游无感知这才叫真正的幂等。4.3 调度与重试退避参数怎么算调度主循环的骨架很朴素取一批到点的任务加锁逐个交给通道写回结果。def dispatch_once(batch_size: int 200): tasks claim_due_tasks(batch_size) # UPDATE ... RETURNING自带行锁 for task in tasks: if not rate_limiter.allow(task.channel, task.recipient_hash): mark_suppressed(task, reasonfrequency) continue try: req build_request(task) result channel_registry[task.channel].send(req) except Exception as exc: result SendResult(False, None, type(exc).__name__, retryableTrue) apply_result(task, result)claim_due_tasks我强烈建议用UPDATE ... RETURNING一条语句完成选中并锁定不要用先 SELECT 再 UPDATE。多个调度实例同时跑时前者天然不会抢到同一条任务后者需要额外加锁容易出问题。重试策略用指数退避加随机抖动。基础间隔 30 秒倍数 2最大间隔 1 小时最多重试 5 次。抖动范围取间隔的 ±20%防止大量任务在同一秒集中重试形成新的尖峰。五次重试的时间点大致是30 秒、1 分、2 分、4 分、8 分钟加上抖动后分布在一个较宽的区间里。注意只有retryableTrue的结果才进重试队列。参数错误、收件人不存在、内容被拒绝这类确定性失败重试一百次也不会成功只会浪费通道配额并可能触发风控。重试次数这块我也想多说一句。很多人把重试次数设成 10 次以上觉得更可靠。实际上如果前 5 次都失败了后面成功的概率极低而且这些任务会一直占着调度队列。5 次是个比较合理的平衡点超过就落到失败表里人工看。4.4 通道适配器实现与本地联调mock 通道的实现要刻意制造糟糕情况否则联调毫无意义。import random class MockChannel: name mock def __init__(self, fail_rate0.1, timeout_rate0.05, limit_rate0.05): self.fail_rate fail_rate self.timeout_rate timeout_rate self.limit_rate limit_rate def send(self, req): r random.random() if r self.limit_rate: return SendResult(False, None, RATE_LIMITED, retryableTrue) if r self.limit_rate self.timeout_rate: return SendResult(False, None, TIMEOUT, retryableTrue) if r self.limit_rate self.timeout_rate self.fail_rate: return SendResult(False, None, INVALID_RECIPIENT, retryableFalse) return SendResult(True, fmock-{req.task_id}, None, retryableFalse)用这个假通道跑一天的量你就能在本地看到各种状态分布也能验证幂等键是否真的挡住了重复。我一般会写一个脚本往里面灌 10 万条任务其中故意混入 20% 的重复幂等键跑完之后检查实际入库量是不是 8 万条。这个测试能一次性发现绝大多数幂等问题。4.5 观测埋点与归因怎么做才有用指标不是越多越好我一般只看五个任务创建量、实际发送量、送达量、抑制量、失败量。这五个数放在一起看能立刻判断系统是否健康——创建量涨但发送量不涨说明策略层把大量任务拦掉了发送量涨但送达量不涨说明通道侧有问题。埋点维度至少要有通道、优先级、失败码、时间窗口。这四个维度组合起来能回答大部分运营问题。比如今天下午推送送达率掉了按通道和时间窗口一筛很快就能定位是不是某个通道在某个时段限流了。归因这块我想提醒一句不要把用户点了直接等同于这次触达有效。用户可能本来就要点跟你发不发没关系。要做得更严谨得留一小部分用户作为对照组不发用两组的转化率差值来评估效果。这个做法在增长圈里叫留白实验成本不高但能让你的效果数据真实很多。我见过太多团队因为没做对照组把自然转化算成了触达功劳最后预算投得越多边际效果看起来越好实际上是错觉。5. 常见问题与排查技巧实录5.1 触达失败排查速查表线上出问题时最缺的是往哪看。下面这张表是我压箱底的排查顺序按错误码直接对。错误码最可能的原因首要排查动作是否重试RATE_LIMITED通道配额用尽或用户频控触发查当前时段令牌桶余量和用户窗口计数是TIMEOUT通道响应慢或网络抖动查通道侧响应时间曲线确认是否整体变慢是但限制次数INVALID_RECIPIENT手机号/地址格式错误或已注销抽样检查收件人字段确认数据源否CONTENT_REJECTED内容触发通道侧审核取出被拒文案检查敏感词和链接否AUTH_FAILED通道凭证过期或权限变更检查密钥有效期和权限配置否需人工DUPLICATE幂等键命中查幂等键生成逻辑是否符合预期否UNKNOWN_TIMEOUT回执超时未返回查回执接收服务是否正常谨慎一个实践技巧给每个错误码配一个建议动作字段写进告警内容里。半夜被告警叫醒的人最需要的是下一步做什么而不是哪个指标超标了。5.2 重复触达与漏触达的排查路径重复触达几乎只有三个原因幂等键设计有问题、重试逻辑把未知当失败、调度器并发冲突。排查顺序我建议从幂等键开始因为这是最常见的。具体做法是拉出重复触达的用户把这几次任务的幂等键打出来对比。如果键不同说明是幂等键设计漏了维度如果键相同但都发出去了说明唯一索引没生效或者写库用的是普通插入。第二种情况往往出现在先查再插的实现里两个进程同时查到不存在然后都插入了——所以一定要依赖数据库唯一约束不能只靠应用层查重。漏触达的原因更多一些调度器没扫到、任务卡在中间状态、时间窗口判断错误、被抑制但没记录。我一般会写一个对账脚本每天跑一次把策略层判定应该发和状态层实际发了两边做差集。差额超过千分之一就告警。这个脚本救过我们好几次——有一次是因为时区配置改了导致某地区用户的时间窗判断全部为处于免打扰时段任务被大量抑制但抑制原因记录不规范看板上一片绿灯是对账脚本先发现的。5.3 内容被判骚扰的几个隐性原因有些失败原因不写在错误码里而是体现在用户投诉里。我总结了几条容易被忽略的。第一条是内容重复。同一模板连续三天发给同一个用户即使每次都是合规的用户也会觉得被骚扰。解决办法是给模板加冷却期同一模板对同一用户七天最多用两次。第二条是触达时间接近用户的休息时间。晚上八点发非紧急消息技术上在窗口内感受上很差。我一般会把非紧急消息的窗口收窄到 10:00 到 19:00。第三条是看起来像群发。同一条消息在同一分钟内发给大量用户如果内容完全一致、没有个性化变量很容易被通道侧识别为批量发送并限流。给内容加上哪怕一个个性化变量称呼、编号通过率都会明显提升。第四条是退订入口不明显。这不是技术问题但会直接影响通道给你的配额。消息里必须提供清晰的免打扰设置方式用户找不到出口的时候只会去投诉。5.4 压测与容量评估的实操经验压测最常犯的错误是只压发送接口忽略状态写回。实际系统里每条消息至少涉及两次数据库写入创建任务、更新结果这两次写入才是瓶颈所在。压测时必须按完整链路打否则得到的数字毫无意义。我的做法是先用假通道把通道耗时设为 0压出一个纯工程上限这个数代表架构本身的吞吐能力然后把假通道耗时调到真实通道的 P95 值再压一次得到业务上限。两个数的差距说明了外部依赖对你的影响有多大。容量评估上我习惯按峰值 QPS 的 3 倍预留。因为线上流量从来不是平滑的活动开始那一刻的瞬时冲击往往是平时的十几倍。留 3 倍余量配合限流做保护基本不会出现雪崩。另外一个小经验压测时一定要同时观察数据库连接池的使用率。很多性能瓶颈其实是连接池打满了加机器没用得调池子大小或者优化事务长度。我们有一次排查了两天最后发现是某个更新语句忘了走索引导致行锁持有时间变长连接池被拖垮。这类问题只会在压测里暴露生产环境是碰运气。最后分享一个我自己一直在用的习惯。每次要改频控参数之前我先把当前一周的实际发送数据导出来按用户维度算一遍如果换成新参数会有多少任务被抑制、多少用户会收不到必要的通知。这个演练几分钟就能跑完但能避免很多上线之后才发现把重要通知也拦掉了的事故。参数调整这种事纸面计算永远比线上试错便宜。