展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > 09 | 重构项目结构

09 | 重构项目结构

09 | 重构项目结构 本文目标 前面三篇已经完成了生成阶段的元数据同步工作: 把表、字段、指标和它们之间的关联关系写入 MySQL meta 库 把字段和指标的向量写入 Qdrant 把低基数的维度值写入 Elasticsearch 等功能逐渐完善了,原先的目录结构就开始暴露出一些问题。这些问题其

09 | 重构项目结构

09 | 重构项目结构

本文目标

前面三篇已经完成了生成阶段的元数据同步工作:

  1. 把表、字段、指标和它们之间的关联关系写入 MySQL meta 库
  2. 把字段和指标的向量写入 Qdrant
  3. 把低基数的维度值写入 Elasticsearch

等功能逐渐完善了,原先的目录结构就开始暴露出一些问题。这些问题其实挺典型的,比如说:

  • scripts/rebuild_metadata.py 这个脚本,同时包揽了配置校验、数据读取、实体构造、向量生成、数据库写入和客户端关闭——几乎一个人干了所有活。
  • 多个 Service 层,里面只有一行 Repository 的转发调用,基本没什么真正的业务逻辑。
  • Elasticsearch 的 Mapping 和 Bulk 细节,直接写在 Service 里,应用层知道了太多底层的存储实现。
  • FastAPI 的路由、请求解析和应用创建,全都挤在根目录的 main.py 里。
  • LangGraph 的 State、节点函数、Graph 声明和演示代码,全在一个文件里混着。
  • 应用配置用的是多层 dict,配置字段拼错了,只能等到运行时才能发现。
  • 更关键的是,没有自动化测试,每次重构以后,只能手动跑一次破坏性的全量同步来验证。

所以,这次的核心任务不是增加新功能,而是完成一次彻底的结构重构。重构完成后,已有功能完全不变,启动方式也保持兼容。


1. 为什么现在需要重构

项目刚起步的时候,代码量少,把逻辑直接往脚本里堆是最快的路子:

 复制代码读取 YAML
→ 查询 DW
→ 写 MySQL
→ 生成向量
→ 写 Qdrant
→ 写 Elasticsearch

这时候如果过早地设计一堆目录,反而会拖慢开发速度。

但当同步目标增加到三个之后,一个脚本需要知道的事情就太多了:

  • MySQL 的 Engine、Session 和事务管理
  • SQLAlchemy 的 Model、Mapper、Repository
  • TEI 的地址、超时和批次大小
  • Qdrant 的 Collection 名称和向量维度
  • Elasticsearch 的 Index、Mapping、Bulk 和 Refresh
  • 三种存储的写入顺序和失败边界

继续往这个脚本里塞功能,比如加入召回、日志、监控,它就会变成一个越来越难维护的“大泥球”。

所以,判断要不要重构,不是看文件行数有没有超过某个固定的数字,而是看它是否出现了下面这些信号:

  1. 一个文件需要知道太多不同技术的细节
  2. 修改一种存储,会影响到与它无关的代码
  3. 同一段初始化或资源关闭的逻辑,在多个入口重复出现
  4. 业务流程只能通过阅读大量底层实现才能理解
  5. 很难在不连接真实数据库的情况下进行测试

当前项目已经出现了这些信号,所以,是时候动手了。

2. 重构前后的目录对比

先看看重构前的核心目录长什么样:

 复制代码app/
├── agent/
│   └── graph.py
├── entities/
├── infrastructure/
├── mappers/
├── models/
├── repositories/
└── services/
    ├── table_info_service.py
    ├── column_info_service.py
    ├── metric_info_service.py
    ├── column_metric_service.py
    ├── dw_db_service.py
    ├── vector_sync_service.py
    └── dim_value_sync_service.py
conf/
└── sync_db.py
main.py

重构之后,变成这样:

 复制代码app/
├── main.py
├── api/
│   ├── routes/
│   │   └── query.py
│   └── schemas/
│       └── query.py
├── agent/
│   ├── state.py
│   ├── nodes.py
│   └── graph.py
├── application/
│   ├── metadata_rebuild_service.py
│   ├── vector_sync_service.py
│   └── dim_value_sync_service.py
├── config/
│   └── settings.py
├── entities/
├── infrastructure/
├── mappers/
├── models/
└── repositories/
    ├── dim_value_repository.py
    ├── dw_db_repository.py
    ├── qdrant_repository.py
    └── ...
conf/
├── app_config.yaml
├── get_config.py
└── meta_config.yaml
scripts/
└── rebuild_metadata.py
tests/
└── unit/

每层的主要职责整理了一下:

层目录主要职责
APIapp/api/HTTP 请求解析、响应格式、路由
Agentapp/agent/LangGraph 状态、节点和工作流
Applicationapp/application/编排一个完整业务用例
Entityapp/entities/与具体存储无关的数据对象
Repositoryapp/repositories/表达数据读取和写入语义
Mapperapp/mappers/Entity 与 ORM Model 转换
Modelapp/models/SQLAlchemy 表映射
Infrastructureapp/infrastructure/外部 SDK、连接池和 HTTP Client
Configapp/config/、conf/配置结构和 YAML 配置内容
Scriptscripts/组装依赖并启动应用用例
Testtests/自动验证行为

这里采用的是轻量分层架构,并没有追求把每个概念都拆成独立的目录。


3. 第一步:增加 Application 层

旧的 scripts/rebuild_metadata.py 直接编排了整个流程。重构之后,把真正的业务用例放到了这里:

 复制代码app/application/metadata_rebuild_service.py

核心类长这样:

 复制代码class MetadataRebuildService:
    """准备并重建 MySQL、Qdrant 和 Elasticsearch 元数据。"""
    def __init__(
        self,
        dw_database: MySQLDatabase,
        meta_database: MySQLDatabase,
        vector_sync_service: VectorSyncService,
        dim_value_sync_service: DimValueSyncService,
        column_collection: str,
        metric_collection: str,
    ):
        self.dw_database = dw_database
        self.meta_database = meta_database
        self.vector_sync_service = vector_sync_service
        self.dim_value_sync_service = dim_value_sync_service
        self.column_collection = column_collection
        self.metric_collection = metric_collection

注意,它不自己创建 MySQL、TEI、Qdrant 和 ES 客户端,而是通过构造参数接收这些能力。

这样有两个好处:

  1. Application Service 只关心流程,不关心客户端怎么初始化
  2. 单元测试可以传入 Fake 对象,不需要真的启动 Docker 服务

完整的重建入口变成了这样:

 复制代码async def rebuild(self, config: dict[str, Any]) -> MetadataRebuildResult:
    prepared = await self.prepare(config)
    await self._write_meta_database(prepared)
    column_vector_count = await self.vector_sync_service.replace_collection(
        self.column_collection,
        prepared.column_points,
    )
    metric_vector_count = await self.vector_sync_service.replace_collection(
        self.metric_collection,
        prepared.metric_points,
    )
    dim_value_count = await self.dim_value_sync_service.replace_index(
        prepared.dim_value_documents
    )
    return MetadataRebuildResult(...)

光看这个方法,就能清楚理解整个业务流程:

 复制代码准备全部数据
→ 写 MySQL
→ 写 Qdrant 字段向量
→ 写 Qdrant 指标向量
→ 写 Elasticsearch 维度值
→ 返回每部分数量

这就是 Application Service 的价值所在。

4. 第二步:同步过程拆成准备和写入两个阶段

全量重建会删除旧表、旧 Collection 或旧 Index。如果读取 DW 或者生成向量的时候中途失败了,可不能先把已有的数据清空。

所以,重建过程拆成了两个阶段。

