LangChain + RAG 入门实战:从模型调用到完整 RAG 流水线

前言

学完了 OpenAI 库的基础调用和提示词优化之后,下一步自然会接触到 LangChain ------ 目前最流行的 LLM 应用开发框架。

LangChain 做的事情可以一句话概括:把大模型应用开发中常见的步骤封装成标准化组件,让你像搭积木一样组装 AI 应用。

本文将按照学习顺序,从最基础的模型调用开始,逐步深入到 RAG(检索增强生成)的完整实现。所有示例围绕"编程学习助手"这个统一主题展开。

环境准备

bash 复制代码
pip3 install langchain langchain-core langchain-community langchain-ollama langchain-chroma langchain-text-splitters chromadb

一、LangChain 中的模型调用

1.1 两种模型类型:LLM vs ChatModel

LangChain 中访问大模型有两种方式,对应两种不同的模型类型:

类型 对应类 输入 输出 适用模型
LLM Tongyi / OllamaLLM 纯字符串 纯字符串 补全类模型(如 qwen-max
ChatModel ChatTongyi / ChatOllama 消息列表 AIMessage 对象 聊天类模型(如 qwen3-max

LLM 示例(字符串进出):

python 复制代码
# 云端模型 - 阿里云 DashScope
from langchain_community.llms.tongyi import Tongyi

model = Tongyi(model="qwen-max")  # qwen-max 是语言补全模型
res = model.invoke(input="用一句话解释什么是递归")
print(res)
python 复制代码
# 本地模型 - Ollama
from langchain_ollama import OllamaLLM

model = OllamaLLM(model="qwen3:4b")
res = model.invoke(input="用一句话解释什么是递归")
print(res)

ChatModel 示例(消息进出):

python 复制代码
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.messages import HumanMessage, SystemMessage

model = ChatTongyi(model="qwen3-max")  # qwen3-max 是聊天模型

messages = [
    SystemMessage(content="你是一个编程助手,只回答技术问题"),
    HumanMessage(content="Python 中列表和元组有什么区别?"),
]

res = model.invoke(input=messages)
print(res.content)  # 注意:ChatModel 的返回值是 AIMessage,用 .content 获取文本

1.2 流式输出

和 OpenAI 库一样,LangChain 的模型也支持流式输出,使用 stream() 方法:

python 复制代码
from langchain_ollama import ChatOllama

model = ChatOllama(model="qwen3:4b")

for chunk in model.stream(input="用 Python 写一个快速排序"):
    print(chunk.content, end="", flush=True)

LLM 和 ChatModel 的流式输出区别

  • LLM:chunk 直接就是字符串,print(chunk) 即可
  • ChatModel:chunkAIMessageChunk 对象,需要用 chunk.content 获取文本内容

1.3 消息的简写形式

构造消息时,可以用 SystemMessage / HumanMessage / AIMessage 这些类对象,也可以直接用元组 (角色, 内容) 简写:

python 复制代码
from langchain_community.chat_models.tongyi import ChatTongyi

model = ChatTongyi(model="qwen3-max")

# 写法一:使用消息类(正式、类型安全)
messages = [
    SystemMessage(content="你是一个编程助手"),
    HumanMessage(content="Python 中列表和元组有什么区别?"),
]

# 写法二:使用元组简写(更简洁)
messages = [
    ("system", "你是一个编程助手"),
    ("human", "Python 中列表和元组有什么区别?"),
]

# 两种写法效果完全一样
for chunk in model.stream(input=messages):
    print(chunk.content, end="", flush=True)

二、嵌入模型(Embedding)

嵌入模型是 RAG 的核心组件之一,它把文本转换成向量,让计算机能够计算"两段文本的相似度"。

2.1 余弦相似度基础(扩展知识)

在理解嵌入之前,先了解一个概念:余弦相似度

两个文本被嵌入模型转换成向量后,如何判断它们的相似度?常用的方法是计算两个向量的余弦相似度:

python 复制代码
import numpy as np

def cosine_similarity(vec_a, vec_b):
    """余弦相似度 = 点积 / (模长A × 模长B)"""
    dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
    norm_a = np.sqrt(sum(a * a for a in vec_a))
    norm_b = np.sqrt(sum(b * b for b in vec_b))
    return dot_product / (norm_a * norm_b)

# 示例
a = [0.5, 0.5]
b = [0.7, 0.7]  # 方向相同,相似度很高
c = [0.7, 0.5]  # 方向有偏差
d = [-0.6, -0.5]  # 方向相反

print("ab:", cosine_similarity(a, b))  # 接近 1 → 非常相似
print("ac:", cosine_similarity(a, c))  # 中等
print("ad:", cosine_similarity(a, d))  # 负值 → 不相似

核心思想:余弦相似度衡量的是两个向量的"方向一致性",取值范围 -1 到 1,越接近 1 表示越相似。嵌入模型就是要把文本转成向量,然后用这个方法计算相似度。

2.2 使用嵌入模型

LangChain 中的嵌入模型有两个常用方法:

  • embed_query(text):把单个查询文本转成向量
  • embed_documents([text1, text2, ...]):批量把文档文本转成向量
python 复制代码
# 云端嵌入模型 - 阿里云 DashScope
from langchain_community.embeddings import DashScopeEmbeddings

model = DashScopeEmbeddings()  # 默认使用 text-embeddings-v1

vector = model.embed_query("什么是 Python 装饰器")
print(vector[:5])  # 打印前5个向量值
python 复制代码
# 本地嵌入模型 - Ollama
from langchain_ollama import OllamaEmbeddings

model = OllamaEmbeddings(model="qwen3-embedding:4b")

# 单条查询
q_vec = model.embed_query("什么是 Python 装饰器")

# 批量文档
doc_vecs = model.embed_documents([
    "Python 装饰器是一种高阶函数",
    "装饰器可以在不修改原函数代码的前提下增强函数功能",
    "今晚吃什么"
])

三、提示词模板(Prompt Template)

提示词模板让你不用手动拼接字符串就能生成完整的提示词。LangChain 提供了三种模板。

3.1 PromptTemplate --- 通用文本模板

最基础的模板,用 {变量名} 做占位符:

python 复制代码
from langchain_core.prompts import PromptTemplate
from langchain_community.llms.tongyi import Tongyi

# 创建模板
template = PromptTemplate.from_template(
    "我的邻居姓{lastname},刚生了{gender},你帮我起个名字,简单回答。"
)

# 方式一:用 .format() 生成字符串,再传给模型
prompt_text = template.format(lastname="张", gender="女儿")
model = Tongyi(model="qwen-max")
res = model.invoke(input=prompt_text)

# 方式二:组成 Chain,直接传入字典(推荐)
chain = template | model
res = chain.invoke(input={"lastname": "张", "gender": "女儿"})

3.2 FewShotPromptTemplate --- Few-Shot 示例模板

当你需要给模型提供"输入→输出"示例时,这个模板可以自动把示例格式化进去:

python 复制代码
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from langchain_community.llms.tongyi import Tongyi

# 每个示例的模板格式
example_template = PromptTemplate.from_template("单词:{word},反义词:{antonym}")

# 示例数据(list 套 dict)
examples_data = [
    {"word": "大", "antonym": "小"},
    {"word": "上", "antonym": "下"},
    {"word": "快", "antonym": "慢"},
]

# Few-Shot 模板
few_shot = FewShotPromptTemplate(
    example_prompt=example_template,     # 示例的模板
    examples=examples_data,              # 示例数据
    prefix="告知我单词的反义词,我提供如下示例:",   # 示例之前说的话
    suffix="基于前面的示例,{input_word} 的反义词是?",  # 示例之后说的话
    input_variables=["input_word"]       # 声明需要在 prefix/suffix 中注入的变量
)

# 生成完整提示词
prompt_text = few_shot.invoke({"input_word": "左"}).to_string()
print(prompt_text)
# 输出:
# 告知我单词的反义词,我提供如下示例:
# 单词:大,反义词:小
# 单词:上,反义词:下
# 单词:快,反义词:慢
# 基于前面的示例,左 的反义词是?

model = Tongyi(model="qwen-max")
print(model.invoke(input=prompt_text))

3.3 ChatPromptTemplate --- 聊天消息模板

用于 ChatModel,支持 MessagesPlaceholder 动态插入历史消息:

python 复制代码
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_models.tongyi import ChatTongyi

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个编程老师,可以写教学代码示例。"),
    MessagesPlaceholder("history"),  # 历史对话占位
    ("human", "请再来一个 Python 示例"),
])

# 历史数据
history = [
    ("human", "写一个 Python 的 Hello World"),
    ("ai", 'print("Hello, World!")'),
    ("human", "再写一个变量相加的示例"),
    ("ai", "a = 10\nb = 20\nprint(a + b)"),
]

# 生成提示词
prompt_text = chat_prompt.invoke({"history": history}).to_string()

model = ChatTongyi(model="qwen3-max")
res = model.invoke(prompt_text)
print(res.content)

3.4 format() 与 invoke() 的区别

所有模板类都继承自 Runnable,所以有两种注入变量的方式:

python 复制代码
template = PromptTemplate.from_template("邻居姓:{lastname},爱好:{hobby}")

# .format() → 返回纯字符串(来自 BasePromptTemplate)
text = template.format(lastname="张", hobby="钓鱼")
# 输出:邻居姓:张,爱好:钓鱼

# .invoke() → 返回 StringPromptValue 对象(来自 Runnable 接口)
value = template.invoke({"lastname": "李", "hobby": "唱歌"})
# 需要 .to_string() 才能看到文本
print(value.to_string())

总结.format() 返回字符串,适合单独使用;.invoke() 返回 PromptValue 对象,适合在 Chain 中流转。


四、Chain 链式调用

Chain 是 LangChain 最重要的概念之一。通过 | 运算符,可以把多个组件串成一条链。

4.1 | 运算符的本质

| 不是魔法,它是 Python 的 __or__ 运算符重写。类似 a | b | c 等价于 a.__or__(b).__or__(c)

python 复制代码
# 模拟 LangChain 的链式调用原理
class Component:
    def __init__(self, name):
        self.name = name
    def __or__(self, other):
        return Chain(self, other)
    def __str__(self):
        return self.name

class Chain:
    def __init__(self, *args):
        self.steps = list(args)
    def __or__(self, other):
        self.steps.append(other)
        return self
    def run(self):
        for step in self.steps:
            print(f"执行: {step}")

# 使用
prompt = Component("Prompt模板")
model = Component("AI模型")
parser = Component("输出解析器")

chain = prompt | model | parser
chain.run()
# 执行: Prompt模板
# 执行: AI模型
# 执行: 输出解析器

LangChain 中所有组件(Prompt、Model、Parser)都是 Runnable 接口的子类,所以可以用 | 串起来。

4.2 基础 Chain 示例

python 复制代码
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_models.tongyi import ChatTongyi

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个编程老师。"),
    MessagesPlaceholder("history"),
    ("human", "请再来一个 Python 示例"),
])

model = ChatTongyi(model="qwen3-max")

# 组成链
chain = chat_prompt | model

# invoke:一次性返回全部结果
res = chain.invoke({"history": [("human", "写个 Hello World"), ("ai", "print('Hello')")]})
print(res.content)

# stream:流式输出
for chunk in chain.stream({"history": []}):
    print(chunk.content, end="", flush=True)

4.3 输出解析器(Output Parser)

模型返回的是消息对象(如 AIMessage),解析器把它转成我们需要的格式。

StrOutputParser --- 字符串解析器

AIMessage 提取为纯字符串,方便后续链传递给下一个需要字符串的组件:

python 复制代码
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_community.chat_models.tongyi import ChatTongyi

