检索增强生成 (RAG) 技术正在成为优化大模型 (LLM) 应用的关键手段。本文将深入探讨如何利用多智能体 RAG 架构,构建一个服务于 Bandung 游客的智能聊天机器人,提升旅游咨询的效率与准确性。该解决方案不仅规避了传统 LLM 的知识截止日期和上下文理解不足等问题,还为个性化旅游推荐提供了可能。

RAG:弥补大模型的局限

RAG 的核心价值在于,它允许 LLM 在生成回复之前,先从外部知识库检索相关信息,从而增强模型的知识广度和时效性。传统的 LLM 受限于训练数据的范围和时间,往往无法回答超出其知识范围的问题。例如,一个模型的数据截止到2023年12月31日,就无法提供在此之后的信息。然而,通过 RAG,我们可以将最新的旅游景点信息、开放时间、票价等数据注入模型,使其能够回答最新的旅游咨询。

更进一步,RAG 还能提升模型对上下文的理解能力。假设用户询问“interface”是什么,在不同的语境下,其含义差异很大。通过 RAG,模型可以根据用户输入的上下文,从编程或应用设计的相关知识库中检索信息,从而给出更准确的答案。比如,如果用户的问题是关于“Java interface”,RAG 系统将会检索出关于面向对象编程中接口的定义和使用方法。

多智能体架构:精细化旅游咨询

为了提高咨询的精度和效率,本文介绍的聊天机器人采用了多智能体架构。在 人工智能 (AI) 领域,智能体通常指具备规划、推理和决策能力的软件系统。而智能助手则是专门设计来协助用户解决特定问题的 AI 智能体。

具体来说,我们将旅游咨询任务分解为多个专业领域,为每个领域创建一个专属的 RAG 智能体。例如,我们设立了以下五种智能体:

  • 自然风光智能体: 负责提供 Bandung 周边的自然景观信息,例如覆舟火山、白色火山口等。
  • 美食智能体: 负责推荐 Bandung 的特色美食和餐厅,例如当地的 Sundanese 美食。
  • 历史文化智能体: 负责介绍 Bandung 的历史遗迹和文化景点,例如亚非会议纪念馆、Gedung Sate 等。
  • 教育智能体: 负责提供 Bandung 的教育机构和科技馆信息,例如 Puspa Iptek Sundial。
  • 位置搜索智能体: 负责根据用户提供的地点和兴趣点,搜索附近的餐馆、咖啡馆等场所。

这种多智能体架构允许我们将复杂的旅游咨询任务分解为更小、更专业的子任务,从而提高每个智能体的响应速度和准确性。此外,这种架构还具有良好的可扩展性,我们可以根据需要随时添加新的智能体,以覆盖更多的旅游领域。

系统流程:从查询到答案

多智能体 RAG 聊天机器人的工作流程如下:

  1. 用户输入查询: 用户通过聊天界面输入旅游咨询问题。
  2. 聚合器智能体 (Aggregator Agent) 分发查询: 聚合器智能体作为总控,将用户查询分发给各个专业领域的 RAG 智能体。聚合器智能体像一个经验丰富的导游,知道将问题交给最合适的“专家”来处理。
  3. RAG 智能体检索和生成答案: 每个 RAG 智能体根据用户查询,从各自的知识库中检索相关信息,并利用 LLM 生成答案。这个过程包括数据清洗、文本分块、向量嵌入、向量数据库存储和提示工程等步骤。
  4. 位置搜索智能体进行地理位置查询: 如果用户查询涉及到地理位置信息,位置搜索智能体会利用 Open Street Map (OSM) 等地理信息服务,搜索相关的地点信息。比如,用户问“Gedung Sate 附近的咖啡馆”,位置搜索智能体会在 OSM 上查询 Gedung Sate 附近的咖啡馆,并返回结果。
  5. 聚合器智能体汇总答案: 聚合器智能体收集各个智能体的答案,并进行汇总和排序,最终将最相关的答案呈现给用户。

RAG 流程的细节:数据准备与知识库构建

