나는 임베디드를 이해하지 못한다. 하지만 여전히 그것을 출시할 수 있는 유일한 사람이었다.
요약
AI 에이전트를 활용한 임베디드 소프트웨어 개발 과정에서 발생하는 피드백 루프의 한계를 다룹니다. 에이전트가 실제 타겟 환경(Windows/Cross-compiler)과 격리된 호스트 환경에서만 테스트를 수행할 때 발생하는 오류와 인간의 수동 개입 문제를 지적합니다.
핵심 포인트
- 에이전트의 테스트 환경과 실제 타겟 환경 간의 불일치 문제
- 에이전트의 확신에 찬 답변이 실제 문제 해결을 보장하지 않음
- 실제 피드백 루프(Ground Truth)를 연결하는 인간의 병목 현상
- 멀티 에이전트 워크플로우에서 검증 단계의 중요성
로컬 테스트 스위트(test suite)는 통과(green) 상태였다. 11개 중 11개 모두. 나는 에이전트(agent)에게 테스트 커버리지(test coverage)가 확보되었다고 말하며, 커밋(commit)하고 푸시(push)하라고 명령했다. 우리는 칩의 실제 컴파일러(compiler)로 단 한 번도 컴파일된 적이 없는 SDK를 출시했다.
나는 백엔드 시스템(backend systems)을 작성한다. 펌웨어(firmware)를 작성하는 것이 아니다. 이 프로젝트는 마이크로컨트롤러(microcontroller)를 위한 C 코드를 생성하며, 생성된 코드는 Windows 머신 상의 CDK(벤더의 빌드 환경) 내에서, RISC-V 크로스 컴파일러(cross-compiler)를 대상으로, 내가 한 번도 사용해 본 적 없는 패키징 시스템(packaging system) 내에서 컴파일되어야 했다. 그 중 어떤 것도 나의 Linux 환경에는 존재하지 않았다. 통과된 11개의 테스트는 호스트 gcc(host gcc) — 프로그램이 실행되는 머신을 위해 프로그램을 빌드하는 컴파일러 — 환경에서 실행되었다. 실제로 중요한 코드는 다른 칩을 위해, 내가 접근할 수 없는 곳에 있는 다른 컴파일러가 필요했다. 두 개의 컴파일러, 두 개의 세계. 나의 테스트는 잘못된 세계에 머물러 있었다.
하루 뒤, Windows 머신으로부터 첫 번째 에러가 돌아왔다. 그다음 또 다른 에러가 왔다. 그리고 다섯 개의 에러가 더 이어졌다.
일곱 번의 시도
에러들은 하나씩 도착했고, 에이전트는 매번 똑같은 방식으로 응답했다. 자신만만한 "근본 원인(root cause):"이라는 말과 함께 저장소(repo)로 수정 사항을 푸시했다. 헤더 파일(header file)이 누락되었다. 타입 이름(type name)이 틀렸다 — 실제 컴파일러는 한 번도 건드린 적 없는 버그들이었다. 그다음에는 SDK가 아예 컴파일되지 않았다: 모호한 매니페스트(manifest) 설정이 누락되어 있었다. 그다음에는 칩의 핀(pin) 이름이 틀렸다 — 에이전트가 작성할 때 사용한 SDK의 핀 이름과, SDK CDK가 실제로 배포한 SDK의 핀 이름이 서로 달랐다. 그다음에는 드라이버 파일 전체가 아무런 결과물 없이 조용히 컴파일되었다. 컴파일러가 볼 수 없는 생성된 파일 안에 해당 드라이버를 켜는 스위치가 있었기 때문이다. 그러고 나서 다시 똑같은 벽에 부딪혔다. 마지막 수정 사항이 증상만을 해결했을 뿐, 진짜 문제는 아키텍처(architecture)였기 때문이다.
일곱 번의 라운드. 에이전트는 매번 그것을 근본 원인이라고 불렀다. 매번 그것은 실제 문제였지만, 동시에 다음 문제의 가장 바깥쪽 껍데기에 불과했다.
내가 지적하고 싶은 것은 버그(bugs)가 아닙니다. 그것은 루프(loop)의 형태입니다. 에이전트는 약 3초 만에 수정 사항을 내놓았습니다. 그것을 검증하는 데는 몇 분에서 몇 시간이 걸렸습니다. Windows에서 코드를 가져오고(pull), 전체 CDK 빌드를 실행하고, 에러 출력(error output)을 읽고, 다시 복사해 넣는 과정 말입니다. 실제 피드백 루프, 즉 실체(ground truth)를 건드리는 루프는 서로 통신할 수 없는 두 대의 기기 사이에서 인간의 속도로 작동했으며, 그 사이에서 제가 수동으로 에러를 실어 날라야 했습니다. 저는 정교한 멀티 에이전트 워크플로우(multi-agent workflow)를 구축했지만, 그 안의 진짜 오라클(oracle)은 복사-붙여넣기를 수행하는 사람이었습니다.
왜 멈출 수 없었는가
매 단계마다 확신에 찬 "근본 원인(root cause):"이라는 말은 수렴(convergence)의 신호가 아닙니다. 그것은 아무런 신호도 아닙니다. 에이전트는 자신이 정답에 가까워지고 있는지 아니면 허우적거리고 있는지와 상관없이 확신에 찬 문장들을 내뱉었습니다. 어느 시점에는 에이전트가 제안한 수정 사항이 드라이버 파일을 침묵시키고 있던 스위치를 그냥 뽑아버리는 것이었습니다. 그렇게 해서 컴파일이 되게 만들려는 것이었죠. 저는 "그건 너무 지저분하다"라고 단호하게 말하며 그 해킹(hack)을 차단했습니다. 그 후에야 진짜 수정 방법이 드러났습니다. 컴파일러가 절대 볼 수 없는 파일 안에 묻혀 있는 스위치에 의존하는 것이 아니라, 컴파일하는 파일에 따라 드라이버가 선택되도록 하는 것이었습니다. 또 다른 패치(patch)가 아닌 구조적인 변화(structural change)였습니다.
여기서 깊이 생각해 볼 부분이 있습니다. 에이전트는 단 한 번도 진실된 문장을 말하지 않았습니다. "타겟(target)을 볼 수 없습니다. 제가 드리는 모든 수정 사항은 추측입니다. 중단하세요. RISC-V 툴체인(toolchain)을 설치하고 오시면, 그때 제가 도움이 되겠습니다." 에이전트는 이 말을 하지 않았습니다. 왜냐하면 추측 한 번에 에이전트는 3초를 소모하지만, 저는 오후 시간을 통째로 날려버리기 때문입니다. 그리고 에이전트는 그 비용을 지불하지 않습니다. 잘못된 추측에 대한 비용을 부담하지 않는 에이전트는, 조준을 가능하게 해줄 도구를 요청하기보다 한 발을 더 쏘는 쪽을 항상 선호할 것입니다. 경제적 논리는 침묵하며, 그 논리는 전적으로 키보드 앞에 앉아 있는 사람에게 불리하게 작용합니다.
도움이 되지 않았을 브리핑
명백한 교훈은 실망스럽게도 다음과 같습니다. 나는 에이전트 (agent)에게 새로운 컴포넌트 (component)의 메커니즘을 사전에 설명했어야 했습니다. 빌드 환경 (build environment)이 매니페스트 (manifest)를 어떻게 작성하기를 원하는지, 빌드 시 컴포넌트를 어떻게 가져오는지, 그리고 각 조각들이 어디에 위치하기를 기대하는지를 말해줬어야 했습니다. 새로운 역할을 브리핑하고, 이를 기존 역할들과 연결했다면, 7번의 시도는 0번으로 줄어들었을 것입니다.
하지만 에이전트에게 브리핑을 하는 것만으로는 도움이 되지 않았을 것입니다. 에이전트는 CDK를 무지했던 것이 아닙니다. 에이전트는 컴포넌트를 처음부터 스캐폴딩 (scaffolding)하고, 매니페스트를 작성하며, 검색 경로 (search paths)를 설정할 수 있을 정도로 빌드 환경을 충분히 잘 알고 있었습니다. 한때 에이전트는 CDK가 의존성 (dependencies)을 어떻게 해결하는지에 대해 유창하고 상세하게 설명하기도 했습니다. 그 설명은 틀렸지만, 정보가 없었던 것은 아니었습니다. 함정은 바로 이것입니다. CDK에 대한 일반적인 지식이, 이 버전이 이 버전의 SDK와 연결되었을 때 빌드가 실행될 때 실제로 어떻게 동작할지를 아는 것과 같지는 않다는 점입니다. 누락되었던 설정들, 즉 매니페스트 형식이나 핀 이름 (pin names) 등은 우리 중 누구도 관찰할 수 없었던 타겟의 런타임 동작 (runtime behavior)에 관한 사실들이었습니다. 에이전트의 실제 지식은 문제가 발생하는 지점의 바로 경계선까지 닿아 있었으며, 어디서 하나가 끝나고 다른 하나가 시작되는지를 나타내는 경계선조차 없었습니다.
이 지점이 바로 숙련된 임베디드 엔지니어라면 에이전트(agent)가 할 수 없는 일을 해냈을 부분입니다. 포트 레이어(port layer)는 칩 SDK의 한 가지 버전에 맞춰 작성되었으나, CDK는 다른 버전을 배포했습니다. 베테랑에게 "동일한 SDK의 두 가지 소스"라는 상황은 반사적인 반응을 일으킵니다. '같은 칩인데 SDK가 두 개라면, 심볼(symbol)이 일치하지 않을 것이니 지금 당장 diff를 돌려봐야 한다'는 반응 말입니다. 이것은 마법이 아닙니다. 적절한 프롬프트(prompt)가 주어진다면, 똑똑한 모델도 동일한 체크리스트를 만들어낼 수 있습니다. 격차는 능력의 문제가 아니라 태도(posture)의 문제입니다. 엔지니어의 흉터는 기본 설정을 의심으로 재작성하지만, 모델의 기본 설정은 자신감이며—그것은 거침없이 돌진합니다. 누군가는 그것을 조종해야 합니다. 그 세션에서는 아무도 조종하지 않았습니다. 우리 둘 다 조종해야 할 함정이 있다는 사실조차 몰랐기 때문입니다. 그리고 두 번째 이유는 더 직설적이며, 모델이 얼마나 똑똑한지와는 상관이 없습니다. 두 번째 SDK가 모델의 컨텍스트(context) 안에 없었습니다. 그것은 Windows 머신에 있었습니다. 에이전트는 두 파일을 모두 가진 적이 없었습니다. 다른 방에 놓여 있는 함정을 훑어보거나, 그것을 찾아보라는 프롬프트를 받는 것은 불가능합니다.
따라서 질문은 "에이전트에게 어떻게 브리핑할 것인가"에서 "그라운드 트루스(ground truth)가 모두에게 알려지지 않았고, 내가 결코 볼 수 없는 머신에 존재할 때 사람들은 무엇을 하는가"로 바뀝니다.
정찰병 (The scout)
벽을 번역하지 마십시오. 벽이 어둡다는 것을 인정하고, 설계를 작성하기 전, 코드를 작성하기 전에, 단 한 번이라도 불을 밝힐 수 있는 가장 저렴한 프로브(probe)를 보내십시오. 제가 첫날에 했어야 했던 조치는 RISC-V 컴파일러를 제 컴퓨터에 설치하고 생성된 코드에 대해 구문 전용 패스(syntax-only pass)를 실행하는 것이었습니다. 실제 바이너리를 빌드하지 않고, 오직 에러를 드러낼 수 있을 만큼만 컴파일하는 것입니다. 일주일 뒤 누군가의 클립보드를 통해 심볼 불일치(symbol mismatches)가 전달되는 대신, 로컬에서 단 몇 초 만에 해결할 수 있었을 것입니다. 그것이 모든 것을 알려주기 때문이 아닙니다. 그 도달할 수 없는 세계의 파편을 제가 실제로 만질 수 있는 곳으로 끌어올 수 있기 때문입니다.
이것이 시행착오 (trial-and-error)를 없애주는 것은 아닙니다. 시행착오 (trial-and-error)는 진정으로 미지의 영역을 탐색하는 방법입니다. 사전에 충분히 깊게 고민한다고 해서 이를 건너뛸 수 있는 방식의 작업은 존재하지 않습니다. 당신이 선택할 수 있는 것은 어디에서 벽에 부딪힐지, 누구의 시간을 들여 그 벽에 부딪힐지, 그리고 그 벽에 한 번 부딪힐지 아니면 일곱 번 부딪힐지뿐입니다. 정찰병은 어둠을 피하지 않습니다. 정찰병은 원정대를 투입하기 전에, 그 어둠의 일부를 알려지게 만들기 위해 가장 적은 대가를 치릅니다.
제2막: 내가 실제로 구축한 것
해결책 없는 진단은 AI에 대한 또 다른 불평에 불과합니다. 그래서 펌웨어 (firmware)가 마침내 연결된 후, 저는 다시 돌아가 에이전트 (agent)와 그가 도달할 수 없었던 진실 사이의 거리를 조정했습니다. 세 가지 변경 사항이 있었고, 이들은 매우 다른 세 가지 수준에 위치합니다. 어떤 것이 실제로 간극을 메웠고 어떤 것이 단지 간극을 좁혔을 뿐인지에 대해 솔직해질 가치가 있습니다.
벽을 허물어라. 가장 강력한 해결책은 코드 생성기 (code generator)가 CDK 프로젝트 파일 자체 — 소스 파일 목록, 검색 경로 (search paths), 메모리 레이아웃 (memory layout) 등 모든 것 — 를 출력하도록 만드는 것이었습니다. 일곱 가지 오류 중 두 가지는 사람이 CDK를 수동으로 구성하다가 무언가를 놓쳐서 발생했습니다. 해결책은 그 구성을 더 주의 깊게 검증하는 것이 아니었습니다. 수동 단계를 완전히 삭제하여 잘못 구성할 요소 자체를 남기지 않는 것이었습니다. 이것은 제가 이전 게시물들에서는 결코 하지 않았던 방식입니다. 검증 계층을 추가하는 것이 아니라, 검증이 필요한 대상 자체를 제거하는 것입니다. 미지의 지형 구간을 단순히 존재하지 않게 만들 수 있다면, 그것은 그 어떤 세심한 정찰보다도 강력합니다.
망원경을 들어 올리십시오 — 그리고 그것이 어디를 향하고 있는지 인정하십시오. 나는 생성 시점에 실행되는 체크 로직을 추가하여, 테스트 스위트(test suite)에 연결된 핵심 SDK 코드에 대한 구문 전용 패스(syntax-only pass)를 수행하도록 했습니다. 이를 통해 "며칠 뒤 클립보드에 있는" 타입 이름(type-name)과 누락된 값의 버그 클래스를 "몇 초 뒤 로컬 테스트"로 끌어올 수 있게 되었습니다. 이것은 실재하는 진전입니다. 하지만 이것은 RISC-V 컴파일러가 아니라 호스트 gcc — 즉 내 머신의 컴파일러 — 하에서 실행됩니다. 따라서 핀 이름(pin-name) 불일치, 매니페스트(manifest) 설정, 컴파일러가 볼 수 없었던 스위치 등은 이 방식으로 잡아낼 수 없습니다. 왜냐하면 그것들은 호스트 gcc가 결코 들어갈 수 없는 세계에 존재하기 때문입니다. 망원경은 더 가까이 다가갔습니다. 하지만 여전히 실제로 중요한 산을 겨냥하고 있지는 않습니다. 그 경계를 좁힐 수 있는 탐사 장비 — 즉 모든 빌드 시 로컬에서 동일한 구문 전용 패스(syntax-only pass)를 실행하는 RISC-V 컴파일러 — 를 나는 아직 설치하지 않았습니다. 나는 그것이 무엇인지 정확히 알고 있습니다. 그것은 내 바로 옆 바닥에 놓여 있습니다. 이것이 이 시스템의 정직한 상태입니다.
다음 단계를 위한 지도를 그리십시오. 나는 어렵게 얻은 사실들을 규칙으로서 기록했습니다: 매니페스트(manifest) 형식, 누락된 설정, 그리고 어떤 핀 이름들이 다른지에 대한 표입니다. 이를 통해 7번의 샷(shots)을 들여 얻은 지능을 고정했습니다. 이 규칙들은 매 세션 시작 시 로드되며, 실제로 효과를 발휘합니다. 하지만 자동화되어 추가적인 인간의 주의가 필요 없는 다른 두 가지 수정 사항과 달리, 작성된 규칙은 유지보수에 의존합니다. 누군가는 규칙이 정확하고, 범위를 올바르게 설정하며, 다음 에이전트(agent)가 오해하지 않을 만큼 충분히 정밀하게 표현되도록 계속 관리해야 합니다. 이는 세 가지 수정 방법 중 인간에 가장 많이 의존하는 방식입니다.
하네스(harness)의 실제 본질
나는 이전까지 AI 보조 작업에서 누락된 단계가 검증(verification) — 동작 확인, 사양(spec) 확인, 다른 곳에 동일한 버그가 있는지 확인 — 이라고 생각했습니다. 그것은 여전히 사실이며, 이전에도 그렇게 쓴 적이 있습니다. 하지만 이번 경험은 그 밑바탕에 있는 단계를 가르쳐 주었습니다.
에이전트(Agent)는 오직 자신의 컨텍스트 (Context) 안에서만 생각합니다. 그것은 당신이 고칠 수 있는 한계가 아니라, 그 존재의 형태 자체입니다. 에이전트는 빌드 환경 (Build environment)을 전반적으로 알고 있었습니다. 진정으로 유능하게 들리고, 실제로 유능하게 보일 만큼은 충분히 알고 있었습니다. 하지만 에이전트가 알 수 없었던 것은, 빌드가 실제로 실행될 때 이 특정 타겟 (Target)이 어떻게 동작할지였으며, 에이전트는 그 두 지점 사이의 경계선을 느낄 수 없었습니다. 에이전트의 실제 지식은 맞물려 돌아가는 부품의 끝단 바로 직전까지만 닿아 있었습니다. 그래서 에이전트는 "저는 앞이 보이지 않습니다"라고 말하지 않았습니다. 대신 벽을 향해 일곱 번의 자신감 넘치는 발을 쏘았고, 그 자신감은 실재하는 지식에서 비롯되었습니다. 위험은 에이전트가 볼 수 없다는 점이 아니었습니다. 위험은 에이전트가 자신의 시야가 어디서 끝나는지 구분할 수 없다는 점이었습니다.
사람은 그 경계에 서서 벽 너머가 어둡다고 말해야 합니다. 사람은 그 너머에 무엇이 있는지 이해할 필요가 없습니다. 저 역시 이해하지 못했고, 지금도 마찬가지입니다. 사람이 하는 일은 에이전트가 구조적으로 할 수 없는 단 한 가지, 즉 눈이 보이지 않는 것에 대한 비용을 감수하고, 그 어둠을 드러내기 위해 의도적으로 그 비용을 지불하는 것입니다. 그것이 바로 하네스 (Harness)가 하는 일입니다. 테스트 스위트 (Test suite)가 아닙니다. 하네스는 에이전트와, 에이전트 스스로는 결코 향하지 않을 그라운드 트루스 (Ground truth) 사이의 거리를 단축하는 작업입니다. 왜냐하면 에이전트는 저 밖에 진실이 있다는 사실을 알지 못하며, 알지 못함에 따른 대가를 치르지 않기 때문입니다.
저는 펌웨어 (Firmware)를 작성할 수 없습니다. 저는 그 칩 (Chip)에 대해 에이전트에게 단 한 가지도 브리핑할 수 없었습니다. 그럼에도 불구하고 제가 여전히 그것을 출시할 수 있는 유일한 사람이었던 이유는, 출시하는 것이 임베디드 개발 (Embedded development)을 이해하는 것과는 아무런 상관이 없었기 때문입니다. 그것은 경계에 서서 어둡다는 것을 인정하고, 저 멀리 있는 기계를 한 걸음 더 가까이 끌어오는 것과 관련이 있었습니다. 에이전트가 당신이 읽을 수 있는 것보다 더 많은 코드를 작성할 때, 그것이 바로 당신에게 남겨진 직무입니다. 도메인 (Domain)을 모르는 것이 아니라, 아직 아무도 부딪히지 않은 벽이 어디인지 알고, 그곳에 가장 먼저 부딪히기 위해 기꺼이 대가를 치르는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기