LangChain 1.0 实战教程 · 10 个 Demo
基于 LangChain
^1.0版本(1.x 主线能力)每个 Demo 含完整代码 + 每行逐词解析 + 作用说明
前置要求:Python 3.11+、uv、API Key(OpenAI / 硅基流动等兼容接口)
开场:先讲清 LangChain 的 3W1H
1) Why:为什么需要 LangChain?
直接调用模型 API 只能"单次问答",但真实业务需要的是"系统能力":提示词管理、工具调用、检索(RAG)、多步编排、可观测性、部署。LangChain 的价值,就是把这些"应用层能力"标准化,减少你重复造轮子。
2) What:LangChain 是什么?
LangChain 是一个 LLM 应用框架,核心是"可组合组件":
- 模型层:
ChatOpenAI等模型适配器 - 编排层:LCEL(
|管道)与 Runnable 统一接口 - 数据层:Loader / Splitter / Vector Store / Retriever
- 能力层:Tool / Agent / Memory / Parser
- 工程层:Callback / LangSmith / LangServe
3) Who:谁应该用?
- 个人开发者:想快速做出可运行的 AI 功能
- 后端工程师:要把"提示词脚本"升级为可维护服务
- AI 应用团队:需要统一的链路、日志、调试、部署方式
4) How:怎么落地(推荐路径)?
- 用 LCEL 先做"可控单链"(Prompt -> LLM -> Parser)。
- 加上结构化输出和错误处理(Parser + Retry)。
- 接入 RAG(Retriever + 引用来源)。
- 再上 Agent(仅在确实需要多工具决策时)。
- 最后做监控与部署(Callback/LangSmith + LangServe/FastAPI)。
LangChain 基础架构(最小闭环)
从一次请求到一次响应,通常经过这几层:
Input:用户问题 + 上下文(可选)Prompt Layer:模板拼装(系统指令、历史、检索内容)Reasoning/Tool Layer:模型推理,必要时调用工具Knowledge Layer:Retriever 从向量库取证据Output Layer:解析为字符串或结构化 JSON/PydanticObservability Layer:记录 token、耗时、错误与链路
一句话记忆:LangChain 不替代模型,而是把模型"工程化"。
竞品情况与选型建议(2026 视角)
常见替代或互补方案:
- LlamaIndex:数据连接与检索抽象强,RAG 体验好
- Haystack:传统检索体系成熟,企业搜索场景扎实
- Semantic Kernel:偏"企业编排 + 插件"风格
- AutoGen / CrewAI:多 Agent 协作范式更突出
- Dify / Flowise:低代码编排快,适合业务验证
- Vercel AI SDK:前端/全栈流式体验非常顺手
简化选型建议:
- 你要"Python 工程化 + 细粒度可控编排":优先 LangChain。
- 你要"重 RAG 数据管道":LangChain + LlamaIndex 可互补。
- 你要"低代码快速验证":先 Dify/Flowise,再迁移到 LangChain。
- 你要"复杂多智能体协作":LangGraph / AutoGen / CrewAI 对比评估。
Demo 01 · LCEL 语法入门 --- 链式调用
本节要学什么?
LCEL(LangChain Expression Language)是 LangChain 1.0 的核心语法 ------它让你像拼积木一样,用 | 管道符把多个组件串联起来组成 Chain。理解 LCEL,就理解了 LangChain 1.0 的一切。
完整演示
python
# ========== LangChain 1.0 核心:LCEL 管道语法 ==========
# 官方文档:https://python.langchain.com/docs/expression_language/
from langchain_core.prompts import ChatPromptTemplate # 聊天提示词模板(LangChain 1.0 统一用这个)
from langchain_core.output_parsers import StrOutputParser # 把 LLM 输出转成字符串
from langchain_openai import ChatOpenAI # OpenAI 聊天模型(langchain-openai 包)
# --- 第 1 步:创建模型(temperature=0.7,控制随机性,越低越确定性)---
llm = ChatOpenAI(
model="gpt-4o-mini", # 模型名称(gpt-4o-mini 便宜速度快,效果也不错)
temperature=0.7, # 0.0=完全确定,2.0=完全随机,建议 0.0~1.0
api_key="sk-xxxxxxxx", # 你的 API Key(生产环境建议用环境变量)
base_url="https://api.openai.com/v1", # API 端点(兼容其他 OpenAI 兼容接口)
)
# --- 第 2 步:创建 Prompt 模板 ---
# PromptTemplate 的作用:把固定的结构和动态变量分开,方便复用和维护
# {topic} 是占位符,调用时会自动替换成实际传入的值
prompt = ChatPromptTemplate.from_template(
"你是一位幽默的科普作家。请用 3 句话介绍 {topic},最后加一个彩蛋笑话。"
)
# --- 第 3 步:组装 Chain(LCEL 核心语法)---
# chain = prompt | llm | output_parser
# 管道符 | 的含义:把左边组件的输出传给右边组件的输入
# prompt.format(topic=xxx) 的结果 → llm.invoke(xxx) → output_parser.invoke(xxx)
chain = prompt | llm | StrOutputParser()
# --- 第 4 步:运行 Chain(两种方式)---
# 方式 A:直接传入字典(最常用)
result = chain.invoke({"topic": "LangChain 框架"})
print(result)
# 方式 B:先 format 再 invoke(分步执行,便于调试)
formatted_prompt = prompt.format(topic="Python 装饰器")
result2 = chain.invoke(formatted_prompt)
print(result2)
# --- 扩展:带配置的调用(stream / batch / async)---
# 批量调用(一次传入多个输入,串行执行)
multi_results = chain.batch([
{"topic": "量子计算"},
{"topic": "区块链"},
{"topic": "神经网络"}
])
for r in multi_results:
print("---", r, "---\n")
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | # ========== |
#=注释分隔线(美观用) |
在代码中标记大段分节 |
| 3 | from langchain_core.prompts import ChatPromptTemplate |
langchain_core=LangChain核心模块, prompts=提示词子模块, ChatPromptTemplate=聊天提示模板类 | 从 LangChain 核心包导入提示模板(1.0 版本统一用 ChatPromptTemplate) |
| 4 | from langchain_core.output_parsers import StrOutputParser |
output_parsers=输出解析器, StrOutputParser=字符串解析器 | 把 LLM 返回的 AIMessage 对象转成纯字符串 |
| 5 | from langchain_openai import ChatOpenAI |
langchain_openai=OpenAI集成包, ChatOpenAI=OpenAI聊天模型类 | 导入调用 OpenAI GPT 系列模型的客户端 |
| 8 | llm = ChatOpenAI(...) |
ChatOpenAI=OpenAI聊天模型类实例化 | 创建 LLM 实例(和 API 通信的实际对象) |
| 9 | model="gpt-4o-mini" |
model=模型名称参数, "gpt-4o-mini"=具体模型名 | 指定用哪个模型(影响质量、速度、价格) |
| 10 | temperature=0.7 |
temperature=温度参数(控制随机性) | 0.0=每次完全相同,2.0=极度发散,建议 0.0~1.0 |
| 11 | api_key="sk-xxxxxxxx" |
api_key=API密钥参数 | 认证凭证(生产环境用环境变量,不要硬编码) |
| 12 | base_url="https://..." |
base_url=API服务器地址 | 兼容 OpenAI API 格式的第三方接口(如硅基流动、火山引擎等) |
| 16 | prompt = ChatPromptTemplate.from_template(...) |
ChatPromptTemplate.from_template=从字符串模板创建提示对象 | 传入模板字符串,自动解析 {topic} 等变量 |
| 18 | {topic} |
{topic}=变量占位符 | 运行时会被实际值替换掉(模板 + 变量 = 最终 Prompt) |
| 22 | `chain = prompt | llm | StrOutputParser()` |
| 25 | chain.invoke({"topic": "..."}) |
.invoke()=同步调用方法, {"topic": ...}=输入字典 | 用字典传入所有变量,触发整条 Chain 执行(同步阻塞式) |
| 29 | formatted_prompt = prompt.format(...) |
.format()=只执行 Prompt 部分,不往下走 | 把模板套上变量,但不到 LLM(用于调试看最终 Prompt 什么样) |
| 30 | result2 = chain.invoke(formatted_prompt) |
直接传入字符串(跳过 format 步骤) | 上一步生成了 ChatPromptValue,这里直接送入 llm |
| 35 | multi_results = chain.batch([...]) |
.batch()=批量调用方法 | 同时提交多个任务,串行执行,返回列表(适合批量处理文档等场景) |
常见坑
Prompt变量名和invoke()传参键不一致,运行时会直接报错。- 在代码里硬编码
api_key,后续很容易泄露到仓库或日志。 - 把
temperature设太高,导致教程结果难复现。
生产建议
- 固定基础模型和温度,先保证可复现,再做 A/B 调参。
- 统一用环境变量注入密钥,避免在源码里出现
sk-。 - 所有链都先加
StrOutputParser(),减少消息对象类型差异带来的坑。
最小可运行命令
bash
uv add langchain langchain-openai python-dotenv
uv run python demo01_lcel.py
Demo 02 · Prompt Template 详解 --- 灵活设计提示词
本节要学什么?
Prompt(提示词)是 LLM 应用的核心。LangChain 1.0 提供了多种 Prompt 模板 :ChatPromptTemplate(聊天型)、PromptTemplate(纯文本型)、MessagesPlaceholder(动态消息历史)。设计好 Prompt,LangChain 才有灵魂。
完整演示
python
from langchain_core.prompts import (
ChatPromptTemplate, # 聊天型模板(传入消息列表)
PromptTemplate, # 纯文本模板(传入单个字符串变量)
MessagesPlaceholder, # 动态消息占位符(LangChain Memory 的关键)
HumanMessagePromptTemplate, # 用户消息模板
SystemMessagePromptTemplate, # 系统消息模板
)
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# ========== 方式 A:纯文本 PromptTemplate(简单场景)==========
# 最基础的模板:变量替换 + 固定前缀/后缀
simple_template = PromptTemplate.from_template(
"请把以下中文翻译成英文:\n\n{text}",
# 可选:给模板加名称和描述(方便调试和日志追踪)
partial_variables={"language": "English"}, # partial_variables=预填充部分变量
)
# partial_variables:先填充 language="English",调用时只需传 text
prompt = simple_template.partial(language="English")
result = prompt.format(text="LangChain 让 AI 开发变得简单有趣")
print(result)
# ========== 方式 B:聊天型 ChatPromptTemplate(推荐,结构更清晰)==========
# LangChain 把 LLM 对话建模为:System(系统设定)+ Human(用户)+ AIMessage(AI回复)
# 这种建模方式让模型"理解"自己在对话中的角色
chat_prompt = ChatPromptTemplate.from_messages([
# SystemMessage:给 AI 定人设(最重要,决定模型行为)
SystemMessage(
content=(
"你是一位{profession}专家。\n"
"你的工作风格是:{style}\n"
"回答问题时,总是用emoji增加趣味性。"
),
additional_kwargs={"profession": None} # additional_kwargs=附加元数据(LangChain内部用)
),
# MessagesPlaceholder:在这里动态插入【消息历史】(实现多轮对话的关键!)
# variable_name="chat_history" 必须和 invoke 时传入的键名一致
MessagesPlaceholder(variable_name="chat_history", optional=True), # optional=True=可省略
# HumanMessagePromptTemplate:用户输入(每个 invoke 都会新建一个)
HumanMessagePromptTemplate.from_template("{user_input}"),
])
# 渲染后的完整 Prompt(调试用)
print("=== 模板结构 ===")
print(chat_prompt.messages)
print("=== 变量列表 ===")
print(chat_prompt.input_variables) # 查看需要传入哪些变量
# ========== 调用方式 ==========
messages = chat_prompt.format_messages(
profession="Python",
style="简洁有趣",
user_input="什么是 LCEL?",
chat_history=[ # 传入历史消息,实现多轮对话上下文
HumanMessage(content="你好,我想学 LangChain"),
AIMessage(content="嗨!很高兴你想学 LangChain。它是一个 LLMs 应用开发框架...")
]
)
response = llm.invoke(messages)
print("AI 回复:", response.content)
# ========== 方式 C:PipelinePrompt(模板嵌套,适合复杂提示词)==========
from langchain_core.prompts import PipelinePromptTemplate
# 先定义多个子模板
introduction = PromptTemplate.from_template(
"你是 {character},一个乐于助人的 AI 助手。"
)
main_prompt = PromptTemplate.from_template(
"{introduction}\n\n用户问:{question}\n\n你的回答:"
)
# 最终模板:把子模板的结果汇入主模板
full_prompt = PipelinePromptTemplate(
pipeline_prompts=[
("introduction", introduction), # ("变量名", 模板) 元组列表
("main", main_prompt),
],
final_prompt=PromptTemplate.from_template("{main}") # 最终输出这个模板的结果
)
final = full_prompt.format(character="LangChain 助手", question="什么是 RAG?")
print(final)
逐词解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain_core.prompts import (...) |
langchain_core.prompts=LangChain核心提示词模块 | 导入所有提示词相关组件 |
| 12 | PromptTemplate.from_template(...) |
from_template=从字符串创建模板的类方法 | 最简单的模板创建方式(够用 90% 场景) |
| 14 | partial_variables={"language": "English"} |
partial_variables=预填充变量(先填一部分,剩下的调用时再填) | 把模板先"部分实例化",后续只需填剩余变量 |
| 15 | prompt = simple_template.partial(language="English") |
.partial()=返回新模板(已预填部分变量) | 调用时不传 language,只传 text |
| 23 | ChatPromptTemplate.from_messages([...]) |
from_messages=从消息列表创建聊天模板 | 最灵活的模板创建方式,支持 System/Human/AI 多角色 |
| 26 | SystemMessage(content=...) |
SystemMessage=系统消息类(定义AI人设) | 给 AI 定角色和规则(最重要的提示词组件) |
| 31 | MessagesPlaceholder(variable_name="chat_history", ...) |
MessagesPlaceholder=动态消息占位符 | 运行时把消息历史插入到这里(多轮对话的核心) |
| 32 | optional=True |
optional=占位符是否可选(不传时不报错) | True=调用时不传 chat_history 也不报错 |
| 38 | HumanMessagePromptTemplate.from_template("{user_input}") |
from_template=从字符串生成用户消息模板 | 每次 invoke 新建一个用户消息(不要在模板里放死的用户消息) |
| 46 | chat_prompt.format_messages(...) |
.format_messages()=格式化消息列表(返回 Message 对象列表) | 把模板 + 变量渲染成 LLM 可以直接吃的消息列表 |
| 48 | HumanMessage(content="...") |
HumanMessage=用户消息类 | 手动构造用户消息(Session 历史中记录用户说了什么) |
| 49 | AIMessage(content="...") |
AIMessage=AI回复消息类 | 手动构造 AI 回复(记录 AI 说了什么,用于上下文) |
| 60 | PipelinePromptTemplate |
PipelinePrompt=流水线模板(子模板嵌套) | 先渲染子模板,把结果汇入主模板(适合超复杂提示词) |
| 61 | pipeline_prompts=[("introduction", introduction), ...] |
pipeline_prompts=流水线模板列表, ("变量名", 模板)=元组 | 每个元组定义一个子模板,输出会注入到下一个模板的 {变量名} 中 |
常见坑
MessagesPlaceholder的变量名与输入键不一致(如chat_historyvshistory)。- 在
SystemMessage里塞过多业务细节,导致 Prompt 难维护。 - 模板层数过深但无命名规范,调试时很难定位错误。
生产建议
- 用"角色/约束/输出格式"三段式组织系统提示词。
- 关键模板加版本号(如
prompt_v1,prompt_v2)便于回滚。 - 在调用前打印一次
prompt.input_variables做自检。
最小可运行命令
bash
uv add langchain langchain-openai
uv run python demo02_prompt_template.py
Demo 03 · 记忆(Memory)--- 让 AI 记住对话历史
本节要学什么?
没有 Memory 的 Chain 是一个"金鱼"------每次对话都不知道之前聊了什么。LangChain 1.0 提供了多种 Memory 方案:ConversationBufferMemory(最简单的历史)、ConversationSummaryMemory(AI 帮你摘要,节省 token)、VectorStoreRetrieverMemory(向量检索记忆,适用长对话)。
完整演示
python
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain # 对话链(内置 Memory 集成)
from langchain.memory import (
ConversationBufferMemory, # 原始历史(最简单,Token 消耗大)
ConversationSummaryMemory, # AI 摘要历史(省 Token,推荐)
ConversationBufferWindowMemory, # 窗口记忆(只保留最近 N 条)
)
from langchain_core.messages import get_buffer_string # 把消息历史转成字符串
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# ========== 方式 A:ConversationBufferMemory(原始历史,简单直接)==========
# ConversationBufferMemory:原封不动地存所有消息
# 优点:信息完整 缺点:对话一长,Token 爆炸(GPT-4o-mini 128k 上下文但贵)
buffer_memory = ConversationBufferMemory(
return_messages=True, # return_messages=True=返回 Message 对象列表(给 LCEL 用)
# return_messages=False=返回普通字符串(给传统 Chain 用)
ai_prefix="助手", # AI 消息的前缀(影响 Prompt 里的名字)
human_prefix="用户", # 人类消息的前缀
)
# 模拟两轮对话
buffer_memory.save_context(
{"input": "我叫布鲁斯,是一名技术合伙人"}, # 存入用户说的
{"output": "你好布鲁斯!很高兴认识你,一位技术背景的合伙人。请问有什么我可以帮你的?"} # 存入AI回复
)
buffer_memory.save_context(
{"input": "我想用 LangChain 开发一个客服机器人"},
{"output": "很棒的选择!LangChain 是目前最流行的 LLM 应用框架。请问你需要什么水平的对话能力?"}
)
# 读取全部历史(返回 Message 对象列表,LCEL 的 MessagesPlaceholder 直接用)
history = buffer_memory.chat_memory.get_messages()
print("=== Buffer Memory 全部消息 ===")
for msg in history:
print(f"[{msg.type}]: {msg.content}")
# ========== 方式 B:ConversationSummaryMemory(AI 摘要,省 Token)==========
# 适用场景:对话很长(比如 20+ 轮),用原始历史太贵
# 原理:每隔几轮,AI 自动把历史"压缩"成摘要,存起来
summary_memory = ConversationSummaryMemory(
llm=llm, # 必须传入 llm(因为要调用 AI 做摘要)
return_messages=True,
buffer="", # 初始 buffer 为空(第一轮对话后自动填充摘要)
)
# 模拟长对话(10 轮)
for i in range(10):
user_input = f"这是第{i+1}轮对话,用户在说一些关于 LangChain 的事情"
ai_output = f"AI 在第{i+1}轮做出了回复"
summary_memory.save_context({"input": user_input}, {"output": ai_output})
# 查看摘要(你会发现 AI 自动把10轮对话压缩成了一段话)
print("=== Summary Memory 摘要 ===")
print(summary_memory.buffer)
# ========== 方式 C:ConversationBufferWindowMemory(窗口记忆)==========
# 适用场景:只想记住"最近 N 条"消息,不需要全部历史
window_memory = ConversationBufferWindowMemory(
k=3, # k=窗口大小,只保留最近 3 轮对话
return_messages=True,
ai_prefix="助手",
human_prefix="用户",
)
for i in range(8):
window_memory.save_context({"input": f"用户第{i}轮"}, {"output": f"AI第{i}轮回复"})
print("=== 窗口 Memory(只看最后3条)===")
print(window_memory.buffer) # 只有第5、6、7轮的内容了
# ========== 把 Memory 接入 LangChain 1.0 LCEL Chain ==========
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# LangChain 1.0 推荐做法:在 Prompt 的 MessagesPlaceholder 插入 Memory
prompt_with_memory = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个友好的 AI 助手,名字叫小 L。请基于对话历史回答问题。"),
MessagesPlaceholder(variable_name="history", optional=True),
HumanMessagePromptTemplate.from_template("{question}"),
])
# 用 LCEL 手动组装带 Memory 的 Chain
def get_history_str(memory):
"""把 Memory 对象中的历史转成字符串,注入到 prompt"""
messages = memory.chat_memory.get_messages()
return get_buffer_string(messages) # 把 Message 对象列表转成可读字符串
# 注意:LangChain 1.0 的 LCEL 方式需要手动把 history 传入 prompt
chain = prompt_with_memory | llm | StrOutputParser()
# 模拟多轮对话(每次都把历史传进去)
memory = ConversationBufferMemory(
return_messages=False, # return_messages=False → get_buffer_string() 返回字符串
ai_prefix="助手",
human_prefix="用户"
)
memory.save_context({"input": "我叫布鲁斯"}, {"output": "你好布鲁斯!"})
memory.save_context({"input": "我喜欢 Python 编程"}, {"output": "Python 是一门很棒的语言!"})
# 调用时把 history 传入字典
result = chain.invoke({
"question": "你还记得我叫什么名字吗?",
"history": get_buffer_string(memory.chat_memory.get_messages()) # 把历史作为字符串传入
})
print("AI 回复:", result)
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain.chains import ConversationChain |
ConversationChain=内置对话链(已集成 Memory) | LangChain 1.0 前的传统方式(现在更推荐用 LCEL 手动组装) |
| 3 | from langchain.memory import ConversationBufferMemory |
memory=记忆模块, ConversationBufferMemory=原始消息历史记忆 | 把所有对话原封不动存起来 |
| 6 | return_messages=True |
return_messages=返回格式控制参数, True=返回Message对象 | LCEL 的 MessagesPlaceholder 需要 Message 对象列表 |
| 7 | ai_prefix="助手" |
ai_prefix=AI消息前缀(显示名) | 影响 Prompt 里 AI 消息的标记方式(中文场景友好) |
| 17 | buffer_memory.save_context(...) |
.save_context()=保存对话上下文方法, {"input": ..., "output": ...}=用户+AI对 | 把一对问答存进 Memory(相当于 append 到历史列表) |
| 23 | history = buffer_memory.chat_memory.get_messages() |
.chat_memory=聊天记录管理器, .get_messages()=获取所有消息列表 | 从 Memory 中取出完整的消息对象列表 |
| 35 | ConversationSummaryMemory(llm=llm, ...) |
llm=必须传入一个 LLM 实例(用于生成摘要) | AI 摘要记忆:每隔几轮调用 LLM 自动压缩历史 |
| 37 | buffer="" |
buffer=初始历史(空字符串) | 第一轮对话后,AI 会自动生成第一段摘要填入这里 |
| 47 | ConversationBufferWindowMemory(k=3, ...) |
k=窗口大小参数 | 只保留最近 k 条对话(更省 Token) |
| 64 | MessagesPlaceholder(variable_name="history", optional=True) |
variable_name=变量名(必须和 invoke 字典的 key 一致) | 动态插入对话历史的地方(LCEL Memory 集成的关键) |
| 69 | get_buffer_string(messages) |
get_buffer_string=把消息列表转成可读字符串的工具函数 | LangChain 提供的格式化工具,把 Message 对象列表转成 "\n\nHuman: ...\nAI: ..." 格式 |
| 78 | memory.chat_memory.get_messages() |
.chat_memory=底层 BaseChatMessageHistory 对象 | 获取 Memory 里存的所有消息(用于传给 get_buffer_string) |
常见坑
- 不限制历史长度,Token 成本会快速失控。
- Memory 存了历史,但调用链没把历史传入 Prompt。
- 把"长期偏好"和"短期会话"混在同一个 Memory 中,语义污染。
生产建议
- 默认从窗口记忆开始(如近 6~10 轮),再评估是否要摘要记忆。
- 长会话建议"窗口 + 摘要"组合,而不是只存原文。
- 历史写入要有脱敏策略,避免日志里留个人隐私或密钥。
最小可运行命令
bash
uv add langchain langchain-openai
uv run python demo03_memory.py
Demo 04 · Output Parser --- 让 LLM 返回结构化数据
本节要学什么?
LLM 默认只返回纯文本 。但在生产环境,你需要 JSON、CSV、Python 对象。Output Parser 就是把 LLM 的"自由文本"变成"结构化数据"的转换器。LangChain 1.0 内置了 JsonOutputParser、PydanticOutputParser、CommaSeparatedListOutputParser 等。
完整演示
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import (
JsonOutputParser, # 把 LLM 输出解析成 JSON 字典(最常用)
PydanticOutputParser, # 把 LLM 输出解析成 Pydantic 模型(最严格,推荐生产用)
CommaSeparatedListOutputParser, # 把 LLM 输出解析成 Python 列表
)
from langchain.output_parsers import RetryOutputParser, OutputFixingParser # 纠错解析器
from langchain_core.runnables import RunnablePassthrough
from pydantic import BaseModel, Field, field_validator # Pydantic:Python 数据验证库
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# ========== 方式 A:JsonOutputParser(简单 JSON 解析)==========
# 定义想要的 JSON 结构(用自然语言描述给 LLM)
json_parser = JsonOutputParser()
print("=== LLM 需要生成的格式 ===")
print(json_parser.get_format_instructions()) # 打印 LLM 看见的格式说明
# 创建模板(把格式说明自动注入到 Prompt 里)
json_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个数据提取助手。"),
HumanMessagePromptTemplate.from_template(
"从以下文本中提取信息,以 JSON 格式返回:\n\n{text}\n\n"
"请严格按照以下格式返回:\n{format}"
),
])
# 自动把 {format} 替换成 JsonOutputParser 的格式说明
chain_json = json_prompt | llm | json_parser
result = chain_json.invoke({
"text": "布鲁斯,37岁,来自深圳,是一名技术合伙人,擅长 Python 和 Java。"
})
print("=== JSON 解析结果 ===")
print(result) # {'name': '布鲁斯', 'age': '37', 'city': '深圳', ...}
print(result["name"]) # 布鲁斯(字典键访问)
# ========== 方式 B:PydanticOutputParser(最推荐,数据验证)==========
# Pydantic 模型:比 JSON Schema 更强大的数据验证(Python 版 TypeScript)
# 优势:类型自动转换、默认值、字段验证、超长报错信息
class PersonInfo(BaseModel):
"""人物信息模型"""
name: str = Field(description="人物姓名") # description=L给LM看的字段说明
age: int = Field(description="人物年龄(必须是整数)")
city: str = Field(description="所在城市")
skills: list[str] = Field(default_factory=list, description="掌握的技能列表")
@field_validator("age")
@classmethod
def age_must_be_positive(cls, v: int) -> int:
"""自定义验证器:年龄必须是正数"""
if v <= 0 or v > 150:
raise ValueError(f"年龄 {v} 不合理!")
return v
# 创建 Pydantic 解析器
pydantic_parser = PydanticOutputParser(pydantic_object=PersonInfo)
# 方式 1:把格式说明注入 Prompt(标准做法)
pydantic_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个数据提取助手。"),
HumanMessagePromptTemplate.from_template(
"从以下文本中提取信息:\n\n{text}\n\n{format}"
),
])
pydantic_prompt = pydantic_prompt.partial(format=pydantic_parser.get_format_instructions())
# 方式 2:用 .from_langchain() 快速创建(LangChain 1.0 新增!)
# prompt = pydantic_parser.get_default_prompt() # 自动包含格式说明
chain_pydantic = pydantic_prompt | llm | pydantic_parser
person: PersonInfo = chain_pydantic.invoke({
"text": "布鲁斯,37岁,深圳技术合伙人,擅长 Python、Java、Go。"
})
print("=== Pydantic 解析结果 ===")
print(f"姓名:{person.name}") # 布鲁斯(直接对象属性访问,比字典更优雅)
print(f"年龄:{person.age}") # 37(字符串 "37" 自动转成 int)
print(f"城市:{person.city}") # 深圳
print(f"技能:{person.skills}") # ['Python', 'Java', 'Go']
# ========== 方式 C:RetryOutputParser(自动纠错)==========
# 当 LLM 返回的格式有瑕疵时(缺字段、多字段),自动让 LLM 重新生成
# 原理:解析失败 → 调用 OutputFixingParser 让另一个 LLM 修复
base_parser = JsonOutputParser()
retry_parser = RetryOutputParser.from_llm(
parser=base_parser, # 基础解析器
llm=llm, # 用来修复错误的 LLM(可以用更便宜的模型)
max_retries=3, # 最多重试 3 次
)
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="提取以下文本中的信息,以 JSON 返回。只返回 JSON,不要其他内容。"),
HumanMessagePromptTemplate.from_template("{text}"),
])
chain_with_retry = prompt | llm | retry_parser
# 即使 LLM 返回了不完美的 JSON,RetryOutputParser 也会尝试修复
# ========== 方式 D:CommaSeparatedListOutputParser(列表解析)==========
list_parser = CommaSeparatedListOutputParser()
list_prompt = ChatPromptTemplate.from_messages([
HumanMessagePromptTemplate.from_template("列出 {subject} 的 {n} 个优点,用逗号分隔。"),
])
list_chain = list_prompt | llm | list_parser
result = list_chain.invoke({"subject": "Python", "n": 5})
print("=== 列表解析结果 ===")
print(result) # ['优点1', '优点2', '优点3', '优点4', '优点5']
print(result[0]) # 第一个元素
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from pydantic import BaseModel, Field, field_validator |
pydantic=数据验证库(FastAPI 的底层依赖), BaseModel=Pydantic模型基类, Field=字段定义器, field_validator=字段验证器 | 导入 Pydantic 三大核心工具(比 JSON Schema 更好用的数据验证) |
| 11 | JsonOutputParser() |
JsonOutputParser=JSON输出解析器 | 把 LLM 返回的文本解析成 Python 字典 |
| 12 | json_parser.get_format_instructions() |
.get_format_instructions()=获取格式说明字符串 | 打印 LLM 需要的输出格式描述(一般是一段自然语言指令) |
| 16 | HumanMessagePromptTemplate.from_template("{text}\n{format}") |
{text}=用户文本占位符, {format}=解析器格式说明占位符 | 把用户输入和格式说明一起拼进 Prompt |
| 22 | `chain_json = json_prompt | llm | json_parser` |
| 25 | result["name"] |
result=Python 字典, ["name"]=键访问 | JsonOutputParser 返回的是 dict,直接用键访问 |
| 30 | class PersonInfo(BaseModel) |
class=定义类, PersonInfo=类名(自定义), BaseModel=Pydantic模型基类 | 定义一个 Pydantic 模型(声明字段、类型、验证规则) |
| 31 | name: str = Field(description="...") |
name=字段名(蛇命名), str=字段类型, Field(...)=字段元数据 | 定义字符串字段,description 是给 LLM 看的说明(决定 LLM 填什么) |
| 32 | skills: list[str] = Field(default_factory=list, ...) |
default_factory=list=默认值工厂(每次创建新实例时调用 list()) | 如果 LLM 没返回 skills,就默认返回空列表(避免 None) |
| 34 | @field_validator("age") |
@field_validator=字段验证器装饰器, "age"=作用于 age 字段 | 声明一个验证方法,检查 age 是否合法 |
| 35 | def age_must_be_positive(cls, v: int) -> int: |
cls=类本身(类方法默认参数), v=字段实际值, -> int=返回类型 | 自定义验证逻辑:年龄必须在 1~150 之间 |
| 36 | raise ValueError(...) |
raise=抛出异常, ValueError=值错误类型 | 不满足条件时报错,Pydantic 会自动把这个字段标记为错误 |
| 45 | pydantic_parser = PydanticOutputParser(pydantic_object=PersonInfo) |
pydantic_object=传入 Pydantic 模型类(不是实例!) | 创建 Pydantic 解析器(会自动调用 PersonInfo 模型验证数据) |
| 51 | pydantic_prompt.partial(format=pydantic_parser.get_format_instructions()) |
.partial()=预填充变量, format=已填充变量名 | 自动把格式说明注入 Prompt(LangChain 推荐做法) |
| 58 | person: PersonInfo = chain_pydantic.invoke(...) |
person=PersonInfo 类型标注, PersonInfo=自定义 Pydantic 类 | 解析结果直接是 PersonInfo 对象,有类型提示(IDE 自动补全友好) |
| 60 | person.name |
person=PersonInfo 对象, .name=属性访问 | 直接用属性访问(比 dict 更优雅,错误时 IDE 会有提示) |
| 65 | RetryOutputParser.from_llm(...) |
RetryOutputParser=重试解析器, from_llm=类方法(从 LLM 创建) | 当基础解析器失败时,自动用 LLM 修复输出格式 |
| 66 | max_retries=3 |
max_retries=最大重试次数 | 连续失败 3 次后放弃(防止无限循环) |
| 76 | CommaSeparatedListOutputParser() |
CommaSeparatedListOutputParser=逗号分隔列表解析器 | 把 "a, b, c" 这样的字符串解析成 ["a", "b", "c"] |
常见坑
- 只让模型"返回 JSON",但没有把格式约束注入 Prompt。
- 解析失败后直接报错退出,没有重试或修复策略。
- Pydantic 模型字段定义不清晰,导致模型输出漂移。
生产建议
- 优先
PydanticOutputParser,让 schema 成为契约。 - 对关键链路增加
RetryOutputParser或失败兜底分支。 - 给解析失败做可观测记录(原始输出、异常、重试次数)。
最小可运行命令
bash
uv add langchain langchain-openai pydantic
uv run python demo04_output_parser.py
Demo 05 · RAG 核心 --- Document Loader + Text Splitter + Vector Store
本节要学什么?
RAG(Retrieval-Augmented Generation)= 检索 + 生成。LangChain 是 RAG 的最佳载体。本 Demo 讲清楚 RAG 的第一步 :如何把文档(PDF、TXT、网页)加载进来,切块 (Chunk),然后存入向量数据库。这是所有 RAG 应用的地基。
完整演示
python
# ========== RAG 第一步:文档加载(Document Loader)==========
from langchain_community.document_loaders import (
TextLoader, # 加载纯文本文件(.txt, .md)
PDFLoader, # 加载 PDF 文件(需要 pip install pypdf)
WebBaseLoader, # 从网页 URL 加载内容
UnstructuredMarkdownLoader, # 更智能的 Markdown 加载器
)
from langchain_text_splitters import (
RecursiveCharacterTextSplitter, # 按字符递归分割(最常用,效果好)
CharacterTextSplitter, # 按单个字符分割(简单场景)
MarkdownTextSplitter, # 按 Markdown 语法分割(保留标题结构)
LanguageTextSplitter, # 按编程语言语法分割(代码文档专用)
)
from langchain_openai import OpenAIEmbeddings # 文本向量化模型
from langchain_community.vectorstores import Chroma # 向量数据库(轻量,支持本地)
# --- 加载文档 ---
# 1. TextLoader:加载本地文本文件
# 假设同目录下有个 essay.txt
loader = TextLoader("essay.txt", encoding="utf-8")
documents = loader.load()
print(f"加载文档数: {len(documents)}")
print(f"第一段内容(前100字): {documents[0].page_content[:100]}...")
print(f"元数据: {documents[0].metadata}") # metadata=文档来源、时间等附加信息
# 2. PDFLoader:加载 PDF(每页单独作为一个 Document)
pdf_loader = PDFLoader("paper.pdf", encoding="utf-8")
pdf_docs = pdf_loader.load()
print(f"PDF 页数: {len(pdf_docs)}")
# 3. WebBaseLoader:从网页加载(适合让 AI 总结文章)
web_loader = WebBaseLoader("https://python.langchain.com/docs/introduction")
web_docs = web_loader.load()
print(f"网页内容(前200字): {web_docs[0].page_content[:200]}")
# --- 文档切块(Text Splitter)---
# 切块的原因:LLM 有上下文窗口限制(不能把整本书塞进去),
# 而且小块检索更精准(相关段落 vs 整本书)
# RecursiveCharacterTextSplitter:按优先级依次尝试分割(["\n\n", "\n", " ", ""])
# 优点:尽量保持语义完整(段落 > 句子 > 词),不会把一句话切成两半
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # chunk_size=每块最大字符数(建议 300~1000)
chunk_overlap=50, # chunk_overlap=块与块之间的重叠字符数( chunk_overlap=50, # chunk_overlap=块与块之间的重叠字符数(建议 chunk_size 的 10%~20%)
length_function=len, # length_function=计算块大小的函数(默认 len=字符数)
add_start_index=True, # add_start_index=True=在元数据里记录每块在原文中的起始位置
)
# 执行切块(返回 Document 列表,每个 Document = 一块文本 + 元数据)
chunks = text_splitter.split_documents(documents)
print(f"切块数量: {len(chunks)}")
print(f"第一块(前100字): {chunks[0].page_content[:100]}")
print(f"元数据(含原文位置): {chunks[0].metadata}")
# 其他切块器(按场景选用)
# MarkdownTextSplitter:按 Markdown 标题/段落切块(适合 md 文件)
md_splitter = MarkdownTextSplitter(chunk_size=300, chunk_overlap=30)
md_chunks = md_splitter.split_text(open("readme.md", encoding="utf-8").read())
# LanguageTextSplitter:按编程语言语法切块(适合代码文档,保留函数/类边界)
from langchain_text_splitters import Language
python_splitter = LanguageTextSplitter(language=Language.PYTHON, chunk_size=500, chunk_overlap=50)
# --- 向量数据库:存储 + 检索 ---
# OpenAI Embeddings:把文本转成 1536 维向量(GPT 系列的Embedding模型)
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # text-embedding-3-small=新版更快更便宜,1536维
api_key="sk-xxxxxxxx"
)
# Chroma:轻量级向量数据库(纯 Python,支持本地文件存储,开发调试首选)
# 生产环境可用 FAISS(Facebook 出品,亿级向量)、Pinecone(云服务)、Milvus(国产开源)
vectorstore = Chroma.from_documents(
documents=chunks, # 要存储的文档块列表
embedding=embeddings, # 用什么向量化模型
persist_directory="./chroma_db", # 持久化到本地目录(重启后数据不丢失)
)
# 保存到磁盘(Chroma 需要手动调用 persist)
vectorstore.persist()
# --- 向量检索:查询最相关的 K 个文档块 ---
# similarity_search:基于语义相似度查找最相关的文档
# query=搜索的文本(不需要完全匹配,Embedding 会做语义理解)
query = "LangChain 的核心概念是什么?"
relevant_docs = vectorstore.similarity_search(query=query, k=3)
# k=返回最相似的 3 个文档块
print(f"检索到 {len(relevant_docs)} 个相关文档块:")
for i, doc in enumerate(relevant_docs, 1):
print(f"\n--- 相关文档 {i}(相似度相关)---")
print(doc.page_content[:200])
# similarity_search_with_score:同时返回相似度分数(0=完全相同,越小越相似)
relevant_docs_with_scores = vectorstore.similarity_search_with_score(query=query, k=3)
for doc, score in relevant_docs_with_scores:
print(f"\n[分数: {score:.4f}] {doc.page_content[:150]}")
# mmr(最大边际相关性):检索时兼顾多样性,避免返回重复内容
mmr_docs = vectorstore.max_marginal_relevance_search(query=query, k=3, fetch_k=10)
# fetch_k=先从向量库取出 10 个候选,再从中选 3 个多样化的
# --- 创建 Retriever(LangChain 1.0 推荐用法)---
# VectorStoreRetriever:把向量库包装成 Retriever 接口
retriever = vectorstore.as_retriever(
search_type="similarity", # similarity=语义相似度(最常用)
search_kwargs={"k": 3} # k=每次返回 3 个最相关块
)
# 使用 Retriever(比直接调 VectorStore 更规范,可被 Chain 直接用)
docs = retriever.invoke("LangChain 是什么?")
print(f"Retriever 返回 {len(docs)} 个文档块")
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | chunk_overlap=50 |
chunk_overlap=块间重叠字符数(保持上下文连续性) | 50字符的重叠让相邻块有上下文衔接(不会在切分处丢失信息) |
| 3 | length_function=len |
length_function=计算文本长度的函数 | len=按字符数计(也可以自定义,比如按 token 数计) |
| 4 | add_start_index=True |
add_start_index=记录原文起始位置 | 方便后续追踪这段文本来自原文哪个位置 |
| 6 | chunks = text_splitter.split_documents(documents) |
.split_documents()=切分文档列表 | 把 Document 对象列表切成更小的 Document 块列表 |
| 13 | MarkdownTextSplitter |
MarkdownTextSplitter=按 Markdown 语法切块 | 保留 # 标题、## 副标题等结构,适合 md 文档 |
| 17 | LanguageTextSplitter(language=Language.PYTHON, ...) |
Language=编程语言枚举, PYTHON=Python语言 | 按函数/类/import 等语法结构切块(适合代码文档) |
| 23 | embeddings = OpenAIEmbeddings(...) |
OpenAIEmbeddings=OpenAI向量化模型客户端 | 把任意文本转成 1536 维浮点数向量 |
| 24 | model="text-embedding-3-small" |
model=Embedding模型名 | 新版 Embedding 模型(比 ada-002 更小更快,价格降 5 倍) |
| 29 | Chroma.from_documents(...) |
Chroma=向量数据库, .from_documents=从文档列表创建 | 把文档块向量化后存入 Chroma,返回 VectorStore 对象 |
| 31 | persist_directory="./chroma_db" |
persist_directory=持久化存储路径 | 把向量库存到本地磁盘(否则内存存储,重启后消失) |
| 34 | vectorstore.persist() |
.persist()=手动持久化(Chroma 默认自动,但显式调用更安全) | 把内存中的向量数据写入磁盘 |
| 40 | vectorstore.similarity_search(query=query, k=3) |
.similarity_search=语义相似度检索, query=查询文本, k=返回数量 | 用 Embedding 把 query 转成向量,在向量库中找最相似的 k 个文档 |
| 44 | similarity_search_with_score |
.similarity_search_with_score=带分数的检索 | 返回 (Document, score) 元组,score 表示相似度(0=完全相同) |
| 48 | max_marginal_relevance_search(query, k, fetch_k) |
mmr=最大边际相关性(一种兼顾相关性和多样性的检索策略) | 先粗筛 fetch_k 个候选,再用 MMR 算法选 k 个多样化的结果 |
| 54 | retriever = vectorstore.as_retriever(...) |
.as_retriever()=把 VectorStore 包装成 Retriever 接口 | Retriever 是 LangChain 的标准检索接口(Chain 的通用输入) |
| 57 | retriever.invoke("...") |
.invoke()=LCEL 统一调用方法 | 用字符串直接检索(Retriever.invoke 等价于 similarity_search) |
常见坑
- 切块过大或过小,分别会导致召回噪声高或上下文丢失。
- 检索时
k固定不调,问答质量很不稳定。 - 向量库更新后没有重建索引或版本管理,结果不可追溯。
生产建议
- 先做离线评估,找到适合你语料的
chunk_size/chunk_overlap。 - 检索默认返回
source元数据,方便答案溯源。 - 把"数据版本 + embedding 模型版本"写入元数据。
最小可运行命令
bash
uv add langchain langchain-openai langchain-community chromadb pypdf beautifulsoup4
uv run python demo05_rag_ingest.py
Demo 06 · RAG Chain --- 完整检索增强生成
本节要学什么?
本 Demo 是 RAG 的核心 ------把 Demo 05 的检索结果接进 LCEL Chain,让 LLM 基于检索到的真实文档回答问题,而不是凭空编造(Hallucination)。这是 LangChain 最多人用的场景。
完整演示
python
# ========== 完整 RAG Chain ==========
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7, api_key="sk-xxx")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small", api_key="sk-xxx")
# --- 加载已有的向量库(不用重新建,用 Demo 05 的)---
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# ========== 方式 A:stuff 模式(把所有文档塞进一次 Prompt,最简单)==========
# stuff=把检索到的所有文档"塞进"一个 Prompt
# 优点:简单,Token 消耗中等 缺点:文档太多时超过上下文窗口
# 适合:检索结果 < 10 个文档的场景
stuff_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content=(
"你是一个知识渊博的助手,基于提供的文档片段回答问题。\n"
"如果文档中没有答案,请直接说「没有找到相关信息」,不要编造。\n"
"每次回答都要注明信息来源。"
)),
MessagesPlaceholder(variable_name="context"), # 检索到的文档塞在这里
HumanMessagePromptTemplate.from_template("问题:{question}"),
])
def format_docs(docs: list) -> str:
"""
把 Document 对象列表格式化成字符串
LCEL 里用这个函数把 retriever 的输出格式化成字符串
"""
return "\n\n".join(
f"[来源 {i+1}] {doc.page_content}" # 加编号方便 LLM 引用
for i, doc in enumerate(docs)
)
# LCEL:检索 → format → prompt → llm → parser
rag_chain = (
{
# RunnablePassthrough:透传参数(把 question 原样传下去)
# retriever.invoke(question):根据 question 检索相关文档
"question": RunnablePassthrough(),
"context": retriever | format_docs, # 先检索,再格式化(管道组合)
}
| stuff_prompt # 把 question + context 注入 prompt
| llm # 调用 LLM
| StrOutputParser() # 解析成字符串
)
# 运行 RAG Chain
result = rag_chain.invoke("LangChain 是什么?有哪些核心概念?")
print("=== RAG 结果 ===")
print(result)
# ========== 方式 B:refine 模式(逐文档处理,迭代优化)==========
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from langchain_core.runnables import RunnablePassthrough
# create_retrieval_chain:LangChain 1.0 推荐的官方 RAG Chain 构造方式
# create_stuff_documents_chain:把文档塞进 prompt(stuff 模式)
retrieval_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个问答助手。根据以下上下文回答用户问题。"),
MessagesPlaceholder(variable_name="context"),
HumanMessagePromptTemplate.from_template("问题:{input}"),
])
document_chain = create_stuff_documents_chain(
llm=llm,
prompt=retrieval_prompt,
)
retrieval_chain = create_retrieval_chain(
retriever, # 检索器
document_chain, # 处理检索结果文档的 Chain
)
response = retrieval_chain.invoke({"input": "LangChain 1.0 有哪些新特性?"})
print("=== Retrieval Chain 结果 ===")
print(f"答案:{response['answer']}")
print(f"涉及的源文档数:{len(response['context'])}")
# ========== 方式 C:map-reduce 模式(大文档集合适用)==========
from langchain.chains.combine_documents.base import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
# map_reduce:把每个文档单独喂给 LLM 生成答案,最后再汇总
# 适合:文档数量 > 10 个,单个文档很长,用 stuff 会超上下文
# 缺点:Token 消耗大(每个文档都要过一次 LLM)
# langchain 的 map_reduceDocumentsChain
from langchain.chains.combine_documents import MapReduceDocumentsChain, ReduceDocumentsChain, StuffDocumentsChain
# Map 阶段:对每个文档单独处理
map_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是文档分析助手。请根据以下文档片段回答问题。"),
HumanMessagePromptTemplate.from_template("问题:{question}\n\n文档:{context}"),
])
# Reduce 阶段:把所有 Map 的结果汇总
reduce_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个汇总助手。请把多个答案合并成一个简洁的回复。"),
HumanMessagePromptTemplate.from_template("多个答案:\n{context}"),
])
# ReduceDocumentsChain:把多个文档(结果)合并
reduce_doc_chain = create_stuff_documents_chain(llm=llm, prompt=reduce_prompt)
reduce_chain = ReduceDocumentsChain(
combine_docs_chain=reduce_doc_chain, # 合并策略(stuff)
collapse_doc_chain=reduce_doc_chain, # collapse=当文档太多时先合并再合并
)
# MapReduceDocumentsChain:Map → Reduce 两阶段链
map_reduce_chain = MapReduceDocumentsChain(
llm_chain=create_stuff_documents_chain(llm=llm, prompt=map_prompt), # 每个文档单独调用
combine_documents_chain=reduce_chain, # 最后合并所有答案
)
# 用 map_reduce_chain 处理超过 context 窗口的大量文档
# result = map_reduce_chain.invoke({"input": "...", "question": "..."})
# ========== RAG + Memory(带上下文的对话 RAG)==========
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# ConversationalRetrievalChain:专门做"带历史的 RAG 对话"
from langchain.chains import ConversationalRetrievalChain
# 每次回答都要参考历史对话内容(比如用户问"它是怎么工作的?"------"它"指代上一轮的"LangChain")
condense_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是对话助手,负责把对话历史和最新问题合并成一个独立问题。"),
MessagesPlaceholder(variable_name="chat_history"),
HumanMessagePromptTemplate.from_template("{question}"),
])
# 合并历史 + 新问题 → 检索 → 回答
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
condense_prompt=condense_prompt, # 把对话历史 + 新问题合并成检索词
memory=None, # 可传入 ConversationMemory(用 Demo 03 的 BufferMemory)
)
chat_history = [
HumanMessage(content="LangChain 支持哪些模型?"),
AIMessage(content="LangChain 支持 OpenAI、Anthropic、Google Gemini、HuggingFace 等主流 LLM 模型。"),
]
result = qa_chain.invoke({
"question": "那 embedding 模型呢?",
"chat_history": chat_history, # 传入历史,自动理解"那...呢"指代什么
})
print("AI 回复:", result["answer"])
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain.chains.combine_documents import create_stuff_documents_chain |
combine_documents=文档组合链模块 | 导入 stuff 模式文档处理链(把多个文档塞进一个 prompt) |
| 3 | from langchain.chains import create_retrieval_chain |
create_retrieval_chain=检索增强生成链工厂函数 | LangChain 1.0 推荐的 RAG Chain 构造方式 |
| 8 | vectorstore = Chroma(persist_directory=..., embedding_function=...) |
embedding_function=embedding 函数参数(Chroma 1.0 新命名) | 从已有目录加载向量库(而不是重新 from_documents) |
| 13 | stuff_prompt = ChatPromptTemplate.from_messages([...]) |
stuff=填充模式(把所有文档塞进一次调用) | 把检索到的所有文档一次性塞进一个 Prompt |
| 17 | MessagesPlaceholder(variable_name="context") |
variable_name="context"=占位符变量名(必须和 format_docs 返回的键名一致) | 在 Prompt 里给检索文档留一个插入位置 |
| 23 | def format_docs(docs: list) -> str: |
format_docs=自定义格式化函数 | 把 Document 对象列表格式化成带编号的字符串,方便 LLM 引用来源 |
| 24 | return "\n\n".join([...]) |
"\n\n".join=双换行拼接, f"[来源 {i+1}]"=来源编号 | 把多个文档用双换行拼接,每段前加 [来源 X] 编号 |
| 30 | RunnablePassthrough() |
RunnablePassthrough=透传组件(把输入原样传递下去) | 在 LCEL 里透传 question 参数(让 question 能同时流向下游) |
| 31 | `"context": retriever | format_docs` | retriever | format_docs=先检索再格式化 |
| 33 | ` | stuff_prompt | llm |
| 39 | rag_chain.invoke("LangChain 是什么?") |
.invoke()=同步调用(传入字符串,RunnablePassthrough 自动处理) | 一句话触发完整 RAG 流程 |
| 43 | create_stuff_documents_chain(llm=llm, prompt=retrieval_prompt) |
create_stuff_documents_chain=stuff模式文档链工厂函数 | 创建处理文档列表的 Chain(把 docs 塞进 prompt) |
| 45 | create_retrieval_chain(retriever, document_chain) |
create_retrieval_chain=检索+问答链工厂 | 创建完整 RAG Chain(检索 → 文档处理 → 回答) |
| 48 | response["answer"] |
response=返回字典, ["answer"]=答案键 | create_retrieval_chain 返回 dict,含 answer + context |
| 49 | response["context"] |
["context"]=检索到的源文档列表 | 查看 RAG 用到了哪些文档(方便调试和溯源) |
| 58 | MapReduceDocumentsChain |
MapReduce=先分后合模式(Map=每个文档单独处理,Reduce=合并结果) | 适合文档量大的场景(避免超过上下文限制) |
| 67 | ReduceDocumentsChain(combine_docs_chain=..., collapse_doc_chain=...) |
combine_docs_chain=合并策略, collapse_doc_chain=压缩合并策略 | 先 reduce 再 collapse(文档太多时先压缩数量再合并) |
| 72 | ConversationalRetrievalChain.from_llm(...) |
ConversationalRetrievalChain=对话检索链(专门做多轮RAG) | 支持"它"、"那"、"这些"等指代消解(理解上下文) |
| 74 | condense_prompt=condense_prompt |
condense_prompt=历史压缩提示词 | 把 chat_history + 新问题合并成一个独立检索词(解决指代问题) |
| 75 | memory=None |
memory=可传入 ConversationMemory | 如果不传 memory,需在每次 invoke 时手动传 chat_history |
常见坑
- 回答阶段没附来源文档,用户无法判断答案可信度。
- 只测"能答对",不测"答错时是否胡编"。
- 把检索上下文无脑塞满 Prompt,导致成本和延迟飙升。
生产建议
- 输出里始终附
sources或context摘要,提升可解释性。 - 对高风险问题加"无答案就拒答"策略。
- 使用
create_retrieval_chain保持链路标准化,便于维护。
最小可运行命令
bash
uv add langchain langchain-openai langchain-community chromadb
uv run python demo06_rag_chain.py
Demo 07 · Tool + Agent --- 让 AI 自主调用工具
本节要学什么?
Agent(智能体)是 LangChain 的最强大特性 ------AI 不是一条道走到黑,而是有"思考能力":看问题 → 决定是否调用工具 → 调用哪个工具 → 看结果 → 继续推理或给出最终答案。LangChain 1.0 用 @tool 装饰器定义工具,用 create_react_agent 构建 ReAct 推理 Agent。
完整演示
python
# ========== 定义 Tool(LangChain 1.0 新语法)==========
from langchain_core.tools import tool # @tool 装饰器(LangChain 1.0 新增)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.utils.openai_functions import convert_to_openai_function_schema
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# --- 用 @tool 装饰器定义工具(LangChain 1.0 最简洁的方式)---
@tool
def multiply(a: int, b: int) -> int:
"""
计算两个整数的乘积。
当你需要做乘法运算时使用这个工具。
Args:
a: 第一个整数
b: 第二个整数
Returns:
两个整数的乘积(a * b)
"""
return a * b
@tool
def get_weather(city: str) -> str:
"""
查询指定城市的当前天气。
当用户问某个城市的天气时使用这个工具。
Args:
city: 城市名称(中文或英文都可以)
Returns:
城市的天气情况描述字符串
"""
# 模拟天气 API(实际项目替换成真实 API,如心知天气、OpenWeatherMap)
weather_db = {
"深圳": "☀️ 晴天,28°C,湿度 75%",
"北京": "🌤️ 多云,22°C,湿度 45%",
"上海": "🌧️ 小雨,18°C,湿度 85%",
}
return weather_db.get(city, f"抱歉,暂未收录 {city} 的天气数据")
@tool
def search_web(query: str) -> str:
"""
在互联网上搜索信息。
当用户询问实时信息、最新新闻、不确定的知识时使用。
Args:
query: 搜索关键词
Returns:
搜索结果摘要字符串
"""
return (
f"关于「{query}」的搜索结果(模拟):\n"
f"1. 来自 Wikipedia:{query} 是一个重要概念...\n"
f"2. 来自知乎:{query} 的最新发展...\n"
f"3. 来自 GitHub:相关开源项目地址..."
)
# 查看自动生成的工具描述(LLM 靠这个决定调用哪个工具)
print("=== 工具定义(OpenAI Function Schema)===")
print(multiply.name) # multiply(函数名)
print(multiply.description) # 工具描述(LLM 读这个决定何时调用)
print(multiply.args) # 参数 schema(LLM 填参数时参考)
# ========== 绑定工具到 LLM ==========
# OpenAI 官方做法:用 bind_tools 把工具列表绑定到模型
# 绑定后,LLM 会自动判断是否需要调用工具,并生成符合 schema 的参数
llm_with_tools = llm.bind_tools(
tools=[multiply, get_weather, search_web],
tool_choice="auto", # tool_choice="auto"=让模型自己决定调用哪个(默认)
# tool_choice={"type": "function", "function": {"name": "multiply"}}=强制指定
)
# ========== 简单做法:create_react_agent(全自动 Agent)==========
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
# Hub:LangChain 内置提示词库(ReAct 格式提示词)
# ReAct = Reasoning + Acting(边推理边行动)
react_prompt = hub.pull("hwchase17/react") # 从 LangChain Hub 拉取标准 ReAct 提示词
print("=== ReAct 提示词结构 ===")
print(react_prompt.template[:300], "...")
# 创建 ReAct Agent
react_agent = create_react_agent(
llm=llm, # LLM(这里不用 bind_tools,create_react_agent 内部处理)
tools=[multiply, get_weather, search_web], # 可用工具列表
prompt=react_prompt, # 推理提示词(决定 Agent 怎么思考)
)
# AgentExecutor:运行 Agent 的引擎(处理循环、错误、超时等)
agent_executor = AgentExecutor.from_agent_and_tools(
agent=react_agent,
tools=[multiply, get_weather, search_web],
max_iterations=10, # 最大推理步数(防止无限循环)
max_execution_time=60, # 最大执行时间(秒)
handle_parsing_errors=True, # 解析错误时自动修复重试
verbose=True, # 打印思考过程(调试用)
)
# ========== 运行 Agent ==========
print("\n=== Agent 执行演示 ===")
result = agent_executor.invoke({"input": "深圳现在的天气怎么样?"})
print("\n=== 最终输出 ===")
print(result["output"])
result2 = agent_executor.invoke({"input": "请问 123 乘以 456 等于多少?"})
print("\n=== 最终输出 ===")
print(result2["output"])
result3 = agent_executor.invoke({
"input": "深圳天气怎么样?然后把 37 和 42 这两个数相乘"
})
print("\n=== 最终输出 ===")
print(result3["output"])
# ========== OpenAI Functions Agent(LangChain 1.0 推荐,兼容 Function Calling)==========
# create_openai_functions_agent:利用 OpenAI Function Calling 能力
# 比 ReAct 更稳定、更快(OpenAI 底层做了优化)
from langchain.agents import create_openai_functions_agent
functions_agent = create_openai_functions_agent(
llm=llm,
tools=[multiply, get_weather, search_web],
prompt=ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个乐于助人的助手,可以调用工具来回答问题。"),
MessagesPlaceholder(variable_name="chat_history", optional=True),
HumanMessagePromptTemplate.from_template("{input}"),
]),
)
functions_executor = AgentExecutor.from_agent_and_tools(
agent=functions_agent,
tools=[multiply, get_weather, search_web],
verbose=True,
)
print("\n=== Functions Agent 执行 ===")
result4 = functions_executor.invoke({"input": "今天北京冷不冷?"})
print(result4["output"])
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain_core.tools import tool |
langchain_core.tools=工具核心模块, tool=@tool装饰器 | 导入 @tool 装饰器(LangChain 1.0 最简洁的 Tool 定义方式) |
| 6 | @tool |
@tool=装饰器(把普通函数转成 LangChain Tool) | 加在函数定义前,自动生成 name/description/args_schema |
| 7 | def multiply(a: int, b: int) -> int: |
def=定义函数, multiply=工具名(自动成为 tool.name), a: int=参数+类型注解 | 定义一个乘法工具(类型注解让 LangChain 自动生成 JSON Schema) |
| 8 | """...""" |
文档字符串(docstring) | LangChain 自动把这个解析成 tool.description |
| 10 | Args: |
Args=参数说明section(Pydantic/LangChain 标准格式) | Args 里的每个参数会被解析成 tool.args 中的参数描述 |
| 17 | return a * b |
return=返回值 | 工具的实际执行逻辑(LLM 会拿到这个返回值) |
| 25 | weather_db.get(city, f"抱歉...") |
.get(key, default)=字典安全取值 | 城市不存在时返回默认值(而不是报错) |
| 41 | llm.bind_tools(tools=[...], tool_choice="auto") |
.bind_tools()=绑定工具到 LLM, tool_choice="auto"=让模型自己判断用哪个 | 绑定后 LLM 会输出 Function Call(结构化调用指令)而非普通文本 |
| 47 | from langchain import hub |
hub=LangChain 提示词中心(大量预置提示词) | 从 LangChain 官方 Hub 拉取经过验证的提示词 |
| 48 | hub.pull("hwchase17/react") |
.pull()=从 Hub 下载提示词, "hwchase17/react"=ReAct 标准提示词 ID | ReAct=Reasoning + Acting(思考→行动→观察→再思考的循环) |
| 52 | create_react_agent(llm=llm, tools=[...], prompt=...) |
create_react_agent=ReAct Agent 工厂函数 | 创建基于 ReAct 框架的 Agent(内置推理循环) |
| 57 | AgentExecutor.from_agent_and_tools(...) |
AgentExecutor=Agent 执行引擎, from_agent_and_tools=工厂方法 | 创建 AgentExecutor(负责运行 Agent、处理循环、控制超时) |
| 58 | max_iterations=10 |
max_iterations=最大迭代次数(防止无限循环) | Agent 超过 10 步推理后强制停止(安全保护) |
| 61 | handle_parsing_errors=True |
handle_parsing_errors=解析错误自动修复 | LLM 返回格式不对时,自动让 LLM 重试(容错机制) |
| 71 | result = agent_executor.invoke({"input": "..."}) |
.invoke()=执行 Agent, {"input": ...}=输入字典 | 触发 Agent 运行(内部会循环:思考→工具调用→看结果→再思考) |
| 78 | create_openai_functions_agent |
create_openai_functions_agent=OpenAI Functions Agent 工厂 | 利用 OpenAI Function Calling API 的 Agent(比 ReAct 更稳定) |
| 79 | llm=llm |
llm=大语言模型(需支持 tool/function calling) | 选择具备工具调用能力的模型;若不支持,Agent 工具链会受限 |
| 83 | tools=[multiply, get_weather, search_web] |
tools=可用工具列表(顺序影响模型选择偏好) | 告诉 Agent 有哪些工具可用(工具顺序也会影响模型的选择倾向) |
常见坑
- 工具描述不清楚,模型不会调或乱调。
- 工具函数副作用太强(写库、扣费)且没有幂等保护。
- Agent 迭代上限过大,异常时可能长时间循环。
生产建议
- 每个工具都写清输入、输出、失败行为和边界条件。
- 工具调用加超时、重试、审计日志和权限控制。
- 先用"确定性链"解决问题,只有必要时再上 Agent。
最小可运行命令
bash
uv add langchain langchain-openai
uv run python demo07_tools_agent.py
Demo 08 · Callback --- 实时监控 Chain 执行全过程
本节要学什么?
Callback 是 LangChain 的可观测性基础设施 ------它让你在 Chain 执行过程中的任意节点插入钩子(开始、结束、出错、令牌统计等),实现:流式输出(Streaming)、计费统计、调试日志、进度条等。LangChain 1.0 的 Callback 全面支持 LCEL 的 .with_config() 语法。
完整演示
python
# ========== Callback 系统 --- 监控 LangChain 执行全过程 ==========
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.callbacks import (
BaseCallbackHandler, # 回调基类(所有回调都继承它)
CallbackManager, # 回调管理器(注册回调的容器)
StreamingLangchainCallbackHandler, # 流式回调处理器
)
from langchain_core.callbacks.base import BaseCallbackManager
from langchain_core.outputs import LLMResult # LLM 输出结果对象
from langchain_core.messages import BaseMessage # 消息基类
from typing import Any, Optional, List, Dict
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
prompt = ChatPromptTemplate.from_template("用三句话讲一个关于 {topic} 的笑话")
chain = prompt | llm | StrOutputParser()
# ========== 自定义 Callback(打印 Chain 执行详情)==========
class MyCallbackHandler(BaseCallbackHandler):
"""
自定义回调处理器:监听 LLM 调用和 Chain 执行全过程
继承 BaseCallbackHandler 并重写感兴趣的方法即可
"""
def on_chain_start(
self,
serialized: Dict[str, Any], # serialized=被调用的 Chain/工具的序列化信息
inputs: Dict[str, Any], # inputs=输入数据(字典)
**kwargs, # **kwargs=保留给未来扩展
) -> None:
"""on_chain_start:Chain 开始执行时触发"""
logger.info(f"🔵 Chain 开始 | 输入: {inputs}")
def on_chain_end(
self,
outputs: Dict[str, Any], # outputs=Chain 的最终输出
**kwargs,
) -> None:
"""on_chain_end:Chain 执行完成时触发(成功或失败都触发)"""
logger.info(f"🟢 Chain 结束 | 输出: {str(outputs)[:100]}")
def on_chain_error(
self,
error: BaseException, # error=发生的异常对象
**kwargs,
) -> None:
"""on_chain_error:Chain 执行出错时触发(在 on_chain_end 之前)"""
logger.error(f"🔴 Chain 出错 | 错误: {error}")
def on_llm_start(
self,
serialized: Dict[str, Any],
prompts: List[str], # prompts=发给 LLM 的 prompt 列表
**kwargs,
) -> None:
"""on_llm_start:LLM 开始推理时触发"""
logger.info(f"🔵 LLM 开始推理 | Prompt: {str(prompts)[:100]}...")
def on_llm_end(
self,
response: LLMResult, # response=LLM 的完整输出(包含 token 使用量)
**kwargs,
) -> None:
"""on_llm_end:LLM 推理结束时触发"""
# LLMResult.generations[0][0] = 第一个候选回复(我们只发了一个)
generation = response.generations[0][0]
logger.info(f"🟢 LLM 结束 | 回复: {generation.text[:50]}...")
# 统计 Token 使用量(计费关键!)
if response.llm_output and "token_usage" in response.llm_output:
usage = response.llm_output["token_usage"]
logger.info(
f"📊 Token 统计 | "
f"Prompt: {usage.get('prompt_tokens', 'N/A')} | "
f"Completion: {usage.get('completion_tokens', 'N/A')} | "
f"总计: {usage.get('total_tokens', 'N/A')}"
)
def on_tool_start(
self,
serialized: Dict[str, Any],
input_str: str,
**kwargs,
) -> None:
"""on_tool_start:工具开始执行时触发"""
tool_name = serialized.get("name", "unknown")
logger.info(f"🔵 工具开始 | {tool_name} | 输入: {input_str[:50]}...")
def on_tool_end(
self,
output: str,
**kwargs,
) -> None:
"""on_tool_end:工具执行结束时触发"""
logger.info(f"🟢 工具结束 | 输出: {output[:50]}...")
# ========== 使用 Callback ==========
callback_handler = MyCallbackHandler()
# 方式 A:with_config()(LangChain 1.0 推荐,LCEL 原生语法)
result = chain.invoke(
{"topic": "程序员"},
config={"callbacks": [callback_handler]} # config=执行配置,callbacks=回调列表
)
print("最终结果:", result)
# ========== Streaming Callback(流式输出,逐字显示)==========
print("\n=== Streaming 流式输出演示 ===")
class StreamingCallbackHandler(BaseCallbackHandler):
"""流式回调:实现打字机效果"""
def on_llm_new_token(
self,
token: str, # token=新生成的 token(一个字或几个字符)
chunk: Any = None, # chunk=完整的 LLM 输出块(用于调试)
**kwargs,
) -> None:
"""on_llm_new_token:每个新 token 生成时触发(流式输出核心!)"""
print(token, end="", flush=True) # flush=True=实时刷新(不用等换行)
# 配合前端 SSE(Server-Sent Events)可实现 Web 实时流式输出
# 绑定流式回调到 LLM
streaming_llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.7,
streaming=True, # streaming=True=开启流式模式(token 会通过回调实时到达)
callbacks=[StreamingCallbackHandler()], # 绑定流式回调
)
streaming_chain = prompt | streaming_llm | StrOutputParser()
print("流式输出:")
streaming_chain.invoke({"topic": "Python 编程"}) # 逐字打印,不用等全部生成
print("\n")
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain_core.callbacks import BaseCallbackHandler |
callbacks=回调模块, BaseCallbackHandler=回调基类 | 所有自定义回调都继承此类并重写对应方法 |
| 8 | class MyCallbackHandler(BaseCallbackHandler): |
自定义回调类名, BaseCallbackHandler=继承的基类 | 定义一个自己的回调处理器 |
| 14 | def on_chain_start(self, serialized, inputs, **kwargs): |
on_chain_start=Chain开始时回调方法, serialized=序列化信息(名字、参数等), inputs=输入数据 | Chain 开始执行时触发(可在这里打印日志、增加监控) |
| 21 | def on_chain_end(self, outputs, **kwargs): |
on_chain_end=Chain结束时回调方法 | Chain 执行完成后触发(不管成功还是失败) |
| 28 | def on_chain_error(self, error, **kwargs): |
on_chain_error=Chain出错时回调方法, error=异常对象 | 发生异常时触发(在 on_chain_end 之前) |
| 35 | def on_llm_start(self, serialized, prompts, **kwargs): |
on_llm_start=LLM推理开始回调, prompts=发送给LLM的prompt列表 | LLM 开始"思考"前触发(可在这里计费) |
| 41 | def on_llm_end(self, response: LLMResult, **kwargs): |
on_llm_end=LLM推理结束回调, response=LLM完整输出对象 | LLM 回答完成后触发(可在这里取 Token 使用量) |
| 45 | response.generations[0][0] |
generations=LLM候选回复列表, [0][0]=第一个模型、第一个候选 | LLMResult.generations 是嵌套列表(多模型 x 多候选) |
| 49 | usage.get('prompt_tokens', 'N/A') |
.get(key, default)=字典安全取值 | 取出 token 统计(避免字段不存在时报错) |
| 58 | def on_llm_new_token(self, token, chunk, **kwargs): |
on_llm_new_token=每个新token时触发(流式核心) | 每生成一个 token 触发一次(配合 SSE 实现流式打字机效果) |
| 62 | print(token, end="", flush=True) |
end=""=打印后不换行(同行追加), flush=True=立即刷新输出 | 逐 token 实时打印(flush 防止缓冲延迟) |
| 68 | streaming_llm = ChatOpenAI(streaming=True, callbacks=[...]) |
streaming=True=开启流式, callbacks=绑定流式回调 | 开启流式后,token 通过回调逐步到达;.invoke() 仍可返回最终结果 |
| 73 | streaming_chain.invoke({"topic": "..."}) |
.invoke()=触发流式 Chain | 逐字打印,不用等 LLM 完全生成(用户体验大幅提升) |
常见坑
- 在回调里做重计算或 I/O,导致链路变慢。
- 只记录成功日志,不记录异常和 token 使用量。
- 流式输出和普通输出混用时,没有统一事件协议。
生产建议
- 回调只做轻量逻辑,重任务异步投递到后台。
- 至少记录
latency / token / error / trace_id四类指标。 - 统一
config={"callbacks": [...]}注入方式,便于全链路追踪。
最小可运行命令
bash
uv add langchain langchain-openai
uv run python demo08_callbacks.py
Demo 09 · LCEL 进阶 --- Runnable 家族与自定义组件
本节要学什么?
LangChain 1.0 的所有组件(Prompt、LLM、Parser、Retriever)都是 Runnable 的子类,它们有统一的 .invoke() / .batch() / .stream() 接口。本 Demo 讲清楚 Runnable 家族的各种组合技巧:分支、并行、动态链、CoT(思维链)。
完整演示
python
# ========== LCEL Runnable 进阶技巧 ==========
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.runnables import (
RunnablePassthrough, # 透传输入(原样传下去)
RunnableLambda, # 把任意 Python 函数转成 Runnable
RunnableBranch, # 条件分支(if/elif/else 的 LCEL 版本)
RunnableParallel, # 并行执行多个 Runnable
chain as lc_chain, # @chain 装饰器(把函数变成 LCEL 链)
)
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from typing import Literal # Python 3.10+ 类型联合
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# ========== 1. RunnableParallel --- 并行执行多个任务 ==========
# 场景:同一个问题,同时执行"LLM 回答 + 本地规则摘要"
parallel_prompt = ChatPromptTemplate.from_template("{topic} 是什么?用一句话回答。")
parallel_chain = RunnableParallel(
{
# dict 的 key 会成为输出字典的键
"openai_answer": parallel_prompt | llm | StrOutputParser(),
# 也可以用 lambda 写更灵活的并行逻辑
"summary": RunnableLambda(lambda x: f"这是关于{x['topic']}的主题分析"),
}
)
result = parallel_chain.invoke({"topic": "LangChain"})
print("=== 并行结果 ===")
print("OpenAI 回答:", result["openai_answer"])
print("摘要:", result["summary"])
# ========== 2. RunnableBranch --- 条件分支 ==========
# 场景:根据用户输入类型,决定走不同的处理流程
# RunnableBranch = [ (条件1, Runnable1), (条件2, Runnable2), ..., default ]
from langchain_core.runnables import RunnableBranch
branch_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个分类助手。"),
HumanMessagePromptTemplate.from_template("请判断以下文本是【代码】还是【普通文本】或【数学问题】:\n\n{input}"),
])
def extract_code(text: str) -> str:
"""如果检测到是代码,返回代码提取结果"""
return f"[代码块]\n{text}\n[/代码块]"
def extract_math(text: str) -> str:
"""如果检测到是数学问题,返回计算结果"""
return f"[数学分析]\n{text}\n[/数学分析]"
def extract_normal(text: str) -> str:
"""普通文本直接返回"""
return f"[普通文本]\n{text}\n[/普通文本]"
# 条件分支链
branch_chain = RunnableBranch(
# 每项:(条件Runnable, 处理Runnable)
(
RunnableLambda(lambda x: "代码" in x["input"] or "def " in x["input"] or "class " in x["input"]),
RunnableLambda(lambda x: extract_code(x["input"])) | llm | StrOutputParser()
),
(
RunnableLambda(lambda x: any(k in x["input"] for k in ["+", "-", "*", "/", "=", "方程"])),
RunnableLambda(lambda x: extract_math(x["input"])) | llm | StrOutputParser()
),
# default(没有匹配时走这里,条件写 lambda x: True)
RunnableLambda(lambda x: extract_normal(x["input"])) | llm | StrOutputParser(),
)
# ========== 3. @chain 装饰器 --- 用普通函数定义复杂 LCEL 链 ==========
# @chain:把函数转成 LCEL Runnable(函数内部用 yield 产生输出)
@lc_chain
def my_custom_chain(input_dict: dict) -> str:
"""
自定义 Chain:先用 LLM 生成,再用另一个 LLM 翻译
@lc_chain 让这个函数可以和其他 Runnable 用 | 拼接
"""
# 第一步:生成内容
first_prompt = ChatPromptTemplate.from_template(
"用一句话介绍 {topic},风格要幽默"
)
first_result = (first_prompt | llm | StrOutputParser()).invoke({"topic": input_dict["topic"]})
# 第二步:翻译(把第一步结果当作变量传入)
second_prompt = ChatPromptTemplate.from_template(
"把以下内容翻译成英文:\n{content}"
)
second_result = (second_prompt | llm | StrOutputParser()).invoke({"content": first_result})
# 第三步:返回最终结果(yield = 返回给下游)
yield second_result
# 测试 @lc_chain 装饰器
result = my_custom_chain.invoke({"topic": "LangChain"})
print("自定义 Chain 结果:", result)
# ========== 4. LCEL + RunnableLambda --- 自定义转换逻辑 ==========
def add_suffix(text: str) -> str:
"""在文本末尾加后缀(作为 RunnableLambda)"""
return text + "\n\n[由 LangChain LCEL 自动处理]"
def filter_short(text: str) -> str:
"""过滤太短的回复(< 10 字)"""
if len(text) < 10:
return "[内容太短,已过滤]"
return text
chain_with_lambda = (
ChatPromptTemplate.from_template("介绍一下 {topic},不少于100字")
| llm
| StrOutputParser()
| RunnableLambda(add_suffix) # 后处理:加后缀
| RunnableLambda(filter_short) # 后处理:过滤太短的回复
)
result = chain_with_lambda.invoke({"topic": "Python 编程语言"})
print("带后处理的 Chain 结果:", result)
# ========== 5. CoT(Chain of Thought)--- 思维链 ==========
# CoT = 让 LLM 先输出推理过程,再给出答案(显著提升复杂推理质量)
cot_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content=(
"你是一个逻辑推理助手。请按以下步骤回答问题:\n"
"步骤 1:理解问题(用自己的话复述问题)\n"
"步骤 2:列出关键信息\n"
"步骤 3:逐步推理\n"
"步骤 4:给出最终答案\n"
"请用【步骤1】【步骤2】【步骤3】【步骤4】格式回答。"
)),
HumanMessagePromptTemplate.from_template("问题:{question}"),
])
cot_chain = cot_prompt | llm | StrOutputParser()
result = cot_chain.invoke({"question": "如果所有的猫都喜欢鱼,A 是猫,B 喜欢鱼,那么 A 和 B 一定都是猫吗?"})
print("=== CoT 推理 ===")
print(result)
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from langchain_core.runnables import RunnablePassthrough, RunnableLambda, ... |
runnables=可运行组件模块, RunnablePassthrough=透传组件, RunnableLambda=函数转Runnable | LangChain 1.0 的 Runnable 家族(统一接口:.invoke/.batch/.stream) |
| 6 | RunnableParallel({...}) |
RunnableParallel=并行执行多个任务的 Runnable | 给 dict,会把值并行执行,结果按 key 组成字典 |
| 8 | "openai_answer": ... |
dict 的 key=输出字典的键名 | parallel 结果是 |
| 11 | RunnableLambda(lambda x: ...) |
RunnableLambda=把 Python lambda/函数转成 Runnable | 作用:在 LCEL 里插入自定义 Python 逻辑 |
| 16 | result["openai_answer"] |
result=字典, ["openai_answer"]=按 key 取并行结果 | RunnableParallel 返回字典,按 key 访问各分支结果 |
| 20 | RunnableBranch([...]) |
RunnableBranch=LCEL 条件分支(if/elif/else) | 根据条件选择不同的处理分支 |
| 23 | RunnableLambda(lambda x: "代码" in x["input"]) |
条件:检查 input 是否含"代码"相关词 | RunnableBranch 的每个条件必须是返回 bool 的 Runnable |
| 24 | `... | llm | StrOutputParser()` |
| 28 | RunnableBranch([...], RunnableLambda(...)) |
末尾可加 default(无条件,写 lambda x: True) | 所有条件都不满足时走 default |
| 35 | @lc_chain |
@chain=装饰器(把普通函数变成 LCEL Runnable) | 让函数内部能用 .invoke()/.batch()/.stream() 系列方法 |
| 37 | def my_custom_chain(input_dict: dict) -> str: |
函数签名:接收字典,返回字符串 | 函数内部可以调用任意个 LCEL 链 |
| 38 | `first_result = (prompt | llm | StrOutputParser()).invoke(...)` |
| 46 | yield second_result |
yield=返回(让函数变成生成器) | @chain 装饰器让 yield 的值成为 Chain 的输出 |
| 52 | RunnableLambda(add_suffix) |
RunnableLambda(函数名)=把函数转成 LCEL 组件 | 管道里加一步:把上一步输出传给 add_suffix 函数再处理 |
| 57 | `chain_with_lambda = prompt | llm | StrOutputParser() |
| 65 | CoT_prompt = ChatPromptTemplate.from_messages([...]) |
CoT=Chain of Thought(思维链) | 让 LLM 按固定格式输出推理过程(步骤化思考) |
| 73 | "步骤1:{...} 步骤2:{...}" |
步骤格式要求 | 在 SystemMessage 里写清楚格式要求(LLM 会严格遵循) |
常见坑
RunnableParallel分支返回结构不统一,后续解析报错。- 分支条件写得太宽泛,输入经常走错路径。
- Fallback 没有区分"可重试错误"和"逻辑错误"。
生产建议
- 并行输出先定义统一 schema,再交给下游使用。
- 分支逻辑尽量纯函数化,便于单元测试。
- 给关键 Runnable 加 fallback,提升线上可用性。
最小可运行命令
bash
uv add langchain langchain-openai
uv run python demo09_runnable_advanced.py
Demo 10 · LangServe 部署 --- 把 Chain 变成 API 服务
本节要学什么?
LangServe 是 LangChain 1.0 自带的一键 API 部署工具------用 10 行代码把任何 LCEL Chain 部署成 FastAPI 服务,支持:自动文档(OpenAPI/Swagger)、流式输出、SSE(Server-Sent Events)、错误处理、身份验证。本 Demo 演示从写 Chain 到上线 API 的全流程。
完整演示
python
# ========== LangServe 部署 --- 把 LCEL Chain 变成 REST API ==========
# 官方文档:https://python.langchain.com/docs/langserve/
# --------------- server.py --- 部署端(服务器)---------------
from fastapi import FastAPI, HTTPException # FastAPI=高性能 Python Web 框架
from langserve import add_routes # add_routes=自动注册 Chain 为 API 路由
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage
from langchain_core.output_parsers import StrOutputParser
from pydantic import BaseModel, Field # Pydantic:请求体验证
# --- 第 1 步:创建 FastAPI 应用 ---
app = FastAPI(
title="LangChain 助手 API", # API 文档标题
version="1.0", # API 版本号
description="基于 LangChain + GPT-4o-mini 的对话助手",
)
# --- 第 2 步:定义 LCEL Chain ---
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
chat_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="你是一个友好的 AI 助手。请用简洁、有趣的语气回答问题。"),
HumanMessagePromptTemplate.from_template("{question}"),
])
chain = chat_prompt | llm | StrOutputParser()
# --- 第 3 步:一行注册为 API(LangServe 核心!)---
# LangServe 自动生成以下端点:
# POST /chat - 对话接口(普通调用)
# POST /chat/stream - 流式对话(SSE)
# GET /chat/playground - 可视化调试界面(LangServe 内置!)
add_routes(
app=app, # FastAPI app
chain=chain, # 要暴露的 LCEL Chain
path="/chat", # URL 前缀(如 http://localhost:8000/chat)
enable_fe=True, # enable_fe=开启 Playground 可视化界面(调试神器!)
)
# --- 运行方式:uvicorn server:app --reload --port 8000 ---
# 访问 http://localhost:8000/chat/playground 打开可视化调试界面
# 访问 http://localhost:8000/docs 打开 Swagger API 文档
# --------------- client.py --- 调用端(客户端)---------------
import requests # 标准 HTTP 客户端
BASE_URL = "http://localhost:8000/chat"
# --- 方式 A:直接调用(POST 请求)---
response = requests.post(
f"{BASE_URL}/invoke", # /invoke 端点
json={"input": {"question": "LangChain 是什么?"} }, # LangServe 标准格式:{"input": {...}}
timeout=30,
)
print("状态码:", response.status_code) # 200=成功
print("响应:", response.json()) # {"output": "..."}
# --- 方式 B:批量调用(batch)---
batch_response = requests.post(
f"{BASE_URL}/batch",
json={
"inputs": [
{"question": "什么是 RAG?"},
{"question": "什么是 LCEL?"},
{"question": "LangChain 支持哪些模型?"},
]
},
)
print("批量响应:", batch_response.json())
# --- 方式 C:流式调用(SSE --- Server-Sent Events)---
import sseclient # pip install sseclient-py
from urllib.request import urlopen
stream_response = requests.post(
f"{BASE_URL}/stream",
json={"input": {"question": "用一句话介绍 Python"}},
stream=True, # stream=True=流式接收(不等待全部响应)
)
print("流式输出:", end="")
client = sseclient.SSEClient(stream_response)
for event in client.events():
if event.data: # event.data=每次 LLM 生成的新 token
print(event.data, end="", flush=True)
print()
# --- 方式 D:用 LangServe RemoteRunnable 客户端 ---
# from langserve import RemoteRunnable
# remote = RemoteRunnable("http://localhost:8000/chat")
# print(remote.invoke({"question": "LangChain 是什么?"}))
# --------------- 添加自定义端点(超越默认的 /invoke/stream/batch)---------------
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
app = FastAPI()
# 自定义请求体模型(LangServe 支持自定义端点)
class ChatRequest(BaseModel):
question: str = Field(..., description="用户问题")
system_prompt: str = Field(default="你是一个助手", description="系统提示词")
temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="随机性参数")
@app.post("/chat/custom")
def chat_custom(req: ChatRequest):
"""自定义端点:支持指定 system_prompt 和 temperature"""
# 动态创建 Chain(每次请求不同参数)
dynamic_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content=req.system_prompt),
HumanMessagePromptTemplate.from_template("{question}"),
])
# 临时创建 LLM(用请求中的 temperature)
temp_llm = ChatOpenAI(model="gpt-4o-mini", temperature=req.temperature)
dynamic_chain = dynamic_prompt | temp_llm | StrOutputParser()
result = dynamic_chain.invoke({"question": req.question})
return {"answer": result, "model": "gpt-4o-mini", "temperature": req.temperature}
# --------------- Docker 部署配置(langchain-serving Dockerfile)---------------
# FROM python:3.11-slim
# WORKDIR /app
# COPY pyproject.toml ./
# RUN pip install uv && uv sync
# COPY server.py ./
# EXPOSE 8000
# CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
# 构建并运行:
# docker build -t langchain-api .
# docker run -p 8000:8000 -e OPENAI_API_KEY=sk-xxx langchain-api
print("=== LangServe 部署完成 ===")
print("本地运行: uvicorn server:app --reload --port 8000")
print("Playground: http://localhost:8000/chat/playground")
print("API 文档: http://localhost:8000/docs")
逐行解析
| 行号 | 内容 | 逐词解释 | 作用 |
|---|---|---|---|
| 1 | from fastapi import FastAPI |
FastAPI=Python 高性能 Web 框架(自动生成 OpenAPI 文档) | 创建 Web API 服务 |
| 2 | from langserve import add_routes |
langserve=LangChain 官方 API 部署库, add_routes=注册 Chain 为 API 路由 | 把 LCEL Chain 一键暴露成 REST API |
| 9 | app = FastAPI(title="...", version="...", description="...") |
FastAPI 实例化 | 创建 Web 应用并配置元信息(影响 /docs 页面显示) |
| 17 | chat_prompt = ChatPromptTemplate.from_messages([...]) |
定义一个聊天提示模板 | LangServe 暴露的 Chain 必须是 LCEL Runnable |
| 20 | `chain = chat_prompt | llm | StrOutputParser()` |
| 23 | add_routes(app=app, chain=chain, path="/chat", enable_fe=True) |
add_routes=注册路由函数, path="/chat"=URL前缀, enable_fe=True=开启 Playground UI | LangServe 自动生成 3 个端点:/invoke, /stream, /batch |
| 25 | enable_fe=True |
enable_fe=开启前端可视化调试界面(LangServe Playground) | 访问 /chat/playground 打开 Web UI(调试 Chain 必备) |
| 29 | uvicorn server:app --reload --port 8000 |
uvicorn=ASGI 服务器(运行 FastAPI 的生产服务器), --reload=开发模式热重载 | 启动 LangServe 服务(开发用 --reload,生产不用) |
| 35 | requests.post(f"{BASE_URL}/invoke", json={"input": {...}}) |
/invoke=LangServe 标准调用端点, json={"input": {...}}=标准请求体格式 | 客户端调用 LangServe API |
| 36 | json={"input": {"question": "..."}} |
LangServe 标准请求格式:{"input": {"变量名": 值}} | LangServe 的 API 格式固定(不是直接传字典) |
| 49 | stream=True |
stream=True=流式请求参数 | 不等待全部响应,逐步接收数据(SSE 实时流) |
| 50 | sseclient.SSEClient(stream_response) |
SSEClient=Server-Sent Events 客户端 | 监听 SSE 流式事件(逐个 token 接收 LLM 输出) |
| 52 | for event in client.events(): |
.events()=遍历 SSE 事件流 | 每次 event = LLM 生成的一个 token |
| 53 | event.data |
event.data=SSE 事件的数据字段 | LLM 每个新 token 会触发一个 SSE event |
| 62 | class ChatRequest(BaseModel): |
ChatRequest=自定义请求体模型, BaseModel=Pydantic 基类 | 定义请求体验证规则(FastAPI 自动校验) |
| 63 | question: str = Field(..., description="...") |
Field(...)=必填字段定义, description=字段描述(进入 OpenAPI 文档) | 定义 question 为必填字符串参数 |
| 64 | system_prompt: str = Field(default="...") |
default=默认值(请求时不传也不会报错) | 可选参数,有默认值 |
| 66 | temperature: float = Field(default=0.7, ge=0.0, le=2.0) |
ge=最小值(greater or equal), le=最大值(less or equal) | Pydantic 自动校验参数范围(类型+范围双重验证) |
| 70 | `dynamic_chain = dynamic_prompt | temp_llm | StrOutputParser()` |
| 76 | docker build -t langchain-api . |
docker build=构建镜像, -t=命名镜像 | 把 LangServe 服务打包成 Docker 镜像 |
| 77 | docker run -p 8000:8000 -e OPENAI_API_KEY=... langchain-api |
-p=端口映射(宿主机:容器), -e=环境变量 | 容器内运行 API 服务(生产部署标准方式) |
常见坑
- 只在本地调通
/playground,没验证真实客户端请求格式。 - 服务里未设置超时/并发限制,压力上来容易雪崩。
- 直接把敏感配置写进镜像层,存在泄露风险。
生产建议
- 把
invoke/batch/stream都做一次集成测试。 - 加入请求限流、鉴权、超时和错误码规范。
- 通过环境变量注入密钥,并配置日志脱敏。
最小可运行命令
bash
uv add langserve fastapi uvicorn langchain langchain-openai
uv run uvicorn server:app --reload --port 8000
附录 · LangChain 1.0 速查卡
| 概念 | 对应 Demo | 核心 API |
|---|---|---|
| LCEL 管道语法 | Demo 01 | `chain = prompt |
| PromptTemplate | Demo 02 | ChatPromptTemplate.from_messages([...]) |
| MessagesPlaceholder | Demo 03 | MessagesPlaceholder(variable_name="history") |
| Memory | Demo 03 | ConversationBufferMemory / SummaryMemory |
| OutputParser | Demo 04 | JsonOutputParser / PydanticOutputParser |
| Document Loader | Demo 05 | TextLoader / PDFLoader / WebBaseLoader |
| Text Splitter | Demo 05 | RecursiveCharacterTextSplitter |
| Vector Store | Demo 05 | Chroma.from_documents(...) |
| Retriever | Demo 05 | vectorstore.as_retriever() |
| RAG Chain | Demo 06 | create_retrieval_chain / RunnablePassthrough |
| Tool | Demo 07 | @tool 装饰器 |
| Agent | Demo 07 | create_react_agent / create_openai_functions_agent |
| Callback | Demo 08 | BaseCallbackHandler / .with_config(callbacks=[...]) |
| Streaming | Demo 08 | llm(streaming=True) + on_llm_new_token |
| RunnableParallel | Demo 09 | RunnableParallel({...}) |
| RunnableBranch | Demo 09 | RunnableBranch([...], default) |
| @chain 装饰器 | Demo 09 | @lc_chain / @chain |
| LangServe | Demo 10 | add_routes(app, chain, path="/...") |
下一步建议 :在硅基流动(siliconflow.cn)等平台申请免费 API Key,
用
uv add langchain langchain-openai装好依赖,直接跑 Demo 01 验证环境。