parser = StrOutputParser()
model = ChatTongyi(model="qwen3-max")
prompt = PromptTemplate.from_template(
    "邻居姓{lastname},生了{gender},请起名,仅告知名字不要其它内容。"
)

# 链:模板 → 模型 → 字符串解析 → 再把结果送给另一个模型 → 再解析
chain = prompt | model | parser | model | parser

res = chain.invoke({"lastname": "张", "gender": "女儿"})
print(res, type(res))  # str 类型

数据流转过程

md 复制代码
{"lastname": "张", "gender": "女儿"}
  ↓ prompt
"邻居姓张,生了女儿,请起名..."
  ↓ model
AIMessage(content="张若曦")
  ↓ StrOutputParser
"张若曦"
  ↓ model (第二个)
AIMessage(content=""若"意为...")
  ↓ StrOutputParser
"「若」意为..."  ← 最终返回

JsonOutputParser --- JSON 解析器

自动将模型返回的 JSON 字符串解析为 Python 字典:

python 复制代码
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.prompts import PromptTemplate

json_parser = JsonOutputParser()
str_parser = StrOutputParser()
model = ChatTongyi(model="qwen3-max")

first_prompt = PromptTemplate.from_template(
    "邻居姓{lastname},生了{gender},请起名。"
    "请严格按JSON格式返回:{{\"name\": \"你起的名字\"}}"
)

second_prompt = PromptTemplate.from_template(
    "姓名:{name},请帮我解析这个名字的含义。"
)

# 链:模板 → 模型 → JSON解析器 → 第二个模板 → 模型 → 字符串解析器
chain = first_prompt | model | json_parser | second_prompt | model | str_parser

for chunk in chain.stream({"lastname": "张", "gender": "女儿"}):
    print(chunk, end="", flush=True)

JsonOutputParser 会自动提取模型输出中的 JSON 并 json.loads() 转成 dict,这样下游的 second_prompt 就可以用 {name} 占位符直接取 dict["name"]

4.4 RunnableLambda --- 自定义处理函数

有时候你需要在链中做自定义数据处理(比如从 AIMessage 中提取某个字段),可以用 RunnableLambda,或者直接写 lambda 函数(LangChain 会自动包装):

python 复制代码
from langchain_core.prompts import PromptTemplate
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.output_parsers import StrOutputParser

model = ChatTongyi(model="qwen3-max")

first_prompt = PromptTemplate.from_template(
    "邻居姓{lastname},生了{gender},请起名,只生成一个名字。"
)

second_prompt = PromptTemplate.from_template(
    "姓名:{name},请帮我解析含义。"
)

# 链中插入一个 lambda 函数,把 AIMessage 转成 dict
chain = (
    first_prompt
    | model
    | (lambda ai_msg: {"name": ai_msg.content})  # RunnableLambda 自动包装
    | second_prompt
    | model
    | StrOutputParser()
)

for chunk in chain.stream({"lastname": "曹", "gender": "女孩"}):
    print(chunk, end="", flush=True)

五、会话记忆(Memory)

大模型本身是无状态的,LangChain 提供了两种记忆方式。

5.1 临时会话记忆(In-Memory)

使用 InMemoryChatMessageHistory 存储在内存中,程序关闭后丢失:

python 复制代码
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory

model = ChatTongyi(model="qwen3-max")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你需要根据会话历史回应用户问题。对话历史:"),
    MessagesPlaceholder("chat_history"),
    ("human", "请回答:{input}"),
])

base_chain = prompt | model | StrOutputParser()

# 内存存储:key 是 session_id,value 是消息历史对象
store = {}

def get_history(session_id):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# 用 RunnableWithMessageHistory 增强原有 Chain,自动附加历史消息
conversation_chain = RunnableWithMessageHistory(
    base_chain,
    get_history,
    input_messages_key="input",          # 模板中用户输入的占位符
    history_messages_key="chat_history",  # 模板中历史消息的占位符
)

# 使用:每次调用需要传入 session 配置
config = {"configurable": {"session_id": "user_001"}}

