Java小子视角下的LangChain的源码解读

Langchain-Chatchat源码模型加载中、可分为在线模型和离线模型的加载。
本次源码解读主要针对在线模型的、离线模型部署加载后续工作中使用到了再更新

Langchain-chatchat源码解读:将模型启动、聊天对话和知识库对话做了较为详细的源码解读。
在你看过一次后相信会有一个整体的理解。
但是 Langchain-Chatchat整个项目会涉及到跟多的知识点、如 Langchain-ChatchatFastApiFastchat、甚至一些比较高级的python语法、因此需要所有的知识点面面俱到是不现实的。
但为了方便能对项目的源码解读、我会对使用到的关键知识进行补充、虽然不多、但都是一些使用频率高且核心的知识。

来看一下大致的框架:
在这里插入图片描述

1. 模型启动

注:由于没有本地大模型、因此启动的配置都是选择在线模型(大语言模型和Embedding)

  1. config目录下的配置复制一份、去掉后缀example
  2. 修改model_config.py配置

API是智谱AI API、具体注册及api key获取请前往 http://open.bigmodel.cn

LLM_MODELS = ["qwen-api","zhipu-api", "openai-api"]
ONLINE_LLM_MODEL = {
    "openai-api": {
        "model_name": "gpt-3.5-turbo",
        "api_base_url": "https://api.openai.com/v1",
        "api_key": "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",
        "openai_proxy": "",
    },


    "zhipu-api": {
        "api_key": "db3865ed46f05c5d2e92abc7dbe65d0d.yeGFgIna6alvZrpS",
        "version": "glm-4",
        "provider": "ChatGLMWorker",
    },
    "qwen-api": {
        "version": "qwen-max",
        "api_key": "sk",
        "provider": "QwenWorker",
        "embed_model": "text-embedding-v1" 
    }
}

选用的 Embedding 名称

embedding 模型名称

embed_model = "qwen-api"

安装依赖

pip install -r requirements.txt

启动

python startup.py -a --lite

2.模型启动脚本

langchain-chatchat 启动脚本为:startup.py

2.1 启动入口

if __name__ == "__main__":
    create_tables()
    if sys.version_info < (3, 10):
        loop = asyncio.get_event_loop()
    else:
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()

        asyncio.set_event_loop(loop)

    loop.run_until_complete(start_main_server())

事件循环(Event Loop)是异步编程的核心概念、它允许程序在等待异步操作(如 I/O、网络请求等)的结果时继续执行其他任务。
在 Python 的 asyncio 库中、事件循环负责管理和执行异步任务、确保程序的高效运行。
具体来说,事件循环做以下几件事情:

  1. 维护一个或多个任务队列。
  2. 执行当前队列中的任务。
  3. 在任务等待外部事件(如数据到达)时暂停执行。
  4. 当外部事件触发(如数据到达)时、将相关任务放回队列继续执行。
    asyncio 中、事件循环是通过 loop 对象来实现的。
    你可以创建事件循环、运行和停止它、以及管理其内部的异步任务。这使得你可以在单个线程内并发地执行多个任务、而不需要多线程、这在 I/O 密集型应用中非常有用

补充一下操作系统IO密集型的知识及面试题:

1. 谈谈你对 I/O 多路复用的理解(重要)

分析
要说出没有 I/O 多路复用时、如果服务端要支持多个客户端 I/O 存在什么问题、然后再说出 I/O 多路复用是怎么解决的。

参考面试回答

I/O 多路复用是一种允许单个线程(或进程)同时监听多个文件描述符(通常是 socket)的技术。 它解决了传统阻塞 I/O 模型中、单个线程只能处理一个连接的瓶颈问题、从而提高了服务器的并发处理能力。

  • 如果不使用 I/O 多路复用、服务端要并发处理多个客户端的 I/O
    事件的话、需要通过创建子进程或者线程的方式来实现、也就是针对每一个连接的 I/O
    事件要需要一个子进程或者线程来处理、但是随着客户端越来越多、意味着服务端需要创建更多的子进程或者线、这样对系统的开销太大了。