RAG 流程的核心在于构建一个高质量的知识库。这需要进行细致的数据准备和处理。

  1. 数据清洗: 清洗原始文本数据,去除无用字符,例如换行符、多余的空格和标点符号等。代码示例如下:

    import re
    
    def cleaning_text(nama_file):
        with open(nama_file, 'r', encoding='utf-8') as file:
            content = file.read()
        content = content.replace('\n', ' ')
        pattern = re.compile(r'''
            (?<!\d)\.(?=\s+[A-Z])
           ''', re.VERBOSE)
        content = pattern.sub('<SP>', content)
        sentences = [sentence.strip() for sentence in content.split('<SP>') if sentence.strip()]
        return sentences
    

    这段代码去除了换行符,并通过正则表达式将句子分割成独立的片段,以便后续处理。

  2. 文本分块 (Chunking): 将大型文档分割成较小的文本块,以便 LLM 处理。每个 LLM 都有其能够处理的文本长度限制 (Token 限制)。文本分块可以有效地控制输入模型的文本长度,同时尽量保持文本块的语义完整性。 代码示例如下:

    from langchain_text_splitters import RecursiveCharacterTextSplitter
    
    def get_chunks(knowledge_base, chunk_size=450, chunk_overlap=85):
        text_splitter = RecursiveCharacterTextSplitter(
                        chunk_size=chunk_size,
                         chunk_overlap=chunk_overlap,
                         length_function=len
        )
        chunks = text_splitter.split_text("\n".join(knowledge_base))
        return chunks
    

    这段代码使用了 Langchain 提供的 RecursiveCharacterTextSplitter 来进行文本分块。 chunk_size 参数控制每个文本块的大小,chunk_overlap 参数控制相邻文本块之间的重叠部分,以保持上下文的连贯性。

  3. 向量嵌入 (Embedding): 将文本块转换成向量表示,以便进行语义搜索。向量嵌入技术可以将文本的语义信息编码成向量,使得语义相似的文本在向量空间中距离更近。 项目中使用了 nomic-embed-text:latest 模型作为嵌入模型,它可以处理大量的文本数据。

    from langchain_community.embeddings import OllamaEmbeddings
    from langchain_community.vectorstores import Chroma
    
    def get_ollama_embedding():
        embedding_Ollama = OllamaEmbeddings(model="nomic-embed-text:latest")
        return embedding_Ollama
    
    def get_vectorstore_chroma(chunks, collection_name):
        try:
            vectorstore_chroma = Chroma.from_texts(
                texts=chunks,
                embedding=get_ollama_embedding(),
                persist_directory=".chroma_db",
                collection_name=collection_name
                )
            vectorstore_chroma.persist()
            return vectorstore_chroma
        except:
            raise
    

    这段代码首先使用 OllamaEmbeddings 创建一个嵌入模型,然后使用 Chroma 将文本块和它们的向量表示存储到向量数据库中。

  4. 向量数据库存储: 将向量嵌入存储到向量数据库中,以便快速检索。向量数据库专门用于存储和检索向量数据,可以高效地进行语义搜索。 这里使用了 Chroma 数据库作为向量数据库。

提升检索效果:集成多种检索器

为了获得更好的检索效果,项目使用了多种检索器,并进行集成。

  1. BM25 检索器: BM25 是一种经典的文本检索算法,擅长进行关键词匹配。

  2. 基于 Embedding 的检索器: 这种检索器使用向量嵌入进行语义搜索,可以找到与查询语句语义相关的文本块。

  3. 集成检索器 (Ensemble Retriever): 将多种检索器结合起来,利用各自的优势,提高检索的准确性和召回率。

    from langchain.retrievers import BM25Retriever, EnsembleRetriever
    
    def get_dense_retriever(vectorstore_chroma):
        dense_retriever = vectorstore_chroma.as_retriever(search_kwargs={"k": 5})
        return dense_retriever
    
    def get_bm25_retriever(chunks, embedding):
        bm25_retriever = BM25Retriever.from_texts(
        texts=chunks,
        embedding=embedding
        )
        return bm25_retriever
    
    def get_ensemble_retirever(chunks, vectorstore_chroma, embedding):
        ensemble_retriever = EnsembleRetriever(
        retrievers=[get_dense_retriever(vectorstore_chroma=vectorstore_chroma), get_bm25_retriever(chunks, embedding)],
        weights=[0.5, 0.5]
        )
        return ensemble_retriever
    

    这段代码展示了如何创建 BM25 检索器、基于 Embedding 的检索器和集成检索器。weights 参数用于控制不同检索器的权重。

Prompt 工程:引导 LLM 生成高质量回复

Prompt 工程是指设计合适的提示语,引导 LLM 生成高质量的回复。 针对不同的任务,需要设计不同的 Prompt。

  • RAG Prompt: 用于引导 LLM 基于检索到的信息生成答案。Prompt 模板如下:

    Anda adalah asisten AI yang bertugas sebagai orang yang mempromosikan wisata alam di Kota Bandung.
    Tugas Anda adalah mencari pengetahuan berdasarkan pertanyaan atau konteks yang diberikan oleh tour guide atau pengarah wisatawan.
    Jika anda tidak mengetahui jawabannya yang sesuai konteks, maka jangan memberikan jawaban yang dikarang sendiri.
    pengetahuan yang didapatkan:
    ---
    {context_string}
    ---
    fakta Pengguna:
    {query_text}
    Jawaban hanya berdasarkan konteks di atas, jawablah dengan jelas dan sopan:
    

    这个 Prompt 引导 LLM 扮演一个 Bandung 旅游推广助手,基于给定的知识和用户的问题,生成清晰和礼貌的回答。

  • 位置搜索 Prompt: 用于引导 LLM 识别用户查询中的起始位置和目标地点。Prompt 模板如下:

    Anda adalah penunjuk lokasi, cari tahu informasi dari pertanyaan user mengenai lokasi awal user dan objek apa yang dicari
    hasil jawaban cukup lokasi awal dan objek yang dicari, hilangkan kata sekitar, daerah, wilayah dalam jawaban yang anda berikan dan jangan menjelaskan jawaban.
    Berikut adalah pertanyaan user: "{query_text}"
    Pilih salah satu dari kategori berikut:
    {options}
    Jawaban lokasi awal dan kategori yang dipisahkan dengan tanda hubung -, tanpa penjelasan.
    

    这个 Prompt 引导 LLM 识别用户的位置和想要搜索的地点类型,以便进行地理位置查询。