4.1 准备阶段

 复制代码async def prepare(self, config: dict[str, Any]) -> PreparedMetadata:
    self.validate_config(config)
    table_infos = self._build_table_infos(config)
    metric_infos = self._build_metric_infos(config)
    column_metrics = self._build_column_metrics(config)
    column_infos, dim_value_documents = await self._read_dw_metadata(config)
    column_points = await self.vector_sync_service.prepare_columns(
        self.column_collection,
        column_infos,
    )
    metric_points = await self.vector_sync_service.prepare_metrics(
        self.metric_collection,
        metric_infos,
    )
    return PreparedMetadata(...)

准备阶段只做这些事:

  • 校验 meta_config
  • 构造 Entity
  • 读取 DW 字段类型、样例和维度值
  • 调用 TEI 生成向量
  • 校验向量维度

这个阶段不会修改 MySQL meta、Qdrant 和 Elasticsearch 里的数据。

4.2 写入阶段

等所有准备操作都成功了,才开始重建目标存储:

 复制代码PreparedMetadata
       │
       ├─ 写 MySQL meta
       ├─ 替换 Qdrant Collections
       └─ 替换 Elasticsearch Index

这样虽然不能实现三个数据库之间的真正原子事务,但可以避免大量“前置计算失败了,旧数据却已经提前丢失”的尴尬情况。

如果生产环境要求切换过程完全无感,那还需要:

  • 临时 MySQL 表
  • 临时 Qdrant Collection
  • 临时 Elasticsearch Index
  • 校验完成后,通过重命名或 Alias 来切换

当前项目还处于生成和学习阶段,先用“准备成功后再重建”的方案,已经够用了。

5. 第三步:合并只有透传作用的 Service

旧代码里存在这样的 Service:

 复制代码class ColumnInfoService:
    def __init__(self, repository: ColumnInfoRepository):
        self.repository = repository
    async def add_all(self, column_infos):
        return await self.repository.add_all(column_infos)

它没有校验、组合、事务或业务判断,只是把调用原样转交给 Repository。

调用链反而变得更长了:

 复制代码Script
→ ColumnInfoService
→ ColumnInfoRepository
→ Mapper
→ Model

删除这些纯透传的 Service 之后,调用链变成了:

 复制代码async with self.meta_database.session() as session, session.begin():
    await TableInfoRepository(session).add_all(prepared.table_infos)
    await ColumnInfoRepository(session).add_all(prepared.column_infos)
    await MetricInfoRepository(session).add_all(prepared.metric_infos)
    await ColumnMetricRepository(session).add_all(prepared.column_metrics)

现在调用链是:

 复制代码MetadataRebuildService
→ Repository
→ Mapper
→ Model

Application Service 负责业务流程,Repository 负责持久化,这两层职责已经足够了。

这里要记住一个原则:只有当一层确实包含独立职责的时候,它才值得存在。

6. 第四步:统一 Repository 的职责

重构之前,情况是这样的:

  • Qdrant 的 Collection 和 Point 操作,在 QdrantRepository 里
  • Elasticsearch 的 Mapping、Bulk、Refresh,却都在 DimValueSyncService 里

抽象标准不一致,这显然不好。

重构后,新增了:

 复制代码app/repositories/dim_value_repository.py

现在 Repository 负责 ES 的存储细节:

 复制代码class DimValueRepository:
    async def reset_index(self) -> None:
        ...
    async def index_values(self, documents) -> int:
        ...
    async def refresh(self) -> None:
        ...

Application Service 只负责调用顺序:

 复制代码class DimValueSyncService:
    async def replace_index(self, documents) -> int:
        await self.repository.reset_index()
        total = await self.repository.index_values(documents)
        await self.repository.refresh()
        return total

现在职责划分就更加一致了:

 复制代码DimValueSyncService
└─ 编排:reset → index → refresh
DimValueRepository
└─ 实现:Index Mapping、Bulk 格式、稳定 UUID、错误解析
ESClient
└─ 连接:官方异步 SDK、地址、超时

7. 第五步:让 Script 只负责启动

重构前的同步脚本超过 400 行,包含了大量业务实现。

