为了账号安全,请及时绑定邮箱和手机立即绑定

从检索到智能:探索RAG、Agent+RAG及其评估(附TruLens使用方法)

解锁GPT生成的专属语料库
介绍

现在有很多很棒的基础模型可以用来启动你的自定义应用程序(比如gpt-4o、Sonnet、Gemini、Llama3.2、Gemma、Ministral等)。这些模型对历史、地理和维基百科上的信息都非常熟悉,但它们也有一些不足之处。主要有两个:一个是在细节上的不足(例如,模型知道宝马公司是什么,车型名称等基本信息;但如果问到比如欧洲的销量数据或某个特定发动机部件的细节,模型就无法回答了);另一个是关于最近信息的不足(例如,Llama3.2模型或Ministral的发布;这些基础模型是在某个特定时间点训练的,有一个知识截止日期,之后模型就不再更新任何新信息了)。

A lot of books, depicting the amount of LLM knowledge.

照片由 Jaredd Craig 拍摄,来自 Unsplash

这篇文章同时关注这两个问题,描述了在知识截止点前成立的假想公司的状况,其中一些信息最近被修改过。

为了应对这些问题,我们将采用RAG技术和LlamaIndex框架。RAG(即检索增强生成)的核心理念是,在生成答案时为模型提供最相关的信息。这样,我们可以建立一个含有定制数据的数据库,模型可以利用这些数据资源。为了进一步评估系统的性能,我们将引入TruLens库和RAG三元组性能指标。

提到知识截止点时,这个问题可以通过谷歌搜索引擎解决。然而,我们不能完全用搜索工具替代知识截止点。要理解这一点,想象两个机器学习专家:第一个专家对目前的生成式AI状态非常熟悉,而第二个专家六个月前从生成式AI转到经典计算机视觉领域。如果你问他们同样的关于如何使用最近的生成式AI模型的问题,他们需要的搜索量会有很大差异。第一个专家对这个问题非常熟悉,也许会查证一些具体命令。而第二个专家则需要查阅大量资料来理解模型的工作原理及其内部机制,之后才能回答。

基本上,这就像领域专家和一般专家的对比,前者能迅速回答问题,而后者则需要通过谷歌搜索来获取信息,因为他不了解所有细节。

关键点在于,大量的谷歌搜索在一个更长的时间范围内才能达到类似的效果。在聊天类应用中,用户不会等待几分钟等待模型搜索信息。此外,并非所有信息都是公开的,可以被搜索到的。

数据部分

目前可能很难找到一个未曾用于基础模型训练的数据集。差不多所有数据都已经被用于大规模模型的预训练。

Humans \(as companies\) walking around the forest looking for logs \(data\) and throwing them into the machine \(LLM\) that converts logs into fire. The “LLM” is written on the machine, and the “Data” is written on the logs. The fire out of the machines are going from the top.

这张图片是作者用AI(Bing)生成的。

那就是为什么我决定自己生成一个版本。为此,我使用了OpenAI界面中的_chatgpt-4o-latest_并通过几个相似的连续提示,所有的提示都类似于下面的这些:

生成一个包含想象中的乌克兰船业公司的一些细节的私人语料库。
比如产品列表、价格、负责人等信息。
我打算将其作为我的私人语料库用于RAG用例。我想要的内容包括各种细节。
你可以生成很多文本。越多越好。
行,继续推进合作伙伴关系,制定相关法律政策,参加比赛项目。可以加上我们制造船只的地方的信息,加一些定制的船只。

增加客户的使用研究

因此,我为四家不同的公司生成了私有语料库。以下是token的统计结果,以便更好地反映数据集的大小。

    # 以下使用`o200k_base`分词工具得到的token数:
    nova-drive-motors.txt: 2757
    aero-vance-aviation.txt: 1860
    ukraine-boats.txt: 3793
    city-solve.txt: 3826
    总token数=12236

以下是乌克兰船业公司简介的开头部分:

    ## **乌克兰游艇有限公司**  
    **公司概况:**  
    乌克兰游艇有限公司是一家位于乌克兰敖德萨的高端船只及海上解决方案的制造商和供应商。公司以其将传统工艺与现代技术相结合而自豪,为全球客户提供服务。自2005年成立以来,该公司已成为游艇行业的领导者,专注于休闲、商业和豪华船只。  
     - -  
    ### **产品线**  
    #### **休闲艇:**  

1. **WaveRunner X200**  
    - **描述:** 这是一款专为水上运动爱好者设计的高速艇,配备了先进的导航和安全功能。  
    - **价格:** $32,000  
    - **目标市场:** 年轻的冒险家和水上运动爱好者。  
    - **特点:**  
    - 最高时速:85英里  
    - 内置GPS及自动驾驶模式  
    - 乘坐人数:4人  
    - 轻质碳纤维艇体  

