继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

使用LangGraph平台构建和部署一个问答机器人助手

慕沐林林
关注TA
已关注
手记 268
粉丝 29
获赞 116

这篇博客是系列迷你博客的一部分,在这个系列中,我将创建并部署一个基于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 配置,则启动抓取过程。

自制爬虫(使用Puppeteer)

我们可以使用 [playwright](https://playwright.dev/python/) 来创建一个 Crawler 组件,它使用无头浏览器(注意:需要在 [pyproject.toml](https://github.com/esxr/retrieval-agent-template/blob/main/pyproject.toml) 中添加 playwrightrequests 作为依赖项)。

你可以通过这个链接查看该代码:在这里

注意: 仅仅安装 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 的网页爬取工具

对于更简单的场景,我们也可以使用一个基于 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"}  
    # ... 现有代码保持不变

我改变了 saveHtmlhtmlTransformer 参数的默认值,因为 OpenAI 的嵌入模型对 HTML 的理解很好。因此,HTML 转换器执行的清理会丢失一些有用的信息。

处理特殊情况
细粒度检索的文档拆分方法

迄今为止,我们直接嵌入了爬取的文档。爬取的文档可能非常大,甚至没有限制。

  1. 这让我们在生成响应时不确定应该考虑多少个 top_k 检索结果。
  2. 对于复杂的查询,生成高质量的回答可能需要从多个文档中获取信息,限制 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云上。这个过程很简单。

  1. LangSmith上创建一个Plus账户。这是必需的。免费开发者账户不允许部署到LangGraph云。这每月需要39美元。
  2. 现在从LangSmith左侧导航栏,进入部署 > LangGraph平台。在这里我们可以启动一个新的部署。
  3. 我的langGraph仓库可以从GitHub导入!并添加环境配置(如.env文件中的配置)。
  4. 然后提交部署即可!这一步需要大约15分钟。一旦部署完成,就可以点击LangGraph Studio进入工作室测试图谱了!
参考资料

这篇博客由我和 Praneet Dhoolia 共同完成 (https://praneetdhoolia.github.io)

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP