AI 驱动的 Rust 项目架构推荐:基于代码仓库分析的模块划分建议

cover

一、Rust 项目架构的痛点:从单文件到多 crate 的迷茫期

学 Rust 的时候,第一个项目通常是单文件 main.rs,所有代码都塞在一个文件里。随着功能增长,开始拆模块——mod networkmod storagemod utils。再往后,项目越来越大,模块之间的依赖关系变得混乱:network 依赖 storagestorage 又依赖 network 中的某个类型,形成循环依赖。

Rust 的模块系统比大多数语言更严格:循环依赖在模块级别不允许(但可以通过 crate 级别拆分解决),pub 可见性控制比 Java 的包级可见性更细粒度。这些限制是好事——它们强制你在项目早期就思考架构。但对于非科班转码的学习者来说,"怎么拆模块"本身就是一个没有标准答案的难题。

AI 驱动的项目架构推荐方案,通过分析代码仓库的结构和依赖关系,自动识别架构问题(循环依赖、模块过大、职责不清),并给出模块拆分和 crate 重组的建议。这不是替代架构设计,而是为缺乏经验的开发者提供一个"架构检查点"。

二、代码仓库分析与架构推荐的底层机制

2.1 代码仓库的依赖图构建

分析的第一步是构建代码仓库的模块依赖图。每个 Rust 模块是一个节点,模块之间的 use 语句是边。依赖图可以揭示三种架构问题:

  • 循环依赖:模块 A 依赖 B,B 又依赖 A。Rust 的模块系统不允许循环依赖,但通过 pub use 重导出可能产生隐式的循环引用。
  • 模块过大:单个模块的代码行数或公开 API 数量过多,职责不清晰。
  • 依赖过深:某个模块被大量其他模块依赖,修改它的影响范围广。
flowchart TD
    A[扫描 src/ 目录] --> B[解析 mod 声明和 use 语句]
    B --> C[构建模块依赖图]
    C --> D[检测循环依赖]
    C --> E[统计模块大小]
    C --> F[计算依赖深度]
    D --> G[架构问题汇总]
    E --> G
    F --> G
    G --> H[LLM 生成重构建议]
    H --> I[输出模块拆分方案]
    H --> J[输出 crate 重组方案]

    subgraph 依赖图示例
        K[main → network]
        L[network → storage]
        M[storage → network]
        N[循环依赖!]
    end

2.2 模块职责推断

通过分析模块中公开函数和结构体的命名模式,推断模块的职责。例如:

  • 包含 connectsendreceive 等函数的模块 → 网络通信职责
  • 包含 saveloadquery 等函数的模块 → 数据存储职责
  • 包含 parsevalidatetransform 等函数的模块 → 数据处理职责

当模块中混合了多种职责的函数时,说明模块职责不清晰,需要拆分。

2.3 Crate 重组建议

当项目规模超过一定阈值(通常 1 万行以上),应该考虑将模块拆分为独立的 crate。Cargo Workspace 允许多个 crate 共享一个 Cargo.lock 和构建缓存,同时保持各 crate 的独立编译和版本管理。

Crate 拆分的原则:

  • 核心库:不依赖任何外部 crate 的基础数据结构和工具函数。
  • 业务逻辑库:依赖核心库,实现具体的业务规则。
  • 接口层:依赖业务逻辑库,提供 CLI/HTTP/gRPC 等外部接口。

三、Rust 生产级代码实现

3.1 模块依赖图构建

use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

/// 模块依赖图
pub struct DependencyGraph {
    /// 模块名 → 依赖的模块集合
    edges: HashMap<String, HashSet<String>>,
    /// 模块名 → 源文件路径
    module_files: HashMap<String, PathBuf>,
    /// 模块名 → 代码行数
    module_lines: HashMap<String, usize>,
}

impl DependencyGraph {
    pub fn new() -> Self {
        Self {
            edges: HashMap::new(),
            module_files: HashMap::new(),
            module_lines: HashMap::new(),
        }
    }

    /// 从项目目录构建依赖图
    pub fn build_from_dir(&mut self, src_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
        self.scan_modules(src_dir, "")?;
        Ok(())
    }

