9.3 大模型交互
在本项目中,“llm”目录包含了与大型语言模型(LLM)交互的核心功能。具体而言,“llm”目录下的模块实现了对不同 LLM 服务的封装,包括 OpenAI 和 DeepSeek。这些实现提供了生成文本响应、将文本嵌入向量化、将语音转换为文本等功能。通过这些模块,项目能够灵活地与各种 LLM 服务进行交互,以满足不同的业务需求。
9.3.1 通用接口
文件base_llm.py实现了类BaseLLM,此类提供了一个通用的接口,子类可以根据不同的实现需求,如不同的语言模型(例如 OpenAI GPT),来扩展这些方法。此外,类BaseResult是一个用于表示生成结果的类,提供了对结果的内容和其他相关信息的封装。
import json
from abc import ABC, abstractmethod
from langchain_core.embeddings import Embeddings
from langchain_core.language_models import BaseChatModel
from pydantic import BaseModel
from app.pkg.config import Config
ROLE_USER = "user"
ROLE_SYSTEM = "system"
ROLE_ASSISTANT = "assistant"
class BaseResult(ABC):
def __init__(self, content: str = None, usage: dict = None, **kwargs):
self.content = content
self.usage = usage
self.kwargs = kwargs
try:
self.json = json.loads(content) if content else {}
except:
self.json = {}
self.price = 0.0 # TODO: 添加成本计算
class BaseLLM(ABC):
def __init__(self, cfg: Config):
self.cfg = cfg
self.embeddings: Embeddings | None = None
self.llm: BaseChatModel | None = None
@abstractmethod
async def invoke(self, prompt: str, instruction: str = None, response_format: dict = None, messages: list[dict] = None, **kwargs) -> dict | BaseModel:
"""
基于传入的提示生成响应。
:param prompt: 请求文本,将作为用户的文本。
:param instruction: 指令文本。
:param response_format: 返回响应的格式。
:param messages: 之前的消息列表。
:param kwargs: 其他适用于不同LLM的参数。
:return: 生成的响应。
"""
pass
@abstractmethod
async def embed_query(self, text: str, dimensions: int = None) -> tuple[list[float], float]:
"""
返回文本的向量表示。
:param text: 要进行向量化的文本。
:param dimensions: 向量的维度。
:return: 文本的向量表示。
"""
pass
@abstractmethod
async def embed_documents(self, texts: list[str], dimensions: int = None) -> tuple[list[list[float]], float]:
"""
返回文本的向量表示。
:param texts: 要进行向量化的文本列表。
:param dimensions: 向量的维度。
:return: 文本的向量表示。
"""
pass
@abstractmethod
async def speech_to_text(self, audio_path: str, is_local_file: bool = False):
"""
将语音转换为文本。
参数:
audio_path (str): 音频文件的URL或本地文件路径。
is_local_file (bool): 如果为True,audio_path将被视为本地文件路径。
返回:
str: 从音频中转录的文本。
"""
pass
9.3.2 工厂函数
文件llm_factory.py定义了一个工厂函数 create_llm(),用于根据传入的配置和模型类型创建相应的语言模型实例。函数 create_llm()根据 llm_type 参数的值(如 "openai" 或 "deepseek")来决定返回 OpenAILLM 还是 DeepSeekLLM 的实例。如果提供的 llm_type 无法识别,则抛出一个 ValueError 异常。
def create_llm(cfg: Config, llm_type: str) -> OpenAILLM | DeepSeekLLM:
llm_choice = llm_type.lower()
if llm_choice == "openai" or llm_choice == "openai_assistant":
return OpenAILLM(cfg)
elif llm_choice == "deepseek":
return DeepSeekLLM(cfg)
else:
raise ValueError(f"Неизвестная модель LLM: {llm_choice}")
9.3.3 调用 DeepSeek
文件deepseek_llm.py定义了一个名为 DeepSeekLLM 的类,继承自 BaseLLM。在初始化方法中,DeepSeekLLM 从配置中获取 DeepSeek 的 API 密钥和 URL。通过invoke 方法用于调用 DeepSeek API 生成响应,支持在消息中加入系统指令和用户输入的提示。
class DeepSeekLLM(BaseLLM):
def __init__(self, cfg: Config):
super().__init__(cfg)
# 假设 DeepSeek 使用的 API 密钥和端点
self.api_key = self.cfg.deepseek.api_key
self.api_url = self.cfg.deepseek.api_url
async def invoke(self, prompt: str, intruction: str = None, response_format: dict = None, messages: list[dict] = None, **kwargs) -> dict | BaseModel:
"""
调用 DeepSeek API 生成响应。
结合 instruction(如果提供),并在消息中加入用户输入的 prompt。
"""
# 如果没有提供消息列表,则创建一个空的
messages = messages or []
# 将 instruction 添加到消息中(如果提供)
if intruction:
messages.insert(0, {"role": "system", "content": intruction})
# 构建 DeepSeek 请求的消息体
messages.append({"role": "user", "content": prompt})
try:
async with aiohttp.ClientSession() as session:
# 发送 POST 请求到 DeepSeek API
async with session.post(
self.api_url,
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={"messages": messages}
) as response:
# 检查响应状态
if response.status != 200:
raise Exception(f"DeepSeek API 请求失败,状态码: {response.status}")
# 解析响应的 JSON 数据
response_data = await response.json()
# 这里假设响应返回的内容是包含 `response` 字段的 JSON 对象
content = response_data.get("response", "")
# 如果指定了格式化响应的格式
if response_format:
if isinstance(response_format, dict):
if response_format.get("type") == "json_object":
# 转换响应内容为 JSON 对象格式
content = json.dumps(response_format.get("json_object", {}))
elif response_format.get("type") == "json_schema":
json_schema = response_format.get("json_schema")
if isinstance(json_schema, type) and issubclass(json_schema, BaseModel):
content = json_schema.parse_obj(content)
else:
content = json_schema(content)
# 返回生成的内容
return {"content": content}
except Exception as e:
return {"error": str(e)}
9.3.4 调用OpenAI
文件openai_llm.py定义了一个名为 OpenAILLM 的类,继承自 BaseLLM,用于与 OpenAI 的 API 进行交互,提供文本生成、文本嵌入和语音转文本等功能。
class OpenAIResult(BaseResult):
"""
OpenAI 的 BaseResult 的具体实现。
"""
def __init__(self, content: str = None, usage: dict = None, **kwargs):
super().__init__(content=content, usage=usage, **kwargs)
class OpenAILLM(BaseLLM):
def __init__(self, cfg: Config):
super().__init__(cfg)
self.embeddings = OpenAIEmbeddings(model=self.cfg.openai.emb_model, dimensions=self.cfg.openai.emb_dimensions)
self.embeddings.openai_api_key = self.cfg.openai.api_key
self.llm = ChatOpenAI(model=self.cfg.openai.model, api_key=self.cfg.openai.api_key)
self.client = AsyncOpenAI(api_key=self.cfg.openai.api_key)
async def invoke(self, prompt: str, intruction: str = None, response_format: dict = None, messages: List[dict] = None, **kwargs) -> dict | BaseModel:
"""
生成响应,将 instruction(如果提供)与 prompt 结合。
"""
messages = messages or []
if intruction:
messages.insert(0, {"role": "system", "content": intruction})
llm = self.llm
if kwargs.get("max_tokens"):
llm.max_tokens = kwargs.get("max_tokens")
if kwargs.get("temperature"):
llm.temperature = kwargs.get("temperature")
if kwargs.get("top_p"):
llm.top_p = kwargs.get("top_p")
if response_format:
if isinstance(response_format, dict):
if response_format.get("type") == "json_object":
llm = llm.bind(response_format={"type": "json_object"})
messages.append({"role": "system", "content": f"请以 JSON 格式回答:\n\n```\n{json.dumps(response_format.get('json_object'))}\n```"})
elif response_format.get("type") == "json_schema":
json_schema_candidate = response_format.get("json_schema")
# 检查传入的值是否为类且继承自 BaseModel
if isinstance(json_schema_candidate, type) and issubclass(json_schema_candidate, BaseModel):
json_schema = json_schema_candidate
llm = llm.with_structured_output(json_schema)
else:
llm = llm.with_structured_output(json_schema_candidate)
messages.append({"role": "human", "content": prompt})
return await llm.ainvoke(messages)
async def embed_query(self, text: str, dimensions: int = None) -> Tuple[List[float], float]:
"""
返回文本的向量表示。
为此,我们使用 LangChain 的 OpenAIEmbeddings。
"""
self.embeddings.dimensions = dimensions
vector = await asyncio.to_thread(self.embeddings.embed_query, text)
price = 0.0 # 此处可根据需要计算费用
return vector, price
async def embed_documents(self, texts: List[str], dimensions: int = None) -> Tuple[List[List[float]], float]:
"""
返回一组文本的向量表示。
"""
self.embeddings.dimensions = dimensions
vectors = await asyncio.to_thread(self.embeddings.embed_documents, texts, chunk_size=10)
price = 0.0
return vectors, price
async def speech_to_text(self, audio_path: str, is_local_file: bool = False):
"""
使用 OpenAI Whisper 通过 LangChain 将语音转换为文本。
参数:
audio_path (str): 音频文件的 URL 或本地路径。
is_local_file (bool): 如果为 True,audio_path 被视为本地文件路径。
返回:
str: 从音频中转录的文本。
"""
temp_filename = None
try:
if not is_local_file:
# 如果是 URL,则下载音频文件
async with aiohttp.ClientSession() as session:
async with session.get(audio_path) as response:
if response.status != 200:
raise Exception(f"无法从 {audio_path} 下载音频文件")
# 创建临时文件来存储音频
with tempfile.NamedTemporaryFile(delete=False, suffix=".m4a") as temp_file:
temp_filename = temp_file.name
# 将音频内容写入临时文件
temp_file.write(await response.read())
else:
# 如果是本地文件,直接使用路径
temp_filename = audio_path
with open(temp_filename, "rb") as audio_file:
response = await self.client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
transcribed_text = response.text
return transcribed_text
finally:
# 如果创建了临时文件且不是本地文件,则删除它
if temp_filename and not is_local_file and os.path.exists(temp_filename):
os.remove(temp_filename)
async def save_file(self, file, purpose: Literal["assistants", "batch", "fine-tune", "vision"] = "assistants", file_path: str = None) -> str | None:
try:
result = await self.client.files.create(
file=file if file else open(file_path, "rb"),
purpose=purpose
)
file_id = result.id
except Exception as e:
file_id = None
return file_id
上述代码用于与 OpenAI 的 API 进行交互,该类包含以下功能:
- 初始化方法 (__init__):设置嵌入模型和语言模型,并初始化 OpenAI 客户端。
- invoke()方法:生成响应,将提示词与可选的指令结合,构建消息列表,并调用 OpenAI 的 API。
- embed_query()方法:获取文本的嵌入向量表示。
- embed_documents()方法:获取多个文本的嵌入向量表示。
- speech_to_text()方法:将音频转换为文本,使用 OpenAI 的 Whisper 模型进行转录。
- save_file()方法:将文件保存到 OpenAI 的服务器,用于助手、批处理、微调或视觉任务。
在异步编程方面,OpenAILLM 使用了库aiohttp来异步发送 HTTP 请求,以提高性能和响应速度。