LangChain 入门 + LCEL

W4 · LCEL:用管道把零件串成流水线

关键词:LCEL / Runnable / 声明式 / 零件库 + 组装流水线 前置:W1 llm_client、W2 fc_loop、W3 MemoryManager(全是手撸) 收尾预告:W5 LangGraph —— node/edge 的雏形


翻车前的自白

W1 你写 llm_client,每次调完模型都要手动抠 resp.choices[0].message.content; W2 你写 fc_loop,自己拼 tools schema、解析 tool_calls、再写循环; W3 你写 MemoryManager,短期窗口、长期 Chroma、滚动压缩,全自己管。

回头看,这三周你一直在做同一件事:手动接线

每加一个功能,就多一串「把 A 的输出塞进 B 的输入」的胶水代码。功能一多,胶水比功能还多——这就是手写 Agent 的隐性成本。

W4 要解决的,就是这团胶水。LCEL 不是新概念,是把你 W1-W3 的手动接线,标准化成一条「传送带」。


零、LangChain 是什么?

如果你之前只用手写 requestsopenai SDK 直连大模型,第一次听到「LangChain」大概率会懵:这玩意儿和「直接调 API」差在哪?

一句话:LangChain 是一套给 LLM 应用用的「乐高积木 + 说明书」。

先想清楚:不用框架,你怎么调大模型?

裸调一个模型,你其实要手写一堆「杂活」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 没有框架时,一次问答你要操心的事
import openai

# 1. 自己拼消息格式
messages = [
    {"role": "system", "content": "你是一个简洁的助手"},
    {"role": "user",   "content": "什么是 LCEL?"},
]

# 2. 自己发起请求
resp = openai.chat.completions.create(
    model="xxx",
    messages=messages,
)

# 3. 自己从嵌套结构里抠出文本
text = resp.choices[0].message.content

# 4. 想要流式?再写一套 chunk 拼接逻辑
# 5. 想要批量?再写一套并发
# 6. 想接数据库/工具/记忆?每一块都自己接线

上面 6 步,每一步都是「胶水」。功能越复杂,胶水越多。这就是你 W1-W3 手写 Agent 的真实体感——你不是在写业务,是在写「让零件转起来的 plumbing」。

LangChain 把这堆杂活标准化了

LangChain 做的事,本质上是把上面那些重复的 plumbing 抽成可复用的标准零件,再用统一的接口(Runnable)接起来:

1
2
3
4
5
6
没有框架:            有了 LangChain:
你写 plumbing  ──→   你只声明「要什么」:
requests请求           chain = prompt | model | parser
手抠 .content          框架自动:拼消息→请求→抠结果→流式/批量
自己写流式/批量
自己接工具/记忆

所以它不是「另一个能调模型的库」——openai SDK 也能调模型。它是「调模型之上的那层编排」:当你需要的不是「问一句话」,而是「带记忆、能查资料、会调工具、能多轮决策」的应用时,框架的价值才显出来。

LangChain 全家福(你后面会陆续碰到)

包名干什么你现在用到没
langchain-core最核心:Runnable 接口、LCEL 管道、``
langchain-openai把 OpenAI / DeepSeek(兼容) 等模型接进来✅ 本文用到
langchain-community社区集成:各种向量库、工具、检索器⏳ 后面接 Chroma/工具时
langchain上层高级封装(chains/agents 老接口)⏳ 渐进接触
langgraph有状态图编排(W5 主角)⏳ 下周

记住一句话就够了:LangChain = 「调模型之上的编排层」。你 W1-W3 手写的那些零件,它都有现成的;你手写那些胶水,它用 | 一条管道替你接好。

回到开头那句自白:你前三周手写的所有东西,不是白写——它们让你亲手踩过每个零件的坑,所以现在看 LangChain,你是在「认领」自己造过的轮子,而不是从天书开始学。


一、LCEL 到底是什么?

LCEL(LangChain Expression Language)= 用 | 把零件串成流水线。

它是 LLM 应用的「零件库 + 组装流水线」:零件(模型、提示词、解析器、记忆、检索、工具)是标准化的,组装方式(管道 |)也是标准化的。

一句话记忆:| 就是传送带,前一个的输出自动变成后一个的输入。

为什么需要它(对照你的自研)

你自研的(W1-W3)LangChain 里的名字一句话
RealLLM / llm_clientChatOpenAI / ChatModel调模型的封装
prompt_lib(W1)ChatPromptTemplate提示词模板
手动取 .content / json.loadsStrOutputParser / JsonOutputParser解析 AI 输出
fc_loop 工具循环(W2)Agent / Tool Calling工具调用循环
MemoryManager(W3)Memory(或包一层自研)记忆
Chroma 长期记忆(W3)Retriever / VectorStore向量召回
tools/ 注册表(天气/计算器)@tool 装饰器工具标准接口

关键认知:框架化不是「学新东西」,是「把你的手动接线变成标准管道」。右边每一格,你左边都写过一遍——所以你不是在从零学,是在给旧东西换统一接口。

