LangChain 入门(一):为什么我们需要给 LLM 套上一层壳?

1. 提出问题:直接调用 API 有什么不好吗?

在开始学习 LangChain 之前,我们需要先问自己一个问题:如果我可以直接使用 openai 的官方 Python 库来调用 GPT 模型,为什么我还需要学习一个复杂的框架?

假设我们要写一个简单的程序:让 AI 给我们讲一个关于特定主题的冷笑话。

如果不使用 LangChain,我们通常会这样写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
from openai import OpenAI

# 假设你已经设置了环境变量 OPENAI_API_KEY
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

def get_joke(topic):
# 手动拼接提示词字符串
prompt = f"请给我讲一个关于{topic}的冷笑话,要非常简短。"

response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "你是一个幽默大师。"},
{"role": "user", "content": prompt}
]
)

# 我们必须知道返回对象的具体结构,才能提取出文本内容
return response.choices[0].message.content

print(get_joke("程序员"))

这段代码看起来没什么问题,对吧?但是,当你试图构建一个真正的应用程序时,你会遇到以下痛点

  1. 模型切换困难:如果你老板明天说,“GPT-3.5 太贵了,换成 Claude 3”或者“换成本地部署的 Llama 3”,你必须重写所有调用代码,因为不同公司的 API 格式完全不同。

  2. 提示词管理混乱:上面的代码中,提示词是硬编码在函数里的 (f"请给我讲一个...")。如果提示词很长,或者需要版本管理,混在 Python 代码里会非常难维护。

  3. 输出处理繁琐:LLM 返回的永远是一个复杂的 JSON 对象。你需要每次都写 response.choices[0].message.content 这种长代码来提取真正的文本。

  4. 缺乏流程编排:如果你想把“讲笑话”的结果,传给下一个步骤(比如“把这个笑话翻译成英文”),你需要手动写很多胶水代码来传递变量。

LangChain 的出现,就是为了解决这些“脏活累活”。 它提供了一套标准化的接口,把模型调用、提示词管理、结果解析等步骤封装成了积木,让你专注于业务逻辑。

2. 环境准备

在开始写代码之前,我们需要安装必要的库。

注意:LangChain 最近进行了架构拆分,为了保证代码的现代性和兼容性,我们不再只安装 langchain 一个大包,而是按需安装。

打开你的终端(Terminal)或者命令行,执行以下命令:

1
2
3
4
5
# 安装 LangChain 核心组件
pip install langchain langchain-core

# 安装 OpenAI 的适配器(LangChain 专门为 OpenAI 写的接口库)
pip install langchain-openai

此外,你需要拥有一个 OpenAI 的 API Key。在代码运行前,请确保在环境变量中设置了它,或者在代码中显式传递(不推荐显式传递,为了安全)。

3. LangChain 的核心概念:LCEL (LangChain 表达式语言)

LangChain 最重要的概念就是 Chain(链)。你可以把它想象成工厂的流水线。

一个最基础的流水线通常包含三个工位:

  1. Prompt Template(提示词模版):负责把原材料(用户输入的主题)包装成半成品(完整的提示词)。

  2. Model(模型):负责加工(进行思考和生成)。

  3. Output Parser(输出解析器):负责包装(把模型吐出的复杂对象,清洗成我们不仅能看懂,还能直接用的字符串)。

LangChain 使用一种非常有表现力的语法来定义这个流水线,叫做 LCEL。它的核心符号是 |(管道符)。

它的逻辑是:输入变量 | 提示词模版 | 模型 | 输出解析器

4. 实战代码:重构“冷笑话”生成器

下面我们用 LangChain 重写上面的逻辑。请仔细阅读每一行代码和对应的注释。

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
import os
# 1. 导入聊天模型包装器
# ChatOpenAI 是 LangChain 对 OpenAI 接口的封装
from langchain_openai import ChatOpenAI

# 2. 导入提示词模版
# ChatPromptTemplate 用于构建对话式的提示词(包含 System 和 User 角色)
from langchain_core.prompts import ChatPromptTemplate

# 3. 导入输出解析器
# StrOutputParser 的作用极其简单:把模型对象转换成纯字符串
from langchain_core.output_parsers import StrOutputParser

