一、LLMOps后端搭建,构建基础聊天机器人

0 阅读7分钟

1. python项目目录

image.png

2. Postgres数据库安装

windows下 get.enterprisedb.com/postgresql image.png 下载地址:www.enterprisedb.com/downloads/p…

默认安装的 PostgreSQL 会开机自启,可以通过以下步骤关闭开机自启:
1. 按下 win+r 打开运行对话框,输入 services.msc 并回车。
2. 找到 postgres-x64-16,右击选择 属性,将启动方式修改为 手动。
3. 可以右击选择 停止,关闭 postgres 服务。
Windows 下的启动与停止命令
pg_ctl start -D "D:\Software\PostgreSQL\16\data"
pg_ctl stop -D "D:\Software\PostgreSQL\16\data"

image.png

3. pycharm使用数据库

image.png

image.png

4. 30行代码实现一个聊天机器人API

internal---handler---app_handler.py

@inject
@dataclass
class AppHandler:
""""应用控制器"""

def completion(self):
    """聊天接口"""
    # 1.提取从接口中获取的输入,POST
    query = request.json.get("query")

    # 2.构建OpenAI客户端,并发起请求
    client = OpenAI(api_key="xxxx",
                    base_url="https://api.xty.app/v1")

    # 3.得到请求响应,然后将OpenAI的响应传递给前端
    completion = client.chat.completions.create(
        model="gpt-3.5-turbo-16k",
        messages=[
            {"role": "system", "content": "你是OpenAI开发的聊天机器人,请根据用户的输入回复对应的信息"},
            {"role": "user", "content": query},
        ]
    )
    content = completion.choices[0].message.content
    return content

internal---router---router.py

@inject
@dataclass
class Router:
    """路由"""
    app_handler: AppHandler

    def register_router(self, app: Flask):
        """注册路由"""
        # 1.创建一个蓝图
        bp = Blueprint("llmops", __name__, url_prefix="")

        # 2.将url与对应的控制器方法做绑定
        bp.add_url_rule("/ping", view_func=self.app_handler.ping)
        bp.add_url_rule("/app/completion", methods=["POST"], view_func=self.app_handler.completion)

        # 3.在应用上去注册蓝图
        app.register_blueprint(bp)

本地运行

python -m app.http.app

5. 校验API接口输入请求

app---http---app.py

# env加载到环境变量中
dotenv.load_dotenv()

.env

# OpenAI服务提供者
OPENAI_API_KEY=sk-OEPO9V0vInTee5WkGDE6tiKBNs5uQYYl7G5iYY8T0ec4Yuvg
OPENAI_API_BASE=https://api.xty.app/v1

internal---handler---app_handler.py

# 2.构建OpenAI客户端,并发起请求
client = OpenAI(base_url=os.getenv("OPENAI_API_BASE"))

internal---schema---app_schema.py


class CompletionReq(FlaskForm):
    """基础聊天接口请求验证"""
    # 必填、长度最大为2000
    query = StringField("query", validators=[
        DataRequired(message="用户的提问是必填"),
        Length(max=2000, message="用户的提问最大长度是2000"),
    ])

internal---handler---app_handler.py

# 1.提取从接口中获取的输入,POST
req = CompletionReq()
if not req.validate():
    return req.errors

internal---server---app_handler

class Http(Flask):
    """Http服务引擎"""

    def __init__(
            self,
            *args,
            conf: Config,
            db: SQLAlchemy,
            migrate: Migrate,
            router: Router,
            **kwargs,
    ):
        # 1.调用父类构造函数初始化
        super().__init__(*args, **kwargs)

        # 2.初始化应用配置
        self.config.from_object(conf)


        # 5.注册应用路由
        router.register_router(self)

config---config.py

class Config:
    def __init__(self):
        # 关闭wtf的csrf保护
        self.WTF_CSRF_ENABLED = _get_bool_env("WTF_CSRF_ENABLED")

6. 异常错误状态统一设计与实现

pkg---response---http_code.py

class HttpCode(str, Enum):
    """HTTP基础业务状态码"""
    SUCCESS = "success"  # 成功状态
    FAIL = "fail"  # 失败状态
    NOT_FOUND = "not_found"  # 未找到
    UNAUTHORIZED = "unauthorized"  # 未授权
    FORBIDDEN = "forbidden"  # 无权限
    VALIDATE_ERROR = "validate_error"  # 数据验证错误

internal---exception---exception.py

class CustomException(Exception):
    """基础自定义异常信息"""
    code: HttpCode = HttpCode.FAIL
    message: str = ""
    data: Any = field(default_factory=dict)

    def __init__(self, message: str = None, data: Any = None):
        super().__init__()
        self.message = message
        self.data = data


class FailException(CustomException):
    """通用失败异常"""
    pass


class NotFoundException(CustomException):
    """未找到数据异常"""
    code = HttpCode.NOT_FOUND


class UnauthorizedException(CustomException):
    """未授权异常"""
    code = HttpCode.UNAUTHORIZED


class ForbiddenException(CustomException):
    """无权限异常"""
    code = HttpCode.FORBIDDEN


class ValidateErrorException(CustomException):
    """数据验证异常"""
    code = HttpCode.VALIDATE_ERROR

internal---exception---init.py

from .exception import (
    CustomException,
    FailException,
    NotFoundException,
    UnauthorizedException,
    ForbiddenException,
    ValidateErrorException,
)

__all__ = [
    "CustomException",
    "FailException",
    "NotFoundException",
    "UnauthorizedException",
    "ForbiddenException",
    "ValidateErrorException",
]

internal---server---http.py

class Http(Flask):
    """Http服务引擎"""

    def __init__(
            self,
            *args,
            conf: Config,
            db: SQLAlchemy,
            migrate: Migrate,
            router: Router,
            **kwargs,
    ):
        # 1.调用父类构造函数初始化
        super().__init__(*args, **kwargs)

        # 2.初始化应用配置
        self.config.from_object(conf)

        # 3.注册绑定异常错误处理
        self.register_error_handler(Exception, self._register_error_handler)


        # 5.注册应用路由
        router.register_router(self)

    def _register_error_handler(self, error: Exception):
        # 1.异常信息是不是我们的自定义异常,如果是可以提取message和code等信息
        if isinstance(error, CustomException):
            return json(Response(
                code=error.code,
                message=error.message,
                data=error.data if error.data is not None else {},
            ))
        # 2.如果不是我们的自定义异常,则有可能是程序、数据库抛出的异常,也可以提取信息,设置为FAIL状态码
        if self.debug or os.getenv("FLASK_ENV") == "development":
            raise error
        else:
            return json(Response(
                code=HttpCode.FAIL,
                message=str(error),
                data={},
            ))

7. PyTest配置与API测试用例

test---interval---handler---test_app_handler.py

class TestAppHandler:
    """app控制器的测试类"""

    @pytest.mark.parametrize("query", [None, "你好,你是谁?"])
    def test_completion(self, query, client):
        resp = client.post("/app/completion", json={"query": query})
        assert resp.status_code == 200
        if query is None:
            assert resp.json.get("code") == HttpCode.VALIDATE_ERROR
        else:
            assert resp.json.get("code") == HttpCode.SUCCESS

8. Flask-SQLAlchemy扩展的配置与使用

.env

SQLALCHEMY_DATABASE_URI=postgresql://root:xxxx@localhost:5432/llmops?client_encoding=utf8

SQLALCHEMY_POOL_SIZE=30
SQLALCHEMY_POOL_RECYCLE=3600
SQLALCHEMY_ECHO=True

config---config.py

def _get_env(key: str) -> Any:
    """从环境变量中获取配置项,如果找不到则返回默认值"""
    return os.getenv(key, DEFAULT_CONFIG.get(key))


def _get_bool_env(key: str) -> bool:
    """从环境变量中获取布尔值型的配置项,如果找不到则返回默认值"""
    value: str = _get_env(key)
    return value.lower() == "true" if value is not None else False


class Config:
    def __init__(self):
        # 关闭wtf的csrf保护
        self.WTF_CSRF_ENABLED = _get_bool_env("WTF_CSRF_ENABLED")

        # 配置数据库配置
        self.SQLALCHEMY_DATABASE_URI = _get_env("SQLALCHEMY_DATABASE_URI")
        self.SQLALCHEMY_ENGINE_OPTIONS = {
            "pool_size": int(_get_env("SQLALCHEMY_POOL_SIZE")),
            "pool_recycle": int(_get_env("SQLALCHEMY_POOL_RECYCLE")),
        }
        self.SQLALCHEMY_ECHO = _get_bool_env("SQLALCHEMY_ECHO")

internal---server---http.py

# 4.初始化flask扩展
db.init_app(self)
migrate.init_app(self, db, directory="internal/migration")

app---http---module.py

class ExtensionModule(Module):
    """扩展模块的依赖注入"""

    def configure(self, binder: Binder) -> None:
        binder.bind(SQLAlchemy, to=db)
        binder.bind(Migrate, to=migrate)

9. 应用ORM模型的创建与增删改查

internal---model---app.py

