ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS2常用工具详解:TF坐标变换、参数机制与Launch文件实战

ROS2常用工具详解:TF坐标变换、参数机制与Launch文件实战 很多初学者在接触 ROS2 时最先接触的是话题、服务、动作这些通信方式但真正常常一头雾水的地方反而是那些不直接跑代码的工具型组件坐标变换 TF、参数机制、Launch 文件。尤其是现在具身智能机器人、机械臂抓取、移动底盘导航这些项目越来越火你会发现后面几乎所有的功能包都在围绕机器人各个部件之间的坐标关系、节点的参数配置、多个节点的统一启动来工作。这篇文章就围绕 B站 全新版 ROS2 常用工具这条主线把 TF 坐标变换工具、参数机制、Launch 文件编写方法这三个核心问题拆开讲清楚。先给出一个明确判断这三个工具是 ROS2 从能跑通例程走向能写真实机器人项目的分水岭。看完你不仅能发布坐标信息还能理解为什么要用静态坐标变换而不是自己手写坐标系以及 Launch 文件到底解决了什么问题。1. 这篇文章真正要解决的问题接触 ROS2 的读者大多经历过这样一个过程安装完环境跑通了小海龟例程然后用 rviz2 看了一个机器人模型接着就卡住了。卡住的原因通常不是语法不会而是我到底该用哪种工具来解决当前问题。举个例子你做一台差速底盘机器人底盘上装了激光雷达激光雷达数据是在雷达坐标系下发布的但导航算法需要的是底盘坐标系下的数据。你不可能让雷达厂商把数据改成底盘坐标系下发更不应该在代码里手动写一个坐标转换的数学公式。这时候就需要 TF 坐标变换工具。再举个例子你写了一个相机标定节点标定结果内参矩阵、畸变系数每次都要改代码重新编译非常痛苦。如果使用 ROS2 的参数机制把内参放到参数文件里节点启动时自动加载甚至运行时动态修改这就省去了大量重复编译时间。再看 Launch 文件。一个完整的机器人系统往往有十几个节点底盘驱动、激光雷达驱动、IMU 驱动、导航算法、可视化工具。如果你每次都开多个终端一个个 source 环境然后 ros2 run不仅低效而且很容易记错启动顺序。Launch 文件本质上就是解决一组节点的统一编排与启动问题。所以这篇文章要解决的问题很明确在 ROS2 开发中如何正确使用 TF 工具发布坐标信息如何利用参数机制解耦代码与配置如何编写 Launch 文件管理多节点启动。这三个技能覆盖了大多数机器人项目的开发日常。2. TF 坐标变换工具的核心概念与适用场景2.1 什么是 TF 坐标变换TFTransform是 ROS 中负责维护机器人各坐标系之间位姿关系的核心工具。机器人在真实世界中运动时车体、激光雷达、相机、机械臂末端、夹爪等部件都有自己的坐标系而每个传感器测量得到的数据都只能基于自己的坐标系描述。为了让不同传感器数据能够在同一参照系下融合使用就需要维护一组坐标变换关系。在 ROS2 中这套机制由 tf2 功能包实现。它最大的特点是树状结构所有坐标系之间的关系构成一棵坐标树每个坐标系只能有一个父坐标系但可以有多个子坐标系。最典型的根节点就是机器人本体 base_link。TF 的核心价值在于它把坐标系之间的变换计算这一数学问题封装成了标准工具开发者不需要手动写旋转矩阵、平移向量只需要声明谁在谁的什么位置剩下的交给 tf2 处理。2.2 ROS1 与 ROS2 的 TF 差异很多人从 ROS1 转 ROS2最容易在 TF 相关 API 上踩坑。ROS1 中常用的lookupTransform、transformLaserScanToPointCloud这类函数在 ROS2 中 API 已经变化tf2_ros::Buffer取代了原来的TransformListenertf2_geometry_msgs的接口也做了调整。更重要的是ROS2 中一个节点可以同时是多个坐标变换的发布者和订阅者职责划分更细。从工具角度ROS2 保留了static_transform_publisher这个命令行工具用于发布静态坐标变换。但注意ROS2 中它可能不再作为一个独立的可执行程序出现在旧路径而是集成在 tf2_ros 功能包中这一点新手经常混淆。2.3 TF 能解决什么不能解决什么TF 解决的是坐标关系描述与查询的问题但它本身不做传感器数据的处理也不做运动学计算。它只负责维护一个高频率更新的坐标树让其他节点能够随时查询此刻机器人的激光雷达坐标系相对于底盘坐标系的位置和姿态。在实际项目中TF 最常见的应用场景包括多传感器融合相机雷达IMU、机械臂运动学可视化、导航路径规划中的坐标系转换、仿真环境与真实机器人之间的坐标映射。2.4 小结论TF 工具的核心任务是管理坐标系关系。对于开发者来说最重要的不是背 API而是理解坐标树的结构和每一个传感器的安装位置参数应该填写在哪里。3. 参数机制的核心概念与运行原理3.1 什么是 ROS2 参数ROS2 参数是一种基于键值对的配置机制属于节点级别的配置数据。每个节点可以定义若干参数每个参数有名字、类型和值。参数可以由节点自己声明也可以由外部通过命令行设置或者通过参数文件在启动时加载。为什么需要参数机制直接原因是避免硬编码。早期的 ROS1 开发者经常把传感器内参、PID 增益、地图路径这些配置直接写在代码里每次调整都要重新编译。ROS2 参数机制把配置和代码分开让同一份代码可以通过不同的参数文件适配不同的机器人硬件。3.2 参数类型与获取方式ROS2 参数支持的数据类型包括bool、int、double、string、byte[]、bool[]、int[]、double[]、string[]。参数存储在一个参数描述符中节点可以通过declare_parameter声明参数通过get_parameter读取参数值。运行参数修改有三种途径命令行传参、参数文件加载、节点内部动态设置。命令行传参适合临时调试参数文件适合正式配置节点内部动态设置适合运行中自适应调整。3.3 参数机制与 TF 之间的关系这里有一个容易忽略的关联TF 的坐标变换关系经常通过参数机制来配置。例如激光雷达安装在底盘前方 0.3 米、高度 0.5 米处这些值可以作为laser_offset_x、laser_offset_y、laser_offset_z参数传入节点节点再根据这些参数发布 static_transform。这种设计的好处是当激光雷达的位置发生变化时只需修改参数文件而不用重新编译整个项目。3.4 小结论参数机制不是 ROS2 特有的功能但它是 ROS2 工程化能力的重要体现。当你的项目需要适配多台机器人时参数文件的质量直接决定调试效率。4. Launch 文件的作用与编写方法4.1 为什么需要 Launch 文件ROS2 的 Launch 文件是用于描述和启动一组 ROS2 节点的配置工具。它使用 Python 编写以 XML 和 YAML 也可以作为辅助格式。Launch 文件不仅能够启动节点还能设置参数、加载参数文件、嵌套启动其他 launch 文件、设置命名空间、执行条件判断等。没有 Launch 文件的时代开发者启动一个导航系统需要手动打开 6 个终端每个终端执行不同的ros2 run命令还要时刻关注启动顺序。Launch 文件把这一切收敛成一个命令ros2 launch。这让系统启动变得可重复、可配置、可分享。4.2 Launch 文件的三大要素Launch 文件有三个核心概念Node 描述定义要启动的可执行文件、节点名称、命名空间、参数。参数配置在 Node 描述中指定参数文件路径或直接设置参数值。组合与嵌套通过 IncludeLaunchFile 将多个 launch 文件组合起来形成层级结构。从实际项目角度看Launch 文件的编写还有一个隐藏价值它就是机器人系统的说明书。一个设计良好的 launch 目录别人看到之后就能理解这个系统有哪些模块、每个模块怎么配置、模块之间的依赖关系。4.3 Launch 文件的工作流程当ros2 launch执行时系统会解析 launch 描述创建对应的节点进程配置参数执行所有启动动作。Launch 文件还支持事件驱动比如在某个节点启动完成后才启动另一个节点这对有依赖关系的系统非常有用。4.4 小结论Launch 文件是 ROS2 工程组织的骨架。它不是语法最复杂的技术但却是从写节点走向做系统的关键一步。5. 环境准备Ubuntu ROS2 安装与功能包检查在开始实操之前需要先准备好 ROS2 开发环境。本文以 ROS2 Humble 或 Jazzy 版本为主演示如果你使用的是其他发行版接口基本一致只是个别包名和版本号略有差异。安装 ROS2 的通用步骤是配置软件源、安装基础包、初始化环境变量。如果你已经安装好 ROS2可以跳过这一节但建议检查以下功能包是否齐全# 检查 ROS2 基本命令 ros2 --version # 检查 TF 相关功能包 ros2 pkg list | grep tf2 # 检查 rviz2 是否可用 rviz2 --help | head -55.1 在 Ubuntu 上安装 ROS2如果还没有安装推荐使用官方二进制安装方式。导入 ROS2 的 GPG 密钥和软件源后运行apt install ros-humble-desktop即可安装完整桌面版包含 rviz2、demo 例程和常用工具。需要特别提醒的是ROS2 不同版本的 Ubuntu 支持不同Humble 对应 Ubuntu 22.04Jazzy 对应 Ubuntu 24.04。跨版本安装虽然可以尝试源码编译但依赖问题非常多新手并不推荐。5.2 安装额外的 tf2 演示包为了验证 TF 功能建议安装tf2_tools和tf2_rossudo apt install ros-humble-tf2-tools ros-humble-tf2-ros如果使用的是 Jazzy将包名中的humble替换为jazzy即可。5.3 环境变量的设置每次打开新终端都需要 source 一次 ROS2 环境source /opt/ros/humble/setup.bash如果不希望每次手动执行可以将这一行写入~/.bashrc。这一步是整个调试的基础很多新手报错Command ros2 not found就是因为没有 source 环境。6. 核心流程拆解如何发布坐标信息TF 工具发布坐标信息的核心操作分为四类发布静态坐标变换、发布动态坐标变换、监听坐标变换并完成计算、在 rviz2 中可视化验证。6.1 发布静态坐标变换静态坐标变换适合那些在机器人运行过程中固定不变的坐标关系比如激光雷达相对于底盘的位置、相机相对于机械臂底座的位置。在 ROS2 中最简单的发布静态坐标变换方式是使用命令行工具ros2 run tf2_ros static_transform_publisher x y z yaw pitch roll parent_frame child_frame例如发布激光雷达相对于底盘 base_link 的坐标变换ros2 run tf2_ros static_transform_publisher 0.30 0.0 0.50 0.0 0.0 0.0 base_link laser_link这条命令会以默认频率持续发布。它做的事情是告诉 TF 系统laser_link 坐标系的原点在 base_link 坐标系下的位置是 (0.30, 0.0, 0.50)且三个轴的旋转角为 0。6.2 发布动态坐标变换动态坐标变换用于那些时刻变化的坐标关系比如底盘在里程计坐标系中的位姿。需要编写一个节点在循环中不断生成TranformStamped消息并发布。核心逻辑创建tf2_ros::TransformBroadcaster。循环中构造geometry_msgs::msg::TransformStamped。填写 header、child_frame_id、变换矩阵和平移。调用sendTransform发布。6.3 监听坐标变换监听是 TF 使用中更常见的需求。其他节点通过tf2_ros::Buffer缓存所有坐标变换关系然后调用lookupTransform查询任意两个坐标系之间的转换。无论是发布还是监听都要注意头文件、命名空间和时间戳的正确填写尤其是时间戳为 0 时TF 系统会将消息标记为不可用这一细节经常导致查询失败。6.4 在 rviz2 中的验证思路启动 rviz2 后添加 TF 显示组件就能看到所有坐标轴箭头。如果静态变换发布正确各个坐标系之间的相对位置关系会直观地显示出来。如果出现Frame [laser_link] does not exist这类提示说明坐标变换没有成功发布需要优先检查发布的 frame_id 是否拼写正确。7. 完整示例与代码实现7.1 示例一用 Python 发布静态坐标变换文件路径src/tf_tools_demo/tf_tools_demo/static_tf_publisher.pyimport rclpy from rclpy.node import Node from tf2_ros import StaticTransformBroadcaster from geometry_msgs.msg import TransformStamped class StaticTfPublisher(Node): def __init__(self): super().__init__(static_tf_publisher) self.broadcaster StaticTransformBroadcaster(self) self.declare_parameter(x, 0.0) self.declare_parameter(y, 0.0) self.declare_parameter(z, 0.0) self.declare_parameter(yaw, 0.0) self.declare_parameter(pitch, 0.0) self.declare_parameter(roll, 0.0) self.declare_parameter(parent_frame, base_link) self.declare_parameter(child_frame, laser_link) x self.get_parameter(x).value y self.get_parameter(y).value z self.get_parameter(z).value yaw self.get_parameter(yaw).value pitch self.get_parameter(pitch).value roll self.get_parameter(roll).value parent self.get_parameter(parent_frame).value child self.get_parameter(child_frame).value self.publish_static_tf(x, y, z, yaw, pitch, roll, parent, child) def publish_static_tf(self, x, y, z, yaw, pitch, roll, parent, child): transform TransformStamped() transform.header.stamp self.get_clock().now().to_msg() transform.header.frame_id parent transform.child_frame_id child transform.transform.translation.x x transform.transform.translation.y y transform.transform.translation.z z transform.transform.rotation.z yaw transform.transform.rotation.y pitch transform.transform.rotation.x roll transform.transform.rotation.w 1.0 self.broadcaster.sendTransform(transform) def main(argsNone): rclpy.init(argsargs) node StaticTfPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这段代码将静态坐标变换的每个参数都定义为 ROS2 参数这样在 Launch 文件中就可以通过参数文件或parameters列表配置而不用每次修改代码。运行验证ros2 run tf_tools_demo static_tf_publisher --ros-args -p x:0.3 -p y:0.0 -p z:0.5 -p parent_frame:base_link -p child_frame:laser_link再用tf2_echo查看变换是否发布成功ros2 run tf2_ros tf2_echo base_link laser_link如果输出了一组稳定的平移和旋转值说明 TF 发布成功。7.2 示例二用 C 监听 TF 坐标变换文件路径src/tf_tools_demo/src/tf_listener_demo.cpp#include chrono #include memory #include geometry_msgs/msg/transform_stamped.hpp #include rclcpp/rclcpp.hpp #include tf2/exceptions.h #include tf2_ros/transform_listener.h #include tf2_ros/buffer.h using namespace std::chrono_literals; class TfListenerDemo : public rclcpp::Node { public: TfListenerDemo() : Node(tf_listener_demo) { tf_buffer_ std::make_sharedtf2_ros::Buffer(this-get_clock()); tf_listener_ std::make_sharedtf2_ros::TransformListener(*tf_buffer_); timer_ this-create_wall_timer(1s, std::bind(TfListenerDemo::on_timer, this)); } private: void on_timer() { geometry_msgs::msg::TransformStamped transform_stamped; try { transform_stamped tf_buffer_-lookupTransform( base_link, laser_link, tf2::TimePointZero); } catch (tf2::TransformException ex) { RCLCPP_WARN(this-get_logger(), 查询失败: %s, ex.what()); return; } RCLCPP_INFO( this-get_logger(), laser_link 在 base_link 中的位置: x%.2f, y%.2f, z%.2f, transform_stamped.transform.translation.x, transform_stamped.transform.translation.y, transform_stamped.transform.translation.z); } std::shared_ptrtf2_ros::Buffer tf_buffer_; std::shared_ptrtf2_ros::TransformListener tf_listener_; rclcpp::TimerBase::SharedPtr timer_; }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_sharedTfListenerDemo()); rclcpp::shutdown(); return 0; }监听端的核心是tf2_ros::Buffer它内部缓存了所有坐标变换关系。lookupTransform传入两个 frame_id 后返回一个TransformStamped这个结构体包含了平移和旋转信息。在查询时tf2::TimePointZero表示获取最新可用数据它会自动维护一个时间滑动窗口避免缓存中的数据过期。编译时需要在CMakeLists.txt中添加依赖find_package(tf2_ros REQUIRED) find_package(tf2 REQUIRED) find_package(geometry_msgs REQUIRED)7.3 示例三使用 Launch 文件统一启动参数与节点文件路径src/tf_tools_demo/launch/tf_demo.launch.pyimport os from launch import LaunchDescription from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory def generate_launch_description(): pkg_share get_package_share_directory(tf_tools_demo) # 静态坐标变换节点通过参数文件加载 static_tf_params os.path.join(pkg_share, config, tf_params.yaml) static_tf_node Node( packagetf_tools_demo, executablestatic_tf_publisher, namestatic_tf_publisher, parameters[static_tf_params], outputscreen ) # 坐标变换监听节点 tf_listener_node Node( packagetf_tools_demo, executabletf_listener_demo, nametf_listener_demo, outputscreen ) # rviz2 可视化 rviz2_node Node( packagerviz2, executablerviz2, namerviz2, arguments[-d, os.path.join(pkg_share, config, tf_demo.rviz)], outputscreen ) return LaunchDescription([ static_tf_node, tf_listener_node, rviz2_node ])参数文件路径src/tf_tools_demo/config/tf_params.yamlstatic_tf_publisher: ros__parameters: x: 0.30 y: 0.0 z: 0.50 yaw: 0.0 pitch: 0.0 roll: 0.0 parent_frame: base_link child_frame: laser_link启动命令ros2 launch tf_tools_demo tf_demo.launch.py这个 Launch 文件做了三件事启动静态 TF 发布节点、启动 TF 监听节点、打开 rviz2。运行时你会看到监听节点每隔一秒输出一次坐标信息同时 rviz2 中能够看到 base_link 和 laser_link 两个坐标系之间的相对位姿。8. 运行结果与效果验证8.1 查看 TF 树运行static_tf_publisher后可以用tf2_tools查看坐标系之间的关系ros2 run tf2_tools view_frames这个命令会生成一个frames.pdf文件里面展示了当前所有坐标系组成的关系图。如果在生成的 PDF 中看到 base_link 和 laser_link 两个节点连在一起说明 TF 树的建立是成功的。8.2 终端输出判断运行tf2_echo后的预期输出At time 171532... - Translation: [0.300, 0.000, 0.500] - Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]如果输出Translation: [nan, nan, nan]或者Could not transform错误说明两个坐标系之间根本没有建立变换关系。8.3 rviz2 中的可视化验证在 rviz2 中点击左下角Add选择TF显示类型。此时你会看到两个坐标轴箭头base_link 和 laser_link。laser_link 应该在 base_link 前方 0.3 m、上方 0.5 m 的位置出现。如果只出现 base_link 而没有 laser_link检查发布的 child_frame 名称是否与 rviz2 中的 Fixed Frame 设置一致。通常推荐将 Fixed Frame 设置为 base_link这样坐标树的显示最直观。8.4 失败时优先排查的方向按照先命令行再节点最后可视化的思路排查。先确认静态坐标变换发布成功再检查监听节点的时间戳最后才看 rviz2 的显示配置。90% 的 TF 显示问题出在 frame_id 拼写或 Fixed Frame 选择错误。9. 常见问题与排查思路问题现象可能原因排查方式解决方案Frame laser_link does not existframe_id 拼写错误或坐标变换未发布ros2 run tf2_ros tf2_echo base_link laser_link检查静态变换节点是否启动检查拼写lookupTransform查询耗时过长且报错目标坐标系不在同一 TF 树中ros2 run tf2_tools view_frames查看 TF 树补发缺失坐标系的变换静态变换发布后 rviz2 无显示Fixed Frame 与 TF 树中父坐标系不一致查看 rviz2 左上角 Fixed Frame 设置将 Fixed Frame 改为 base_link 或其他存在的 frame参数文件中参数未生效launch 文件未加载参数文件或节点名不匹配查看节点参数列表ros2 param list确认 YAML 中的节点名与 launch 中节点 name 一致C 节点lookupTransform抛异常时间戳不合法或坐标系不存在打印异常信息ex.what()使用tf2::TimePointZero获取最新数据ros2 launch找不到 launch 文件构建后未重新 source 或路径错误ls install/tf_tools_demo/share/tf_tools_demo/launch重新colcon build并 source install/setup.bash两个节点同时发布同一个 TF 变换坐标变换存在冲突ros2 topic info /tf --verbose查看发布者只保留一个发布者或用不同 frame_id 区分TF 变换偶尔跳变发布频率不足或时间戳抖动检查节点发布循环频率提高发布频率保证时间戳单调递增10. 最佳实践与工程建议这里整理一些从实际项目中沉淀下来的建议对刚开始做 ROS2 项目的读者会有帮助。10.1 TF 命名的行业习惯frame_id 的命名建议遵循语义化规则。底盘坐标系通常叫 base_link 或 base_footprint传感器坐标系一般用部件名加 link例如 laser_link、camera_link、imu_link。不建议使用中文或带空格的名称否则在 rviz2 和工具链中会出现各种奇怪问题。10.2 静态变换不要写在节点循环里StaticTransformBroadcaster 设计为只发布一次或低频发布不要在主循环里用 TransformBroadcaster 高频发送静态变换。虽然功能上可行但会占用不必要的网络带宽而且会让 TF 树看起来混乱。静态变换的默认发布频率是 0也就是发布一次后由缓存放量扩散。10.3 参数文件与 launch 文件的组织方式推荐一个功能包内部划分为launch/、config/、src/三个目录。launch 只负责编排启动方式config 存放参数文件和 rviz 配置src 存放源码。这样做的最大好处是更换机器人平台时只需要修改 config 目录下的参数文件而源代码完全不用动。10.4 慎用remap和命名空间Launch 文件支持 remap 重映射话题名称和命名空间但对新手来说非常容易造成话题名称错乱。建议前期保持默认话题名通过--ros-args -r临时重映射来调试等熟悉之后再在 launch 文件中使用命名空间特性。10.5 不要遗忘 colcon build 后的 source每当你修改了功能包代码或新增 launch 文件都要重新执行colcon build然后 source 对应的 install/setup.bash。忘记 source 是明明改了代码但运行结果没变化的最常见原因。10.6 善用日志定位问题ROS2 节点的日志级别默认是 INFO。在 TF 相关节点中在 catch 到tf2::TransformException时打印ex.what()是定位坐标变换问题的第一条线索。不要只靠 rviz2 的显示判断因为 rviz2 对错误的提示不如日志精确。10.7 具身智能项目中的落地建议如果你正在做具身智能机器人项目机械臂、视觉系统、移动底座之间会有大量坐标变换关系。建议在项目启动前先画一张坐标系规划图谁是谁的父坐标系传感器安装在哪个参考系下坐标系之间的物理位置是多少。把这张图落到参数文件中之后所有代码层面的坐标查询都依赖这套定义能省掉大量框架不闭合的联调时间。10.8 版本兼容性提醒ROS2 的分布式版本非常多Foxy、Galactic、Humble、Iron、Jazzy 都存在。你在网上看到的大多数教程都是基于特定版本写的如果遇到了 API 不兼容优先查看ros2 pkg list | grep tf2和一些官方迁移指南。不要因为一个文档里的命令在你的版本上报错就怀疑 ROS2 全局出错。11. 总结与后续学习方向这篇内容围绕 ROS2 常用工具的三个关键组件展开TF 坐标变换工具、参数机制、Launch 文件。核心收获可以概括为四点第一TF 坐标变换工具解决了多个坐标系之间数据融合的问题具体操作分为发布静态变换、发布动态变换和监听查询三个步骤。真正容易出错的地方不是 API 记忆而是对 frame_id 命名、坐标树结构、时间戳一致性的理解。第二参数机制实现了配置与代码分离让同一套代码能够适配不同传感器安装位置和不同机器人平台。在真实的具身智能机器人项目中参数文件通常比代码调整得更频繁。第三Launch 文件是系统集成的基石它把多个节点的启动顺序、参数加载、rviz2 可视化管理成一条命令。从一个 launch 文件的结构基本能判断一个 ROS2 项目的工程化水平。第四这三个工具不是孤立使用的。TF 变换的数值通常来自参数文件多个节点又由 launch 统一管理。只有把三者组合起来才能真正展现 ROS2 在复杂机器人系统开发中的效率价值。下一步你可以尝试这样实践为自己的机器人或仿真模型创建一个功能包定义底盘、激光雷达、相机三个坐标系发布对应的静态坐标变换在 rviz2 中观察坐标关系再把所有启动逻辑写入 launch 文件并加载参数配置。把这个最小流程跑通之后再进入导航、机械臂控制或者视觉抓取等更具体的应用领域会发现基础牢靠很多。ROS2 的学习路径很长但 TF、参数、Launch 这三个工具是绕不开的基础设施。建议收藏备用实际写项目时反复对照检查。
返回列表