LangGraph:构建复杂的工作流程

LangGraph 是 LangChain 的一个扩展,用于构建有状态的、基于图的 AI 应用。它简化了多智能体系统的构建过程,使得数据的传递和处理更加高效。

在本项目中,LangGraph 用于定义多智能体之间的工作流程。 首先,需要定义状态 (State),它包含了在各个节点之间传递的数据,例如用户查询、各个智能体的回复等。 然后,使用 StateGraph 构建图结构,定义节点和边。 节点代表不同的智能体或处理步骤,边代表数据流动的方向。

from operator import add
from typing import TypedDict
from typing_extensions import Annotated
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.runnables import RunnableLambda

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    query: str
    w_a : Annotated[list[AnyMessage], add_messages]
    w_k : Annotated[list[AnyMessage], add_messages]
    w_p : Annotated[list[AnyMessage], add_messages]
    w_sb: Annotated[list[AnyMessage], add_messages]
    location:Annotated[list[AnyMessage], add_messages]
    final_answer: Annotated[list[AnyMessage], add_messages]
    role:str
    content:Annotated[list[AnyMessage], add_messages]
    map:object

builder = StateGraph(State)
builder.add_node("wisata_alam", RunnableLambda(ka.wisata_alam_asisten))
builder.add_node("wisata_kuliner", RunnableLambda(ka.wisata_kuliner_asisten))
builder.add_node("wisata_pendidikan", RunnableLambda(ka.wisata_pendidikan_asisten))
builder.add_node("wisata_sejarah", RunnableLambda(ka.wisata_sejarah_budaya_asisten))
builder.add_node("pencarian_lokasi", RunnableLambda(ka.lokasi_asisten))
builder.add_node("relevant_node", RunnableLambda(self.select_relevant_node))
builder.add_edge(START, "wisata_alam")
builder.add_edge(START, "wisata_kuliner")
builder.add_edge(START, "wisata_pendidikan")
builder.add_edge(START, "wisata_sejarah")
builder.add_edge(START, "pencarian_lokasi")
builder.add_edge("wisata_alam", "relevant_node")
builder.add_edge("wisata_kuliner", "relevant_node")
builder.add_edge("wisata_pendidikan", "relevant_node")
builder.add_edge("wisata_sejarah", "relevant_node")
builder.add_edge("relevant_node", END)
graph = builder.compile()
result = graph.invoke(
            input={"query":query, "messages": [], "w_a": [], "w_k": [], "w_p":[], "w_sb":[], "location":[], 'final_answer':[], "role":"user","content":[], "map":""}
        )

这段代码定义了一个包含五个智能体的 LangGraph 图。 用户查询首先被分发给五个智能体,每个智能体根据各自的知识库生成回复。 然后, relevant_node 节点负责选择最相关的回复,并将其呈现给用户。

结果与展望:持续优化 RAG 应用

初步实验结果表明,该多智能体 RAG 聊天机器人能够有效地回答 Bandung 旅游咨询问题。 例如,当用户询问 “Gedung Sate 附近的餐馆” 时,聊天机器人能够结合美食智能体和位置搜索智能体的结果,给出准确的推荐,并在地图上标出餐馆的位置。

尽管如此,该应用仍有许多改进空间。 例如,可以进一步优化知识库的内容,使其更加全面和准确。 还可以调整 Prompt 工程,提高 LLM 生成回复的质量。 此外,还可以探索更多的 RAG 技术,例如上下文压缩和查询重写,以进一步提高检索效果。 最后,除了 Bandung,未来可以将此方案扩展到其他城市,提供更广泛的旅游信息服务。

结语

RAG 技术为 大模型 的应用开辟了新的可能性。 通过集成多智能体架构和 LangGraph 等工具,我们可以构建更加智能、高效的 AI 应用,为用户提供更好的服务。随着 大模型 技术的不断发展,RAG 将在各行各业发挥越来越重要的作用。