실패는 버그가 아니라 입력이다
자율 로봇을 만들다 보면 처음에는 이런 목표를 세우기 쉽다. 실패하지 않는 로봇을 만들자. 하지만 실제 작업대 위에서 로봇을 움직여 보면 곧 알게 된다. 실패하지 않는 로봇은 거의 없다. 카메라는 컵을 못 볼 수 있고, 로봇팔은 경로를 못 찾을 수 있으며, 그리퍼는 물체를 살짝 밀어 버릴 수 있다. 이동 로봇은 갑자기 지나가는 사람 때문에 멈출 수 있다.
그래서 AI 로봇 에이전트의 진짜 목표는 조금 다르게 잡아야 한다. 실패하지 않는 로봇이 아니라, 실패한 뒤 안전하게 회복하는 로봇을 만드는 것이다.
사람도 마찬가지다. 책상 위 컵을 집으려다 손끝이 미끄러지면 다시 보고 손 위치를 바꾼다. 문으로 가려는데 누군가 앞을 막고 있으면 잠시 기다리거나 옆길로 돌아간다. 상자를 들었는데 예상보다 무거우면 두 손을 쓰거나 도움을 요청한다. 지능은 한 번에 맞히는 능력만이 아니다. 틀렸을 때 멈추고, 원인을 살피고, 다른 방법을 고르는 능력이다.
LLM 로봇 에이전트에서도 같은 원칙이 필요하다. LLM은 처음 계획을 만들 수 있다. 하지만 실제 세계는 계획대로만 움직이지 않는다. 따라서 실행 계층은 실패를 감지하고, 디스패처는 실패를 구조화하고, LLM은 그 정보를 바탕으로 새 계획을 세워야 한다.
계획 생성
↓
스킬 실행
↓
성공 여부 확인
↓
실패 원인 구조화
↓
재계획 또는 사용자 확인
↓
다시 실행 또는 안전 중단이 장에서는 이 루프를 만든다. 핵심은 단순하다. 로봇에게 실패를 숨기지 말고, 실패를 잘 말하게 하자. 그래야 LLM이 같은 실수를 반복하지 않는다.

