🧭 ROS2 导航 Navigation2 实战:从建图到避障导航

面向已会 ROS2 基础(话题 / 服务 / launch)的读者 —— 手把手跑通 Nav2 导航栈:建图 → 保存地图 → AMCL 定位 → 全局规划 → 局部控制 → 避障与恢复,参考 Navigation2 官方文档整理。
Nav2 SLAM 建图 AMCL 定位 代价地图 行为树 路径规划
🎯 本页学习目标
1. 能画出 Nav2 的六大核心模块(行为树 BT Navigator、规划器、控制器、代价地图、AMCL、恢复行为)并说清各自职责
2. 能用 SLAM Toolbox 或 Cartographer 建一张栅格地图,读懂 .pgm + .yaml 地图格式与代价含义
3. 能看懂 nav2_params.yaml 的关键参数(代价地图 inflation_radius、规划器插件、控制器插件、行为树流程)
4. 能独立完成:启动仿真 → 建图 → map_saver 保存地图 → 加载地图导航 → RViz 2D Goal Pose 导航 → 避障与恢复
5. 能解释 map → odom → base_link 的 TF 树关系,以及 AMCL 粒子滤波定位原理
6. 能说清人形机器人为什么也用 Nav2,以及轮式与足式导航的差异
建议用时:90-150 分钟(含动手)。前置:建议先读本站 04-09 ROS2 入门实战04-10 URDF 与 MoveIt2,本页默认你已会话题、launch 文件与 RViz2。

1 Nav2 是什么:ROS2 的「生产级」导航栈

Navigation2(Nav2)是 ROS2 官方生态下的生产级自主导航框架,由 Open Robotics 与社区共同维护,截至 2025 年已有超过 100 家公司采用。它把「从 A 点安全走到 B 点」这一件事拆成一组松耦合、可插拔的模块,你既可以用默认插件快速跑通,也可以像换乐高积木一样替换其中的规划器、控制器、行为树。

1.1 与 ROS1 move_base 的关系

Nav2 是 ROS1 move_base 的「精神继承者」,但架构有本质区别:

对比项ROS1 move_baseROS2 Nav2
调度方式写死的状态机(global planner → local planner)行为树(Behavior Tree)驱动,可自由编排任务逻辑
通信Topic/Service,依赖 MasterTopic/Service/Action(导航目标用 Action,带进度反馈),DDS 去中心化
规划器global_planner / navfn 为主Navfn / SmacPlanner(Hybrid-A*) / ThetaStar 等多种插件
控制器DWA / TEB(ROS1 版)DWB / RegulatedPurePursuit / TEB(ROS2 版)/ MPPI
代码组织navigation 单一仓库navigation2 仓库拆成数十个 nav2_* 包,独立迭代
💡 一句话记忆:move_base 是「固定的流水线」,Nav2 是「可编程的行为树」—— 同一套底层算法,但任务编排能力远超 move_base。

1.2 核心模块地图

Nav2 的六大核心模块以及它们在导航中的分工:

模块对应包职责类比
行为树 BT Navigatornav2_bt_navigator顶层调度:按 XML 行为树决定「先规划→再跟随→失败就恢复」的流程大脑皮层(总指挥)
规划器 Plannernav2_planner(nav2_navfn_planner / nav2_smac_planner 等)全局路径:在地图上算一条从起点到目标的长距离路径导航软件(看路)
控制器 Controllernav2_controller(nav2_dwb_controller / nav2_regulated_pure_pursuit_controller 等)局部控制:把全局路径变成速度指令(cmd_vel),实时避障双手方向盘(开车)
代价地图 Costmapnav2_costmap_2d维护「哪里能走、哪里不能走」的 2D 代价网格(全局 + 局部两张)实时地图(眼睛+记忆)
定位 AMCLnav2_amcl粒子滤波定位:根据激光+里程计估计机器人在 map 坐标系里的位姿GPS(我在哪)
恢复行为 Recoverynav2_recoveries(nav2_behaviors)卡住时自救:Spin(原地转)、BackUp(后退)、Wait(等待障碍清除)脱困技巧(自救)

