LangChain base_url 配置

最后更新: 2026-07-16

ChatOpenAI 接受一个 base_url 参数。填 https://api.kkaiapi.com/v1,传一个 Key,你搭的每条链、每个 Agent、每个工具调用循环就都经一个端点路由,目录里任意模型按 ID 寻址,Python 和 JS/TS 都能这么配。

先说结论:base_url 写在 ChatOpenAI 构造器里

langchain-openai 包的 ChatOpenAI 直接接受 base_url、api_key、model 三个参数。base_url 填 https://api.kkaiapi.com/v1,客户端就会往网关发标准 /v1/chat/completions 请求,model 字段按纯字符串传递。往下游走的一切,LCEL 管线、bind_tools、with_structured_output、流式输出,都不受影响,因为全都走同一套协议。 后面事情看起来不对时,解析顺序很重要:显式的 base_url 参数优先,然后是 OPENAI_API_BASE 环境变量,再然后是底层 OpenAI SDK 读取的 OPENAI_BASE_URL。优先用构造器参数,它把覆盖范围精确限定在你构建的这一个客户端上。

import os
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="claude-sonnet-5",              # 任意目录模型 ID
    base_url="https://api.kkaiapi.com/v1",
    api_key=os.environ["KKAIAPI_KEY"],
)

print(llm.invoke("用一句话总结什么是 RAG").content)

为什么 LangChain 和网关搭配得好

LangChain(GitHub 上的 langchain-ai,约 14.1 万星标)把模型抽象在一个可运行接口后面,但这层抽象有一个实际的接缝:换厂商通常意味着换集成包,ChatOpenAI 换成 ChatAnthropic 换成 ChatGoogleGenerativeAI,各自有各自的依赖、鉴权、参数怪癖。一个多厂商网关把这条接缝挪出了你的代码。一个 ChatOpenAI 客户端,构造一次,通过改一个字符串就能服务 Claude、GPT、Gemini、DeepSeek 的 ID。 这在多模型应用里能收掉真实的复杂度。一条把难问题送去 claude-opus-4-8、批量摘要送去 deepseek-v4-flash 的路由链,是同一个类的两个实例对着同一个端点,不是依赖树里塞了两个厂商的 SDK。用 with_fallbacks() 搭的降级链也留在同一个客户端类型里,重试行为和错误形状都保持一致。 一个诚实的边界说明:上游文档把 ChatOpenAI 定位对着官方 OpenAI 接口面,厂商专属的响应扩展字段不会经它保留下来。一个提供标准 chat-completions 形状的网关正是能正常工作的场景;如果需要某个厂商专有的响应字段,那些调用用那个厂商的原生集成。

完整配置:Python 里的工具调用,JS/TS 写法

工具调用和结构化输出走的是同一套 chat-completions 请求,所以 bind_tools 和 with_structured_output 在网关上对任何支持工具调用的 ID 都能用,下面表格里的模型也一样。真正省事的模式是按角色实例化客户端,让一个快速模型承担走量的部分。 JavaScript 和 TypeScript 里,@langchain/openai 把同样的覆盖能力放深了一层:ChatOpenAI 构造器接受一个 configuration 对象,这个对象会交给底层的 OpenAI SDK 客户端,baseURL 就住在这里面。apiKey 和 model 在顶层。其余部分,工具调用、流式输出、LCEL 组合,和 Python 的行为一致。

import os
from pydantic import BaseModel
from langchain_openai import ChatOpenAI

GATEWAY = dict(
    base_url="https://api.kkaiapi.com/v1",
    api_key=os.environ["KKAIAPI_KEY"],
)

class Verdict(BaseModel):
    decision: str
    confidence: float

strong = ChatOpenAI(model="claude-sonnet-5", **GATEWAY)
fast = ChatOpenAI(model="deepseek-v4-flash", **GATEWAY)

judge = strong.with_structured_output(Verdict)
result = judge.invoke("这条查询每小时重复一次,要不要缓存?")

# 走量的工作交给快速实例:
summaries = fast.batch([f"总结:{doc}" for doc in docs])

按链条角色选模型

因为每个实例都是同一个类对着同一个端点,按角色选模型就变成一个真能跑的 A/B:固定链条,换一个角色的模型字符串,在按 Key 的用量视图里对着质量差异和成本差异看结果。

  • Agent 循环和裁判角色需要能撑住的最强推理:claude-sonnet-5 和 gpt-5.5 在多步工具调用上表现稳,一次判断错误的工具调用会拖累整个循环
  • 批量 map 步骤、先摘要再归约的流水线、富集环节是走量的工作;deepseek-v4-flash 或 glm-5.2 能在质量门槛不高的地方压平单文档成本
  • 整份文档直接塞进一个 prompt 而不是切块检索这种长上下文摄入,是 gemini-3.1-pro-preview 在链条里值得占一个位置的地方
  • Embedding 是独立的客户端类(OpenAIEmbeddings),有自己的 base URL 设置;kkaiapi 目前不提供 Embedding 模型,挪动聊天流量不会也不该动你的 Embedding 供应商,向量索引保持有效

主力模型价格

价格按 kkaiapi 中文区实时价格计算,单位 ¥/1M tokens(输入/输出)。