    /// 递归扫描模块
    fn scan_modules(
        &mut self,
        dir: &Path,
        parent_module: &str,
    ) -> Result<(), Box<dyn std::error::Error>> {
        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() {
                // 检查是否有 mod.rs
                let mod_rs = path.join("mod.rs");
                if mod_rs.exists() {
                    let module_name = path.file_name()
                        .unwrap()
                        .to_str()
                        .unwrap()
                        .to_string();
                    let full_name = if parent_module.is_empty() {
                        module_name.clone()
                    } else {
                        format!("{}::{}", parent_module, module_name)
                    };

                    self.module_files.insert(full_name.clone(), mod_rs.clone());
                    self.parse_dependencies(&mod_rs, &full_name)?;
                    self.count_lines(&mod_rs, &full_name);

                    // 递归扫描子模块
                    self.scan_modules(&path, &full_name)?;
                }
            } else if path.extension().map_or(false, |e| e == "rs") {
                let file_name = path.file_stem()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string();

                if file_name == "mod" || file_name == "main" || file_name == "lib" {
                    continue;
                }

                let full_name = if parent_module.is_empty() {
                    file_name.clone()
                } else {
                    format!("{}::{}", parent_module, file_name)
                };

                self.module_files.insert(full_name.clone(), path.clone());
                self.parse_dependencies(&path, &full_name)?;
                self.count_lines(&path, &full_name);
            }
        }
        Ok(())
    }

    /// 解析文件中的 use 语句,提取模块依赖
    fn parse_dependencies(
        &mut self,
        file_path: &Path,
        module_name: &str,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let content = std::fs::read_to_string(file_path)?;
        let mut deps = HashSet::new();

        for line in content.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with("use ") {
                // 简化解析:提取 use crate::module::... 中的模块名
                if let Some(dep) = self.extract_module_from_use(trimmed) {
                    deps.insert(dep);
                }
            }
        }

        self.edges.insert(module_name.to_string(), deps);
        Ok(())
    }

    /// 从 use 语句中提取模块名
    fn extract_module_from_use(&self, use_stmt: &str) -> Option<String> {
        // use crate::network::tcp::Connection → network
        // use super::storage::Repository → storage
        let stmt = use_stmt.trim_start_matches("use ")
            .trim_end_matches(';')
            .trim();

        if stmt.starts_with("crate::") {
            let parts: Vec<&str> = stmt["crate::".len()..].split("::").collect();
            if !parts.is_empty() {
                return Some(parts[0].to_string());
            }
        } else if stmt.starts_with("super::") {
            let parts: Vec<&str> = stmt["super::".len()..].split("::").collect();
            if !parts.is_empty() {
                return Some(parts[0].to_string());
            }
        }
        None
    }

    fn count_lines(&mut self, file_path: &Path, module_name: &str) {
        if let Ok(content) = std::fs::read_to_string(file_path) {
            self.module_lines.insert(
                module_name.to_string(),
                content.lines().count(),
            );
        }
    }
}

3.2 架构问题检测

/// 架构问题
#[derive(Debug)]
pub enum ArchitectureIssue {
    /// 循环依赖
    CircularDependency {
        path: Vec<String>,
    },
    /// 模块过大
    OversizedModule {
        module: String,
        lines: usize,
        threshold: usize,
    },
    /// 依赖过深(被过多模块依赖)
    HighFanIn {
        module: String,
        dependents: usize,
        threshold: usize,
    },
}

/// 架构分析器
pub struct ArchitectureAnalyzer {
    graph: DependencyGraph,
    module_size_threshold: usize,
    fan_in_threshold: usize,
}

impl ArchitectureAnalyzer {
    pub fn new(graph: DependencyGraph) -> Self {
        Self {
            graph,
            module_size_threshold: 500,  // 超过 500 行视为过大
            fan_in_threshold: 5,          // 被超过 5 个模块依赖视为高扇入
        }
    }

    /// 检测所有架构问题
    pub fn detect_issues(&self) -> Vec<ArchitectureIssue> {
        let mut issues = Vec::new();

        // 1. 检测循环依赖
        issues.extend(self.detect_cycles());

        // 2. 检测模块过大
        for (module, lines) in &self.graph.module_lines {
            if *lines > self.module_size_threshold {
                issues.push(ArchitectureIssue::OversizedModule {
                    module: module.clone(),
                    lines: *lines,
                    threshold: self.module_size_threshold,
                });
            }
        }

        // 3. 检测高扇入
        let fan_in = self.compute_fan_in();
        for (module, count) in fan_in {
            if count > self.fan_in_threshold {
                issues.push(ArchitectureIssue::HighFanIn {
                    module: module.clone(),
                    dependents: count,
                    threshold: self.fan_in_threshold,
                });
            }
        }

        issues
    }

    /// 检测循环依赖(DFS)
    fn detect_cycles(&self) -> Vec<ArchitectureIssue> {
        let mut visited = HashSet::new();
        let mut path = Vec::new();
        let mut cycles = Vec::new();

        for module in self.graph.edges.keys() {
            self.dfs_find_cycle(
                module, &mut visited, &mut path, &mut cycles,
            );
        }

        cycles.into_iter().map(|path| {
            ArchitectureIssue::CircularDependency { path }
        }).collect()
    }