重构后的 scripts/rebuild_metadata.py,只负责三件事:

  1. 根据配置创建客户端和 Database
  2. 把依赖组装成 MetadataRebuildService
  3. 执行、打印结果并关闭资源

核心入口:

 复制代码async def main() -> None:
    dw_database = MySQLDatabase(...)
    meta_database = MySQLDatabase(...)
    embedding_client = EmbeddingClient(...)
    qdrant_client = QdrantClient(...)
    es_client = ESClient(...)
    rebuild_service = MetadataRebuildService(...)
    try:
        result = await rebuild_service.rebuild(meta_config)
        print_result(result)
    finally:
        await asyncio.gather(
            dw_database.close(),
            meta_database.close(),
            embedding_client.close(),
            qdrant_client.close(),
            es_client.close(),
        )

Script 仍然需要知道如何创建具体依赖,因为它是这个命令的组合根。

但它不再负责:

  • 怎样校验元数据
  • 怎样查询 DW
  • 怎样生成 Qdrant Point
  • 怎样组织 ES Bulk 请求
  • 怎样控制业务写入顺序

运行方式:

 复制代码uv run python scripts/rebuild_metadata.py

8. 第六步:拆分 FastAPI API 层

原来的根目录 main.py 同时包含了:

  • FastAPI 创建
  • CORS 配置
  • 健康检查
  • SSE 生成器
  • 查询路由
  • 请求 dict 解析

重构后:

 复制代码app/
├── main.py
└── api/
    ├── routes/query.py
    └── schemas/query.py

请求模型:

 复制代码class QueryRequest(BaseModel):
    query: str = Field(
        min_length=1,
        description="需要转换为 SQL 的自然语言问题",
    )

这样空字符串会自动返回 HTTP 422,而不需要在路由里手动判断。

查询路由:

 复制代码router = APIRouter(prefix="/api", tags=["query"])
@router.post("/query", response_class=StreamingResponse)
async def query(payload: QueryRequest) -> StreamingResponse:
    return StreamingResponse(
        sse_stream(payload.query),
        media_type="text/event-stream",
    )

应用工厂:

 复制代码def create_app() -> FastAPI:
    application = FastAPI(title="n2sql-agent")
    application.add_middleware(...)
    application.include_router(query_router)
    return application
app = create_app()

推荐启动方式:

 复制代码uv run fastapi dev app/main.py

根目录的 main.py 仍然保留了兼容导入,所以旧的命令也能继续使用:

 复制代码uv run fastapi dev main.py

9. 第七步:拆分 Agent

原来的 app/agent/graph.py 同时包含了 State、节点函数、Graph 声明和演示代码。

重构后:

 复制代码app/agent/
├── state.py   # State、RuntimeContext、进度事件
├── nodes.py   # 工作流节点
└── graph.py   # Graph 拓扑和路由

9.1 state.py

 复制代码class State(TypedDict, total=False):
    query: str
    keywords: list[str]
    error: str | None

total=False 表示每个节点不需要返回全部字段,只返回自己修改的那部分就可以了。

比如关键词节点只返回:

 复制代码return {"keywords": keywords}

9.2 nodes.py

这里放节点的具体行为:

 复制代码extract_keywords
recall_column
recall_metric
recall_value
generate_sql
validate_sql
run_sql
...

当前未实现的节点,共用一个占位辅助函数,避免重复写进度推送的代码。

9.3 graph.py

这里只声明节点之间怎么连接:

 复制代码def route_after_validation(state: State) -> str:
    return "run_sql" if state.get("error") is None else "correct_sql"
def create_graph():
    return (
        StateGraph(...)
        .add_node(extract_keywords)
        .add_node(recall_column)
        ...
        .add_conditional_edges(...)
        .compile()
    )

以后查看工作流结构时,不需要再穿过每个节点的实现细节。

10. 第八步:把应用配置改成强类型

旧代码用的是多层字典:

 复制代码app_config["qdrant"]["embedding_size"]
app_config["embedding"]["batch_size"]