整条数据流大致是:目标 pose → BT Navigator → Planner 算全局路径 → Controller 算 cmd_vel → 底盘执行,期间Costmap 持续更新障碍、AMCL 持续更新位姿,一旦控制器卡住就触发 Recovery

2 环境准备:Ubuntu + ROS2 + Nav2

推荐 Ubuntu 22.04 + ROS2 Humble(最稳、教程最多),或 Ubuntu 24.04 + ROS2 Jazzy(更新,但个别包如 cartographer_ros 官方源暂未发布,需源码编译)。安装 Nav2 与建图工具:

sudo apt update
sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup   # Nav2 核心
sudo apt install ros-humble-slam-toolbox                          # 建图:SLAM Toolbox
sudo apt install ros-humble-cartographer ros-humble-cartographer-ros  # 建图:Cartographer(Humble 有,Jazzy 需源码编译)
# 可选:小乌龟仿真(最常用的 Nav2 入门载体)
sudo apt install ros-humble-turtlebot3*
⚠️ 版本提示:Jazzy 下把 ros-humble-* 换成 ros-jazzy-*;cartographer_ros 在 Jazzy 官方源暂未发布(约截至 2025 年),想用 Cartographer 建议留在 Humble,或从 GitHub 源码编译。

每次新开终端都要先 source 环境(可把全局 setup.bash 写进 ~/.bashrc,工作空间的 setup.bash 手动 source,详见 04-09 页):

source /opt/ros/humble/setup.bash          # 全局环境
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc   # 可选:写进 .bashrc 免重复

仿真载体可选小乌龟 TurtleBot3(教程最多)、Nav2 官方示例机器人,或你自己的差速/阿克曼底盘。人形机器人的轮式移动底座完全可以直接套用差速小车这一套。

3 建图两种方式:SLAM Toolbox 与 Cartographer

导航的第一步是建一张栅格地图(Occupancy Grid Map)——把环境切成一个个小格子,每个格子标注「空闲 / 占用 / 未知」。这由 SLAM(同时定位与建图)算法完成:机器人一边走、一边用激光雷达扫环境、一边估计自己的位姿。

3.1 三种建图方案对比

方案算法特点优点缺点适用
SLAM Toolbox基于粒子滤波(GMapping 的 ROS2 正规继承者)+ 图优化可选安装简单、稳定、支持终生建图与地图序列化大场景回环能力弱于 Cartographer绝大多数入门与中小场景
CartographerGoogle 开源,基于图优化 + 子图(submap)+ 回环检测大场景、回环鲁棒、可 2D/3D、可多传感器融合参数多、调参门槛高、Jazzy 未发布大型/复杂环境、追求精度
GMapping(ROS1 经典)纯粒子滤波 FastSLAM 2.0历史悠久、资料多ROS1 专属,ROS2 里已被 SLAM Toolbox 取代仅 ROS1 项目对照参考

日常推荐 SLAM Toolbox 起步;场景很大、回环很多时再上 Cartographer。两者输出的都是同一种栅格地图,下游 Nav2 用法完全一致。

3.2 用 SLAM Toolbox 建图(命令)

# 终端 1:启动机器人仿真(以小乌龟为例,含底盘+雷达+TF)
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# 终端 2:启动 SLAM Toolbox(在线同步建图)
ros2 launch slam_toolbox online_async_launch.py

# 终端 3:启动 RViz2 看地图,再用键盘/手柄遥控小车走遍房间
ros2 run turtlebot3_teleop teleop_keyboard

遥控小车慢速、完整地走遍整个房间,让激光雷达扫到每一面墙,RViz2 里地图会逐渐被填满。

3.3 保存地图(map_saver)与地图文件格式

建图满意后,保存地图(RViz 打开时地图不变即可保存):

# Nav2 用 map_saver_cli(注意不是 ROS1 的 map_server map_saver)
ros2 run nav2_map_server map_saver_cli -f ~/map

会生成两个文件:map.pgm(灰度图片)与 map.yaml(元数据)。示例 map.yaml:

