因为上一篇写超字数了。
所以,请先阅读上一篇,本篇算彩蛋
只能加个番外篇进行补充了。
haystack是如何将管道结构做到如此优雅的?因为他们的base_class设计师将类继承,多模态,动态方法,静态方法,玩得炉火纯青。
说人话就是:代码整体结构设计者,是个资深的厉害家伙~ 他们的项目,是在动工之前,在设计层面就把所有可能性,扩展性,细节都考虑周全之后,由整体代码设计主管,或者某些大神,设计基础功能组件,每个功能基础组件都继承base组件,进行开发
haystack框架代码思路
haystack的管道思维的代码非常优雅。将大部分功能封装,然后做成功能组件。同时haystack也支持开发人员自定义功能处理组件,比如做一个文本翻译器,语言分类器什么的。为了实现这些自定义的管道组件,并加入进haystack框架,haystack提供了一个模板代码,只需要照着模板代码,即可开发兼容haystack管道的组件。
在仔细研究研了一下haystack的管道。我发现他们的代码架构师对于类继承封装用得炉火纯青。
类,对象
面向对象编程,是很常见的编程思路。你的老师会教过你“new”一个对象,虽然你也依旧是个光棍儿,但是在代码的世界,你已经有了多个“对象”。
以LOL为例,不懂编程的人眼里,是一个一个英雄小兵,草丛河道,野怪,大龙小龙。但是在优秀的开发者眼里,他们是一个一个对象。 程序框架设计师,会标定基础类,base类,这个类对所有继承自base类实例化的对象做了严格的限制,必须提供哪些功能,必须存在哪些变量,有的是必须实例化才可以使用,有的可以不用实例化对象直接在类里面引用。
因此,当我们是LOL的开发人员,对LOL里的英雄标定base类,首先是所有英雄必须存在的共同点。hero_base类包含英雄的共同的功能(不讨论动画什么的),移动,普攻(区分远程近战),一个被动,三个基础技能,一个大招(慧这种只需要增加翻页的功能)。为了实现英雄进入草丛会使敌方看不见,在实例化草丛的时候,标定范围大小,英雄所在的地图坐标,如果进入了草丛所在的地图坐标,则敌方视野消失。 那么当需要开发一个新的英雄,将基础英雄类继承过来,在技能这个function里面重写技能的功能。
haystack框架代码分析
上面的例子如果理解了。 那么回到haystack,标定基础类的一定是个很厉害的人,把所有可能出现的情况都考虑到,然后标定base类。根据开发过程随时修正base类。我们看到的每个功能组件都有一个base类。比如,document_store基础类,如果chroma继承这个基础类,就做成了chroma_document_store,faiss继承这个类,就变成了faiss_document_store,而base_document_store类,统计并约束了所有document_store的功能,如增删改查function等。
同时,由于一个类是可以传入对象的。因此Document对象约束了所有数据的json格式,包括必须包含的字段。将Document对象作为参数传入document_store类,在document_store类的内部执行Document对象的分析,调用writte写入等功能。
这种编程实在是优雅:
非常值得学习的代码设计思路!!!
一个代码结构设计师的工作,需要考虑很多可能性的出现,并标定base类,但是标定好的base类,可以让开发人员围绕base类进行开发,统一代码结构,格式,只需要修改功能部分。 因此超强的扩展性,以及工整程度!
非常值得学习的项目代码,极致的优雅,极致的封装复杂部分,把功能封装成一行命令也很值得学习。
于是,本厮尝试了一下封装huggingface。
首先我创建了一个base_LLM的类:
import torch
import requests
from abc import ABC, abstractmethod
from typing import Union
class BaseModel(ABC):
def __init__(self, model_path: str = None, server_url: str = None, mode: str = "offline"):
"""
初始化大模型类
:param model_path: 离线模式下模型文件的路径
:param server_url: 在线模式下服务的URL
:param mode: 模式选择,"offline"或"online"
"""
self.mode = mode
if mode == "offline":
if not model_path:
raise ValueError("离线模式需要提供模型路径")
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model_path = model_path
# 解包返回值并赋值
model_and_tokenizer = self.load_model(model_path)
self.model = model_and_tokenizer[0]
self.tokenizer = model_and_tokenizer[1]
elif mode == "online":
if not server_url:
raise ValueError("在线模式需要提供服务URL")
self.server_url = server_url
else:
raise ValueError("模式应为 'offline' 或 'online'")
@abstractmethod
def load_model(self, model_path: str):
"""
加载模型到显卡(需要子类实现)
:param model_path: 模型路径
"""
pass
@abstractmethod
def _generate_offline(self, user_input: str) -> str:
"""
离线模式生成回复(需要子类实现)
:param user_input: 用户输入
:return: 回复内容
"""
pass
def _generate_online(self, user_input: str) -> dict:
"""
在线模式发送请求获取回复
:param user_input: 用户输入
:return: 回复内容
"""
print("在线模式运行中...")
payload = {"input": user_input}
response = requests.post(self.server_url, json=payload)
if response.status_code == 200:
return response.json()
else:
response.raise_for_status()
def generate(self, user_input: str) -> Union[str, dict]:
"""
生成模型回复
:param user_input: 用户输入的内容
:return: 模型的回复
"""
if self.mode == "offline":
if not hasattr(self, 'model'):
raise RuntimeError("模型未加载,请检查模型路径或模式选择")
return self._generate_offline(user_input)
elif self.mode == "online":
if not hasattr(self, 'server_url'):
raise RuntimeError("服务URL未设置")
return self._generate_online(user_input)
在这个类中,我标定模型是离线部署,还是部署成一个服务的在线部署,可以通过参数进行自定义。
如果是离线部署,则需要提供模型的路径,在线部署,需要传入请求的链接。
接下来,我要创建一个Qwen的LLM类,继承自这个基础类。
我需要这个Qwen离线部署,则继承后,覆盖写入load_model功能,加载模型,以huggingface进行加载。
再覆写generate功能,调用已加载的模型进行输出。
同时将其设置为私有属性,不暴露细节,并且只有实例化了这个Qwen的类,才可以使用这个方法。进行生成。
class QwenLLM(BaseModel):
def load_model(self, model_path: str):
"""
自定义加载 Qwen 模型和 tokenizer
"""
print(f"加载 Qwen 模型: {model_path}")
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_path)
print("模型加载完成!")
return model, tokenizer
def _generate_offline(self, user_input: str) -> str:
"""
自定义 Qwen 的离线模式生成逻辑
"""
print("离线模式运行中...")
# 添加提示词模板
messages = [
{"role": "system", "content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."},
{"role": "user", "content": user_input},
]
# 使用 tokenizer 的方法生成文本
text = self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
model_inputs = self.tokenizer([text], return_tensors="pt").to(self.model.device)
# 模型生成回复
generated_ids = self.model.generate(
**model_inputs,
max_new_tokens=512,
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
return response
接下来再开一个脚本,测试一下加载刚刚的Qwen类,进行文本生成。
只需要下面的短短几行~~~~~
from Qwen import QwenLLM # 从 qwen_model 模块中导入 QwenLLM
if __name__ == '__main__':
# 设置模型路径
model_path = r"F:\MC-PROJECT\CUDA_Preject\medical_assistant\LLM\models_cache\Qwen2.5-0.5B"
# 实例化 QwenLLM 类
llm = QwenLLM(model_path=model_path, mode="offline")
# 测试输入
user_input = "能解释一下大语言模型吗?"
response = llm.generate(user_input)
print(response)
上面的Qwen代码还可以优化, 还记得python的__init__吗,在实例化对象的时候__init__会立即执行,且执行一次。
因此,加载的模型的代码部分可以写入__init__,在该类被实例化成对象的时候,立刻执行加载命令,后面等待输入内容。
好啦,所谓的彩蛋,只有这么多,希望看完的你能觉得: