LangChain 从入门到实战(04):一套代码接所有模型——多 Provider 统一适配
前 3 篇我们不停在写ChatOpenAI(...),但真实项目里你几乎不会只用 OpenAI 一家——国产的 DeepSeek、智谱 GLM、通义千问,各有各的价码和长项。如果每个厂商都改一遍调用代码,维护就炸了。
这一篇做一件事:把「会变的模型差异」全部收进一份配置,用一套代码接上 OpenAI、DeepSeek、智谱、通义等任意模型,业务代码一行不改。你会得到一个「切换模型只改一处配置、其他代码完全不动」的最小方案——这正是 LangChain「对模型做接口抽象」最值钱的地方。
一、一个标准就够了:OpenAI 兼容协议
先立一个关键认知:OpenAI、DeepSeek、智谱 GLM、通义千问、Moonshot……绝大多数厂都实现了OpenAI 兼容协议(/chat/completions同款接口)。所以 LangChain 只需一个 OpenAI 适配器,再换算一下base_url就能接上所有厂商——「选模型」本质上是在选一个「OpenAI 兼容端点 + 一个模型名」。
先看 LangChain 提供的工厂函数init_chat_model,它能按名字直接拿到模型实例:
importosfromlangchain.chat_modelsimportinit_chat_modelfromdotenvimportload_dotenv load_dotenv()# 只靠一个字符串 + 供应商 ID,拿到模型实例llm=init_chat_model("gpt-4o",# 模型名model_provider="openai",# 供应商:openai / deepseek / mooonshot / ollama / ...api_key=os.getenv("API_KEY"),temperature=0,)注意:model_provider只能填 init_chat_model注册表里有的 id(如openai、deepseek)。像智谱、通义这种没内置注册的厂商,别硬填厂商名(会报 provider not found),统一走model_provider="openai"+ 自定义base_url即可,见下一节。
二、把「会变的」做成配置:一个极简 MultiProvider
正确姿势是「全部走 OpenAI 兼容 + 各家 base_url」——不依赖任何第三方集成包,最稳。把「会变的模型名/key/base_url」收进一张配置表,用函数按档返回模型:
importosfromlangchain.chat_modelsimportinit_chat_modelfromdotenvimportload_dotenvfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser load_dotenv()# 一份配置 = 一个厂商。切换只改这里(或环境变量 PROFILE)CONFIG={"openai":{"model":"gpt-4o","api_key":os.getenv("OPENAI_API_KEY"),"base_url":""},# 留空=用官方默认"deepseek":{"model":"deepseek-chat","api_key":os.getenv("DEEPSEEK_API_KEY"),"base_url":"https://api.deepseek.com/v1"},"zhipu":{"model":"glm-4-flash","api_key":os.getenv("ZHIPU_API_KEY"),"base_url":"https://open.bigmodel.cn/api/paas/v4/"},"tongyi":{"model":"qwen-plus","api_key":os.getenv("DASHSCOPE_API_KEY"),"base_url":"https://dashscope.aliyuncs.com/compatible-mode/v1"},}defmake_chain(profile:str):cfg=CONFIG[profile]# 关键:统一走 openai 适配器 + 各自 base_urlllm=init_chat_model(model=cfg["model"],model_provider="openai",api_key=cfg["api_key"],base_url=cfg["base_url"]orNone,temperature=0,)prompt=ChatPromptTemplate.from_messages([("system","你是严谨的 Java 后端工程师。"),("human","{question}"),])return(prompt|llm|StrOutputParser())if__name__=="__main__":chain=make_chain("deepseek")# 换厂商只需改这一行 / 或读环境变量 PROFILEprint(chain.invoke({"question":"什么是一致性哈希?"}))这样:改供应商,只改make_chain传的 profile(或.env里的PROFILE),业务调用chain.invoke()完全不动。这就是「面向模型抽象编程」,跟你在 Java 里用接口隔离 DAO 实现是同一套思想。
踩坑提醒(新手第一坑):base_url 别漏路径尾段——DeepSeek 是
/v1、DashScope 是/compatible-mode/v1,写错直接 404。
三、数 Token:别拍脑袋,用库或模型自算
切供应商后你最关心两件事:这一单要多少钱、响应快不快。
「数 token」不是字符数 × 常数就能估准的(中文、英文、代码的编码差异极大),不要自己拍脑袋乘系数。最省心的做法是让 LangChain 或模型自己数:
fromlangchain_core.messagesimportHumanMessage# ① 用模型自带的 tokenizer(最准)—— 不实际发请求messages=[HumanMessage(content="什么是 CAP 定理?")]tokens=llm.get_num_tokens_from_messages(messages)# 或 llm.get_num_tokens("文本")print("本段约",tokens,"token")schema提示:这段只发 msg 到模型自带 tokenizer 数数,不产生调用费用。做成本对比时按模型每千 token 单价 × 这个数即可。
四、流式(stream):首字更快,体验更顺
普通invoke要等整句出完才返回;流式能让字符一边生成一边吐:
forchunkinllm.stream("用一句话解释什么叫幂等"):print(chunk.content,end="",flush=True)想做 UI 打字机效果、或想在超长生成中途分段处理,流式都是产品必上的能力。整句话耗时没省,但「首字延迟」和「给用户的感知」天差地别。
五、工程化对照(Java 视角)
init_chat_model≈ Spring 的工厂:传一个字符串,返回对应适配器的实现——正是 Java「按配置实例化不同实现」的套路。- 配置表 CONFIG ≈ 多 profile 配置:把「哪家/模型/key/base_url」收进一张表,跟
application-{env}.yml是同一个思路:改配置不改代码。 - 流式 ≈ SSE / WebFlux:Java 后端做「打字机」版 Agent,本质是走流式响应 + 主动推送,而不是整包 JSON 一次返回。
小结
| 能力 | 解决了什么 |
|---|---|
| OpenAI 兼容协议 | 用一个适配器兼容绝大多数厂商 |
配置表CONFIG | 换厂商只改配置,业务代码不动 |
get_num_tokens_from_messages | 准确数 token,不拍脑袋 |
流式stream() | 首字更快,体验陡升 |
下一篇预告:模型能随意切了,但它手里没有你「上一轮说过什么」——第05篇《记忆与上下文:让对话真的有状态》我们把记忆接进链里,讲窗口记忆、摘要记忆、以及怎么让 Agent 跨轮次「不健忘」。我们下回见。