☰
CCX 竞速模式(Racing)技术指南:跨渠道影子请求的延迟对冲机制全解
2026/10/4 1:54:15 网站建设 项目流程
  • API网关
  • LLM 网关
  • 后端

【免费下载链接】ccx

Claude / Codex / Gemini API Proxy - CCX

项目地址:https://gitcode.com/gh_mirrors/cc/ccx
点击查看免费下载

本文是 CCX(Claude / Codex / Gemini API Proxy)竞速模式(Racing)的深度技术解析。竞速模式在首字明显慢的渠道(如中转站排队)触发跨渠道影子请求(hedging):主渠道慢时并行派出影子请求,谁先交付有效响应,客户端就用谁的。读完本文,你将掌握竞速模式的两个配置开关、CostPreference 策略表、自适应阈值推导、影子候选五元组选取、claim-once 提交闸门与分支写出隔离的裁决原理,以及延迟负反馈学习的完整闭环。

一、机制概述:从"排队等待"到"谁快用谁"

在真实的多渠道网关场景中,渠道质量波动是常态:某个模型家族的首字延迟可能因为上游中转站排队而急剧恶化,而客户端无法感知、只能干等。CCX 竞速模式的解决思路来自对冲思想——参照兄弟项目 kiro.rs 的对冲竞速(hedging):主请求等待超过自适应阈值时,向候选渠道并行派发影子请求,谁先交付有效响应,客户端就透传谁的。

竞速模式复用 CCX preflight 缓冲的"未提交可弃"窗口作为裁决点:在响应真正写入客户端之前,各分支的响应都处于可丢弃状态,只有胜者完成提交。从源码结构看,其核心实现分布在:

模块职责
backend-go/internal/racing/阈值注册表、Gate 提交闸门、全局并发信号量、策略表、ErrRacingSuperseded
backend-go/internal/config/racing_config.go全局/渠道级开关 +ResolveRacingPolicy+SetRacingEnabled
backend-go/internal/handlers/common/racing.go编排器RunRacingAttempt(武装检查/定时器/影子派发/裁决归并/样本记录)
backend-go/internal/handlers/common/racing_writer.go分支 writer:pre-commit 私有缓冲、赢家 Commit 桥接真实 writer、败者 Discard
backend-go/internal/autopilot/racing_candidates.go五元组影子候选选取
backend-go/main.goRacingHub 注入 +/api/racing/config

二、用户可见面(极简设计):只有两个开关

竞速模式的核心理念是极简配置面:用户只控制开关,其余行为参数(影子数、触发下限、候选成本过滤)全部由策略自动推导,不暴露为配置项。这一点在 backend-go/internal/config/racing_config.go 的源码注释中明确写明。

2.1 全局开关racing.enabled

开关位置语义
racing.enabled(全局)智能路由面板「竞速模式」开关(随PUT /smart-routing/config整卡保存,字段名racingEnabled;另有独立端点PUT /api/racing/config供脚本直调)竞速总开关,默认关

全局开关的底层实现由GlobalRacingConfig.Enabled承载(nil 视为默认开启),管理接口通过SetRacingEnabled更新并持久化,同时fireConfigChangeCallbacks通知运行时刷新(backend-go/internal/config/racing_config.go)。

2.2 渠道级开关racing.enabled

开关位置语义
racing.enabled(渠道级)渠道编辑 → 自定义参数渠道参与开关:关闭 = 既不做主触发,其候选也不进影子池

渠道级配置由ChannelRacingConfig.Enabled承载(nil 时继承全局)。最终生效策略由ResolveRacingPolicy按渠道级字段 > 全局字段 > 默认开启的优先级解析(backend-go/internal/config/racing_config.go)。该解析在武装检查(shouldArmRacing)与影子候选构建(buildSelectionFromCandidate、调度器回退路径)三处被调用,保证关闭渠道既不会触发竞速、其候选也不会被选中。

2.3 武装条件(何时才会进入竞速流程)

从 backend-go/internal/handlers/common/racing.go 的shouldArmRacing可以确认竞速只武装四类对话协议(messages/chat/responses/gemini),并有三类请求明确不竞速:

  • 显式渠道 pin:请求头X-Channel非空时,渠道选择是用户意图,不做竞速;
  • 含图请求:影子需要整包重传图片,v1 不竞速;
  • 竞速未武装时:编排器RunRacingAttempt退化为直接调用渠道尝试闭包,路径零开销。

