本地大模型编程实战(21)支持多参数检索的RAG(Retrieval Augmented Generation,检索增强生成)(5)

在实现 RAG(Retrieval Augmented Generation,检索增强生成) 系统的时候,在检索(retrieve)知识库时通常一个参数。

本文将演练更复杂一点的情况:通过两个参数进行知识库的检索。为此会在 langgraph链 中增加一个 analyze_query 节点,它用来基于用户问题推理检索的参数。

本次构建的 LangGraph 链结构如下图:

使用 qwen2.5deepseek 以及 llama3.1 做实验,用 shaw/dmeta-embedding-zh 做中文嵌入和检索。

准备

在正式开始撸代码之前,需要准备一下编程环境。

  1. 计算机

    本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:

    • CPU: Intel i5-8400 2.80GHz
    • 内存: 16GB
  2. Visual Studio Code 和 venv 这是很受欢迎的开发工具,相关文章的代码可以在 Visual Studio Code 中开发和调试。 我们用 pythonvenv 创建虚拟环境, 详见:
    在Visual Studio Code中配置venv

  3. Ollama 在 Ollama 平台上部署本地大模型非常方便,基于此平台,我们可以让 langchain 使用 llama3.1qwen2.5deepseek 等各种本地大模型。详见:
    在langchian中使用本地部署的llama3.1大模型

准备矢量数据库

这次我们使用网上的一篇博客做数据源,在使用 WebBaseLoader 加载文档时,用 bs4 只将网页中 css样式为"post-header","post-content"的内容解析出来,最后把数据存储在本地备用。

python 复制代码
def create_db(model_name,url):    
    """生成本地矢量数据库"""

    persist_directory = get_persist_directory(model_name)

    # 判断矢量数据库是否存在,如果存在则不再做索引,方便反复测试
    if os.path.exists(persist_directory):
        return
    
    embedding = OllamaEmbeddings(model=model_name)
    vectordb = Chroma(persist_directory=persist_directory,embedding_function=embedding)

    # 加载并分块博客内容
    loader = WebBaseLoader(
        web_paths=(url,),
        bs_kwargs=dict(
            parse_only=bs4.SoupStrainer(
                class_=("post-header","post-content")       # 指解析css class 为post-header和post-content 的内容
            )
        ),
    )
    docs = loader.load()

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    all_splits = text_splitter.split_documents(docs)

    # 在文档的 元数据(metadata) 中添加 section 标签
    
    total_documents = len(all_splits)
    third = total_documents // 3

    for i, document in enumerate(all_splits):
        if i < third:
            document.metadata["section"] = "开头"
        elif i < 2 * third:
            document.metadata["section"] = "中间"
        else:
            document.metadata["section"] = "结尾"

    print(f'Metadata: {all_splits[0].metadata}') 
    

    for i in tqdm(range(0, len(all_splits), batch_size), desc="嵌入进度"):
        batch = all_splits[i:i + batch_size]

        # 从文本块生成嵌入,并将嵌入存储在本地磁盘。
        vectordb.add_documents(batch)

 
create_db(embed_model_name,src_url)
vector_store = Chroma(persist_directory=get_persist_directory(embed_model_name),embedding_function=OllamaEmbeddings(model=embed_model_name))

这次我们把分割后的文档分为三部分,并记录在 metadata 中。

嵌入操作可能很耗时,分批嵌入文档并使用 tqdm 显示进度可以缓解焦虑。

如果想了解更多相关内容,可参见:语义检索(1)语义检索(2)

检索和生成回答

之前我们都是使用原始输入执行检索。我们也可以让 LLM 生成用于检索目的的查询条件,它有一些优势。例如: 可以将用户的自然语言重写为多条件的查询。

查询参数

我们为搜索查询定义一个结构。当然,在实际应用中,您也可以随意定义。

python 复制代码
class Search(TypedDict):
    """查询检索的参数"""

    query: Annotated[str, ..., "查询的关键词"]
    section: Annotated[str, ..., "要查询的部分,必须是'开头、中间、结尾'之一。"] 

State(状态)

我们明确定义一个 State ,用它在 langgraph链的不同节点间保存和传递数据。

python 复制代码
class State(TypedDict):
    question: str
    query: Search
    context: List[Document]
    answer: str

检索节点

定义检索节点,这里增加了过滤条件。

python 复制代码
def retrieve(state: State):
    """检索"""

    query = state["query"]
    retrieved_docs = vector_store.similarity_search(
        query["query"],
        filter={"section": query["section"]},
    )
    return {"context": retrieved_docs}

langgraph 创建链

在创建链的过程中,我们将定义查询分析方法,还将定义生成方法:

