基于人工智能的自然语言查询用于知识发现
应用人工智能的自然语言查询技术进行知识发现

在这篇文章中,我想分享一个我一直在进行的名为UE5_documentalist的概念验证项目。这是一个令人兴奋的项目,它使用自然语言处理(NLP)来潜在地增强您在大量文档中的体验。
虽然我在这个项目中使用了Unreal Engine 5的文档,但它可以应用于任何类型的用例,比如您公司的内部文档。
什么是UE5_documentalist?
UE5_documentalist是一个智能助手,旨在简化您在Unreal Engine 5.1或任何其他文档中的导航。通过利用NLP技术,该项目允许您进行自然语言查询,轻松找到超过1700个网页中最相关的部分。
例如,您可以查询“我可以使用什么系统来避免代理之间的碰撞?”并被重定向到最适合您需求的文档。
您可以在此存储库中查看我的代码。
演示
为了让您更好地了解UE5_documentalist的功能,这里有一个快速演示展示它的能力:

演示(图片由作者提供)
它是如何工作的?
步骤1 – 网页抓取
我首先抓取了Unreal Engine 5.1的网页文档,将HTML输出转换为Markdown,并将文本保存到一个字典中。
抓取函数如下所示:
def main(limit, urls_registry, subsections_path): urls_registry = "./src/utils/urls.txt" with open(urls_registry, 'r') as f: urls = f.read() urls = urls.split('\n') # 初始化用于存储子部分的字典 subsections = {} for idx, url in enumerate(urls): # 达到限制时停止 if limit is not None: if idx > limit: break if idx % 100 == 0: print(f"处理网页 {idx}") # 发送请求 try: with urllib.request.urlopen(url) as f: content = f.read() except HTTPError as e: print(f"网页 {url} 出错") print('服务器无法完成请求。') print('错误代码:', e.code) continue except URLError as e: print(f"网页 {url} 出错") print('无法连接服务器。') print('原因:', e.reason) continue # 解析内容 md_content = md(content.decode('utf-8')) preproc_content = split_text_into_components(md_content) # 提取URL名称信息 subsection_title = extract_info_from_url(url) # 添加到字典中 subsections[url] = { "title": subsection_title, "content": preproc_content } # 保存字典 with open(subsections_path, 'w') as f: json.dump(subsections, f)
该函数调用了另一个名为split_text_into_components的函数,它是一系列预处理步骤,删除链接、图像、加粗和标题等特殊字符。
最终,该函数返回一个如下所示的字典:
{url : {'title' : '一些上下文标题', 'content' : '页面内容' }...}
步骤2 – 嵌入向量
基于此,我使用了Instructor-XL模型生成嵌入向量。一开始,我使用了OpenAI的text-embedding-ada-002模型。它非常便宜(超过1.7亿字符不到2美元),速度快,全球适用于我的用例。然而,我切换到了Instructor-XL模型,它运行稍慢,但可以在本地使用,而无需与在线API通信。如果您关心隐私或者处理敏感数据,这个解决方案更好。
嵌入函数如下:
def embed(subsection_dict_path, embedder, security): """嵌入目录中的文件。 参数: subsection_dict_path (dict): 包含子节的字典路径。 security (str): 安全设置。可选值为“activated”或“deactivated”。 如果不是“deactivated”,则阻止函数运行 并避免意外费用。 返回: embeddings (dict): 包含嵌入内容的字典。 """ # 如果嵌入内容已经存在,则加载 if os.path.exists(os.path.join("./embeddings", f'{embedder}_embeddings.json')): print("嵌入内容已存在。正在加载。") with open(os.path.join("./embeddings", f'{embedder}_embeddings.json'), 'r') as f: embeddings = json.load(f) else: # 初始化用于存储嵌入内容的字典 embeddings = {} # 检查安全设置,如果embedder是openai(避免错误花费$$$) if security != "deactivated": if embedder == 'openai': raise Exception("安全设置未停用。") # 加载子节 with open(subsection_dict_path, 'r') as f: subsection_dict = json.load(f) # 仅供调试目的 # 计算要嵌入的平均文本长度 dict_len = len(subsection_dict) total_text_len = 0 for url, subsection in subsection_dict.items(): total_text_len += len(subsection['content']) avg_text_len = total_text_len / dict_len # 如果embedder是'openai',则初始化openai api if embedder == "openai": openai_model = "text-embedding-ada-002" # 从环境变量中获取API密钥,如果不存在则提示用户输入 api_key = os.getenv('API_KEY') if api_key is None: api_key = input("请输入您的OpenAI API密钥:") openai.api_key = api_key # 如果embedder是'instructor',则初始化instructor模型 elif embedder == "instructor": instructor_model = INSTRUCTOR('hkunlp/instructor-xl') # 如果GPU可用,则设置设备为GPU if (torch.backends.mps.is_available()) and (torch.backends.mps.is_built()): device = torch.device("mps") elif torch.cuda.is_available(): device = torch.device("cuda") else: device = torch.device("cpu") else: raise ValueError(f"Embedder必须是'openai'或'instructor'。而不是{embedder}") # 遍历子节 for url, subsection in tqdm(subsection_dict.items()): subsection_name = subsection['title'] text_to_embed = subsection['content'] # 如果已经嵌入过了,则跳过 if url in embeddings.keys(): continue # 发送嵌入请求 # 情况1:openai if embedder == 'openai': try: response = openai.Embedding.create( input=text_to_embed, model=openai_model ) embedding = response['data'][0]['embedding'] except InvalidRequestError as e: print(f"url {url} 处发生错误") print('服务器无法满足请求。') print('错误代码:', e.code) print(f'尝试嵌入 {len(text_to_embed)} 个字符,而平均值为 {avg_text_len}') continue # 情况2:instructor elif embedder == 'instructor': instruction = "代表用于检索的UnrealEngine文档:" embedding = instructor_model.encode([[instruction, text_to_embed]], device=device) embedding = [float(x) for x in embedding.squeeze().tolist()] else: raise ValueError(f"Embedder必须是'openai'或'instructor'。而不是{embedder}") # 将嵌入内容添加到字典中 embeddings[url] = { "title": subsection_name, "embedding": embedding } # 每100次迭代保存一次字典 if len(embeddings) % 100 == 0: print(f"已完成 {len(embeddings)} 次迭代后保存嵌入内容。") # 将嵌入内容保存到pickle文件 with open(os.path.join("./embeddings", f'{embedder}_embeddings.pkl'), 'wb') as f: pickle.dump(embeddings, f) # 将嵌入内容保存到json文件 with open(os.path.join("./embeddings", f'{embedder}_embeddings.json'), 'w') as f: json.dump(embeddings, f) return embeddings
该函数返回一个类似以下的字典:
{url : {'title' : '一些上下文标题', 'embedding' : 内容的向量表示 }...}
步骤3 — 向量索引数据库
然后将这些嵌入上传到在Docker容器上运行的Qdrant向量索引数据库中。
使用以下Docker命令启动数据库:
docker pull qdrant/qdrantdocker run -d -p 6333:6333 qdrant/qdrant
并使用以下函数填充数据库(您将需要qdrant_client软件包:pip install qdrant-client)
import qdrant_client as qcimport qdrant_client.http.models as qmodelsimport uuidimport jsonimport argparsefrom tqdm import tqdmclient = qc.QdrantClient(url="localhost")METRIC = qmodels.Distance.DOTCOLLECTION_NAME = "ue5_docs"def create_index(): client.recreate_collection( collection_name=COLLECTION_NAME, vectors_config = qmodels.VectorParams( size=DIMENSION, distance=METRIC, ) )def create_subsection_vector( subsection_content, section_anchor, page_url ): id = str(uuid.uuid1().int)[:32] payload = { "text": subsection_content, "url": page_url, "section_anchor": section_anchor, "block_type": 'text' } return id, payloaddef add_doc_to_index(embeddings, content_dict): ids = [] vectors = [] payloads = [] for url, content in tqdm(embeddings.items()): section_anchor = content['title'] section_vector = content['embedding'] section_content = content_dict[url]['content'] id, payload = create_subsection_vector( section_content, section_anchor, url ) ids.append(id) vectors.append(section_vector) payloads.append(payload) # Add vectors to collection client.upsert( collection_name=COLLECTION_NAME, points=qmodels.Batch( ids = [id], vectors=[section_vector], payloads=[payload] ), )
此函数将网页URL和相关内容嵌入后,将其上传到Qdrant数据库中。
您必须上传ID、矢量和有效载荷作为矢量数据库。在这里,我们的ID是程序生成的,我们的矢量是嵌入(稍后将与查询匹配),有效载荷是额外的信息。
在我们的案例中,在有效载荷中包含的主要信息是Web URL和Web页面的内容。这样,当我们将查询与Qdrant数据库中最相关的条目匹配时,我们可以打印文档并打开网页。
最后一步 – 查询
现在我们可以对数据库进行查询。
查询必须首先与文档中使用的相同的embedder进行嵌入。我们可以使用以下函数来进行嵌入:
def embed_query(query, embedder): if embedder == "openai": # 从环境变量中获取API密钥或提示用户输入 api_key = os.getenv('API_KEY') if api_key is None: api_key = input("Please enter your OpenAI API key: ") openai_model = "text-embedding-ada-002" openai.api_key = api_key response = openai.Embedding.create( input=query, model=openai_model ) embedding = response['data'][0]['embedding'] elif embedder == "instructor": instructor_model = INSTRUCTOR('hkunlp/instructor-xl') # 如果可用,将设备设置为GPU if (torch.backends.mps.is_available()) and (torch.backends.mps.is_built()): device = torch.device("mps") elif torch.cuda.is_available(): device = torch.device("cuda") else: device = torch.device("cpu") instruction = "Represent the UnrealEngine query for retrieving supporting documents:" embedding = instructor_model.encode([[instruction, query]], device=device) embedding = [float(x) for x in embedding.squeeze().tolist()] else: raise ValueError("Embedder must be 'openai' or 'instructor'") return embedding
然后使用嵌入的查询来查询Qdrant数据库:
def query_index(query, embedder, top_k=10, block_types=None): """ 查询与给定查询匹配的Qdrant向量索引数据库中的文档。 Args: query(str):要搜索的查询。 embedder(str):要使用的embedder。必须是“openai”或“instructor”。 top_k(int,可选):要返回的文档的最大数量。默认为10。 block_types(str或str列表,可选):要搜索的文档块的类型。默认为“text”。 Returns: 一个列表,表示匹配的文档,按相关性排序。每个字典包含以下键: - “id”:文档的ID。 - “score”:文档的相关性得分。 - “text”:文档的文本内容。 - “block_type”:与查询匹配的文档块的类型。 """ collection_name = get_collection_name() if not collection_exists(collection_name): raise Exception(f"Collection {collection_name} does not exist. Exisiting collections are: {list_collections()}") vector = embed_query(query, embedder) _search_params = models.SearchParams( hnsw_ef=128, exact=False ) block_types = parse_block_types(block_types) _filter = models.Filter( must=[ models.Filter( should= [ models.FieldCondition( key="block_type", match=models.MatchValue(value=bt), ) for bt in block_types ] ) ] ) results = CLIENT.search( collection_name=collection_name, query_vector=vector, query_filter=_filter, limit=top_k, with_payload=True, search_params=_search_params, ) results = [ ( f"{res.payload['url']}#{res.payload['section_anchor']}", res.payload["text"], res.score ) for res in results ] return resultsdef ue5_docs_search( query, embedder=None, top_k=10, block_types=None, score=False, open_url=True): """ 搜索与给定查询相关的文档的Qdrant向量索引数据库,并打印顶部结果。 Args: query (str): 要搜索的查询。 embedder (str): 要使用的embedder。必须是“openai”或“instructor”。 top_k (int, optional): 要返回的文档的最大数量。默认为10。 block_types (str or list of str, optional): 要搜索的文档块的类型。默认为“text”。 score (bool, optional): 是否在输出中包含相关性得分。默认为False。 open_url (bool, optional): 是否在网络浏览器中打开顶部URL。默认为True。 Returns: None """ # 检查embedder是否为'openai'或'instructor'。如果不是则引发错误 assert embedder in ['openai', 'instructor'], f"Embedder must be 'openai' or 'instructor'. Not {embedder}" results = query_index( query, embedder=embedder, top_k=top_k, block_types=block_types ) print_results(query, results, score=score) if open_url: top_url = results[0][0] webbrowser.open(top_url)
这些函数调用我仓库中的其他内部处理函数。
就是这样!查询被嵌入,匹配到最接近的文档嵌入(您可以选择评估度量),结果将在终端中打印,并自动打开相应的网页!
想自己试试吗?
如果您有兴趣探索UE5_documentalist,请参阅我在您本地的设置步骤的详细的逐步指南。嵌入已经完成,所以您可以直接开始填充Qdrant数据库,并且您将在我的GitHub仓库中找到所有必要的资源和详细的说明。
目前,它使用Python命令运行。
为什么试一下呢?
UE5_documentalist旨在简化搜索冗长的文档,可能为您节省宝贵的开发时间。通过用简单的英语提问,您将被引导到回答您问题的特定部分。这个工具可能会改善您的体验,并允许您更多地关注使用Unreal Engine 5.1构建令人惊叹的项目或文档。
截至目前,我对UE5_documentalist的进展感到自豪,并且很希望听到您的反馈和改进建议。
太长了,没看:简要介绍:
介绍UE5_documentalist – 一款由NLP驱动的智能文档助手。它使开发人员可以使用自然语言查询轻松导航Unreal Engine 5.1的文档。查看演示,并在GitHub仓库中找到设置指南,也许,如果效果好,您可能会告别繁琐的文档搜索,并潜在地提高您的知识发现体验!
链接
鸣谢
我从一个Reddit用户那里得到了简化UE5文档的想法,他用LLMs开始了一个项目(我忘记了他的名字,但这是他的存储库的链接)。
我基于这篇TDS文章(作者是Jacob Marks)。他的代码对于大部分解析和索引步骤非常有用,没有他详细的文章,我无法成功。不要犹豫,去看看他的工作!
文章最初发布于这里,获得了转载许可。