三、策略表:CostPreference 决定影子数与触发时机

竞速行为参数由请求生效的CostPreference推导,其解析优先级为:请求头X-Cost-Preference> 场景预设(X-Routing-Scenario)> 全局配置 Mode,未知值回退balanced(backend-go/internal/handlers/common/racing.go)。

策略表为代码内常量,定义于 backend-go/internal/racing/registry.go 的BehaviorForCostPreference:

CostPreference影子数流式触发 floor影子候选过滤
quality_first34s无限制
balanced(默认)18s无限制
cost_first18s仅综合倍率 ≤ 主候选一半(渠道 CostMultiplier × key GroupMultiplier);无合适候选不派

从源码注释可以看到 floor 档位的演进脉络:早期 2s/3s/5s 会对"慢而真"的渠道(如 ark kimi-k3 真实首字 2-4s)系统性误判慢,且家族分位数窗口混入快而差的中转假模型首字后 p90 被拉低、2s 档形同虚设,成为抢闸交付伪工具标记文本的根因之一;floor 上调后 clamp 兜底两类污染。行为推导的正确性由 backend-go/internal/racing/registry_test.go 的TestBehaviorForCostPreference固化(如quality_first → 3 影子/4s floor、cost_first → 1 影子/8s floor/cheap-only、空值与未知值按balanced)。

其余内部常量同样定义在 backend-go/internal/racing/registry.go:

常量值语义
分位数目标p90percentileTarget = 90
样本门槛20低于该数回退 floor(sampleFloor)
观测窗口15 分钟windowRetention,滚动淘汰过期样本
窗口容量512maxSamplesPerWindow,超限淘汰最旧
全局并发影子信号量12maxConcurrentShadows,防主渠道系统性事故时影子堆积放大
非流式 floor10sNonStreamFloorMs,全策略统一

3.1 隐藏推理阶段的保护下限

带隐藏推理阶段的流式请求(请求画像ReasoningNeed=true)使用更高的首内容保护下限,避免在模型正常思考期间过早误判"慢渠道":

  • 未声明 effort:24s;
  • low/minimal:16s;
  • medium:24s;
  • high/xhigh/max/ultra:30s;
  • none/off:保持普通请求 floor。

该逻辑实现于 backend-go/internal/racing/registry.go 的StreamFloorForReasoning,并且该下限仍受渠道StreamFirstContentTimeoutMsceiling 裁剪(if floorMs > ceilingMs { floorMs = ceilingMs },见编排器),避免把正常推理等待误判为慢渠道。测试 backend-go/internal/racing/registry_test.go 验证了各 effort 档位的下限值,以及"已有更高 floor 时不应降低"的语义(StreamFloorForReasoning(35_000, "low") = 35_000)。

四、触发阈值:p90 自适应 + floor/ceiling 双裁剪

竞速触发的核心公式:

threshold = p90(近 15 分钟同家族×阶段成功样本, n≥20) 否则 floor, 再 clamp[floor, ceiling]
  • 维度:模型家族(claude/gpt/gemini/other)× 阶段(流式首字 / 非流式完成时长),家族全局共用一个窗口。家族归类实现于FamilyForModel(backend-go/internal/racing/registry.go):claude/sonnet/opus/haiku/fable归 Claude 家族,gpt/o1/o3/o4/codex归 GPT 家族,gemini前缀归 Gemini 家族,其余(含 deepseek、kimi)归 other。同家族首字延迟特征接近,按家族分窗可加速样本积累;这也意味着慢性慢渠道持续超 p90 会被持续竞速。
  • ceiling:流式 = 渠道StreamFirstContentTimeoutMs解析值;非流式 = 渠道ResponseHeaderTimeout(同源超时,等过它请求已失败)。实现上流式 ceiling 由racingStreamCeilingMs取渠道与全局熔断配置中的首字超时(backend-go/internal/handlers/common/racing.go),非流式 ceiling 由racingNonStreamCeilingMs取渠道有效响应头超时。
  • 样本消赢家偏差:流式分支出过首字即记(含被取消的败者);非流式仅记赢家完成耗时。对应实现recordRacingStreamSample与recordRacingNonStreamSample(backend-go/internal/handlers/common/racing.go)。
  • 计时起点= 外壳attemptStartedAt(含渠道内 key 轮转等待,符合用户体感;与 kiro 的 attempt 级重置刻意不同)。

阈值注册表实现于 backend-go/internal/racing/registry.go 的Registry:每个(family, stage)键维护一个 15 分钟滚动环形样本窗口(sampleRing,按时间升序、超容量淘汰最旧、过期样本 prune 淘汰),ThresholdMs在样本不足时回退 floor,否则取排序后 p90,再 clamp 到[floorMs, ceilingMs]。测试 backend-go/internal/racing/registry_test.go 完整验证了四类边界:样本不足回退 floor、p90 低于 floor 向上 clamp、p90 超过 ceiling 向下 clamp、窗口过期后重新回退 floor,以及家族×阶段窗口隔离与容量封顶。

五、影子候选:五元组粒度选取

影子候选以五元组(channelUID + keyIdentity + actualModel)为粒度,选取分两条路径:

  1. 主源(SmartRouter 排名缓存):autopilot.RacingShadowCandidates从可行集(Selected=true)中选取,并排除与主尝试相同的五元组——同渠道不同 key / 不同执行模型可作影子。选取纪律在 backend-go/internal/autopilot/racing_candidates.go 中实现:

    • 主 key 一律不作影子:同一明文 key 跨渠道重复配置时,与主分支并发同账号请求是放大器(限速/空流);
    • 异渠道候选优先:同渠道行一律不作影子(同 provider 同队列的重复消耗,key 级对冲已由 attempt 内部轮转覆盖);
    • 多影子按 keyIdentity 去重:同 key 只取一行,保持排名原序。
  2. 回退:缓存为空或无可选时,SelectChannelWithOptions按FailedRoutes(已用路由)重选(纯跨渠道)。该回退同样应用渠道去重、竞速开关、限流热渠道过滤与成本过滤(backend-go/internal/handlers/common/racing.go)。

  3. 执行:候选反查渠道构造SelectionResult+ pin(WithExecutionPin经WithSelectionTrace透传),reason 记racing_shadow(backend-go/internal/handlers/common/racing.go)。

影子构建出口还有两道关键约束:

  • 工具调用白名单路由排他:带工具定义的请求,影子不从非白名单路由派(伪工具标记方言的根治约束);该协议无白名单时 fail-open,但"连续伪标记 miss 达阈值"的已知劣化组合仍不派(toolWhitelistAllows)。
  • 限流热渠道不派影子:渠道级冷却中或全部 key scope 被限速延迟时该候选被跳过——排名缓存路径绕过调度器冷却过滤,候选构造处兜底,防止 429 风暴中 shadow 放大真实上游消耗(IsChannelRateLimitHot检查)。

此外,影子使用自己的完整 ceiling 超时窗口(从影子发出起算),不共享主链剩余 deadline。

六、裁决与分支治理:claim-once 闸门 + 单写者分支隔离

6.1 提交闸门racing.Gate(claim-once)

裁决点是竞速正确性的核心。claim 点在 preflight 首字确认后 / 非流式完整响应校验后、写客户端之前;胜者 claim 即取消其余分支(ctx 级),败者 claim 失败以ErrRacingSuperseded收尾。

Gate实现于 backend-go/internal/racing/gate.go:claimed/winner用 atomic 存储保证并发安全查询,ClaimBy(ownerID)通过 CAS 实现首次调用返回 true 并取消其余分支的 claim-once 语义;CancelExcept(ownerID)则主动取消除赢家外的全部分支。测试 backend-go/internal/racing/registry_test.go 验证了串行 claim-once 与 32 个并发分支下唯一赢家的不变量。ErrRacingSuperseded在失败分类链中必须先于isClientSideError拦截:不计渠道失败、不触发熔断/Key 拉黑/自学习,仅完成渠道日志终态。

主分支返回后的编排器止血只取消非赢家分支(CancelExcept(ClaimedBy())):影子已 claim 时必须保留赢家透传(固定CancelExcept(0)会把赢家一并取消、客户端流被切断 context canceled——这是 248deb01 的回归修复);无 claim(主真实失败)时保留在飞影子,由结算路径兜底接管。

6.2 分支写出隔离racingBranchWriter

主/影子分支各挂独立分支 writer(backend-go/internal/handlers/common/racing_writer.go):

  • pre-commit 阶段:头/状态码/体写私有缓冲(Write返回len(p), nil,不干扰上游流读取);
  • claim 赢家Commit:一次性回放私有响应头与已缓冲状态/体到真实客户端 writer,之后全部操作转为透传;
  • 败者Discard:丢弃缓冲,后续写出操作静默无操作。

其不变量由构造保证:真实 writer 只被 claim 赢家触碰,分支间不共享响应头 map——取代早期"影子回填主 Writer + meta 锁串行化 echo 头"的约定式模型(后者是 2026-09-08 双 panic 事故的临时修复形态)。主分支同样经分支 writer 写出(编排器中c.Writer = newRacingBranchWriter(origWriter)),保证真实客户端 writer 全程只被赢家触碰。

6.3 败者治理:静默失败,不污染健康度

败者不计失败指标、不熔断、不拉黑、不标 URL 失败、不参与自学习(工具调用/严重度/上下文棘轮)。唯一例外是质量闸门让出——伪标记已在败者分支自身首包缓冲实测命中(已完成观察,非部分流推断),让出时单独计一次白名单负反馈(notePseudoToolCallYield,见 docs/specs/tool-call-capability.md)。影子真实上游错误(超时/500/拉黑)仍照常记账。

渠道日志终态:败者racing_lost+racingStatus=lost,赢家racingStatus=won。主/影子尝试共享请求 correlation ID,渠道日志按用户请求折叠为一行(「N 次尝试」徽章,见 docs/specs/web-ui-dialogs.md)。

6.4 防误判赢家

被取消的影子可能以空流 EOF → 内部轮转 →context.Canceled + Handled=true收尾,pickWinner判据为Handled && LastError == nil(backend-go/internal/handlers/common/racing.go);cancel 连带的空流响应直接按败出终止(racingSupersededOrCanceledEmptyStream判定)。

6.5 防放大机制汇总

  • 每请求最多maxShadows条(策略表);
  • 全局并发信号量 12(TryAcquire不阻塞主流程,超容量直接放弃派影子;backend-go/internal/racing/gate.go);
  • 影子分支内禁递归竞速(影子分支执行trySelectedChannel,不再套RunRacingAttempt);
  • X-Channel pin / 含图请求 / 非四对话协议不竞速;
  • 限流热渠道不派影子。

6.6 影子赢时的状态归并

影子赢时分支 gin keys 回拷主 context(copyBranchKeysBack,跳过ccx.racing.*内部 key,保证responseText/lastUserMessage等后处理读到赢家数据);primary selection 补记 trace 终态(notifyPrimarySuperseded,防悬空)。

七、后端结构总览

位置职责
backend-go/internal/racing/阈值注册表、Gate、信号量、策略表、ErrRacingSuperseded
backend-go/internal/config/racing_config.go全局/渠道级开关 +ResolveRacingPolicy+SetRacingEnabled
backend-go/internal/handlers/common/racing.go编排器RunRacingAttempt(武装检查/定时器/影子派发/裁决归并/样本记录)
backend-go/internal/handlers/common/multi_channel_failover.go外壳接线(TrySelectedChannelFunc增加 gin context 参数;AlsoFailedRoutes并入 failedRoutes)
backend-go/internal/handlers/common/upstream_failover.goecho 头块写出(分支 writer 隔离)、败者分类豁免(先于 isClientSideError)、成功路径 won 标记、日志角色
backend-go/internal/handlers/common/racing_writer.go分支 writer:pre-commit 私有缓冲、赢家 Commit 桥接真实 writer、败者 Discard
backend-go/internal/handlers/common/stream_processor.go + 4 协议 handler流式/非流式 claim 点
backend-go/internal/autopilot/racing_candidates.go五元组候选选取
backend-go/main.goRacingHub 注入 +/api/racing/config

八、延迟负反馈学习(竞速姊妹机制)

竞速样本不只驱动阈值,还回流为组合级学习,构成"竞速触发 → 慢组合标记 → 影子排除 + 软降权 → 快样本恢复"的完整闭环。

8.1 键粒度与存储

学习键为渠道 × keyHash × 实际出站模型 × 任务类(TaskClass 7 值,未分类落unknown全量桶)。存储在 ChannelCompatCache 第四分区latencyPenalties(.config/channel_compat.json,TTL 24h,GET/DELETE /api/compat-cache可查/可清?section=latency-penalty)。

8.2 三个慢信号

  1. 竞速 primary 被影子击败(upstream_failover 败者豁免点,非 shadow 分支才记);
  2. 竞速触发本身(派影子时,主组合记一次——recordPrimaryRacingTriggerEvidence,见编排器startShadow);
  3. 普通流式成功请求首字超同家族 p90 阈值(MaybeLearnLatencyDegradation)。

竞速败出/被取消分支不学习。

8.3 判定与恢复

连续慢证据streak ≥ 3判劣化(单次是抖动);任一快样本(首字 < 阈值一半)乐观翻转清零。观测日志为[Latency-Learn](首次达阈值)/[Latency-Recover](翻转)各一行。

8.4 消费:软降权,非硬排除

延迟差不是能力缺失,因此采用软降权而非硬排除:ScoringCandidate.LatencyDegraded→calcPenalty叠加 −15(介于 degraded −5 与 limited −20 之间);竞速影子候选排除学习过的慢组合(racingShadowExcludesLookup→learnedLatencyDegradedLookup,见 backend-go/internal/autopilot/racing_candidates.go,由 backend-go/internal/autopilot/latency_memory.go 的查询入口接入)。taskClass精确桶优先,unknown桶对任意任务类生效(全量记录)。排除逻辑的测试见 backend-go/internal/autopilot/racing_candidates_test.go。

九、与 kiro.rs 的刻意偏离

竞速机制参照 kiro.rs,但结合 CCX 的架构做了四处刻意偏离:

  1. 只做首字/完成阶段竞速,无响应头阶段——CCX preflight 闸门已覆盖主要收益;
  2. 影子用独立完整超时窗口——CCX preflight 超时是 per-attempt 口径,共享剩余窗口会让 5s 短超时渠道竞速失效;
  3. 候选是五元组绑定(跨渠道 + 同渠道换绑定),非 kiro 的同模型换凭据;
  4. 无 SQLite 分钟桶预热(重启后 20 样本内走 floor);样本记流式败者(消赢家偏差)。

十、快速上手:如何开启与验证

竞速模式开启后无需额外运维动作,行为参数自动推导,可通过以下途径验证其工作状态:

  1. 开启竞速:在智能路由面板打开「竞速模式」,或脚本直调PUT /api/racing/config(全局)与渠道编辑页自定义参数(渠道级);
  2. 观察阈值窗口:样本积累与触发行为由Registry维护,阈值随 p90 自适应浮动,可通过源码中SampleCount/ThresholdMs的测试用例理解边界行为(backend-go/internal/racing/registry_test.go);
  3. 观察日志终态:竞速发生时渠道日志按请求折叠为一行,带「N 次尝试」徽章,终态标记racingStatus=won/racingStatus=lost;影子派发时有[Racing] 首字等待超阈值(...ms),向候选 ... 派出影子分支 #N日志;
  4. 清理延迟负反馈:需要时通过DELETE /api/compat-cache?section=latency-penalty清除 24h TTL 的学习结论。

十一、适用前提与限制

  • 竞速以冗余流量换延迟,默认关闭,按需开启;cost_first通过仅派更便宜候选(综合倍率 ≤ 主候选一半)控制成本;
  • 竞速只对四类对话协议(messages/chat/responses/gemini)生效,显式渠道 pin、含图请求不竞速;
  • 重启后样本窗口为空,前 20 个样本内走静态 floor,需等待窗口积累后阈值才能自适应;
  • 延迟负反馈是软降权(−15),不参与硬排除,任务类精确桶优先于unknown全量桶。
  • API网关
  • LLM 网关
  • 后端

【免费下载链接】ccx

Claude / Codex / Gemini API Proxy - CCX

项目地址:https://gitcode.com/gh_mirrors/cc/ccx
点击查看免费下载
上一篇:MateCloud配置中心:动态配置更新与热加载机制详解
下一篇:构建完整数据血缘追踪系统:OpenLineage终极指南与最佳实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询