python 复制代码
def create_graph(llm_model_name):
    """创建langgraph"""

    llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)

    def analyze_query(state: State):
        """分析查询,推理出查询参数"""
        
        structured_llm = llm.with_structured_output(Search)
        query = structured_llm.invoke(state["question"])
        return {"query": query}

    def generate(state: State):
        """生成回答"""

        prompt = ChatPromptTemplate.from_messages([
            ("human", """你是问答任务的助手。
            请使用以下检索到的**上下文**来回答问题。
            如果你不知道答案,就说你不知道。最多使用三句话,并保持答案简洁。
            
            问题: {question} 

            上下文: {context} 

            回答:"""),
        ])
        
        docs_content = "\n\n".join(doc.page_content for doc in state["context"])
        messages = prompt.invoke({"question": state["question"], "context": docs_content})
        response = llm.invoke(messages)
        return {"answer": response.content}

    graph_builder = StateGraph(State).add_sequence([analyze_query, retrieve, generate])
    graph_builder.add_edge(START, "analyze_query")
    graph = graph_builder.compile()

    return graph

通过以上代码,我们可以清晰的看到整个执行过程:

  • analyze_query 将用户的请求转换成查询参数,将推理出的查询条件放在 statequery 键中;
  • retrieve 根据 statequery 中的查询条件检索矢量数据库,并将检索结果放在 statecontext 键中;
  • generate 基于 statecontextquestion 填充提示词,提交给 LLM(大语言模型) 处理。

现在一般的大语言模型处理聊天都很好了,针对这种 RAG 场景,使用 analyze_query 推理出查询参数的能力很关键。

见证效果

先定义一个问答方法:

python 复制代码
def ask(llm_model_name,question):
    """问答"""

    graph = create_graph(llm_model_name)
    for step in graph.stream(
        {"question": question},
        stream_mode="updates",
    ):
        print(f"{step}\n\n----------------\n")

再准备一个问题:

python 复制代码
q = "文章的结尾讲了langgraph的哪些优点?"

来看看各个大模型的表现吧。

  • qwen2.5
text 复制代码
{'analyze_query': {'query': {'query': 'langgraph 的优点 结尾', 'section': '结尾'}}}

----------------

{'retrieve': ...}

----------------

{'generate': {'answer': 'langgraph 的优点包括可视化程度高、兼容性强以及易定制。这些特点使得代码虽然多一些,但更容易理解和跟踪执行细节,并且可以顺畅运行多种大模型,同时也方便修改和增删节点。'}}

----------------

显然,qwen2.5 比较好的推理出了查询参数,也获得了妥帖的结果。

完美!

  • llama3.1
text 复制代码
{'analyze_query': {'query': {'query': 'langgraph的优点', 'section': '给安'}}}

----------------

...

不知道为什么推理出的 query 内容没问题,但是 section 显然不对了。

  • MFDoom/deepseek-r1-tool-calling:7b
text 复制代码
{'analyze_query': {'query': {'query': 'langgraph的优点', 'section': '结尾'}}}

----------------

{'retrieve': ...}

----------------

{'generate': {'answer': '大象的学名是Elephas spp.。它们具有极高的智商和良好的记忆能力,并在文化中被视为神圣动物,曾用于古代战争、运输和旅游业。'}}

完美!

总结

这次我们创建的 RAG 系统的检索步骤增加了过滤条件,相当于可以用两个参数进行检索。实际上我们完全可以定义很多条件,由大语言模型推理出查询参数。

在此次的演练中,qwen2.5deepseek 都不错。

如果您想自己实现一个包含前端和后端的 RAG 系统,从零搭建langchain+本地大模型+本地矢量数据库的RAG系统 可能对您入门有帮助。


代码

本文涉及的所有代码以及相关资源都已经共享,参见:

为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。

参考

🪐感谢您观看,祝好运🪐

相关推荐
小森( ﹡ˆoˆ﹡ )3 分钟前
DeepSeek 全面分析报告
人工智能·自然语言处理·nlp
deephub17 分钟前
用PyTorch从零构建 DeepSeek R1:模型架构和分步训练详解
人工智能·pytorch·python·深度学习·deepseek
阿正的梦工坊24 分钟前
详解 @符号在 PyTorch 中的矩阵乘法规则
人工智能·pytorch·矩阵
人类群星闪耀时29 分钟前
大数据平台上的机器学习模型部署:从理论到实
大数据·人工智能·机器学习
仙人掌_lz1 小时前
DeepSeek开源周首日:发布大模型加速核心技术可变长度高效FlashMLA 加持H800算力解码性能狂飙升至3000GB/s
人工智能·深度学习·开源
天上掉下来个程小白1 小时前
登录-10.Filter-登录校验过滤器
spring boot·后端·spring·filter·登录校验
合方圆~小文1 小时前
跨境宠物摄像头是一种专为宠物主人设计的智能设备
java·数据库·人工智能·扩展屏应用开发
猎人everest1 小时前
DeepSeek基础之机器学习
人工智能·机器学习·ai
SomeB1oody2 小时前
【Rust中级教程】2.8. API设计原则之灵活性(flexible) Pt.4:显式析构函数的问题及3种解决方案
开发语言·后端·性能优化·rust
Kai HVZ2 小时前
《计算机视觉》——图像拼接
人工智能·计算机视觉