[T.6] 团队项目:技术规格说明书

项目 内容
这个作业属于哪个课程 2025年春季软件工程(罗杰、任健)
这个作业的要求在哪里 [T.6] 团队项目:技术规格说明书
我在这个课程的目标是 学习软件工程的基础知识,和团队成员们实践各种软件工程的方法与流程,开发一个让我们值得骄傲的项目
这个作业在哪个具体方面帮助我实现目标 制定了规格说明书,有助于明确项目需求、指导团队开发、统一开发标准、便于团队协作

1.技术栈

后端框架

后端使用 fastapi 做 http 服务框架,sqlalchemy 做 orm 框架,uv 用作环境/包管理工具。

存储服务

  • 关系数据库使用 postgresql
  • 向量数据库使用 qdrant
  • 对象存储服务使用 minio,兼容 aws s3 协议的服务
  • (可选)全局 kv 缓存使用 redis

前端框架

前端使用 react 框架,canvas 使用 react-flow 库

2.软件架构

后端主体分为三层,上层依赖于下层提供的接口服务,下层不直接感知上层的应用

暂时无法在飞书文档外展示此内容

  1. toolkit 层:作为软件底层驱动层,提供全局配置,向量数据库与对象存储操作封装,组件基类和类型标注检查功能;同时配置全局的权限管理器,通过 blanker 信号机制,由 server 层注册 handler 函数进行数据窃取,架构解藕
  2. core 层:主要提供 component 全局管理器以及 pipeline 实体的解析与执行,将异步生成器包装后透传到 server 进行流式输出
  3. server 层:传统后端网络服务层。提供 rdb 各实体管理 crud 操作,以及 fastapi 路由导航,中间件注入以及依赖注入等功能。

3.代码规范

  1. Fastapi 天然支持异步编程模型,所有的 IO 操作(如网络交互/磁盘读写)必须使用异步版本的 sdk,或考虑自己封装,异步库统一使用 asycio 标准库作为运行时
  2. 代码统一规范由 pre-commit 配置,拉取项目进行开发前,首先要运行 pre-commit install,确保每次 commit 的代码风格统一
  3. 功能单实例化:对于“连接服务属性”的客户端逻辑代码,考虑封装为 manager 全局单实例组件,如 minio / rdb repository / vdb_client 等
  4. 命名规范:遵循 python 社区规范,变量 / 函数名采用 snake 命名法,类名使用驼峰命名法
  5. 。。。

4.功能拆解

4.1组件化工作流实现+自定义插件二次开发

所有组件都必须基于 toolkit 层提供的基类,无论官方组件还是用户自定义组件。当用户编写自定义组件时,可以通过 pip install ragnarok_toolkit 安装,以获得系统底层支持能力以及类型标注。

class RagnarokComponent(ABC):
    """
    base class for a Ragnarök component,
    designed to standardize component code format
    """

    # the description of the component's functionality
    DESCRIPTION: str

    # whether to enable type annotation check for identification
    ENABLE_HINT_CHECK: bool = True

    @classmethod
    @abstractmethod
    def input_options(cls) -> Tuple[ComponentInputTypeOption, ...]:
        """the options of all the input value"""
        return ()

    @classmethod
    @abstractmethod
    def output_options(cls) -> Tuple[ComponentOutputTypeOption, ...]:
        """the options of all the output value"""
        return ()

    @classmethod
    @abstractmethod
    def execute(cls, *args, **kwargs) -> Dict[str, Any]:
        """
        execute the component function, could be either sync or async
        """
        pass

    @classmethod
    def validate(cls) -> bool:
        """check if the 'execute' function corresponds to the INPUT_OPTIONS and OUTPUT_OPTIONS"""
        execute_params = get_type_hints(cls.execute)
        execute_params.pop("return")

        input_options = cls.input_options()
        input_option_names = {option["name"] for option in input_options}

        # check input name set
        if set(execute_params.keys()) != input_option_names:
            return False

        # check input type
        for input_option in input_options:
            param_name = input_option["name"]
            hint_type = execute_params.get(param_name)
            allowed_types = {io_type.python_type for io_type in input_option.get("allowed_types")}

            if input_option.get("required"):
                if hint_type not in allowed_types:
                    return False
            else:
                # optional param
                if (
                    hint_type is not None
                    and hasattr(hint_type, "__origin__")
                    and (hint_type.__origin__ is Optional or hint_type.__origin__ is Union)
                ):
                    # extract the inner type from Optional
                    actual_types = {arg for arg in hint_type.__args__ if arg is not type(None)}
                    if not actual_types.issubset(allowed_types):
                        return False
                else:
                    return False

        # TODO is it possible to validate output value here
        return True

    def __new__(cls, *args, **kwargs):
        raise TypeError(f"Class {cls.__name__} and its subclasses cannot be instantiated.")

组件会通过 manager 自动注册到系统之中。

4.2异步流式/断点运行

