这篇博客是系列迷你博客的一部分,在这个系列中,我将创建并部署一个基于RAG(检索增强生成)的问答聊天机器人,用于对网站内容进行问答。使用了在AGAILE开发的人工智能解决方案模板。这将介绍如何在LangGraph上设置和部署后端。
LangGraph 是开发 AI 解决方案的最流行的平台。它提供了常见 AI 解决方案模式的现成的模板。我们采用了它的检索代理模板并根据客户的具体需求进行了定制。在接下来的部分里,我会详细描述这些定制。
请参考此仓库中的代码:此仓库 以便跟进行代码。
从langgraph模板着手处理我们来安装一下 LangGraph CLI
pip install langgraph-cli --upgrade
# 更新langgraph-cli插件
我们现在来用一个模板创建一个新应用。
langgraph 新建
图谱:
图谱增强
在 [configuration.py](https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/configuration.py)
中,将 starter_urls
和 hops 添加到 IndexConfiguration
中。同时添加一个方法,用于从逗号分隔的 starter_urls
字符串中提取网址列表。
starter_urls: str = field(
default="https://zohlar.com",
metadata={
"description": "用于索引爬取的网页的逗号分隔的起始网址字符串。"
},
)
hops: int = field(
default=2,
metadata={
"description": "从起始网址链接到的网页中最大遍历跳数。"
},
)
def parse_starter_urls(self) -> list[str]:
"""将起始网址解析为列表。
返回:
list[str]: 解析自逗号分隔字符串的网址列表。
"""
return [url.strip() for url in self.starter_urls.split(",") if url.strip()]
支持开源检索器:Milvus
Milvus 是一个开源软件,它的 Lite 版本可以使用文件 URI 运行。这使得它在开发中更加经济实惠,非常适合做演示。
我们首先从将 langchain-milvus
添加为依赖项开始,在 [pyproject.toml](https://github.com/esxr/retrieval-agent-template/blob/main/pyproject.toml)
中。
接着将 milvus
添加到 retriever_provider
列表里
retriever_provider: Annotated[
Literal["elastic", "elastic-local", "pinecone", "mongodb", "milvus"],
{"__template_metadata__": {"kind": "retriever"}},
] = field(
default='milvus',
metadata={
"description": "用于检索的向量存储提供者。选项为 'elastic', 'pinecone', 'mongodb' 和 'milvus'。",
},
)
现在我们可以在 [retrieval.py](https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/retrieval.py)
里添加一个新的方法来创建一个 Milvus 检索器实例。
@contextmanager
def make_milvus_retriever(
configuration: IndexConfiguration, embedding_model: Embeddings
) -> Generator[VectorStoreRetriever, None, None]:
"""配置此代理使用Milvus Lite文件基础URI来存储向量索引。"""
from langchain_milvus.vectorstores import Milvus
vstore = Milvus(
embedding_function=embedding_model,
collection_name=configuration.user_id,
connection_args={"uri": os.environ["MILVUS_DB"]},
auto_id=True
)
yield vstore.as_retriever()
然后在工厂模式中使用它。
@contextmanager
def make_retriever(
config: RunnableConfig,
) -> Generator[VectorStoreRetriever, None, None]:
# 此处代码与之前相同
match configuration.retriever_provider:
# 此处代码与之前相同
case "milvus":
with make_milvus_retriever(configuration, embedding_model) as retriever:
yield retriever
case _:
# 其他情况如前
注: "make_retriever" 和 "make_milvus_retriever" 为函数名,无需翻译。
我们将以下文件 URI(URI)添加到 .env
用于存储向量索引:
## Milvus(米尔维斯)
MILVUS_DB=数据文件名(milvus.db)
爬虫支持
直接使用 index_graph
实现的期望作为输入的是所有需要索引的文档。因为我们正在增强该图,使其包含一个从指定 URL 开始抓取的管道,我们将修改 index_docs
节点的功能:如果状态中的文档列表为空,并且提供了 starter_urls
配置,则启动抓取过程。
我们可以使用 [playwright](https://playwright.dev/python/)
来创建一个 Crawler 组件
,它使用无头浏览器(注意:需要在 [pyproject.toml](https://github.com/esxr/retrieval-agent-template/blob/main/pyproject.toml)
中添加 playwright
和 requests
作为依赖项)。
你可以通过这个链接查看该代码:在这里。
APIfy 的网页爬取工具注意: 仅仅安装 playwright 包到 Python 环境中是不够的,还需要安装无头浏览器(headless browser)及其依赖项。因此,我们还需要运行 playwright install 和 playwright install-deps 这两个命令。
你可以使用 LangGraph Studio 应用在 Mac 上使用 Docker 部署本地图形。在这种情况下,这些命令必须在 Docker 中运行。为此,我们需要在 langgraph.json 文件中加入如下代码段:
_"dockerfile_lines": ["RUN pip install playwright", "RUN python -m playwright install", "RUN python -m playwright install-deps"]_
对于更简单的场景,我们也可以使用一个基于 Apify 的爬虫。像这样修改 indexx_graph.py
即可。
# 新导入项
import json
# 新导入的库
from langchain_community.utilities import ApifyWrapper
from langchain_community.document_loaders import ApifyDatasetLoader
# ... 现有代码保持不变
def load_site_dataset_map() -> dict:
with open("sites_dataset_map.json", 'r', encoding='utf-8') as file:
return json.load(file)
def apify_crawl(tenant: str, starter_urls: list, hops: int):
site_dataset_map = load_site_dataset_map()
if dataset_id := site_dataset_map.get(tenant):
loader = ApifyDatasetLoader(
dataset_id=dataset_id,
dataset_mapping_function=lambda item: Document(
page_content=item["html"] or "", metadata={"url": item["url"]}
),
)
else:
apify = ApifyWrapper()
loader = apify.call_actor(
actor_id="apify/website-content-crawler",
run_input={
"startUrls": starter_urls,
"saveHtml": True,
"htmlTransformer": "none"
},
dataset_mapping_function=lambda item: Document(
page_content=item["html"] or "", metadata={"url": item["url"]}
),
)
print(f"站点: {tenant} 爬取并加载到Apify数据集: {loader.dataset_id}")
return loader.load()
# ... 现有代码保持不变
# 异步定义索引文档函数
async def index_docs(
state: IndexState, *, config: Optional[RunnableConfig] = None
) -> dict[str, str]:
# ... 如之前一样
with retrieval.make_retriever(config) as retriever:
# 需要时启动爬取的代码
configuration = IndexConfiguration.from_runnable_config(config)
if not state.docs and configuration.starter_urls:
print(f"开始爬取 ...")
# 调用 apify_crawl 函数并更新 state.docs
state.docs = apify_crawl (
configuration.user_id,
[{"url": url} for url in configuration.parse_starter_urls()],
configuration.hops
)
# 确保文档包含用户ID并更新 stamped_docs
stamped_docs = ensure_docs_have_user_id(state.docs, config)
if configuration.retriever_provider == "milvus":
# 检索器添加带有用户ID的文档
retriever.add_documents(stamped_docs)
else:
# 异步添加带有用户ID的文档到检索器
await retriever.aadd_documents(stamped_docs)
# 返回删除文档的字典
return {"docs": "delete"}
# ... 现有代码保持不变
我改变了 saveHtml
和 htmlTransformer
参数的默认值,因为 OpenAI 的嵌入模型对 HTML 的理解很好。因此,HTML 转换器执行的清理会丢失一些有用的信息。
迄今为止,我们直接嵌入了爬取的文档。爬取的文档可能非常大,甚至没有限制。
- 这让我们在生成响应时不确定应该考虑多少个
top_k
检索结果。 - 对于复杂的查询,生成高质量的回答可能需要从多个文档中获取信息,限制
top_k
可能会导致较差的结果。
为解决这个问题,我们将抓取文档分成多个文档,如下所述:
# src/retrieval_graph/index_graph.py
# 新的导入
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 其他部分与之前相同
async def index_docs(
state: IndexState, *, config: Optional[RunnableConfig] = None
) -> dict[str, str]:
if not config:
raise ValueError("配置信息是运行index_docs所必须的。")
with retrieval.make_retriever(config) as retriever:
configuration = IndexConfiguration.from_runnable_config(config)
if not state.docs and (configuration.starter_urls or configuration.apify_dataset_id):
print(f"启动爬取...")
crawled_docs = apify_crawl(configuration)
# 使用一个基于1000字符大小和200字符重叠的分割器,将文档切分为更小的片段以便索引
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
state.docs = text_splitter.split_documents(crawled_docs)
# 其他部分与之前相同
我们现在可以获取更大的 top_k
结果集。由于这里的最大分块数量是 1000
,我们可以根据用于生成响应的语言模型支持的 token 限制来确定性地配置 top_k
。例如,如果 token 限制是 100,000 个 token,我们可以在这种情况下将 top_k
设置为 500
(假设每个 token 平均长度为 5 个字符)。
# src/retrieval_graph/retreival.py
# ...
def make_milvus_retriever(
configuration: IndexConfiguration, embedding_model: Embeddings, **kwargs
) -> Generator[VectorStoreRetriever, None, None]:
# 创建Milvus检索器的函数
# 参数:configuration (IndexConfiguration),embedding_model (Embeddings),**kwargs
# 返回:VectorStoreRetriever生成器
# ...
yield vstore.as_retriever(search_kwargs=configuration.search_kwargs)
# ...
这里,search_kwargs = {"k": 10}
意味着会返回前 10 个。
嵌入模型对每单位时间内令牌的数量有限制。最初,我们将抓取(或拆分)的所有文档一次性全部嵌入。对于大型网站来说,这会导致超出速率限制的错误。
通过以下增强,我们可以添加批量大小的配置选项,将文档拆分为多个批次,并添加时间延迟以避免触发速率限制,这样可以实现。
# src/retrieval_graph/configuration.py
# ...
@dataclass(kw_only=True)
class IndexConfiguration(CommonConfiguration):
# ...
batch_size: int = field(
default=400,
metadata={
"description": "每次批量处理时索引的文档数。"
},
)
# ...
# src/retrieval_graph/index_graph.py
# ...
# 生成器函数,用于创建批次
def create_batches(docs, batch_size):
"""将文档拆分成更小的批次。"""
for i in range(0, len(docs), batch_size):
yield docs[i:i + batch_size]
async def index_docs(
state: IndexState, *, config: Optional[RunnableConfig] = None
) -> dict[str, str]:
if not config:
raise ValueError("需要配置来运行 index_docs.")
with retrieval.make_retriever(config) as retriever:
configuration = IndexConfiguration.from_runnable_config(config)
# ...
stamped_docs = ensure_docs_have_user_id(state.docs, config)
# 分批次进行嵌入以避免超出速率限制错误
batch_size = configuration.batch_size
for i, batch in enumerate(create_batches(stamped_docs, batch_size)):
if configuration.retriever_provider == "milvus":
retriever.add_documents(batch)
else:
await retriever.aadd_documents(batch) # 等待检索器添加批次中的文档
# 如果还有更多的批次需要处理
if i < (len(stamped_docs) // batch_size):
time.sleep(60)
return {'docs': 'delete'}
# ...
云部署
我们现在可以把这个检索图放到langgraph云上。这个过程很简单。
- 在LangSmith上创建一个Plus账户。这是必需的。免费开发者账户不允许部署到LangGraph云。这每月需要39美元。
- 现在从LangSmith左侧导航栏,进入部署 > LangGraph平台。在这里我们可以启动一个新的部署。
- 我的langGraph仓库可以从GitHub导入!并添加环境配置(如
.env
文件中的配置)。 - 然后提交部署即可!这一步需要大约15分钟。一旦部署完成,就可以点击LangGraph Studio进入工作室测试图谱了!
这篇博客由我和 Praneet Dhoolia 共同完成 (https://praneetdhoolia.github.io)
共同学习,写下你的评论
评论加载中...
作者其他优质文章