LangChain教程-2、Langchian基础

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:怎么落地(推荐路径)?

  1. 用 LCEL 先做"可控单链"(Prompt -> LLM -> Parser)。
  2. 加上结构化输出和错误处理(Parser + Retry)。
  3. 接入 RAG(Retriever + 引用来源)。
  4. 再上 Agent(仅在确实需要多工具决策时)。
  5. 最后做监控与部署(Callback/LangSmith + LangServe/FastAPI)。

LangChain 基础架构(最小闭环)

从一次请求到一次响应,通常经过这几层:

  1. Input:用户问题 + 上下文(可选)
  2. Prompt Layer:模板拼装(系统指令、历史、检索内容)
  3. Reasoning/Tool Layer:模型推理,必要时调用工具
  4. Knowledge Layer:Retriever 从向量库取证据
  5. Output Layer:解析为字符串或结构化 JSON/Pydantic
  6. Observability Layer:记录 token、耗时、错误与链路

一句话记忆:LangChain 不替代模型,而是把模型"工程化"。


竞品情况与选型建议(2026 视角)

常见替代或互补方案:

  • LlamaIndex:数据连接与检索抽象强,RAG 体验好
  • Haystack:传统检索体系成熟,企业搜索场景扎实
  • Semantic Kernel:偏"企业编排 + 插件"风格
  • AutoGen / CrewAI:多 Agent 协作范式更突出
  • Dify / Flowise:低代码编排快,适合业务验证
  • Vercel AI SDK:前端/全栈流式体验非常顺手

简化选型建议:

  1. 你要"Python 工程化 + 细粒度可控编排":优先 LangChain。
  2. 你要"重 RAG 数据管道":LangChain + LlamaIndex 可互补。
  3. 你要"低代码快速验证":先 Dify/Flowise,再迁移到 LangChain。
  4. 你要"复杂多智能体协作":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()=批量调用方法 同时提交多个任务,串行执行,返回列表(适合批量处理文档等场景)

常见坑

  1. Prompt 变量名和 invoke() 传参键不一致,运行时会直接报错。
  2. 在代码里硬编码 api_key,后续很容易泄露到仓库或日志。
  3. temperature 设太高,导致教程结果难复现。

生产建议

  1. 固定基础模型和温度,先保证可复现,再做 A/B 调参。
  2. 统一用环境变量注入密钥,避免在源码里出现 sk-
  3. 所有链都先加 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=流水线模板列表, ("变量名", 模板)=元组 每个元组定义一个子模板,输出会注入到下一个模板的 {变量名} 中

常见坑

  1. MessagesPlaceholder 的变量名与输入键不一致(如 chat_history vs history)。
  2. SystemMessage 里塞过多业务细节,导致 Prompt 难维护。
  3. 模板层数过深但无命名规范,调试时很难定位错误。

生产建议

  1. 用"角色/约束/输出格式"三段式组织系统提示词。
  2. 关键模板加版本号(如 prompt_v1, prompt_v2)便于回滚。
  3. 在调用前打印一次 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)

常见坑

  1. 不限制历史长度,Token 成本会快速失控。
  2. Memory 存了历史,但调用链没把历史传入 Prompt。
  3. 把"长期偏好"和"短期会话"混在同一个 Memory 中,语义污染。

生产建议

  1. 默认从窗口记忆开始(如近 6~10 轮),再评估是否要摘要记忆。
  2. 长会话建议"窗口 + 摘要"组合,而不是只存原文。
  3. 历史写入要有脱敏策略,避免日志里留个人隐私或密钥。

最小可运行命令

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 内置了 JsonOutputParserPydanticOutputParserCommaSeparatedListOutputParser 等。

完整演示

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"]

常见坑

  1. 只让模型"返回 JSON",但没有把格式约束注入 Prompt。
  2. 解析失败后直接报错退出,没有重试或修复策略。
  3. Pydantic 模型字段定义不清晰,导致模型输出漂移。

生产建议

  1. 优先 PydanticOutputParser,让 schema 成为契约。
  2. 对关键链路增加 RetryOutputParser 或失败兜底分支。
  3. 给解析失败做可观测记录(原始输出、异常、重试次数)。

最小可运行命令

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)

常见坑

  1. 切块过大或过小,分别会导致召回噪声高或上下文丢失。
  2. 检索时 k 固定不调,问答质量很不稳定。
  3. 向量库更新后没有重建索引或版本管理,结果不可追溯。

生产建议

  1. 先做离线评估,找到适合你语料的 chunk_size/chunk_overlap
  2. 检索默认返回 source 元数据,方便答案溯源。
  3. 把"数据版本 + 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

常见坑

  1. 回答阶段没附来源文档,用户无法判断答案可信度。
  2. 只测"能答对",不测"答错时是否胡编"。
  3. 把检索上下文无脑塞满 Prompt,导致成本和延迟飙升。

生产建议

  1. 输出里始终附 sourcescontext 摘要,提升可解释性。
  2. 对高风险问题加"无答案就拒答"策略。
  3. 使用 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 有哪些工具可用(工具顺序也会影响模型的选择倾向)

常见坑

  1. 工具描述不清楚,模型不会调或乱调。
  2. 工具函数副作用太强(写库、扣费)且没有幂等保护。
  3. Agent 迭代上限过大,异常时可能长时间循环。

生产建议

  1. 每个工具都写清输入、输出、失败行为和边界条件。
  2. 工具调用加超时、重试、审计日志和权限控制。
  3. 先用"确定性链"解决问题,只有必要时再上 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 完全生成(用户体验大幅提升)

常见坑

  1. 在回调里做重计算或 I/O,导致链路变慢。
  2. 只记录成功日志,不记录异常和 token 使用量。
  3. 流式输出和普通输出混用时,没有统一事件协议。

生产建议

  1. 回调只做轻量逻辑,重任务异步投递到后台。
  2. 至少记录 latency / token / error / trace_id 四类指标。
  3. 统一 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 会严格遵循)

常见坑

  1. RunnableParallel 分支返回结构不统一,后续解析报错。
  2. 分支条件写得太宽泛,输入经常走错路径。
  3. Fallback 没有区分"可重试错误"和"逻辑错误"。

生产建议

  1. 并行输出先定义统一 schema,再交给下游使用。
  2. 分支逻辑尽量纯函数化,便于单元测试。
  3. 给关键 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 服务(生产部署标准方式)

常见坑

  1. 只在本地调通 /playground,没验证真实客户端请求格式。
  2. 服务里未设置超时/并发限制,压力上来容易雪崩。
  3. 直接把敏感配置写进镜像层,存在泄露风险。

生产建议

  1. invoke/batch/stream 都做一次集成测试。
  2. 加入请求限流、鉴权、超时和错误码规范。
  3. 通过环境变量注入密钥,并配置日志脱敏。

最小可运行命令

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 验证环境。

官方文档:https://python.langchain.com/docs/