实时价格以 /pricing 为准
模型模型 IDkkaiapi 价格(输入/输出)适合链条角色
Claude Sonnet 5claude-sonnet-5¥3 / ¥15Agent 循环 / 裁判
GPT-5.5gpt-5.5¥4 / ¥25Agent 循环 / 裁判
DeepSeek V4 Flashdeepseek-v4-flash¥0.9 / ¥1.8批量 map / 摘要
GLM-5.2glm-5.2¥7.2 / ¥25.2批量富集
Gemini 3.1 Pro Previewgemini-3.1-pro-preview¥7 / ¥42长上下文摄入

翻车点与版本锁定

环境层的覆盖挪动的范围比你想的要大。OPENAI_API_BASE 会被进程里每一个 langchain-openai 客户端读取,包括 OpenAIEmbeddings。如果网关服务着你的聊天模型但不提供 Embedding,一个环境层的覆盖会在聊天照常工作的同时弄坏检索。用构造器参数限定覆盖范围,Embedding 客户端根本不会察觉。 包拆分带来的困惑。ChatOpenAI 活在 langchain-openai 包里(从 langchain_openai 导入),不在 langchain 核心包里,从 langchain.chat_models 导入的旧教程说的是拆分之前的版本。在依赖里明确锁定 langchain-openai,因为决定客户端行为的正是这个包的版本,主动去解析它,而不是任由某个元包顺带拖进来什么版本。 厂商专属参数不会被翻译。OpenAI 协议没定义的字段在这里就是不存在,请求带着不支持的参数会得到 400。把请求形状保持在标准面上,temperature、max_tokens、tools、response_format,厂商选择通过 model ID 来表达。 悄悄回落到默认端点。如果因为某层配置注入失败导致 base_url 是 None,客户端会开心地去连 api.openai.com,报出一个看起来像鉴权 bug 的 Key 错误。一个 401 让你意外时,先打印客户端的实际 base URL,再决定要不要轮换 Key。 JS:baseURL 放错层级。在 @langchain/openai 里,baseURL 属于 configuration 内部,不是构造器对象的顶层。放错位置会产生和上面 Python 情形一样的默认端点行为。

谁会经网关接入 LangChain

  • 跑多模型链条的团队:路由模式、裁判-工人拆分、map-reduce 摘要,想按角色选厂商又不想维护多套 SDK
  • 整合账单的生产应用:一个 Key、按 Key 的用量、按环境或功能归因花费,而不是几个厂商各自的仪表盘
  • 搭降级链的开发者,统一的错误处理,因为一个端点上跨 ID 的 with_fallbacks 让每种失败的形状保持一致
  • 想立刻用上新模型的原型开发者:新的目录 ID 出现的那一刻就能寻址,零依赖变更
  • 一些地区厂商注册流程麻烦的开发者;支付宝微信免翻墙充值,省掉逐个厂商的开户依赖

验证接入与排查首条链

先列出网关的模型;代码里的模型字符串必须和提供的某个 ID 精确匹配,版本后缀也算。 首次跑的失败有规律。一个你确信没错的 Key 报 401,通常意味着客户端实际解析到了另一个 base URL,常见原因是残留的 OPENAI_API_BASE 导出;继续排查前先打印实际生效的值。模型不存在的报错是 ID 打错字。400 报出一个意外参数,说明有厂商专属字段混进了请求。notebook 里一个卡住的流式调用,通常是你这边的代理或事件循环问题,不是端点问题。 链条跑通之后,kkaiapi 控制台会显示每次请求的模型、token 数和花费。对 map-reduce 和 Agent 负载来说,按 Key 的用量能回答审查时真正关心的问题:链条里哪个角色在花 token,强模型这份花费花得值不值。

curl -s https://api.kkaiapi.com/v1/models \
  -H "Authorization: Bearer $KKAIAPI_KEY" | head -50

常见问题

ChatOpenAI 能经一个端点跟 Claude 或 Gemini 模型对话吗?

能。ChatOpenAI 对 base_url 指定的地址说 chat-completions 协议,model 字段按纯字符串传递。网关提供的任何 ID 都能用,Claude、Gemini、DeepSeek、GLM 都包括在内,工具支持的 ID 上工具调用和结构化输出都完好。

Python 和 JS 里 base_url 分别填在哪?

Python:顶层构造器参数,ChatOpenAI(base_url=...)。JS/TS:放在 configuration 对象里,new ChatOpenAI({ configuration: { baseURL } }),因为这个对象会交给底层的 OpenAI SDK 客户端。

base_url 和环境变量之间的优先级是什么?

显式的 base_url 参数优先,然后是 OPENAI_API_BASE,再然后是底层 SDK 的 OPENAI_BASE_URL。优先用构造器参数:它把覆盖范围限定在一个客户端上,不会被环境里某处忘记清理的 export 劫持。

bind_tools 经网关能用吗?

能。工具定义和工具调用响应走的是标准 chat-completions 请求和响应,所以 bind_tools、Agent、with_structured_output 对端点提供的任何支持工具调用的 ID 都能用,Agent 任务选真正支持工具调用的 ID。

我的 Embedding 和向量库需要跟着改吗?

不需要。OpenAIEmbeddings 是独立的客户端,有自己的端点设置;kkaiapi 目前不提供 Embedding 模型。把覆盖范围限定在 ChatOpenAI 构造器上,你的 Embedding 供应商和已有向量完全不受影响,正因如此才要避免用环境层覆盖,那样会把两者一起挪走。

应该锁定哪个包的版本?

锁 langchain-openai 本身,它决定 ChatOpenAI 的行为。从 langchain_openai 导入,不走遗留的 langchain.chat_models 路径,升级时有意为之,因为客户端层面的变化是经这个包而不是 langchain 核心包发布的。