2. **AquaCruise 350**  
    - **描述:** 这款摩托船非常适合垂钓、家庭旅行和休闲巡航。  
    - **价格:** $45,000  
    - **特点:**  
    - 乘坐人数:12人  
    - 双300马力引擎  
    - 可模块化的内部布局,可配置座位和储物空间  
    - 可选的钓鱼设备升级包  

3. **SolarGlide EcoBoat**  
    - **描述:** 这款太阳能驱动的游艇适合环保意识强的客户。  
    - **价格:** $55,000  
    - **特点:**  
    - 太阳能电池板顶盖,12小时充电时间  
    - 零排放  
    - 最高时速:50英里  
    - 静音电机技术  
     - -  
    …

完整的私人语料库可以在GitHub找到。

为了评估数据集,我还让模型根据给定的语料库生成了10个问题(仅关于乌克兰船只公司 的,乌克兰船只股份有限公司)。

基于以上整个语料库,生成10个问题及其对应答案,并将这些问答对放入Python的原生数据结构中

我们获得的数据集如下。

    [  
        {  
            "question": "乌克兰游艇公司(乌克兰游艇有限公司)的主要业务重点是什么?",  
            "answer": "乌克兰游艇公司专注于制造高质量的娱乐用游艇、豪华游艇和商用游艇,将传统工艺与现代技术相结合。"  
        },  
        {  
            "question": "乌克兰游艇公司提供的娱乐用游艇价格范围是多少?",  
            "answer": "娱乐用游艇的价格范围从WaveRunner X200的32,000美元到SolarGlide EcoBoat的55,000美元不等。"  
        },  
        {  
            "question": "哪个制造基地专注于定制游艇?",  
            "answer": "利沃夫定制工艺车间专门制造定制游艇和高端定制,包括手工木作和高级材料。"  
        },  
        {  
            "question": "乌克兰游艇公司提供的船只提供有怎样的保修范围?",  
            "answer": "所有船只都提供有5年的制造缺陷保修,而发动机则有单独的3年性能保证。"  
        },  
        {  
            "question": "哪个客户使用了Neptune Voyager双体船,对他们的业务有什么影响?",  
            "answer": "天堂度假村国际公司使用了Neptune Voyager双体船,导致度假预订量增加了45%,并赢得了“最佳旅游体验”奖。"  
        },  
        {  
            "question": "SolarGlide EcoBoat在2022年全球海洋设计挑战赛中获得了什么奖项?",  
            "answer": "SolarGlide EcoBoat在2022年全球海洋设计挑战赛中获得了“最佳环保设计”奖。"  
        },  
        {  
            "question": "北极研究联盟如何从Poseidon Explorer中受益?",  
            "answer": "Poseidon Explorer使北极研究联盟成功完成了五次北极研究任务,提升了数据采集效率60%,并改善了极端条件下的安全性。"  
        },  
        {  
            "question": "Odessa Opulence 5000豪华游艇的价格是多少?",  
            "answer": "Odessa Opulence 5000豪华游艇的起售价为1,500,000美元。"  
        },  
        {  
            "question": "哪些功能使WaveRunner X200适合水上运动等水上活动?",  
            "answer": "WaveRunner X200配备最高时速85英里,轻量化碳纤维船身,内置GPS和自动驾驶模式,非常适合水上运动等水上活动。"  
        },  
        {  
            "question": "乌克兰游艇公司正在推进什么样的可持续性倡议行动?",  
            "answer": "乌克兰游艇公司正在推进绿色海事倡议行动(GMI),计划到2030年通过引入可再生能源解决方案,降低碳足迹50%。"  
        }  
    ]

现在,当我们有了私人语料库和问答数据集,就可以把这些数据放到合适的存储里了。

数据的传播过程

我们可以使用多种数据库,但为了这个项目以及未来关系的处理,我将Neo4j数据库整合进了我们的解决方案。此外,注册后Neo4j会提供一个免费实例。

现在,我们开始准备节点吧。首先,我们创建了一个嵌入模型的实例。我们使用了256维的向量,因为最近的一些测试表明,更大的向量维度会导致得分的方差更小(而这并不是我们所需要的)。作为嵌入模型,我们选择了text-embedding-3-small 模型。

    # 初始化嵌入模型  
    embed_model = OpenAIEmbedding(  
      model=CFG['configuration']['models']['embedding_model'],  
      api_key=os.getenv('AZURE_OPENAI_API_KEY'),  
      dimensions=CFG['configuration']['embedding_dimension']  
    )

之后,我们就阅读了语料库:

    # 获取文档的路径  
    document_paths = [Path(CFG['configuration']['data']['raw_data_path']) / document for document in CFG['configuration']['data']['source_docs']]  

    # 初始化文件读取器对象  
    reader = SimpleDirectoryReader(input_files=document_paths)  

    # 将文档加载为LlamaIndex中的文档  
    documents = reader.load_data()

另外,我们使用SentenceSplitter将文档分割成单独的节点。并将存储在Neo4j数据库中。

    neo4j_vector = Neo4jVectorStore(  
        username=CFG['configuration']['db']['username'],  
        password=CFG['configuration']['db']['password'],  
        url=CFG['configuration']['db']['url'],  
        embedding_dimension=CFG['configuration']['embedding_dimension'],  
        hybrid_search=CFG['configuration']['hybrid_search']  
    )  

    # 设置上下文环境  
    storage_context = StorageContext.from_defaults(  
        vector_store=neo4j_vector  
    )  

    # 填充节点  
    index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

混合搜索目前被关闭了。这是特意这样做的,目的是展示向量搜索算法的性能。

我们准备好了,现在我们可以开始查询流程。

UI of the Neo4j Aura depicting the Nodes we have inserted to the DB.

来源:作者自拍的图片

流水线

RAG(检索增强生成技术)可以作为一个独立的解决方案实现,也可以作为一个代理的一部分实现。这个代理需要处理所有聊天历史、工具管理、推理以及生成输出。接下来,我们来看看如何实现单独的RAG查询引擎以及代理(它能将RAG作为工具之一来使用)。

在谈论聊天模型时,很多人通常会选择OpenAI的模型而忽略其他选择。我们将介绍在OpenAI模型和Meta Llama 3.2模型上使用RAG的情况。让我们来比较一下哪个表现更好。

所有的配置参数现在都在 pyproject.toml 文件里了。

    [配置]
    similarity_top_k = 10  
    vector_store_query_mode = "默认模式"  
    similarity_cutoff = 0.75  
    response_mode = "紧凑"  
    distance_strategy = "余弦"  
    embedding_dimension = 256  
    chunk_size = 512  
    chunk_overlap = 128  
    separator = " "  
    max_function_calls = 2  
    混合搜索 = false  

    [配置数据]  
    raw_data_path = "../data/companies"  
    dataset_path = "../data/companies/dataset.json"  
    source_docs = ["city-solve.txt", "aero-vance-aviation.txt", "nova-drive-motors.txt", "ukraine-boats.txt"]  

    [配置模型]  
    llm = "gpt-4o-mini"  
    embedding_model = "text-embedding-3-small"  
    temperature = 0  
    llm_hf = "meta-llama/Llama-3.2-3B-Instruct"  
    上下文窗口 = 8192  
    max_new_tokens = 4096  
    hf_token = "hf_custom-token"  
    llm评估 = "gpt-4o-mini"  

    [配置数据库]  
    url = "neo4j+s://custom-url"  
    username = "neo4j"  
    password = "custom-password"  
    database = "neo4j"   
    索引名称 = "文章" # 如果需要加载新的不会与已上传的数据冲突的数据,请更改此项  
    text_node_property = "text"

两个模型共同的步骤是连接到neo4j中现有的向量索引。

    # 连接到现有的neo4j向量索引
    vector_store = Neo4jVectorStore(
      username=CFG['configuration']['db']['username'],
      password=CFG['configuration']['db']['password'],
      url=CFG['configuration']['db']['url'],
      embedding_dimension=CFG['configuration']['embedding_dimension'],
      distance_strategy=CFG['configuration']['distance_strategy'],
      index_name=CFG['configuration']['db']['index_name'],
      text_node_property=CFG['configuration']['db']['text_node_property']
    )
    index = VectorStoreIndex.来自向量存储(vector_store)
开放AI

首先,我们应初始化所需的OpenAI模型。我们将使用_gpt-4o-mini_作为语言模型,并使用相同的嵌入模型。我们将LLM和嵌入模型指定为Settings对象。这样我们就无需再传递这些模型了。如果需要,LlamaIndex会尝试从Settings中解析LLM。

    # 初始化LLM和嵌入模型  
    llm = OpenAI(  
      api_key=os.getenv('AZURE_OPENAI_API_KEY'),  
      model=CFG['configuration']['models']['llm'],  
      temperature=CFG['configuration']['models']['temperature']  
    )  
    embed_model = OpenAIEmbedding(  
      model=CFG['configuration']['models']['embedding_model'],  
      api_key=os.getenv('AZURE_OPENAI_API_KEY'),  
      dimensions=CFG['configuration']['embedding_dimension']  
    )  

    Settings.llm = llm  
    Settings.embed_model = embed_model
