09 | 重构项目结构

09 | 重构项目结构

项目地址:github.com/frontzhm/n2...

每一步对应的完整代码都在仓库里,跟着文档卡住了就去翻源码。

这是一篇系列文,请按顺序阅读。

本文目标

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

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

功能逐渐完整以后,原来的目录开始暴露出一些问题:

  • scripts/rebuild_metadata.py 同时承担配置校验、数据读取、实体构造、向量生成、数据库写入和客户端关闭
  • 多个 Service 只有一行 Repository 转发,没有真正的业务逻辑
  • Elasticsearch 的 Mapping 和 Bulk 细节写在 Service 中,应用层知道了太多存储实现
  • FastAPI 路由、请求解析和应用创建都挤在根目录 main.py
  • LangGraph 的 State、节点、Graph 声明和演示代码都在一个文件
  • 应用配置使用多层 dict,配置字段拼错只能运行时发现
  • 没有自动化测试,重构后只能手动执行破坏性的全量同步验证

本文不增加新业务功能,而是完成一次结构重构:

让每层代码只承担一种主要职责,让依赖方向更清晰,并为后面的召回和 SQL 生成阶段留出扩展空间。

重构完成后,已有功能和启动方式保持兼容。


1. 为什么现在需要重构

项目刚开始时,代码少,把逻辑直接写进脚本最快:

text 复制代码
读取 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. 重构前后的目录对比

重构前的核心目录:

text 复制代码
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

重构后的核心目录:

text 复制代码
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/

每层的主要职责:

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

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


3. 第一步:增加 Application 层

旧的 scripts/rebuild_metadata.py 直接编排全部流程。重构后,把真正的业务用例放进:

text 复制代码
app/application/metadata_rebuild_service.py

核心类:

python 复制代码
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 服务

完整重建入口变成:

python 复制代码
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(...)

只看这个方法,就能理解完整业务流程:

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

这就是 Application Service 的价值:

它描述"系统要完成什么用例",而不是描述某个 SDK 的调用细节。

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

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

因此重建过程拆成两个阶段。

4.1 准备阶段

python 复制代码
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 写入阶段

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

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

这样不能实现三个数据库之间的真正原子事务,但可以避免大量"前置计算失败导致旧数据提前丢失"的情况。

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

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

当前项目处于生成和学习阶段,先采用"准备成功后再重建"的方案。

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

旧代码中存在这样的 Service:

python 复制代码
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。

调用链反而变长:

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

删除纯透传 Service 后:

python 复制代码
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)

现在调用链是:

text 复制代码
MetadataRebuildService
→ Repository
→ Mapper
→ Model

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

这里要注意:

不是 Service 文件越多,架构就越好。

只有当一层确实包含独立职责时,它才值得存在。

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

重构前:

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

两者抽象标准不一致。

重构后新增:

text 复制代码
app/repositories/dim_value_repository.py

Repository 负责 ES 存储细节:

python 复制代码
class DimValueRepository:
    async def reset_index(self) -> None:
        ...

    async def index_values(self, documents) -> int:
        ...

    async def refresh(self) -> None:
        ...

Application Service 只负责调用顺序:

python 复制代码
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

现在职责更加一致:

text 复制代码
DimValueSyncService
└─ 编排:reset → index → refresh

DimValueRepository
└─ 实现:Index Mapping、Bulk 格式、稳定 UUID、错误解析

ESClient
└─ 连接:官方异步 SDK、地址、超时

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

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

重构后的:

text 复制代码
scripts/rebuild_metadata.py

只负责三件事:

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

核心入口:

python 复制代码
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 请求
  • 怎样控制业务写入顺序

运行方式:

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

8. 第六步:拆分 FastAPI API 层

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

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

重构后:

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

请求模型:

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

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

查询路由:

python 复制代码
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",
    )

应用工厂:

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


app = create_app()

推荐启动方式:

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

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

shell 复制代码
uv run fastapi dev main.py

9. 第七步:拆分 Agent

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

重构后:

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

9.1 state.py

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

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

例如关键词节点只返回:

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

9.2 nodes.py

