LangChain + OpenRouter:一条 API 调用 400+ 大模型的实战指南
A Practical Guide: Calling 400+ LLMs Through a Single LangChain Integration
如果你正在用 LangChain 构建 AI 应用,迟早会面临一个选择题:绑定 OpenAI 的 GPT 系列?接入 Anthropic 的 Claude?还是给 Google 的 Gemini 留一条后路?每个选择都意味着一套独立的 API 密钥、不同的参数格式、各自的故障模式。更麻烦的是,当你想从 GPT-4 切换到 Claude,或者给国内用户配一个 DeepSeek 的备选方案时,你的链条(chain)里几乎每一行代码都要跟着改。
OpenRouter 的解法很直接:一个 OpenAI 兼容的 API 端点,背后挂着 400 多个模型、70 多家提供商。你改一个字符串,模型就换了,而你的 prompts、工具定义、输出解析逻辑完全不用动。LangChain 官方现在也有了自己的 ChatOpenRouter 封装包,不再是以前那种”用 ChatOpenAI 改 base_url“的野路子。
这篇文章不讲虚的,只讲怎么在真实项目里把这套东西跑起来,以及那些官方文档不会告诉你的细节。
为什么要在 LangChain 里接 OpenRouter?
Why OpenRouter Instead of a Single Provider?
核心问题:我已经在用 LangChain 了,为什么还要在中间加一层 OpenRouter?
答案分三层。第一层是模型选择的自由度。OpenRouter 的模型目录里有 400 多个选项,从 OpenAI 的 GPT-5 系列到 Anthropic 的 Claude Sonnet 4.5,从 Google 的 Gemini 到 Meta 的 Llama,再到国内的 DeepSeek、Qwen,全部走同一个端点。你的 model 参数只是一个 provider/model 格式的字符串,比如 anthropic/claude-sonnet-4.5。想换模型?改这个字符串就行,prompt 不用重写,工具定义不用改,甚至连 token 计费的返回格式都保持一致。
第二层是路由层的隐性价值。OpenRouter 不只是个 API 聚合器,它自带负载均衡和故障转移。当你调用一个模型时,它会在多个提供该模型的服务商之间做价格负载均衡,自动避开最近 30 秒内出过故障的节点。如果某个提供商挂了,请求会自动漂移到下一个可用的节点,你的应用代码里连 try-except 都不用写。更关键的是——没成功的请求不收钱。
第三层是LangChain 的原生支持。以前接 OpenRouter 要靠 ChatOpenAI 的 base_url 覆盖,参数透传全靠 model_kwargs,结构化输出和工具调用经常踩坑。现在 PyPI 上有 langchain-openrouter,npm 上有 @langchain/openrouter,是官方维护的封装包,类型提示、参数校验、文档都齐全。beta 阶段更新快,但功能已经完整可用。
我最早接 OpenRouter 是在一个客服工单分类的项目里。当时客户要求同时测试 Claude 和 GPT-4 的效果,如果用原生 SDK,我得维护两套调用逻辑。切到 OpenRouter 之后,我把模型名改成变量,跑 A/B 测试只需要改配置文件里的一行。后来生产环境上线,某个 Claude 的提供商凌晨挂了两次,我早上看日志才发现——应用自己扛过去了,一条告警都没触发。这就是路由层的意义,它不是锦上添花,是兜底。
五分钟跑通:安装、认证、第一次调用
Quickstart: Install, Authenticate, and Invoke in 5 Minutes
核心问题:最快需要几步才能让 LangChain 通过 OpenRouter 调通一个模型?
三步:装包、配密钥、写代码。
Step 1: 安装与认证
Python 用户:
pip install -U langchain-openrouter
export OPENROUTER_API_KEY="sk-or-..."
TypeScript 用户:
npm install @langchain/openrouter
-U 这个参数很重要。langchain-openrouter 目前处于 beta,迭代速度不慢,锁旧版本容易遇到兼容性问题。密钥在 openrouter.ai/settings/keys 生成,格式是 sk-or- 开头。ChatOpenRouter 会自动读取环境变量 OPENROUTER_API_KEY,当然你也可以在构造函数里显式传 api_key,如果你用 Vault 或者 AWS Secrets Manager 管理密钥的话。
Step 2: Python 调用
from langchain_openrouter import ChatOpenRouter
model = ChatOpenRouter(
model="anthropic/claude-sonnet-4.5",
temperature=0,
max_tokens=1024,
max_retries=2,
)
response = model.invoke("Summarize this support ticket in one sentence.")
print(response.content)
temperature、max_tokens、max_retries 的语义和 LangChain 里其他任何 chat model 完全一致。唯一的 OpenRouter 专属参数是 model,它必须是 provider/model 的 slug 格式。
想先确认密钥和模型是否可用,可以直接 curl:
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-4.5",
"messages": [{"role": "user", "content": "Summarize this support ticket in one sentence."}]
}'
端点返回的是标准 OpenAI Chat Completion 格式,ChatOpenRouter 本质上就是对这个端点的类型化封装。
Step 3: TypeScript 版本
import { ChatOpenRouter } from "@langchain/openrouter";
const model = new ChatOpenRouter("anthropic/claude-sonnet-4.5", {
temperature: 0.8,
});
const response = await model.invoke(
"Summarize this support ticket in one sentence.",
);
console.log(response.content);
TypeScript 的构造函数签名略有不同,第一个位置参数是模型名,第二个是配置对象。行为和 Python 版本一致。
一个小坑:TypeScript 版的
@langchain/openrouter版本号不一定和 Python 版同步。我遇到过 Python 包已经支持reasoning参数了,npm 包还没跟上。如果你需要用到后面讲到的进阶功能,先查一下 npm 上最新版的 release notes。
模型选择:provider/model 字符串是唯一的”开关”
Picking a Model: The Only Thing You Change Is a String
核心问题:怎么知道该填什么模型名?换了模型之后我的链条会坏吗?
模型目录在 openrouter.ai/models,这是唯一可信的来源。页面上会显示每个模型的提供商列表、每百万 token 的输入/输出价格、支持的功能(工具调用、JSON Schema、多模态等)。文档里的模型名只是示例,实际使用时务必以目录为准。
provider/model 这个格式的妙处在于零侵入切换。今天你的链条跑在 anthropic/claude-sonnet-4.5 上,明天产品经理说”试试 GPT-5-mini 看效果有没有差”,你只需要改一行:
model = ChatOpenRouter(
model="openai/gpt-5-mini", # 就改这里
temperature=0,
max_tokens=1024,
)
Prompts、工具定义、输出解析器、记忆模块——全部原地不动。这种设计在需要快速做模型 A/B 测试或者按成本动态降级的场景里特别实用。
LangChain 的 agent 还有个更短的写法:
from langchain.agents import create_agent
agent = create_agent(model="openrouter:anthropic/claude-sonnet-4.5")
openrouter: 前缀会让 create_agent 自动解析到 ChatOpenRouter 类。适合那种”只想快速搭个原型,不想显式 import”的场景。
流式输出:用户体验 vs 账单真相
Streaming: What It Costs and What It Doesn’t
核心问题:流式输出(streaming)会不会更贵?怎么在 LangChain 里实现?
不会。流式和非流式的 token 单价完全一样。你用 streaming 是为了让用户看到”字一个个蹦出来”的实时感,不是为了省钱。
LangChain 里用 stream_events(或异步版的 astream_events):
for event in model.stream_events(
"Explain provider routing in three sentences.",
version="v3"
):
if event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].text, end="", flush=True)
version="v3" 必须传,否则事件 schema 可能不对。异步版本:
async for event in model.astream_events(
"Explain provider routing in three sentences.",
version="v3"
):
if event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].text, end="", flush=True)
流结束后,最终聚合消息上会有 usage_metadata,包含完整的 token 计数。不需要为了拿用量信息再发一次非流式请求。
我在一个实时写作助手项目里用过这个。用户输入一个标题,模型开始流式生成大纲。起初我担心频繁的事件回调会拖慢前端,实际上 LangChain 的
stream_events开销很低,瓶颈在模型本身的 TTF(Time To First Token)。OpenRouter 的sort="latency"路由策略在这里能派上用场,后面会讲。
工具调用与结构化输出:从”返回文本”到”返回数据”
Tool Calling and Structured Output: From Text to Data
核心问题:OpenRouter 接进来的模型,能不能像原生 Claude 或 GPT-4 一样做工具调用和返回结构化 JSON?
能,而且比 base_url 覆盖时代干净得多。
工具调用(Tool Calling)
from pydantic import BaseModel, Field
class GetWeather(BaseModel):
"""Get the current weather for a city."""
city: str = Field(description="City name, e.g. 'Lisbon'")
model_with_tools = model.bind_tools([GetWeather], strict=True)
result = model_with_tools.invoke("What's the weather in Lisbon?")
print(result.tool_calls)
strict=True 会强制模型严格遵循 schema,不会瞎编参数名。这在生产环境里是必选项——我见过太多模型把 city 写成 location 或者 place,导致下游解析失败。
结构化输出(Structured Output)
class TicketSummary(BaseModel):
sentiment: str
priority: int
summary: str
structured = model.with_structured_output(TicketSummary, method="json_schema")
summary = structured.invoke("Customer is furious the export button is broken again.")
print(summary.priority, summary.summary)
默认方法是 function_calling,传 method="json_schema" 会用模型原生的 JSON Schema 约束能力(如果模型支持)。不是所有模型都支持 json_schema,目录页面上会标注每个模型的能力矩阵。
这里有个实际踩过的坑:有些模型声称支持工具调用,但
strict=True下表现不稳定。我的做法是,对关键路径一律设require_parameters: true(在openrouter_provider里),让 OpenRouter 只把请求路由到那些真正完整支持参数约束的提供商。这比在代码里写一堆 fallback 逻辑干净多了。
路由策略与故障转移:你的应用怎么自动”换供应商”
Provider Routing and Fallbacks: Automatic Failover Without Code
核心问题:如果某个模型提供商挂了,我的应用会崩吗?我能控制流量走向吗?
默认行为下,不会崩。OpenRouter 的路由层做了三件事:
-
价格负载均衡:在多个提供同一模型的服务商之间,按价格分配流量。 -
故障避让:最近 30 秒内出过故障的提供商会被自动跳过。 -
跨提供商故障转移:首选提供商挂了,请求自动漂到下一个,你的代码无感知。
这些都不需要你在 LangChain 里写重试逻辑。没完成的请求最终也不会计费。
自定义提供商偏好
通过 openrouter_provider 参数,你可以精细控制路由:
model = ChatOpenRouter(
model="anthropic/claude-sonnet-4.5",
openrouter_provider={
"order": ["Anthropic", "Google"],
"allow_fallbacks": True,
"data_collection": "deny",
"sort": "throughput",
},
)
-
order: 提供商优先级列表。 -
allow_fallbacks: 是否允许在首选提供商不可用时漂移到其他提供商。 -
sort:"throughput"按吞吐量排序,"latency"按延迟排序。对实时交互场景选latency,对批量处理选默认的价格排序。 -
data_collection: "deny": 拒绝把请求路由到那些会用你的 prompt 做训练的提供商。合规场景必开。 -
only/ignore: 显式白名单或黑名单某些提供商。 -
require_parameters: True: 只路由到完整支持你传入参数(如strict=True、自定义reasoning等)的提供商。
跨模型故障转移
提供商层面的故障转移是默认开启的。如果你想进一步——当 Claude 不可用时自动降级到 GPT-5-mini——可以用 route="fallback" 配合 model_kwargs 里的 models 数组:
model = ChatOpenRouter(
model="anthropic/claude-sonnet-4.5",
route="fallback",
model_kwargs={
"models": [
"anthropic/claude-sonnet-4.5",
"openai/gpt-5-mini",
"google/gemini-3-flash-preview",
],
},
)
models 不是构造函数的具名参数,所以必须塞进 model_kwargs,它会原样透传给 API。OpenRouter 会依次尝试:先找 Claude 的可用提供商,全挂了再试 GPT-5-mini,再试 Gemini。配合 openrouter_provider 里的 sort: {by, partition: "none"},还可以让排序逻辑横跨所有列出的模型,而不是每个模型内部单独排序。
这个功能在我眼里是 OpenRouter 最大的差异化价值。很多团队自己写故障转移逻辑,本质上是在重复造轮子——检测超时、维护提供商健康状态、处理不同模型的响应格式差异。OpenRouter 把这一层抽走了,你只付成功请求的钱,失败的尝试由平台承担成本。
进阶功能:推理预算、多模态、缓存与可观测性
Reasoning, Multimodal, Caching, and Observability
核心问题:除了基本的文本生成,OpenRouter 还支持哪些高级能力?在 LangChain 里怎么用?
推理预算(Reasoning)
model = ChatOpenRouter(
model="anthropic/claude-sonnet-4.5",
reasoning={"effort": "high", "summary": "auto"},
)
effort 从 xhigh 到 none 共六档。推理 token 的消耗会单独体现在 usage_metadata.output_token_details.reasoning 里,方便你核算”思考成本”和”输出成本”。
多模态输入
图片、音频、视频、PDF 都通过 LangChain 标准的 HumanMessage 内容块传入,和接其他多模态模型没有区别。具体支持哪些模态取决于你选的模型,目录页面有标注。
Prompt 缓存
在消息内容块里加 cache_control: {"type": "ephemeral"} 即可启用缓存。缓存命中情况会显示在 usage_metadata.input_token_details.cache_read 里。对于长上下文、重复性强的场景(比如每次请求都带一份很长的系统 prompt),这个优化能显著降低 token 成本。
可观测性
传 session_id(最长 256 字符)可以把相关请求归为一组;传 trace 对象可以附加自定义元数据。这些数据会转发到你配置的 Broadcast 目的地,不需要在应用层额外埋点。
这些全都是构造函数或请求级别的参数,不需要改动你的 chain 结构。
常见问题排查:四个高频坑
Common Problems and Fixes
核心问题:接入过程中最容易踩的坑是什么?
1. 版本兼容性
langchain-openrouter 是 beta 包,要求较新版本的 LangChain 核心库。不要从旧教程里复制版本号,直接去 PyPI 或 npm 看最新版,同步升级 LangChain。
2. 还在用 ChatOpenAI + base_url?
老版本 LangChain 没有专用包时,用 ChatOpenAI(base_url="https://openrouter.ai/api/v1") 加 OpenRouter 密钥是一种 workaround。现在有了 ChatOpenRouter,能直接访问路由策略、推理参数、结构化输出等原生功能。如果你的 LangChain 版本够新,建议迁移。
3. 模型总是返回相同答案
先检查 temperature 是不是设成了 0。再检查是不是启用了 prompt 缓存,命中缓存时会返回之前的结果。这不是缺陷,是预期行为。
4. 参数不被支持
不是所有模型都支持所有参数。不确定时,在 openrouter_provider 里设 require_parameters: true,让 OpenRouter 自动过滤掉不兼容的提供商,或者先去目录页面确认目标模型的能力矩阵。
FAQ:你可能还想问
Frequently Asked Questions
Q1: OpenRouter 和 LangChain 是什么关系?是竞争关系吗?
不是竞争,是组合。OpenRouter 是模型提供商和路由器,提供统一的 API 端点;LangChain 是编排框架,负责链条、代理、记忆、工具调用等逻辑。你在 LangChain 里把 OpenRouter 当作一个 model 组件来用。
Q2: 怎么在 LangChain 里接入 OpenRouter?
装包(langchain-openrouter 或 @langchain/openrouter),设 OPENROUTER_API_KEY,实例化 ChatOpenRouter(model="provider/model"),然后像任何 LangChain chat model 一样调用 .invoke()、.stream_events()、.bind_tools() 或 .with_structured_output()。
Q3: LangChain 支持通过 OpenRouter 做工具调用和结构化输出吗?
支持。用 model.bind_tools([...], strict=True) 做工具调用,用 model.with_structured_output(Schema, method="json_schema") 做结构化输出。这些是 ChatOpenRouter 包的一等公民方法,不需要 workaround。
Q4: 能在 LangChain 里配置提供商路由和故障转移吗?
可以。传 openrouter_provider={...} 控制提供商偏好和排序;传 model_kwargs={"models": [...]} 实现跨模型故障转移。提供商故障转移默认开启,无需额外配置。
Q5: 还有必要用 ChatOpenAI + base_url 的方式吗?
当前版本的 LangChain 下,专用 ChatOpenRouter 包是推荐路径。ChatOpenAI 覆盖 base_url 的方式只建议在你无法升级 LangChain 的旧项目里保留。
Q6: 可以用哪些模型?
OpenRouter 目录里的 400+ 模型都可以,通过 provider/model slug 引用。模型可用性和价格会动态变化,以 openrouter.ai/models 的实时数据为准。
Q7: 流式输出会额外收费吗?
不会。流式和非流式的 per-token 费率相同。
Q8: 请求失败了会扣费吗?
不会。OpenRouter 只对你最终成功的请求计费,故障转移过程中的失败尝试由平台承担。
实用摘要 / 操作清单
Practical Cheat Sheet
| 任务 | Python 代码片段 | 关键参数 |
|---|---|---|
| 基础调用 | ChatOpenRouter(model="anthropic/claude-sonnet-4.5") |
model, temperature, max_tokens |
| 流式输出 | model.stream_events(..., version="v3") |
version="v3" |
| 工具调用 | model.bind_tools([Schema], strict=True) |
strict=True |
| 结构化输出 | model.with_structured_output(Schema, method="json_schema") |
method="json_schema" |
| 提供商偏好 | openrouter_provider={"order": [...], "sort": "latency"} |
order, sort, data_collection |
| 跨模型降级 | model_kwargs={"models": [A, B, C]} + route="fallback" |
models 数组 |
| 推理预算 | reasoning={"effort": "high", "summary": "auto"} |
effort 档位 |
| 缓存控制 | 消息块内加 cache_control: {"type": "ephemeral"} |
cache_read 在 metadata 中 |
一页速览
One-Page Summary
-
装包: pip install -U langchain-openrouter(Python)或npm install @langchain/openrouter(TS) -
认证:环境变量 OPENROUTER_API_KEY,或构造函数传api_key -
选模型:去 openrouter.ai/models 查 slug,格式 provider/model -
切模型:改 model参数即可,prompts 和工具定义不动 -
流式: stream_events/astream_events,费率不变 -
工具/结构化输出: bind_tools(strict=True)+with_structured_output(method="json_schema") -
路由: openrouter_provider控制提供商偏好;model_kwargs["models"]实现跨模型故障转移 -
故障转移:默认开启,失败请求不扣费 -
进阶: reasoning控推理深度,消息块加cache_control省缓存,session_id+trace做可观测性 -
避坑:保持包和 LangChain 核心版本同步,不确定参数兼容性时开 require_parameters: true
最后说两句。OpenRouter 不是那种”有了它你就不需要关心底层模型”的魔法——你仍然需要理解每个模型的能力边界、token 限制、价格结构。但它确实把”接入”和”切换”这两件事的摩擦降到了最低。在 LangChain 的生态里,
ChatOpenRouter现在是一个一等公民,值得在新项目里直接采用,而不是先搭一套原生 SDK 再考虑迁移。beta 阶段更新快,锁好版本号,保持关注 release notes,基本不会踩大坑。