r1 = conversation_chain.invoke({"input": "小明有2个猫"}, config)
print("第1次:", r1)

r2 = conversation_chain.invoke({"input": "小刚有1只狗"}, config)
print("第2次:", r2)

r3 = conversation_chain.invoke({"input": "总共有几个宠物?"}, config)
print("第3次:", r3)  # 模型应该能回答"3个",因为前两次对话已被记住

5.2 长期会话记忆(File-Based)

自定义 BaseChatMessageHistory,将对话持久化到文件:

python 复制代码
import os, json
from typing import Sequence
from langchain_core.messages import message_to_dict, messages_from_dict, BaseMessage
from langchain_core.chat_history import BaseChatMessageHistory


class FileChatMessageHistory(BaseChatMessageHistory):
    """将聊天历史保存到本地文件,实现跨会话的长期记忆"""

    def __init__(self, session_id, storage_path):
        self.session_id = session_id
        self.file_path = os.path.join(storage_path, session_id)
        os.makedirs(os.path.dirname(self.file_path), exist_ok=True)

    def add_messages(self, messages: Sequence[BaseMessage]) -> None:
        # 读取已有消息 + 追加新消息
        all_messages = list(self.messages)
        all_messages.extend(messages)
        # 转为字典列表后写入 JSON 文件
        dicts = [message_to_dict(msg) for msg in all_messages]
        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump(dicts, f)

    @property
    def messages(self) -> list[BaseMessage]:
        try:
            with open(self.file_path, "r", encoding="utf-8") as f:
                return messages_from_dict(json.load(f))
        except FileNotFoundError:
            return []

    def clear(self) -> None:
        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump([], f)


# 使用方式与临时记忆完全一样,只是 get_history 返回的类不同
def get_history(session_id):
    return FileChatMessageHistory(session_id, "./chat_history")

conversation_chain = RunnableWithMessageHistory(
    base_chain, get_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

# 调用方式完全一样
config = {"configurable": {"session_id": "user_001"}}
res = conversation_chain.invoke({"input": "上次我说了什么?"}, config)

关键转换函数message_to_dict() 将消息对象转为字典,messages_from_dict() 将字典列表还原为消息对象。这两个函数来自 langchain_core.messages


六、文档加载与分割

RAG 的第一步是把你的文档加载进来,转换成 LangChain 的 Document 对象。

6.1 文档对象 Document

LangChain 中的 Document 是一个简单数据类:

  • page_content:文档的文本内容(字符串)
  • metadata:文档的元数据(字典),如来源、页码等

6.2 各种 Loader

TextLoader --- 纯文本文件

python 复制代码
from langchain_community.document_loaders import TextLoader

loader = TextLoader("./data/Python知识点汇总.txt", encoding="utf-8")
docs = loader.load()  # 返回 list[Document]

print(f"加载了 {len(docs)} 个文档")
for doc in docs:
    print(doc.page_content[:100])  # 打印前100个字符

CSVLoader --- CSV 文件

python 复制代码
from langchain_community.document_loaders import CSVLoader

loader = CSVLoader(
    file_path="./data/学生信息表.csv",
    encoding="utf-8",
    source_column="来源",      # 指定哪一列作为 metadata 的 source 字段
)

# .load() 批量加载
documents = loader.load()

# .lazy_load() 懒加载(适合大文件,逐行加载省内存)
for doc in loader.lazy_load():
    print(doc)

如果 CSV 没有表头,可以用 csv_args 手动指定列名:

python 复制代码
loader = CSVLoader(
    file_path="./data/no_header.csv",
    csv_args={
        "delimiter": ",",
        "quotechar": '"',
        "fieldnames": ["姓名", "年龄", "性别", "爱好"],  # 手动指定列名
    },
    encoding="utf-8",
)

JSONLoader --- JSON / JSON Lines 文件

python 复制代码
from langchain_community.document_loaders import JSONLoader

loader = JSONLoader(
    file_path="./data/students.jsonl",
    jq_schema=".name",          # jq 语法,指定要抽取的字段
    text_content=False,         # False 表示抽取的不是纯文本而是结构化值
    json_lines=True,            # 这是 JSON Lines 格式(每行一个独立 JSON)
)

documents = loader.load()

jq_schema 使用的是 jq 查询语法,.name 表示提取每条记录的 name 字段。如果想提取嵌套字段可以用 .info.description

PyPDFLoader --- PDF 文件

python 复制代码
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader(
    file_path="./data/技术文档.pdf",
    mode="single",    # "page"=每页一个Document(默认);"single"=全部合并为一个Document
    password="123456"  # 如果 PDF 有密码
)

for doc in loader.lazy_load():
    print(doc.page_content[:200])

6.3 文档分割器

大模型有上下文长度限制,且向量检索需要合理的文本块大小。RecursiveCharacterTextSplitter 会按照优先级递归切分:

python 复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = TextLoader("./data/Python知识点汇总.txt", encoding="utf-8")
docs = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,           # 每个文本块的最大字符数
    chunk_overlap=50,         # 相邻块之间的重叠字符数(保持上下文连贯)
    separators=[              # 切分优先级(从高到低)
        "\n\n",   # 先按双换行(段落)切
        "\n",     # 再按单换行(行)切
        "。", "!", "?",  # 再按中文句子结束符切
        ".", "!", "?",      # 再按英文句子结束符切
        " ",      # 再按空格切
        ""        # 最后按字符切
    ],
    length_function=len,
)