查询处理引擎

然后,我们可以从现有的向量索引创建一个默认的查询引擎来。

    # 创建查询引擎实例
    query_engine = index.as_query_engine()  # 创建查询引擎实例

此外,我们还可以通过简单的查询方法(如query())获取RAG逻辑。此外,我们还打印了从数据库中获取的源节点的列表以及最终的LLM回复。

    # 自定义问题  
    response = query_engine.query("乌克兰船舶公司的主要重点是什么?")  

    # 获取相似度得分  
    for node in response.source_nodes:  
      打印(f'{node.node.id_}, {node.score}')  

    # 预测的答案  
    打印(response.response)

这是示例输出:

    ukraine-boats-3, 0.8536546230316162  
    ukraine-boats-4, 0.8363556861877441  

    乌克兰游艇公司主要业务是设计、制造和销售豪华及环保游艇,非常注重客户满意度和环境保护。

如您所见,我们为节点创建了自定义ID,以便我们可以理解文件来源及块的序号标识。通过使用低级别的LlamaIndex API,我们可以使查询引擎表现更加具体。

    # 自定义检索模块  
    retriever = VectorIndexRetriever(  
      index=index,  
      similarity_top_k=CFG['configuration']['similarity_top_k'],  
      vector_store_query_mode=CFG['configuration']['vector_store_query_mode']  
    )  

    # 相似度门槛  
    similarity_postprocessor = SimilarityPostprocessor(similarity_cutoff=CFG['configuration']['similarity_cutoff'])  

    # 自定义响应生成器  
    response_synthesizer = get_response_synthesizer(  
      response_mode=CFG['configuration']['response_mode']  
    )  

    # 创建自定义查询引擎  
    query_engine = RetrieverQueryEngine(  
      retriever=retriever,  
      node_postprocessors=[similarity_postprocessor],  
      response_synthesizer=response_synthesizer  
    )

在这里我们指定了自定义检索器、相似度处理和细化阶段的操作。

为了让你的LlamaIndex组件更加符合你的需求,你可以为它们创建自定义封装。

官代理或经纪

为了在LlamaIndex中实现基于RAG的代理功能,我们需要使用预定义的AgentWorkers之一。我们将采用OpenAIAgentWorker,它用OpenAI的LLM作为其智能核心。此外,我们将上一节中的查询引擎封装成了QueryEngineTool,代理可以根据工具描述进行选择。

    AGENT_SYSTEM_PROMPT = "你是一个乐于助人的助手。在回答任何问题之前,你应该总是先使用retrieve_semantically_similar_data工具。如果使用该工具无法找到答案,则只需回复`没有找到相关信息`。"
    TOOL_NAME = "retrieve_semantically_similar_data"
    TOOL_DESCRIPTION = "提供有关公司的额外信息。输入:字符串"

    # agent 工作者
    agent_worker = OpenAIAgentWorker.from_tools(
        [
            QueryEngineTool.from_defaults(
                query_engine=query_engine,
                name=TOOL_NAME,
                description=TOOL_DESCRIPTION,
                return_direct=False,
            )
        ],
        system_prompt=AGENT_SYSTEM_PROMPT,
        llm=llm,
        verbose=True,
        max_function_calls=CFG['配置']['max_function_calls']
    )

要进一步使用代理,我们需要一个代理运行器。这个运行器更像是一个调度程序,负责高层次的交互和状态的管理,而工作者则负责执行具体的任务,例如工具和LLM的应用。

    # agent 运行器(agent_runner)
    # 初始化agent运行器
    agent = AgentRunner(agent_worker=agent_worker)

AgentRunner holding the context, history, tool calls and the AgentWorker doing all the low-level work.

图片来源:图片来自LlamaIndex 的官方文档(点击访问)https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/agent_runner/稳定版本

为了高效测试用户代理之间的交互,我实现了一个简单的类似聊天界面的交互方式。

    while True:  
      # 获取用户输入  
      current_message = input('请输入您的消息:')  
      print(f'{datetime.now().strftime("%H:%M:%S.%f")[:-3]}|用户: {current_message}')  

      response = agent.对话(current_message)  # "chat" translated as "对话"
      print(f'{datetime.now().strftime("%H:%M:%S.%f")[:-3]}|助手: {response.response}')  # Direct reference to "response.response"
      # 时间格式: 小时:分钟:秒.毫秒