class App(db.Model):
    """AI应用基础模型类"""
    __tablename__ = "app"
    __table_args__ = (
        PrimaryKeyConstraint("id", name="pk_app_id"),
        Index("idx_app_account_id", "account_id"),
    )

    id = Column(UUID, default=uuid.uuid4, nullable=False)
    account_id = Column(UUID, nullable=False)
    name = Column(String(255), default="", nullable=False)
    icon = Column(String(255), default="", nullable=False)
    description = Column(Text, default="", nullable=False)
    status = Column(String(255), default="", nullable=False)
    updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
    created_at = Column(DateTime, default=datetime.now, nullable=False)

internal---service---app_service.py

@inject
@dataclass
class AppService:
    """应用服务逻辑"""
    db: SQLAlchemy

    def create_app(self) -> App:
        with self.db.auto_commit():
            # 1.创建模型的实体类
            app = App(name="测试机器人", account_id=uuid.uuid4(), icon="", description="这是一个简单的聊天机器人")
            # 2.将实体类添加到session会话中
            self.db.session.add(app)
        return app

    def get_app(self, id: uuid.UUID) -> App:
        app = self.db.session.query(App).get(id)
        return app

    def update_app(self, id: uuid.UUID) -> App:
        with self.db.auto_commit():
            app = self.get_app(id)
            app.name = "慕课聊天机器人"
        return app

    def delete_app(self, id: uuid.UUID) -> App:
        with self.db.auto_commit():
            app = self.get_app(id)
            self.db.session.delete(app)
        return app

internal---router---

bp.add_url_rule("/app", methods=["POST"], view_func=self.app_handler.create_app)
bp.add_url_rule("/app/<uuid:id>", view_func=self.app_handler.get_app)
bp.add_url_rule("/app/<uuid:id>", methods=["POST"], view_func=self.app_handler.update_app)
bp.add_url_rule("/app/<uuid:id>/delete", methods=["POST"], view_func=self.app_handler.delete_app)
# 4.初始化flask扩展
db.init_app(self)
with self.app_context():
    _ = App()
    db.create_all()

对于这类规律性+重复性的代码,可以考虑使用 ChatGPT 来生成,创建 ORM 模型提示词## 角色

你是一个拥有10年经验的资深Python工程师,精通Flask,Flask-SQLAlchemy,Postgres,以及其他Python开发工具,能够为用户提出的需求或者提供的代码段生成指定的完整代码。

技能说明

  • 如果需要实现Flask-SQLAlchemy的ORM类,集成db.Model时,从from internal.extension.database_extension import db这里导入db;

  • 创建ORM模型时,表名__tablename__及类名全部都是单数;

  • 所有的字段都要添加nullable=False代表字段不允许为空;

  • UUID类型的字段添加默认值default=uuid.uuid4,String类型的字段长度均设置为String(255),默认值设置为default=""

  • 所有模型都有updated_atcreated_at字段,类型均是DateTime,其中updated_at包含defaultonupdate,而created_at仅包含default,值全部都是datetime.now

  • 请给ORM模型添加上__table_args__属性,涵盖PrimaryKeyConstraint为主键,所有模型都以id为主键,主键的类型为UUID,如果用户声明其他约束,例如UniqueConstraintIndex等时,请按照需求进行添加;

  • 属性的类型全部从sqlalchemy包中导入,例如:from sqlalchemy import (Column, UUID, String, DateTime, PrimaryKeyConstraint, UniqueConstraint)

  • uuid.uuid4import uuid中导入,datetime.nowfrom datetime import datetime导入;

  • 对于description等字段,通过字面意思,可以看出是描述,一般内容比较长,可以使用Text类型;

  • 用户如果表明了某个字段类型为json,则统一设置成JSONB,并从from sqlalchemy.dialects.postgresql import JSONB导入,这是Postgres特有的;

  • 其他的规范请根据你的知识库进行操作,项目使用的数据库是Postgres;

image.png

image.png

10. 重写SQLAlchemy核心类实现自动提交

pkg---sqlalchemy---sqlalchemy.py

class SQLAlchemy(_SQAlchemy):
    """重写Flask-SQLAlchemy中的核心类,实现自动提交"""

    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            raise e
with self.db.auto_commit():

11. Flask-Migrate扩展介绍与使用

# 4.初始化flask扩展
db.init_app(self)
migrate.init_app(self, db, directory="internal/migration")
# 初始化脚本
flask --app app.http.app db init
# 生成迁移脚本
flask --app app.http.app db migrate -m "项目初始化"
flask --app app.http.app db upgrade
flask --app app.http.app db downgrade
flask --app app.http.app db downgrade base

12. LangChain安装

pip install langchain langchain-community