之前在做 AI 代码生成的质量治理时我一直在想一个问题LLM 生成代码的速度越来越快但怎么让“快”和“稳”同时成立限制提示词只是最外层的手段真正想约束生成结果还得回到代码本身的结构。Argot 这个项目给了我一个很有意思的答案把 AI guardrail 建立在代码库自身的 AST 模式上用 Rust 来做规则解析和匹配。这篇文章我会从 AI guardrail 的常见痛点切入拆解 AST 模式匹配的基本原理并带大家动手写一个最小的“AST 风格 guardrail 检查器”Demo。适合正在做 AI 编码工具落地、想了解代码结构校验思路或者对 Rust 生态中 syn/quote 感兴趣的同学阅读。1. 背景与核心概念1.1 AI guardrail 为什么越来越重要在 AI 辅助编程的场景里guardrail 通常指“护栏”或“安全边界”本质是一组规则用来限制模型输出内容的范围、格式和风险。如果我们把一次 AI 代码生成请求拆开看流程大致是用户输入需求 - 模型输出代码 - 工具把代码写入工程。在这里guardrail 可以出现在两个位置生成前通过系统提示词约束模型要求它“只输出 Rust 代码”“不要引入未声明的依赖”。生成后对模型输出做静态检查比如编译、lint、测试再决定是否接受这段代码。很多团队只做了前者效果并不理想。原因很直接提示词是软约束模型可能“理解”规则但不一定“遵循”规则。你觉得你已经写清楚了“不要用 unwrap”结果模型还是给你生成了一堆带 unwrap 的示例代码然后你还要在 review 的时候手动挑出来。生成后检查是目前更可靠的方案。但传统 lint 工具面向的是“人写的代码”场景规则本身是通用的不一定能感知某个具体代码库的风格或约定。比如这个代码库要求所有错误必须走自定义的 AppError不允许直接返回 anyhow::Error。这个代码库禁止在业务模块中出现 unwrap只在测试代码里允许。这个代码库要求所有对外 API 的入参必须实现某个 trait。这些规则用通用 lint 也能配但配置成本高而且只能在工程级别生效。AI 工具每次生成的是“一小段”代码我们需要更细粒度的、能嵌到流程里的检查方式。这正是基于 AST 模式的 guardrail 能发挥价值的地方。1.2 AST 模式匹配是什么ASTAbstract Syntax Tree抽象语法树是源代码的树形表示。它会去掉空格、换行等纯格式信息保留代码的结构信息。比如下面这段 Rust 代码let x value.unwrap();如果把它解析成 AST大致结构是一个 Let 语句左侧是标识符 x右侧是一个方法调用表达式方法调用表达式的接收者是 value方法名是 unwrap没有参数AST 模式匹配就是在这个树上描述“我要找什么样的结构”。不是去匹配字符串而是匹配“一个方法调用名字叫 unwrap接收者是某个表达式”。这样做有几个好处不受空格、换行、注释影响。能精确区分函数调用、方法调用、宏调用。能结合类型信息做更复杂的判断如果解析器支持。对比一下正则表达式正则更适合在“代码文本”里找特征但很容易误判。比如注释里的 unwrap、字符串里的 unwrap都会被正则命中。AST 就不会因为注释和字符串在 AST 里是不同类型的节点。1.3 Argot 的核心思路Argot 这个名字来自程序设计语言里的“行话、黑话”概念。它想做的事情是让 guardrail 规则直接来自代码库本身的 AST 模式。也就是说不是让维护者去写“不要用 unwrap”这种自然语言规则而是从代码库里提取真实的 AST 模式找出那些“曾经出过问题的代码结构”“被禁止的 API 调用方式”“必须遵循的封装修饰方式”然后把这些结构描述成模式规则。这样做的意义在于规则和代码库的真实形态高度一致。比如你的代码库里所有错误处理都长这样fn handle() - Result(), AppError { do_something().map_err(AppError::from)?; Ok(()) }那么 guardrail 就可以把 AppError、map_err、? 这个组合描述成一个“期望模式”。如果生成的代码变成fn handle() - Result(), AppError { do_something().unwrap(); Ok(()) }模式匹配就能立刻发现结构不一致因为出现了 unwrap 方法调用而不是 map_err 加 ?。这种思路在业务代码风格统一、AI 生成结果校验、大规模代码迁移质量保障等场景下都很实用。2. 环境准备与版本说明这一节我们先把 Rust 开发环境准备好。如果你之前已经装好 Rust可以直接跳过前半部分。2.1 安装 RustRust 官方推荐使用 rustup 管理工具链。在 Linux 或 macOS 上执行curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh在 Windows 上官方提供了 rustup-init.exe下载后按提示安装即可。需要注意的是Rust 工具链在 Windows 上默认使用 MSVC 构建工具如果你本机没有安装 Visual Studio Build Tools可能会遇到链接错误。这时有两个选择安装 Visual Studio Build Tools勾选 C 桌面开发工作负载。使用 GNU 工具链rustup toolchain install stable-x86_64-pc-windows-gnu并在项目里配置对应 toolchain。如果你在国内网络环境下安装速度很慢可以配置国内镜像源来加速。常用的做法是配置环境变量或修改 cargo 配置文件。下面给出一个常见的国内源配置示例具体镜像地址以你实际可用的为准# 文件路径$CARGO_HOME/config.toml 或 ~/.cargo/config.toml [source.crates-io] replace-with rsproxy [source.rsproxy] registry https://rsproxy.cn/crates.io-index [registries.rsproxy] index https://rsproxy.cn/crates.io-index [net] git-fetch-with-cli true注意不同镜像源的维护情况不同如果某个地址失效建议直接搜索最新的可用源。安装完成后在终端执行rustc --version cargo --version能正常输出版本号说明环境就绪。本文示例以 Rust 2021 edition 为基准使用 syn 2.x 来解析和遍历 AST。具体版本号在 Cargo.toml 中可以这样声明[dependencies] syn { version 2, features [full, visit] } quote 1 proc-macro2 1如果你拉取依赖时发现版本冲突可以把版本号放宽为2.0或1的兼容范围具体以你本地 cargo 解析结果为准。2.2 IDE 或编辑器建议写 Rust 代码我建议使用 VS Code 搭配 rust-analyzer 插件或者使用 JetBrains 家的 CLion / IntelliJ IDEA 加 Rust 插件。rust-analyzer 能提供补全、跳转、类型信息查看 AST 结构时也可以借助它来定位代码位置。查看 AST 结构还有一个常用的方式把代码写在一个小示例里用syn::parse_file解析然后打印调试输出。这个我们后面的实战部分会用到。2.3 示例项目结构为了演示 AST 模式匹配的 guardrail 思路我准备创建一个独立的 Rust 项目实现一个“最小版 AST 检查器”。它的功能是读取一个 Rust 源文件。用 syn 解析成 AST。遍历 AST查找指定的 AST 模式比如unwrap方法调用。如果发现命中输出警告并返回非零退出码。这个 Demo 虽然不能直接等价于 Argot 的全部功能但它能帮助你理解 Argot 这类工具背后的核心机制。理解了机制再看 Argot 的规则定义和使用方式时会顺畅很多。项目结构如下ast-guardrail-demo/ ├── Cargo.toml ├── src/ │ ├── main.rs │ ├── rule.rs │ └── visitor.rs └── examples/ └── sample.rsmain.rs入口解析命令行参数读取源文件。rule.rs定义规则结构体比如规则名、匹配模式描述。visitor.rs实现 AST 遍历逻辑完成模式匹配。examples/sample.rs一个待检查的 Rust 示例文件。3. 核心原理拆解AST 模式匹配的关键点3.1 syn 库的基本用法syn 是 Rust 生态里最常用的语法解析库。它能把 Rust 代码解析成 AST并且提供了visit模块方便我们遍历 AST 节点。先看一个最简单的例子解析一个字符串形式的 Rust 文件并打印 AST 调试信息。use syn::parse_file; fn main() { let code r# fn main() { let x vec![1, 2, 3]; let first x.first().unwrap(); println!({:?}, first); } #; let ast parse_file(code).expect(failed to parse); println!({:#?}, ast); }在这个输出里你能清楚看到ExprMethodCall这样的节点里面记录了方法名、接收者表达式、参数列表等元素。模式匹配要做的就是在这些节点上做判断。注意syn 默认解析的是“语法上合法的 Rust 代码”。如果 AI 生成的代码本身语法错误parse_file 会直接失败。此时 guardrail 应该明确报告“语法错误”而不是继续做模式匹配。3.2 规则描述的本质一个规则本质上就是一个“节点类型 条件集合”的描述。比如节点类型ExprMethodCall方法名等于unwrap接收者表达式类型等于ExprPath且路径最后一段是Result相关的值为了简单起见Demo 里我们用一种 JSON 形式来描述规则{ name: no-unwrap-in-business-code, kind: method_call, method: unwrap, level: error, message: business code should not use unwrap() }含义是匹配所有方法名为unwrap的方法调用。如果命中输出message内容级别为error。当然真实工具里的规则表达能力会更强比如支持“接收者类型是 Result”、支持“嵌套模式”、支持“排除测试模块”等。这里先用最简单的方式讲通原理。3.3 遍历 AST 的两种方式在 syn 里遍历 AST 有两种常见方式方式一手动匹配对已经解析出来的 AST自己写递归逻辑去判断每个节点类型。灵活但要写很多分支代码冗长。方式二使用 Visit traitsyn 提供visit模块里面定义了Visittrait。这个 trait 为每种 AST 节点类型都提供了一个访问方法比如visit_expr_method_call。我们只需要实现关心的那个方法在方法里写匹配逻辑即可。Demo 中使用方式二实现更简洁。use syn::visit::{self, Visit}; use syn::ExprMethodCall; pub struct UnwrapVisitor { pub hits: VecString, } implast Visitast for UnwrapVisitor { fn visit_expr_method_call(mut self, node: ast ExprMethodCall) { if node.method unwrap { self.hits.push(format!(line {}, node.method.span().start().line)); } visit::visit_expr_method_call(self, node); } }这里需要注意一点我们重写了visit_expr_method_call在里面先做自己的判断然后调用visit::visit_expr_method_call(self, node)继续往下遍历。如果不调用这个递归AST 里嵌套在该方法调用内部的节点就不会被访问到。举个具体场景foo().unwrap().unwrap();外层unwrap匹配后如果停止遍历内层unwrap就会漏掉。所以“先处理当前节点再递归子节点”是正确姿势。3.4 行号与定位信息在生成 guardrail 报告时让用户知道“哪一行代码有问题”很重要。syn 的 token 带有Span信息通过proc_macro2::Span可以获取行号和列号。上面示例里node.method.span().start().line获取的是unwrap方法名所在的行。这种方式比较粗糙但对大多数定位场景已经够用。更精细的做法是对整个方法调用表达式取 span但方法名的位置通常更接近用户真正关心的位置。需要注意Span 信息只有在“启用了 proc-macro2 的 span-locations 功能”时才能稳定获取到行号。所以在 Cargo.toml 里可以加上proc-macro2 { version 1, features [span-locations] }具体是否需要取决于你使用的 syn 版本和是否在 proc-macro 环境里运行。3.5 递归与性能考虑AST 遍历是线性扫描复杂度通常和节点数量成正比。对于一个几千行的小文件性能几乎可以忽略。但如果你的 guardrail 要跑在整个大型代码库上就需要考虑并行编译或并行遍历多个文件。只对变更文件做增量检查。对模式匹配结果做缓存。在 Demo 阶段不需要考虑这些但设计规则结构时保持简单能让你以后扩展并行处理更容易。4. 完整实战案例写一个最小 AST Guardrail Demo下面的代码是一个完整的可运行项目实现思路和 Argot 这类 AST guardrail 工具的原理一致。它不依赖任何 API 为编译时检查器仅作为核心机制演示。4.1 创建项目cargo new ast-guardrail-demo cd ast-guardrail-demo4.2 修改 Cargo.toml[package] name ast-guardrail-demo version 0.1.0 edition 2021 [dependencies] syn { version 2, features [full, visit] } quote 1 serde { version 1, features [derive] } serde_json 1 proc-macro2 { version 1, features [span-locations] }4.3 编写规则文件在项目根目录下创建一个目录rules里面放一个规则文件no-unwrap.json{ name: no-unwrap-in-business-code, kind: method_call, method: unwrap, level: error, message: business code should not use unwrap() }规则字段解释name规则唯一标识。kind要匹配的 AST 节点类型这里固定为method_call。method方法名。level问题级别可以是warning或error。message命中规则时的提示信息。这个规则文件结构很简单。真实项目的规则通常还会包含“排除路径”“作用域限定”“接收者类型”等更复杂的字段但核心思路一致。4.4 编写规则读取代码文件路径src/rule.rsuse serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct Rule { pub name: String, pub kind: String, pub method: String, pub level: String, pub message: String, } impl Rule { pub fn load_from_str(content: str) - ResultSelf, serde_json::Error { serde_json::from_str(content) } pub fn is_error(self) - bool { self.level error } }这里主要做了两件事定义规则结构体字段和 JSON 对应。提供从 JSON 字符串加载规则的方法。is_error方法用于判断当前规则是 error 级别还是 warning 级别后续决定退出码。4.5 编写 AST 访问器文件路径src/visitor.rsuse proc_macro2::Span; use syn::visit::{self, Visit}; use syn::ExprMethodCall; #[derive(Debug)] pub struct HitInfo { pub rule_name: String, pub line: usize, pub message: String, } pub struct MethodCallVisitora { pub rule_name: a str, pub target_method: a str, pub message: a str, pub hits: VecHitInfo, } impla MethodCallVisitora { pub fn new(rule_name: a str, target_method: a str, message: a str) - Self { MethodCallVisitor { rule_name, target_method, message, hits: Vec::new(), } } } implast, a Visitast for MethodCallVisitora { fn visit_expr_method_call(mut self, node: ast ExprMethodCall) { if node.method self.target_method { let start: Span node.method.span(); let line start.start().line; self.hits.push(HitInfo { rule_name: self.rule_name.to_string(), line, message: self.message.to_string(), }); } visit::visit_expr_method_call(self, node); } }这里核心逻辑在visit_expr_method_call判断当前方法调用节点的方法名是否等于目标方法。如果命中记录规则名、行号、提示信息。无论是否命中都继续递归遍历内部节点避免漏掉嵌套调用。关于node.method self.target_method这行判断需要说一点细节。node.method的类型是syn::Ident它和str可以直接做比较所以这里语义上等价于比较方法名的标识符文本。它不受空白、换行影响只比较真实的标识符。4.6 编写主程序文件路径src/main.rsmod rule; mod visitor; use rule::Rule; use std::fs; use std::process::ExitCode; use visitor::MethodCallVisitor; fn main() - ExitCode { let args: VecString std::env::args().collect(); if args.len() 3 { eprintln!(Usage: ast-guardrail-demo rule.json source.rs); return ExitCode::from(1); } let rule_path args[1]; let source_path args[2]; let rule_content match fs::read_to_string(rule_path) { Ok(content) content, Err(err) { eprintln!(failed to read rule file: {err}); return ExitCode::from(1); } }; let rule: Rule match Rule::load_from_str(rule_content) { Ok(rule) rule, Err(err) { eprintln!(failed to parse rule file: {err}); return ExitCode::from(1); } }; let source_content match fs::read_to_string(source_path) { Ok(content) content, Err(err) { eprintln!(failed to read source file: {err}); return ExitCode::from(1); } }; let ast match syn::parse_file(source_content) { Ok(ast) ast, Err(err) { eprintln!(failed to parse source file: {err}); return ExitCode::from(2); } }; let mut visitor MethodCallVisitor::new(rule.name, rule.method, rule.message); visitor.visit_file(ast); if visitor.hits.is_empty() { println!(OK: no match for rule {}, rule.name); ExitCode::SUCCESS } else { for hit in visitor.hits { let level if rule.is_error() { ERROR } else { WARN }; println!({level} {}:{} {}, hit.rule_name, hit.line, hit.message); } if rule.is_error() { ExitCode::from(1) } else { ExitCode::SUCCESS } } }主程序的逻辑顺序很清晰从命令行读取两个参数规则文件路径和待检查的源文件路径。读取并解析规则文件。读取待检查的源文件并用 syn 解析成 AST。创建 visitor遍历 AST。根据命中结果输出报告并决定退出码。需要说明的是这里的visitor.visit_file(ast)调用是Visittrait 提供的默认方法。它表示从文件根节点开始遍历整个 AST所有符合条件的节点都会进入我们的回调。4.7 准备一个待检查的示例文件文件路径examples/sample.rsuse std::fs; fn read_config(path: str) - String { let content fs::read_to_string(path).unwrap(); content } fn parse_value(raw: str) - i32 { raw.parse::i32().unwrap() } fn main() { let content read_config(config.txt); let value parse_value(content); println!(value: {value}); }这个文件故意包含两处unwrap()用来验证检查器是否都能找到。4.8 运行与验证在项目根目录执行cargo run -- rules/no-unwrap.json examples/sample.rs预期输出类似ERROR no-unwrap-in-business-code:3 business code should not use unwrap() ERROR no-unwrap-in-business-code:8 business code should not use unwrap()退出码此时是非零的表示检查未通过。如果我们把规则里的level改成warning退出码会变为 0但警告信息仍会打印。如果把examples/sample.rs改成不包含 unwrap 的版本输出就是OK: no match for rule no-unwrap-in-business-code退出码为 0。4.9 测试一个更复杂的模式为了验证 AST 模式不只是“找文本”我们可以再设计一个规则检查代码中是否出现了某个特定函数调用链。比如不允许在Result上直接调用ok().unwrap()这样的组合。这个规则用字符串匹配很容易被注释、字符串字面量干扰但 AST 遍历不会。你只需要在 visitor 里同时监听ExprMethodCall然后在递归时判断“外层方法是 unwrap接收者的方法名是 ok”。这就是 AST 模式匹配相对字符串搜索的核心优势你能精确处理“结构”而不是“文本”。由于 Demo 的规则结构目前只支持单一方法名暂不展开这个高级写法。但它说明了一个方向规则能力可以不断扩进从“禁止调用某个方法”扩展到“禁止某种调用链”“禁止某种嵌套结构”。5. 常见问题与排查思路在实际使用或二次开发这类工具时最容易遇到下面几个问题。问题现象常见原因解决思路解析失败failed to parse source file待检查的 Rust 代码语法不完整比如只有代码片段而没有函数包裹先用代码补全或格式化工具处理或改用syn::parse_str解析表达式级别的输入检查结果为空但代码里明显有问题规则结构或方法名写错比如把unwrap写成unwrap_先打印 AST 调试信息确认目标节点类型和方法名只匹配到最外层调用内层调用漏检visitor 里没有调用visit::visit_expr_method_call(self, node)在回调方法末尾补充递归调用cargo build 很慢依赖较多且可能默认使用 crates.io 源配置国内镜像源或者使用 workspace 缓存输出行号不准确没有启用proc-macro2的span-locations功能在 Cargo.toml 里加上对应 feature误报太多规则过于宽泛没有限定接收者类型或上下文增加规则的过滤条件比如限定函数名、模块路径、代码目录大型代码库跑得慢每次全量解析所有文件在 CI 里做增量检查只检查变更文件或并行处理下面针对几个高频问题展开说。5.1 解析失败时怎么办syn 的parse_file要求输入是一整个 Rust 文件。如果 AI 生成的是一段代码片段比如只生成了一个impl块直接丢给parse_file会报错。这时可以在上层先做“代码补全”把片段包进一个虚拟的fn或mod里。或者改用syn::parse_str配合表达式类型解析。但这个 Demo 只演示文件级解析足够你理解机制。真实的 AI 工具链路里往往还需要做“片段上下文拼接”让模型输出和已有文件合并后再检查。5.2 为什么匹配不到目标方法一个很常见的原因是你用的不是方法调用而是函数调用或宏调用。unwrap(value); // 函数调用不是方法调用 value.unwrap(); // 方法调用 unwrap!(value); // 宏调用这三者在 AST 里是完全不同的节点。Demo 的visit_expr_method_call只会命中第二种。如果你要匹配宏调用需要实现visit_macro函数调用则是visit_expr_call。所以遇到“匹配不到”的情况第一步是先打印 AST看清楚目标代码到底长什么样。5.3 如何降低误报AST 模式匹配比字符串匹配精准但它仍然可能误报。比如代码库里某个unwrap出现在测试模块里而你的规则目标只是“业务代码”。此时规则需要增加上下文条件比如所在函数是否带有#[test]属性。所在模块路径是否包含tests。所在文件路径是否以_test.rs结尾。在 Demo 里这些条件没有实现但设计 Rule 结构时预留了扩展余地。你可以给 Rule 增加一个context字段在 visitor 里访问父模块或函数信息来判断。需要注意的是syn 的 Visit 机制不会自动告诉你“当前节点属于哪个函数”。要获取这种上下文信息通常需要做“祖先节点栈”维护在进入函数时压栈在退出时弹栈。这是 AST 工具进阶开发的常见技巧但复杂度会明显上升。项目初期可以先用文件路径、模块路径做粗粒度过滤。6. 最佳实践与工程建议把这套思路用在真实项目里时有几条工程建议特别值得强调。6.1 规则要小而明确不要试图用一条规则描述清“高质量代码”这种宏大命题。规则越具体越容易落地。比如“业务模块中禁止调用unwrap()”“请求处理函数必须返回Result”“对数据库操作必须使用事务包装”这些规则的共同点是结构特征清晰容易用 AST 表达。相比之下“代码要优雅”“函数不要太长”这类规则就很难用 AST 模式可靠表达。6.2 guardrail 不是安全边界这是尤其要强调的一点。AST guardrail 能做的是“代码质量治理”它不能替代安全审查。即使所有规则都通过也不能说明代码没有安全问题。在实际项目中建议把 guardrail 定位成“第一道自动筛查”安全审计、人工 review 仍然必须保留。另外守卫规则本身也要被 review。因为规则是代码的一部分它会有 bug会过期会误伤正确的代码。不能因为“机器检查过”就认为结果一定正确。6.3 与 CI 集成的姿势在 CI 里跑这类检查器时建议让它和编译检查并行执行不阻塞编译。对 warning 级别结果先观察一段时间确认无误报后提升为 error。规则变更走代码 review 流程不要直接改线上规则。输出格式要可机器解析方便 CI 汇总。如果是 AI 生成代码的链路应该把 guardrail 放在生成后、合入前的自动检查阶段。发现命中时可以自动触发“重新生成”或“带错误信息回退给模型修正”而不是简单丢弃结果。这个“检查-反馈-重试”的闭环才是 AI 编码工具质量提升的关键。6.4 预留豁免机制任何代码治理工具都需要有豁免通道。总会有些场景确实是合理的例外。比如在原型代码中允许使用 unwrap。在测试代码中允许使用 unwrap。在某些性能敏感路径中必须使用不安全代码。如果没有豁免机制开发者会用“静默忽略”来绕过检查反而更危险。正确做法是设计显式的豁免语法比如在代码里写一个特殊的注释标记guardrail 识别后跳过。甚至可以做成“豁免必须在规则管理后台登记超过约定期限自动过期”避免豁免被永久滥用。6.5 关注可观测性当 guardrail 被嵌入 AI 生成链路后它的行为本身也需要被观测规则命中率是多少哪些规则经常触发模型是否在按照反馈修正结果是否有大量“检查无问题但用户不采纳”的情况这些数据能帮你判断规则是否有效以及模型生成质量的演进趋势。工具只是手段最终目标是让 AI 协作开发的过程更可控。6.6 从规则库到治理平台单个规则文件比较轻量适合团队内部快速使用。当规则数量增多并且需要让不同项目复用同一套规则时建议把规则做成“规则库”集中管理。规则文件本身就是代码资产可以进 Git可以做版本化也可以做发布。更进一步你可以给规则增加“自动生成”能力从代码库的历史提交里提取“被 revert 的代码片段”“被 review 打回的改动”自动生成 AST 模式并推荐给维护者。这就是 Argot 这类项目未来值得探索的方向。7. 总结与下一步这篇文章从一个具体项目 Argot 出发梳理了 AI guardrail 的需求背景、AST 模式匹配的核心概念并用一个可运行的 Rust Demo 演示了“解析代码 - 遍历 AST - 匹配模式 - 输出报告”的完整链路。如果你之前没接触过 syn 和 AST现在应该能回答这几个问题AI guardrail 能做什么不能做什么。为什么 AST 模式匹配优于字符串匹配。一个最简单的 AST 模式检查器由哪几个模块组成。递归遍历时为什么要调用visit::visit_expr_method_call。把检查器嵌入 CI 或 AI 生成链路时要注意什么。下一步可以试试这些方向给 Demo 增加“多规则支持”用一个目录装载多个 JSON 规则遍历时逐个执行。增加“上下文判断”比如只在特定函数或模块内检查。尝试解析调用链匹配ok().unwrap()这样的组合模式。把规则命中后的建议信息同时输出方便接入 AI 修码工具。尝试用syn的fold功能做自动重写命中规则后不仅报错还能用 quote 生成替代代码。如果这篇文章对你有帮助可以收藏备用后面用到 Rust AST 处理或者 AI 代码质量治理时可以翻出来参考。动手改一改 Demo你会发现自己对“代码结构”的理解会上一个台阶。