OmniRoute Context Relay 上下文中继策略:跨账户轮换时的会话连续性保障
2026/9/14 10:04:20 网站建设 项目流程

OmniRoute Context Relay 上下文中继策略:跨账户轮换时的会话连续性保障

【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute

context-relay是 OmniRoute 提供的一种 Combo(组合路由)策略,用于解决多账户轮换场景下的会话断裂问题:当活跃账户因配额耗尽而在对话结束前被切换时,OmniRoute 会预先生成一份紧凑的结构化摘要,并在确认账户真正切换后,将摘要作为系统消息注入下一次请求,让新账户无缝承接未完成的任务。本文基于 docs/i18n/ru/docs/features/context-relay.md 展开,并结合 open-sse/services/contextHandoff.ts、src/sse/handlers/chat.ts 等源码,讲清其运行时流程、交接载荷结构、配置字段与底层实现原理。

核心概念:在优先级路由之上叠加一层"交接"

从模型选择的角度看,context-relay的行为与普通优先级路由(priority routing)一致:先按路由策略挑选目标账户。区别在于,它在此基础上增加了一个交接层(handoff layer),其完整生命周期包含三步:

  1. 生成:在活跃账户配额耗尽之前,OmniRoute 在后台生成一份紧凑的结构化会话摘要;
  2. 注入:当身份认证为同一会话选择了不同账户后,OmniRoute 将该摘要作为系统消息(system message)注入到下一次请求中;
  3. 消费:交接被成功消费后,立即从存储中移除,避免过期内容被重复注入。

这三步分别对应仓库中的三处实现:生成逻辑位于 open-sse/services/contextHandoff.ts(maybeGenerateHandoff),注入逻辑位于 src/sse/handlers/chat.ts(getHandoff+injectHandoffIntoBody),持久化与删除位于 src/lib/db/contextHandoffs.ts。

适用场景

原文档明确指出,只有以下条件全部满足时才建议启用context-relay

  • Combo 预期会在同一服务商的多个账户之间轮换;
  • 丢失短期会话连续性会损害任务质量
  • 服务商暴露了足够的配额信息,能够预测即将到达的账户上限。

最典型的用例是超长编程或研究会话:一次任务可能跨越单个账户的额度窗口,需要在账户间接力完成。反之,如果任务很短、单个账户完全够用,或者服务商不提供配额探针,则这个策略带来的收益有限。

运行时流程:按配额水位分阶段工作

context-relay的行为被刻意拆分为两个运行时层级:Combo 循环负责"何时生成摘要",认证与注入层负责"何时注入摘要"。整个流程按配额使用率分为四个阶段:

配额用量 0%~84%:不生成交接

此时请求行为与普通优先级路由完全一致,不产生任何摘要请求,无额外开销。

配额用量 85%~94%:后台预生成交接

当配额使用率进入预警区间,且活跃服务商在handoffProviders白名单中已启用时,OmniRoute 会在账户完全耗尽前在后台生成一份结构化的交接摘要。几个关键细节:

  • 默认警告阈值(warning threshold)为0.85,对应源码中的HANDOFF_WARNING_THRESHOLD(见 open-sse/services/contextHandoff.ts);
  • 生成摘要的硬停止线0.95,对应HANDOFF_EXHAUSTION_THRESHOLD(同文件 L13),超过该水位不再调度新的摘要请求;
  • 每个sessionId + comboName只允许一个进行中的交接生成:源码通过模块级Set<string>inflightHandoffGenerations)记录在途任务,重复触发会被直接忽略(见 maybeGenerateHandoff);
  • 如果该会话/Combo 已存在活跃交接(hasActiveHandoff命中),也不会生成重复摘要。

摘要生成采用setImmediate异步调度,不阻塞主请求路径;调用前还会执行cleanupExpiredHandoffs()清理过期记录。

配额用量 95% 及以上:不再生成

此时系统已处于或接近耗尽状态,运行时会避免再调度一个摘要请求——因为为生成摘要而消耗的配额可能进一步加剧耗尽。

账户轮换后:注入交接

当同一会话的下一次请求经过身份认证后,解析到的实际账户与生成交接时的账户不同时,OmniRoute 将存储的交接作为系统消息前置注入到请求体中。注入只在实际账户切换被确认后发生——这是本策略正确性的关键前提(详见下文架构说明)。

交接载荷与消息格式

持久化结构:context_handoffs 表

持久化的交接载荷存储在context_handoffs表中,字段与 src/lib/db/contextHandoffs.ts 中HandoffPayload接口一一对应:

字段说明
sessionId会话标识,与comboName共同构成交接的作用域与唯一键
comboName产生交接的 Combo 名称
fromAccount生成交接时活跃的账户(connectionId)
summary紧凑摘要正文,最大 2000 字符
keyDecisions关键决策列表,最多 8 项,单项最长 240 字符
taskProgress任务进度描述,最大 1200 字符
activeEntities活跃上下文实体(文件、功能、服务商等),最多 10 项
messageCount参与摘要的消息条数
model实际生成摘要所用的模型
warningThresholdPct触发摘要的警告阈值(默认 0.85)
generatedAt生成时间(ISO 字符串)
expiresAt过期时间,默认 TTL 为 5 小时(DEFAULT_TTL_MS = 5 * 60 * 60 * 1000

写入使用INSERT ... ON CONFLICT(session_id, combo_name) DO UPDATE的 upsert 语义(见 contextHandoffs.ts),即同一会话同一 Combo 永远只保留一份交接,新生成的会覆盖旧的。交接天然带过期时间,并按sessionId + comboName隔离,不会串到其他会话。

摘要模型的 JSON 输出契约

摘要模型被提示词(HANDOFF_PROMPT_TEMPLATE)要求只返回一个结构固定的 JSON 对象,不可附带 markdown 或解释:

{ "summary": "对连续性重要内容的紧凑摘要", "keyDecisions": ["决策 1", "决策 2"], "taskProgress": "已完成项、待完成项以及下一步", "activeEntities": ["fileA.ts", "功能 X", "服务商 Y"] }

源码中的parseHandoffJSON会先剥除 markdown 代码围栏与<omniModel>标签,再尝试JSON.parse;若解析失败,会退化为截取第一个{到最后一个}之间的片段。解析成功但summary为空时返回null,该次生成被判定为 "unparseable",不会写入任何数据。

生成摘要的请求本身带有内部标记_omnirouteInternalRequest: "context-handoff"_omnirouteSkipContextRelay: true,防止摘要请求自身再次触发交接逻辑或配额监控,形成递归。摘要请求使用temperature: 0.1max_tokens: 800,且为非流式(stream: false),见 contextHandoff.ts。

注入时的 <context_handoff> 系统消息

注入时,OmniRoute 将载荷转换为带 XML 语义的<context_handoff>系统消息(见buildHandoffSystemMessage,contextHandoff.ts):

<context_handoff> <transfer_reason>Account quota transfer - continuing from previous session</transfer_reason> <session_summary>对连续性重要内容的紧凑摘要</session_summary> <task_progress>已完成项、待完成项以及下一步</task_progress> <key_decisions> - 决策 1 - 决策 2 </key_decisions> <active_context>fileA.ts, 功能 X, 服务商 Y</active_context> <messages_processed>42</messages_processed> </context_handoff>

所有动态内容都会经过 XML 转义(&<>、引号),并追加一段指令:"You are continuing a conversation that was transferred from another account due to quota limits...",引导新账户从上次会话结束的位置无缝继续。

injectHandoffIntoBody对两类请求做了区分:Responses 协议请求(请求体含inputinstructions字段)会把交接文本拼接到instructions之前;Chat Completions 请求则把交接作为role: "system"的消息插入messages数组头部(见 contextHandoff.ts)。

配置字段

context-relay支持以下配置字段,全局默认值可在 Settings 中配置,Combo 特定值可在 Combos 页面覆盖,Combo 级配置优先于全局配置

字段类型默认值说明
handoffThresholdnumber0.85摘要生成的警告阈值;必须在(0, 0.95)区间内,否则回退到默认值
handoffModelstring可选模型覆盖,用于摘要生成;为空时使用当前请求的模型
handoffProvidersstring[]["codex"]允许触发交接生成的服务商白名单;未显式配置时默认仅codex

此外,源码中的ContextRelayConfig还暴露了两个扩展字段(见 contextHandoff.ts):

  • maxMessagesForSummary:参与摘要的最大消息数,默认30,合法范围[5, 100]
  • relayMode"schema-locked" | "standard",默认"standard"schema-locked模式下摘要输入严格排除 system/developer 消息,仅取最近的非系统消息,适配对消息结构有严格要求的服务商。

resolveContextRelayConfig的解析逻辑(contextHandoff.ts)值得注意:handoffThreshold若不在(0, 0.95)内会被静默回退为0.85handoffProviders只有显式传入数组时才按白名单处理,否则默认["codex"]——这也是"当前运行时支持集中于 codex 配额轮换"的直接原因。

架构说明:为什么交接生成与注入被拆到两个文件

原文档特别强调:当前实现没有独立的handleContextRelayCombo处理器,而是刻意采用分层设计:

  • open-sse/services/combo 决定成功的回合是否应生成交接。以 executeTargetAttempt.ts 为例:当strategy === "context-relay"、服务商在handoffProviders白名单中且为codex时,Combo 层会通过fetchCodexQuota拉取账户配额信息,计算使用率,然后调用maybeGenerateHandoff决定是否触发后台摘要生成;
  • src/sse/handlers/chat.ts 负责注入:在认证解析出本次请求实际使用的账户(credentials.connectionId)之后,调用getHandoff(sessionId, comboName),仅当handoff.fromAccount !== credentials.connectionId(即确认发生了真实账户切换)时才调用injectHandoffIntoBody并把交接写入请求体,同时打印CONTEXT_RELAY日志记录账户切换方向。

这种分离是刻意的:Combo 循环本身并不知道请求最终停留在同一账户还是切换了账户——账户选择发生在认证环节内部。如果把注入放在 Combo 层,可能把交接注入到根本没切换账户的请求里,浪费载荷且污染上下文。注入后交接并不会立即删除,而是等该请求成功消费后才移除(一次性消费语义)。

该行为在 tests/unit/chat-context-relay.test.ts 中有完整验证:测试同时 seed 两个 Codex OAuth 账户(codex-a配额 87%、codex-b配额 20%),创建一个strategy: "context-relay"的 Combo,断言首次请求触发摘要生成并写入context_handoffs,下一次请求解析到不同账户时,上游请求体中出现<context_handoff>,且消费后getHandoff返回null。另一个测试还覆盖了 Responses 原生 Codex 请求在 live failover 期间的注入路径,并回归验证"账户切换后交接必须被注入、注入后必须被消费删除"这一核心不变量。

补充:通用交接(Universal Handoff)

需要说明的是,contextHandoff.ts 中还实现了面向任意模型/服务商切换的通用交接maybeGenerateUniversalHandoff,特性开关UNIVERSAL_CONTEXT_HANDOFF_ENABLED),其触发时机可以是always(每次回合)、on-switch(模型变化时,默认)或on-error(错误回退后)。这与context-relay的配额驱动路径共享同一套摘要生成、JSON 解析、存储与注入基建(selectMessagesForSummaryparseHandoffJSONupsertHandoff<context_handoff>消息模板),但作用域与触发依据不同:前者按配额水位,后者按模型切换。若配置了providerAllowlist,通用交接还会校验摘要模型所属服务商是否在白名单内。当模型输出不可解析时,系统会按(session, combo)维度记录退避冷却(从 5 分钟指数增长到最长 1 小时),避免在每次切换时反复发起注定被丢弃的上游摘要请求。

