合集 - langChain(4)

1.LangChain教程-1、python基础03-26

2.LangChain教程-2、Langchain基础03-26

3.LangChain教程-3、Langchain进阶03-264.LangChain教程-4、构建简易智能 PPT 生成器03-27

收起

langchain_9_demos_summary

LangChain 1.x 实战教程 · 9 个 Demo

基于 demo_basic 实战代码
前置要求:Python 3.10+、uv、API Key(MiniMax 硅基流动)、Ollama 本地服务
官方文档:https://python.langchain.com/docs/


开场:LangChain 的 3W1H

1) Why:为什么需要 LangChain?

直接调用模型 API 只能解决"单次问答",但真实业务要的是"系统能力":提示词管理、记忆、检索(RAG)、工具调用、多步编排和可观测性。LangChain 的价值,是把这些应用层能力标准化、组件化。

2) What:LangChain 是什么?

LangChain 是一个 LLM 应用编排框架,核心是把不同能力拼成可维护的链路:

  • 模型层:ChatOpenAI / ChatOllama 负责推理与生成
  • 编排层:LCEL 和 Runnable 负责把步骤连起来
  • 数据层:Loader / Splitter / Vector Store / Retriever 负责处理知识
  • 能力层:Tool / Agent / Memory / Parser 负责扩展应用能力

3) Who:谁适合学?

  • 想把模型能力接进业务流程的开发者
  • 想把提示词脚本做成可维护系统的人
  • 想做 RAG、Agent、工具调用、可观测性的人

4) How:怎么把它落地?

先从最小闭环开始:Prompt -> LLM -> Parser
再往上叠加:Memory 解决上下文,Retriever 解决知识,Tool 和 Agent 解决外部能力,Callback 解决可观测性。
最后把这些组件组合成真正可维护的应用链路。

补充:这篇文章怎么读

  • 基于 demo_basic 真实运行代码
  • 使用 MiniMax API(硅基流动)+ Ollama 本地 Embedding
  • 每章节对应一个可运行的 demo 文件

LangChain 架构图

[用户输入]
   ↓
[Prompt / LCEL]
   ↓
[LLM 推理]
   ↓
[Output Parser]
   ↓
[结果输出]

旁路能力:
Memory ──► Prompt
Retriever ──► Prompt
Tool / Agent ◄──► LLM
Callback / Streaming ──► 全链路观测

9 个 Demo 一览

点击下表里的 Demo 名称可直接跳转到对应章节;每个 Demo 标题右侧的  可以返回这里。

Demo 主题 对应文件
D01 LCEL 管道语法 demo01_lcel_basics.py
D02 Prompt Template demo02_prompt_template.py
D03 Memory 记忆 demo03_memory.py
D04 Output Parser demo04_output_parser.py
D05 RAG 核心链路 demo05_rag_ingest.py
D06 RAG Chain demo06_rag_chain.py
D07 Tool + Agent demo07_tools_agent.py
D08 Callback 可观测性 demo08_callbacks.py
D09 LCEL 进阶 demo09_runnable_advanced.py

Demo 01 · LCEL 语法入门 ↑ 返回目录

学习目标

  • ✅ 掌握 LCEL 三件套:ChatPromptTemplate + llm + StrOutputParser
  • ✅ 理解管道符 \| 原理
  • ✅ 掌握 .invoke() / .batch() 调用方式

重点知识强化

知识模块      内容
组件说明    ChatPromptTemplate:定义提示词模板,把变量位预留出来;ChatOpenAI:负责把 prompt 送去模型并拿回结果;StrOutputParser:把模型输出转成纯字符串;from_template():创建模板;.partial():预填固定变量;|:串联提示词、模型和解析器。
学习目标组件: ChatPromptTemplate、ChatOpenAI、StrOutputParser、|.invoke().batch()

 
代码对应    ChatPromptTemplate.from_template(...) 负责定义模板;.partial(profession="技术写作") 负责预填固定变量;prompt | llm | StrOutputParser() 负责串起最小闭环;.invoke() 做单次调用,.batch() 做批量调用。

 
对比理解    format() 只是渲染模板,不会调用模型;.invoke() 才会真正执行整条链;.batch() 适合批量生成;| 表示上游输出自动流入下游输入。

 
常见坑    常见坑是变量名不一致、批量文本过长、调试时直接看模型输出而不先看渲染结果。建议把模板、模型参数和输出解析拆开配置,便于复用、排查和 A/B 测试。

 
真实场景    你要做一个最小问答助手,输入一个主题就输出固定风格的介绍。这个 Demo 讲的就是把“提示词 -> 模型 -> 输出”的最小闭环先跑通。

 

