news 2026/9/13 4:05:20

LLM生成Python代码库的分层审计:从AST扫描到CI集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM生成Python代码库的分层审计:从AST扫描到CI集成

大约从去年开始,我观察到越来越多团队的代码库里开始出现一批"风格高度统一"的 Python 文件:函数命名规范、注释完整、 docstring 齐全,但整体结构透着一股"生成感"。这些代码不是某位高级工程师手写的,而是由 Cursor、Copilot、Codex、Claude 等工具批量产出的。

代码能跑,测试也能过,但问题往往藏在后面:依赖版本为什么这样锁?这个异常为什么被吞掉了?这条 import 链为什么会循环?当你在 code review 里连问三个"为什么"却没人能回答的时候,LLM 生成代码带来的工程债就已经开始积累了。

这篇文章要讨论的,是专门用来"快速步进式审查 LLM 生成 Python 代码库"的工具思路。它解决的不是"有没有 bug"这种初级问题,而是"这个代码库是怎么被构建出来的、哪些地方不值得信任、哪些地方必须人工复核"这层更深的问题。读完你会理解这类审计工具的设计逻辑,也能亲手搭起一套最小可用的审计脚本,把它接入自己的项目流程。

1. 为什么 LLM 生成的代码库需要专门的审计工具

如果只是审查几段 AI 生成的函数,用肉眼加 IDE 的警告就足够了。但当一个完整的代码库——几十个文件、几千行代码——都是由 LLM 生成并逐步拼接而成时,问题性质就变了。

第一个变化是代码量增长速率远超人类阅读速度。传统 code review 可以逐行审,因为一个 PR 通常只有几百行改动。但 LLM 生成代码库时,一次可能产生几十个文件,人类 Reviewer 根本看不过来。第二个变化是LLM 对项目上下文的语义理解是概率性的,它可能在一个模块里认为某个函数返回 Optional,在另一个模块里却当成非空直接用;可能为了避免报错而用except Exception: pass掩盖掉真实异常;可能为了满足静态检查而写冗余的防御代码。这些都不是"语法错误",而是"语义裂缝"。

第三点更关键:审计的目的不是找错,而是建立信任边界。当你接手一个 LLM 生成的代码库,你需要快速知道:哪些部分可以放心修改,哪些部分一碰就碎,哪些部分应该在合适时机重写。人工逐行看当然可以,但效率太低。

专门针对 LLM 生成代码的审计工具,核心能力不是替代人工审查,而是缩小需要人工审查的范围。它通过静态分析、依赖追踪、模式识别、结构度量等手段,把几千行代码归类为"高置信度区域"和"低置信度区域",让审查者把精力集中在真正有风险的地方。

这类工具之所以单独成为一类,是因为传统静态分析工具(如 pylint、bandit)是"规则驱动"的,它们检查的是代码是否违反了既定规范;而 LLM 代码审计需要的是"生成过程感知"——它要回答的是"这段代码可能是怎么来的、为什么会被写成这样",而不只是"这段代码是否符合规范"。

2. 审计什么:LLM 代码库的六大高风险点

要设计或使用这类工具,首先得知道该把放大镜放在哪里。从实际项目经验看,LLM 生成的 Python 代码库里有六个最值得重点关注的方向。

2.1 表面正常但语义错误的 import

LLM 在生成代码时,import 部分经常出问题。常见形态包括:导入了一个未安装的第三方库;导入了项目里根本不存在的模块;为了满足命名习惯而重复导入;更隐蔽的是导入路径写错导致运行时才报错。从代码审查角度,import 区域应该最先看,因为它决定了模块之间的依赖关系,也最容易暴露"这个项目是不是由多个互不知晓上下文的片段拼接而成"。

2.2 被静默吞掉的异常

这几乎成了 LLM 生成代码的标志性特征。生成模型在训练语料里见过大量try...except结构,但未必理解异常处理的业务语义。结果就是:

try: result = api_client.fetch_data() except Exception: pass # 什么都不做,假装成功

代码能跑,但出现问题时什么都查不到。审计工具应该能统计异常捕获语句中passreturn None、仅打印日志等"掩盖型"处理,并按数量排序输出。

