主题
LangChain v1 包分层与运行时模型切换
本文基于 2026 年 LangChain v1 架构整理,覆盖:包分层与依赖关系、各包常用子包、
init_chat_model自适应路由、thread_id与会话状态、运行时切换模型 /base_url/api_key的完整机制与坑位。
目录
- 一、整体架构:为什么 langchain 与 langchain-core 平级
- 二、各包常用子包清单
- 三、init_chat_model:自适应模型工厂
- 四、运行时切换模型
- 五、thread_id:会话状态的真正主键
- 六、运行时切换 base_url 与 api_key
- 七、优雅换模型:预设配置模式
- 八、常见坑位汇总
- 九、完整最小可运行示例
- 十、一句话总结
一、整体架构:为什么 langchain 与 langchain-core 平级
LangChain v1 把代码库拆成了若干独立发版的 Python 包。langchain 与 langchain-core 在 site-packages 下是两个平级目录,不是历史遗留,而是刻意的分层设计。
1.1 分层图
mermaid
flowchart TD
App["你的业务代码"]
LC["langchain<br/>实现层:agents / chains / retrieval / middleware"]
CORE["langchain-core<br/>抽象层:BaseChatModel / BaseTool / BaseMessage / Runnable / LCEL"]
COMM["langchain-community<br/>第三方集成"]
PARTNER["langchain-openai / langchain-anthropic / ...<br/>官方独立集成包"]
LG["langgraph<br/>图运行时:checkpointer / Command / interrupt"]
App --> LC
App --> PARTNER
App --> LG
LC --> CORE
COMM --> CORE
PARTNER --> CORE
LC --> LG1.2 拆包的三个动机
| 动机 | 说明 |
|---|---|
| 版本解耦 | langchain-core 只放稳定的抽象接口,几乎不依赖第三方 SDK;langchain 放会频繁变动的编排逻辑。两者独立发版,避免改了 chain 就逼着所有集成包跟着升版本。 |
| 集成包轻量 | langchain-openai、langchain-anthropic 等只依赖 langchain-core,不依赖 langchain。只想用一个 ChatModel 做单次调用时,不会把整套 agent/chain 编排代码拖进来。 |
| 契约统一 | 所有"模型 / 工具 / 消息"在 core 里都是同一套基类,换厂商不需要改业务代码。 |
1.3 安装与 import 的关系
bash
pip install langchain # 自动带上 langchain-core
pip install langchain-openai # 单独装一个官方集成langchain-core是所有包的公共底座。langchain是默认推荐的"全家桶入口"。- 集成包按需安装,不装就不会在运行时被 import。
二、各包常用子包清单
2.1 langchain-core:写自定义组件时碰这一层
| 子包 | 用途 | 典型导出 |
|---|---|---|
langchain_core.messages | 消息抽象 | HumanMessage、AIMessage、SystemMessage、ToolMessage |
langchain_core.language_models | 模型基类 | BaseChatModel、BaseLLM |
langchain_core.tools | 工具定义 | BaseTool、@tool 装饰器、ToolException |
langchain_core.prompts | 提示词模板 | ChatPromptTemplate、MessagesPlaceholder |
langchain_core.output_parsers | 输出解析 | StrOutputParser、PydanticOutputParser |
langchain_core.runnables | LCEL 执行协议 | Runnable、RunnableLambda、RunnableParallel |
langchain_core.documents | 检索文档 | Document |
langchain_core.retrievers | 检索器抽象 | BaseRetriever |
注意:
@tool装饰器的真身在langchain_core.tools。from langchain.tools import tool是 v1 提供的便捷 re-export,两者等价。
2.2 langchain:把 core 零件拼起来的高层 API
| 子包 | 用途 | 典型导出 |
|---|---|---|
langchain.agents | Agent 组装(v1 新入口) | create_agent(取代旧的 langgraph.prebuilt.create_react_agent) |
langchain.agents.middleware | Agent 中间件 | HumanInTheLoopMiddleware、SummarizationMiddleware、ToolCallingMiddleware |
langchain.agents.structured_output | 结构化输出策略 | ToolStrategy |
langchain.messages | 消息类型 re-export | 等价于 langchain_core.messages |
langchain.tools | 工具工具集 re-export | @tool、内置搜索 / 计算器 |
langchain.chains | LCEL 链(legacy 兼容) | RetrievalQA 等老代码 |
langchain.retrievers | 检索器具体实现 | 多路召回、重排包装器 |
langchain.chat_models | 模型工厂 | init_chat_model |
2.3 独立集成包(按厂商)
| 包 | 用途 |
|---|---|
langchain-openai | OpenAI、Azure OpenAI、所有 OpenAI 兼容网关 |
langchain-anthropic | Claude 系列 |
langchain-google-genai / langchain-google-vertexai | Gemini |
langchain-community | 大量第三方集成(社区维护) |
2.4 langgraph:图运行时
| 子包 | 用途 |
|---|---|
langgraph.checkpoint.memory | InMemorySaver(本地调试) |
langgraph.checkpoint.postgres | PostgresSaver(生产持久化) |
langgraph.types | Command、interrupt |
langgraph.prebuilt | 旧版 create_react_agent(兼容用) |
三、init_chat_model:自适应模型工厂
init_chat_model 位于 langchain.chat_models,是 v1 推荐的统一模型入口。
3.1 它不是什么
- 它不是一个新的模型抽象层。
- 它不是优先选 OpenAI。
- 它不是绕过集成包。
3.2 它是什么
一个按字符串路由到具体集成类的工厂。返回值类型完全由字符串决定。
python
from langchain.chat_models import init_chat_model
# 显式 provider:model —— 最稳,推荐
init_chat_model("openai:gpt-5.5") # -> langchain_openai.ChatOpenAI 实例
init_chat_model("anthropic:claude-...") # -> langchain_anthropic.ChatAnthropic 实例
init_chat_model("google_genai:gemini-...")# -> langchain_google_genai.ChatGoogleGenerativeAI
init_chat_model("bedrock:anthropic.claude-...")
init_chat_model("openrouter:auto")3.3 省略 provider 时的启发式猜测
| 模型名前缀 | 猜测 provider |
|---|---|
gpt-*、o1、o3、o4 | openai |
claude-* | anthropic |
gemini* | google_genai |
command-* | cohere |
amazon.* | bedrock |
mistral* | mistralai |
python
init_chat_model("gpt-4o-mini") # 猜成 openai
init_chat_model("claude-3-5-...") # 猜成 anthropic猜测失败或歧义时,用 model_provider= 显式覆盖:
python
init_chat_model("deepseek-chat", model_provider="deepseek")3.4 关键事实
- 返回值就是对应集成类的实例。
init_chat_model("openai:gpt-5.5")等价于ChatOpenAI(model="gpt-5.5"),对象类型完全一样。 - 懒加载、不自动装包。字符串解析到哪个 provider,运行时才
import那个集成包;没装就 ImportError,提示你pip install langchain-openai。 - 支持任意初始化参数透传:
temperature、max_tokens、base_url、api_key、model_kwargs等。
3.5 什么时候用它 vs 直接 new ChatOpenAI
| 场景 | 推荐 |
|---|---|
| 写死用 OpenAI,要 IDE 补全、类型提示 | 直接 from langchain_openai import ChatOpenAI |
| 模型名来自 yaml / 环境变量 / 数据库 | init_chat_model(os.environ["MODEL"]) |
| 需要在 config 里运行时切 provider | init_chat_model(..., configurable_fields=[...]) |
| 内部封装公司模型路由服务 | init_chat_model("our:xxx", model_provider="our") |
四、运行时切换模型
4.1 前提条件(三件套,缺一不可)
python
model = init_chat_model(
"openai:gpt-4o-mini", # ① 用 init_chat_model 构造
configurable_fields=["model", "model_provider"], # ② 注册要覆盖的字段
config_prefix="llm", # ③ 给这组字段起前缀
)4.2 写法对比
python
# ❌ 写法 A:写死模型 —— config 里塞什么都不会换
agent = create_agent(
model=ChatOpenAI(model="gpt-4o-mini"),
tools=[...],
checkpointer=InMemorySaver(),
)
# ✅ 写法 B:可配置模型 —— invoke 时在 config 里覆盖
agent = create_agent(
model=init_chat_model(
"openai:gpt-4o-mini",
configurable_fields=["model", "model_provider", "temperature"],
config_prefix="llm",
),
tools=[...],
checkpointer=InMemorySaver(),
)
agent.invoke(
req,
config={"configurable": {
"thread_id": "user-1",
"llm.model": "claude-sonnet-4-6", # 这一轮换 Claude
"llm.model_provider": "anthropic",
}},
)4.3 本质
agent.invoke 本身不神奇。是底层 Runnable 在被构造时,通过 config_prefix 给自己注册了一组"可被 config 覆盖的槽位"。invoke 时 LangGraph 把 config["configurable"] 里的同名值填进这些槽位,再实例化真正的模型对象。
五、thread_id:会话状态的真正主键
5.1 核心概念
模型是无状态的。 它每一轮只做一件事:接收一组 messages,吐出一个 response,自己不记得任何事。
会话记忆完全由 checkpointer 提供。
┌─────────────────────────────────────────────────────┐
│ checkpointer(InMemorySaver / PostgresSaver) │
│ │
│ key: thread_id = "user-123" │
│ └─ State { │
│ messages: [Human(...), AI(...), Tool(...)] │
│ interrupt: None / {pending tool call} │
│ ...middleware 状态 │
│ } │
└──────────────────┬──────────────────────────────────┘
│ 每次 invoke 时把 State 读出来
▼
┌─────────────────────────────────────────────────────┐
│ Model(无状态推理单元) │
│ 这一轮用 gpt-4o-mini,下一轮换成 claude, │
│ 它只是"读 messages 历史 -> 生成下一条" │
└─────────────────────────────────────────────────────┘5.2 一句话比喻
thread_id 是"工单",model 是"接电话的人"。 工单跟着 thread_id 走,接电话的人可以随时换,历史不丢。
5.3 同 thread_id 内换模型的完整例子
python
agent = create_agent(
model=init_chat_model(
"openai:gpt-4o-mini",
configurable_fields=["model", "model_provider"],
config_prefix="llm",
),
tools=[search_tool],
checkpointer=InMemorySaver(),
)
cfg = {"configurable": {"thread_id": "user-7"}}
# 第 1 轮:默认模型
agent.invoke({"messages": [HumanMessage("我叫小明,住在上海")]}, config=cfg)
# 第 2 轮:同一个 thread_id,中途换 Claude
agent.invoke(
{"messages": [HumanMessage("我叫什么?住哪?")]},
config={"configurable": {
"thread_id": "user-7", # ← 同一个会话
"llm.model": "claude-sonnet-4-6", # ← 换大脑
"llm.model_provider": "anthropic",
}},
)
# Claude 能正确答"小明 / 上海",因为 messages 历史从 checkpointer 取出5.4 多用户隔离
- 多用户隔离 = 多 thread_id,不是多模型。
- 每个用户一个
thread_id = f"user_{uid}"。 - 模型可以全局共享,也可以按用户在 config 里覆盖。
六、运行时切换 base_url 与 api_key
6.1 可以,且是 init_chat_model 最值钱的场景
configurable_fields 不仅能注册 model / model_provider,也能注册 base_url 和 api_key。
python
model = init_chat_model(
"openai:gpt-4o-mini",
configurable_fields=[
"model", "model_provider",
"base_url", "api_key", "temperature",
],
config_prefix="llm",
)
agent = create_agent(model=model, tools=[...], checkpointer=InMemorySaver())
# 同一次会话里,临时切到自建中转
agent.invoke(
{"messages": [HumanMessage("hi")]},
config={"configurable": {
"thread_id": "user-1",
"llm.base_url": "https://one-api.your.com/v1",
"llm.api_key": os.environ["RELAY_KEY"],
"llm.model": "deepseek-chat",
# model_provider 保持 openai,因为 one-api 走 OpenAI 兼容协议
}},
)6.2 字段名的两个坑
| 坑 | 说明 |
|---|---|
| 统一别名 vs 底层真名 | ChatOpenAI 内部字段叫 openai_api_key / openai_api_base,但带 alias 叫 api_key / base_url。在 init_chat_model 这一层统一用 api_key / base_url 即可,它负责翻译。 |
| 跨 provider 切时语义跟着走 | 切到 model_provider="anthropic",你塞的 api_key 必须是 Anthropic 的 key,base_url 要指向 Anthropic 兼容端点;不能把 OpenAI 中转的 key 塞进 Anthropic provider 还用。 |
6.3 实战推荐姿势
方案 A:所有端点都是 OpenAI 兼容协议(推荐)
one-api、new-api、DeepSeek、Moonshot、本地 vLLM、Ollama 的 /v1 端口、Xinference,全部兼容 OpenAI 协议。
python
model = init_chat_model(
"gpt-4o-mini",
model_provider="openai",
configurable_fields=["model", "base_url", "api_key"],
config_prefix="llm",
)
# model_provider 永远是 "openai",不动
# 切流量 / 故障转移 / 灰度到本地模型,只改三个字符串方案 B:真要跨厂商(OpenAI ↔ Anthropic ↔ Gemini)
四个一起切,否则会出现"model 换成 claude 但 key 还是 OpenAI 的"这种 401:
python
"llm.model_provider": "anthropic",
"llm.model": "claude-sonnet-4-6",
"llm.api_key": os.environ["ANTHROPIC_API_KEY"],
"llm.base_url": None, # 用官方默认,或指向你的 Anthropic 代理6.4 优先级与安全
运行时 config 传值
> 构造 init_chat_model(api_key=...) 传值
> OPENAI_API_KEY 环境变量- config 里的
api_key只在当次 invoke 生效,不持久化到 checkpointer。下一轮不传就回退到默认值或环境变量。 - 不要硬编码密钥。通过 config 注入也要走环境变量 / Secret Manager。
- LangChain debug log 可能把 configurable 字段打出来,生产环境注意日志级别。
七、优雅换模型:预设配置模式
7.1 核心思想
业务调用点只写
thread_id和"预设名",所有base_url/api_key/model外置。
config = {"configurable": {"thread_id": "1"}} 这种裸写法一旦要切模型,每次 invoke 都得手写一串 llm.model / llm.base_url / llm.api_key,散落全代码库。优雅的姿势是把"换模型"收敛成"换一个配置名"。
7.2 方案 1:预设字典 + 一行构造器(最常用)
python
import os
from langchain_core.messages import HumanMessage
# ---------- 预设:模型路由表 ----------
PRESETS: dict[str, dict] = {
"official": {
"llm.model": "gpt-4o-mini",
"llm.model_provider": "openai",
# base_url / api_key 留空 -> 走默认与 OPENAI_API_KEY
},
"relay": {
"llm.model": "deepseek-chat",
"llm.model_provider": "openai", # 走 OpenAI 兼容协议
"llm.base_url": os.environ["RELAY_BASE_URL"],
"llm.api_key": os.environ["RELAY_API_KEY"],
},
"local-ollama": {
"llm.model": "qwen2.5:7b",
"llm.model_provider": "openai", # Ollama 也兼容 OpenAI 协议
"llm.base_url": "http://localhost:11434/v1",
"llm.api_key": "ollama",
},
"claude": {
"llm.model": "claude-sonnet-4-6",
"llm.model_provider": "anthropic",
"llm.api_key": os.environ["ANTHROPIC_API_KEY"],
},
}
def cfg(thread_id: str, preset: str = "official") -> dict:
"""一行拼出 config:thread_id + 预设字段合并"""
return {"configurable": {"thread_id": thread_id, **PRESETS[preset]}}
# ---------- 业务调用点:干净到只有两行 ----------
agent.invoke(
{"messages": [HumanMessage("你好")]},
config=cfg("1", preset="relay"), # 换预设 = 换模型
)
# 同一会话下一轮想切回官方:
agent.invoke(
{"messages": [HumanMessage("继续")]},
config=cfg("1", preset="official"), # thread_id 不变,历史连续
)7.3 方案 2:dataclass 化(类型安全、IDE 补全)
预设多了以后,字符串 key 容易拼错。用 dataclass 包一层:
python
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class ModelPreset:
model: str
model_provider: str = "openai"
base_url: str | None = None
api_key: str | None = None
PRESETS = {
"official": ModelPreset(model="gpt-4o-mini"),
"relay": ModelPreset(
model="deepseek-chat",
base_url=os.environ["RELAY_BASE_URL"],
api_key=os.environ["RELAY_API_KEY"],
),
"local": ModelPreset(
model="qwen2.5:7b",
base_url="http://localhost:11434/v1",
api_key="ollama",
),
}
def cfg(thread_id: str, preset: str = "official") -> dict:
p = PRESETS[preset]
c: dict = {"thread_id": thread_id}
if p.model: c["llm.model"] = p.model
if p.model_provider: c["llm.model_provider"] = p.model_provider
if p.base_url: c["llm.base_url"] = p.base_url
if p.api_key: c["llm.api_key"] = p.api_key
return {"configurable": c}好处:预设可以用 replace(PRESETS["relay"], model="deepseek-v3") 临时派生,不用复制字典。
7.4 方案 3:预设外置到 yaml(生产姿势)
预设一多就别放代码里,丢到 config/models.yaml:
yaml
# config/models.yaml
official:
model: gpt-4o-mini
model_provider: openai
relay:
model: deepseek-chat
model_provider: openai
base_url: ${RELAY_BASE_URL} # 启动时从 .env 插值
api_key: ${RELAY_API_KEY}
local:
model: qwen2.5:7b
model_provider: openai
base_url: http://localhost:11434/v1
api_key: ollama加载器:
python
import os, yaml
from string import Template
def load_presets(path: str = "config/models.yaml") -> dict:
raw = Template(open(path).read()).safe_substitute(os.environ)
data = yaml.safe_load(raw)
# 把 key 加 prefix
return {
name: {f"llm.{k}": v for k, v in fields.items() if v is not None}
for name, fields in data.items()
}
PRESETS = load_presets()
def cfg(thread_id: str, preset: str = "official") -> dict:
return {"configurable": {"thread_id": thread_id, **PRESETS[preset]}}加新预设 = 改一行 yaml,零代码改动。
7.5 让它"真优雅"的五个细节
- 字段为 None 就不要写进 config。让底层回退到
init_chat_model构造时的默认值或环境变量,避免把base_url=None这种垃圾塞进去。 - prefix 必须对齐。
create_agent里config_prefix="llm",预设 key 就必须是llm.model、llm.base_url。集中在cfg()函数里拼装,别在业务代码里手写字符串。 - thread_id 永远是第一个参数。预设是"可选覆盖",会话身份才是一等公民。
- 不要每次都换预设。同一轮对话建议固定一个预设,切换发生在新一轮而不是 HITL 恢复那一刻,否则工具调用格式 / reasoning 字段可能串味。
- 密钥只走环境变量。yaml 里用
${VAR}插值,git 里看不到真 key。
7.6 最终业务调用点长这样
python
# 99% 的业务代码里你只会看到这一行:
agent.invoke(req, config=cfg("user-123", preset="relay"))
# 想切模型?改 preset 名字即可,其他代码一行不动
agent.invoke(req, config=cfg("user-123", preset="official"))
agent.invoke(req, config=cfg("user-123", preset="local"))优雅的本质:换模型是一个配置名,不是一次代码改动。
八、常见坑位汇总
| 坑 | 现象 | 正确做法 |
|---|---|---|
直接 ChatOpenAI(...) 构造 | config 里塞 llm.model 无效 | 换成 init_chat_model + configurable_fields |
| 切模型但新开 thread_id | 历史丢失,会话从头开始 | 同一个 thread_id,只在 config 里换 model |
| 跨 provider 切时只换 model 不换 key | 401 Unauthorized | model / model_provider / api_key / base_url 一起切 |
| OpenAI 兼容中转却换 provider | 协议不匹配报错 | 保持 model_provider="openai",只换 url/key/model |
| HITL 中断恢复那一刻换模型 | 工具调用格式不一致 | 切换发生在"新一轮",不要在 resume 那次 invoke 换 |
| 频繁切换 provider | reasoning_content 字段差异导致跨轮不稳定 | 同一轮对话内模型保持一致 |
| 把密钥写进 config 常量 | 进日志 / 进 git | 走环境变量 / Secret Manager |
from langchain.tools import tool 疑惑 | 为什么不是从 core 导入 | v1 的便捷 re-export,等价 |
以为 init_chat_model 会自动装包 | ImportError | 自己 pip install langchain-xxx |
九、完整最小可运行示例
python
# pip install langchain langchain-openai langchain-anthropic
import os
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
# ---------- 1. 定义工具 ----------
@tool
def write_file(path: str, content: str) -> str:
"""写文件到磁盘"""
with open(path, "w") as f:
f.write(content)
return f"written {path}"
# ---------- 2. 构造可配置模型 ----------
model = init_chat_model(
"openai:gpt-4o-mini",
configurable_fields=[
"model", "model_provider",
"base_url", "api_key", "temperature",
],
config_prefix="llm",
)
# ---------- 3. 组装 Agent ----------
agent = create_agent(
model=model,
tools=[write_file],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"write_file": True})],
checkpointer=InMemorySaver(),
)
# ---------- 4. 第 1 轮:默认模型 ----------
cfg = {"configurable": {"thread_id": "demo-1"}}
for chunk in agent.stream(
{"messages": [HumanMessage("把 hello 写入 /tmp/a.txt")]},
config=cfg,
stream_mode="values",
):
pass # 观察流式输出
# ---------- 5. 人工审批后恢复 ----------
agent.invoke(Command(resume={"approve": True}), config=cfg)
# ---------- 6. 第 2 轮:同一个 thread_id,换中转 + 换模型 ----------
agent.invoke(
{"messages": [HumanMessage("再帮我写 /tmp/b.txt,内容是 world")]},
config={"configurable": {
"thread_id": "demo-1", # 同一个会话
"llm.base_url": "https://one-api.your.com/v1",
"llm.api_key": os.environ["RELAY_KEY"],
"llm.model": "deepseek-chat",
# model_provider 保持 openai,因为走 OpenAI 兼容协议
}},
)十、一句话总结
core 是"接口",langchain 是"默认实现 + 编排",langgraph 是"运行时",集成包是"驱动"。
日常写业务代码时,这四个方向的 import 会同时出现,它们平级共存就是因为职责不同、版本独立。
thread_id 是"工单",model 是"接电话的人"。工单跟着 thread_id 走,接电话的人(包括厂商、端点、密钥)都可以随时换,历史不丢。