split_docs = splitter.split_documents(docs)
print(f"切分后共 {len(split_docs)} 个文本块")
for i, doc in enumerate(split_docs):
    print(f"--- 第 {i+1} 块 ({len(doc.page_content)} 字符) ---")
    print(doc.page_content)

七、向量存储与检索

向量存储是 RAG 的"记忆系统"。文档被嵌入模型转成向量后存入向量库,查询时也转成向量,通过相似度检索最相关的文档片段。

7.1 内存向量存储(InMemoryVectorStore)

轻量级,适合测试和原型开发,数据存在内存中:

python 复制代码
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.document_loaders import CSVLoader

# 创建向量存储
vector_store = InMemoryVectorStore(embedding=DashScopeEmbeddings())

# 加载文档并添加到向量库
loader = CSVLoader(file_path="./data/学生信息表.csv", encoding="utf-8")
documents = loader.load()

vector_store.add_documents(
    documents=documents,
    ids=["stu_" + str(i) for i in range(len(documents))],  # 每条文档一个唯一ID
)

# 删除指定文档
vector_store.delete(["stu_0", "stu_1"])

# 相似度检索
results = vector_store.similarity_search("Python编程", k=3)
for doc in results:
    print(doc.page_content)

7.2 Chroma 向量存储(持久化)

Chroma 是一个轻量级向量数据库,支持持久化到本地文件系统:

python 复制代码
from langchain_chroma import Chroma
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.document_loaders import CSVLoader

vector_store = Chroma(
    collection_name="python_knowledge",   # 集合名称,类似数据库表名
    embedding_function=DashScopeEmbeddings(),
    persist_directory="./chroma_db",      # 持久化存储的目录
)

# 添加文档
loader = CSVLoader(file_path="./data/学生信息表.csv", encoding="utf-8", source_column="来源")
documents = loader.load()
vector_store.add_documents(
    documents=documents,
    ids=["id_" + str(i) for i in range(1, len(documents) + 1)],
)

# 检索(支持元数据过滤)
results = vector_store.similarity_search(
    "周杰伦帅吗?",
    k=3,
    filter={"source": "只检索市一中的数据"}  # 只检索来源为"市一中"的文档
)

八、搭建 RAG 问答系统

最后,把以上组件组装成一个完整的 RAG(Retrieval Augmented Generation,检索增强生成)流水线。

8.1 手动组装版