2.3 类型标注与真实逻辑不一致

LLM 经常会生成看起来很规范的类型注解,但函数体内的实际返回路径并不完全符合注解。比如注解写了-> list[str],但在某个分支里返回了None。这类问题在小型脚本里不会暴露,一旦代码库变大,调用方就会因为这种不一致而出现运行时错误。

2.4 过度防御或永远触发的条件分支

一些生成代码会写出大量防御性判断:检查一个必然存在的 key、处理一个永远抛不出的异常。这类代码表面上看"更稳健",实际上是增加了阅读负担和测试成本。异常分支覆盖率极低,一旦真触发,逻辑往往是错的。

2.5 隐藏的动态执行

eval()exec()__import__()os.system()subprocess等动态执行接口在 LLM 生成的代码中并不少见。这类代码既可能是功能需要,也可能是安全风险来源。审计工具必须把它们全部标记出来人工确认。

2.6 依赖与实际调用不匹配

LLM 生成的代码库通常伴随自动生成的requirements.txt,但里边的依赖可能比实际需要的多——模型会把训练时见过的常见依赖也写进去。多出来的包不仅增加安装时间和安全攻击面,还可能带来版本冲突。反过来,也可能漏掉隐式依赖。

3. 这类工具的核心设计思路:分层审计

一个合格的 LLM 代码审计工具,不应该是一次性扫描后输出一个"风险分",而应该是多层级、可交互、支持逐步深入的。我比较推荐把审计拆成四个层次。

3.1 第一层:结构快照

先不判断对错,只做"事实提取"。包括:文件清单、文件大小、函数数量、类数量、import 关系图、函数调用图、嵌套深度、圈复杂度等。这一层的目的是让审查者在几分钟内知道"这个代码库长什么样",哪里有异常大的文件、哪里有循环依赖。

3.2 第二层:模式匹配

利用正则、AST 模板、语义规则去匹配高风险模式。这一层可以复用不少传统静态分析工具的思想,但侧重点要针对 LLM 生成代码的特点:异常掩盖、类型不一致、影子变量、重复代码、无意义的条件判断、硬编码的超时和密钥等。

3.3 第三层:语义推断

这是最难也最有价值的一层。工具尝试理解代码的"意图"与"实现"是否匹配。常见做法包括:对函数进行摘要生成,然后对比函数命名与摘要语义;分析异常处理路径是否覆盖了调用方的真实需求;检查状态变更是否缺乏持久化或事务保护。这一层通常需要用 LLM 做辅助分析,但因为这里是"用 LLM 审 LLM 的代码",所以必须允许人工介入,不能全自动下结论。

3.4 第四层:人工复核工作台

好的审计工具最后必须输出一个"复核队列",把人需要看的内容、建议看的理由和建议的修改方向都列出来。它不代替人类做决策,而是让人类决策更高效。

4. 环境准备与前置条件

如果你想把这类审计能力接入自己的项目,不需要一上来就找什么重型平台,很多能力可以用几个开源库组合出来。下面是一个建议的最小技术栈。

  • Python 3.10 或更高版本(AST 相关能力和类型注解支持更完整)
  • ast模块(Python 标准库,用于解析代码结构)
  • pathlib(标准库,用于路径遍历)
  • networkx(非标准库,用于依赖图分析,可选但推荐)
  • pytest(如果要做审计规则自测,会非常方便)
  • 一个能输出 JSON 的 CLI 框架,方便后续 CI 集成

版本方面没有必须锁死的限制,以你自己的项目为准。下面演示的是实现思路,不是某个特定工具的 API 说明。如果你要审 Large 项目,建议把代码托管仓库 clone 到本地一个专门目录,不要在线上直接跑审计脚本,避免把生产密钥扫进报告。

5. 核心实现:一个最小可用的 LLM 代码库审计脚本

我们直接写一个可运行的审计脚本。这个脚本的设计目标有三个:

  1. 扫描一个 Python 项目目录,提取基础结构信息。
  2. 识别 LLM 生成代码中常见的高风险模式。
  3. 输出分层的审计报告,供人工继续检查。

5.1 项目结构建议