如果写成:

 复制代码app_config["qdrant"]["embeding_size"]

只能运行到这一行的时候,才发现 KeyError。

重构后,在 app/config/settings.py 定义了 Pydantic Model:

 复制代码class QdrantSettings(StrictSettings):
    host: str
    port: int
    embedding_size: int = Field(gt=0)
    column_collection: str
    metric_collection: str
    timeout: float = Field(default=60, gt=0)

总配置:

 复制代码class AppSettings(StrictSettings):
    logging: LoggingSettings
    meta_db: DatabaseSettings
    dw_db: DatabaseSettings
    qdrant: QdrantSettings
    embedding: EmbeddingSettings
    es: ElasticsearchSettings
    llm: LLMSettings

配置加载后,可以用属性访问:

 复制代码app_config.qdrant.embedding_size
app_config.embedding.batch_size
app_config.es.index_name

extra="forbid" 会拒绝没有声明的字段,所以 YAML 的 key 拼错了,程序会在启动阶段直接报错,不会等到运行时才发现。

需要注意:

  • conf/app_config.yaml 仍然保存配置值
  • app/config/settings.py 描述配置结构和约束
  • .env 仍然保存密码、API Key 等敏感值

三者职责不同,互不冲突。

11. 第九步:补充自动化测试

重构最危险的地方在于:目录看起来更整齐了,但业务行为可能已经变了。

因此,新增了:

 复制代码tests/unit/
├── test_api.py
├── test_dim_value_repository.py
├── test_dim_value_sync_service.py
├── test_metadata_rebuild_service.py
└── test_vector_sync_service.py

重点验证几个方面:

11.1 向量准备阶段不写 Qdrant

 复制代码points = await service.prepare_columns("columns", columns)
self.assertEqual(repository.calls, [])

只有执行了:

 复制代码await service.replace_collection("columns", points)

才允许调用 reset_collection 和 upsert。

11.2 ES 写入顺序

 复制代码reset_index
→ index_values
→ refresh

11.3 配置引用校验

指标不能引用不存在的字段,只有 dimension 字段才能配置 sync: true。

11.4 API 参数校验

空查询应返回 HTTP 422,健康检查应返回 200。

运行测试:

 复制代码uv run python -m unittest discover -s tests -v

当前结果:

 复制代码Ran 8 tests
OK

12. 重构后的依赖方向

生成阶段的完整依赖关系:

 复制代码scripts/rebuild_metadata.py
          │
          ▼
MetadataRebuildService             Application
          │
          ├─ VectorSyncService      Application
          ├─ DimValueSyncService    Application
          │
          ▼
Repository                         Data access
          │
          ├─ MySQL Repository
          ├─ QdrantRepository
          └─ DimValueRepository
          │
          ▼
Infrastructure Client              External technology
          │
          ├─ MySQLDatabase
          ├─ EmbeddingClient
          ├─ QdrantClient
          └─ ESClient

依赖方向应该从业务流程指向外部实现:

 复制代码入口 → 应用用例 → 数据访问 → 外部 SDK

而不是让 Repository 反过来 import API,或者让 Entity import Elasticsearch Client。

13. 哪些目录没有继续拆

这次没有把项目改造成特别重的 DDD 结构。

保留了:

 复制代码app/entities/
app/models/
app/mappers/
app/repositories/

没有继续增加:

 复制代码domain/ports/
domain/repositories/
infrastructure/adapters/
application/commands/
application/handlers/

原因是当前项目规模还不需要这么多抽象。

举个例子,Repository 目前只有一种实现,没有必要先定义一个完全相同的抽象接口:

 复制代码class AbstractColumnInfoRepository(Protocol):
    async def add_all(...): ...

等到真的出现以下需求的时候,再增加接口也不迟:

  • MySQL 与 PostgreSQL 两种实现需要共存
  • 生产实现与内存实现需要互换
  • 多个业务用例依赖同一套稳定抽象

架构设计应该解决已经出现或很快会出现的真实问题,而不是试图预测所有可能性。