完整代码

# 文件:demo01_lcel_basics.py

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

# 使用 MiniMax 模型(硅基流动)
llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimaxi.com/v1",
    api_key=os.getenv("MINIMAX_API_KEY"),
    temperature=0.7,
    max_tokens=1000,
    request_timeout=60,
    max_retries=2,
)

# Prompt 模板
prompt = ChatPromptTemplate.from_template(
    "你是一位{profession}专家。请用3句话介绍{topic},最后加一个彩蛋笑话。"
).partial(profession="技术写作")

# LCEL 管道
chain = prompt | llm | StrOutputParser()

# 单次调用
result = chain.invoke({"topic": "LangChain 框架"})
print("=== 单次调用 ===")
print(result)

# 渲染后的 Prompt
rendered = prompt.format(topic="Python 装饰器")
print("=== 渲染后的 Prompt ===")
print(rendered)

# 批量调用
results = chain.batch([
    {"topic": "量子计算"},
    {"topic": "区块链"},
    {"topic": "神经网络"},
])
print("=== 批量调用 ===")
for r in results:
    print(r)

demo01_what_when_how_wide


Demo 02 · Prompt Template ↑ 返回目录

学习目标

  • ✅ 掌握 PromptTemplate(纯文本)和 ChatPromptTemplate(消息型)
  • ✅ 理解 MessagesPlaceholder 的作用
  • ✅ 掌握 PipelinePromptTemplate 嵌套模板

重点知识强化

知识模块      内容
组件说明    PromptTemplate:纯文本模板,适合单段变量替换;ChatPromptTemplate:消息模板,适合多角色对话;MessagesPlaceholder:把历史消息插到指定位置;HumanMessagePromptTemplate / SystemMessagePromptTemplate:分别定义用户消息和系统消息;PipelinePromptTemplate:把子模板串起来。
学习目标组件: PromptTemplate、ChatPromptTemplate、MessagesPlaceholder、HumanMessagePromptTemplate、SystemMessagePromptTemplate、PipelinePromptTemplate。

 
代码对应    PromptTemplate.from_template(...) 适合纯文本替换;ChatPromptTemplate.from_messages([...]) 适合多角色对话;MessagesPlaceholder(variable_name="chat_history") 负责插入历史消息;PipelinePromptTemplate(...) 演示模板套模板。

 
对比理解    纯文本模板适合简单变量替换,消息模板更适合对话场景;MessagesPlaceholder 是消息级插入,不是字符串拼接;模板嵌套适合复杂提示词拆分。

 
常见坑    常见坑是历史消息传成字符串、嵌套变量漏填、模板层级越写越复杂。建议系统提示词尽量固定,历史消息只保留必要内容,复杂场景再做模板组合。

 
真实场景    客服、陪伴类、知识问答类应用,都需要把系统人设、历史对话和当前问题拆开管理。这个 Demo 讲的就是提示词模板化。

 

完整代码

# 文件:demo02_prompt_template.py

from langchain_core.prompts import (
    ChatPromptTemplate,
    PromptTemplate,
    MessagesPlaceholder,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate
)
from langchain_core.messages import HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimaxi.com/v1",
    api_key=os.getenv("MINIMAX_API_KEY"),
    temperature=0.7,
    max_tokens=1000,
)

# PipelinePrompt 替代方案(更简洁)
introduction = PromptTemplate.from_template("你是 {character},一个乐于助人的 AI 助手。")
main_template = "{introduction}\n\n用户问:{question}\n\n你的回答:"

def create_pipeline_prompt(character, question):
    intro = introduction.format(character=character)
    return main_template.format(introduction=intro, question=question)

