Llama Index
有许多用例(语义搜索、摘要等),并且都有很好的记录。然而,这并不意味着我们不能将Llama Index应用到非常具体的用例中!
在本教程中,我们将介绍使用Llama Index
从文本中提取专业术语和定义的设计过程,同时允许用户稍后查询这些术语。使用Streamlit
,我们可以提供一种简单的方法来构建用于运行和测试所有这些的前端,并快速迭代我们的设计。
本教程假设您已经安装了Python3.9+
和以下软件包:
- llama-index-core
- llama-index-llms-dashscope
- llama-index-embeddings-dashscope
- streamlit
在最基础的功能设计层面上,我们的目标是从文档中获取文本,提取术语和定义,然后为用户提供查询专业术语和定义知识库的方法。本教程将介绍Llama Index
和Streamlit
的功能,并希望为出现的常见问题提供一些有用的解决方案。
上传文本
第一步是为用户提供一种手动输入文本的方法。让我们使用Streamlit
编写一些代码来提供此网页界面!使用以下代码并使用streamlit run app.py
命令启动应用程序。
import streamlit as stst.title("🦙 骆驼索引术语提取器 🦙")document_text = st.text_area("输入原始文本")
if st.button("提取术语和定义") and document_text:with st.spinner("提取中..."):extracted_terms = document_text # 这是一个临时的,下面是提取术语实现st.write(extracted_terms)
超级简单,对吧!但你会注意到这个应用程序还没有做任何有用的事情。要使用llama_index
,我们还需要设置DashScope LLM
。LLM
有很多可能的设置,所以我们可以让用户自己决定哪种设置最好。我们也应该允许用户设置用于提取术语的提示词(这也有助于我们调试出最有效的设置)。
LLM 设置
下一步给我们的应用引入了一些标签页,以将其分为提供不同功能的不同面板。让我们创建一个标签页用于 LLM 设置,另一个用于上传文本:
import streamlit as stDEFAULT_TERM_STR = ("列出在上下文中定义的术语和定义的列表, ""每一行上有一对。 ""如果一个术语缺少它的定义,用你最好的判断。""按如下方式书写每一行:\n术语: <term> 定义: <definition>"
)st.title("🦙 骆驼索引术语提取器 🦙")setup_tab, upload_tab = st.tabs(["设置", "上传/提取术语"])with setup_tab:st.subheader("LLM 设置")api_key = st.text_input("在这里输入OpenAI API密钥", type="password")llm_name = st.selectbox("选择LLM模型?", ["qwen-turbo", "qwen-plus"])model_temperature = st.slider("LLM Temperature", min_value=0.0, max_value=1.0, step=0.1)term_extract_str = st.text_area("提取术语和定义的查询。",value=DEFAULT_TERM_STR,)with upload_tab:st.subheader("提取和查询定义")document_text = st.text_area("输入原始文本")if st.button("提取术语和定义") and document_text:with st.spinner("提取中..."):extracted_terms = document_text #默认提示符 临时定义,后面实现st.write(extracted_terms)
现在我们的应用有了两个标签页,这真的有助于组织应用的功能。你还会注意到我添加了一个默认的提示词用于提取术语——你可以稍后在尝试提取一些术语后更改它,这只是我在经过一些实验后得出的一个提示词。
说到提取术语,是时候添加一些函数来实现这一功能了!
提取和存储术语
现在我们已经能够定义LLM
设置和输入文本,我们可以尝试使用Llama Index
从文本中提取专业术语!
我们可以添加以下函数来初始化LLM
,并使用它从输入文本中提取术语。
import osimport streamlit as st
from llama_index.core import Document, SummaryIndex
from llama_index.llms.dashscope import DashScopeDEFAULT_TERM_STR = ("列出在上下文中定义的术语和定义的列表, ""每一行上有一对。 ""如果一个术语缺少它的定义,用你最好的判断。""按如下方式书写每一行:\nTerm: <term> Definition: <definition>"
)def get_llm(llm_name, model_temperature, api_key, max_tokens=256):os.environ["DASHSCOPE_API_KEY"] = api_keyreturn DashScope(emperature=model_temperature,model_name=llm_name, api_key=api_key, max_tokens=max_tokens)def extract_terms(documents, term_extract_str, llm_name, model_temperature, api_key
):llm = get_llm(llm_name, model_temperature, api_key, max_tokens=1024)temp_index = SummaryIndex.from_documents(documents,)query_engine = temp_index.as_query_engine(response_mode="tree_summarize", llm=llm)terms_definitions = str(query_engine.query(term_extract_str))print(terms_definitions)terms_definitions = [xfor x in terms_definitions.split("\n")if x and "Term:" in x and "Definition:" in x]# 将文本解析为字典terms_to_definition = {x.split("Definition:")[0].split("Term:")[-1].strip(): x.split("Definition:")[-1].strip()for x in terms_definitions}return terms_to_definitionst.title("🦙 骆驼索引术语提取器 🦙")setup_tab, upload_tab = st.tabs(["设置", "上传/提取术语"])with setup_tab:st.subheader("LLM 设置")api_key = st.text_input("在这里输入OpenAI API密钥", type="password")llm_name = st.selectbox("选择LLM模型?", ["qwen-turbo", "qwen-plus"])model_temperature = st.slider("LLM Temperature", min_value=0.0, max_value=1.0, step=0.1)term_extract_str = st.text_area("提取术语和定义的查询。",value=DEFAULT_TERM_STR,)with upload_tab:st.subheader("提取和查询定义")document_text = st.text_area("输入原始文本")if st.button("提取术语和定义") and document_text:with st.spinner("提取中..."):extracted_terms = extract_terms([Document(text=document_text)],term_extract_str,llm_name,model_temperature,api_key,)st.write(extracted_terms)
代码发生了很多变动,让我们花点时间来回顾一下代码发生的改变。
get_llm()
基于setup
选项卡中的用户配置实例化LLM
。基于模型名称,我们需要使用适当的类(DashScope
)。
extract_terms()
函数是所有核心操作发生的地方。首先,我们使用max_tokens=1024
调用get_llm()
,因为我们不想在提取术语和定义时过多地限制模型(如果没有设置,默认值是256)。然后,我们定义Settings
对象,将num_output
与max_tokens
值对齐,并将块大小设置为不大于输出。当文档被Llama Index
索引时,如果文档很大,它们会被分解成块(也称为节点),chunk_size
设置这些块的大小。
接下来,我们创建一个临时摘要索引并传入我们的llm
。摘要索引将读取索引中的每一条文本,这对于提取术语来说是完美的。最后,使用response_mode="tree_summarize "
使用预定义的查询文本提取术语。此响应模式将自底向上生成一个摘要树,其中每个父节点总结其子节点。最后,返回树的顶部,它将包含我们提取的所有术语和定义。
最后,我们做一些次要的后期处理。我们假设模型遵循了说明,并在每行上放置了一个术语/定义对。如果一行缺少Term
:或Definition
:标签,我们跳过它。然后,我们将其转换为便于存储的字典!
保存提取的术语
现在我们可以提取术语了,我们需要将它们放在某个地方,以便以后查询它们。VectorStoreIndex
现在应该是一个完美的选择!但除此之外,我们的应用程序还应该跟踪哪些项被插入到索引中,以便我们稍后可以检查它们。使用st.session_state
,我们可以将当前的术语列表存储在会话字典中,每个用户都是唯一的!
首先,让我们添加一个特性来初始化全局向量索引,并添加另一个函数来插入提取的术语。
import osimport streamlit as st
from llama_index.core import Document, SummaryIndex, Settings, VectorStoreIndex
from llama_index.embeddings.dashscope import DashScopeEmbedding, DashScopeTextEmbeddingModels, \DashScopeTextEmbeddingType
from llama_index.llms.dashscope import DashScopeDEFAULT_TERM_STR = ("列出在上下文中定义的术语和定义的列表, ""每一行上有一对。 ""如果一个术语缺少它的定义,用你最好的判断。""按如下方式书写每一行:\nTerm: <term> Definition: <definition>"
)if "all_terms" not in st.session_state:st.session_state["all_terms"] = {}def insert_terms(terms_to_definition):for term, definition in terms_to_definition.items():doc = Document(text=f"Term: {term}\nDefinition: {definition}")st.session_state["llama_index"].insert(doc)@st.cache_resource
def initialize_index(api_key):Settings.embed_model = DashScopeEmbedding(api_key=api_key,model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2,text_type=DashScopeTextEmbeddingType.TEXT_TYPE_DOCUMENT,)return VectorStoreIndex([])def get_llm(llm_name, model_temperature, api_key, max_tokens=256):return DashScope(emperature=model_temperature, model_name=llm_name, api_key=api_key, max_tokens=max_tokens)def extract_terms(documents, term_extract_str, llm_name, model_temperature, api_key
):llm = get_llm(llm_name, model_temperature, api_key, max_tokens=1024)temp_index = SummaryIndex.from_documents(documents,)query_engine = temp_index.as_query_engine(response_mode="tree_summarize", llm=llm)terms_definitions = str(query_engine.query(term_extract_str))print(terms_definitions)terms_definitions = [xfor x in terms_definitions.split("\n")if x and "Term:" in x and "Definition:" in x]# 将文本解析为字典terms_to_definition = {x.split("Definition:")[0].split("Term:")[-1].strip(): x.split("Definition:")[-1].strip()for x in terms_definitions}return terms_to_definitionst.title("🦙 骆驼索引术语提取器 🦙")setup_tab, upload_tab = st.tabs(["设置", "上传/提取术语"])with setup_tab:st.subheader("LLM 设置")api_key = st.text_input("在这里输入OpenAI API密钥", type="password")llm_name = st.selectbox("选择LLM模型?", ["qwen-turbo", "qwen-plus"])model_temperature = st.slider("LLM Temperature", min_value=0.0, max_value=1.0, step=0.1)term_extract_str = st.text_area("提取术语和定义的查询。",value=DEFAULT_TERM_STR,)with upload_tab:st.subheader("提取和查询定义")if st.button("初始化索引和重置术语"):st.session_state["llama_index"] = initialize_index(api_key)st.session_state["all_terms"] = {}if "llama_index" in st.session_state:st.markdown("请在下面输入要提取术语的文本。")document_text = st.text_area("输入原始文本")if st.button("提取术语和定义") and document_text:st.session_state["terms"] = {}terms_docs = {}with st.spinner("提取中..."):terms_docs.update(extract_terms([Document(text=document_text)],term_extract_str,llm_name,model_temperature,api_key,))st.session_state["terms"].update(terms_docs)if "terms" in st.session_state and st.session_state["terms"]:st.markdown("提取术语")st.json(st.session_state["terms"])if st.button("是否插入术语?"):with st.spinner("正在插入术语"):insert_terms(st.session_state["terms"])st.session_state["all_terms"].update(st.session_state["terms"])st.session_state["terms"] = {}st.rerun()
现在你真的开始使用Streamlit
的强大功能了!让我们从upload
上传标签下的代码开始。我们添加了一个按钮来初始化向量索引,并将其存储在全局的Streamlit
状态字典中,同时重置当前提取的术语。然后,在从输入文本中提取术语之后,我们将提取的术语再次存储到全局状态中,并给用户一个机会来审查这些术语。如果按下插入按钮,那么我们将调用我们的插入术语函数,更新我们对已插入术语的全局跟踪,并从会话状态中移除最近提取的术语。
查询提取的术语/定义
提取并保存了术语和定义后,我们如何使用它们?用户如何记住之前保存的内容?我们可以简单地在应用程序中添加一些选项卡来处理这些功能。
import streamlit as st
from llama_index.core import Document, SummaryIndex, Settings, VectorStoreIndex
from llama_index.embeddings.dashscope import DashScopeEmbedding, DashScopeTextEmbeddingModels, \DashScopeTextEmbeddingType
from llama_index.llms.dashscope import DashScope, DashScopeGenerationModelsDEFAULT_TERM_STR = ("列出在上下文中定义的术语和定义的列表, ""每一行上有一对。 ""如果一个术语缺少它的定义,用你最好的判断。""按如下方式书写每一行:\nTerm: <term> Definition: <definition>"
)if "all_terms" not in st.session_state:st.session_state["all_terms"] = {}def insert_terms(terms_to_definition):for term, definition in terms_to_definition.items():doc = Document(text=f"Term: {term}\nDefinition: {definition}")st.session_state["llama_index"].insert(doc)def initialize_index(api_key: str):print('api_key', api_key)Settings.llm = DashScope(model_name=DashScopeGenerationModels.QWEN_TURBO, api_key=api_key, max_tokens=512)Settings.embed_model = DashScopeEmbedding(api_key=api_key,model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2,text_type=DashScopeTextEmbeddingType.TEXT_TYPE_DOCUMENT,)return VectorStoreIndex([])def get_llm(llm_name, model_temperature, api_key, max_tokens=256):return DashScope(emperature=model_temperature, model_name=llm_name, api_key=api_key, max_tokens=max_tokens)def extract_terms(documents, term_extract_str, llm_name, model_temperature, api_key
):llm = get_llm(llm_name, model_temperature, api_key, max_tokens=1024)temp_index = SummaryIndex.from_documents(documents,)query_engine = temp_index.as_query_engine(response_mode="tree_summarize", llm=llm)terms_definitions = str(query_engine.query(term_extract_str))print(terms_definitions)terms_definitions = [xfor x in terms_definitions.split("\n")if x and "Term:" in x and "Definition:" in x]# 将文本解析为字典terms_to_definition = {x.split("Definition:")[0].split("Term:")[-1].strip(): x.split("Definition:")[-1].strip()for x in terms_definitions}return terms_to_definitionst.title("🦙 骆驼索引术语提取器 🦙")setup_tab, terms_tab, upload_tab, query_tab = st.tabs(["设置", "所有术语", "上传/提取术语", "查询术语"]
)with setup_tab:st.subheader("LLM 设置")api_key = st.text_input("在这里输入OpenAI API密钥", type="password")llm_name = st.selectbox("选择LLM模型?", ["qwen-turbo", "qwen-plus"])model_temperature = st.slider("LLM Temperature", min_value=0.0, max_value=1.0, step=0.1)term_extract_str = st.text_area("提取术语和定义的查询。",value=DEFAULT_TERM_STR,)with upload_tab:st.subheader("提取和查询定义")if st.button("初始化索引和重置术语"):st.session_state["llama_index"] = initialize_index(api_key)st.session_state["all_terms"] = {}if "llama_index" in st.session_state:st.markdown("请在下面输入要提取术语的文本。")document_text = st.text_area("输入原始文本")if st.button("提取术语和定义") and document_text:st.session_state["terms"] = {}terms_docs = {}with st.spinner("提取中..."):terms_docs.update(extract_terms([Document(text=document_text)],term_extract_str,llm_name,model_temperature,api_key,))st.session_state["terms"].update(terms_docs)if "terms" in st.session_state and st.session_state["terms"]:st.markdown("提取术语")st.json(st.session_state["terms"])if st.button("是否插入术语?"):with st.spinner("正在插入术语"):insert_terms(st.session_state["terms"])st.session_state["all_terms"].update(st.session_state["terms"])st.session_state["terms"] = {}st.rerun()with terms_tab:with terms_tab:st.subheader("当前提取的术语和定义")st.json(st.session_state["all_terms"])with query_tab:st.subheader("查询术语/定义!")st.markdown(("LLM将尝试回答您的查询,并使用您插入的术语/定义来扩展它的答案。 ""如果一个项不在索引中,它将使用它的内部知识来回答。"))if st.button("初始化索引和重置术语", key="init_index_2"):st.session_state["llama_index"] = initialize_index(api_key)st.session_state["all_terms"] = {}if "llama_index" in st.session_state:query_text = st.text_input("询问一个术语或定义:")if query_text:query_text = (query_text+ "\n如果你找不到答案,那就尽你所能去回答这个问题。")with st.spinner("生成回答..."):response = (st.session_state["llama_index"].as_query_engine(similarity_top_k=5,response_mode="compact",).query(query_text))st.markdown(str(response))
虽然这大部分是基础性的,但有些重要的点需要注意:
- 我们的初始化按钮与其它按钮有相同的文本。
Streamlit
会对此提出警告,因此我们提供了一个唯一的键来解决这个问题。 - 查询中添加了一些额外的文本!这是为了尝试弥补索引没有答案的情况。
- 在我们的索引查询中,我们指定了两个选项:
similarity_top_k=5
表示索引将获取与查询最接近的前5个匹配的术语/定义。response_mode="compact"
表示尽可能多的文本将从5个匹配的术语/定义中用于每次LLM调用。如果没有这个设置,索引会对LLM进行至少5次调用,这可能会减慢用户的使用速度。
测试运行
好吧,实际上我希望我们在进行的过程中一直在测试。但现在,让我们尝试一次完整的测试。
- 刷新应用程序
- 输入您的LLM设置
- 转到查询选项卡
- 询问以下问题:什么是兔抱(bunnyhug)?
- 应用程序应该给出一些无意义的回答。如果你不知道的话,兔抱(bunnyhug)是加拿大草原地区人们用来指代连帽衫的另一种说法!
6.让我们把这个定义添加到应用程序中。打开上传标签并输入以下文本:兔抱(bunnyhug)是用来描述连帽衫的一个常见术语。这个术语由加拿大草原地区的人们使用。 - 单击提取按钮。几分钟后,应用程序应该显示正确提取的术语/定义。单击插入术语按钮保存它!
- 如果我们打开术语选项卡,应该会显示我们刚刚提取的术语和定义。
- 回到查询选项卡,尝试询问兔抱是什么。现在,答案应该是正确的!