image: map.pgm
mode: trinary          # trinary(三值)/ scale / raw
resolution: 0.05       # 每像素 = 0.05 米
origin: [-1.0, -1.0, 0.0]   # 地图左下角像素在 map 坐标系下的位姿 [x,y,yaw]
negate: 0              # 0 = 黑是占用;1 = 反转
occupied_thresh: 0.65  # 占用概率 >= 0.65 视为「占用」
free_thresh: 0.196     # 占用概率 <= 0.196 视为「空闲」

3.4 地图代价解释

.pgm 是普通灰度图,黑 = 障碍,白 = 可通行,灰(常为 205)= 未知。加载到 Nav2 后,代价地图把这些像素换算成 0~255 的代价值,数值越大越「危险」:

代价类别数值含义RViz 中颜色
FREE_SPACE(空闲)0完全可走浅色/白
INSCRIBED_INFLATED(内切膨胀)253机器人「内切圆」会撞到的区域,致死深色/紫红
LETHAL_OBSTACLE(致死障碍)254真实障碍物本体
NO_INFORMATION(未知)255没扫到、不知道能否走灰/蓝紫

关键概念是膨胀(Inflate):为了不让机器人「贴墙走」,代价地图会以 inflation_radius 为半径,把障碍物附近的格子代价值从 254 向外逐级衰减到 0。机器人中心离障碍越近,代价越高,规划器和控制器就自然绕开。

4 导航配置详解:nav2_params.yaml

Nav2 的所有行为都由一个参数文件控制,默认叫 nav2_params.yaml(由 ros2 launch nav2_bringup bringup_launch.py 加载)。下面拆解最关键的几块。

4.1 代价地图参数(全局 + 局部两张)

全局代价地图(global_costmap)给规划器用、范围大、更新慢;局部代价地图(local_costmap)给控制器用、范围小、更新快(跟随机器人移动)。两者结构相同:

local_costmap:
  ros__parameters:
    update_frequency: 5.0      # 更新频率(Hz)
    publish_frequency: 2.0     # 发布频率(Hz)
    global_frame: odom         # 局部地图坐标系(跟随机器人,用 odom)
    robot_base_frame: base_link
    rolling_window: true       # 局部地图用滚动窗口
    width: 3                   # 窗口宽(米),rolling_window 时生效
    height: 3
    resolution: 0.05           # 分辨率(米/格)
    robot_radius: 0.22         # 机器人半径(圆形底盘);footprint 二选一
    plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
    inflation_layer:
      plugin: "nav2_costmap_2d::InflationLayer"
      inflation_radius: 0.55   # ★ 膨胀半径:越大越远离障碍,越大越难钻窄缝
      cost_scaling_factor: 3.0 # ★ 代价衰减因子:越大衰减越快(越贴墙)
    obstacle_layer:
      plugin: "nav2_costmap_2d::ObstacleLayer"
      observation_sources: scan
      scan: {topic: /scan, data_type: LaserScan, clearing: true, marking: true}
⚠️ 两个最常调的参数:inflation_radius 决定「离障碍多远开始绕」,要大于机器人半径、略小于最窄门宽的一半;cost_scaling_factor 越大,代价从障碍向外衰减越快,机器人越「敢贴墙」。贴墙蹭到家具调大前者,太保守钻不过门调小前者。

4.2 全局规划器插件(Planner)

插件包名(plugin 字段)算法适用
NavfnPlannernav2_navfn_planner/NavfnPlannerDijkstra(在势场/栅格上)差速、全向,入门默认
SmacPlanner2Dnav2_smac_planner/SmacPlanner2D2D A* / Hybrid-A*差速,较快
SmacPlannerHybridnav2_smac_planner/SmacPlannerHybridHybrid-A*(考虑朝向,支持 Dubin/Reeds-Shepp)阿克曼/车式底盘
ThetaStarPlannernav2_theta_star_planner/ThetaStarPlannerTheta*(任意角度路径)追求更短更直路径
planner_server:
  ros__parameters:
    plugins: ["GridBased"]
    use_sim_time: true
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5            # 目标点容差(米)
      use_astar: false

4.3 局部控制器插件(Controller)

