前言
学完了 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:
chunk是AIMessageChunk对象,需要用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 的全景认知,欢迎在评论区交流讨论!