先理解 RAG 的流程:用户提问 → 向量检索 → 把检索到的文档拼入提示词 → 模型回答。

python 复制代码
from langchain_community.chat_models import ChatTongyi
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatTongyi(model="qwen3-max")

prompt = ChatPromptTemplate.from_messages([
    ("system", "请根据提供的参考资料回答用户问题。参考资料:{context}"),
    ("user", "用户提问:{input}"),
])

# 1. 准备向量库
vector_store = InMemoryVectorStore(embedding=DashScopeEmbeddings(model="text-embedding-v4"))
vector_store.add_texts([
    "减肥的关键是控制热量摄入和增加热量消耗,也就是常说的管住嘴迈开腿",
    "减脂期间建议清淡饮食,少油少盐,控制卡路里摄入,同时保持规律运动",
    "跑步是非常好的有氧运动,可以有效燃烧脂肪,建议每周跑步3-5次"
])

# 2. 检索相关文档
query = "怎么减肥?"
docs = vector_store.similarity_search(query, k=2)

# 3. 拼接检索结果
context_text = "\n".join([doc.page_content for doc in docs])

# 4. 通过 Chain 生成回答
chain = prompt | model | StrOutputParser()
res = chain.invoke({"input": query, "context": context_text})
print(res)

8.2 Chain 全自动组装版(推荐)

利用 as_retriever()RunnablePassthrough 实现全自动流水线:

python 复制代码
from langchain_community.chat_models import ChatTongyi
from langchain_core.documents import Document
from langchain_core.runnables import RunnablePassthrough
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatTongyi(model="qwen3-max")

prompt = ChatPromptTemplate.from_messages([
    ("system", "请根据提供的参考资料回答用户问题。参考资料:{context}"),
    ("user", "用户提问:{input}"),
])

# 1. 准备向量库
vector_store = InMemoryVectorStore(embedding=DashScopeEmbeddings(model="text-embedding-v4"))
vector_store.add_texts([
    "减肥的关键是控制热量摄入和增加热量消耗,也就是常说的管住嘴迈开腿",
    "减脂期间建议清淡饮食,少油少盐,控制卡路里摄入,同时保持规律运动",
    "跑步是非常好的有氧运动,可以有效燃烧脂肪"
])

# 2. 将向量库转为 Retriever(Runnable 子类,可在链中使用)
retriever = vector_store.as_retriever(search_kwargs={"k": 2})

# 3. 定义文档格式化函数(把 list[Document] 转为可读字符串)
def format_docs(docs: list[Document]):
    if not docs:
        return "无相关参考资料"
    return "\n".join([doc.page_content for doc in docs])

# 4. 组装完整 Chain
chain = (
    {
        "input": RunnablePassthrough(),      # 用户原问题原样传递
        "context": retriever | format_docs,  # 检索 + 格式化
    }
    | prompt
    | model
    | StrOutputParser()
)

# 5. 调用
result = chain.invoke("怎么减肥?")
print(result)

完整数据流图解

md 复制代码
用户输入:"怎么减肥?"
     │
     ├─ RunnablePassthrough() → {"input": "怎么减肥?"}
     │
     └─ retriever | format_docs
          │
          ├─ retriever: 向量检索,返回 list[Document]
          │   → ["减肥的关键是控制热量...", "减脂期间建议清淡..."]
          │
          └─ format_docs: 拼接为字符串
              → "减肥的关键是控制热量...\n减脂期间建议清淡..."
     │
     ▼
{"input": "怎么减肥?", "context": "减肥的关键是...\n减脂期间建议..."}
     │
     ▼ prompt
"system: 请根据参考资料回答... 参考资料:减肥的关键是...
 user: 用户提问:怎么减肥?"
     │
     ▼ model
AIMessage(content="减肥需要控制饮食并坚持运动...")
     │
     ▼ StrOutputParser
"减肥需要控制饮食并坚持运动..."  ← 最终结果

8.3 关键组件说明