final = create_pipeline_prompt("LangChain 助手", "什么是 RAG?")
print("PipelinePrompt 结果:", final)
response = llm.invoke(final)

# response 是 AIMessage 对象
answer = response.content
token_usage = response.response_metadata["token_usage"]
model_name = response.response_metadata["model_name"]
finish_reason = response.response_metadata["finish_reason"]

print("AI 回复:", answer)
print("Token用量:", token_usage)
print("模型名称:", model_name)
print("结束原因:", finish_reason)

demo02_what_when_how_wide


Demo 03 · Memory 记忆 ↑ 返回目录

学习目标

  • ✅ 掌握窗口记忆 WindowedMemory
  • ✅ 掌握摘要记忆 SummaryMemory
  • ✅ 理解 get_buffer_string 的用法

重点知识强化

知识模块      内容
组件说明    WindowedMemory:只保留最近几轮对话;SummaryMemory:把长对话压缩成摘要;get_buffer_string():把消息列表转成可读文本,方便继续喂给模型;MessagesPlaceholder:把历史消息接入提示词;ChatPromptTemplate / StrOutputParser:负责接回推理链。
学习目标组件: WindowedMemory、SummaryMemory、get_buffer_string

 
代码对应    WindowedMemory(k=2) 保留最近窗口;SummaryMemory(llm, max_history=3) 负责自动摘要;get_buffer_string(...) 把消息列表转成可读上下文;summary_chain 让摘要后的上下文继续参与回答。

 
对比理解    窗口记忆保留原文但会丢掉更早内容,摘要记忆保信息密度但会损失细节。窗口适合短对话,摘要适合长对话,真实项目里通常要结合使用。

 
常见坑    常见坑是只保存在内存里、不做持久化;窗口太短会丢上下文,太长又会浪费 token;摘要会失真。建议长对话先摘要,再保留最近几轮原文,并持久化到 Redis 或数据库。

 
真实场景    用户希望助手“记得我”,但历史又不能无限增长。这个 Demo 讲的是如何把记忆做成一个可控的工程组件。

 

完整代码

# 文件:demo03_memory.py

from langchain_openai import ChatOpenAI
from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    HumanMessagePromptTemplate,
)
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, get_buffer_string
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
import os

load_dotenv()

llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimaxi.com/v1",
    api_key=os.getenv("MINIMAX_API_KEY"),
    temperature=0.7,
    max_tokens=1000,
)

class WindowedMemory:
    """只保留最近 k 条消息的窗口记忆"""
    def __init__(self, k: int = 6):
        self.k = k
        self.messages = []

    def add_user_message(self, text: str):
        self.messages.append(HumanMessage(content=text))
        if len(self.messages) > self.k * 2:
            self.messages = self.messages[-self.k * 2:]

    def add_ai_message(self, text: str):
        self.messages.append(AIMessage(content=text))
        if len(self.messages) > self.k * 2:
            self.messages = self.messages[-self.k * 2:]

    def get_messages(self) -> list:
        return self.messages[-self.k:]

class SummaryMemory:
    """AI 摘要记忆:对话太长时自动压缩"""
    def __init__(self, llm, max_history: int = 20):
        self.llm = llm
        self.max_history = max_history
        self.summary = ""
        self.current_messages = []

    def add_user_message(self, text: str):
        self.current_messages.append(HumanMessage(content=text))

    def add_ai_message(self, text: str):
        self.current_messages.append(AIMessage(content=text))

    def get_context(self) -> str:
        recent = get_buffer_string(self.current_messages[-self.max_history:])
        if self.summary:
            return f"【历史摘要】{self.summary}\n\n【最近对话】\n{recent}"
        return recent

    def should_summarize(self) -> bool:
        return len(self.current_messages) >= self.max_history * 2

    def summarize(self):
        summary_prompt = ChatPromptTemplate.from_messages([
            SystemMessage(content="请把以下对话压缩成一段简短摘要,保留关键信息:"),
            HumanMessagePromptTemplate.from_template("{messages}"),
        ])
        self.summary = (
            summary_prompt | self.llm | StrOutputParser()
        ).invoke({"messages": get_buffer_string(self.current_messages)})
        self.current_messages = []

# 去掉 <think> 标签的辅助函数
def extract_plain_text(summary_with_thinking: str) -> str:
    import re
    text = re.sub(r'<think>.*?', '', summary_with_thinking, flags=re.DOTALL)
    return text.strip()

# 示例
print("\n===== WindowedMemory 示例 =====")
w_memory = WindowedMemory(k=2)
w_memory.add_user_message("你好!")
w_memory.add_ai_message("你好,有什么可以帮你的?")
w_memory.add_user_message("我想了解 LangChain")
w_memory.add_ai_message("LangChain 是一个构建 LLM 应用的框架。")
print("只保留最近 2 条:", w_memory.get_messages())

print("\n===== SummaryMemory 示例 =====")
s_memory = SummaryMemory(llm, max_history=3)
s_memory.add_user_message("我想计划下个月去日本东京旅游")
s_memory.add_ai_message("东京旅游很棒!必去景点包括:浅草寺、东京塔...")
s_memory.add_user_message("签证怎么办理?")
s_memory.add_ai_message("日本旅游签证需要通过旅行社代办...")
s_memory.add_user_message("机票和酒店有推荐吗?")
s_memory.add_ai_message("机票建议提前2个月订,东京往返含税约2500-4500元...")

print(f"消息数: {len(s_memory.current_messages)}, 需摘要: {s_memory.should_summarize()}")

s_memory.summarize()
plain_summary = extract_plain_text(s_memory.summary)
print(f"压缩后摘要: {plain_summary}")

demo03_what_when_how_wide


Demo 04 · Output Parser ↑ 返回目录

学习目标

  • ✅ 掌握 JsonOutputParser
  • ✅ 掌握 PydanticOutputParser + BaseModel
  • ✅ 学会处理 MiniMax 输出的 <think> 标签

重点知识强化

知识模块      内容
组件说明    JsonOutputParser:解析 JSON 字典;PydanticOutputParser:把输出校验成模型对象;CommaSeparatedListOutputParser:解析逗号列表;RunnableLambda:在 parser 前做清洗;clean_json_output() / clean_list_output():去掉 <think> 和中文标点噪声;PersonInfo / field_validator:定义字段和校验规则。
学习目标组件: JsonOutputParser、PydanticOutputParser、BaseModel、clean_json_output、RunnableLambda、CommaSeparatedListOutputParser。

 
代码对应    build_llm() 把温度调到 0.0 降低漂移;chain_json = ... | RunnableLambda(clean_json_output) | json_parser 先清噪音再解析 JSON;PersonInfo 负责 schema 和字段校验;list_chain 负责把输出归一成逗号列表。

 
对比理解    JSON 解析更灵活,Pydantic 解析更严格,列表解析更轻量;parser 负责“解析”,不负责“纠错”。结构越关键,校验就越要严格。

 
常见坑    常见坑是模型看起来像 JSON 但其实不是合法 JSON,中文标点会影响列表解析,只靠提示词也不够稳。建议关键字段做 schema 验证、重试和人工兜底。

 
真实场景    当你需要从简历、工单、合同或订单备注里抽字段时,最怕模型胡说八道。这个 Demo 讲的就是结构化输出。

 

完整代码

# 文件:demo04_output_parser.py

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage
from langchain_core.output_parsers import (
    JsonOutputParser,
    PydanticOutputParser,
    CommaSeparatedListOutputParser,
)
from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, Field, field_validator
import re
from dotenv import load_dotenv
import os

load_dotenv()

def build_llm() -> ChatOpenAI:
    return ChatOpenAI(
        model="MiniMax-M2.7",
        base_url="https://api.minimaxi.com/v1",
        api_key=os.getenv("MINIMAX_API_KEY"),
        temperature=0.0,
        max_tokens=1000,
        timeout=60,
        max_retries=2,
    )

# 关键:处理 MiniMax 输出的 <think> 标签
def clean_json_output(input_str: str) -> str:
    if hasattr(input_str, "content"):
        input_str = input_str.content
    # 去掉 <think>...
    pattern = r"<think>.*?"
    text = re.sub(pattern, "", str(input_str), flags=re.DOTALL)
    return text.strip()

