LangChain框架学习

一、前言

最近系统学习了 LangChain 框架,并通过 5 个由浅入深的 Demo 完成了实战练习。本文记录完整的实践过程、核心概念理解以及踩坑经验,希望能帮助同样在学习 LangChain 的读者快速上手。

本文所有代码已开源:[GitHub 仓库链接](https://github.com/binbin3828/langchain_demo.git "GitHub 仓库链接")

技术选型

组件 选型 说明
LLM 框架 LangChain 当前最流行的 LLM 应用开发框架
模型 DeepSeek V4 Pro 国产大模型,性价比高,兼容 OpenAI API
向量库 ChromaDB 轻量级本地向量数据库
嵌入模型 sentence-transformers 本地运行,无需 API Key

二、项目概览

整个项目包含 5 个 Demo,覆盖了 LangChain 最核心的功能:

Demo 核心功能 关键组件
demo_translation 翻译链 ChatPromptTemplate, ChatOpenAI, StrOutputParser
demo_getjson 结构化 JSON 输出 PydanticOutputParser, BaseModel
demo_getlist 列表输出 CommaSeparatedListOutputParser
demo_talk 多轮对话记忆 MessagesPlaceholder, RunnableWithMessageHistory
rag 知识库问答(RAG) Chroma, HuggingFaceEmbeddings, TextLoader

三、环境准备

3.1 安装依赖

复制代码
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install langchain langchain-openai langchain-community langchain-text-splitters
pip install chromadb sentence-transformers pydantic python-dotenv httpx

3.2 配置 API Key

在项目根目录创建 .env 文件:

复制代码
DEEPSEEK_API_KEY=sk-your-key-here

3.3 初始化模型

所有 Demo 共用同一个模型初始化逻辑:

复制代码
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="deepseek-v4-pro",
    openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
)

因为 DeepSeek 兼容 OpenAI 的 API 格式,所以可以直接用 langchain-openai 包,只需改 base_urlapi_key 即可。

四、Demo 详解

Demo 1:翻译链 --- 理解 LCEL 核心

文件demo_translation.py

这是最简单的入门示例,展示了 LangChain 最核心的 LCEL(LangChain Expression Language) 管道操作。

复制代码
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

system_template = "你是一个专业的翻译官,请将用户输入的文本从 {source_lang} 翻译成 {target_lang}。"
human_template = "{text}"

prompt_template = ChatPromptTemplate.from_messages([
    ("system", system_template),
    ("human", human_template)
])

parser = StrOutputParser()

# LCEL 管道:Prompt → Model → Parser
translation_chain = prompt_template | model | parser

result = translation_chain.invoke({
    "source_lang": "中文",
    "target_lang": "英文",
    "text": "你好,今天天气不错。"
})
print(result)  # Hello, the weather is nice today.
关键理解

LCEL 的 | 运算符 是 LangChain 的灵魂。a | b | c 意味着数据从左到右依次流过每个组件:

复制代码
输入字典 → ChatPromptTemplate(填充模板)→ ChatOpenAI(调用 LLM)→ StrOutputParser(提取文本)→ 最终结果

StrOutputParser 的作用是把 LLM 返回的 AIMessage 对象转成纯字符串,省去了手动提取的麻烦。


Demo 2:结构化 JSON 输出

文件demo_getjson.py

很多场景下我们需要 LLM 输出结构化数据(JSON),而不是只有文本。LangChain 提供了 PydanticOutputParser,配合 Pydantic 模型定义,可以自动解析和校验输出。

复制代码
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

# 1. 用 Pydantic 定义想要的 JSON 结构
class MovieInfo(BaseModel):
    title: str = Field(description="电影名")
    year: int = Field(description="上映年份")
    director: str = Field(description="导演")

parser = PydanticOutputParser(pydantic_object=MovieInfo)

# 2. 提示词中加入格式说明
prompt = ChatPromptTemplate.from_template(
    "请用 JSON 提供电影《{movie_name}》的基本信息。\n{format_instructions}"
)

chain = prompt | model | parser

result = chain.invoke({
    "movie_name": "盗梦空间",
    "format_instructions": parser.get_format_instructions()
})

print(result)  # title='盗梦空间' year=2010 director='克里斯托弗·诺兰'
print(result.model_dump_json(indent=2, ensure_ascii=False))
运行结果
复制代码
{
  "title": "盗梦空间",
  "year": 2010,
  "director": "克里斯托弗·诺兰"
}
关键理解

parser.get_format_instructions() 会自动生成一段格式说明文本,插入到提示词中告诉 LLM 应该输出什么结构。比如它会生成类似这样的指令:

复制代码
你输出的内容应该格式化为符合以下 JSON 模式的 JSON 实例...
{
  "title": "电影名 (string)",
  "year": "上映年份 (int)",
  "director": "导演 (string)"
}

这就是 "Format Instructions Injection" 模式------在提示词中嵌入格式约束,让 LLM 按照预期输出。


Demo 3:列表输出

文件demo_getlist.py

复制代码
from langchain_core.output_parsers import CommaSeparatedListOutputParser

parser = CommaSeparatedListOutputParser()

prompt = ChatPromptTemplate.from_template(
    "列出5个关于{topic}的著名电影,中文回答。\n{format_instructions}"
)

chain = prompt | model | parser

result = chain.invoke({
    "topic": "人工智能",
    "format_instructions": parser.get_format_instructions()
})

print(result)      # ['银翼杀手', '黑客帝国', '人工智能', '她', '机械姬']
print(type(result))  # <class 'list'>

CommaSeparatedListOutputParser 会把 LLM 输出的逗号分隔文本自动解析成 Python 列表,适合标签生成、关键词提取等场景。


Demo 4:多轮对话记忆

文件demo_talk.py

没有记忆的 LLM 是"金鱼"------每次对话都是独立的。LangChain 提供了多种记忆机制,这里使用 RunnableWithMessageHistory + InMemoryChatMessageHistory 实现多轮对话。

复制代码
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 1. 提示词中预留历史消息位置
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的AI助手。"),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

chain = prompt | model | StrOutputParser()

# 2. 会话历史存储
store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# 3. 包装链,注入记忆能力
chain_with_memory = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

# 4. 对话
session_id = "user123"
while True:
    user_input = input("你: ")
    if user_input.lower() in ["退出", "quit", "exit"]:
        break
    response = chain_with_memory.invoke(
        {"input": user_input},
        config={"configurable": {"session_id": session_id}}
    )
    print(f"AI: {response}")