那么有了 I/O 多路复用就可以解决这个问题:

  • I/O 多路复用可以实现是多个 I/O 复用一个进程、也就是只需要一个进程就能监听多个文件描述符、并发处理多个客户端的 I/O 事件
    进程可以通过通过操作系统提供的系统调用来实现 例如select、poll、epoll 这类 I/O
    多路复用系统调用接口从内核中获取有事件发生的 socket 集合、当任何一个被监听的文件描述符准备好进行 I/O
    操作(例如有数据可读、可以写入数据)时、I/O 多路复用机制会通知应用程序、对每一个 socket
    事件进行处理。避免了为每个连接创建线程/进程的开销。

  • 总结: I/O 多路复用的核心思想是将多个 I/O操作的阻塞等待交给操作系统内核来处理。 应用程序只需要关注哪些 socket 连接上有事件发生、而不需要阻塞等待每个连接。Redis 单线程也能做到高性能的原因、也跟 I/O 多路复用有关系。

2. select、poll、epoll 有什么区别?(重要)

分析

  • select:调用开销大(需要复制集合)、集合大小有限制、需要遍历整个集合找到就绪的描述符
  • poll:poll 采用数组的方式存储文件描述符、没有最大存储数量的限制、其他方面和 select 没有区别、调用开销大(也需要复制集合)去
  • epoll:调用开销小(不需要复制)、集合大小无限制、采用回调机制、不需要遍历整个集合
    selectpoll 都是在用户态维护文件描述符集合、因此每次需要将完整集合传给内核
    epoll 由操作系统在内核态中维护文件描述符集合、因此只需要在创建的时候传入文件描述符。

回答
selectpoll 内部都使用线性结构(例如数组或链表)来存储进程所关注的 Socket 集合。首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态。 内核会遍历这个 Socket 集合、检查每个 Socket 上是否有事件发生。 如果某个 Socket 上有事件(例如可读或可写)、内核会标记该 Socket 的状态。 最后内核会将整个 Socket 集合从内核态拷贝回用户态、应用程序需要再次遍历这个集合、找到被标记为可读或可写的 Socket、并进行相应的处理。

很明显发现selectpoll的缺陷在于、当客户端越多、也就是 Socket 集合越大、Socket 集合的遍历和拷贝会带来很大的开销、epoll 通过两个方面解决了 select/poll 的问题。

  • epoll 在内核里使用【红黑树】来关注进程所有待检测的 Socket、红黑树是个高效的数据结构、增删改一般时间复杂度是 O(logn)、通过对这棵黑红树的管理、不需要像 select/poll 在每次操作时都传入整个 Socket 集合、只需要在添加或删除 Socket 时更新红黑树、避免了重复的数据拷贝。减少了内核和用户空间大量的数据拷贝和内存分配。
  • epoll 采用事件驱动的方式。 内核维护一个就绪链表、只记录发生事件(例如可读或可写)的 Socket。 当有事件发生时、内核会将相应的 Socket 添加到就绪链表中。 应用程序只需要从就绪链表中获取事件,而无需像 select 和 poll 那样遍历整个 Socket 集合。 这大大提高了检测效率,尤其是在只有少数 Socket 准备就绪的情况下。

ok让我们继续分析源码:

loop.run_until_complete(main())asyncio.run(main()) 都用于运行协程,但它们之间有一些区别:

  1. loop.run_until_complete(main()):
    ○ 这是一个低级别的 API,通常用于需要更精细控制事件循环行为的情况。
    ○ 它会阻塞当前线程,直到传递给它的协程完成。
    ○ 你可以多次调用 run_until_complete 来运行不同的协程、但它们会共享同一个事件循环。
  2. asyncio.run(main()):
    ○ 这是一个高级别的 API,从 Python 3.7 开始引入。
    ○ 它会自动创建一个新的事件循环,并在协程完成后关闭它。
    ○ 你不需要显式地管理事件循环,因此更易于使用。
    ○ 这是大多数应用程序开发者的首选方式。

总之,如果你只需要运行一个协程并且不需要更多的控制,那么使用 asyncio.run(main()) 是最好的选择。如果你需要更细粒度的控制,可以使用 loop.run_until_complete(main())
让我们通过一个复杂的案例来对比 loop.run_until_complete(main())asyncio.run(main()) 的使用。了解loop.run_until_complete(main())为什么能更精准的控制。

假设我们有一个网络应用、需要启动一个服务器、同时还要定期检查服务器的状态。我们希望在启动服务器后立即开始状态检查、并且在服务器运行期间定期进行检查

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值