def clean_list_output(input_str: str) -> str:
    text = clean_json_output(input_str)
    return text.replace(",", ",").replace("、", ",")

def run_json_demo(llm: ChatOpenAI) -> None:
    json_parser = JsonOutputParser()
    json_prompt = ChatPromptTemplate.from_messages([
        SystemMessage(content="你是一个数据提取助手。只返回合法 JSON,不要解释。"),
        HumanMessagePromptTemplate.from_template(
            "从以下文本中提取信息,以 JSON 格式返回:\n{text}\n\n格式要求:\n{format}"
        ),
    ])
    chain_json = json_prompt | llm | RunnableLambda(clean_json_output) | json_parser

    result = chain_json.invoke({
        "text": "布鲁斯,37岁,来自深圳,是一名技术合伙人,擅长 Python 和 Java。",
        "format": json_parser.get_format_instructions(),
    })
    print("JSON 结果:", result)

# Pydantic 模型
class PersonInfo(BaseModel):
    name: str = Field(description="人物姓名")
    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

def run_pydantic_demo(llm: ChatOpenAI) -> None:
    pydantic_parser = PydanticOutputParser(pydantic_object=PersonInfo)
    pydantic_prompt = (
        ChatPromptTemplate.from_messages([
            SystemMessage(content="你是一个数据提取助手。只返回符合格式要求的 JSON 本身。"),
            HumanMessagePromptTemplate.from_template("从以下文本中提取信息:\n{text}\n\n{format}"),
        ])
        .partial(format=pydantic_parser.get_format_instructions())
    )
    chain_pydantic = pydantic_prompt | llm | RunnableLambda(clean_json_output) | pydantic_parser

    person: PersonInfo = chain_pydantic.invoke({
        "text": "布鲁斯,37岁,深圳技术合伙人,擅长Node Python、Java、Go。"
    })
    print(f"姓名:{person.name},年龄:{person.age},城市:{person.city},技能:{person.skills}")

def run_list_demo(llm: ChatOpenAI) -> None:
    list_parser = CommaSeparatedListOutputParser()
    list_prompt = (
        ChatPromptTemplate.from_messages([
            SystemMessage(content="你是一个只输出逗号分隔列表的助手。使用英文逗号。"),
            HumanMessagePromptTemplate.from_template("列出 {subject} 的 {n} 个优点。\n\n{format}"),
        ])
        .partial(format=list_parser.get_format_instructions())
    )
    list_chain = list_prompt | llm | RunnableLambda(clean_list_output) | list_parser
    result = list_chain.invoke({"subject": "Python", "n": 5})
    print("列表结果:", result)

def main() -> None:
    llm = build_llm()
    # run_json_demo(llm)
    # run_pydantic_demo(llm)
    run_list_demo(llm)

if __name__ == "__main__":
    main()

demo04_what_when_how_wide


Demo 05 · RAG 核心链路 ↑ 返回目录

学习目标

  • ✅ 掌握 TextLoader / PyPDFLoader / WebBaseLoader
  • ✅ 掌握 RecursiveCharacterTextSplitter
  • ✅ 掌握 Chroma 向量库 + Ollama Embedding

重点知识强化

知识模块      内容
组件说明    TextLoader:读本地文本;PyPDFLoader:读 PDF;WebBaseLoader:读网页;RecursiveCharacterTextSplitter:把长文切块;Chroma:存向量并检索;OllamaEmbeddings:把文本转成向量;requests:做 Ollama 健康检查;Document:统一承载内容和 metadata。
学习目标组件: TextLoader、PyPDFLoader、WebBaseLoader、RecursiveCharacterTextSplitter、Chroma、OllamaEmbeddings。

 
代码对应    build_embeddings() 先做 Ollama 健康检查;load_documents() 把文本、PDF、网页统一成 Documentsplit_documents() 切块;build_vectorstore() 写入 Chroma;run_queries() 验证检索效果。

 
对比理解    文档加载和文档切块是两件事;similarity_search() 只看结果,similarity_search_with_score() 还能看分数;不同来源最终都要统一成 Document 列表。

 
常见坑    常见坑是文档来源不统一、切块过大或过小、网页加载受网络影响。建议入库前做清洗、去重、metadata 标注,文档更新后做重建或增量同步。

 
真实场景    企业知识库、FAQ 搜索、产品文档问答,都需要先把文本处理成向量库。这个 Demo 讲的是“入库”阶段。

 

