很多投放人员配置分流的时候,其实都有一个默认做法:只要请求里没有 Cookie,就先把这个访客送进标准页,等后面识别信号补齐了再切过去。这个做法放在低风险场景里,看着确实挺稳妥,但一旦放到会话播种链路里,就会出问题。说白了,问题在于系统在第一次响应的时候,可能把“默认版本”当成最终结果写进会话状态了。这样后面再想纠正就很难,版本选择被提前固化住。所以我的习惯是,从一次无Cookie请求进入系统开始,把版本判定、会话标识写入和第二跳确认之间的完整链路拆开看清楚。这样标准页才只是一个暂存动作,而不是终局判定。
一、无Cookie请求进入时先不急着写结论
一次请求到达的时候,Nginx 会把 PHP-FPM 的入口脚本拉起来,系统先读取 $_COOKIE 里的会话标识。这里容易搞混,很多人一看读不到会话标识,就直接按普通访客处理了。其实无Cookie只表示这个请求没有带历史上下文,不代表没有可用信号。URL 上的 gclid、fbclid、搜索词参数、落地路径、UA、语言头、IP 段,甚至页面内已经回传的指纹预采结果,这些全都可以成为本次判定的输入。我们先别急着下结论。
正确的链路动作其实是下面这几个:
- 读不到会话 Cookie 的时候,先生成一个
seed_id。随机源要用random_bytes(16)这个级别,避免用自增序号,否则容易被枚举出来。 - 把本次上下文写进短期 KV,例如 Redis 里的
seed:<seed_id>,TTL 设成 30-90 秒。 - 响应阶段写
Set-Cookie: ssid=<seed_id>; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400。 - 本阶段不要写
v之类的版本字段,只写stage=seeding。
这样做的设计意图说白了就是:第一次响应只负责“播种”,不负责“定版”。如果标准页是本次唯一能返回的页面,那它也应该带着“待确认”的状态出去,而不是最终结论。这个处理方式和无指纹首次访问的默认选择里提到的问题是一致的:冷启动阶段的默认值只是过渡,不应该变成画像结论。
二、候选版本生成与仲裁:短时信号和会话结论分开
候选生成层会并行匹配多个条件,然后输出若干可选版本以及命中原因。举个例子,一个无Cookie请求可能同时命中“移动端 UA”“来源为搜索”“地区某市”这些条件,其中一部分条件指向标准页,一部分条件指向完整页。这种时候就不能简单地取第一条命中结果了,而要进入仲裁层。这里的关键是,短时信号和会话结论不能混在一起,候选生成层只负责给出可能性,仲裁层才负责下动作。
仲裁层会读取规则条件的短路顺序,判断哪些是过滤型条件,哪些是决策型条件。对于无Cookie请求,仲裁结果最好采用“动作 + 状态”这种双字段表示:action=render_standard、strategy=hold_for_confirm。页面版本服务读到这个结果以后,就明白本次返回标准页只是一个暂存动作;如果第二跳在窗口期内补充了足够信号,就可以重新仲裁。
这里的关键差异在于:默认版本不是访客分层结果,只是本次响应结果。否则一旦 Cookie 里记录下版本,后续规则里 Cookie 条件往往优先级靠前,很容易把首次判断固化成整段会话的结论。这也正是先定版本映射再选条件分支里强调的:先想清楚条件分支的终局与过渡状态,再写规则。
三、Set-Cookie写入时机:会话标识和版本字段必须拆开
会话播种链路里最容易踩的配置,就是把 ssid 和 v 同时写进 Cookie。比如首次无Cookie访问的时候,响应里同时带 Set-Cookie: v=standard; ssid=abc...。第二跳再进来以后,系统读到 v=standard,就不会再去重新仲裁,页面版本就被固定住了。这个问题很隐蔽,因为表面上看起来首跳已经给了版本,实际上却是把终局提前了。
我推荐的写法是这样:
ssid:会话标识,随机字符串。stage:seed、confirming、confirmed三种状态,带 HMAC 签名。v:版本字段,只在 stage 变成confirmed之后才写入,值是一个签名字符串。
签名能防止注入伪造,校验失败就重建会话。SameSite=Lax 能保证跨站顶跳的时候基本能携带;如果跨子域,那就需要显式声明 Domain,并且确保两处域名一致。Secure 在 HTTPS 下应该开启。另外,写 Cookie 和指纹采集不能串行等待。同步等指纹会让首跳延迟多出几百毫秒,很多投放人员为了“更准”牺牲了加载体验。其实更稳妥的做法是异步确认:响应先返回,指纹脚本完成后再通过第二跳或像素回传补充信号。具体取舍可以参考指纹采集空窗期的响应取舍。
四、第二跳确认:回填暂存上下文后重新判定
第二跳请求进入系统的时候,入口如果读到 ssid 并且 stage=seed,链路是下面这样的:
- 用
ssid去 KV 取暂存上下文。 - 如果取到且未过期,把暂存上下文和本次实时上下文合并。
- 重新进入候选生成与仲裁。这个时候指纹、页面停留、来源参数等新增信号会让置信度更高。
- 仲裁输出最终动作后,系统才把
v=<signed_variant>写入 Cookie,并把stage推进为confirmed。 - 删除暂存 KV,或者保留短 TTL 用来写日志。
如果 KV 因为 Redis 重启丢了,那么读到了 ssid 但是拿不到 stage 的时候,不要直接进永久默认,而是重新播种一次。这样能避免因为基础设施抖动造成会话版本错乱。
这里的 TTL 需要和业务节奏匹配。太短,访客不过几秒再点第二次就会失联;太长,暂存上下文占用内存,误用风险也会增加。记忆时长与误判率的关系,在风险标记的记忆时长里有更详细展开,可以直接迁移到播种窗口的设计上。
五、匿名案例复盘:标准页提前写入带来的连续性问题
我见过一个团队的情况。他们做教育行业投放,日均点击大概一千二三,客单价较高。最初配置里把“无Cookie访问”直接映射为标准页,并且写入了 v=standard。结果不少从高转化词进来的无Cookie访客,第一跳看了标准页,第二跳即使系统已经识别为高价值访客,页面还是不会切换。排查以后发现,规则里 Cookie 版本条件优先于来源参数条件,v=standard 一旦存在,后续所有请求都会被强制压到标准页。
他们的调整方式是把 v 从首跳响应里移除,改成写 ssid 和 stage=seed,并且把第一跳的 gclid、搜索词、UA 暂存进 Redis。第二跳合并上下文后重新仲裁,高转化词访客就可以在第二次点击时进入完整页。调整后表单流失明显下降,但也没有出现首跳标准页比例被完全抹掉的情况。这个案例说明,投放人员需要理解的不只是“默认版本配置”,还包括默认版本是否被提前持久化。
小结
说白了,无Cookie首次访问不是一次分类,而是一次三态链路:播种、暂存、确认。会话标识和版本字段分开写,标准页作为暂存动作,第二跳回填重判,才能避免首次信号不足导致整会话误判。对投放人员来说,调优的重点不是找更多规则,而是为不充分的输入保留可修正空间。
常见问题
无Cookie首次访问应该返回标准页还是完整页?
取决于信号充分度。如果只有无Cookie但来源参数、UA、IP等足够明确,可以直接返回完整页;如果信号不足,就返回标准页作为暂存动作,并写入播种态,而不是把默认版本固化到会话里。
会话播种会拖慢首次响应速度吗?
一般不会。通常只增加一次随机标识生成和一次KV写入,延迟很小。真正拖慢的是同步等待指纹采集或远程规则查询,这些操作应拆成异步确认,避免加长首跳。
第二跳为什么不继续继承第一跳版本?
因为第一跳可能因信号不足选择了标准页,直接继承会让有潜力的访客无法进入完整页。第二跳通过合并暂存上下文重新判定,能利用更多信号纠正第一跳的过渡结论。
Cookie里的版本字段需要签名吗?
建议签名。未签名版本字段可能被修改,导致后续页面版本被篡改或规则失效。签名值可用HMAC生成,每次读取时校验,失败就重建会话。
多台服务器部署会丢失播种状态吗?
如果暂存上下文只放本机内存,会丢。应放入共享KV或数据库,并把TTL控制在30-90秒,确保第二跳请求在窗口内能取到,又不会长期占用存储。
延伸阅读
理解会话播种链路后,再看分流系统的完整实现时,很多配置和时序设计会更容易对应起来。
分流系统完整实现链路解析