这里有这样一个聊天记录样本:

    输入下一条消息:Hi  
    15:55:43.101|用户:Hi  
    已将用户消息添加到内存:Hi  
    15:55:43.873|代理:未找到相关信息。  
    输入下一条消息:你知道关于城市解决方案的事情吗?  
    15:56:24.751|用户:你知道关于城市解决方案的事情吗?  
    已将用户消息添加到内存:你知道关于城市解决方案的事情吗?  
    === 调用函数 ===  
    调用函数:retrieve_semantically_similar_data,参数:{"input":"城市解决方案"}  
    获取输出:空响应  
    ========================  

    15:56:37.267|代理:未找到相关信息。  
    输入下一条消息:乌克兰船业有限公司的主要业务是什么?  
    15:57:36.122|用户:乌克兰船业有限公司的主要业务是什么?  
    已将用户消息添加到内存:乌克兰船业有限公司的主要业务是什么?  
    === 调用函数 ===  
    调用函数:retrieve_semantically_similar_data,参数:{"input":"乌克兰船业有限公司"}  
    获取输出:乌克兰船业有限公司是一家总部位于乌克兰敖德萨的高端船舶和海洋解决方案制造商和供应商。公司成立于2005年,专注于休闲、商业和豪华船只的生产和供应,将传统工艺与现代技术相结合。该公司在欧洲、北美和亚洲建立了强大的市场地位,通过与德国的巴尔蒂克海洋分销商、美国的OceanCraft LLC、日本的横滨海科技术公司等分销合作伙伴建立合作关系。  

    该公司分为几个部门,包括工程、销售和市场营销、生产和客户服务,每个部门都有特定的责任,确保高效的运营和客户满意度。乌克兰船业有限公司致力于可持续发展,通过诸如绿色海洋倡议等举措减少碳足迹,采用了可再生能源解决方案。  

    产品系列包括休闲船如WaveRunner X200和AquaCruise 350、豪华游艇如Odessa Opulence 5000和商业船只如Maritime Hauler 7000。该公司还提供定制选项、维护计划和一系列配件,以增强航海体验。  

    乌克兰船业有限公司专注于创新和质量,多次荣获海事设计奖项,并不断扩展其全球足迹,同时坚持环境合规和客户服务卓越。  
    ========================  

    15:57:49.505|代理:乌克兰船业有限公司的主要业务是制造和供应高端船只和海洋解决方案。该公司总部位于乌克兰敖德萨,专注于休闲、商业和豪华船只的生产和供应,结合传统工艺与现代技术。公司致力于可持续发展,通过诸如绿色海洋倡议等举措减少碳足迹。产品系列包括各种类型的船只,还提供定制选项和维护计划。  
    输入下一条消息:你知道关于政府公司城市解决方案的事情吗?  
    15:59:02.830|用户:你知道关于政府公司城市解决方案的事情吗?  
    已将用户消息添加到内存:你知道关于政府公司城市解决方案的事情吗?  
    === 调用函数 ===  
    调用函数:retrieve_semantically_similar_data,参数:{"input":"政府公司城市解决方案"}  
    获取输出:CitySolve市政服务公司是一家由政府所有和运营的实体,成立于1988年,总部位于美国新都市。其使命是通过及时解决居民的日常需求,确保城市的生活、可持续性和高效性。该公司专注于供水和排水、公园绿地管理、道路和人行道、废物管理、公共安全基础设施和城市公用事业等关键服务领域。CitySolve强调核心价值观,包括响应性、可靠性、责任性和可持续性,并利用现代技术提高运营效率和社区影响力。  
    ========================  

    15:59:12.693|代理:CitySolve市政服务公司是一家由政府所有和运营的实体,成立于1988年,总部位于美国新都市。其使命是通过及时解决居民的日常需求,确保城市的生活、可持续性和高效性。该公司专注于供水和排水、公园绿地管理、道路和人行道、废物管理、公共安全基础设施和城市公用事业等关键服务领域。CitySolve强调核心价值观,包括响应性、可靠性、责任性和可持续性,并利用现代技术提高运营效率和社区影响力。  
    输入下一条消息:谢谢  
    15:59:28.761|用户:谢谢  
    已将用户消息添加到内存:谢谢  
    15:59:30.327|代理:没有更多相关信息。

我们可以看到,为了正确执行向量搜索,你需要提供更多能够语义匹配的输入细节。

开源项目

我们使用了一个开源模型,即_meta-llama/Llama-3.2–3B-Instruct_. 这一选择是基于模型延迟和性能的权衡。首先,我们需要通过访问令牌来验证我们的HuggingFace账号。

在这里使用你的token  
login(token=CFG['configuration']['models']['hf_token'])

为了在LlamaIndex中使用Llama作为LLM,我们需要创建一个模型包装器。我们将使用单个NVIDIA GeForce RTX 3090来运行我们的Llama 3.2模型。