先建立一个小项目,结构如下:

llm_code_auditor/ ├── auditor/ │ ├── __init__.py │ ├── scanner.py │ ├── patterns.py │ └── report.py ├── tests/ │ └── test_auditor.py └── main.py

5.2 scanner.py:核心扫描器

这个文件负责遍历目录,把每个 Python 文件解析成 AST 树,并保存文件级信息。

# auditor/scanner.py import ast import os from pathlib import Path from typing import Dict, List, Optional class FileScanResult: """单个 Python 文件的扫描结果""" def __init__(self, file_path: Path): self.file_path = file_path self.lines = 0 self.function_count = 0 self.class_count = 0 self.exception_handlers: List[dict] = [] self.dynamic_exec: List[dict] = [] self.imports: List[str] = [] self.type_hint_issues: List[dict] = [] def to_dict(self) -> dict: return { "file_path": str(self.file_path), "lines": self.lines, "function_count": self.function_count, "class_count": self.class_count, "exception_handlers": self.exception_handlers, "dynamic_exec": self.dynamic_exec, "imports": self.imports, "type_hint_issues": self.type_hint_issues, } def parse_python_file(file_path: Path) -> Optional[ast.AST]: """解析一个 Python 文件,返回 AST 树。语法错误时返回 None。""" try: code = file_path.read_text(encoding="utf-8") return ast.parse(code) except SyntaxError as e: print(f"[解析失败] {file_path}: {e}") return None def scan_file(file_path: Path) -> Optional[FileScanResult]: """扫描单个文件,提取基础结构和风险模式。""" tree = parse_python_file(file_path) if tree is None: return None result = FileScanResult(file_path) result.lines = len(file_path.read_text(encoding="utf-8").splitlines()) for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): result.function_count += 1 elif isinstance(node, ast.ClassDef): result.class_count += 1 elif isinstance(node, ast.ExceptHandler): result.exception_handlers.append( { "lineno": node.lineno, "name": node.name, "body_length": len(node.body), } ) elif isinstance(node, ast.Call): # 检测动态执行相关调用 if isinstance(node.func, ast.Name) and node.func.id in { "eval", "exec", "__import__", "compile", }: result.dynamic_exec.append( { "lineno": node.lineno, "func": node.func.id, } ) elif ( isinstance(node.func, ast.Attribute) and node.func.attr == "system" and isinstance(node.func.value, ast.Name) and node.func.value.id == "os" ): result.dynamic_exec.append( { "lineno": node.lineno, "func": "os.system", } ) elif isinstance(node, ast.Import): for alias in node.names: result.imports.append(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: result.imports.append(node.module) return result def scan_directory(root_dir: Path) -> List[FileScanResult]: """递归扫描目录下所有 .py 文件。""" results: List[FileScanResult] = [] for file_path in sorted(root_dir.rglob("*.py")): # 跳过常见虚拟环境和构建目录,避免把依赖包也扫进来 skip_dirs = {".venv", "venv", "node_modules", "__pycache__", ".git", "build", "dist"} if any(part in skip_dirs for part in file_path.parts): continue result = scan_file(file_path) if result is not None: results.append(result) return results

5.3 patterns.py:风险模式检测规则

这里实现几个典型的"LLM 痕迹"检测规则。

# auditor/patterns.py import ast from typing import List from .scanner import FileScanResult def detect_silent_exception(scan: FileScanResult) -> List[dict]: """ 检测异常处理中 body 为空、只有 pass、或只打印日志后继续 执行的模式。这类代码会吞掉真实异常,导致线上问题难排查。 """ issues = [] for handler in scan.exception_handlers: lineno = handler["lineno"] body_length = handler["body_length"] if body_length < 2: issues.append( { "type": "silent_exception", "lineno": lineno, "file": str(scan.file_path), "why": "异常处理体过短,可能有吞异常风险", } ) return issues def detect_dynamic_execution(scan: FileScanResult) -> List[dict]: """ 检测 eval / exec / os.system 等动态执行调用。 这类代码在 LLM 生成的代码库中尤其需要警惕, 因为它可能来自任意来源的字符串输入。 """ issues = [] for item in scan.dynamic_exec: issues.append( { "type": "dynamic_execution", "lineno": item["lineno"], "func": item["func"], "file": str(scan.file_path), "why": f"动态执行接口 {item['func']} 被调用,请确认输入来源可信", } ) return issues def collect_all_issues(scans: List[FileScanResult]) -> List[dict]: """汇总所有风险问题。""" all_issues = [] for scan in scans: all_issues.extend(detect_silent_exception(scan)) all_issues.extend(detect_dynamic_execution(scan)) return all_issues