组件 作用
as_retriever() 把向量存储包装成一个 Retriever(Runnable 子类),接收查询文本,返回 list[Document]
RunnablePassthrough() 原样传递输入值,在 Chain 的 dict 构建中用于保留原始用户输入
{"input": ..., "context": ...} 字典映射语法:把不同的数据流路由到提示词模板的不同占位符
format_docs 把检索到的文档列表转为一段可读文本,也可以在这里加调试用函数打印中间结果

总结:LangChain 核心知识地图

md 复制代码
┌─────────────────────────────────────────────────────────┐
│                   LangChain 组件图谱                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  模型层                                                  │
│  ├── LLM (Tongyi/OllamaLLM)         字符串 → 字符串     │
│  └── ChatModel (ChatTongyi/ChatOllama) 消息 → AIMessage │
│                                                         │
│  嵌入层                                                  │
│  └── Embeddings (DashScope/OllamaEmbeddings)             │
│      文本 → 向量(用于相似度计算和检索)                    │
│                                                         │
│  提示词层                                                │
│  ├── PromptTemplate          通用文本模板                  │
│  ├── FewShotPromptTemplate   Few-Shot 示例模板            │
│  └── ChatPromptTemplate      聊天消息模板                  │
│                                                         │
│  链式层(Runnable)                                      │
│  ├── | 运算符            组件串联                        │
│  ├── StrOutputParser     提取字符串                      │
│  ├── JsonOutputParser    提取 JSON → dict               │
│  ├── RunnableLambda      自定义函数介入                   │
│  └── RunnablePassthrough  原样传递                       │
│                                                         │
│  记忆层                                                  │
│  ├── InMemoryChatMessageHistory  内存(临时)            │
│  └── 自定义 BaseChatMessageHistory 文件/数据库(持久)    │
│                                                         │
│  文档层                                                  │
│  ├── Loader: TextLoader / CSVLoader / JSONLoader /       │
│  │           PyPDFLoader                                │
│  └── Splitter: RecursiveCharacterTextSplitter            │
│                                                         │
│  向量存储层                                              │
│  ├── InMemoryVectorStore       内存存储                   │
│  └── Chroma                    持久化向量数据库             │
│                                                         │
│  完整 RAG 流水线                                         │
│  用户提问 → 向量检索 → 文档格式化 → 提示词组装 → 模型回答  │
│                                                         │
└─────────────────────────────────────────────────────────┘

学习 LangChain 的关键是理解 Runnable 这个统一接口 ------ 几乎所有组件都实现了 invoke()stream() 方法,所以可以用 | 运算符自由组合。掌握这个核心思想后,再去学习更高级的 Agent、Tool 等概念就会轻松很多。

希望这篇文章能帮你快速建立 LangChain + RAG 的全景认知,欢迎在评论区交流讨论!

相关推荐
小马过河R1 小时前
从官方定义读懂智能体的时代分量
人工智能·语言模型·大模型·llm·agent·ai编程·多模态
ftpeak2 小时前
LangGraph Agent 开发指南(1~概述)
人工智能·ai·langchain·langgraph
RxGc2 小时前
斯坦福AI Agent报告解读:哪些方向真的落地了
人工智能·agent
一个处女座的程序猿2 小时前
OpenAI之CLI:OpenAI CLI的简介、安装和使用方法、案例应用之详细攻略
llm·openai·cli
code bean2 小时前
【LangChain】少样本提示(Few-Shot Prompting)实战指南
开发语言·python·langchain
TGITCIC2 小时前
Redis之父为DeepSeek V4 Flash打造的Mac本地推理工具ds4.c是个什么东东
llm·deepseek·deepseekv4·大模型mac·mac上跑大模型
王钧石的技术博客2 小时前
Harness Engineering学习
人工智能·学习·agent
星浩AI3 小时前
多智能体并行协作开发模式(Claude Code Agent Teams)
agent·claude·vibecoding
HIT_Weston3 小时前
78、【Agent】【OpenCode】bash 工具提示词(持久化)(二)
人工智能·agent·opencode