在探讨 AI Agent 时,一个常被忽视的关键问题是:请求到来时,究竟由谁负责响应?

一个对话请求应当分配给哪个模型?简单问答可以选用小模型,复杂推理则需大模型处理;某个 Agent 实例发生故障时,如何自动切换到备用实例;上下文过长时,怎样压缩才能保留核心信息。这些问题,PilotDeck 提供了一套完整的工程化解决方案。
一、为什么需要路由?
传统的 LLM 调用通常采用一对一的模式:收到请求,用固定的模型进行处理。然而生产环境中的 AI Agent 面临更加复杂的挑战——
- 依据任务复杂度选择适宜的模型,将资源用在最需要的地方
- 多个模型组成降级链路,一个失败后自动切换至下一个
- 跨 Agent 的上下文压缩与管理
- 子 Agent 的编排以及状态维护
在此背景下,简单的 if-else 判断已难以胜任,必须引入专业的路由层来承担这一职责。
二、PilotDeck 路由的整体架构
用户请求 ↓
RouterRuntime.decide() ← 决策:用什么模型? ↓
RouterRuntime.execute() ← 执行:调用模型,返回流式事件 ↓
AgentLoop ← 循环:工具调用、上下文管理、重试
RouterRuntime 负责“选用何种模型”这一决策环节,而 AgentLoop 则负责“模型如何运作”这一执行环节。二者职责明确分离,架构层次清晰。
三、决策链:优先级层层递进
在 decide() 方法中,路由决策的优先级采用层层传递的机制:
Custom Router(插件自定义)→ Scenario(显式指定)→ TokenSa ver Sticky(会话粘性)→ TokenSa ver Classification(智能分类)→ Default(默认兜底)
每一层均为独立的策略层,一旦当前策略失败,便自动传递至下一层。这种设计模式在工程领域被称为责任链模式。
1. Custom Router:插件化的魔力
通过 PilotDeckCustomRouter 接口,开发者能够像拼接积木一样插入任意自定义的路由策略:
export type PilotDeckCustomRouter = {
id: string;
decide(input: CustomRouterDecideInput): Promise | undefined>;
};
外部扩展可以自主实现路由逻辑,并通过 CustomRouterRegistry 进行注册。在 RouterRuntime 进行决策时,会优先调用自定义路由,这一特性为开发者提供了极大的灵活性。
2. TokenSa ver:用小模型为任务分类
TokenSa ver 是 PilotDeck 中极具巧思的设计之一。它利用一个小型模型(Judge)判断当前任务应归入哪个等级:
用户消息 → Judge 模型 → tier name → 对应模型
它将任务划分为四个层级:
| Tier | 适用场景 |
|---|---|
| simple | 简单问答、确认、一次性文件写入 |
| medium | 单步工具调用、短文本生成、1-2 个文件读写 |
| reasoning | 深度单 Agent 工作:多文件操作、数据分析、多步工作流 |
| complex | 需要子 Agent 编排:并行工作流、委托任务 |
Judge 模型只需返回一个 tier 名称,计算量很小,却能显著降低整体成本。值得注意的是,针对用户仅发送“继续”、“好的”这类简短确认消息,PilotDeck 专门做了 Smart Continuation 优化——这类消息不应被重新分类,而是直接沿用上一轮的 tier。因为小模型很容易把这些短消息误判为“simple”。
3. Session Sticky:会话粘性机制
同一会话中的连续请求,若内容高度相似,每次都用 Judge 来分类显然是一种浪费。SessionRouterStore 采用内存缓存来保存会话状态:
get(sessionId, isSubagent) {
// 检查 TTL 是否过期
// LRU 提升最近访问的条目
return this.map.get(key)?.state;
}
支持 TTL(默认 60 分钟)与 LRU 淘汰(默认容量 500),并且主/子 Agent 的存储是分离的(key = sessionId 或 sessionId:sub)。有限的内存资源,被精准投入到最关键的地方。
四、执行层:容错与恢复
execute() 方法不仅执行模型调用,还需要处理各种棘手的故障场景。
1. Fallback 降级链
attempts = [主模型, ...fallbackPlan.attempts // 配置的多级降级模型]
当主模型失败时,会按顺序尝试降级模型。但降级并非盲目进行,有些错误适合降级,有些不适合:
function isFallbackEligible(error) {
if (error.code === "invalid_tool_arguments") return true; // 可自修复
if (!error.retryable) return false; // 非重试错误不降级
if (error.code === "prompt_too_long") return false; // 长度问题降级也解决不了
return true;
}
2. Zero-Usage Retry:空响应检测
有时模型确实返回了内容,但实际内容为空(尤其是在流式响应中途出错时)。Zero-Usage Retry 专门用于检测这类场景:
function shouldRetryZeroUsage(state) {
if (state.observedFinish && // 收到 message_end
!state.observedAnyText && // 但没有任何内容
totalTokens === 0) { // 且 Token 数为 0
return true; // 触发重试
}
}
3. Streaming 防重复机制
这是整个工程中体现精细程度最高的地方。当 Fallback 切换模型时,如果已经向用户输出了部分内容,再切换就会导致重复输出。PilotDeck 使用 hasYieldedContent 标记来解决此问题:
let hasYieldedContent = false;
let pending: CanonicalModelEvent[] = [];
for await (const event of streamAttempt(...)) {
if (!hasYieldedContent && isContentEvent(event)) {
// 先 flush 之前 buffer 的 framing events
for (const queued of pending) yield queued;
pending = [];
yield event;
hasYieldedContent = true; // 标记:已经有输出了
continue;
}
if (hasYieldedContent) {
yield event; // 直接输出
continue;
}
pending.push(event); // 还没内容,先 buffer
}
五、AgentLoop:循环中的艺术
AgentLoop 是整个系统的核心循环,其管理非常精细。
1. 两阶段压缩
压缩(Compaction)在路由决策前后各执行一次:
第一阶段:路由决策前(用主 Agent 的默认上下文窗口) ↓
第二阶段:路由决策后(用目标模型的上下文窗口)
为什么需要这样做?因为不同模型的上下文窗口大小不同。如果主 Agent 配置了 20k token 窗口,但路由决定使用 4k 窗口的模型,就必须进行二次压缩,否则模型根本无法容纳那么多内容。
2. Circuit Breaker:防止模型卡死
如果连续 3 轮所有工具调用都出现 invalid_tool_input 错误,说明模型陷入了某种死循环(比如反复生成空参数),此时应当果断熔断:
const MAX_CONSECUTIVE_ALL_INVALID_TURNS = 3;
if (consecutiveAllInvalidTurns >= MAX_CONSECUTIVE_ALL_INVALID_TURNS) {
throw new Error("模型陷入工具调用错误循环,终止执行");
}
3. JSON Self-Correct
模型生成的 JSON 参数有时会格式错误(例如缺少引号、尾随逗号)。PilotDeck 会自动检测并让模型重试:
if (error.code === "invalid_tool_arguments" && jsonSelfCorrectCount < 3) {
messages.push({
role: "user",
content: "你上一个工具调用的参数包含无效 JSON,请用有效 JSON 重试。"
});
continue; // 重试
}
最多重试 3 次,给模型一次自我纠错的机会。
六、CompactionEngine:上下文压缩
当对话历史过长时,CompactionEngine 会使用一次额外的模型调用来总结历史:
保留最近 35% 的消息 → 用模型总结前面的内容 → 插入边界标记
总结后的结构如下:
boundaryMarker → summary → keep → attachments → hookResults
工具对的完整性检查同样重要:如果被总结的消息中存在一个工具调用,但它的结果位于保留部分,就会产生悬垂引用。CompactionEngine 会剥离这些不配对的工具调用和结果,确保上下文的完整性。
七、设计模式总结
| 模式 | 体现位置 | 作用 |
|---|---|---|
| 责任链 | Custom → Scenario → TokenSa ver → Default | 策略可插拔,层层传递 |
| 适配器 | normalizeStreamEvent | 统一多 Provider 差异 |
| 装饰器 | Mutation Log | 记录请求的“副作用”而不改核心逻辑 |
| 熔断器 | Circuit Breaker | 防止模型卡死烧钱 |
| 两次提交 | decide + execute 分离 | 支持路由后二次压缩 |
| TTL + LRU | SessionRouterStore | 有限内存的高效利用 |
八、写在最后
研读 PilotDeck 的代码,最大的感受是其工程精细度。很多框架设计一个功能,画一张架构图就结束了。但 PilotDeck 的每个功能都具备完整的边界情况处理:空响应如何处理、Token 预算超限如何处理、连续错误如何熔断、流式输出如何防止重复……
这些并非过度设计,而是生产级系统不可或缺的能力。当你的系统每天处理成千上万个请求时,每一个边界情况的处理质量,直接决定了系统的稳定性与成本。这恰恰是 AI Agent 框架从“玩具”迈向“产品”的关键一步。