5.4 main.py:CLI 入口

做一个简单的命令行入口,支持指定目录、输出 JSON 报告。

# main.py import argparse import json from pathlib import Path from auditor.scanner import scan_directory from auditor.patterns import collect_all_issues def main(): parser = argparse.ArgumentParser( description="LLM 生成 Python 代码库的快速审计工具" ) parser.add_argument( "target", help="要审计的项目目录路径", ) parser.add_argument( "--output", "-o", help="报告输出路径,如果不指定则打印到标准输出", ) args = parser.parse_args() root_dir = Path(args.target) if not root_dir.is_dir(): print(f"[错误] {root_dir} 不是有效目录") return 1 scans = scan_directory(root_dir) issues = collect_all_issues(scans) report = { "summary": { "scanned_files": len(scans), "total_lines": sum(s.lines for s in scans), "total_issues": len(issues), }, "issues": issues, "files": [s.to_dict() for s in scans], } if args.output: output_path = Path(args.output) output_path.write_text( json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"[完成] 报告已写入 {output_path}") else: print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())

5.5 如何运行和验证

假设你要审计的代码库位于~/projects/llm_generated_app,在项目根目录执行:

python main.py ~/projects/llm_generated_app

如果只想看汇总信息,可以加--output report.json输出到文件。

你可以先拿一个小型"模拟 LLM 生成代码"的目录做验证,比如:

# demo_app/main.py import os import subprocess def fetch_data(): try: result = os.popen("echo hello") return result.read() except Exception: pass def run_task(cmd): return subprocess.run(cmd, shell=True) def safe_divide(a: int, b: int) -> float: try: return a / b except ZeroDivisionError: pass

运行审计脚本后,你会在报告里看到silent_exceptiondynamic_execution两类问题被标记出来。这三处代码分别对应:异常被吞、动态 shell 执行、异常被吞后再返回 None。

6. 从脚本到工程化:把审计接入 CI 流程

单机审计脚本只是第一步。真正有价值的用法是把它接入团队的 CI 流程,让每一个新提交的代码库变更都自动跑一遍审计。这样在 PR 阶段就能看到风险提示。

下面是接入 GitHub Actions 的示例配置,你也可以按自己团队使用的 CI 平台做等价配置。

# .github/workflows/llm-code-audit.yml name: LLM Code Audit on: pull_request: paths: - "**.py" workflow_dispatch: jobs: audit: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: | pip install networkx pytest - name: Run audit script run: | python main.py . --output audit-report.json - name: Upload audit report uses: actions/upload-artifact@v4 with: name: llm-audit-report path: audit-report.json - name: Check issue threshold run: | python - <<'EOF' import json with open("audit-report.json", "r", encoding="utf-8") as f: report = json.load(f) total = report["summary"]["total_issues"] print(f"Total issues: {total}") if total > 100: print("Issues exceed threshold, please review generated code.") exit(1) EOF

这段配置会在每次 PR 修改 Python 文件时触发审计,并把报告作为构建产物上传。你可以根据自己的容忍度调整阈值,比如把超过 100 个问题设为失败门禁。注意这里的阈值只是示例,实际项目应该根据代码库大小、团队人力、发布风险综合设定。

7. 运行效果与结果验证

把审计脚本接入 CI 后,你会在 PR 页面看到一个"LLM Code Audit"的检查项。点开报告,可以重点看三块内容。

第一块是summary,它告诉你这个代码库有多大、扫出多少问题。如果代码量很小但问题数量很多,说明这块代码的生成痕迹很重,需要优先人工处理。如果代码量很大但问题数量不多,也不代表完全安全,只说明最容易出问题的地方没有踩雷,仍然需要抽查。