插件plugin 字段算法适用
DWBdwb_core::DWBLocalPlanner采样轨迹打分(ROS1 DWA 的继承者)差速,经典稳妥
RegulatedPurePursuitnav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController纯追踪 + 速度调节差速,默认推荐,参数少
TEB(ROS2)teb_local_planner::TebLocalPlannerROS时间弹性带(Timed Elastic Band),优化时间与位姿车式/阿克曼,需源码编译(非 Nav2 官方包)
MPPInav2_mppi_controller::MPPIController模型预测路径积分(采样+GPU 可选)高动态、对平滑要求高
controller_server:
  ros__parameters:
    plugins: ["FollowPath"]
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"
      desired_linear_vel: 0.22    # 期望线速度(m/s)
      max_linear_vel: 0.26
      min_lookahead_dist: 0.3     # 前视距离(纯追踪核心参数)
      max_lookahead_dist: 0.9
💡 选型建议:差速/小乌龟用 RegulatedPurePursuit 起步(参数最少、最不易抖);要平滑跟车用 DWB;阿克曼车式底盘用 TEBSmacPlannerHybrid 搭配。

4.4 行为树(BT Navigator)流程

Nav2 用行为树 XML 描述导航逻辑,默认行为树叫 navigate_to_pose_w_replanning_and_recovery.xml,核心流程:

bt_navigator:
  ros__parameters:
    default_nav_to_pose_bt_xml: "<path>/navigate_to_pose_w_replanning_and_recovery.xml"

该行为树大致按顺序执行这些节点(节选,实际 XML 里是 <Sequence> / <Fallback> / <RecoveryNode> 组合):

1ComputePathToPose
调用规划器,算一条全局路径
2FollowPath
调用控制器,沿路径走(实时避障)
3判断失败
路径被挡 / 卡住时进入 RecoveryNode 恢复
4Recovery 恢复行为
依次 Spin(原地转)→ BackUp(后退)→ Wait(等待),每执行一次再重试导航

行为树的好处:你可以自己写 XML,插入「到门口先停一下」「绕行检测」等自定义逻辑,而不必改 C++ 源码 —— 这是 Nav2 相比 move_base 最大的升级点。

5 定位:AMCL 粒子滤波

机器人要在地图里导航,必须先知道「我在地图里的哪个位置、朝哪个方向」。Nav2 默认用 AMCL(Adaptive Monte Carlo Localization,自适应蒙特卡洛定位)

5.1 粒子滤波原理

AMCL 是粒子滤波的一种:MCL 在整张地图上撒一堆「粒子」(每个粒子是一个可能的位姿假设),然后反复做两步 —— 运动更新(里程计动了,粒子跟着动)与测量更新(激光扫到墙,离真实墙更吻合的粒子获得更高权重,并重采样)。几轮之后粒子收敛成一团,就是机器人的真实位姿。「Adaptive」指它用 KLD 采样动态调整粒子数量:不确定时粒子多(最多 max_particles),确定时粒子少(最少 min_particles),省算力。

💡 直观理解:想象地图是一张「找自己」的游戏,你闭眼被随机丢进房间,只靠「走几步 + 摸墙」逐步猜出自己站哪。粒子越多、墙摸得越多,猜得越准。

5.2 初始位姿估计与重定位

AMCL 刚启动时粒子均匀撒满整张地图,所以第一件事是告诉它「我大概在哪」——在 RViz2 里用 2D Pose Estimate 按钮(或发布 /initialpose 话题)在地图上点一下机器人真实位置并拖出朝向:

ros2 topic pub /initialpose geometry_msgs/msg/PoseWithCovarianceStamped \
  "{header: {frame_id: 'map'}, pose: {pose: {position: {x: 0.0, y: 0.0, z: 0.0}, \
  orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}}, }" --once

5.3 重定位技巧(被「绑架」问题)

6 实战步骤:一条完整导航链路

把前面全部串起来,跑通「建图 → 保存 → 导航 → 避障」的完整闭环。

6.1 ① 启动仿真 + ② 建图(见第 3 节)

export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py   # 终端 1:仿真
ros2 launch slam_toolbox online_async_launch.py            # 终端 2:建图
ros2 run turtlebot3_teleop teleop_keyboard                 # 终端 3:遥控建图

6.2 ③ 保存地图 → ④ 加载地图导航