这里放节点的具体行为:

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

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

9.3 graph.py

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

python 复制代码
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. 第八步:把应用配置改成强类型

旧代码使用多层字典:

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

如果写成:

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

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

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

python 复制代码
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)

总配置:

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

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

python 复制代码
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. 第九步:补充自动化测试

重构最危险的情况是:目录看起来更整齐,但业务行为已经改变。

因此新增:

text 复制代码
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

python 复制代码
points = await service.prepare_columns("columns", columns)

self.assertEqual(repository.calls, [])

只有执行:

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

才允许调用 reset_collectionupsert

11.2 ES 写入顺序

text 复制代码
reset_index
→ index_values
→ refresh

11.3 配置引用校验

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

11.4 API 参数校验

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

运行测试:

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

当前结果:

text 复制代码
Ran 8 tests

OK

12. 重构后的依赖方向

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

text 复制代码
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

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

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

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

13. 哪些目录没有继续拆

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

保留了:

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

没有继续增加:

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

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

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

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

等到真的出现以下需求时再增加接口:

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

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

14. 验证重构结果

14.1 编译检查

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

14.2 单元测试

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

14.3 启动 API

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

访问:

text 复制代码
http://localhost:8000/hello
http://localhost:8000/docs

14.4 完整重建

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

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

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

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

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


科普项目架构

1. 什么是项目架构

项目架构不是目录树本身,也不是文件夹名字是否"高级"。

架构描述的是:

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

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

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

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

不能只通过目录名判断架构是否合理,要继续查看实际依赖和行为。

2. 什么是分层架构

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

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

常见理解:

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

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

高层业务流程不要被底层技术细节淹没。

3. 什么是关注点分离

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

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

例如:

text 复制代码
QueryRequest
→ 负责 HTTP 输入校验

MetadataRebuildService
→ 负责同步流程

DimValueRepository
→ 负责 ES Bulk 格式

ESClient
→ 负责连接 Elasticsearch

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

4. 什么是依赖注入

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

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

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

而不是在 Service 内部写死:

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

通过外部传入依赖后:

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

5. 什么是组合根

组合根,英文是 Composition Root。

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

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

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

text 复制代码
scripts/rebuild_metadata.py

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

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

6. 什么是 Repository

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

python 复制代码
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 表达一个完整用例,例如:

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

它通常负责:

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

它通常不负责:

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

8. 什么是重构

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

本次重构后,用户仍然可以:

shell 复制代码
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. 单元测试与集成测试有什么区别

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

text 复制代码
FakeEmbeddingClient
FakeQdrantRepository
FakeDimValueRepository

优点:

  • 稳定
  • 失败原因明确
  • 不会清空真实数据

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

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

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

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

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

11. 架构需要一直重构吗

不需要。

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

text 复制代码
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. 保留轻量结构,避免继续过度设计

重构后的核心原则可以概括为:

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

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

相关推荐
ZhengEnCi2 小时前
AI Agent(AI智能体) 记忆管理系统设计 — 从向量库边界到生产级 Memory(记忆) 架构
人工智能
凉菜lc2 小时前
对比文《IM Bot 框架怎么选?Zhin vs Koishi vs NoneBot》
人工智能
阿里云大数据AI技术2 小时前
DataWorks Data Agent 实战课堂(三):对话式完成数据同步与智能诊断
人工智能·agent
骇客野人2 小时前
AI全栈开发指定技术架构
人工智能
Escape3 小时前
为什么你的 AI 越聊越傻?从 Token 到 Agent,彻底搞懂 AI Agent的秘密㊙️
前端·人工智能·后端
Sisphusssss3 小时前
香橙派5plus GPIO
linux·python·ubuntu
2501_926978333 小时前
认知边缘的双向耦合:从核技术类比到智力货币时代
人工智能·经验分享·笔记·ai写作
SQDN3 小时前
Chatbox 1.22.1 获取不到模型?先验 /models,再对齐精确 model ID
人工智能·测试工具·机器学习·chatgpt·json
颜酱3 小时前
08 | 把维度值同步到 Elasticsearch(生成阶段)
人工智能·python·langchain