    fn dfs_find_cycle(
        &self,
        current: &str,
        visited: &mut HashSet<String>,
        path: &mut Vec<String>,
        cycles: &mut Vec<Vec<String>>,
    ) {
        if path.contains(&current.to_string()) {
            // 找到循环
            let cycle_start = path.iter().position(|p| p == current).unwrap();
            let cycle: Vec<String> = path[cycle_start..].to_vec();
            cycles.push(cycle);
            return;
        }
        if visited.contains(current) {
            return;
        }

        visited.insert(current.to_string());
        path.push(current.to_string());

        if let Some(deps) = self.graph.edges.get(current) {
            for dep in deps {
                self.dfs_find_cycle(dep, visited, path, cycles);
            }
        }

        path.pop();
    }

    /// 计算每个模块的扇入(被多少模块依赖)
    fn compute_fan_in(&self) -> HashMap<String, usize> {
        let mut fan_in: HashMap<String, usize> = HashMap::new();

        for (_, deps) in &self.graph.edges {
            for dep in deps {
                *fan_in.entry(dep.clone()).or_insert(0) += 1;
            }
        }

        fan_in
    }
}

3.3 LLM 生成重构建议

/// 架构建议生成器
pub struct SuggestionGenerator {
    llm_client: LlmClient,
}

impl SuggestionGenerator {
    pub fn new(llm_client: LlmClient) -> Self {
        Self { llm_client }
    }

    pub async fn generate(
        &self,
        issues: &[ArchitectureIssue],
        graph: &DependencyGraph,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let issues_desc: Vec<String> = issues.iter().map(|issue| {
            match issue {
                ArchitectureIssue::CircularDependency { path } => {
                    format!("循环依赖: {}", path.join(" → "))
                }
                ArchitectureIssue::OversizedModule { module, lines, threshold } => {
                    format!(
                        "模块过大: {} ({} 行,阈值 {} 行)",
                        module, lines, threshold
                    )
                }
                ArchitectureIssue::HighFanIn { module, dependents, threshold } => {
                    format!(
                        "高扇入: {} (被 {} 个模块依赖,阈值 {})",
                        module, dependents, threshold
                    )
                }
            }
        }).collect();

        let prompt = format!(
            "你是一个 Rust 项目架构专家。以下是项目架构分析发现的问题:\n\n{}\n\n\
            模块依赖关系:{:?}\n\n\
            模块大小:{:?}\n\n\
            请给出具体的重构建议,包括:\n\
            1. 如何解决循环依赖\n\
            2. 如何拆分过大的模块\n\
            3. 是否需要拆分为多个 crate\n\
            4. 推荐的 Cargo Workspace 结构",
            issues_desc.join("\n"),
            graph.edges,
            graph.module_lines,
        );

        let response = self.llm_client.chat(&prompt).await?;
        Ok(response)
    }
}

四、Trade-offs:AI 架构推荐的局限

4.1 依赖分析的精度

基于 use 语句的依赖分析只能发现显式依赖,无法发现运行时依赖(如通过 trait object 的动态分发)。此外,use super::* 这样的通配符导入会导致依赖关系模糊。更精确的分析需要使用 rust-analyzer 的语义分析能力,但这增加了实现的复杂度。

4.2 重构建议的落地难度

LLM 生成的重构建议可能理论正确但落地困难。例如,"将 network 模块拆分为 network-core 和 network-protocol 两个 crate"——这个建议听起来合理,但实际拆分可能涉及数百处 use 语句的修改和 API 重新设计。建议需要配合具体的修改步骤和渐进式重构方案。

4.3 适用边界

AI 架构推荐适用于以下场景:项目超过 5000 行、模块数量超过 10 个、存在明显的架构问题(循环依赖、模块过大)。不适用于:小型项目(手动审查更高效)、架构已经成熟的项目(AI 建议可能破坏现有设计)、团队有资深架构师(人工判断更准确)。

五、总结

AI 驱动的项目架构推荐,为缺乏架构经验的开发者提供了一个"架构检查点"。核心落地步骤如下:

  1. 构建模块依赖图:扫描 src/ 目录,解析 use 语句,构建模块间的依赖关系。
  2. 检测架构问题:循环依赖、模块过大、高扇入,三个维度自动检测。
  3. LLM 生成建议:将问题汇总后交给 LLM,生成具体的重构方案。
  4. 渐进式重构:不要一次性重构,先解决最严重的循环依赖,再逐步拆分过大模块。
  5. 持续监控:在 CI 中集成架构分析,每次提交都检查是否引入新的架构问题。

架构不是一次性的设计,而是持续的演进。AI 的价值在于提供"架构体检"的自动化能力,让你在项目早期就发现和解决问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