多模型编排的三层:框架、模型路由与提供商路由
原文:OpenRouter Blog - 《LangChain vs CrewAI: Orchestration Compared to OpenRouter-Native Routing》(https://openrouter.ai/blog/insights/langchain-vs-crewai-orchestration-compared-to-openrouter-native-routing/)
做 Agent 应用时,「多模型」很容易被当成一件事:接几个模型,写个 if-else 挑一个。真到要落地才发现这里混着三件不同的事——谁负责拆任务、谁负责在模型之间切换、谁负责给同一个模型挑服务端点。三层揉成一层,代码会越写越难改;分开看,很多「我是不是得上 LangGraph」的纠结能自己回答。下面按 OpenRouter 10 月 2 日那篇对比文的结构把三层摊开,再给出可以直接跑的最小代码。
一、先分清三层职责
| 层 | 回答的问题 | 由谁负责 |
|---|---|---|
| 工作流编排 | 任务怎么拆、状态放哪、哪一步要人看、子任务交给谁 | LangGraph、CrewAI |
| 模型路由 | 这一次调用打哪个模型,它报错之后怎么办 | 请求里的 models 参数 |
| 提供商路由 | 已经选定的模型,由哪个端点来服务 | OpenRouter 自动完成 |
三层是三种不同的问题,混在一个「模型选择器」里写,后面任何一层要改都得动同一坨代码。分开之后,三层可以各自替换。
二、工作流编排层:LangGraph 和 CrewAI 给了什么
这一层解决的是「长任务怎么活下去」,两个主流框架走的是两条路。
2.1 LangGraph:把工作流画成图
LangGraph 把工作流建模成一张节点图,确定性的人写步骤和模型驱动的步骤可以混在同一张图里。它靠两个组件解决持久化:
- Checkpointer:按 thread 保存图的状态,中断后可以从断点继续;
- Store:把应用数据存在图状态之外,跨 thread 复用状态;
interrupt():在图里任意位置暂停,等人审批后继续。
它的取舍很清楚:控制权完全在你手里,代价是节点、边、状态 schema、持久化配置都得自己写。
2.2 CrewAI:用角色和任务描述替代图
CrewAI 的心智模型更接近「写一张任务单」,而不是「画一张图」:
- Crew:一组 Agent,每个有 role、goal、backstory,按分配的任务推进;流程可以是 sequential(顺序),也可以是 hierarchical(带管理者);
allow_delegation打开后 Agent 之间可以互相委派;每个 Agent 有max_iter上限(默认 20),还可以设 max_execution_time;- Flow:包在 Crew 外面的结构化、事件驱动层。官方把 Flow 定位成应用的骨架,Crew 是里面的工作单元。
你花的力气更多在 role / goal / task 这三段文字上,更少在连节点上。代价是把更多执行路径的决定权交给了 Agent 自己。
三、模型路由层:一个列表就能覆盖大部分需求
这一层不需要框架。直接请求 chat/completions 端点,把候选模型按优先级写进 models 参数,剩下的交给服务端:第一个模型报错就试下一个。默认情况下任何错误都能触发回退,包括上下文长度校验失败、被过滤模型的审核拦截、限流、服务不可用。计费按最终提供服务的那个模型算,响应里的 model 字段告诉你是哪一个。
fallback 只对错误生效,它不判断第一个模型的答案好不好。这句话是整个话题里最容易误会的地方:想要「答案质量不达标就升级」,那是工作流编排层的活,得自己在代码里判。
最小可跑的例子(官方原文示例):
# 直接请求 OpenRouter,用 models 列表做优先级回退importosimportrequestsdefroute(models:list[str],prompt:str)->tuple[str,str]:response=requests.post("https://openrouter.ai/api/v1/chat/completions",headers={"Authorization":f"Bearer{os.environ['OPENROUTER_API_KEY']}"},# models 是有序回退列表:第一个报错就用下一个json={"models":models,"messages":[{"role":"user","content":prompt}]},timeout=120,)response.raise_for_status()body=response.json()# 返回值里带上是哪个模型真正服务的,方便埋点returnbody["choices"][0]["message"]["content"],body["model"]draft,draft_model=route(["anthropic/claude-sonnet-5","openai/gpt-5.6-sol"],"Draft a one-paragraph summary of what a model fallback list does.",)review,review_model=route(["openai/gpt-5.6-sol","anthropic/claude-sonnet-5"],f"Review this draft for accuracy and suggest one improvement:\n\n{draft}",)print(f"draft by{draft_model}, review by{review_model}")print(review)这段代码里三个参数值得记一下:models 是有序列表,越靠前越优先;timeout 给足(示例取 120 秒),因为回退意味着可能真的跑完一次才失败;返回值直接取 model 字段。拿到它以后建议立刻打点——回退率、回退最终命中哪个模型,是判断这条链健不健康的第一手数据。
同样的两步流程换成 LangChain,写法是给每个模型对象挂 fallbacks:
# 同样的事情换成 LangChain:编排由框架管,模型层仍然走 OpenRouterfromlangchain_openrouterimportChatOpenRouter drafter=ChatOpenRouter(model="anthropic/claude-sonnet-5").with_fallbacks([ChatOpenRouter(model="openai/gpt-5.6-sol")])reviewer=ChatOpenRouter(model="openai/gpt-5.6-sol").with_fallbacks([ChatOpenRouter(model="anthropic/claude-sonnet-5")])draft=drafter.invoke("Draft a one-paragraph summary of what a model fallback list does.")review=reviewer.invoke(f"Review this draft for accuracy and suggest one improvement:\n\n{draft.content}")print(review.content)对比两段代码能看出分工:一个是你自己写调用顺序,一个是框架替你管流程。模型层的部分(用哪些模型、出错怎么退)两边完全一样。
四、提供商路由层:同一个模型,不同端点
一个模型在 OpenRouter 上往往有多个提供商在服务。提供商路由换的是端点,不是模型:你请求的是同一个模型,系统在符合条件的提供商里挑一个。当请求里带工具时,Auto Exacto 默认生效,按吞吐、工具调用成功率、基准数据重排提供商顺序。
这一层平时不需要你操心,但对 Agent 场景有两层含义:工具调用密集的链路,端点质量会直接影响成功率;而这类重排发生在「你已经选定的模型」内部,不会悄悄换成另一个模型。
五、中间那一段:带边界的工具循环
很多 Agent 既不需要一张持久化的图,也不需要一支带角色的团队,只需要一个「有上限的多轮工具循环」。OpenRouter 的 Agent SDK 就是冲这一段来的(示例为官方 TypeScript 代码):
// 有界的多轮工具循环:停止条件写在调用里import{OpenRouter,tool,stepCountIs,maxCost}from"@openrouter/agent";import{z}from"zod";constclient=newOpenRouter({apiKey:process.env.OPENROUTER_API_KEY});constresult=client.callModel({model:"anthropic/claude-sonnet-5",input:"What time is it in Tokyo?",tools:[tool({name:"get_time",description:"Get the current time in a timezone",inputSchema:z.object({timezone:z.string()}),execute:async({timezone})=>({time:newDate().toLocaleString("en-US",{timeZone:timezone}),}),}),],// 什么时候停:步数上限或成本上限,先到先停stopWhen:[stepCountIs(5),maxCost(0.5)],});consttext=awaitresult.getText();console.log(text);stepCountIs(5)和maxCost(0.5)是停止条件,不是消费上限——原文特意点明了这一点:达到条件循环就停,别把它当成账单封顶。该 SDK 里还有一个openrouter:subagent能力,标注为 beta。
六、对照表与选型建议
| LangGraph | CrewAI | 直接路由 | |
|---|---|---|---|
| 为谁而生 | 显式状态、持久化、人工审批的图式编排 | 事件驱动 Flow 里的角色制 Agent 团队 | 每次调用选模型、错误驱动回退、提供商路由 |
| 多模型支持 | 每个节点或 Agent 一个模型对象 | 每个 Agent、Crew 或 manager 一个 LLM | 每个请求一个 models 列表 |
| 规划、记忆、委派 | 有,写在图、Checkpointer、Store 里 | 有,通过 Agent、流程与 Flow 状态 | 没有,只有路由 |
| 流式输出 | 有 | 有,Crew 级别 | 有,按请求 |
| 人工介入 | interrupt + Checkpointer | 自己写 Flow 逻辑 | 不提供 |
| 你要写什么 | 节点、边、状态 schema、持久化配置 | Agent、Task、Crew、Flow 定义 | 一个请求体 |
选型上原文的建议很实际:先做小承诺。想不清楚需不需要框架,就先用 models 列表把一个两步流程跨两个模型跑通,再判断是不是真需要上面那层。两层不互相替代,也不需要为了拿到下面两层而先引入框架——一个列表加几个 if 就能在多模型间路由。
反过来,如果你已经在用 LangChain 或 CrewAI,也没必要换掉:LangChain 有专门的ChatOpenRouter集成,CrewAI 通过 LLM 类把 OpenRouter 当 provider。保留框架的编排,把模型与提供商路由放在下面一层,是更常见的组合。
七、给 Agent 开发学习者的启示
真正的分界线不是「用不用框架」,而是「状态要不要跨轮、跨运行活下去」。需要人工审批、需要断点续跑、需要把子任务稳定地派给不同 Agent,就上编排层;只要一次请求内把活干完,一个 models 列表往往就够了,多引入一层只是多一层要维护的东西。
另外两条可以直接拿来用的经验:回退策略要按错误类型梳理一遍,别默认「所有错误都该回退」——比如内容审核拦截,回退到另一个模型大概率还是被拦,白白多花一次调用;工具调用链路的成功率要跟端点质量一起看,工具密集时这类差异会被放大。
小结
多模型编排是三层:工作流编排负责规划、状态、记忆与委派,LangGraph 用图换来显式控制,CrewAI 用角色与 Flow 换来更少的连边工作;模型路由负责选模型和在报错时回退,一个 models 列表即可;提供商路由负责在同一个模型的多个端点间挑选,并在带工具时优先工具调用质量。三层可以拆开用,也可以叠起来用——先判断题目的边界,再决定要不要框架。
文中模型名与代码来自 OpenRouter 原文示例,框架与 SDK 的具体版本请以官方文档为准,此处未验证最新版本。