第二块是issues列表。每条问题都包含文件路径、行号、问题类型、审计理由。这个列表应该按"建议处理优先级"排序。比如dynamic_executionsilent_exception优先级高,因为它们可能带来安全风险。

第三块是files列表。这里的数据可以辅助你识别"异常文件"——几千行的单文件、没有任何函数定义的脚本、import 了 30 个库的文件。这些特征往往对应着某次大段生成,没有经过第二次重构。

验证成功的判断标准有两个:

  1. 审计脚本能稳定输出 JSON 报告,CI 流程能正常获取并展示。
  2. 你能根据报告的指引快速定位到真正需要看的位置,而不是把报告扔到一边。

如果报告生成了但没有人看过,那这个工具就没有接入流程,只是多了一个无人问津的 artifact。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
脚本运行时报SyntaxError被扫描目录里存在非 UTF-8 编码文件或语法损坏的 Python 碎片查看报错路径,手动打开文件确认修改编码读取逻辑,或跳过无法解析的文件并记录到报告中
扫描到了虚拟环境中的大量第三方包忽略目录列表没有覆盖.venvvenv等目录检查scan_directory中的skip_dirs是否包含相关目录扩充忽略列表,或在调用时传入排除参数
报告里没有检测到类型不一致问题当前脚本只做了结构检测,没有实现语义推断检查patterns.py中是否有类型分析逻辑增加针对 typing 和函数返回体的语义检测规则
CI 中审计脚本超时代码库过大,逐文件 AST 解析太慢在本地执行time python main.py .观察耗时启用增量扫描,只扫描本次变更涉及的 Python 文件
报告里的 JSON 过大,CI 界面打不开文件列表和 issues 列表全部输出,数据量过大查看报告体积,用du -h检查精简报告字段,或把 files 明细输出到单独文件
误报率太高,团队成员不再信任审计结果规则过于宽泛,比如把所有except都标记问题随机抽 10 条报告中的问题,人工核对准确率提高检测置信度,对低置信度问题降级为信息提示而非警告
审计了别人手写的老代码,问题数量爆炸老代码本来就是历史产物,审计规则对新代码更严格对比两份不同代码库的问题密度分模块设置审计阈值,优先审计新增/重写代码

9. 最佳实践与工程建议

9.1 不要让审计工具替代 Code Review

这一点要反复强调。审计工具的价值在于缩小需要人工关注的范围,而不是给出"通过/不通过"的结论。建议的协作姿势是:工具负责把所有可疑点列出来,人类负责对这些可疑点做最终裁决。工具只是提高了审查者的起点。

9.2 把 LLM 生成的代码与手写代码分开审计

如果项目里同时存在 LLM 生成代码和手写代码,建议把审计结果按来源分组。可以在项目里约定,LLM 生成的文件放在scripts/generated/或者文件头部有明确的生成标记。这样审计工具可以优先检查生成代码,而手写代码采用传统 review 流程。

9.3 审计要分层,不要只输出一个综合风险分

综合风险分看似方便,但对定位问题没有帮助。更好的输出方式是按风险和问题类型分类:

  • P0:动态执行、明文密钥、越权接口
  • P1:异常吞没、类型不一致、依赖冲突
  • P2:过度防御、死代码、无意义注释

按这个优先级排序,人工审查者能更快决定先看哪里。

9.4 引入审计工具要渐进,不要一次性扫描全部存量代码

如果你把整个历史代码库一次性跑一遍,大概率会得到一份几千条问题的报告,然后这个问题追踪单就永远没有人去关闭。更务实的做法是:新代码强制审计,存量代码仅做基线统计,不阻塞发布。等团队习惯了审计报告的格式,再逐步清理高优先级存量问题。

9.5 审计规则的维护是一个持续动作

审计工具写完后,不是一劳永逸的。LLM 能力在变,生成代码的模式也在变。今天常见的问题类型,半年后可能就不再常见,而新的问题模式会出现。建议每个季度复盘一次审计规则,把实际 code review 中发现的、规则还没覆盖到的问题补进规则库。同时给规则写单测,防止规则更新时误伤正常代码。

