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候选回复列表, 00=第一个模型、第一个候选 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/