完整代码

# 文件:demo05_rag_ingest.py

from pathlib import Path
import os
import requests
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader, PyPDFLoader, WebBaseLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

load_dotenv()

def build_embeddings() -> OllamaEmbeddings:
    """使用本地 Ollama embedding 模型"""
    model = os.getenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:0.6b")
    base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
    
    # 检查 Ollama 服务
    health_url = f"{base_url.rstrip('/')}/api/tags"
    try:
        requests.get(health_url, timeout=3)
    except requests.RequestException as exc:
        raise RuntimeError(f"Ollama 服务不可用,请先启动 ollama") from exc

    return OllamaEmbeddings(model=model, base_url=base_url)

def load_documents() -> list[Document]:
    """读取本地文本 / PDF / 网页"""
    # 文本
    text_path = Path("essay.txt")
    if text_path.exists():
        text_docs = TextLoader(str(text_path), encoding="utf-8").load()
    else:
        text_docs = [Document(
            page_content="LangChain 是一个用于构建 LLM 应用的框架。",
            metadata={"source": "inline:essay.txt"},
        )]
        print("essay.txt 不存在,已使用内置示例文本代替。")

    # PDF
    pdf_path = Path("paper.pdf")
    if pdf_path.exists():
        pdf_docs = PyPDFLoader(str(pdf_path)).load()
    else:
        pdf_docs = []
        print("paper.pdf 不存在,已跳过 PDF 读取。")

    # 网页
    web_url = "https://python.langchain.com/docs/introduction"
    try:
        web_docs = WebBaseLoader(web_url).load()
    except Exception as exc:
        web_docs = []
        print(f"网页读取失败,已跳过:{exc}")

    return text_docs + pdf_docs + web_docs

def split_documents(documents: list[Document]) -> list[Document]:
    """切块"""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=50,
        length_function=len,
        add_start_index=True,
    )
    chunks = text_splitter.split_documents(documents)
    print(f"切块数: {len(chunks)}")
    return chunks

def build_vectorstore(chunks: list[Document], embeddings: OllamaEmbeddings) -> Chroma:
    """构建向量库"""
    return Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory="./chroma_db",
    )

def run_queries(vectorstore: Chroma) -> None:
    """检索测试"""
    # 相似度检索
    query = "LangChain 的核心概念是什么?"
    docs = vectorstore.similarity_search(query=query, k=3)
    for i, doc in enumerate(docs, 1):
        print(f"[{i}] {doc.page_content[:150]}")

    # 带分数的检索
    docs_with_scores = vectorstore.similarity_search_with_score(query=query, k=3)
    for doc, score in docs_with_scores:
        print(f"  [分数:{score:.4f}] {doc.page_content[:100]}")

    # Retriever
    retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
    retrieved_docs = retriever.invoke("LangChain 是什么?")
    print(f"Retriever 返回 {len(retrieved_docs)} 个文档块")

def main() -> None:
    embeddings = build_embeddings()
    documents = load_documents()
    for doc in documents:
        print(doc.metadata)
    
    chunks = split_documents(documents)
    for i, chunk in enumerate(chunks, 1):
        print(f"Chunk {i}: 长度={chunk.page_content.__len__()}, 元数据={chunk.metadata}")
    
    vectorstore = build_vectorstore(chunks, embeddings)
    run_queries(vectorstore)

if __name__ == "__main__":
    main()

demo05_what_when_how_wide


Demo 06 · RAG Chain ↑ 返回目录

学习目标

  • ✅ 掌握 RAG Chain 的 LCEL 组装
  • ✅ 理解 RunnablePassthrough 透传
  • ✅ 掌握多轮 RAG(condense query)

重点知识强化