ros2 run nav2_map_server map_saver_cli -f ~/map    # 保存 ~/map.pgm + ~/map.yaml

# 建图完成后,关掉 slam_toolbox,改跑导航(加载刚保存的地图 + 启动 AMCL + Nav2)
ros2 launch nav2_bringup bringup_launch.py map:=/home/$USER/map.yaml use_sim_time:=true

bringup_launch.py 会一次性启动:map_server(加载地图)、AMCL(定位)、planner_server、controller_server、bt_navigator、代价地图、RViz2 等,并自动读取 nav2_params.yaml

6.3 ⑤ RViz 里点 2D Goal Pose 导航

1设初始位姿
RViz 工具栏点「2D Pose Estimate」,在地图上机器人真实位置单击并拖出朝向
2给目标点
点「Nav2 Goal」(2D Goal Pose),在地图上点目标位置并拖朝向
3观察链路
绿色全局路径(Planner 算的)出现,机器人开始沿路径走;局部代价地图实时避障
4到达
机器人停在目标点,状态由 ACTIVE 变为 SUCCEEDED

6.4 ⑥ 遇到障碍物避障与恢复(Recovery)

导航途中在机器人前方扔一个箱子(或让一个人挡路),观察两件事:

✅ 通关标准:点 2D Goal Pose 后,机器人能自主规划路径、途中绕开临时障碍、被堵住时触发 Spin/BackUp 恢复并最终到达目标 —— Nav2 导航栈就完整跑通了。

7 人形机器人为什么也用 Nav2

人形机器人的导航需求与轮式机器人高度重叠:自主移动到指定点、跟随人、巡游、避障。这些「去那里」的顶层能力完全可以直接复用 Nav2 的成熟框架,而不用重复造轮子。

7.1 与 3D 感知 / 激光雷达的配合

7.2 轮式 vs 足式导航差异

维度轮式(差速/阿克曼)足式(双足人形 / 四足)
全局规划Nav2 Planner(2D 代价地图)Nav2 Planner 同样适用(把足式整体当作一个「点」)
局部规划/控制DWB / RegulatedPurePursuit 输出 cmd_vel常用深度强化学习(DRL)运动控制器输出步态/落脚点,而非简单 cmd_vel
障碍表达2D 代价足够需额外考虑台阶、斜坡、崎岖地形(3D/高程)
恢复行为Spin / BackUp / Wait需配合步态:原地转向、侧移、跨步,难度更高

结论:全局「去哪儿」用 Nav2,局部「怎么走」分家—— 轮式用经典规划控制器,足式常见做法是 Nav2 出全局路径 + 强化学习局部策略负责落足与平衡(如四足/人形常用的 RL 步态控制器)。这也是为什么「Nav2 是人形机器人导航的地基,而不是全部」。

8 常见坑(排查清单)

症状原因解法
「Timed out waiting for transform from base_link to map」TF 树断了:map → odom 或 odom → base_link 缺一环确认 AMCL(发布 map→odom)与里程计/robot_localization(发布 odom→base_link)都在跑;用 ros2 run tf2_tools view_frames 看 TF 树
机器人在 RViz 里飘 / 穿墙AMCL 初始位姿给错,或里程计漂移重新点 2D Pose Estimate;检查里程计标定
导航「抖」或原地打转控制器前视距离 / 速度参数不合适调 min/max_lookahead_dist、max_linear_vel,或换 RegulatedPurePursuit
CPU 满载、地图卡顿代价地图分辨率太高 / 更新频率太高resolution 从 0.05 降到 0.1,降低 update_frequency
找不到对方节点 / 通信异常DDS 发现失败(多机、Docker、混杂 RMW)统一用 CycloneDDS:sudo apt install ros-humble-rmw-cyclonedds-cppexport RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
仿真里 TF 老报错use_sim_time 没对齐所有 launch 都加 use_sim_time:=true(或参数文件里统一设)
⚠️ 坐标系关系记牢:map → odom → base_footprint → base_link。其中 map→odom 由 AMCL 发布(修正累积漂移),odom→base_link 由里程计发布(高频、连续)。map 是「世界固定」系,odom 是「里程计累积」系,两者靠 AMCL 对齐。

