
1. 为什么我要给 Pi Agent 做一套 Model RouterPi Agent 这类编码智能体本质上是一个“会自己调工具、自己读文件、自己改代码”的循环执行器。它每一轮都要做一件事把当前上下文丢给某个模型拿到回复解析出下一步动作然后继续跑。跑得多了你就会发现一个很现实的问题——不是每一步都值得用最贵的模型。我最初用 Pi Agent 跑一个中型项目的重构任务全程挂同一个高配模型结果一个下午烧掉的额度够我吃一周午饭。更难受的是很多步骤其实只是“读一下文件、列一下目录、确认一下变量名”这种活儿用高配模型纯属浪费。但你要是全程换成便宜模型遇到复杂推理、跨文件依赖分析、报错定位的时候它又开始胡言乱语给你改出一堆新 bug。这就是我做 Model Router 的起点让 Pi Agent 根据当前任务的性质自动在“高配推理挡”和“轻量执行挡”之间切换。我把它叫做“自动挡”对应的是原来那种“手动指定一个模型跑到底”的“手动挡”。这套东西解决的核心问题有三个。第一是成本把大量低价值步骤下沉到轻量模型实测能省下六到八成的调用开销。第二是速度轻量模型响应快Agent 的整体循环节奏明显更顺。第三是稳定性当某个模型接口抽风、超时、返回格式错乱时Router 要能自己兜住而不是让整个 Agent 卡死在那儿。适合谁来参考如果你正在用 Pi Agent 或者类似的编码智能体框架已经跑通了基本流程开始被成本和稳定性折磨那这篇就是写给你的。如果你还没上手 Pi Agent建议先把基础跑通再来看路由这块不然容易一头雾水。我下面会从整体设计思路讲起然后拆核心细节、实操过程、故障自愈最后把我踩过的坑整理成速查表。全程都是我自己项目里跑出来的东西不是纸上谈兵。2. Model Router 的整体设计与两挡思路拆解2.1 为什么是“两挡”而不是“多挡”一开始我也想过做多挡比如搞个五档变速超轻、轻、中、重、超重。但真动手的时候发现挡位越多判定逻辑越复杂调参成本越高而且实际收益并不明显。原因很简单——模型的能力和价格并不是线性分布的中间那几挡往往性价比尴尬用起来还不如直接二选一。所以最后我收敛成两挡轻量挡执行挡负责读文件、列目录、简单文本处理、格式转换、状态确认这类“确定性高、推理需求低”的步骤。推理挡重挡负责跨文件依赖分析、复杂 bug 定位、架构级重构、多步逻辑推演这类“需要真正动脑子”的步骤。两挡的好处是判定边界清晰。你只需要回答一个问题这一步到底需不需要“想”不需要想走轻量挡需要想走推理挡。中间那些模棱两可的情况我后面会用一套打分机制来处理但整体上就两个出口维护起来轻松很多。提示两挡设计的关键不是“挡位少”而是“判定标准要单一且可解释”。如果你的判定逻辑自己都说不清楚那路由一定会乱。2.2 路由判定的三个信号来源Router 要决定走哪挡得有依据。我用的是三个信号源按优先级从高到低排列。第一个是显式指令。Pi Agent 的每一步动作其实都带类型标签比如read_file、list_dir、apply_patch、analyze_error。这些标签本身就是最强的信号。读文件、列目录直接判轻量分析错误、生成补丁直接判推理。这部分覆盖了大概七成的步骤而且几乎不会判错。第二个是上下文特征。有些步骤标签一样但难度差很多。比如同样是apply_patch改一个变量名和重构一个模块完全是两码事。这时候我会看几个特征涉及的文件数量、代码块的行数、是否跨目录、是否触及核心配置文件。这些特征加权算一个分数超过阈值就走推理挡。第三个是历史反馈。如果轻量挡连续两次在同类任务上返回了低质量结果比如补丁应用失败、解析出错Router 会把这类任务临时提升到推理挡跑一段时间再降回来。这是一个自适应的过程相当于给 Router 加了个“记忆”。三个信号源的权重我设的是 0.6 / 0.3 / 0.1。显式指令占大头因为它最可靠上下文特征做补充历史反馈做微调。这个比例不是拍脑袋定的是我拿一批历史任务回放调出来的后面实操部分会讲怎么调。2.3 两挡之间的“换挡时机”换挡时机是这套设计里最容易被忽略、但最影响体验的部分。我见过有人把 Router 做成“每步独立判定”结果 Agent 在轻量和推理之间疯狂横跳上下文风格不统一输出质量反而下降。我的做法是以任务段为单位换挡而不是以单步为单位。一个任务段指的是一段连续的、目标一致的执行序列。比如“定位某个报错”可能包含读日志、读相关文件、分析调用链、生成修复补丁这几步这整个序列算一个段段内尽量保持同一挡位只在段与段之间切换。这样做的好处是上下文连贯。同一个模型连续处理一段任务它的输出风格、变量命名习惯、推理深度都是一致的Agent 解析起来更稳。如果每步都换模型你会看到前一步还在用简洁风格下一步突然变得啰嗦解析器很容易懵。段边界的判定我用了两个规则一是动作类型发生大类切换比如从“读取类”切到“修改类”二是显式指令里带了新的任务目标。这两个规则覆盖了绝大多数情况实测下来换挡频率大概每五到八步一次节奏比较舒服。2.4 为什么把“故障自愈”和路由绑在一起做很多人会把路由和容错当成两件事分开做。我一开始也是这么想的后来发现不行。因为路由本身就引入了新的故障点轻量模型可能返回格式不对的内容推理模型可能超时两个模型的输出结构可能不一致。如果容错不跟着路由一起设计你会遇到一堆“路由判对了但执行挂了”的情况。所以我把故障自愈直接内建在 Router 里核心思路是每一挡都有对应的降级和重试策略。轻量挡挂了先原地重试一次还不行就升挡到推理挡兜底推理挡挂了先重试再不行就降挡用轻量挡跑一个简化版本同时标记这个任务段需要人工确认。这套机制后面会单独用一节讲这里先记住一个原则路由决定走哪条路自愈保证这条路走不通时还有退路。两者是一体的。3. 核心细节解析与实操要点3.1 动作类型到挡位的映射表这是 Router 最基础的一张表我直接放在配置里改起来方便。下面是我实际用的映射你可以根据自己的任务类型调整。动作类型默认挡位说明read_file轻量纯读取无推理list_dir轻量目录枚举search_text轻量关键词检索format_output轻量格式整理apply_patch推理涉及代码修改需谨慎analyze_error推理错误定位plan_steps推理多步规划refactor_module推理架构级改动summarize轻量摘要生成verify_result轻量结果校验这张表不是死的。比如summarize如果摘要的是整个项目的架构文档那它其实需要推理这时候就要靠上下文特征把它顶上去。所以映射表只是第一层后面还有加权修正。注意映射表里我把apply_patch默认放推理挡是因为代码修改一旦出错回滚成本很高。宁可多花点额度也别让轻量模型乱改代码。这是我踩过坑之后的血泪教训。3.2 上下文特征的打分公式上下文特征这块我用了一个简单的加权打分公式不复杂但每个权重都是调出来的。score w1 * file_count w2 * code_line_count / 100 w3 * cross_dir_flag w4 * core_config_flag w5 * error_depth各权重的取值和含义w1 0.8涉及文件数每多一个文件加 0.8 分。跨文件是复杂度上升的最直接信号。w2 0.5代码行数除以 100 后加权。行数多意味着上下文长轻量模型容易丢信息。w3 1.5是否跨目录是则加 1.5。跨目录往往意味着模块间依赖推理需求高。w4 2.0是否触及核心配置是则加 2.0。核心配置改错影响面大必须重挡。w5 1.2错误堆栈深度越深越可能是根因问题需要推理。阈值我设的是score 3.0走推理挡否则轻量挡。这个阈值调过好几轮3.0 是我这边任务分布下的甜点值。你的项目如果文件普遍偏大可以把阈值往上提如果任务偏碎可以往下调。3.3 历史反馈的滑动窗口设计历史反馈这块我用了一个长度为 10 的滑动窗口记录每个任务段的路由结果和质量评分。质量评分来自两个地方一是补丁是否应用成功二是解析器是否能正常解析输出。两个都成功记 1 分有一个失败记 0 分。当某个动作类型在窗口内的平均分低于 0.6 时Router 会把这类任务临时提升一挡持续 20 个任务段然后再评估是否降回来。这个机制解决了一个很实际的问题有些任务看起来简单但轻量模型就是搞不定。比如某些特定框架的配置文件解析表面上是读文件实际上格式很绕轻量模型经常读错。历史反馈能自动把这类任务识别出来。提示滑动窗口长度别设太短太短会抖动也别太长太长反应慢。10 到 20 之间比较合适我用的 10。3.4 输出格式的统一约定两挡模型不一样输出格式很容易不一致。我的做法是在 Router 层做格式归一化不管哪个模型返回什么都先过一层适配器转成统一的内部结构再交给 Agent。统一结构大概长这样{ action: apply_patch, target: src/utils/parser.js, payload: ..., confidence: 0.85, model_tier: reasoning }model_tier字段很关键它让下游知道这一步是谁产出的方便做质量追踪。confidence是模型自评的置信度低于 0.5 的时候 Router 会考虑升挡重跑。格式归一化这层看起来不起眼但它是我这套 Router 能稳定跑起来的关键。没有它Agent 的解析器要同时兼容两套输出维护成本翻倍。4. 实操过程与核心环节实现4.1 环境准备与依赖确认动手之前先把环境理清楚。我用的是 Node.js 环境Pi Agent 本身跑在 Node 20 以上Router 作为中间层嵌在 Agent 的模型调用环节。需要确认的几件事Pi Agent 版本要支持自定义模型调用钩子。老版本没有这个钩子Router 插不进去。两个模型的接口凭证都配好轻量挡和推理挡各一套。日志系统要能记录每次路由决策不然调参的时候两眼一抹黑。日志这块我单独说一句。我一开始没做详细日志结果调阈值的时候完全靠猜浪费了两天。后来加了结构化日志每次决策记录动作类型、打分、最终挡位、执行结果调参效率直接起飞。4.2 Router 核心代码结构Router 的主体我拆成四个模块职责清晰方便单独测试。class ModelRouter { constructor(config) { this.actionMap config.actionMap; this.weights config.weights; this.threshold config.threshold; this.history new SlidingWindow(10); } decide(context) { const baseTier this.actionMap[context.action] || light; const score this.computeScore(context); const historyTier this.history.suggest(context.action); return this.finalize(baseTier, score, historyTier); } computeScore(context) { const w this.weights; return w.fileCount * context.fileCount w.codeLines * (context.codeLines / 100) w.crossDir * (context.crossDir ? 1 : 0) w.coreConfig * (context.coreConfig ? 1 : 0) w.errorDepth * context.errorDepth; } finalize(baseTier, score, historyTier) { if (historyTier reasoning) return reasoning; if (score this.threshold) return reasoning; return baseTier; } }decide是入口computeScore算上下文分finalize做最终裁决。历史反馈优先级最高因为它代表实际表现其次是上下文分最后才是基础映射。4.3 参数调优的实操记录调参这块我拿了一批历史任务做回放大概 200 个任务段覆盖读、写、分析、重构四类。调参目标是两个路由准确率尽量高同时推理挡的使用比例尽量低。第一轮我用的是拍脑袋的权重结果推理挡使用率高达 65%成本没省下来。分析日志发现w2代码行数权重给太高了很多大文件其实只是读一下不需要推理。把w2从 1.0 降到 0.5 之后推理挡使用率降到 42%。第二轮发现有些跨目录的简单任务被误判成推理挡把w3从 2.0 降到 1.5使用率降到 35%准确率没掉。第三轮调阈值从 2.5 提到 3.0使用率降到 28%这时候开始出现少量误判——有几个复杂任务被降到轻量挡补丁应用失败。把历史反馈窗口从 5 调到 10误判被自动纠正最终稳定在推理挡使用率 30% 左右准确率 92%。这个 30% 是我这边的甜点值。你的项目如果任务偏复杂这个比例会更高别硬套。4.4 换挡边界的实现细节换挡边界我用了一个简单的状态机记录当前任务段的挡位只有满足换挡条件才切换。class TierStateMachine { constructor() { this.currentTier null; this.segmentAction null; } shouldSwitch(nextAction, nextTier) { if (this.currentTier null) return true; if (this.isMajorCategoryChange(this.segmentAction, nextAction)) return true; if (nextTier ! this.currentTier this.hasNewGoal(nextAction)) return true; return false; } isMajorCategoryChange(prev, next) { const readGroup [read_file, list_dir, search_text]; const writeGroup [apply_patch, refactor_module]; return readGroup.includes(prev) ! readGroup.includes(next); } }isMajorCategoryChange判断是否从读取类切到修改类这是最典型的换挡点。hasNewGoal判断显式指令里是否带了新目标这个需要 Agent 在动作里带上目标标记我在 Pi Agent 的 prompt 里加了一个字段来实现。4.5 故障自愈的实现自愈这块我分三层做从轻到重。第一层是原地重试。任何模型调用失败先重试一次间隔 500 毫秒。这一层能解决大部分网络抖动和偶发超时。第二层是升挡兜底。轻量挡重试还失败或者返回内容解析不了直接升到推理挡重跑这一步。推理挡模型更强通常能给出可解析的结果。第三层是降挡简化。推理挡也失败的话降回轻量挡但把任务简化——比如把“重构整个模块”降级成“只改这一个函数”先保证 Agent 能往前走同时打标记让后续人工确认。async function callWithHealing(router, context) { const tier router.decide(context); try { return await callModel(tier, context); } catch (err) { const retry await callModel(tier, context).catch(() null); if (retry) return retry; const fallbackTier tier light ? reasoning : light; const simplified simplifyContext(context); return await callModel(fallbackTier, simplified); } }这套三层自愈跑下来我这边 Agent 因为模型问题卡死的概率从原来的每天好几次降到基本为零。5. 常见问题与排查技巧实录5.1 路由判定不准的排查思路判定不准是最常见的问题表现是轻量挡老出错或者推理挡用得太频繁。排查我一般按这个顺序走。先看日志里动作类型的分布。如果某个动作类型频繁出错先检查映射表是不是把它放错挡了。我遇到过一次verify_result被放轻量挡但实际校验逻辑很复杂轻量模型经常漏判后来挪到推理挡就好了。再看上下文打分。把出错任务的打分打出来看看是不是卡在阈值附近。如果大量任务分数在 2.8 到 3.2 之间说明阈值设得不合适要么调阈值要么调权重让分布更分散。最后看历史反馈有没有生效。如果某类任务反复出错但历史反馈没把它顶上去检查滑动窗口是不是太短或者质量评分的判定逻辑有问题。5.2 两挡输出风格不一致怎么办这个问题我前面提过根子在格式归一化。如果你发现 Agent 解析经常出错先确认归一化层是不是覆盖了所有字段。我踩过的坑是只归一化了action和payload忘了target字段结果轻量模型返回的 target 格式和推理挡不一样解析器直接崩。解决办法很简单把所有下游会用到的字段都列出来逐个确认两挡输出都能映射到统一格式。宁可多写几行适配代码也别让格式问题漏到下游。5.3 成本没降下来的原因分析有人做完路由发现成本没怎么降通常是三个原因。一是推理挡使用率太高。回去看日志如果超过 50%说明判定太保守权重或阈值需要调。二是轻量挡模型选得太贵。轻量挡要选真正便宜的模型别选那种“中等价位”的不然省不下来。三是重试和自愈带来的额外调用。如果自愈触发太频繁说明基础稳定性有问题得先解决模型接口的稳定性而不是靠自愈硬扛。5.4 常见问题速查表现象可能原因排查动作轻量挡频繁出错映射表放错挡检查动作类型映射推理挡使用率过高阈值太低或权重偏大调阈值、降权重解析器频繁报错格式归一化不全补全字段适配成本没降轻量挡模型太贵换更便宜的模型自愈触发频繁模型接口不稳定先解决基础稳定性换挡太频繁段边界判定太细放宽换挡条件历史反馈不生效窗口太短或评分逻辑错调窗口长度、查评分5.5 我踩过的几个坑第一个坑是一开始没做段级换挡每步独立判定结果 Agent 输出风格横跳解析器天天报错。改成段级之后立刻稳定。第二个坑是历史反馈窗口设成 3太短导致挡位抖动一会儿升一会儿降。改成 10 之后平滑多了。第三个坑是自愈没有上限有一次模型接口挂了自愈疯狂重试把额度烧光了。后来加了重试上限超过三次直接标记任务失败交给人工。第四个坑是日志没记 model_tier出问题的时候不知道是哪挡出的错排查全靠猜。加上这个字段之后定位问题快了一倍。提示这四个坑里段级换挡和日志字段是最值得优先做的投入小、收益大。6. 一些实操后的个人体会这套 Model Router 我在自己的 Pi Agent 项目里跑了大概三个月中间迭代了四五版。最大的感受是路由的核心不是模型选得多聪明而是判定逻辑多稳定。你不需要一个完美预测任务难度的系统你需要一个大部分时候判对、判错时能自愈的系统。两挡设计对我来说刚刚好。挡位再少就没法区分再多就维护不动。故障自愈和路由绑在一起做是我觉得最正确的决定它让整套系统从“能跑”变成“敢让它自己跑”。如果你准备动手我的建议是先别急着调权重先把日志和格式归一化做好。这两块是地基地基不稳后面调什么都是白搭。等日志能清楚告诉你每次决策的依据调参就是个体力活不再是玄学。最后分享一个小技巧历史反馈的滑动窗口你可以按动作类型分别维护而不是全局一个窗口。这样不同任务的反馈互不干扰判定会更准。我最近刚改成这样效果还在观察但初步看误判率又降了一点。