LangChain / LangGraph 是 Agent 岗位出现率最高的框架。会这套「通用语言」,比会某个手写实现更有市场价值。


二、八个零件:从你的自研到 LangChain

LangChain 把 LLM 应用拆成 8 个标准化零件。下面每个都对应你 W1-W3 的代码——你早就造过它们,只是名字不同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌──────────────────────────────────────────────────────────┐
  1. Model(模型)= 厨师                                      
     你已有 RealLLMChatOpenAI "统一厨师证"——              
     DeepSeek/通义/Kimi 都一个接口,走 OpenAI 兼容协议,       
      base_url 就行(你早就这么干了)                       
├──────────────────────────────────────────────────────────┤
  2. Prompt(提示词模板)= 菜谱模板                            
      W1  prompt_libChatPromptTemplate 支持 {变量} 插值: 
     "请翻译成英文:{text}",传 {"text":"你好"} 自动填          
├──────────────────────────────────────────────────────────┤
  3. OutputParser(输出解析器)= 装盘                          
     你平时手动 json.loads(...)StrOutputParser 把复杂输出   
     变纯字符串;JsonOutputParser 帮你做 json.loads           
├──────────────────────────────────────────────────────────┤
  4. Chain / LCEL(链)= 传送带  本周核心                    
     前者的输出自动变后者的输入。"声明式":你描述"要什么流程" 
     不用写"怎么一步步调"                                    
├──────────────────────────────────────────────────────────┤
  5. Memory(记忆)= 记忆                                      
      W3  MemoryManager 已经很强(短期窗口+长期Chroma+     
     滚动压缩)。本周 D3 把它接进链,而不是重写                
├──────────────────────────────────────────────────────────┤
  6. Retriever(检索器)= 档案室管理员                         
      W3  Chroma 就是这个。RAG "知识记忆"和你的          
     "长期向量记忆"是同一个东西——接一次复用,不另起炉灶        
├──────────────────────────────────────────────────────────┤
  7. Tool(工具)= 工具箱                                      
      W2 的天气、计算器。LangChain  @tool 装饰器把函数     
     变成"模型可调用的工具"                                   
├──────────────────────────────────────────────────────────┤
  8. Agent(智能体)= 服务员                                    
      W2  fc_loop:模型决定调哪个工具→执行→结果喂回→再决定。│
     LangChain  Agent 把这个循环标准化了                     
└──────────────────────────────────────────────────────────┘

三个核心概念先记住(它们是 W5 LangGraph 的 node/edge 雏形):

  • Chain = 组合:把零件用 | 串起来
  • Retriever = 取数:从外部/向量库取上下文
  • Tool = 可调函数:模型能主动调用的函数

三、工厂流水线:prompt | model | parser

LCEL 最小形态就是一条三件套链。| 是传送带——配料出来自动进加工机,加工完自动进包装机。

1
2
3
4
5
6
7
8
9
PromptInput            ← 入口:你传的 {"question": "..."}(dict)
ChatPromptTemplate     ← 配料机:dict → [system, user] 消息列表
ChatOpenAI             ← 加工机:消息 → AIMessage
StrOutputParser        ← 包装机:AIMessage → 纯字符串
StrOutputParserOutput  ← 出口:str

声明式 vs 命令式(这是 LCEL 的精髓,面试爱问):

  • 命令式(你 W1-W3 的写法):resp = client.chat(...); text = resp.choices[0].message.content——你写「怎么一步步调」
  • 声明式(LCEL):chain = prompt | model | parser——你只描述「要什么流程」,中间的传递、类型转换全交给框架

这就是为什么叫「表达式语言」:你写的是一个表达式(一条链),不是一串语句


四、最小可跑代码(逐块讲解)

先装依赖(一行命令):

1
2
pip install langchain langchain-openai langchain-community python-dotenv
pip install -U langchain langchain-openai langchain-core

① 配料机:ChatPromptTemplate

1
2
3
4
5
6
7
from langchain_core.prompts import ChatPromptTemplate

# {question} 是占位符,invoke 时传 dict 自动填
prompt = ChatPromptTemplate([
    ("system", "你是一个简洁的助手"),
    ("user", "{question}"),
])

对应你 W1 的 prompt_lib:把「系统提示 + 用户问题」拼成消息列表,但不用你手写 f-string 拼接了。

② 加工机:ChatOpenAI(接 DeepSeek)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

load_dotenv()

# DeepSeek 走 OpenAI 兼容协议:换 model 名 + base_url 即可
model = ChatOpenAI(
    model="deepseek-v4-flash",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
)

对应你 W1 的 RealLLM:你当时就是靠 base_url 切到 DeepSeek 的。ChatOpenAI 是「统一厨师证」——DeepSeek、通义、Kimi 都是同一套接口,换 model + base_url 就行,不用为每家重写客户端。

③ 包装机:StrOutputParser

1
2
3
4
from langchain_core.output_parsers import StrOutputParser

# AIMessage → 纯字符串,对应你平时手写的 .content 提取
parser = StrOutputParser()

对应你 W1 手动抠的 resp.choices[0].message.content——现在一句 StrOutputParser() 替代,而且流式/批量场景自动适配。

④ 传送带:一条链

1
2
# 核心就这一行:前一个的输出自动喂给后一个
chain = prompt | model | parser

这就是本周核心。| 把三个零件声明式地组装成 Runnable。

⑤ 三种调用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# invoke:一次性拿到完整结果
result = chain.invoke({"question": "什么是 LCEL?"})
print(isinstance(result, str))   # True ← 这才是验收

# stream:流式,一个字一个字往外冒(对应你 W3 博客里写的 streaming)
for chunk in chain.stream({"question": "数到5"}):
    print(chunk, end="", flush=True)

# batch:一次处理多个输入,框架自动并行
results = chain.batch([
    {"question": "1+1=?"},
    {"question": "2+2=?"},
])
print(results)

# get_graph:把链结构打印成 ASCII 图,肉眼确认流向
chain.get_graph().print_ascii()   # 应看到 prompt → model → parser

invoke / stream / batch 是 LCEL 所有 Runnable 的统一三件套——你写的任何链都自动获得这三种能力,不用自己实现。


五、完整代码(可直接抄)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 安装依赖
# pip install langchain langchain-openai langchain-community python-dotenv
# pip install -U langchain langchain-openai langchain-core

# 最小 LCEL 链:管道 prompt | model | parser

import os
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

load_dotenv()

# 配料机:{question} → [system, user] 消息
prompt = ChatPromptTemplate([
    ("system", "你是一个简洁的助手"),
    ("user", "{question}"),
])

# 加工机:ChatOpenAI 接 DeepSeek(OpenAI 兼容协议,换 base_url 即可)
model = ChatOpenAI(
    model="deepseek-v4-flash",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
)

# 包装机:AIMessage → 纯字符串
parser = StrOutputParser()

# 传送带:一条链
chain = prompt | model | parser

# 调用
print("=== invoke ===")
result = chain.invoke({"question": "什么是 LCEL?"})
# StrOutputParser 返回 Python str
print(isinstance(result, str))   # True ← 这才是验收

print("\n=== stream(流式,一个字一个字冒)===")
for chunk in chain.stream({"question": "数到5"}):
    print(chunk, end="", flush=True)

print("\n\n=== batch(一次处理多个)===")
results = chain.batch([
    {"question": "1+1=?"},
    {"question": "2+2=?"},
])
print(results)

print("\n=== 链结构图 ===")
chain.get_graph().print_ascii()   # 应看到 prompt → model → parser 的流向

六、踩坑记 & 知识点

① 验收用 isinstance(result, str),别 assert 具体类名 StrOutputParser 在不同 LangChain 版本里,底层返回类型可能裹一层包装类型(比如某些版本会看到 TextAccessor 这类名字)。跨版本会变的是具体类名,不会变的是「它是 str 的子类」。所以稳健验收写 isinstance(result, str) == True,不要写 type(result).__name__ == 'str',后者会在版本升级时假红。

② DeepSeek 是 OpenAI 兼容协议,不是特例 你 W1 的 RealLLM 已经验证了 base_url="https://api.deepseek.com" 这套走法。ChatOpenAI 只是把这套走法做成标准接口——model 名按 DeepSeek 官方给的填(笔记里写的 deepseek-v4-flash 按你当时环境填,真实环境以官方模型名为准),api_key 走环境变量,别硬编码进代码(呼应你 Vibe Coding 手册里的「密钥不上库」红线)。

③ 「声明式」省的是胶水,不是脑子 chain = prompt | model | parser 看起来比手写 resp = ...; text = ... 短,但它没替你设计流程——你 still 得想清楚「先配料、再加工、再装盘」的顺序。LCEL 把「传递」自动化了,把「架构决策」留给你。这也是为什么 W3 的 MemoryManager 不用重写、直接包一层接进链——决策逻辑是你的,管道只是载体。

get_graph().print_ascii() 是你的排错眼 链一复杂(尤其后面接 Retriever / Tool / Memory),print_ascii() 能一眼看出「数据从哪流到哪、有没有断」。每次改完链先打一张图,比对着报错猜快十倍。


七、总结

LangChain 是 LLM 应用的编排框架:模型、提示词、解析器、记忆、检索、工具都标准化成 Runnable,用 LCEL 管道(|)声明式组合。

它和你 W1-W3 的自研一一对应——RealLLM=ChatModel、prompt_lib=ChatPromptTemplate、手动解析=OutputParser、fc_loop=Agent、MemoryManager=Memory、Chroma=Retriever。所以框架化不是学新东西,是把你的手动接线变成标准管道。

三个核心概念记住:Chain=组合、Retriever=取数、Tool=可调函数——这正是 W5 LangGraph 的 node/edge 雏形。


Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计