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内容关联

这句话我不理解

  1. 由于每个 Runnable 都有明确的输入/输出类型签名,LangChain 可以在编译时进行类型推断,并自动适配不同组件(如 ChatModel、PromptTemplate、OutputParser 等)。同时,RunnableSequence 也支持 streambatch 等方法,实现流式处理和批量处理。

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:极其灵活。它可以轻松与 ChatPromptTemplateRetrieverToolNode 等组合,构建复杂的 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)

img

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检索逻辑

posted @ 2026-08-23 15:50  zxsoul  阅读(8)  评论(0)    收藏  举报