9 本节自测

1. Nav2 相比 ROS1 move_base 最本质的架构升级是什么?
💡 move_base 是写死的状态机,Nav2 用行为树 XML 编排「规划→跟随→恢复」,这是最核心的架构升级。
2. 保存建图结果时,Nav2 应该用哪个命令?
💡 Nav2 里地图保存由 nav2_map_server 的 map_saver_cli 完成;ROS1 才是 map_server map_saver。
3. 代价地图中 LETHAL_OBSTACLE(致死障碍)的代价值通常是?
💡 0 = 空闲,253 = 内切膨胀,254 = 致死障碍,255 = 未知(NO_INFORMATION)。
4. inflation_radius 参数的作用是什么?
💡 inflation_radius 是 InflationLayer 的核心参数,把障碍代价向外逐级衰减,让规划绕开障碍。
5. 关于 map → odom → base_link 的 TF 关系,下列哪项正确?
💡 AMCL 发布 map→odom(校正漂移),里程计/robot_localization 发布 odom→base_link(高频连续)。
6. 机器人被堵死时,Nav2 默认的恢复行为顺序通常是?
💡 默认行为树在控制器失败后进入 RecoveryNode,依次执行 Spin、BackUp、Wait 恢复行为后再重试。
7. 人形机器人做「全局去哪儿」的规划时,最常见的做法是?
💡 轮式局部用经典规划控制器,足式常见做法是 Nav2 全局 + RL 局部策略,分层解耦。
8. 关于 cost_scaling_factor(代价衰减因子),下列说法正确的是?
💡 见 4.1:cost_scaling_factor 越大,代价从障碍向外衰减越快,机器人越「敢贴墙」;膨胀半径由 inflation_radius 决定,两者别搞混。
9. AMCL 名字里的「Adaptive(自适应)」具体指什么?
💡 见 5.1:AMCL 用 KLD 采样动态调整粒子数,不确定时最多 max_particles、确定时最少 min_particles,省算力;不是固定满负荷运行。
10. 阿克曼(车式)底盘做全局路径规划,更适合选用哪个插件?
💡 见 4.2 表格:SmacPlannerHybrid 是 Hybrid-A*,考虑朝向、支持 Dubin/Reeds-Shepp,适合阿克曼/车式底盘;DWB 是控制器而非全局规划器。
📌 本节要点速查:① Nav2 = 行为树驱动的六大模块(BT Navigator / Planner / Controller / Costmap / AMCL / Recovery);② 建图用 SLAM Toolbox(入门)或 Cartographer(大场景),map_saver_cli 保存 .pgm + .yaml;③ 代价地图 0=空闲、254=致死障碍、255=未知,inflation_radius 定绕障距离、cost_scaling_factor 越大越贴墙;④ AMCL 粒子滤波 + KLD 自适应粒子数,靠 map→odom→base_link TF 链定位;⑤ 人形机器人「全局用 Nav2、局部足式用 RL 步态」分层解耦。
📌 本节小结:Nav2 是 ROS2 的生产级导航栈,核心是行为树驱动的六大模块(BT Navigator / Planner / Controller / Costmap / AMCL / Recovery);建图用 SLAM Toolbox 或 Cartographer,产出 .pgm + .yaml 栅格地图;导航配置集中在 nav2_params.yaml(代价地图 inflation_radius、规划器、控制器、行为树);AMCL 用粒子滤波定位,靠 map→odom→base_link 的 TF 链串联;实战按「仿真→建图→存图→加载导航→2D Goal Pose→避障恢复」跑通,人形机器人复用 Nav2 做全局规划、局部再分轮式与足式处理。
🤔 思考题: 1. 如果把 inflation_radius 调得比房间门宽的一半还大,会发生什么?为什么?
2. 机器人被抱起来换了个房间放下,AMCL 还能自己恢复吗?为什么必须重新给初始位姿?
3. 把 3D 激光点云「投影成 2D 扫描」喂给 Nav2,可能丢失哪些信息?这些信息对足式机器人重要吗?

10 参考来源

📅 资料整理更新至 2026-08 · 关键事实(包名、命令、参数名、插件名)已经 web_search 核实。