Pipeline 作为程序中的数据实体存在,统一封装为 PipelineEntity,组件运行依靠 python 动态配置判断同步或异步版本,进行执行。

组件返回的携程对象应被上层 await 后包装,统一封装为 AsyncGenerator,传输到 server 层进行 sse 流式返回。

class PipelineEntity:
    def __init__(self, node_map: Dict[str, PipelineNode], inject_input_mapping: Dict[str, Tuple[str, str]]) -> None:
        # store the mapping of the node_id and node entity
        self.node_map = node_map
        # store the processing result, breaking the contagiousness of multi async generator
        self.result_queue: asyncio.Queue[PipelineExecutionInfo] = asyncio.Queue(maxsize=2 * len(self.node_map))
        # num of the unfinished node
        self.remaining_num = len(node_map)
        # outer input inject mapping. eg: inject_name -> (node_id, node_input_name)
        self.inject_input_mapping = inject_input_mapping
        # beginning nodes, whose input is either empty or totally injected
        self.begin_nodes = [
            node
            for node in node_map.values()
            if len(node.component.input_options()) == 0
            or {input_option.get("name") for input_option in node.component.input_options()}.issubset(
                {
                    node_input_name
                    for node_id, node_input_name in inject_input_mapping.values()
                    if node_id == node.node_id
                }
            )
        ]

    async def run_async(self, *args, **kwargs) -> AsyncGenerator[PipelineExecutionInfo, None]:
        """execute the pipeline, async version"""
        # 1. inject outer input
        for inject_name, (node_id, node_input_name) in self.inject_input_mapping.items():
            actual_input_value = kwargs.get(inject_name)
            # TODO check if actual_input_value is None or not correspond to node expected type
            self.node_map[node_id].input_data[node_input_name] = actual_input_value

        # 2. run beginning task
        for node in self.begin_nodes:
            asyncio.create_task(self.run_node_async(node))

        # 3. collect result
        while self.remaining_num > 0:
            execution_info = await self.result_queue.get()
            if execution_info.type == "process_info":
                self.remaining_num -= 1

            yield execution_info

    async def run_node_async(self, node: PipelineNode) -> None:
        """run a node execution function, async version"""

        # TODO error handling
        if asyncio.iscoroutinefunction(node.component.execute):
            node_outputs = await node.component.execute(**node.input_data)
        else:
            node_outputs = node.component.execute(**node.input_data)

        # if is output node, yield output info
        # HINT!: this have to be set before putting process_info, because we use process_info to count remaining num
        if node.output_name is not None:
            self.result_queue.put_nowait(
                PipelineExecutionInfo(node.node_id, "output_info", {node.output_name: node_outputs})
            )

        # return current node result
        self.result_queue.put_nowait(PipelineExecutionInfo(node.node_id, "process_info", node_outputs))

        # trigger forward nodes
        tasks = []
        for connection in node.forward_node_info:
            current_output = node_outputs[connection.from_node_output_name]
            trigger_node = self.node_map[connection.to_node_id]
            if trigger_node is None:
                continue

            trigger_node.input_data[connection.to_node_input_name] = current_output
            trigger_node.waiting_num -= 1
            if trigger_node.waiting_num == 0:
                task = asyncio.create_task(self.run_node_async(trigger_node))
                tasks.append(task)

        await asyncio.gather(*tasks)

    @classmethod
    def from_json_str(cls, json_str: str) -> "PipelineEntity":
        """instantiate a pipeline entity from a json format string"""
        pass

4.3权限管理

知识库权限划分为公有库和私有库,在 toolkit 层由 permission_manager实例统一管理。

通过 blinker 信号机制窃取到 server 层 rdb 的信息,得到权限三元组进行统一鉴权。

4.4业务层

对于业务模块,应当实现如下一些主功能:

  • 创建企业
  • 创建用户
  • 创建知识库
  • 权限变更
  • 创建/删除/修改/更新 pipeline
  • 调用运行 pipeline(区分普通用户前端调用和企业账号 api 调用)

5.出口条件

  1. 基本功能开发完毕,能跑通 rag 全流程,串联起各个模块
  2. 对于正常的用户行为能有正确的输出结果。由于是 b 端的应用,对非法操作有一定容忍度

6.测试计划

  1. 功能开发阶段,对单个模块进行单元测试,项目已配置好 pytest 以及 asyncio 异步版本插件
  2. 前后端交互环节,主要测试 pipeline 格式编解码
  3. 整体开发完成后统一进行功能测试,包括前端调试界面以及 api 接口

7.技术风险识别和评估

主要有两个风险点:

  1. 权限系统的设定:如果设计不佳,会有用户越权的可能性,要在 toolkit 底层接口层提供权限安全的接口,不能暴露隐患api
  2. 工作流执行阶段,对单节点故障以及非法工作流(如死循环)要有检测识别能力,避免程序跑死
posted @ 2025-04-13 19:20  Dvorag  阅读(51)  评论(0)    收藏  举报