# --- 第一步:初始化模型 ---
# 我们实例化一个 ChatOpenAI 对象。
# model参数指定具体的模型版本。
# temperature=0.7 表示让 AI 稍微有点创造力(0 是最死板,1 是最狂野)。
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# --- 第二步:创建提示词模版 ---
# 这里的 from_messages 接收一个列表,模拟对话历史。
# {topic} 是一个“占位符”,类似于 Python f-string 中的变量,
# 但它现在还只是一个空坑,等待后续填充。
prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一个幽默大师,擅长讲简短的冷笑话。"),
("user", "请给我讲一个关于 {topic} 的冷笑话。")
])

# --- 第三步:创建输出解析器 ---
# 它的工作是从 response.content 中提取文本,其余元数据统统丢弃。
parser = StrOutputParser()

# --- 第四步:构建链 (Chain) ---
# 这是 LangChain 最魔性的地方:使用 '|' 管道符。
# 数据流向:
# 用户字典 {"topic": "..."} -> 传入 prompt_template -> 生成完整 Prompt 对象
# -> 传入 llm -> 生成 ChatMessage 对象
# -> 传入 parser -> 生成 纯字符串
chain = prompt_template | llm | parser

# --- 第五步:运行链 ---
# 使用 .invoke() 方法来触发这个流水线。
# 我们需要传入一个字典,字典的 key 必须和模版中的占位符 {topic} 一致。
topic_name = "程序员"
print(f"正在生成关于 {topic_name} 的笑话...")

result = chain.invoke({"topic": topic_name})

print("--- 结果如下 ---")
print(result)

5. 代码深度解析(小白必读)

如果你是第一次看到这段代码,可能会有几个疑问,我们逐一拆解:

为什么要是 ChatOpenAI 而不是 OpenAI

LangChain 中的 ChatOpenAI 是一个类(Class)。当你运行 llm = ChatOpenAI(...) 时,你并没有真的去调用 API,你只是在配置一个“工人”。你告诉这个工人:“等会儿干活的时候,你要用 gpt-3.5-turbo 这个工具”。这样做的好处是,如果你想换成 Google 的 Gemini 模型,你只需要把这行代码改成 from langchain_google_genai import ChatGoogleGenerativeAI,后续的代码几乎不用动。

prompt_template 到底做了什么?

.invoke({"topic": "程序员"}) 发生的那一瞬间,prompt_template 接收到了字典。它内部会执行类似于字符串替换的操作,把 {topic} 替换成 "程序员"。 更重要的是,它把这串文本格式化成了 OpenAI API 所需要的特殊 JSON 格式(包含 role: system, role: user 等结构)。

| 管道符是如何工作的?

在 Python 中,| 通常是“或”运算。但是 LangChain 重写了这个符号的定义(这叫运算符重载)。 在 LangChain 里,A | B 的意思是:运行 A,拿到 A 的输出结果,把它直接作为 B 的输入参数,然后运行 B。 如果没有这个符号,我们的代码会变成极其丑陋的嵌套调用: parser.invoke(llm.invoke(prompt_template.invoke({"topic": "程序员"}))) 你可以看到,嵌套写法非常反人类,阅读顺序是从里到外的。而管道写法是从左到右的,符合人类直觉。

6. 进阶一点点:如果我有两个变量怎么办?

现实生活中,我们的提示词往往不止一个变量。比如,我们不仅要指定主题,还要指定笑话的风格。

修改起来非常简单,只需要改动模版和调用处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 修改模版,增加 {style} 占位符
complex_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个幽默大师。"),
("user", "请用 {style} 的风格,讲一个关于 {topic} 的冷笑话。")
])

# 重新构建链
complex_chain = complex_prompt | llm | parser

# 调用时,字典里提供两个 key
result = complex_chain.invoke({
"topic": "人工智能",
"style": "莎士比亚戏剧" # 比如这就很有趣
})

print(result)

7. 本篇总结

在这一篇,我们并没有通过构建什么惊天动地的大项目来吓唬你。我们只做了一件事:标准化

  • 标准化模型:用 ChatOpenAI 统一了接口。

  • 标准化输入:用 ChatPromptTemplate 管理了提示词。

  • 标准化输出:用 StrOutputParser 清洗了数据。

  • 标准化流程:用 | 把它们串联起来。

这就是 LangChain 的基石。在下一篇文章中,我们将面对一个 LLM 最大的缺陷:它不知道它训练之后发生的事情,也不知道你公司的私有数据。我们将介绍 RAG(检索增强生成),让 AI 学会“翻书考试”。