关键理解
  1. MessagesPlaceholder:就像一个占位符,运行时会用真正的历史消息列表替换掉它
  2. session_id:支持多用户/多会话隔离,每个 session_id 拥有独立的聊天记录
  3. InMemoryChatMessageHistory :存在内存中,重启即丢失。生产环境可以替换为数据库持久化方案(如 PostgresChatMessageHistory
对话效果
复制代码
你: 我叫小明
AI: 你好小明!有什么我可以帮你的吗?
你: 我刚才说了什么?
AI: 你告诉我你叫小明。

看到没?AI 记得上一轮对话的内容,这就是记忆的作用。


Demo 5:RAG 知识库问答

文件rag.py

RAG(Retrieval-Augmented Generation)是目前最实用的 LLM 应用模式之一。核心思想:先检索相关知识,再让 LLM 基于检索结果作答,有效解决 LLM 知识过时和幻觉问题。

复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

# 1. 加载文档
loader = TextLoader("knowledge.txt", encoding="utf-8")
documents = loader.load()

# 2. 文本分块
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

# 3. 向量化并存入向量库
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = Chroma.from_documents(docs, embeddings)
retriever = vectorstore.as_retriever()

# 4. 构建 RAG 链
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

answer = rag_chain.invoke("小明的猫叫什么名字?")
print(answer)  # 旺财
RAG 完整流程
复制代码
                     ┌─────────────┐
                     │ knowledge.txt│
                     └──────┬──────┘
                            │ TextLoader
                            ▼
                     ┌─────────────┐
                     │    Documents │
                     └──────┬──────┘
                            │ CharacterTextSplitter
                            ▼
                     ┌─────────────┐
                     │    Chunks    │
                     └──────┬──────┘
                            │ HuggingFaceEmbeddings
                            ▼
                     ┌─────────────┐
                     │   ChromaDB   │
                     └──────┬──────┘
                            │ 相似度检索
                            ▼
          ┌──────────────────────────┐
          │ 检索到的相关文档片段        │
          └────────────┬─────────────┘
                       │ 注入到 Prompt 的 {context}
                       ▼
          ┌──────────────────────────┐
          │  Prompt + Context + Q    │
          └────────────┬─────────────┘
                       │ ChatOpenAI
                       ▼
          ┌──────────────────────────┐
          │       最终答案            │
          └──────────────────────────┘
关键理解

这里最值得学习的是 LCEL 的并行输入写法:

复制代码
{"context": retriever | format_docs, "question": RunnablePassthrough()}
  • retriever | format_docs:检索文档 → 格式化为字符串,作为 context
  • RunnablePassthrough():直接把用户输入原样传递,作为 question

这两个分支并行执行,结果合并成一个字典传入 Prompt。这是 LCEL 非常优雅的地方。

关于嵌入模型

HuggingFaceEmbeddings 使用 sentence-transformers/all-MiniLM-L6-v2,这是一个轻量级模型(~80MB),第一次运行时会自动下载。优点是完全本地运行,不需要任何 API Key,适合学习和开发阶段。


五、踩坑与最佳实践

坑 1:SSL 验证问题

复制代码
# ❌ 错误做法:全局禁用 SSL 验证
http_client = httpx.Client(verify=False)

# ✅ 正确做法:使用默认客户端(移除 http_client 参数)
model = ChatOpenAI(
    model="deepseek-v4-pro",
    openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
    # 不要传 http_client=xxx,使用默认安全配置
)

除非你的网络环境确实有 SSL 拦截(如公司代理),否则不应该禁用证书验证。

坑 2:RAG 脚本覆写知识文件

复制代码
# ❌ 每次运行都会清空 knowledge.txt
with open("knowledge.txt", "w", encoding="utf-8") as f:
    f.write("写死的知识...")

# ✅ 只读不写,保持文件内容稳定
loader = TextLoader("knowledge.txt", encoding="utf-8")

坑 3:忘记添加 .gitignore 就提交

提交后再想移除已跟踪的文件会比较麻烦:

复制代码
git rm --cached .env     # 从 Git 跟踪中移除但不删除本地文件
git rm --cached -r venv/ # 移除虚拟环境目录

最佳实践 :创建项目后第一步就配置 .gitignore,然后再写代码。


六、项目目录结构

复制代码
langchain_demo/
├── demo_translation.py     # 翻译链(入门)
├── demo_getjson.py         # 结构化JSON输出
├── demo_getlist.py         # 列表输出
├── demo_talk.py            # 多轮对话记忆
├── rag.py                  # RAG 知识库问答
├── knowledge.txt           # RAG 知识文件
├── requirements.txt        # 依赖清单
├── .env                    # API Key(不提交)
├── .gitignore              # Git 忽略规则
└── README.md               # 项目说明

七、总结与收获

学到的核心概念

概念 理解
LCEL(管道操作) LangChain 提供的一种声明式编程语法 ,使用 `
Prompt Template 模板 + 变量的模式,运行时注入具体值,复用提示词
Output Parser 将 LLM 原始输出解析为结构化数据(str/list/dict/Pydantic)
Message History 通过 MessagesPlaceholder + RunnableWithMessageHistory 实现多轮记忆
RAG 架构 检索增强生成:Embedding → 向量检索 → LLM 生成,解决知识过时问题

下一步可以继续探索的方向

  1. Agent(智能代理):让 LLM 自主决定调用哪些工具(搜索、计算器、API)
  2. Callback(回调):监控和追踪 LLM 调用过程
  3. 流式输出:实现打字机效果,提升用户体验
  4. 多模态:LangChain 已支持图片、音频等多模态输入
  5. 部署:用 FastAPI + LangServe 将链部署为 API 服务

给读者的建议

  • 先跑通再理解:不要试图一次性搞懂所有概念,先让代码跑起来,看到输出再去理解原理
  • 多看官方文档:LangChain 官方文档更新很快,很多国内博客已经过时
  • 善用调试 :用 langchain.debug = True 可以看到 LLM 调用细节
  • 小步迭代:每次只加一个组件,理解透了再加下一个

八、参考资料


本文首发于 CSDN,转载请注明出处。