简介:一份关于阿里妈妈开放实时竞价广告(RTB)的行业分析文档,面向互联网广告从业者、站长及对程序化广告感兴趣的初学者,系统梳理RTB模式的运作机制与国内发展现状。文档以docx格式呈现,共1个文件,压缩包大小约120KB,内容精炼,便于快速阅读。目前已有210人学习浏览。文档重点介绍阿里妈妈转型为媒体流量平台后开放的Tanx SSP橱窗推广,解析实时竞价CPM与原有CPS计费模式的差异,说明广告主按PV出价、价高者得展示的投放逻辑;同时汇总淘宝、腾讯、新浪等巨头的Ad Exchange布局,并讨论Cookie隐私泄露与消费者权益保护法草案对RTB产业链的潜在影响。读者可获得对RTB生态的系统认知,理解RTB广告交易链条、竞价机制及合规风险,适合用作行业入门参考或简报素材。
1. 实时竞价广告对中小广告主意味着什么:从黑盒采买到可控流量
RTB 实时竞价广告有一个反直觉的起点:平台排序时看的不是你出多少钱,而是「出价 × 预估点击率」算出来的 eCPM。出价只是上限,点击率预估才是拿量的入场券。阿里妈妈开放实时竞价广告之后,广告主第一次能按「一次曝光」为单位直接参与流量竞价,不用再像传统媒介采买那样整包买下某个广告位,也不用忍受品牌广告位的黑匣子式排期。代价是,你的服务器要在 50 毫秒内完成一次竞价决策:解析请求、算价格、回响应。这篇笔记适合正在接入 ADX、自建出价服务或已经在跑程序化投放的从业者。我会把竞价机制、Bidder 实现、预算调控和排障链路一条条讲透,每一步都按能直接落地的标准来写。
2. RTB 竞价机制与出价逻辑:BCPM、二价计费与 50ms 决策链
RTB 的链路不长,但每一环都藏着计价规则。理解这套规则之前,先得知道一次请求从产生到被响应之间经过什么。用户打开 App 或网页,媒体侧的广告 SDK 会拼装一条曝光请求,发给广告交易平台;平台根据流量标签和底价,把请求广播给接入的 DSP / Bidder;Bidder 在超时时间内回一个出价;平台选出 eCPM 最高的出价方,把广告素材下发到用户端。全过程发生在用户看到广告之前的几十毫秒内,这就是「实时」二字的含义。如果 Bidder 没超时返回,平台不会等你,直接弃标并通知下一位。
2.1 一条 RTB 请求里到底有什么:设备、上下文与出价指针
接入方拿到手的请求,一般是 JSON 格式的 POST 数据。不同平台字段命名有差异,但结构上大同小异。我习惯把它拆成三块:设备信息(设备 ID 类型、操作系统、网络环境、屏幕尺寸),上下文信息(广告位宽高、类目、页面 URL 或 App 包名、用户分段标签),竞价信息(底价、是否支持视频、限价策略)。下面是一个典型的最小请求结构:
{ "id": "8b1c3f2a-...", "imp": [{ "id": "imp_001", "banner": { "w": 640, "h": 100 }, "bidfloor": 0.3, "category": "news" }], "device": { "ua": "Mozilla/5.0 ...", "ip": "110.42.x.x", "devicetype": 4, "os": "android", "osv": "12", "w": 1080, "h": 1920 }, "site": { "name": "xx_info", "page": "https://example.com/news?id=123", "publisher": "pub_1024" }, "bcat": ["403", "407"], "alimama_extra": { "traffic_level": 2, "freq": 5 } }拿到请求后,第一件事不是出价,而是快速做三层过滤:是否在屏蔽类目 bcat 里、流量标记是否在黑名单、请求 ID 是否重复。多余的处理全部省掉,因为超时窗口从平台发出请求那一刻就开始计时。参数里最关键的是 bidfloor(底价)和 traffic_level(流量分层)。bidfloor 低于 0.3 意味着这个曝光不花流量也未必亏,但要结合自己的转化数据再定;traffic_level 是平台对流量质量的粗分,常见是 1~5,等级越高越好,低等级流量要么拉低点击率,要么浪费模型学习机会。
2.2 二价计费为什么是行业默认:GSP 机制下的出价策略
行业主流的计价方式是二价计费(也叫 GSP,Generalized Second Price)。它解决了广告主的一个核心顾虑:如果按一价计费,你每次都得猜别人出多少,猜高了就冤。二价计费下,你赢了竞价,只需要付第二名的出价加一个最小步长。比如说你出价 5 元,第二名出 2.4 元,你赢下这次曝光,实际扣费是 2.4 元多一点,不是 5 元。如果你的流量价格整体偏低,出价保守也不会严重吃亏。
但二价不代表出价可以随意。排序管的是 eCPM,不是出价本身。平台把每个竞价的出价乘以预估点击率 pCTR 得到一个 eCPM,按 eCPM 排名。假设你出价 10 元、预估点击率 2%,eCPM 就是 0.2;对手出价 3 元、预估点击率 8%,他的 eCPM 是 0.24,赢得竞价的是对手。这就是为什么「出价高不一定拿量」——平台认为你的广告没点击潜力,给你流量是浪费。所以做 RTB 不能只在出价上做文章,素材质量、设备定向、目标用户的预筛选,每一步都在影响 pCTR。
提示:接入时先确认平台支持的是 CPC 竞价还是 CPM 竞价。阿里妈妈开放实时竞价广告的主流形态是 CPM 曝光竞价,点击关注用于模型优化与素材效果判断,不是计费口径。别把 CPC 和 CPM 混在一个出价逻辑里,否则成本核算必乱。
2.3 阿里妈妈开放 RTB 的流量标记与多层定向
开放 RTB 的流量池,不等于对外开放全部流量。平台一般会按流量质量、类目敏感度、媒体归属做分层,接入方拿到的流量是「授权开放」的那一部分,而且通常附带流量标记。traffic_level 是一个维度,广告位类型是另一个维度。新闻信息流、短视频信息流、激励视频、开屏,不同位置的转化率差出一个量级,出价不能一刀切。
对接这类平台时,我会先按流量标记做映射:把对方的 traffic_level 换算成自己内部的流量分桶(比如 1~2 直接砍掉不出价,3 试探,4~5 主力追量)。也可以利用请求里的类目信息做内容投放。比如不投竞品类目、不投低质内容页。很多人一上来就想要「全量流量」,实际血泪经验是:先拿 20% 的优质流量跑通模型,比全量铺开省钱得多。流量标记的意义就是让你实现这个「先窄后宽」的策略。
3. 跑通第一次实时竞价:Bidder 骨架、回传口径与压测参数
接入 RTB,最核心的是把 Bidder 服务搭起来。这个服务的技术栈没有太多讲究,常见做法是 Java、Go 或者 Python 开一个 HTTP 服务,接收 POST 请求,解析、出价、原路返回。比起技术栈,更重要的是响应速度和出价逻辑的清晰程度。下面我按一个最简可用的 Python 骨架来讲,重点在逻辑,不在框架。如果你生产环境用 Java/Go,把下面这段翻译过去也一样。
3.1 最小可用的 Bidder:解析、出价、回响应
from flask import Flask, request, jsonify import json app = Flask(__name__) BLACK_CATEGORY = {"403", "407"} MAX_CPM = 5.0 # 最高出价,按 CPM 计 TRAFFIC_BUCKET_MIN = 3 # 低于 3 级流量不出价 def parse_request(payload): imp = payload.get("imp", [])[0] device = payload.get("device", {}) site = payload.get("site", {}) extra = payload.get("alimama_extra", {}) return { "req_id": payload.get("id"), "bidfloor": float(imp.get("bidfloor", 0)), "traffic_level": int(extra.get("traffic_level", 5)), "category": imp.get("category", ""), "os": device.get("os", ""), "bcat": set(payload.get("bcat", [])), "publisher": site.get("publisher", "") } def decide_bid(ctx, pctr=0.02): if ctx["traffic_level"] < TRAFFIC_BUCKET_MIN: return 0 if ctx["bcat"] & BLACK_CATEGORY: return 0 if ctx["bidfloor"] > MAX_CPM: return 0 # 简单出价:pCTR 越高,出价越高,但不能超过保底限制 bid = max(ctx["bidfloor"], 0.01) + pctr * 10.0 return min(round(bid, 2), MAX_CPM) @app.route("/bid", methods=["POST"]) def bid(): payload = request.get_json(force=True) ctx = parse_request(payload) price = decide_bid(ctx) if price <= 0: return jsonify({"code": 1, "nbr": 2}) # nbr=2 表示不竞价 return jsonify({ "code": 0, "id": ctx["req_id"], "seat": [{ "price": price, "adm": "<html>...</html>" }] })这段代码有三个关键设计。第一,decide_bid 是一个独立函数,方便后面替换成 PID 调价或者机器学习模型,业务逻辑和协议解析解耦。第二,不满足条件时返回 nbr=2,平台就会把你从这次竞价中去掉,不会影响后续请求。第三,price 的上下限都做了钳制——低于底价不参与,高于最高出价砍掉。理由很实在:初期接流量,你不知道一次真实曝光能带来多少收入,宁可错过,不要把 CPM 成本拉爆。
生产环境还要注意一个事:出价和响应里的 price 单位要跟平台对齐。有的用 CPM,有的用千次曝光预算,有的用单次点击预算换算,单位错了会把成本放大一千倍。对接当天第一件事就是用测试请求跑通一版最简单的 mapping,把平台的请求 ID 和响应 ID 打印到日志里,确认能收到 win 通知再说调价。
3.2 点击回调与转化回传:归因口径决定模型质量
赢下竞价只是开始,整个 RTB 能否盈利,靠的是回传数据进到模型里形成正向循环。平台通常会提供点击回调(也叫 click callback)和转化回传接口。用户在页面看到广告后点了,媒体端会向你的服务器发一条点击通知;用户在你落地页完成了注册、下单等行为,你的业务系统再通过 s2s 接口把转化事件回传给平台。这个链路不通,平台就不知道广告真实效果,你的 pCTR 会被低估,出价也会跟着变形。
@app.route("/callback/click", methods=["GET", "POST"]) def click_callback(): # 平台回传的点击参数:通常包含竞价请求 ID、创意 ID、时间戳 click_id = request.args.get("click_id") or request.form.get("click_id") req_id = request.args.get("req_id") or request.form.get("req_id") log(click_id, req_id, "click", int(time.time())) return "ok" @app.route("/callback/conv", methods=["POST"]) def conv_callback(): # 业务端上报转化:订单号、click_id、转化值 data = request.get_json() conv_log = { "click_id": data["click_id"], "order_id": data["order_id"], "conv_value": float(data.get("conv_value", 0)), "ts": int(time.time()) } write_to_queue(conv_log) # 先写队列,异步落库 return jsonify({"code": 0})转化回传的一个常见坑是「晚回传」。用户从点击到付费可能间隔很长,有的短视频链路用户先收藏,三天后才下单。平台归因窗口一般设置 1~7 天,如果回传延迟超过窗口,平台就不认了。常见做法是:点击当天做点击日志快照,转化发生在 3 天以后,直接按点击日志的 req_id 匹配回去,手动补报一次。另外,回传数值字段 conv_value 尽量是订单金额或毛利,不要只回传一个「转化了」的布尔值——出价优化需要知道「这次转化的体量有多大」,否则高价值订单和低价订单在模型里变成同一个学习样本。
3.3 压测与超时设置:QPS、兜底价与丢标日志
Bidder 上线前必须压测。平台不会因为你的服务慢而照顾你,超时即当弃标。通常 50ms 内必须完成响应。压测要覆盖两个维度:单请求耗时和各环节的丢标率。常见做法是先按你预估的日均曝光量反推 QPS,比如说日预算 3 万元、平均成交 CPM 30 元、一天覆盖 24 小时,每秒约 11 个请求——实际因为流量峰谷,要按 3~5 倍峰值来压。压测参数我建议按下面这张表来做:
| 压测项 | 建议值 | 说明 |
|---|---|---|
| 单请求 P99 耗时 | 小于 45ms | 留 5ms 给平台和网络抖动 |
| 单机 QPS | 目标峰值的 3~5 倍 | 预留下半年流量涨的余量 |
| 超时时间 | 平台要求 50ms,本地设置 30ms | 给自己留出宽限 |
| 点击回调缓冲区 | 1 万个以上 | 防止回传高峰打满内存 |
| 丢标率 | 低于 10% | 丢标率过高,算法样本量不够 |
压测时最容易忽略的是丢标日志。很多团队只看中标率,不看为什么丢了。日志里至少记录三件事:请求 ID、出价结果、平台回来通知时带的丢标原因码(nbr)。nbr 大概率是 1(超时)、2(未竞价)、3(出价低于底价)这几种。如果丢标原因集中在超时,说明服务性能不够,先不调出价策略,先扩机器或做本地缓存优化;如果集中在未竞价,那是策略太保守,流量便宜的时候该出价没出价。没有丢标日志,排障就全靠猜。
4. 把 RTB 从「参与」调到「盈利」:预算平滑、PID 调价与冷启动
技术链路通了之后,接下来就是真金白银的博弈。很多团队跑到这一步就陷入一个魔咒:每天烧的钱不少,ROI 始终在盈亏线附近挣扎。问题多数不在模型,而在预算节奏和调价反馈环没建立起来。我在这个阶段会先做三件事:限制消耗速度、上 PID 调价、设置冷启动保护。三件事顺序不能反,先控住预算上限,再谈追求效率。
4.1 日预算与平滑消耗:分时预算和调速档位
开放 RTB 平台一般会给广告主两类预算控制:日预算和单次曝光出价上限。但日预算不是均匀消耗的,新接入的流量往往集中在晚间高峰,如果只设日预算,可能出现晚上 8 点开始冲量、10 点烧完的现象。预算提前烧完意味着最后一个小时的优质流量全部错过,第二天再起量还得重新跑模型学习。常规做法是拆分时段预算表,先按历史转化率把 24 小时切成高峰、平峰、低谷三档,每一档设置独立预算系数:
# 时段预算系数:系数 × 日预算 = 该时段可用预算 hourly_budget_coeff = { 0: 0.02, 1: 0.01, 2: 0.01, 3: 0.01, 4: 0.02, 5: 0.02, 6: 0.04, 7: 0.05, 8: 0.06, 9: 0.08, 10: 0.09, 11: 0.08, 12: 0.09, 13: 0.08, 14: 0.07, 15: 0.07, 16: 0.06, 17: 0.07, 18: 0.08, 19: 0.10, 20: 0.12, 21: 0.10, 22: 0.06, 23: 0.03 }系数加总约为 1.0。实际跑的时候,每小时允许消耗的预算等于日预算乘以该小时系数,再按 15 分钟粒度做一次检查。如果发现某小时已经用完,Bidder 直接降级出价,只接 traffic_level 最优的流量,避免预算耗尽。平台侧一般也有「平滑投放」或「加速投放」的选项,平滑投放会在全天匀速分配预算,适合稳定期;加速投放适合冷启动期要快速积累样本的场景。别一上来就开加速,成本容易失控。
4.2 用 PID 做自动调价:把 ROI 目标换算成出价增量
出价不是一拍脑袋就能定的。人工盯价只能做日维度修正,但 RTB 的流量价格是分钟级波动的。同样一个广告位,早上 8 点竞争激烈,中午 12 点可能降价一半。所以我一般会在第一版手动出价跑通后,第二周就切换成 PID 自动调价。PID 的逻辑不复杂:设定一个目标成本,比如转化成本目标 80 元,系统按实际成本与目标成本的差值,实时增减出价增量。
# PID 调价:输入当前实际成本、目标成本,输出出价增量 class PidBidAdjuster: def __init__(self, kp, ki, kd, integral_max=5.0): self.kp = kp self.ki = ki self.kd = kd self.integral = 0.0 self.last_error = 0.0 self.integral_max = integral_max def update(self, actual_cost, target_cost, dt=1.0): error = target_cost - actual_cost # 实际成本高,error 为负,降低出价 self.integral += error * dt self.integral = max(-self.integral_max, min(self.integral_max, self.integral)) derivative = (error - self.last_error) / dt if dt > 0 else 0.0 delta = self.kp * error + self.ki * self.integral + self.kd * derivative self.last_error = error return delta参数里 kp、ki、kd 的取值在不同流量环境下差异很大。我一般先用 kp=0.2、ki=0.05、kd=0.5 起步,运行一小时观察出价增量的波动范围:如果增量一直在上限附近震荡,说明 kp 偏大;如果增量长期贴着 0 走,说明目标成本设得太保守,流量都拒了。这个调价器输出的不是最终出价,而是底价之上的增量,最终出价要跟流量分层联合起来用。高等级流量可以把增量拉满,低等级流量增量打五折甚至归零。同时给 PID 设一个上下限,比如增量区间在 -5 到 +3 元之间,防止单次波动直接打穿预算。
提示:PID 适合目标成本变化不频繁的业务。如果你的业务本身波动大,比如大促前后转化率能翻 3 倍,先手动把目标成本改成预期值,再让 PID 在预期值附近微调,不要指望 PID 能自动跟上趋势突变。
4.3 冷启动阶段的投放策略:小池子、宽定向、频控兜底
冷启动阶段模型没样本,pCTR 是瞎猜的,这时候最忌讳的是「激进拉量」。平台算法没有你的转化数据,它会按行业平均值给你预估,你的出价和对手的竞争就变成无信息博弈。我现在的习惯是,任何新账号、新素材、新人群包,都先跑三天「小池子」:预算压到正常计划的三分之一,定向放宽到平台建议的目标人群,出价按建议出价的 60% 到 80%,同时开启频控(单个用户每天最多看到 3 次广告)。
频控是冷启动最容易漏的设置。不设频控,媒体端会疯狂给同一批高活跃用户塞广告,因为平台知道你愿意出价,结果就是点击成本很好看、转化成本拉高、真实用户覆盖量极低。我现在跑 RTB 有一个铁律:所有新计划必须同时交给平台频控接口和本地频控双重控制。本地频控怎么做?在点击回调落库的时候维护一个 device_id 到曝光次数的映射,超过阈值就拒绝后续出价。前期宁可错过流量,也要保住新计划前几天的样本质量。
5. RTB 投放避坑指南:五个实战翻车场景与排查思路
RTB 接入后前四周,是踩坑的高发期。以下五个场景我基本每个项目都遇到过,有的坑在数据口径上,有的坑在平台逻辑理解上。每个都按「现象 → 原因 → 解决」写清楚,方便你真遇到了照着排。
5.1 现象:出价不低却拿不到量
出价已经高于平台建议价 20%,但 Bidder 的 win 通知寥寥无几。查看日志发现丢标原因集中在 nbr=3(低于底价)或者干脆请求量本身就少。原因通常不是出价低,而是预估点击率 pCTR 被模型打得很低。平台按 eCPM 排序时,出价乘以 pCTR 之后,你的竞争力远低于对手。解决思路分两步:先看素材点击率是否确实低于行业均值,如果是,换素材或者换投放形式;如果素材没问题,再查自己的回传链路——转化回传延迟或未回传,会直接压制模型的 pCTR 预估。
5.2 现象:ROI 持续为负但业务增长正常
收入对得上,订单真实增长,但是 ROAS 算出来不到 0.8。这时候先别急着调整出价,先把归因口径对一遍。最常见的原因是点击时间和转化时间不在一个统计窗口:你把 7 天归因窗口的订单全部记在这个月,但点击发生在窗口开启之前;或者业务系统把「重新激活老用户」的订单也算作新客转化。解决方法是统一归因口径:以一个点击 ID 为唯一主键,只统计该点击发生后归因窗口内的第一次转化,且在报表里单列「新客转化」和「老客转化」两列。口径理顺之前,所有调价都是瞎调。
5.3 现象:转化回传延迟导致归因漂移
平台侧显示的转化数比业务侧少很多,差出来的订单都是 3~5 天前的点击转化今天才发生。原因很直白:归因窗口关闭前数据没送到。平台一般要求转化回传在点击后 24 小时内完成,超过时间窗口,平台就记「无转化」。解决方法是做回传补偿机制:业务侧订单落库后,如果发现 click_id 对应的点击时间距离当前已经超过 72 小时,走补报通道,同时修正这条流量的真实转化价值。补报要控制频率,每天一次批量补报是合理的,别做实时补报,防止数据抖动。
5.4 现象:预算半小时烧完,后面全是不转化流量
日预算设了 2 万元,上午 10 点跑完,剩下时间全是空耗。查日志发现消耗集中在某个定向人群或某个广告位上,且这些流量转化率低于整体平均。原因是冷启动期模型未收敛,加上平台算法的「模拟学习」会把预算优先分配给点击率高的流量,但这些流量未必转化。解决手段有三个:第一,设置单广告位预算占比上限,比如不超过整体预算的 30%;第二,开启频控,限制同一用户重复曝光;第三,把冷启动期拉长到 5~7 天,前三天不追求零损耗,只追求样本覆盖。预算烧完这个问题,多数情况不是平台的错,是你的出价策略缺少「刹车装置」。
5.5 现象:反作弊拦截误伤导致定向人群完全失效
后台看定向设置正常,但实际拿到的曝光里目标人群占比很低。检查数据发现大量请求的 device_id 相同或 UA 异常,比如 Windows 设备出现在手机 App 的流量里,或者同一 device_id 一秒钟请求十次。平台反作弊拦截先把这种流量拦掉,你的计划自然失效。解决套路是:先在本地上线流量质量过滤器,对高并发设备 ID、异常 UA、可疑 IP 段直接不出价;再为优质人群单独建一个高预算计划,防止被低质流量稀释。记住,反作弊拦截不针对你的账号,但误伤了以后平台不会主动告诉你,必须自己在日志里盯。
6. 验证 RTB 链路的一件小事:用离线日志复盘每一次竞价
当出价、回传、预算都稳定跑起来之后,我发现最有价值的一件事不是调参,而是每周固定做一次「竞价日志复盘」。具体做法是把每一次竞价请求的出价、eCPM、pCTR 预估、是否赢得、实际扣费、点击是否发生、后续是否转化,全部落一份离线日志,然后用一次 SQL 就能看清全链路。
SELECT traffic_level, COUNT(*) AS auction_cnt, SUM(CASE WHEN win=TRUE THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS win_rate, AVG(ecpm) AS avg_ecpm, AVG(win_price) AS avg_cost_price, SUM(conv_value) / NULLIF(SUM(bid_price), 0) AS roas_sample FROM rtb_bid_log WHERE dt = CURRENT_DATE - 1 GROUP BY traffic_level ORDER BY traffic_level;这张表跑出来能回答三个问题:哪个流量等级的赢标率偏高(可能出价太激进),哪个等级拿了量但 ROAS 样本很低(该降级或暂停),哪个等级请求量少但转化率高(该加预算)。我习惯把这个结果和调价记录放在一起看,能明显看到每一次 PID 参数调整对 ROI 的实际影响。有一回我把点击归因窗口从 1 天改成 7 天,ROI 立刻变好看,但后来发现那只是窗口变长的数学幻觉,真实转化并没有变多——从那以后我复盘只信同一归因口径下的环比数据,不追单周绝对值。这个习惯帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取