SYSTEM_PROMPT = """你是一个根据给定来源文档友好回答问题的人工智能助手。你需要遵循以下规则:  
- 生成人类可读的输出,避免生成无意义的文字。  
- 只生成请求的输出,不要在请求的输出前后包括其他语言。  
- 直接回答问题,不要说诸如‘谢谢’、‘我很乐意帮忙’或‘我是一个AI助手’之类的废话。  
- 使用北美商业文档中通常使用的专业语言。  
- 不要生成任何冒犯或粗俗的语言。  
"""

query_wrapper_prompt = PromptTemplate(
    "<|start_header_id|>system<|end_header_id|>\n" + SYSTEM_PROMPT + "<|eot_id|><|start_header_id|>user<|end_header_id|>{query_str}<|eot_id|><|start_header_id|>assistant<|end_header_id|>"
)

llm = HuggingFaceLLM(
    context_window=CFG['configuration']['models']['context_window'],
    max_new_tokens=CFG['configuration']['models']['max_new_tokens'],
    generate_kwargs={"temperature": CFG['configuration']['models']['temperature'], "do_sample": False},
    query_wrapper_prompt=query_wrapper_prompt,
    tokenizer_name=CFG['configuration']['models']['llm_hf'],
    model_name=CFG['configuration']['models']['llm_hf'],
    device_map="cuda:0",
    model_kwargs={"torch_dtype": torch.bfloat16}
)

Settings.llm = llm
查询处理引擎

接口都一样。例如:如下:

    ukraine-boats-3, 0.8536546230316162  
    ukraine-boats-4, 0.8363556861877441  

    乌克兰游艇股份有限公司的主要业务是设计、制造和销售高端及环保型游艇,十分注重客户满意度和环境可持续性。
代理人

对于OpenAI的模型,LlamaIndex为OpenAI模型设计了一个特别的代理包装器,但对于开源模型的话,则需要使用另一个包装器。我们选择了ReActAgent,它通过反复推理和行动直到得出最终答案。

    agent_worker = ReActAgentWorker.from_tools(  
        [  
            QueryEngineTool.from_defaults(  
                query_engine=query_engine,  
                name=TOOL_NAME,  
                description=TOOL_DESCRIPTION,  
                return_direct=False,  
            )  
        ],  
        llm=llm,  
        verbose=True,  
        chat_history=[ChatMessage(content=AGENT_SYSTEM_PROMPT, role="system")]  
    )  

    # 代理运行
    agent = AgentRunner(agent_worker=agent_worker)

以下为相同内容的讨论,但使用了不同的AI代理:

    插入下一个消息:嗨  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    16:04:29.117|用户: 嗨  
    思考: 用户说的是英语,我需要使用工具来回答这个问题。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': 'hello world', 'num_beams': 5}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察: 没有响应  
    思考: 我还是没有足够的信息来回答这个问题,工具提供的信息也没有帮助。我打算收集更多信息。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': 'hello world', 'num_beams': 5}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察: 没有响应  
    思考: 我还是没有足够的信息来回答这个问题,工具提供的信息也没有帮助。我打算收集更多信息。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': 'hello world', 'num_beams': 5}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察: 没有响应  
    思考: 我开始认为这个工具不是回答这个问题的最佳方法。用户的输入 "hello world" 非常通用,工具没有提供任何有用的输出。我将尝试采用不同的方法。  
    回答: 你好,今天我能帮你什么?  
    16:04:37.764|代理: 你好,今天我能帮你什么?  
    插入下一个消息:你知道有关解决城市的任何事情吗?  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    16:05:08.756|用户: 你知道有关解决城市的任何事情吗?  
    思考: 用户说的是英语,我需要使用工具来回答这个问题。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': 'solve city'}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察:   

    CitySolve Municipal Services 是新乌拉尼亚市的命脉,解决一系列城市问题,并为居民提供日常需求的快速解决方案。  
    思考: 我不需要再使用工具了,我将用用户的语言回答问题。  
    回答: CitySolve Municipal Services 是一家城市组织,在新乌拉尼亚为居民提供日常需求的解决方案。  
    16:05:13.003|代理: CitySolve Municipal Services 是一家城市组织,在新乌拉尼亚为居民提供日常需求的解决方案。  
    插入下一个消息:乌克兰船业的主要关注点是什么?  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    16:05:34.892|用户: 乌克兰船业的主要关注点是什么?  
    思考: 用户说的是英语,我需要使用工具来回答这个问题。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': 'Ukraine Boats Inc.'}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察:   

    乌克兰船业公司是一家位于乌克兰奥德萨的优质船只和海事解决方案的领先制造商和供应商。该公司以结合传统工艺和现代技术而自豪,服务于世界各地的客户。成立于2005年,该公司已成为造船业的领导者,专注于休闲、商业和豪华船只。  

    该公司已成功向包括Blue Horizon Fisheries、Azure Seas Luxury Charters、Coastal Safety Patrol、EcoTrade Logistics、Team HydroBlitz Racing和Paradise Resorts International在内的各种客户交付了各种船只和解决方案。这些客户报告了与乌克兰船业公司合作带来的显著好处,包括效率提高、成本降低和客户满意度增强。  

    乌克兰船业公司提供一系列产品和服务,包括豪华游艇、商用船只和配件。该公司的产品设计满足每个客户的具体需求,其专家团队与客户紧密合作,确保每艘船都符合客户的要求。  

    该公司的一些知名产品包括最先进的豪华游艇Odessa Opulence 5000和强大的货船Maritime Hauler 7000。该公司还提供船只定制套餐、年度维护计划和其他服务,以支持客户的需要。  

    总之,乌克兰船业公司是客户寻求高品质船只和海事解决方案时值得信赖和可靠的合作伙伴。  
    思考: 我不需要再使用工具了,我将用用户的语言回答问题。  
    回答: 乌克兰船业公司是一家位于乌克兰奥德萨的优质船只和海事解决方案的领先制造商和供应商,结合传统工艺与现代技术为世界各地的客户服务。  
    16:05:53.311|代理: 乌克兰船业公司是一家位于乌克兰奥德萨的优质船只和海事解决方案的领先制造商和供应商,结合传统工艺与现代技术为世界各地的客户服务。  
    插入下一个消息:你知道有关政府公司城市解决的事情吗?  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    16:06:09.949|用户: 你知道有关政府公司城市解决的事情吗?  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    思考: 用户说的是英语,我需要使用工具来回答这个问题。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'input': AttributedDict([('title', 'CitySolve'), ('type', 'string')])}  
    观察: 错误: 2 个校验错误  
    query.str  
      输入应为有效字符串 [类型=string_type, 输入值=AttributedDict([('title',...'), ('type', 'string')]), 输入类型=AttributedDict]  
        有关更多相关信息,请访问 https://errors.pydantic.dev/2.9/v/string_type  
    query.QueryBundle.query_str  
      字段缺失 [类型=missing, 输入值=AttributedDict([('title',...'), ('type', 'string')]), 输入类型=AttributedDict]  
        有关更多相关信息,请访问 https://errors.pydantic.dev/2.9/v/missing  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察: 错误: 无法解析输出。请遵循思考-行动-输入格式。请再次尝试。  
    思考: 我明白工具 retrieve_semantically_similar_data 需要特定的输入格式。我将确保遵循正确的格式。  
    行动: retrieve_semantically_similar_data  
    行动输入: {'title': 'CitySolve', 'type': 'string'}  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    观察:   

    CitySolve Municipal Services 是位于新乌拉尼亚的一个由政府所有和支持的城市服务公司,解决各种城市问题。  
    思考: 我不需要再使用工具了,我将用用户的语言回答问题。  
    回答: CitySolve Municipal Services 是位于新乌拉尼亚的一个由政府所有和支持的城市服务公司,解决各种城市问题。  
    16:06:17.799|代理: CitySolve Municipal Services 是位于新乌拉尼亚的一个由政府所有和支持的城市服务公司,解决各种城市问题。  
    插入下一个消息:谢谢  
    设置 `pad_token_id` 为 `eos_token_id`:None 以进行开放式生成。  
    16:06:34.232|用户: 谢谢  
    思考: 我不需要再使用工具了,我将用用户的语言回答问题。  
    回答: CitySolve Municipal Services 是位于新乌拉尼亚的一个由政府所有和支持的城市服务公司,解决各种城市问题。  
    16:06:35.734|代理: CitySolve Municipal Services 是位于新乌拉尼亚的一个由政府所有和支持的城市服务公司,解决各种城市问题。

正如我们所见,这些代理的推理方式有所不同。面对同样的问题,两个模型选择以不同的方式查询工具。第二个代理在使用工具时也遇到了一次失败,但这更多是工具描述不够清晰的问题,而不是代理本身的问题。它们都为用户提供了非常有用的答案,这正是RAG方法所追求的目标。

此外,你可以为你的模型添加许多不同的代理封装器,它们可能会极大地改变模型与世界的互动方式。

评价

如今,要评估RAG,有许多框架可以选择。总体来说,其中一个框架就是TruLens。RAG的性能是通过所谓的RAG三元组(即答案的相关性、上下文的关联性和依据性)来评估的。

为了估计相关性和实证性,我们将利用大语言模型。大语言模型将作为评估者,根据给定的信息对答案评分。

TruLens 本身就是很方便的工具,用来在指标层面上测量系统性能,并帮助分析具体记录的评分。这是排行榜 UI 的样子:

UI leaderboard view of the TruLens framework

来源:作者自制的图片

下面列出了每条记录的评估表,您可以在其中查看所有被触发的内部流程。

Per-record table of assessments, where you can review all the internal processed being invoked. Part of the TruLens UI.

来源:作者的原创图片

要了解更多详情,你可以查看特定记录的执行流程。

Execution process for a specific record inside the TruLens UI.

来源:作者创作的图片

为了实施RAG三元组评估,我们首先需要定义实验名称和模型提供商。我们将使用_gpt-4o-mini_模型进行评估。

    experiment_name = "llama-3.2-3B-custom-retriever"  

    provider = OpenAIProvider(  
        model_engine=CFG['configuration']['models']['llm_evaluation']  
    )

接下来,我们定义了三元组(答案的相关性、上下文的相关性、依据性)。对于每个指标,我们需要明确输入和输出。

    context_selection = TruLlama.select_source_nodes().node.text  

    # 上下文相关性评分(针对每个上下文片段)
    f_context_relevance = (  
        Feedback(  
            provider.context_relevance, name="上下文相关性"  
        )  
        .on_input()  
        .on(context_selection)  
    )  

    # 事实基础
    f_groundedness_cot = (  
        Feedback(  
            provider.groundedness_measure_with_cot_reasons, name="事实基础"  
        )  
        .on(context_selection.collect())  
        .on_output()  
    )  

    # 问答相关性
    f_qa_relevance = (  
        Feedback(  
            provider.relevance_with_cot_reasons, name="问答相关性"  
        )  
        .on_input_output()  
    )

此外,我们初始化了这个TruLlama对象,在代理调用期间计算反馈。

    # 创建TruLlama代理
    tru_agent = TruLlama(
        agent,
        app_name=experiment_name,
        tags="测试代理",
        feedbacks=[f_qa_relevance, f_context_relevance, f_groundedness_cot],
    )

现在我们可以开始在我们的数据集上运行评估流程了。

    for item in tqdm(dataset):  
        try:  
            agent.reset()  

            with tru_agent as recording:  
                agent.query(item.get('问题'))  
            record_agent = recording.get()  

            # 等待所有反馈函数它们完成  
            for feedback, result in record_agent.wait_for_feedback_results().items():  
                logging.info(f"{feedback.name}: {result}")  
        except Exception as e:  
            logging.error(f"发生错误:{e}")  
            traceback.format_exc()

我们使用了2个模型,即默认和自定义查询引擎,并加入了额外的工具输入参数说明(ReAct代理在缺少明确的工具输入参数说明时,尝试调用不存在的工具来重构输入,从而遇到困难)。我们可以使用get_leaderboard()方法来查看结果,结果将以DataFrame形式展示。

结尾

Data -> neo4j -> agent -> rag pipeline

来源:作者用AI(Bing)制作的图片

我们获得了一个私有语料,该语料包含通过GPT模型生成的自定义数据集。实际的语料内容非常有趣且多样化,涵盖了许多主题。这也是为什么现在很多模型能够成功地使用GPT生成的样本进行微调。

Neo4j DB 为许多框架提供了方便的接口,同时拥有出色的 UI(Aura)。在实际项目中,数据之间经常存在关系,图形数据库(GraphDB)在这种情况下是完美的选择。

基于私人语料库,我们实现了不同的RAG方法(独立和作为代理的一部分)。依据RAG三元组指标,我们观察到基于OpenAI的代理运行得非常完美,效果令人满意,而一个经过良好提示的ReAct代理表现相当一致。一个显著的区别在于使用了自定义查询引擎。这是因为我们配置了一些特定的流程和阈值,以适应我们的数据。此外,这两种解决方案都有很高的实体性,这对RAG应用而言至关重要。

还有一个挺有意思的地方是,Llama3.2 3B 和 gpt-4o-mini API 的代理呼叫的延迟时间几乎相同(当然,大部分时间都花在了数据库查询上,但这个差别还是不大的)。

虽然我们的系统运行得相当不错,但仍有许多改进的空间,比如关键词搜索、重排序器、邻近块选择算法以及真实标签对比等功能。这些话题将在接下来的几篇文章中进一步探讨,这些文章会涉及RAG(检索增强生成)应用。

私人的语料库,以及代码和提示信息,可以在 GitHub 找到。

附言:

特别感谢我的同事们:Alex SimkivAndy Bosyi,和Nazar Savchenko,感谢他们富有成效的交流和合作,以及他们的宝贵建议,还要感谢整个MindCraft.ai团队的成员一直以来的支持。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消