环境搭建 + 第一个链:一根竖线串起三节水管
Day01 认完门牌,今天真正跑起来。三件事:①装好环境并理解"装了哪几个包";②钻进 init_chat_model 源码,看"一行拿模型"是怎么按厂商前缀分发的;③写下第一根管道 prompt | model | parser,并认识三节管道各自的源码位置。今天出现的每个新面孔(ChatPromptTemplate、StrOutputParser、竖线 |),都是后面某一天的主角——今天先让水流通。
prompt | model | parser 就是三节水管——第一节把"一桶散装原料"(字典 {"topic": "猫"})压成"标准水流"(消息列表);第二节是"净水器"(模型),进消息出 AIMessage;第三节是"出水口滤网"(解析器),把 AIMessage 里的纯文字滤出来给你。竖线 | 就是水管接口,两节管口径对上(前一节的输出类型 = 后一节的输入类型)就能拧上。类比二:init_chat_model("openai:gpt-5.5") 像全国连锁的手机卡商店——你报"运营商:套餐"(厂商:型号),店员(源码里的 _init_chat_model_helper)看运营商是谁,就去对应柜台(partners 包)给你开卡;你甚至可以不说运营商,店员会根据号段(模型名前缀 gpt-/claude)猜出来。痛点:换个模型厂商,代码全重写?
openai.chat.completions.create(...),换 Claude 要学 anthropic.messages.create(...)——参数名不同(max_tokens 位置都不一样)、返回结构不同、流式写法不同。老板说"下周换个便宜模型试试",你就得把调用层重写一遍。而且"拼提示词→调模型→抠出回复文本"这三步样板代码,每个项目都要手写一遍。BaseChatModel(D05 主角),对你来说都长一样:.invoke(消息) → AIMessage;工厂层——init_chat_model("厂商:型号") 一行按字符串分发到对应厂商包。换模型 = 改一个字符串。至于"拼提示→调模型→抠文本"的样板,就用一根管道 prompt | model | parser 声明出来,写一次到处 invoke。装环境:三条命令 + 验证装了谁
# 1. 建一个干净的虚拟环境(uv 或 python -m venv 都行)
uv venv .venv && source .venv/bin/activate
# 2. 装主包 + 你要用的厂商小包(用哪家装哪家,见 D01 L04 的设计取舍)
uv pip install langchain langchain-openai # 用 OpenAI
# uv pip install langchain-anthropic # 用 Claude 则装这个
# uv pip install langchain-ollama # 本地模型用 Ollama
# 3. 配 API Key(厂商包会自动读对应环境变量)
export OPENAI_API_KEY="sk-..."
# 验证:看看"装一个包、来一家人"(D01 讲过的依赖链)
uv pip list | grep langchain
# langchain 1.x ← 新房(libs/langchain_v1)
# langchain-core 1.4.x ← 地基,被自动拖下来
# langchain-text-splitters 1.1.x ← 也被主包声明拖下来
# langchain-openai 1.x ← 你手动装的厂商包
pip list 里的四个包,正好对应 Day01 地图上的四栋楼:新房、地基、工具间、一间客房。没钱买 API?装 langchain-ollama + 本地跑个小模型,后面所有例子把 "openai:gpt-5.5" 换成 "ollama:qwen3" 一样能跟。教程重点是读源码,模型本身不重要。init_chat_model:一行拿模型的源码真相(真源码第 1、2 段)
它定义在新房 libs/langchain_v1/langchain/chat_models/base.py:210。docstring 长达两百多行(写满了各厂商映射表),真正的身体很短——裁掉文档后的核心(base.py:475-499):
# libs/langchain_v1/langchain/chat_models/base.py:475(裁剪,行为一致)
def init_chat_model(model=None, *, model_provider=None,
configurable_fields=None, config_prefix=None, **kwargs):
if model is not None and not isinstance(model, str):
raise TypeError("`model` must be a string (e.g., 'openai:gpt-5.5') ...")
if not model and not configurable_fields:
configurable_fields = ("model", "model_provider") # 没给模型名 → 变成"运行时可配置"
...
if not configurable_fields:
return _init_chat_model_helper( # ★普通情况:直接造模型
cast("str", model), model_provider=model_provider, **kwargs,
)
... # 可配置情况:返回 _ConfigurableModel(D18 再讲)
真正干活的是 _init_chat_model_helper(base.py:510-518):
# libs/langchain_v1/langchain/chat_models/base.py:510
def _init_chat_model_helper(model, *, model_provider=None, **kwargs):
model, model_provider = _parse_model(model, model_provider) # ① 拆 "openai:gpt-5.5"
creator_func = _get_chat_model_creator(model_provider) # ② 按厂商找"造模型的函数"
return creator_func(model=model, **kwargs) # ③ 造!返回 BaseChatModel
# 同文件 :521 起,_attempt_infer_model_provider:没写厂商前缀时按模型名猜
# model 以 "gpt-" / "o1" / "o3" / "chatgpt" 开头 → "openai"
# 以 "claude" 开头 → "anthropic" ……(一长串 if)
_parse_model把 "openai:gpt-5.5" 按第一个冒号拆成 ("gpt-5.5", "openai")。如果你只给了 "claude-opus-4-7" 没写厂商,就调 _attempt_infer_model_provider 按前缀猜(base.py:521)——猜是"尽力而为",所以官方推荐写全冒号形式。_get_chat_model_creator★按厂商名返回一个"创建函数",里面本质是 from langchain_openai import ChatOpenAI 这样的延迟 import——没装对应厂商包会在这里报 ImportError,提示你 pip install langchain-openai。这就是"用哪家装哪家"能成立的机关:主包不 import 任何厂商代码,直到你真的要用。creator_func(model=...)最终返回 ChatOpenAI(model="gpt-5.5", ...) 实例——它是 BaseChatModel 的子类(D05 主角),所以天生会 .invoke()/.stream()/.batch()。configurable_fields 分支如果你一个模型名都不给,它不报错,而是返回一个"运行时再决定用什么模型"的可配置壳(_ConfigurableModel)——invoke 时通过 config 传 {"configurable": {"model": "..."}} 现场切换。这是 D18 的内容,今天知道有这条岔路即可。ChatOpenAI(...) 类型更明确、IDE 补全更好;字符串工厂则让"换厂商"变成改配置而非改代码(可以放进环境变量/配置文件)。LangChain 两条路都留着:写死用类、想灵活用工厂。代价是字符串写错要到运行时才炸——所以源码里第一件事就是类型/格式检查(上面 :476 那个 TypeError)。第一个链:prompt | model | parser 跑起来
# first_chain.py —— 你的第一根管道
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个只会说大实话的喜剧演员。"),
("human", "讲一个关于{topic}的一句话笑话"),
])
model = init_chat_model("openai:gpt-5.5", temperature=0.7)
parser = StrOutputParser()
chain = prompt | model | parser # ★一根竖线拧三节水管
print(chain.invoke({"topic": "猫"})) # → '我家猫把我当铲屎官培训基地的实习生。'
chain = prompt | model | parser 这行执行时并没有调模型——它只是把三节水管拧好,做出一根新水管(RunnableSequence 对象)。真正放水是 chain.invoke({...}) 那一刻。"先声明结构、后执行",这是 LCEL 的核心气质,D03/D04 会反复见到。三节管道各自的源码位置(真源码第 3、4 段)
第一节 ChatPromptTemplate.from_messages(libs/core/langchain_core/prompts/chat.py:1124-1172,类定义在 chat.py:794):
# libs/core/langchain_core/prompts/chat.py:1124(裁剪 docstring)
@classmethod
def from_messages(cls, messages, template_format="f-string") -> ChatPromptTemplate:
"""...支持的消息写法:
3. 2-tuple of (message type, template) 例如 ('human', '{user_input}')
5. 纯字符串 = ('human', template) 的简写
"""
return cls(messages, template_format=template_format) # 就一行:交给构造器
("system", "...")、("human", "...{topic}") 这种 2 元组写法是官方正道;{topic} 是 f-string 风格占位符(template_format="f-string" 默认值)。它的 invoke 继承自 BasePromptTemplate.invoke(prompts/base.py:210)——D04 走 invoke 旅程时会正式路过。第三节 StrOutputParser,全类不到 70 行(libs/core/langchain_core/output_parsers/string.py:8,parse 在 string.py:61-63):
# libs/core/langchain_core/output_parsers/string.py:8
class StrOutputParser(BaseTransformOutputParser[str]):
"""Extract text content from model outputs as a string. ...
Supports streaming, yielding text chunks as they're generated."""
@override
def parse(self, text: str) -> str: # string.py:61
"""Returns the input text with no changes."""
return text # ★核心逻辑就这一行!
parse 只是原样返回?对——真正的功夫在它继承的基类里:基类负责"从 AIMessage 里抠出 .content 文本",再把文本交给 parse 做最后加工。StrOutputParser 的加工 = 什么都不做。子类只写差异,共性全在基类——这个模式 D05 的 BaseChatModel 会玩到极致。BaseTransformOutputParser名字里的 Transform 表示它支持流式:模型一块块吐字时,它能一块块解析转发,不用等全文。所以 chain.stream() 时最后一节不会卡住水流。第二节 model 呢?ChatOpenAI 住在客房 libs/partners/openai/,但它 95% 的行为(invoke/缓存/回调/流式兜底)都继承自地基的 BaseChatModel(libs/core/langchain_core/language_models/chat_models.py:272)——那是 D05 一整天的主菜。| 串起来、都能单独 .invoke()——因为它们都继承了同一个基类 Runnable。水管接口的"标准口径"就是 Runnable 协议。明天我们就去读这个协议本身。串起来 + 今日小结
chain.invoke({"topic": "猫"}) 的三段变身:① prompt 收到
{"topic": "猫"},吐出消息列表 → [SystemMessage("你是一个只会说大实话的喜剧演员。"), HumanMessage("讲一个关于猫的一句话笑话")];② model 收到消息列表,请求 OpenAI,吐出 →
AIMessage(content="我家猫把我当铲屎官培训基地的实习生。", response_metadata={"model_name": "gpt-5.5", ...});③ parser 收到 AIMessage,抠出 content,吐出 →
'我家猫把我当铲屎官培训基地的实习生。'(纯 str)。换成
chain.stream({"topic": "猫"}),第③步会一小段一小段吐:'我家'、'猫把我当'……👶 小白:我不写 prompt 模板,直接 model.invoke("讲个笑话") 传字符串,为什么也能跑?
👨🏫 老师:因为 BaseChatModel.invoke 收到输入后第一件事是 self._convert_input(input)(D05 会看到,chat_models.py:463 的 invoke 里就有)——字符串会被自动包成 HumanMessage("讲个笑话")。方便是方便,但真实应用里提示词几乎总有"固定部分 + 变量部分",所以模板还是标配。另外注意:invoke 传的是一份输入;想一次发多份用 batch([...]),想逐字出用 stream(...)——这三兄弟明天正式介绍。
🧠 今天你应该能回答
- 装 langchain + langchain-openai 后环境里有哪四个家族包?(主包/core/text-splitters/openai 客房)
init_chat_model("openai:gpt-5.5")内部三步?(拆字符串 → 按厂商找创建函数(延迟 import)→ 造出 BaseChatModel 子类)- 没写厂商前缀会怎样?(按模型名前缀猜,
gpt-→openai、claude→anthropic;建议写全) chain = prompt | model | parser这行调模型了吗?(没有!只是拧水管,invoke 才放水)- StrOutputParser.parse 的核心逻辑?(
return text原样返回,抠 content 的活在基类) - 三节管道为什么能用 | 串起来?(都实现了 Runnable 协议——明天的主角)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/langchain
# 1. init_chat_model 的身体与帮手
sed -n '475,500p' libs/langchain_v1/langchain/chat_models/base.py # 主体
sed -n '510,545p' libs/langchain_v1/langchain/chat_models/base.py # helper + 前缀推断
# 2. 第一节和第三节管道
sed -n '1124,1175p' libs/core/langchain_core/prompts/chat.py # from_messages
sed -n '1,65p' libs/core/langchain_core/output_parsers/string.py # StrOutputParser 全文
# 3. 跑真链(装好环境后)
python first_chain.py
# 没 API key 就把模型行换成 init_chat_model("ollama:qwen3")(需本地 ollama)
| 是全框架最大的魔法。明天打开 6713 行的 runnables/base.py:Runnable 基类的三件套(invoke/stream/batch)、__or__ 怎么把竖线变成 RunnableSequence、以及 dict/函数为什么也能混进管道(coerce_to_runnable)。