14. 验证重构结果

14.1 编译检查

 复制代码uv run python -m compileall -q app conf scripts tests main.py

14.2 单元测试

 复制代码uv run python -m unittest discover -s tests -v

14.3 启动 API

 复制代码uv run fastapi dev app/main.py

14.4 完整重建

确认 MySQL、TEI、Qdrant 和 Elasticsearch 已经启动后,执行:

 复制代码uv run python scripts/rebuild_metadata.py

注意,这个命令会重建开发环境中的:

  • MySQL meta 表
  • Qdrant 字段和指标 Collection
  • Elasticsearch 维度值 Index

不要直接把当前“删除后重建”的实现用于生产环境。


科普项目架构

1. 什么是项目架构

项目架构不是目录树本身,也不是文件夹名字听起来是否“高级”。

架构描述的是:

  • 系统由哪些部分组成
  • 每部分负责什么
  • 各部分怎样通信
  • 谁可以依赖谁
  • 改动一个部分时,会影响多少其他部分

目录只是架构的一种可见表达。

举个例子,下面两个项目都叫 services,但内部的职责可能完全不同:

 复制代码项目 A:Service 负责完整业务用例
项目 B:Service 只转发 Repository

不能只通过目录名来判断架构是否合理,要深入看到实际的依赖和行为。

2. 什么是分层架构

分层架构就是把不同职责放在不同层:

 复制代码API 层
  ↓
Application 层
  ↓
Repository 层
  ↓
Infrastructure 层

一种常见的理解方式:

  • API:外界怎样调用系统
  • Application:系统要完成什么用例
  • Repository:数据怎样读取和保存
  • Infrastructure:具体使用哪个数据库、SDK 或 HTTP 服务

分层的核心不是“所有请求必须经过固定数量的文件”,而是:

3. 什么是关注点分离

关注点分离,英文是 Separation of Concerns。

它表示不同类型的问题,应该由不同的代码来处理。

例如:

 复制代码QueryRequest
→ 负责 HTTP 输入校验
MetadataRebuildService
→ 负责同步流程
DimValueRepository
→ 负责 ES Bulk 格式
ESClient
→ 负责连接 Elasticsearch

如果一个类同时处理 HTTP、业务规则、SQL 和日志文件轮转,那就混合了太多不同的关注点。

4. 什么是依赖注入

依赖注入不是某个框架的专属功能。

最简单的依赖注入,就是把对象从构造参数传进去:

 复制代码service = VectorSyncService(
    embedding_client=embedding_client,
    qdrant_repository=qdrant_repository,
    vector_size=1024,
    model_name="BAAI/bge-large-zh-v1.5",
)

而不是在 Service 内部写死:

 复制代码class VectorSyncService:
    def __init__(self):
        self.client = EmbeddingClient("http://localhost:8081")

通过外部传入依赖后:

  • 生产环境可以传真实的 Client
  • 测试环境可以传 Fake Client
  • Service 不需要知道地址从哪里读取
  • 客户端的生命周期可以由入口统一管理

5. 什么是组合根

组合根,英文是 Composition Root。

它是集中创建并连接各个对象的地方:

 复制代码创建 Database
创建 Client
创建 Repository
创建 Application Service
调用用例

本项目的命令行组合根是:

 复制代码scripts/rebuild_metadata.py

组合根可以依赖很多具体类,因为它的职责就是“把系统组装起来”。

业务类内部则不应该到处重复创建这些对象。

6. 什么是 Repository

Repository 把数据存取表达成业务可以理解的操作:

 复制代码get_all_column_types()
get_distinct_column_values()
reset_collection()
index_values()

它隐藏了具体实现:

  • SQL 怎么写
  • Qdrant PointStruct 怎么构造
  • Elasticsearch Bulk 的 operations 怎么排列
  • 稳定 UUID 怎么生成

Repository 不应该决定整个业务用例的执行顺序,也不应该随意提交上层的数据库事务。