局限性

  • 有效运行时支持目前集中于codex配额轮换handoffProviders虽然已建模为通用配置面,但实际交接生成仍依赖服务商特定的配额管道(目前主要是 codex 的 quota 接口);
  • 摘要刻意保持紧凑并基于近期历史:它不是完整对话回放机制。源码中的selectMessagesForSummary默认只取最近 30 条消息,并通过estimateTokens将摘要输入裁剪到 8000 token 预算内;
  • 交接作用域限定于sessionId + comboName,并自动过期(默认 TTL 5 小时),清理由cleanupExpiredHandoffs按节流(30 分钟)执行;
  • 如果会话没有切换账户,存储的交接不会被注入,会一直保留到过期或被覆盖。

推荐使用模式

  • 为同一服务商配置多个账户,并放入同一 Combo;
  • 在整个会话中保持稳定的sessionId,因为交接按sessionId + comboName索引,sessionId 漂移会导致摘要无法命中;
  • handoffThreshold设置得足够早(例如 0.7~0.85),为后台摘要请求留出执行时间——太接近 0.95 硬停止线时,摘要可能来不及生成账户就已耗尽;
  • context-relay当作连续性辅助工具,而不是持久记忆的替代品:摘要只承载"当前任务做到哪、下一步做什么"的短期上下文,长期事实仍应依赖 OmniRoute 的记忆(Memory)或外部上下文机制。

通过理解配额水位、交接载荷契约与双层架构,你可以针对自己的多账户轮换场景精确调优context-relay,让长会话在账户切换时真正"无缝接力"。

【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute

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

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

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

立即咨询