ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pi Agent 模型路由实战:快慢双挡设计与故障自愈机制

Pi Agent 模型路由实战:快慢双挡设计与故障自愈机制 1. 为什么给 Pi Agent 加一个自动挡是刚需Pi Agent 这类编码代理跑起来之后最让人头疼的不是它不会写代码而是它在用哪个模型这件事上完全没有主见。你给它配了 GPT-4 级别的模型它连改个变量名都要调用一次你给它配了便宜的小模型它遇到复杂重构就开始胡言乱语。手动切换模型这件事在真实项目里根本不可持续——你不可能一边盯着 Agent 跑任务一边手动给它换挡。我给 Pi Agent 做 Model Router 的出发点很朴素让模型选择这件事从人肉决策变成系统决策。所谓自动挡核心就是两挡设计——快挡和慢挡。快挡负责高频、低复杂度、对延迟敏感的调用比如文件读取后的意图识别、简单代码补全、格式化判断慢挡负责低频、高复杂度、需要深度推理的调用比如跨文件重构、架构级代码生成、复杂 bug 定位。两挡之间不是简单的 if-else而是一套带故障自愈的路由机制。这套东西解决的核心问题是三个第一成本失控。Pi Agent 在长任务里会发起几十甚至上百次模型调用如果全部走慢挡token 账单会非常难看。第二延迟堆积。快挡模型响应通常在几百毫秒级慢挡可能要到几秒甚至十几秒全部走慢挡会让 Agent 的交互体验变得不可接受。第三单点故障。只配一个模型提供商一旦对方限流或超时整个 Agent 就卡死了。Model Router 的故障自愈就是针对这一点设计的。这篇文章适合两类人看一类是正在用 Pi Agent 或类似编码代理做自动化开发流的人另一类是想理解模型路由这个中间层到底该怎么设计的人。我会把两挡的划分逻辑、路由决策的实现、故障自愈的状态机、以及我在实测中踩过的坑全部摊开讲。代码层面我会给出可复现的结构但不会绑定某个具体框架因为路由这层逻辑本身是通用的。先说一个反直觉的结论Model Router 最难的不是选哪个模型而是什么时候该降级、什么时候该重试、什么时候该放弃。选模型是一个分类问题而故障自愈是一个状态管理问题后者才是真正让 Agent 稳定跑下去的关键。我见过太多人把路由写成了一堆 if-else 堆砌结果一遇到超时就整个崩掉。下面我从两挡设计开始拆。2. 快挡与慢挡的划分逻辑不是按模型大小而是按任务特征2.1 两挡的本质区别在于决策成本而非模型能力很多人第一反应是快挡用小模型慢挡用大模型。这个理解对了一半但不够准确。真正决定一个请求走哪挡的是这个请求的决策成本——也就是如果选错了模型代价有多大。举个例子。Pi Agent 在读取一个文件之后需要判断这个文件是否与当前任务相关。这个判断的决策成本极低判断错了最多是多读一个无关文件或者少读一个相关文件后续步骤还能补救。这种请求就应该走快挡。反过来Agent 要生成一个跨三个文件的接口重构方案决策成本极高生成错了可能导致整个代码库编译失败回滚成本巨大。这种必须走慢挡。所以我的划分标准是三个维度维度快挡特征慢挡特征决策可逆性高错了容易补救低错了代价大上下文规模小通常单文件大跨文件/跨模块输出确定性高答案空间小低需要推理延迟敏感度高影响交互体验低可以等待调用频率高占总量 70%低占总量 30%-这张表是我在实际调优中反复修正出来的。最开始我只用了上下文规模一个维度结果发现很多单文件但需要深度推理的任务比如一个复杂函数的边界条件分析被错误地分到了快挡导致输出质量很差。后来加入了决策可逆性和输出确定性两个维度路由准确率明显提升。2.2 快挡的典型任务清单与模型选型快挡任务在我的 Pi Agent 里大概占 70% 到 75% 的调用量。具体包括意图分类判断用户指令属于读代码写代码改代码跑测试中的哪一类文件相关性打分给定任务描述和文件路径判断是否需要读取该文件简单补全单行代码补全、变量命名建议、import 语句补全格式判断判断代码是否符合项目风格规范错误信息摘要把冗长的编译错误压缩成一句话这些任务的共同点是输入输出都很短答案空间有限而且错了之后 Agent 的后续步骤有纠错机会。快挡的模型选型我实测下来最稳的是小参数量的指令微调模型参数量在 7B 到 13B 之间或者商业 API 里的轻量档。关键指标不是多聪明而是首 token 延迟和吞吐稳定性。我试过用一个大模型加请简短回答的提示词来充当快挡结果延迟比专用小模型高了 3 倍完全不划算。提示快挡模型一定要做输出格式约束。我早期没做约束快挡模型偶尔会输出一大段解释直接把 token 成本拉上去了。后来强制要求 JSON 输出并且用 grammar 约束解码成本立刻降下来了。2.3 慢挡的触发条件与升级路径慢挡不是默认挡而是升级挡。也就是说一个请求默认走快挡只有满足特定条件才升级到慢挡。这个设计很关键因为如果默认走慢挡成本会失控如果默认走快挡但升级条件太宽松也会频繁触发慢挡。我的升级触发条件有四条满足任意一条就升级快挡置信度低于阈值快挡模型输出时带一个置信度分数可以用 logprob 或者让模型自评低于 0.7 就升级任务复杂度评分超阈值用一个轻量规则引擎对任务打分涉及文件数超过 3 个、或者包含重构架构设计等关键词直接升级快挡连续失败两次同一个请求快挡重试两次都失败升级到慢挡显式标记Agent 的某些步骤在代码里硬编码标记为必须慢挡比如最终的代码生成步骤这四条里第一条和第三条是动态的第二条和第四条是静态的。动态条件负责处理快挡搞不定的情况静态条件负责处理快挡不该搞的情况。两者配合才能既省成本又保质量。2.4 两挡之间的成本与质量权衡实测我做过一组对比测试用同一个 Pi Agent 跑 50 个真实编码任务分别用全快挡全慢挡两挡路由三种配置。结果如下配置任务完成率平均延迟相对 token 成本全快挡62%1.2s/步1.0x全慢挡94%4.8s/步6.3x两挡路由91%2.1s/步2.4x两挡路由用 2.4 倍的成本拿到了接近全慢挡的完成率延迟只有全慢挡的 44%。这个数据是我坚持做路由的直接理由。全快挡虽然便宜但完成率太低Agent 跑一半就卡住了反而浪费更多。全慢挡质量好但成本是路由方案的 2.6 倍长期跑下来账单很难看。这里有个细节值得说两挡路由的完成率比全慢挡低了 3 个百分点主要损失在快挡误判上——有些任务快挡给出了看似合理但实际错误的输出Agent 没有触发升级直接用了错误结果。这 3 个点的损失我后来通过加强置信度检测补回来了一部分但没法完全消除。这是路由方案的固有代价接受它比追求完美更重要。3. Model Router 的核心实现从请求进入到模型返回的完整链路3.1 路由决策器的数据结构设计路由决策器是整个 Model Router 的大脑。我把它设计成一个无状态的纯函数加一个有状态的历史记录器。纯函数负责给定当前请求输出路由决策历史记录器负责记录最近 N 次调用的结果供故障自愈使用。请求的数据结构我定义成这样class RouteRequest: task_type: str # 任务类型如 intent_classify context_size: int # 上下文 token 数 file_count: int # 涉及文件数 complexity_score: float # 复杂度评分 0-1 explicit_tier: str # 显式指定挡位可为空 retry_count: int # 当前重试次数 last_error: str # 上次错误类型可为空这个结构里complexity_score是核心。我用一个轻量规则引擎算它规则包括文件数每超过 1 个加 0.15上下文超过 4k token 加 0.2任务类型是重构或架构加 0.3等等。这个评分不需要很精确它的作用是提供一个粗粒度的分流依据。路由决策函数的逻辑def route(req: RouteRequest) - str: if req.explicit_tier: return req.explicit_tier if req.retry_count 2: return slow if req.complexity_score 0.6: return slow if req.context_size 8000: return slow return fast这段代码看起来简单但每一条都有讲究。retry_count 2放在最前面是因为重试两次还失败的请求不管复杂度多低都应该给慢挡一个机会。complexity_score 0.6这个阈值是我调了十几轮才定下来的太低会导致慢挡调用过多太高会导致快挡误判增加。3.2 快挡调用的超时与降级策略快挡的核心诉求是快所以超时设置必须激进。我给快挡设的超时是3 秒超过 3 秒没返回就判定为超时直接走降级逻辑。这个 3 秒不是拍脑袋定的是我统计了快挡模型在正常情况下的 P99 延迟之后定的——P99 在 2.2 秒左右留 0.8 秒余量。降级逻辑分三步同挡重试先在同一挡位重试一次因为超时可能是偶发的网络抖动降级到备用快挡如果配了多个快挡模型切换到备用模型升级到慢挡如果备用也失败升级到慢挡这三步不是每次都全走一遍而是根据retry_count决定。第一次超时走第 1 步第二次走第 2 步第三次走第 3 步。这样设计的好处是偶发超时不会直接触发慢挡避免了成本浪费。注意快挡重试一定要用指数退避我第一次重试等 200ms第二次等 500ms第三次等 1s。不用退避的话如果对方是限流导致的超时连续重试只会加重限流。3.3 慢挡调用的流式处理与部分结果保留慢挡调用通常比较慢如果等全部生成完再返回Agent 的交互体验会很差。所以慢挡必须用流式输出。但流式输出带来一个问题如果流到一半失败了已经生成的部分要不要保留我的做法是保留部分结果但标记为不完整。具体来说慢挡调用时维护一个 buffer每收到一个 chunk 就追加到 buffer。如果中途失败把 buffer 里的内容返回给上层同时带上incompleteTrue标记。上层 Agent 拿到不完整结果后可以选择基于已有内容继续或者重新发起请求。这个设计在实测中救过好几次场。有一次慢挡模型在生成一个长函数时流到 80% 突然断了但前面 80% 的内容是有效的Agent 基于这部分内容补全了剩下的 20%省了一次完整的慢挡调用。3.4 路由决策的日志与可观测性路由这层如果不可观测出了问题根本没法排查。我在每个路由决策点都打了结构化日志字段包括请求 ID、任务类型、复杂度评分、最终挡位、决策耗时、模型返回耗时、是否降级、降级原因。这些日志我接了一个简单的看板实时看三个指标快挡占比、升级率、降级率。快挡占比正常应该在 70% 左右如果掉到 50% 以下说明复杂度评分阈值可能设低了。升级率正常在 15% 到 25% 之间太高说明快挡能力不足太低说明升级条件太严。降级率正常在 5% 以下超过 10% 说明快挡模型不稳定该换模型了。这三个指标是我调优路由的主要依据。没有它们调优就是盲人摸象。4. 故障自愈让 Agent 在模型抽风时还能继续跑4.1 故障分类超时、限流、格式错误、内容错误故障自愈的前提是先把故障分类。我把 Model Router 遇到的故障分成四类每类的处理策略完全不同故障类型典型表现是否可重试处理策略超时请求超过阈值无响应是退避重试必要时降级限流返回 429 或类似错误是退避重试切换备用模型格式错误输出不符合预期格式是重新请求加强格式约束内容错误输出格式对但内容错视情况升级挡位或人工介入这四类里超时和限流是基础设施故障跟模型能力无关重试通常有效。格式错误是约束故障重试时加强约束往往能解决。内容错误最麻烦因为它是能力故障重试同一个模型大概率还是错必须升级挡位或者换模型。我早期犯过一个错误把所有故障都当成重试就能解决结果内容错误重试了五次还是错白白浪费了五次调用。后来加了故障分类内容错误直接触发升级不再重试同挡。4.2 熔断器模式连续失败时的快速失败故障自愈里最重要的组件是熔断器。它的作用是当某个模型连续失败达到阈值时直接快速失败不再发起真实请求避免雪崩。我的熔断器有三个状态关闭、半开、打开。关闭状态正常调用同时统计最近 20 次调用的失败率打开状态失败率超过 50% 时进入此时所有请求直接返回失败不发起真实调用持续 30 秒半开状态30 秒后进入允许一个探测请求通过如果成功则回到关闭状态失败则回到打开状态这个状态机是经典设计但用在 Model Router 上有两个细节要注意。第一熔断粒度要按模型分不能全局熔断。快挡模型挂了不应该影响慢挡。第二半开状态的探测请求要选低风险任务不要拿一个复杂重构任务去探测失败了会误判。class CircuitBreaker: def __init__(self, threshold0.5, window20, cooldown30): self.threshold threshold self.window window self.cooldown cooldown self.failures [] self.state closed self.opened_at 0 def allow(self): if self.state closed: return True if self.state open: if time.time() - self.opened_at self.cooldown: self.state half_open return True return False if self.state half_open: return True这段代码是简化版实际用的时候还要加线程安全和持久化但核心逻辑就是这样。4.3 重试预算避免无限重试拖垮整个 Agent重试不能无限进行必须有一个重试预算。我的预算是按任务维度算的每个任务最多允许 8 次模型调用失败超过就放弃任务返回错误给用户。这个预算的分配也有讲究。快挡重试便宜可以多给几次慢挡重试贵要少给。我的分配是快挡最多 3 次重试慢挡最多 2 次重试跨挡升级算 1 次。加起来一个任务最多 6 次重试留 2 次余量给意外情况。重试预算要跟熔断器配合。如果熔断器已经打开了重试预算再高也没用因为请求根本发不出去。所以实际逻辑是先看熔断器熔断器允许才消耗重试预算。4.4 自愈过程中的状态一致性保障故障自愈最怕的是状态不一致。比如一个任务在快挡失败了升级到慢挡成功但快挡失败时可能已经产生了一些副作用比如写入了部分文件。如果不处理这些副作用慢挡再写一次就会冲突。我的做法是所有模型调用都设计成幂等的。具体来说模型调用不直接写文件而是返回一个操作计划由上层 Agent 统一执行。这样即使模型调用失败重试也不会产生副作用。这个设计在早期增加了一些复杂度但在故障自愈场景下价值巨大。提示如果你的 Agent 架构里模型调用直接产生副作用强烈建议改成计划-执行两阶段。这是故障自愈能可靠工作的前提。4.5 一个真实的故障自愈案例复盘上个月我跑一个批量重构任务涉及 40 多个文件。跑到第 23 个文件时慢挡模型突然开始返回格式错误。熔断器在连续 5 次格式错误后打开路由自动切换到备用慢挡模型。备用模型跑了 3 个文件后也开始限流熔断器再次打开。此时两个慢挡都不可用路由降级到快挡但快挡处理复杂重构质量不够连续失败 2 次后触发重试预算上限任务暂停。整个过程中Agent 没有崩溃而是有序地降级、暂停并保留了已完成 22 个文件的状态。我手动检查了备用模型限流的原因是并发太高调整并发后恢复任务从第 23 个文件继续跑完。这个案例让我意识到故障自愈的目标不是永不失败而是失败得有序。Agent 可以暂停可以降级但不能崩溃不能丢状态。这才是自愈的真正价值。5. 实测中踩过的坑与调优经验5.1 复杂度评分阈值调了十几轮才稳定复杂度评分阈值是路由的核心参数我一开始设的 0.5结果慢挡调用占比到了 45%成本太高。调到 0.7慢挡占比降到 18%但快挡误判明显增加任务完成率掉了 8 个点。最后定在 0.6慢挡占比 25% 左右完成率损失控制在 3 个点以内。这个调优过程没有捷径就是拿真实任务跑看数据调参数再跑。我建议你一开始就把阈值做成可配置的并且把每次调参的结果记录下来形成自己的调优曲线。5.2 快挡模型的过度自信问题快挡模型有个通病过度自信。它明明答错了但置信度给得很高。我早期用模型自评置信度结果发现快挡模型自评 0.9 的答案实际正确率只有 70%。后来改用 logprob 算置信度准确了一些但还是有偏差。最终的方案是组合置信度模型自评占 40%logprob 占 40%历史同任务类型的成功率占 20%。这个组合置信度比单一指标准很多升级判断的准确率提升了大概 15 个点。5.3 慢挡流式输出的断流处理慢挡流式输出断流是我踩过最深的坑。早期没做部分结果保留断流后整个请求作废重试成本很高。后来加了 buffer 保留但发现保留的部分结果有时候是半句话直接给上层会导致解析错误。解决方案是在 buffer 里维护一个完整语义单元边界。比如生成 JSON 时只在完整的键值对边界保留生成代码时只在完整的语句边界保留。这样即使断流保留的部分也是可用的。5.4 多模型提供商的并发控制当你配了多个模型提供商时并发控制变得很复杂。我早期没做并发控制结果同时发起太多请求触发了限流熔断器频繁打开。后来加了一个令牌桶每个提供商一个桶桶容量根据提供商的限流策略设置。请求前先取令牌取不到就排队。这个改动之后限流导致的熔断从每天十几次降到了几乎为零。5.5 路由日志的采样与存储成本路由日志很有用但量太大了。我一开始全量存储一周就存了几百万条存储成本很高。后来改成分层采样正常请求采样 1%降级请求采样 100%错误请求采样 100%。这样既保留了关键信息又把存储量降到了可接受范围。这个采样策略的关键是降级和错误全采。因为这两类才是排查问题的关键正常请求采样率低一点没关系。6. 两挡设计的边界与后续扩展方向两挡设计不是万能的它有明确的适用边界。当任务复杂度分布非常均匀时两挡的收益会下降。因为如果大部分任务都落在中间地带快挡慢挡的划分就变得模糊路由决策的准确率会下降。我实测下来两挡设计在任务复杂度呈双峰分布的场景下收益最大——也就是简单任务和复杂任务都很多中间地带少。Pi Agent 的编码任务恰好符合这个分布所以两挡效果很好。如果任务复杂度是均匀分布可能需要考虑三挡甚至连续路由。但挡位越多路由决策越复杂维护成本越高。我的建议是先从两挡开始只有当两挡的完成率或成本明显不达标时才考虑加挡。后续扩展方向我列几个正在尝试的基于历史的路由学习用历史调用数据训练一个轻量分类器替代规则引擎做路由决策动态阈值复杂度评分阈值根据当前系统负载动态调整负载高时阈值降低多用快挡跨任务的路由记忆同一个会话里的连续任务路由决策可以参考前一个任务的结果这些方向都还在实验阶段等有稳定结论了再单独写一篇。最后分享一个我在实际使用中体会最深的点Model Router 的价值不在于它选得多准而在于它选错之后能多快恢复。我见过太多人把精力花在如何一次选对上结果系统一遇到意外就崩。真正让 Agent 稳定跑下去的是那套故障自愈机制——熔断、降级、重试预算、状态保留。选模型是战术自愈是战略。把战略做扎实了战术上的小失误都能被兜住。
返回列表