LangChain+RAG 实践篇
沉浸式学习LangChain,想想都特别爽!!!
LangChain 在LLM应用程序生命周期的各个阶段
-
开发:使用LangChain的开源组件和第三方集成构建应用程序
-
生产化:使用LangSmith检查,监控和评估应用程序
-
部署:LangGraph 平台 将LangGraph应用程序变为可生产的api和助手
LangChain 的核心组件
Model I/O
-
提供与大模型交互的统一接口
-
动态生成提示词,避免硬编码
-
将模型输出解析为结构化数据
Indexes
-
文档加载,切割,向量化,向量存储,向量检索
-
Document Loader:各种格式的文件加载器
-
Text Splitters:长文本拆分
-
Embedding Model:向量化模型
-
Vector Store:向量数据库
-
Retriever:向量检索
Chains
将多个组件串联成完整工作流
Agents ,Tools
-
Tools 外部API和外部的幻术
-
代理执行器(agentExecute):动态决策调用工具
Memory
- 管理对话历史,实现多轮对话
Callbcaks
- 监控和调试应用的执行过程
Text Embedding 模型中有两个接口,一个是对文档的向量化,一个是对句子的向量化,embed_query
Langchain 提供的三种本地向量数据库, chroma FAISS Lance
上下文增强检索器,contextualCompressionRetriever,将检索的文档让大模型先进行文本压缩,去除无关内容,再作为检索结果返回
好处,可能存在一些文档是80%背景,20%答案,降低上下文容量
父子文档检索器,原来父子切割的原理是这样的,儿子有一个向量数据库,存小向量片段,父子有一个文档数据库,不向量化,然后通过检索器实现ID内容关联
这句话我不理解
- 由于每个
Runnable都有明确的输入/输出类型签名,LangChain 可以在编译时进行类型推断,并自动适配不同组件(如 ChatModel、PromptTemplate、OutputParser 等)。同时,RunnableSequence也支持stream、batch等方法,实现流式处理和批量处理。
RunnableLambda 可以将编写的函数作为链的一部分
Runnablemap和parallel可以实现任务并行进行,链上一次的结果同时作为多个节点的输入
Langchain记忆系统
Runnablewithmessagehistory
包括两种,
-
专门的消息历史组件 chatmessagehistory
-
自动会话历史管理组件Runnablewithmessagehistory
两个都是容器,一个需要手动添加,一个自动注入
LangSmith 提供评估LLM应用
LangServe的add_routes 可以直接实现api一键部署,实现输入和流式输出、
ConversationChain 的记忆输入和载入过程是完全自动化的。你只需要在初始化时传入一个 Memory 对象(如代码中的 ConversationBufferMemory),之后每次调用 .predict() 方法时,它都会在后台自动完成“读取历史 → 拼接 Prompt → 调用模型 → 保存新对话”这一整套流程,无需你手动干预
Runnablewithmessagehistory 好处:
上面的记忆系统已经过时,原因
-
状态管理
-
ConversationChain:实例本身是“有状态”的。一旦初始化,它就绑定了一个特定的 Memory 对象。这意味着你不能把这个实例同时服务多个用户,否则会导致聊天记录混乱。在 Web 服务中,你必须为每个会话创建一个新的 Chain 实例。 -
RunnableWithMessageHistory:是“无状态”的。它通过session_id参数在运行时动态获取对应的历史记录。同一个 Runnable 实例可以安全地并发服务成千上万个用户,只需在调用时传入不同的session_id即可。
-
-
灵活性与扩展性
-
ConversationChain:功能固定,只能做简单的对话。如果你想加 RAG、工具调用或复杂逻辑,就必须放弃它,改用其他方式。 -
RunnableWithMessageHistory:极其灵活。它可以轻松与ChatPromptTemplate、Retriever、ToolNode等组合,构建复杂的 Agent 或 RAG 系统。例如,你可以先检索文档,再结合历史消息生成回答,整个过程依然能自动管理记忆。
-
ids=[f"id{i}" for i in range(len(documents))]
列表表达式,有点像printf里面的输出格式
client = chromadb.PersistentClient(path="./chroma_db")
self.collection=client.get_or_create_collection(name=collection_name)
self.collection.add(
# """向量"""
embeddings=self.embedding.embed_documents(documents),//存储字符串的列表
# """原文"""
documents=documents,//向量列表
# 每个文档的id
ids=[f"id{i}" for i in range(len(documents))]
)
def search(self,query,top_k):
'''检索向量数据库'''
results=self.collection.query(
query_embeddings=self.embedding.embed_query(query),
n_results=top_k//返回的前k的数据
)
search_results=self.vector_db.search(user_query,self.top_k)
print('search_results:',search_results["documents"][0][0])
chroma数据库使用方式,results 返回的是一个字典,存在几个关键字,ids Documents metadatas 元数据,distance 距离值,
['documents'][0][0] 是第1个问题的相似度最高的字段,如果只有一维就是把第i个问题的所有回答返回,不过返回的是列表,需要用join实现字符串拼接
os.path.splitext(self.filePath)[1].lower()
截取路径后缀文件
with open(self.filePath,'rb') as file:
打开文件,rb只读
response = client.chat.completions.create(
model=model,
messages=message,
temperature=0,
)
return response.choices[0].message.content
LLM 参数设计,具体response的返回内容为
{
"id": "chatcmpl-123456...", // 本次请求的唯一ID
"object": "chat.completion",
"created": 1715000000,
"model": "MiniMax-M2.7",
"choices": [ // 【第一层】 choices 是一个列表
{
"index": 0,
"message": { // 【第二层】 message 是列表里的一个对象
"role": "assistant", // 标记:这是 AI 的回答
"content": "你好!有什么我可以帮你的吗?" // 【第三层】 content 才是你要的文字
},
"finish_reason": "stop"
}
],
"usage": { ... }
}
所以可以理解为最后的return是整层查找
message=[{"role":"user","content":prompt}]
[]是列表,{} 是字典,key-value 形式,项相当于列表内的元素是字典
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
docs=text_splitter.create_documents([text]) #create_documents 处理字符串列表
#split_documents 处理Document列表
documents=[doc.page_content for doc in docs]
return documents
用recursive的文本分割器,调用的文本分割 create_documents必须是列表,便于批量操作,但是注意返回的是 Document对象的列表docs,每个对象包含两个元素, page_content字符串(原内容) 和 metadata
client = chromadb.PersistentClient(path="./chroma_db")
self.collection=client.get_or_create_collection(name=collection_name)
先创建可持久化Chroma 数据库,保存在路径的磁盘中,然后get_or_create_collection是查找或者创建集合 collection
from langchain_huggingface import HuggingFaceEmbeddings
embed_model= HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
Hugging 哈给嗯 模型是 all-miniLM
from openai import OpenAI
from dotenv import load_dotenv
from langchain_text_splitters import RecursiveCharacterTextSplitter
import chromadb
from langchain_huggingface import HuggingFaceEmbeddings
from loadFile import File
import os
load_dotenv()
client=OpenAI(api_key=os.getenv("MINIMAX_API_KEY"),
base_url=os.getenv("MINIMAX_BASE_URL"))
prompt_template="""
作为回答机器人,你需要依据下方的已知信息回复用户提问。
请严格基于提供的内容作答,禁止虚构信息。
若信息不足,直接回复“我无法回答您的问题”。
已知信息:
{info}
用户提问:
{query}
请用中文回应。
"""
#定义LLM模型
def llm(prompt,model="MiniMax-M2.7"):
'''封装minimax接口'''
message=[{"role":"user","content":prompt}]
response = client.chat.completions.create(
model=model,
messages=message,
temperature=0,
)
return response.choices[0].message.content
#文本切割
def chunk_with_overlap(text,chunk_size=200,chunk_overlap=50):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
docs=text_splitter.create_documents([text])
documents=[doc.page_content for doc in docs]
return documents
class VectorDB:
def __init__(self,collection_name,embedding):
client = chromadb.PersistentClient(path="./chroma_db")
self.collection=client.get_or_create_collection(name=collection_name)
self.embedding=embedding
def add_documents(self,documents):
'''向 collection 中添加文档和向量'''
self.collection.add(
# """向量"""
embeddings=self.embedding.embed_documents(documents),
# """原文"""
documents=documents,
# 每个文档的id
ids=[f"id{i}" for i in range(len(documents))]
)
def search(self,query,top_k):
'''检索向量数据库'''
results=self.collection.query(
query_embeddings=self.embedding.embed_query(query),
n_results=top_k
)
return results
class RAG:
def __init__(self,vector_db,llm,top_k=2):
self.vector_db=vector_db
self.llm=llm
self.top_k=top_k
def chat(self,user_query):
# 1 检索问题答案
search_results=self.vector_db.search(user_query,self.top_k)
print('search_results:',search_results["documents"][0][0])
# 2 拼接 prompt
prompt=prompt_template.format(info=search_results["documents"][0][0],query=user_query)
# 3 调用LLM
response=self.llm(prompt)
return response
if __name__=='__main__':
# 使用实例
file_path= "C:\\Users\\zxsoul\\Desktop\\面试\\java后端开发\\23年至25年ACM生涯总结.pdf"
# 向量模型
embed_model= HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# 读入文档
text=File(file_path).read_file()
# 文本分割
documents=chunk_with_overlap(text)
# 创建数据库对象
vector_db=VectorDB("demo",embed_model)
# 在向量数据库中添加文章
vector_db.add_documents(documents)
# 创建一个RAG
rag=RAG(vector_db,llm)
user_query="ACM的魅力是什么?"
response=rag.chat(user_query)
print(response)
word可以载入半结构数据,不仅仅是文本,
ChatPromptTemplate.from_messages() 和from_template的区别
- message:接收列表,支持动态插入,通过system 和 human 区分对话
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个信息补充助手..."),
("placeholder", "{history}"), # ← 关键!用于注入历史记录 ,这里就是支持动态插入的地方
("human", "{input}")
])
- Template:接收字符串,也是通过system human区分角色,但是无法实现动态插入,只能实现占位符更新
prompt = ChatPromptTemplate.from_template(
"system: 你是一个助手\nuser: {input}"
)
自定义runnable batch 批量操作
sub_llm_chain = RunnableLambda(
lambda sub_query: self.sub_llm_chain.invoke({
"question": query,
"sub_question": sub_query,
"documents": [doc.page_content for doc in self.retriever.invoke(sub_query)]
})
)
通过lambda 实现任意函数成为Runnable对象,使用batch 的时候都会按照自定义的路径去实现。
Invoke 的重写
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> List[Document]:
# 生成子问题
sub_queries = self.generate_queries(query)
# 解决子问题
documents = self.retrieve_documents(query, sub_queries)
return documents
Runnable 的invoke底层是调用 _get_relevant_document 去实现的,可以通过重写这个方法,完成自定义检索器的invoke检索逻辑

各种与langchain相关的小demo
浙公网安备 33010602011771号