실패 감지는 LLM이 아니라 스킬이 한다
먼저 중요한 경계를 분명히 하자. 실패를 실제로 감지하는 주체는 대부분 LLM이 아니다. LLM은 카메라 픽셀을 직접 보지 않고, 모터 전류를 직접 읽지 않으며, MoveIt2 경로 탐색 내부를 실시간으로 들여다보지 않는다. 실패의 1차 감지는 ROS2 스킬 서버, 센서 노드, 컨트롤러, 안전 모니터가 맡아야 한다.
예를 들어 빨간 컵을 찾는 상황을 생각해 보자. 컵이 보이지 않는지 판단하는 것은 detect_object 스킬이다. 로봇팔이 목표 자세까지 갈 수 없는지 판단하는 것은 move_to 또는 pick 내부의 경로 계획기다. 그리퍼가 물체를 실제로 잡았는지 판단하는 것은 그리퍼 폭, 힘 센서, 전류 변화, 들어 올린 뒤의 시각 검증이 맡는다. 이동 중 장애물이 생겼는지 판단하는 것은 라이다, 깊이 카메라, Nav2, 안전 정지 노드가 맡는다.
LLM의 역할은 그 다음이다. 스킬들이 감지한 실패를 읽고, 의미를 해석하고, 다음 행동을 고르는 것이다. 이때 실패 정보가 너무 빈약하면 LLM은 추측을 시작한다. failed만 받으면 LLM은 왜 실패했는지 모른다. 물체가 안 보였는지, 경로가 막혔는지, 그리퍼가 헛집었는지, 사용자가 중간에 정지시켰는지 구분할 수 없다.
따라서 각 스킬은 성공 여부만 반환해서는 안 된다. 실패가 났다면 적어도 어느 단계에서, 어떤 이유로, 현재 상태가 어떤지, 다시 시도할 수 있는지까지 알려 줘야 한다.
skill_result:
success: false
skill: pick
stage: grasping
reason_code: empty_grasp
message: 그리퍼를 닫았지만 물체 접촉이 감지되지 않았습니다.
retryable: true
robot_state:
arm_ready: true
gripper_ready: true
holding_object: false
suggested_recovery:
- redetect_object
- try_next_grasp_candidate
- ask_user_if_object_is_occluded이런 결과는 로봇의 짧은 보고서다. 사람이 읽어도 이해할 수 있고, LLM도 다음 계획의 재료로 쓸 수 있다.
상태 코드가 있어야 재계획이 가능하다
실패를 제대로 다루려면 먼저 실패의 이름을 정해야 한다. 이름이 없으면 대화도, 로그도, 재계획도 흐릿해진다. 개발자가 로그를 볼 때마다 something went wrong만 나온다면 어디를 고쳐야 할지 알 수 없다. LLM도 마찬가지다.
상태 코드는 로봇이 실패를 분류하는 공통 언어다. 모든 스킬이 제각각 다른 표현을 쓰면 상위 에이전트가 처리하기 어렵다. not found, object missing, no detection, vision failed가 모두 같은 뜻이라면 하나의 코드로 통일하는 편이 좋다.
다음은 AI 로봇 에이전트에서 자주 쓰는 실패 코드 예시다.
| 범주 | reason_code | 의미 | 대표 대응 |
|---|---|---|---|
| 인식 | object_not_found | 요청한 물체를 찾지 못함 | 다시 검출, 시점 변경, 사용자 확인 |
| 인식 | low_confidence | 물체 후보는 있지만 신뢰도가 낮음 | 추가 관측, 후보 비교, 사용자 확인 |
| 인식 | object_occluded | 물체가 다른 물체에 가려짐 | 다른 각도에서 보기, 장애물 제거 요청 |
| 경로 계획 | no_ik_solution | 목표 자세에 대한 관절 해를 찾지 못함 | 목표 자세 수정, 접근 방향 변경 |
| 경로 계획 | path_collision | 계획 경로가 장애물과 충돌함 | 우회 경로, 중간 경유점, 물체 재배치 요청 |
| 경로 계획 | planning_timeout | 제한 시간 안에 경로를 찾지 못함 | 단순한 목표로 분해, 재시도, 다른 플래너 사용 |
| 그리퍼 | empty_grasp | 그리퍼를 닫았지만 물체가 잡히지 않음 | 다시 검출, 다른 집기 후보 사용 |
| 그리퍼 | slip_detected | 들어 올리는 중 물체가 미끄러짐 | 집기 힘 조정, 측면 잡기, 속도 낮추기 |
| 그리퍼 | force_limit_exceeded | 허용 힘을 넘었거나 끼임 위험이 있음 | 즉시 정지, 그리퍼 열기, 사람 확인 |
| 이동 | obstacle_blocking_path | 이동 중 장애물이 경로를 막음 | 대기, 우회, 사용자 확인 |
| 이동 | goal_unreachable | 목적지까지 도달 가능한 경로가 없음 | 다른 목표점, 가까운 위치로 변경 |
| 이동 | localization_lost | 로봇 위치 추정이 불안정함 | 정지, 재초기화, 사람 개입 |
| 시스템 | precondition_failed | 실행 전 조건이 맞지 않음 | 상태 점검, 필요한 선행 스킬 실행 |
| 시스템 | safety_stop_triggered | 안전 정지가 발생함 | 즉시 중단, 원인 확인, 사람 개입 |
| 시스템 | user_cancelled | 사용자가 작업을 취소함 | 작업 종료, 안전 자세 유지 |
이 표를 처음부터 완벽하게 만들 필요는 없다. 오히려 작게 시작하는 것이 좋다. 처음에는 object_not_found, path_collision, empty_grasp, obstacle_blocking_path, safety_stop_triggered 정도만 있어도 충분하다. 중요한 것은 코드가 안정적으로 유지되는 것이다. 로그마다 코드 이름이 바뀌면 재계획 정책을 만들 수 없다.
상태 코드는 사람을 위한 문장과 함께 제공해야 한다. empty_grasp는 프로그램이 쓰기 좋고, 그리퍼를 닫았지만 물체 접촉이 감지되지 않았습니다는 사람이 이해하기 좋다. 둘 다 필요하다.
단계와 원인을 함께 기록하라
실패 코드는 단독으로도 유용하지만, 실제로는 stage와 함께 있을 때 훨씬 강력하다. 같은 실패라도 어느 단계에서 발생했는지에 따라 대응이 달라지기 때문이다.
pick 스킬을 예로 들어 보자. 이 스킬은 하나의 동작처럼 보이지만 내부적으로는 여러 단계로 나뉜다.
pick
planning: 집기 목표 자세와 접근 경로 계산
approaching: 물체 근처의 pre-grasp pose로 이동
aligning: 카메라나 센서로 마지막 위치 보정
grasping: 그리퍼를 닫아 물체를 잡음
lifting: 물체를 들어 올려 잡힘 여부 확인만약 path_collision이 planning에서 발생했다면 로봇팔이 물체까지 가는 경로를 찾지 못한 것이다. 이때는 접근 방향을 바꾸거나 중간 경유점을 추가해야 한다. 반면 path_collision이 lifting에서 발생했다면 물체를 들어 올리는 경로가 주변 물체와 충돌할 위험이 있다는 뜻일 수 있다. 이때는 더 수직으로 들어 올리거나 주변 장애물을 피하는 방향으로 목표를 바꿔야 한다.
empty_grasp도 마찬가지다. grasping에서 발생했다면 집기 후보가 틀렸거나 물체 위치가 조금 바뀌었을 가능성이 크다. 하지만 lifting 이후 시각 검증에서 발생했다면 처음에는 잡았지만 들어 올리는 동안 놓쳤을 가능성이 있다.
따라서 결과 메시지는 다음처럼 stage와 reason_code를 함께 가져야 한다.
result:
success: false
skill: pick
stage: lifting
reason_code: slip_detected
message: 컵을 들어 올리는 동안 그리퍼 폭이 갑자기 벌어졌고 물체가 아래로 이동했습니다.
retryable: true
suggested_recovery:
- redetect_object
- use_side_grasp
- reduce_lift_speed이 정도 정보가 있으면 LLM은 단순히 같은 pick을 반복하지 않는다. 들어 올리는 속도를 줄이거나, 측면 잡기를 선택하거나, 다시 위치를 확인한 뒤 집는 계획을 만들 수 있다.
실패 번역기 만들기
ROS2 시스템 내부의 오류 메시지는 LLM과 사용자에게 그대로 보여 주기에 적합하지 않은 경우가 많다. 예를 들어 경로 계획기가 No solution found 같은 짧은 메시지를 반환하거나, 컨트롤러가 특정 라이브러리의 예외 이름을 던질 수 있다. 개발자에게는 단서가 되지만, LLM이 재계획하기에는 정보가 부족하거나 너무 저수준일 수 있다.
그래서 디스패처에는 실패 번역기가 필요하다. 실패 번역기는 스킬 서버에서 온 원시 오류를 공통 결과 형식으로 바꾸는 계층이다.
원시 오류
↓
실패 번역기
↓
공통 reason_code
↓
자연어 설명
↓
재계획 힌트예를 들어 MoveIt2 경로 계획 실패가 발생했다고 하자. 내부 오류는 단순히 계획 실패로만 나올 수 있다. 실패 번역기는 현재 목표 자세, 충돌 검사 결과, 타임아웃 여부, 로봇 작업 공간 범위를 함께 보고 더 구체적인 메시지를 만든다.
raw_error:
source: moveit2
text: planning failed
timeout: true
translated_failure:
success: false
skill: move_to
stage: planning
reason_code: planning_timeout
message: 제한 시간 안에 목표 자세로 가는 안전한 경로를 찾지 못했습니다.
retryable: true
suggested_recovery:
- add_intermediate_waypoint
- try_alternative_approach_pose
- simplify_motion인식 실패도 마찬가지다.
raw_error:
source: detector
candidates: 0
label: cup
color: red
translated_failure:
success: false
skill: detect_object
stage: perception
reason_code: object_not_found
message: 현재 카메라 시야에서 빨간 컵을 찾지 못했습니다.
retryable: true
suggested_recovery:
- redetect_with_wider_region
- move_camera_to_scan_workspace
- ask_user_to_place_object_in_view좋은 실패 번역기는 세 가지를 분리한다. 첫째, 프로그램이 처리할 코드는 reason_code로 제공한다. 둘째, LLM과 사용자가 이해할 설명은 message로 제공한다. 셋째, 다음 행동 후보는 suggested_recovery로 제공한다. 이 셋을 섞지 않는 것이 중요하다.
LLM에게 전달할 실패 보고서
LLM에게 실패 정보를 줄 때는 너무 길어도 안 되고, 너무 짧아도 안 된다. 카메라 이미지 전체, 라이다 포인트 클라우드, 모든 ROS 로그를 넣는 것은 낭비다. 반대로 실패함 한마디만 넣으면 재계획이 불가능하다.
LLM에게는 실행 판단에 필요한 요약을 제공한다.
failure_report:
task_id: task_034
failed_step: 3
failed_skill: pick
stage: grasping
reason_code: empty_grasp
summary: 빨간 컵을 집으려 했지만 그리퍼가 빈 공간을 집었습니다.
facts:
object_id: obj_cup_17
object_still_visible: true
estimated_object_shift:
x: 0.025
y: -0.010
z: 0.000
arm_ready: true
gripper_ready: true
holding_object: false
allowed_recovery_actions:
- detect_object
- pick
- check_status
- ask_user
- stop
forbidden_actions:
- ignore_failure_and_continue_to_place
- increase_force_beyond_limit
retry_count_for_same_reason: 1여기서 중요한 필드는 allowed_recovery_actions와 forbidden_actions다. LLM에게 실패를 알려 주는 것만으로는 부족하다. 어떤 회복 행동이 허용되는지, 어떤 행동은 금지되는지도 함께 알려야 한다. 예를 들어 빈 grasp가 발생했을 때 그리퍼 힘을 무작정 올리는 것은 위험할 수 있다. 따라서 안전 정책은 increase_force_beyond_limit을 금지 행동으로 명시한다.
LLM은 이 보고서를 바탕으로 새 계획을 만든다.
replan:
reason: 물체 위치가 약간 이동했으므로 다시 검출한 뒤 다른 집기 후보를 사용한다.
steps:
- skill: detect_object
args:
label: cup
color: red
region: last_known_area
- skill: pick
args:
object_ref: step_1.object_id
grasp_hint: side물론 이 재계획도 바로 실행해서는 안 된다. 직전 장에서 만든 디스패처가 다시 검증해야 한다. LLM이 만든 계획은 언제나 후보이고, 실행 권한은 검증기를 통과한 뒤에만 주어진다.
재계획 패턴 1: 다시 보기
가장 흔한 실패는 인식 실패다. 로봇이 물체를 못 찾았다고 해서 곧바로 작업을 포기할 필요는 없다. 사람도 물건이 안 보이면 고개를 조금 움직이고, 책상 위를 다시 보고, 조명을 확인한다. 로봇도 비슷한 패턴을 쓸 수 있다.
object_not_found가 발생했을 때의 기본 재계획은 다시 보기다. 다만 같은 방식으로 무작정 반복하면 안 된다. 같은 카메라, 같은 각도, 같은 영역에서 계속 검출하면 같은 실패가 반복될 가능성이 높다. 다시 보기는 조건을 바꾸는 재시도여야 한다.
첫 번째 시도: 전체 작업대에서 빨간 컵 검출
실패
두 번째 시도: 마지막으로 컵이 있었던 영역 주변을 넓게 검출
실패
세 번째 시도: 카메라 시점을 약간 바꾸거나 로봇팔을 관측 자세로 이동
실패
사용자 확인: 빨간 컵이 카메라에서 보이지 않습니다. 작업대 안쪽에 놓아 주시겠어요?다시 보기 패턴에는 몇 가지 변형이 있다. 고정 카메라를 쓰는 로봇이라면 검출 영역을 넓히거나 색상 조건을 완화할 수 있다. 손목 카메라가 있는 로봇팔이라면 관측 자세로 이동해 다른 각도에서 볼 수 있다. 이동 로봇이라면 조금 앞으로 이동하거나 고개를 돌려 주변을 스캔할 수 있다.
하지만 모든 다시 보기는 안전 조건을 통과해야 한다. 카메라를 보려고 로봇팔을 움직이는 것 자체가 충돌 위험을 만들 수 있기 때문이다. 따라서 move_to_observe_pose 같은 스킬도 일반 move_to와 마찬가지로 작업 공간, 속도, 충돌 검사를 거쳐야 한다.
재계획 패턴 2: 다른 경로 선택하기
두 번째로 흔한 실패는 경로 계획 실패다. 목표는 맞는데 로봇이 그곳까지 안전하게 갈 수 없는 상황이다. 로봇팔에서는 목표 자세로 가는 관절 경로가 없거나, 경로 중간에 장애물이 있을 수 있다. 이동 로봇에서는 복도에 사람이 서 있거나, 지도상 통로가 막혀 있을 수 있다.
경로 실패를 처리할 때는 먼저 실패의 종류를 구분해야 한다. no_ik_solution은 목표 자세 자체가 로봇팔이 도달하기 어려운 위치라는 뜻에 가깝다. 이때 같은 목표를 계속 보내도 소용없다. 목표 자세를 바꾸거나, 접근 방향을 바꾸거나, 물체를 다른 방식으로 잡아야 한다.
반면 path_collision은 목표 자세는 가능하지만 가는 길이 막혔다는 뜻일 수 있다. 이때는 중간 경유점을 추가하거나, 더 높이 들어 올린 뒤 접근하거나, 반대쪽에서 접근하는 전략이 가능하다.
failure:
skill: pick
stage: planning
reason_code: path_collision
message: 컵 왼쪽에 있는 상자 때문에 위쪽 접근 경로가 충돌합니다.
replan:
strategy: alternative_approach
steps:
- skill: pick
args:
object_ref: obj_cup_17
grasp_hint: side_from_right이동 로봇에서는 조금 다른 재계획이 필요하다. obstacle_blocking_path가 발생했을 때 장애물이 일시적인지, 고정된 장애물인지 판단해야 한다. 사람이 지나가는 중이라면 잠시 기다리는 것이 좋다. 문 앞에 박스가 놓여 있다면 우회 경로를 찾아야 한다. 모든 경로가 막혀 있으면 사용자에게 확인해야 한다.
장애물 발생
↓
짧게 대기하며 변화 확인
↓
장애물이 사라지면 기존 경로 계속 실행
↓
사라지지 않으면 우회 경로 요청
↓
우회 경로도 실패하면 사용자에게 선택 요청중요한 것은 로봇이 경로 실패를 힘으로 밀어붙이지 않는 것이다. 로봇팔이 장애물을 밀면서 지나가거나, 이동 로봇이 사람을 향해 계속 전진하면 안 된다. 경로 실패는 새로운 계획을 요구하는 신호이지, 더 강한 명령을 보내라는 신호가 아니다.
재계획 패턴 3: 다른 집기 후보 사용하기
세 번째 재계획 패턴은 다른 집기 후보를 사용하는 것이다. 물체 집기는 생각보다 섬세하다. 컵의 중심점을 정확히 찾아도 손잡이 때문에 미끄러질 수 있고, 상자의 모서리를 잡으려다 손가락이 빗나갈 수 있다. 바운딩 박스 중심이 항상 좋은 집기 지점은 아니다.
그래서 pick 스킬은 가능하면 여러 집기 후보를 만들어 두는 편이 좋다.
grasp_candidates:
- id: grasp_top_01
type: top
score: 0.82
risk: medium
- id: grasp_side_02
type: side
score: 0.77
risk: low
- id: grasp_handle_03
type: handle
score: 0.64
risk: medium처음에는 점수가 가장 높은 후보를 선택할 수 있다. 하지만 실패하면 같은 후보를 반복하지 말고 후보 목록을 갱신해야 한다. 특히 empty_grasp가 발생했다면 물체 위치가 바뀌었을 수 있으므로 다시 검출한 뒤 후보를 다시 계산하는 것이 좋다. slip_detected가 발생했다면 접촉은 했지만 안정성이 부족했다는 뜻이므로 다른 방향의 grasp를 고른다.
empty_grasp
→ 다시 검출
→ 물체 위치 갱신
→ 이전 후보 제외
→ 다음 후보로 집기
slip_detected
→ 물체 재검출
→ 더 넓은 접촉면 후보 선택
→ 들어 올리는 속도 낮춤
→ 허용 범위 안에서 그리퍼 힘 조정그리퍼 실패에서 가장 조심해야 할 것은 힘을 무작정 올리는 것이다. 물체가 말랑하거나 깨지기 쉬우면 힘을 올리는 순간 손상될 수 있다. 사람 손이나 케이블이 끼어 있다면 더 위험하다. 따라서 그리퍼 힘 조정은 물체 종류, 안전 센서, 허용 범위 안에서만 이루어져야 한다. LLM이 더 세게 잡기를 제안하더라도 디스패처와 스킬 서버가 제한해야 한다.
재계획 루프 구현하기
이제 구조를 코드 수준으로 내려 보자. 재계획 루프는 복잡해 보이지만 기본 뼈대는 단순하다. 계획을 실행하고, 실패하면 실패 보고서를 만들고, 정책에 따라 다시 계획하거나 멈춘다.
def run_task(user_command):
context = build_initial_context()
plan = make_plan_with_llm(user_command, context)
failure_history = []
while True:
result = dispatch_plan(plan, context)
if result.success:
return report_success(result, context)
failure = translate_failure(result)
failure_history.append(failure)
log_failure(failure)
decision = decide_recovery(failure, failure_history, context)
if decision.type == 'retry_with_rule':
plan = build_rule_based_recovery_plan(failure, context)
continue
if decision.type == 'replan_with_llm':
report = build_failure_report(failure, failure_history, context)
plan = make_replan_with_llm(user_command, report)
plan = validate_plan(plan)
continue
if decision.type == 'ask_user':
question = build_user_question(failure, context)
return ask_user(question)
if decision.type == 'stop':
move_to_safe_state_if_needed()
return report_stop(failure)여기서 decide_recovery가 핵심이다. 모든 실패를 LLM에게 바로 넘길 필요는 없다. 너무 단순한 실패는 규칙 기반으로 처리하는 편이 빠르고 안정적이다. 예를 들어 객체 검출 신뢰도가 낮으면 한 번 더 검출하는 정도는 규칙으로 처리할 수 있다. 반면 여러 단계가 얽힌 실패, 사용자 의도와 관련된 선택, 대체 위치 제안은 LLM에게 맡기는 것이 좋다.
좋은 구조는 규칙과 LLM을 섞는다.
규칙 기반 회복에 적합한 경우
- 같은 물체를 한 번 더 검출하기
- 이전 집기 후보를 제외하고 다음 후보 사용하기
- 짧은 시간 대기 후 이동 경로 재계획하기
LLM 재계획에 적합한 경우
- 사용자의 목표를 유지하면서 대체 방법을 찾아야 할 때
- 놓기 위치나 대상이 모호해졌을 때
- 여러 스킬 순서를 다시 구성해야 할 때
- 사용자에게 어떤 선택지를 제시할지 정해야 할 때LLM은 강력하지만 느리고, 때로는 그럴듯한 잘못된 계획을 만든다. 따라서 빠르고 명확한 복구는 규칙으로 처리하고, 의미 해석이 필요한 복구는 LLM에게 맡기는 편이 실용적이다.
반복 실패를 멈추는 기준
재계획에서 가장 위험한 버그는 무한 반복이다. 로봇이 컵을 못 찾는데 계속 같은 위치를 보고, 계속 실패하고, 다시 보고, 또 실패하는 상황이다. 이동 로봇이 막힌 복도 앞에서 계속 경로를 재계산하는 것도 마찬가지다. 이런 로봇은 똑똑해 보이지 않는다. 답답하고 위험해 보인다.
따라서 반복 실패를 멈추는 기준을 코드로 정해야 한다. 프롬프트에만 너무 많이 반복하지 마라고 쓰는 것으로는 부족하다. 디스패처가 시도 횟수, 시간, 실패 종류, 안전 상태를 추적해야 한다.
| 조건 | 권장 행동 |
|---|---|
| 같은 스킬과 같은 reason_code가 2회 이상 반복 | 다른 전략으로 전환하거나 사용자 확인 |
| 같은 작업 전체가 3회 이상 실패 | 작업 중단 후 사용자에게 상황 보고 |
| safety_stop_triggered 발생 | 즉시 중단, 사람 개입 요청 |
| force_limit_exceeded 발생 | 즉시 중단, 그리퍼와 주변 상태 확인 |
| localization_lost 발생 | 이동 중단, 위치 재초기화 또는 사람 개입 |
| object_not_found가 여러 시점에서도 반복 | 사용자에게 물체 위치 확인 요청 |
| path_collision이 대체 경로에서도 반복 | 작업 공간 정리 또는 사람 개입 요청 |
| 사용자가 취소 | 즉시 작업 종료 |
| 제한 시간 초과 | 안전 자세로 이동 후 실패 보고 |
이 기준은 로봇 종류와 작업 위험도에 따라 달라질 수 있다. 교육용 시뮬레이터에서는 5회 재시도를 허용해도 괜찮을 수 있다. 하지만 실제 로봇팔이 유리컵을 다루거나 사람 가까이에서 움직인다면 1회 실패 후 바로 사용자 확인을 요구하는 편이 낫다.
반복 실패 정책은 실패 보고서에도 들어가야 한다.
retry_policy_state:
same_reason_count: 2
total_task_failure_count: 3
time_elapsed_sec: 84
remaining_allowed_retries: 0
next_allowed_action: ask_user_or_stopLLM이 이 정보를 받으면 무리한 재시도를 줄일 수 있다. 하지만 최종 제한은 디스패처가 강제해야 한다. LLM이 다시 시도하겠다고 해도 remaining_allowed_retries가 0이면 실행하지 않아야 한다.
사용자에게 물어봐야 하는 순간
좋은 로봇 에이전트는 모든 문제를 혼자 해결하려고 하지 않는다. 스스로 해결할 수 없는 문제, 사용자 의도가 필요한 문제, 안전 판단이 필요한 문제에서는 질문해야 한다. 질문은 실패가 아니라 협업이다.
사용자 확인이 필요한 대표 상황은 다음과 같다.
물체가 보이지 않거나 여러 후보가 있어 대상을 확정할 수 없을 때
원래 목표 위치가 막혀 있어 대체 위치를 선택해야 할 때
로봇이 물체나 환경을 건드릴 위험이 있을 때
반복 실패 후 추가 시도가 의미 없거나 위험할 때
안전 정지, 센서 오류, 위치 추정 실패처럼 사람 확인이 필요한 상태일 때
질문은 구체적이어야 한다. 어떻게 할까요?라고만 묻는 것은 사용자를 당황하게 만든다. 로봇이 무엇을 보았고, 무엇이 문제이며, 어떤 선택지가 있는지 알려 줘야 한다.
나쁜 질문:
문제가 생겼습니다. 어떻게 할까요?
좋은 질문:
빨간 컵이 카메라에서 보이지 않습니다. 컵을 작업대 중앙으로 옮겨 주시면 다시 시도할 수 있습니다. 다시 찾아볼까요?
좋은 질문:
노트북 오른쪽 공간이 다른 물체로 막혀 있습니다. 노트북 오른쪽 앞쪽의 빈 공간에 놓아도 될까요?
좋은 질문:
회의실로 가는 복도가 30초 이상 막혀 있습니다. 왼쪽 복도로 우회할까요, 아니면 여기서 기다릴까요?질문에는 가능한 답변 형식도 함께 주면 좋다. 예, 아니오, 우회해, 기다려, 중단해처럼 간단히 답할 수 있어야 한다. 사용자가 답하면 LLM은 그 답을 새 제약으로 넣어 재계획한다.
사람 개입으로 전환하기
어떤 실패는 재계획 대상이 아니다. 즉시 사람에게 넘겨야 한다. 특히 안전과 관련된 실패가 그렇다. 안전 정지가 발생했는데 LLM이 다시 시도해 보겠습니다라고 말하면 안 된다. 힘 제한 초과, 끼임 가능성, 위치 추정 상실, 컨트롤러 오류, 하드웨어 에러는 자동 복구보다 안전 확보가 먼저다.
사람 개입으로 전환할 때 로봇은 다음 순서를 따른다.
1. 현재 동작을 취소하거나 정지한다.
2. 가능한 경우 안전 자세로 이동한다.
3. 물체를 들고 있다면 상황에 따라 유지하거나 안전한 위치에 내려놓는다.
4. 모터, 그리퍼, 이동 베이스 상태를 안전 모드로 전환한다.
5. 사용자와 운영자에게 실패 이유를 보고한다.
6. 로그와 센서 스냅샷을 저장한다.여기서 물체를 들고 있다면 내려놓는다는 항상 정답이 아니다. 뜨거운 컵, 깨지기 쉬운 유리, 사람 근처의 물체라면 함부로 놓는 것이 더 위험할 수 있다. 따라서 물체를 들고 있는 상태에서 안전 정지가 발생하면 스킬 서버는 작업별 정책을 따라야 한다. 가능한 경우 그 자리에서 유지하고, 사람에게 개입을 요청하는 편이 안전할 수 있다.
사람 개입 요청도 구조화해서 남기면 좋다.
human_intervention_request:
severity: high
reason_code: safety_stop_triggered
summary: 로봇팔 작업 중 안전 정지가 발생했습니다.
robot_state:
arm_motion: stopped
gripper_state: holding
holding_object: true
requested_action:
- 주변 장애물 확인
- 그리퍼에 끼인 물체 확인
- 안전 확인 후 reset 명령 입력이 메시지는 운영자 화면, 로그, 음성 안내, 모바일 알림 등으로 보낼 수 있다. 중요한 것은 로봇이 조용히 멈춰 있는 것이 아니라, 왜 멈췄고 무엇을 해야 하는지 알려 주는 것이다.
통합 예제: 빨간 컵을 놓기 전의 실패
앞 장에서 다룬 예제를 이어 보자. 사용자가 말한다.
빨간 컵을 집어서 노트북 오른쪽에 놓아줘.처음 계획은 단순하다.
1. 빨간 컵을 찾는다.
2. 노트북을 찾는다.
3. 컵을 집는다.
4. 노트북 오른쪽에 놓는다.
5. 놓인 상태를 확인한다.이번에는 pick까지는 성공했다고 하자. 로봇은 컵을 들고 있다. 그런데 place 단계에서 실패한다.
skill_result:
success: false
skill: place
stage: planning
reason_code: path_collision
message: 노트북 오른쪽 목표 위치로 내려가는 경로가 마우스와 충돌할 가능성이 있습니다.
retryable: true
robot_state:
holding_object: true
arm_ready: true
gripper_ready: true
observation:
blocking_object: mouse
target_relation: right_of_laptop
suggested_recovery:
- try_nearby_place_pose
- ask_user_for_alternative_location
- stop_and_hold_object이 실패를 받은 LLM이 바로 place를 다시 호출하면 나쁜 재계획이다. 같은 목표 위치와 같은 경로라면 다시 충돌할 가능성이 높다. 더 좋은 재계획은 노트북 오른쪽 주변의 다른 빈 공간을 찾거나, 사용자에게 대체 위치를 확인하는 것이다.
replan:
reason: 원래 놓기 위치가 마우스와 충돌하므로 노트북 오른쪽 앞쪽의 빈 공간을 대체 후보로 사용한다.
steps:
- skill: place
args:
reference_object_id: obj_laptop_03
relation: right_front_of
offset:
x: 0.12
y: 0.18
z: 0.00디스패처는 이 재계획을 검증한다. right_front_of 관계가 허용된 관계인지 확인하고, 오프셋이 작업 공간 안에 있는지 검사한다. 또한 로봇이 컵을 들고 있는 상태이므로 너무 오래 공중에 들고 있지 않도록 시간 제한을 확인한다.
만약 대체 위치도 실패하면 이제 사용자에게 물어볼 수 있다.
노트북 오른쪽 공간이 마우스로 막혀 있어 컵을 안전하게 내려놓지 못했습니다. 노트북 오른쪽 앞쪽 빈 공간에 놓아도 될까요?사용자가 그래라고 답하면 새 위치로 놓기를 실행한다. 사용자가 아니, 중단해라고 답하면 로봇은 안전한 임시 위치에 컵을 내려놓거나, 내려놓을 수 없는 상황이면 컵을 든 채 정지하고 사람 개입을 요청한다.
이 예제에서 중요한 점은 LLM이 실패를 창의적으로 해석했다는 것이 아니다. 더 중요한 것은 실패 보고서가 충분히 구체적이었고, 디스패처가 재계획을 다시 검증했으며, 반복 실패 시 사용자 확인으로 넘어갔다는 점이다.
실패 로그는 다음 성공의 재료다
재계획 시스템을 만들면 로그의 가치가 크게 올라간다. 단순한 성공률만 보면 무엇이 문제인지 알기 어렵다. pick 성공률 70퍼센트라는 숫자보다 empty_grasp가 컵 작업에서 주로 발생하고, 그중 절반은 다시 검출 후 해결된다는 정보가 훨씬 유용하다.
실패 로그에는 다음 항목을 남기는 것이 좋다.
사용자 명령과 task_id
LLM이 만든 최초 계획과 재계획 버전
각 스킬의 입력 인자와 결과
실패가 발생한 stage와 reason_code
실패 당시 로봇 상태와 주요 센서 요약
선택한 회복 전략
회복 성공 여부
사용자 확인 또는 사람 개입 여부
이 로그는 디버깅뿐 아니라 정책 개선에도 쓰인다. 예를 들어 object_not_found가 특정 조명 조건에서 자주 발생한다면 비전 모델이나 조명 환경을 개선해야 한다. path_collision이 특정 놓기 위치에서 자주 발생한다면 작업대 배치를 바꾸거나 place 후보 생성 방식을 수정해야 한다. empty_grasp가 특정 물체에서 반복된다면 grasp 후보 점수화 방식을 바꿔야 한다.
LLM 프롬프트만 고쳐서 모든 문제를 해결하려고 하면 안 된다. 실패 로그를 보면 문제의 원인이 LLM이 아니라 센서, 좌표 변환, 경로 계획, 그리퍼 하드웨어, 작업 환경인 경우가 많다. 로봇 에이전트는 언어 모델 하나가 아니라 전체 시스템이다.
작은 상태 기계로 시작하자
처음부터 복잡한 에이전트 프레임워크를 만들 필요는 없다. 실패 감지와 재계획은 작은 상태 기계로도 충분히 시작할 수 있다.
IDLE
↓ 사용자 명령
PLAN
↓ 계획 검증 성공
EXECUTE
↓ 스킬 성공
EXECUTE 다음 단계
↓ 전체 성공
DONE
EXECUTE
↓ 스킬 실패
EVALUATE_FAILURE
↓ 단순 복구 가능
RETRY
↓ 의미 재계획 필요
REPLAN
↓ 사용자 결정 필요
ASK_USER
↓ 위험하거나 반복 실패
STOP_SAFE이 상태 기계의 장점은 디버깅이 쉽다는 것이다. 지금 로봇이 계획 중인지, 실행 중인지, 실패 평가 중인지, 사용자 응답을 기다리는지 명확히 알 수 있다. LLM은 PLAN과 REPLAN에서만 호출해도 된다. EXECUTE와 STOP_SAFE는 ROS2와 안전 노드가 책임진다.
이 구조를 구현할 때는 각 상태 전환을 로그로 남기자.
task_034 PLAN → EXECUTE
task_034 EXECUTE step_3 pick → EVALUATE_FAILURE empty_grasp
task_034 EVALUATE_FAILURE → REPLAN
task_034 REPLAN → EXECUTE
task_034 EXECUTE step_4 place → DONE이런 로그는 나중에 문제가 생겼을 때 이야기처럼 읽힌다. 로봇이 무엇을 하려 했고, 어디서 실패했고, 어떤 판단으로 회복했는지 따라갈 수 있다.
좋은 재계획 시스템의 기준
마지막으로, 이 장에서 만든 구조가 잘 작동하는지 확인할 기준을 정리해 보자.
첫째, 실패가 구체적으로 분류되어야 한다. 실패 하나로 끝나면 안 된다. 인식 실패인지, 경로 실패인지, 그리퍼 실패인지, 이동 중 장애물인지, 안전 정지인지 구분해야 한다.
둘째, 실패 정보는 LLM이 이해할 수 있어야 한다. 원시 예외 메시지를 그대로 던지는 대신 reason_code, stage, message, suggested_recovery로 바꿔야 한다.
셋째, 재계획은 같은 행동의 반복이 아니어야 한다. 다시 검출하더라도 영역이나 시점을 바꾸고, 다시 이동하더라도 경로나 목표를 바꾸고, 다시 집더라도 grasp 후보를 바꿔야 한다.
넷째, 반복 실패에는 끝이 있어야 한다. 횟수 제한, 시간 제한, 안전 제한을 코드로 강제해야 한다. LLM이 더 시도하겠다고 해도 시스템 정책이 멈추라고 판단하면 멈춰야 한다.
다섯째, 사람에게 물어보는 능력이 있어야 한다. 자율성은 무조건 혼자 처리하는 것이 아니다. 안전하고 정확한 작업을 위해 필요한 순간에 사용자와 협력하는 것도 자율 로봇의 중요한 능력이다.
결국 AI 로봇 에이전트의 실력은 성공 장면보다 실패 장면에서 드러난다. 데모 영상에서는 로봇이 한 번에 컵을 집고 멋지게 놓는 장면만 보일 수 있다. 하지만 실제 현장에서는 컵이 가려지고, 경로가 막히고, 그리퍼가 미끄러지고, 사람이 갑자기 지나간다. 그때 로봇이 당황하지 않고 멈추고, 설명하고, 다시 보고, 다른 방법을 고르고, 필요하면 사람에게 도움을 요청한다면 비로소 믿을 수 있는 에이전트가 된다.
다음 장에서는 이 믿음을 더 단단하게 만드는 주제를 다룬다. LLM의 환각, 권한 제한, 안전 구역, 충돌 방지, 로그와 감사 가능성까지 포함해 안전한 AI 로봇 에이전트를 설계하는 방법으로 넘어간다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.