本次重构删除了 TableInfoRepository.add() 中自行 commit() 的做法,统一由 Application 层来控制事务边界。

7. 什么是 Application Service

Application Service 表达一个完整用例,比如:

 复制代码重建全部元数据
同步字段向量
替换维度值索引
执行一次自然语言查询

它通常负责:

  • 调用多个 Repository
  • 控制步骤顺序
  • 控制事务边界
  • 组织输入和输出结果
  • 处理跨组件的失败场景

它通常不负责:

  • HTTP JSON 格式
  • SQLAlchemy 字段声明
  • Elasticsearch Mapping 细节
  • 官方 SDK 初始化参数

8. 什么是重构

重构,是在尽量不改变外部行为的前提下,改善内部结构。

本次重构后,用户仍然可以这样启动:

 复制代码uv run fastapi dev main.py
uv run python scripts/rebuild_metadata.py

同步的数据结构、Collection 名称和 Index 名称也没有改变。

改变的是内部的职责划分和代码位置。

一次安全的重构通常包含以下步骤:

  1. 先确认当前行为
  2. 补测试或准备验证命令
  3. 小步迁移
  4. 更新所有 import
  5. 执行测试和集成验证
  6. 最后删除旧代码

9. 什么是过度设计

过度设计,就是为尚未出现的复杂度,提前增加大量不必要的结构。

常见的表现:

  • 每张表都有 Interface、Base、Impl、Factory、Manager、Service
  • 只有一种实现,却提前设计了很多可插拔的接口
  • 一个十行功能,需要跳转七八个文件才能看懂
  • 文件夹很多,但每层都只是参数透传

避免过度设计的方法:

  1. 先看是否存在真实的变化点
  2. 同一种模式重复出现两三次之后,再考虑抽象
  3. 抽象之后,必须减少调用方需要知道的细节
  4. 删除没有独立职责的层

“代码少”不一定简单,“目录多”也不一定专业。

10. 单元测试与集成测试有什么区别

单元测试只验证一个较小的单元,通常不连接真实的外部服务:

 复制代码FakeEmbeddingClient
FakeQdrantRepository
FakeDimValueRepository

优点:

  • 速度快
  • 结果稳定
  • 失败原因明确
  • 不会清空真实数据

集成测试则验证多个真实组件能否一起工作:

 复制代码SQLAlchemy → MySQL
Qdrant Client → Qdrant
ES Client → Elasticsearch
EmbeddingClient → TEI

它更接近真实环境,但速度慢,也需要准备和清理数据。

一个成熟的项目,通常两种测试都需要:

 复制代码大量快速的单元测试
+
少量关键的集成测试

11. 架构需要一直重构吗

不需要。

当前的结构已经能清楚地表达:

 复制代码API
Agent
Application
Repository
Infrastructure

下一步的重点,应该是继续实现真正的字段召回、指标召回、维度值召回和 SQL 生成,而不是继续搬动目录。

等到出现新的真实问题时,再考虑调整,比如:

  • API 节点需要统一共享数据库客户端
  • Repository 出现了第二种存储实现
  • MetadataRebuildService 再次膨胀
  • 需要生产级的 Alias 原子切换
  • 单元测试之外,需要 Docker 集成测试

架构的目标,是帮助业务持续演进,而不是让项目永远处于“重构中”的状态。

本文小结

本文完成了项目的第一次结构性重构:

  1. 增加 application 层,承载完整业务用例
  2. 将元数据同步拆成准备与写入两个阶段
  3. 删除没有独立职责的透传 Service
  4. 将 Elasticsearch 存取细节迁入 Repository
  5. 把重建脚本缩小为组合根
  6. 拆分 FastAPI 路由和 Pydantic Schema
  7. 拆分 LangGraph 的 State、Nodes 和 Graph
  8. 使用 Pydantic 建立强类型应用配置
  9. 增加单元测试,保护重构行为
  10. 保留轻量结构,避免过度设计

重构完成后,核心原则归结起来其实就一句话:

 复制代码入口负责组装
Application 负责用例
Repository 负责存取
Infrastructure 负责连接
Entity 保持独立
测试保护行为

完成这一步后,项目就可以在更稳定的结构上,继续实现 NL2SQL 的运行阶段了。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
精品专题 更多
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

IOS软件

正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

AI

正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

Mac软件 更多
WPS Office macOS版
WPS Office macOS版

WPS Office是一款面向macOS用户的综合办公套件,将文字处理、电子表格、演示文稿、PDF工具、模板与云协作整合在同一应用中。它兼容常见Microsoft Office格式,支持Intel及Apple芯片Mac,基础功能可免费使用,

Shapr3D macOS版
Shapr3D macOS版

Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。

REAPER macOS版
REAPER macOS版

REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

Ableton Live macOS版
Ableton Live macOS版

Ableton Live 是面向音乐制作人与现场表演者的数字音频工作站,提供编曲视图、独具特色的现场视图、音频录制、MIDI创作、实时变速、乐器及效果器。Mac版原生支持Apple芯片,并可连接音频接口、MIDI控制器和第三方插件。

Adobe After Effects macOS版
Adobe After Effects macOS版

Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

Adobe Audition macOS版
Adobe Audition macOS版

Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

Adobe Bridge macOS版
Adobe Bridge macOS版

Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

Adobe Illustrator macOS版
Adobe Illustrator macOS版

Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

Adobe InDesign macOS版
Adobe InDesign macOS版

Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

Adobe Lightroom Classic macOS版
Adobe Lightroom Classic macOS版

Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

Adobe Media Encoder macOS版
Adobe Media Encoder macOS版

Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

Adobe Photoshop macOS版
Adobe Photoshop macOS版

Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

WINDOWS 更多
Adobe Photoshop Windows版
Adobe Photoshop Windows版

Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

Adobe InDesign Windows版
Adobe InDesign Windows版

Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

Adobe Illustrator Windows版
Adobe Illustrator Windows版

Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

Adobe Bridge Windows版
Adobe Bridge Windows版

Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

Affinity Photo Windows版
Affinity Photo Windows版

Affinity Photo 是一款面向摄影师、修图师和视觉设计人员的桌面图像编辑工具,提供 RAW 显影、多图层合成、蒙版、实时滤镜、批处理和色彩管理等功能。原独立产品已停止销售,其照片编辑能力已延续至免费的新 Affinity 应用;

Adobe Lightroom Classic Windows版
Adobe Lightroom Classic Windows版

Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

Figma Windows版
Figma Windows版

Figma 是面向产品设计与开发团队的云端协作平台,可在 Windows 桌面客户端中完成界面绘制、组件管理、交互原型、评论评审和开发交付。文件保存在云端,多名成员能够同步编辑并共享设计系统;官方同时提供免费 Starter 方案及按席位计

Affinity Designer Windows版
Affinity Designer Windows版

Affinity Designer 是面向矢量图形、数字插画和界面设计的专业创作工具,可在矢量、像素与导出工作区之间快速切换。它提供曲线编辑、形状构建、画板、排版、图层和专业色彩管理,并支持常见设计文件的导入及多种印刷、网页格式输出。

Capture One Pro Windows版
Capture One Pro Windows版

Capture One Pro 是面向摄影师和影像团队的专业桌面照片处理工具,提供相机定制色彩配置、RAW转换、图层与蒙版、联机拍摄、目录及会话管理等功能。它适合棚拍、人像、商业产品、婚礼和风光摄影,

Adobe Acrobat Pro Windows版
Adobe Acrobat Pro Windows版

Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

Adobe After Effects Windows版
Adobe After Effects Windows版

Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

DaVinci Resolve Windows版
DaVinci Resolve Windows版

DaVinci Resolve是Blackmagic Design推出的一体化影视后期工具,将素材管理、非线性剪辑、专业调色、Fusion视觉特效、Fairlight音频处理和成片交付整合在统一工程中。免费版已覆盖多数个人创作需求,