在当今数字化时代,企业和教育机构每天都会收到海量的咨询问题。无论是客户支持、销售团队的提问,还是内部员工的咨询,手动回复这些问题不仅耗时费力,还容易出现回答不一致的情况。而基于人工智能的查询解答系统,能够快速、准确且高效地提供答案,极大地提升了工作效率和用户体验。
今天,我们就来聊聊如何利用LangChain、ChromaDB和CrewAI构建一个基于检索增强生成(RAG)的智能查询解答系统。这个系统不仅能自动处理各种问题,还能生成精准的回答,帮助企业和教育机构更好地服务用户。
在传统的业务流程中,手动回复客户或学员的咨询问题是一个极其低效的过程。客户希望得到即时的回复,而企业则需要快速获取准确的信息来做出决策。AI驱动的查询解答系统通过自动化处理这些问题,不仅减轻了人工负担,还能提供一致且高质量的回答。
在客户服务领域,AI系统可以自动回复常见问题,提升客户满意度;在销售和市场营销中,它可以实时提供产品细节和客户洞察;在金融、医疗、教育和电商等行业,AI驱动的查询处理能够确保操作顺畅,提升用户体验。
在动手构建系统之前,我们先来了解一下检索增强生成(RAG)系统是如何工作的。RAG架构主要分为三个关键阶段:索引、检索和生成。
系统首先需要处理和存储相关文档,以便能够快速检索。具体步骤如下:
当用户提交问题时,系统会先检索相关数据,然后再生成回答。具体步骤如下:
为了生成准确的回答,系统会将检索到的数据与原始查询结合。具体步骤如下:
接下来,我们将通过一个实际案例,展示如何构建一个基于RAG的查询解答系统。这个系统将高效地回答学员的问题,帮助他们更好地学习。
在构建RAG系统之前,最重要的就是数据。一个结构良好的知识库是关键,因为回答的准确性和相关性完全依赖于数据的质量。以下是一些适合不同类型用途的数据:
在我们的学员查询解答系统中,我们尝试了多种数据类型,最终发现使用课程视频的字幕是最有效的方法。字幕提供了与学员问题直接相关的结构化和详细内容,能够快速生成相关答案。
在动手编写代码之前,我们需要先规划系统的架构。系统需要完成以下三个主要任务:
为了实现这些功能,我们将系统分为三个组件:
现在,我们已经规划好了系统的架构,接下来就是动手实现。
构建AI驱动的学习支持系统,首先需要导入一些关键的库:
importpysrt
fromlangchain.text_splitterimportRecursiveCharacterTextSplitter
fromlangchain.schemaimportDocument
fromlangchain.embeddingsimportOpenAIEmbeddings
fromlangchain.vectorstoresimportChroma
fromcrewaiimportAgent, Task, Crew
importpandasaspd
importast
importos
importtime
fromtqdmimporttqdm
这些库的作用如下:
为了使用OpenAI的API进行嵌入,我们需要加载API密钥并配置模型设置。
withopen('/home/janvi/Downloads/openai.txt','r')asfile:
openai_api_key = file.read()
os.environ['OPENAI_API_KEY'] = openai_api_key
os.environ["OPENAI_MODEL_NAME"] ='gpt-4o-mini'
通过这些配置,我们可以确保系统能够高效地处理和存储嵌入。
字幕文件中包含了视频讲座的宝贵信息,是AI检索系统中结构化内容的丰富来源。有效地提取和处理字幕数据,能够让我们在回答学员问题时快速检索到相关信息。
我们使用pysrt库从SRT文件中提取文本,并将其组织成结构化的形式,以便进一步处理和存储。
defextract_text_from_srt(srt_path):
"""从SRT字幕文件中提取文本"""
subs = pysrt.open(srt_path)
text =" ".join(sub.textforsubinsubs)
returntext
由于课程可能包含多个字幕文件,我们需要系统地组织和迭代这些文件,以便无缝提取文本。
course_folders = {
"深度学习入门(使用PyTorch)":"C:\M\Code\GAI\Learn_queries\Subtitle_Introduction_to_Deep_Learning_Using_Pytorch",
"构建生产级RAG系统(使用LlamaIndex)":"C:\M\Code\GAI\Learn_queries\Subtitle of Building Production-Ready RAG systems using LlamaIndex",
"LangChain入门(构建生成式AI应用与代理)":"C:\M\Code\GAI\Learn_queries\Subtitle_introduction_to_langchain_using_agentic_ai"
}
course_srt_files = {}
forcourse, folder_pathincourse_folders.items():
srt_files = []
forroot, _, filesinos.walk(folder_path):
srt_files.extend(os.path.join(root, file)forfileinfilesiffile.endswith(".srt"))
ifsrt_files:
course_srt_files[course] = srt_files
这些提取的文本将成为我们AI驱动学习支持系统的基础,使我们能够进行高级检索和查询解答。
接下来,我们将课程字幕存储到ChromaDB中,包括文本切分、嵌入生成、持久化存储和成本估算。
persist_directory是一个文件夹路径,用于保存存储的数据。这样即使程序重新启动,嵌入数据也能保留下来。
persist_directory ="./subtitles_db"
大型文档(如整个课程字幕)可能会超出嵌入的标记限制。为了处理这种情况,我们使用RecursiveCharacterTextSplitter将文本切分为更小的、有重叠的块,以提高搜索精度。
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
每个块的长度为1000个字符,为了在块之间保留上下文,我们将前一个块的200个字符包含在下一个块中。这种重叠有助于保留重要细节,提高检索精度。
我们需要将文本转换为数值向量表示,以便进行相似性搜索。OpenAI的嵌入功能允许我们将课程内容编码为可以高效搜索的格式。
embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)
这里,OpenAIEmbeddings()使用我们的OpenAI API密钥初始化嵌入模型,确保每段文本都能转换为高维向量表示。
现在,我们将这些向量嵌入存储到ChromaDB中。
vectorstore = Chroma(
collection_name="course_materials",
embedding_function=embeddings,
persist_directory=persist_directory
)
collection_name="course_materials"在ChromaDB中创建了一个专门的集合,用于组织所有与课程相关的嵌入。embedding_function=embeddings指定了OpenAI嵌入用于将文本转换为数值向量。persist_directory=persist_directory确保所有存储的嵌入在程序重新启动后仍然可用。
在将文档添加到向量数据库之前,估算标记使用成本是非常重要的。由于OpenAI按每1000个标记收费,我们需要提前估算成本,以便高效管理开支。
COST_PER_1K_TOKENS =0.0001# 每1000个标记的成本(使用'text-embedding-ada-002'模型)
TOKENS_PER_CHUNK_ESTIMATE =750# 每1000字符块的估计标记数
total_tokens =0
total_cost =0
start_time = time.time()
COST_PER_1K_TOKENS=0.0001定义了使用OpenAI嵌入时每1000个标记的成本。TOKENS_PER_CHUNK_ESTIMATE=750估计每个1000字符块包含约750个标记。total_tokens和total_cost变量用于跟踪整个执行过程中处理的数据量和产生的成本。start_time变量记录了开始时间,用于测量整个过程的耗时。
我们希望避免重新处理已经存储在向量数据库中的课程。因此,我们先查询ChromaDB,检查课程是否已经存在。如果不存在,我们则提取并存储其字幕数据。
forcourse, srt_listincourse_srt_files.items():
existing_docs = vectorstore._collection.get(where={"course": course})
ifnotexisting_docs['ids']:
srt_texts = [extract_text_from_srt(srt)forsrtinsrt_list]
course_text ="\n\n\n\n".join(srt_texts)
doc = Document(page_content=course_text, metadata={"course": course})
chunks = text_splitter.split_documents([doc])
字幕通过extract_text_from_srt()函数提取,多个字幕文件通过\n\n\n\n连接,以提高可读性。创建了一个Document对象,存储完整的字幕文本及其元数据。最后,使用text_splitter.split_documents()将文本切分为更小的块,以便高效处理和检索。
在将块添加到ChromaDB之前,我们估算成本。
chunk_count = len(chunks)
batch_tokens = chunk_count * TOKENS_PER_CHUNK_ESTIMATE
batch_cost = (batch_tokens /1000) * COST_PER_1K_TOKENS
total_tokens += batch_tokens
total_cost += batch_cost
chunk_count表示切分后的块数量。batch_tokens根据块数量估算总标记数。batch_cost计算当前课程的处理成本。total_tokens和total_cost累加每次处理的值,以跟踪整体处理量和开支。
vectorstore.add_documents(chunks)
print(f"已添加课程:{course}(块数:{chunk_count}, 成本:${batch_cost:.4f})")
处理后的块被存储到ChromaDB中,以便高效检索。程序会显示添加的块数和估算的处理成本。
如果课程已经存在,则会显示以下信息:
print(f"课程已存在:{course}")
一旦所有课程处理完成,我们计算并显示最终结果。
end_time = time.time()
print(f"\n课程嵌入更新完成!?")
print(f"总处理块数:{total_tokens // TOKENS_PER_CHUNK_ESTIMATE}")
print(f"估算总标记数:{total_tokens}")
print(f"估算总成本:${total_cost:.4f}")
print(f"总耗时:{end_time - start_time:.2f}秒")
end_time - start_time计算总处理时间。系统会显示处理的块数、估算的标记使用量、总成本以及整个嵌入过程的总结。
一旦字幕存储到ChromaDB中,系统需要一种方式来检索相关内容,以便在学员提交问题时提供答案。这个检索过程通过相似性搜索实现,它能够识别与输入问题最相关的存储文本段。
defretrieve_course_materials(query: str, course):
"""按课程名称检索课程材料"""
filter_dict = {"course": course}
results = vectorstore.similarity_search(query, k=3, filter=filter_dict)
return"\n\n".join([doc.page_contentfordocinresults])
例如:
course_name ="深度学习入门(使用PyTorch)"
question ="什么是梯度下降?"
context = retrieve_course_materials(query=question, course=course_name)
print(context)
从输出中可以看到,ChromaDB通过相似性搜索,根据课程名称和问题检索到最相关的信息。
这种机制确保学员提交问题时,能够从存储的课程材料中获得相关且上下文准确的信息。
检索到相关课程材料后,下一步是利用AI驱动的代理生成有意义的回答。我们使用CrewAI定义一个智能代理,负责分析查询并生成结构化的回答。
查询回答代理通过清晰的角色和背景故事来指导其行为,以便更好地回答学员的问题。
query_answer_agent = Agent(
role="学习支持专家",
goal="您需要为学员提供最准确的回答",
backstory="""
您是一家专注于数据科学、机器学习和生成式AI的在线教育公司学员查询解答部门的负责人。您负责回答学员关于课程内容、作业、技术问题和行政问题的咨询。您礼貌、圆滑,并且对可以改进的地方负有责任感。
""",
verbose=False
)
在代码块中,我们首先定义了代理的角色为“学习支持专家”,因为它充当虚拟助教的角色,回答学员的问题。然后,我们定义了目标,确保代理在回答时优先考虑准确性和清晰性。最后,我们将verbose设置为False,这样在不需要调试时,执行过程将保持安静。这种清晰定义的代理角色确保回答既有帮助性,又结构化,且符合教育平台的语气。
定义了代理之后,我们需要为其分配任务。
query_answering_task = Task(
description="""
尽您所能回答学员的问题。尽量保持回答简洁,不超过100个单词。
这是问题:{query}
这是从课程字幕中提取的相关内容,仅在需要时使用:{relevant_content}。
由于这些内容是从课程字幕中提取的,可能存在拼写错误,请在回答中纠正这些错误。
这是与学员之前的对话记录:{thread}。
在对话中,以“学员”开头的是学员的问题,以“支持”开头的是您的回答。请根据之前的对话适当调整您的回答。
这是学员的全名:{learner_name}。
如果不确定学员的名字,直接用“嗨”开头。
在回答的结尾添加一些适当的、鼓励性的安慰语句,例如“希望您觉得有帮助”、“希望这些信息有用。继续努力!”、“很高兴能帮到您!随时联系我。”等。
如果您不确定答案,请注明:“抱歉,我不确定这个问题的答案,我会稍后回复您。”
""",
expected_output="简洁准确的回答",
agent=query_answer_agent
)
接下来,我们来分解分配给AI的任务。处理学员的查询时,{query}代表学员的问题。回答应简洁(不超过100个单词)且准确。如果需要使用课程内容,{relevant_content}是从存储在ChromaDB中的字幕中提取的,AI必须在回答中纠正任何拼写错误。
如果存在之前的对话,{thread}有助于保持连贯性。学员的问题以“学员”开头,而之前的回答以“支持”开头,这使得代理能够提供与上下文相关的回答。通过{learner_name}实现个性化——代理会用学员的名字称呼他们,如果不确定名字,就简单地用“嗨”开头。
为了使回答更具吸引力,AI会在结尾添加一句积极的结束语,比如“希望您觉得有帮助!”或者“随时联系我。”如果AI不确定答案,它会明确说明:“抱歉,我不确定这个问题的答案,我会稍后回复您。”这种方法确保了回答的礼貌性、清晰性和结构化,提升了学员的参与度和信任感。
现在我们已经定义了代理和任务,接下来初始化CrewAI,它能够动态处理学员的查询。
response_crew = Crew(
agents=[query_answer_agent],
tasks=[query_answering_task],
verbose=False
)
agents=[query_answer_agent]将“学习支持专家”代理添加到团队中。tasks=[query_answering_task]将查询回答任务分配给这个代理。设置verbose=False可以保持输出简洁,除非需要调试。CrewAI能够动态处理多个学员的查询,使系统具有可扩展性和高效性,能够动态处理查询。
设置好AI代理后,我们需要动态处理存储在结构化数据集中的学员查询。
以下代码处理存储在CSV文件中的学员查询,并使用AI代理生成回答。它首先加载包含学员查询、课程详情和对话线程的数据集。reply_to_query函数提取相关细节,如学员姓名、课程名称和当前查询。如果存在之前的对话,它会提取出来以提供上下文。如果查询包含图片,则会跳过。然后,它从ChromaDB中检索相关的课程材料,并将查询、相关内容和之前的对话发送给AI代理,以生成结构化的回答。
df = pd.read_csv('C:\M\Code\GAI\Learn_queries\filtered_data_top3_courses.csv')
defreply_to_query(df, index=1):
learner_name = df.iloc[index]["thread_starter"]
course_name = df.iloc[index]["course"]
ifdf.iloc[index]['number_of_replies'] >1:
thread = ast.literal_eval(df.iloc[index]["modified_thread"])
else:
thread = []
question = df.iloc[index]["current_query"]
ifdf.iloc[index]['has_image'] ==True:
return" "
context = retrieve_course_materials(query=question, course=course_name)
response_result = response_crew.kickoff(inputs={"query": question,"relevant_content": context,"thread": thread,"learner_name": learner_name})
print('Q: ', question)
print('\n')
print('A: ', response_result)
print('\n\n')
测试该函数时,我们为一个查询(index=1)执行它:
reply_to_query(df, index=1)
从输出中可以看到,它能够正常工作,仅针对一个索引生成回答。
现在,我们通过所有查询进行迭代,处理每一个查询,同时处理可能出现的错误。这确保了查询解答过程的高效自动化,能够动态处理多个学员的查询。
foriinrange(len(df)):
try:
reply_to_query(df, index=i)
except:
print("索引号出错:", i)
continue
这一步确保每个学员的查询都能被分析、结合上下文并有效回答,从而提升整体学习体验。
从输出中可以看到,回答查询的过程已经实现自动化,首先是问题,然后是回答。
为了进一步提升基于RAG的查询解答系统,我们可以考虑以下改进方向:
这个基于RAG的查询解答系统利用LangChain、ChromaDB和CrewAI,高效地实现了学员支持的自动化。它从课程字幕中提取文本,将其嵌入存储到ChromaDB中,并通过相似性搜索检索相关内容。CrewAI代理处理查询,参考之前的对话,并生成结构化的回答,确保回答的准确性和个性化。
该系统提升了可扩展性、检索效率和回答质量,使自主学习更加互动。未来的改进方向包括多模态支持、更好的检索优化和增强的回答生成。通过自动化查询解答,这个系统简化了学习支持流程,为学员提供了更快、更具上下文意识的回答,提升了整体参与度。
| 欢迎光临 链载Ai (https://www.lianzai.com/) | Powered by Discuz! X3.5 |