9.6 注意审计范围与权限控制

审计脚本可能会读取到代码库里的环境变量、配置文件、密钥占位符。在编写审计报告时,要有意识地避免把敏感值直接写入报告。更稳妥的做法是只报告"这里存在硬编码密钥",不要把密钥内容本身打印出来。生产环境接入审计时,建议用最小权限的服务账号运行,定期轮换访问凭据。

10. 总结与下一步

围绕"快速 step through 并审计 LLM 生成的 Python 代码库"这个话题,这篇文章讲清楚了三点:

第一,传统静态分析解决的是"代码是否符合规范"的问题,而 LLM 代码审计解决的是"这个代码库是否值得信任"的问题。两者目标不同,手段也不同。

第二,一个最小可用的 LLM 代码审计工具,核心能力在于分层:先做结构快照,再做模式匹配,再辅助语义推断,最后输出人工复核队列。这个分层思路可以逐步完善,不用一开始就做得很重。

第三,工具工程化的关键在于 CI 集成和阈值管理。没有人工复核的审计只是徒增噪音;没有阈值控制的审计只会积累大量无人处理的低优先级问题。

如果你手头正好有 LLM 生成的 Python 项目,建议按文章里的脚本思路先搭一个最小版本,跑一遍看看报告长什么样。不要急着加更多规则——先把"哪些文件最可疑"这个结论拿到手,你自然会知道接下来的审计规则该往哪个方向写。

往下走,可以考虑在这些方向深入:对 import 环路做图分析、对函数的 docstring 与实现做语义一致性检查、把审计结果与项目里的 issue 系统打通。每个方向都够写一系列文章,但起点都是你现在跑通的这个最小脚本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 4:02:56

Grok Bot实战指南:从API配置到批量任务部署

Grok 这个名字最近频繁出现在技术社区&#xff0c;不只是因为它背后的模型&#xff0c;还因为“Bot”这个词正从聊天助手变成真正的生产力工具。Lee Robinson 那句“Grok Bot 是未来工作方式”之所以能被讨论&#xff0c;是因为它指向了一个更具体的趋势&#xff1a;AI 不再只是…

作者头像 李华
网站建设 2026/9/4 20:00:53

从零搭建AI内容治理服务:深度伪造检测与批量审核实战

"AI Threatens Our Economy and Democracy Itself"&#xff0c;这句话自带流量&#xff0c;但放到工程技术人员的桌面上&#xff0c;它不是一个哲学命题&#xff0c;而是一组可以被拆解、被检测、被管控的风险场景。AI 生成内容已经从实验室走向流水线&#xff1a;一…

作者头像 李华
网站建设 2026/9/2 1:06:58

ChatGPT桌面端Codex集成故障排查:从CLI安装到config.toml修复完整指南

ChatGPT 桌面端集成 Codex 后&#xff0c;用户的日常使用从“AI 只能给代码”变成了“AI 可以打开终端、执行命令、修改文件”。这部分新功能的核心依赖不是聊天窗口本身&#xff0c;而是 Codex CLI。大量用户在实际体验时却发现&#xff0c;新的客户端首屏就出现ChatGPT faile…

作者头像 李华
网站建设 2026/9/4 13:23:15

智能驾驶研发工程师笔试全解析:从编程算法到感知规划

2018年那会儿&#xff0c;智能驾驶这四个字在出行行业里几乎就是“高薪”和“技术壁垒”的代名词。滴滴那年的校园招聘内推里&#xff0c;智能驾驶研发工程师这个岗位的笔试&#xff0c;绝对是不少想进自动驾驶圈子同学的第一个硬门槛。我当时身边有不少朋友投了这个岗位&#…

作者头像 李华
网站建设 2026/9/10 3:04:10

大模型应用开发Demo

目录 一.初步连接模型 二.非流式输出的响应结构 三.流式输出的请求体响应结构 四.使用大模型进行情感分析 五.使用大模型进行图像识别 六.使用大模型进行图像生成 七.函数功能的使用 一.初步连接模型 首先&#xff0c;本文采用uv在根目录下创建虚拟环境&#xff0c;代码…

作者头像 李华