知识模块      内容
组件说明    RunnablePassthrough:原样透传问题;retriever:负责检索;format_docs():把检索结果拼成字符串上下文;ChatPromptTemplate:组织回答提示词;StrOutputParser:输出字符串;get_buffer_string():把历史对话转成文本;ChatOpenAI / OllamaEmbeddings / Chroma:分别负责生成、向量化和向量库。
学习目标组件: RunnablePassthrough、retriever、format_docs、ChatPromptTemplate、StrOutputParser、get_buffer_string

 
代码对应    rag_chain = {"question": RunnablePassthrough(), "context": retriever | format_docs} | prompt | llm | parser 负责检索+回答;condense_prompt 负责把多轮问题改写成独立查询;final_chain 负责把检索到的文档内容转成最终答案。

 
对比理解    主流程负责检索和回答,改写流程负责把“它”“这个功能”这类多轮问题转成独立查询;在这条链里 context 是字符串,不是消息列表。

 
常见坑    常见坑是检索结果没有来源、问题改写偏题、上下文过长导致超窗。建议加来源标签,先 rewrite 再 retrieval,回答里加“未找到”兜底。

 
真实场景    用户常常会说“这个功能”“它”“上一条那个问题”,而不是完整句子。这个 Demo 讲的是知识检索和多轮上下文怎么接起来。

 

完整代码

# 文件:demo06_rag_chain.py

from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import HumanMessage, AIMessage, get_buffer_string
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os

load_dotenv()

# 使用 MiniMax 作为 LLM
llm = ChatOpenAI(
    model="MiniMax-M2.7",
    base_url="https://api.minimaxi.com/v1",
    api_key=os.getenv("MINIMAX_API_KEY"),
    temperature=0.7,
    max_tokens=1000,
    timeout=60,
    max_retries=2,
)

# 使用 Ollama 作为 Embedding
embeddings = OllamaEmbeddings(
    model="qwen3-embedding:0.6b",
    base_url="http://localhost:11434",
)

vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})

def format_docs(docs):
    return "\n\n".join(f"[来源{i+1}] {d.page_content}" for i, d in enumerate(docs))

# RAG Chain
rag_chain = (
    {"question": RunnablePassthrough(), "context": retriever | format_docs}
    | ChatPromptTemplate.from_messages([
        SystemMessage(content=(
            "你是知识渊博的助手,基于文档片段回答。"
            "如果文档中没有答案,说「没有找到相关信息」,不要编造。"
        )),
        HumanMessagePromptTemplate.from_template("文档内容:\n{context}\n\n问题:{question}"),
    ])
    | llm
    | StrOutputParser()
)

result = rag_chain.invoke("LangChain 是什么?有哪些核心概念?")
print("RAG 结果:", result[:200])

# 多轮 RAG(合并历史问题)
chat_history = [
    HumanMessage(content="LangChain 支持哪些模型?"),
    AIMessage(content="LangChain 支持 OpenAI、Anthropic、Google Gemini、HuggingFace 等主流 LLM。"),
]

condense_prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content="把对话历史和最新问题合并成一个独立的问题。"),
    HumanMessagePromptTemplate.from_template("历史:{chat_history}\n\n最新问题:{question}"),
])

combined_query = (
    condense_prompt | llm | StrOutputParser()
).invoke({
    "chat_history": get_buffer_string(chat_history),
    "question": "那 embedding 模型呢?",
})
print("合并检索词:", combined_query)

docs = retriever.invoke(combined_query)
final_chain = (
    ChatPromptTemplate.from_messages([
        SystemMessage(content="基于文档回答问题。"),
        HumanMessagePromptTemplate.from_template("文档:\n{context}\n\n问题:{question}"),
    ])
    | llm
    | StrOutputParser()
)
answer = final_chain.invoke({"context": format_docs(docs), "question": combined_query})
print("最终答案:", answer)

demo06_what_when_how_wide


Demo 07 · Tool + Agent ↑ 返回目录

学习目标

  • ✅ 掌握 @tool 装饰器定义工具
  • ✅ 掌握 create_agent 的用法

重点知识强化

知识模块      内容
组件说明    @tool:把普通函数注册成工具;create_agent:创建会自己选工具的 Agent;
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