임베디드에서 AI Agents: 컨텍스트 엔지니어링
AI 에이전트는 Yocto 레시피는 잘 쓰지만 센서 데이터를 조용히 깨뜨리고 SPI 레지스터를 잘못 설정합니다. 해법은 더 좋은 모델이 아닙니다. 컨텍스트 엔지니어링입니다.
On this page
Claude Code한테 Yocto 레시피를 시켰더니 첫 시도에 바로 돌아가는 걸 뽑아냈다. inherit도 맞고 SRC_URI도 제대로고, systemd 서비스 파일도 올바른 경로에 깔렸다. 2주 뒤, 같은 도구에 같은 유형의 세션에서 i.MX8M Plus용 IMU 센서 드라이버의 DMA 콜백을 짜 달라고 했다. 코드는 에러 없이 컴파일됐다. 그런데 부하를 주자 센서 데이터가 간헐적으로 깨지기 시작하더니, 30초 만에 커널이 통째로 멈췄다.
같은 모델, 같은 엔지니어. 달랐던 건 컨텍스트다. Yocto 레시피 때는 CLAUDE.md에 빌드 시스템이 meson이라는 것(cmake 아님), 레이어 구조, 표준 레시피 패턴을 다 적어 뒀다. DMA 핸들러 때는? “센서 데이터 경로의 DMA 콜백을 짜라” 딱 이 한 줄이 전부였다. 이 플랫폼의 캐시 동기화 요구사항이나 공유 버퍼 락킹 규칙을 알 도리가 없었다.
에이전트가 멀쩡하게 해내는 작업과 위험한 코드를 뱉는 작업, 그 사이의 격차가 이 글의 주제다. 첫 번째 포스트에서는 스펙 기반 TDD로 피드백 루프를 고쳤다. 이번엔 에이전트 그 자체를 고친다. 핵심은 하나다. 병목은 모델이 아니다. 컨텍스트다.
이 글도 Claude Code로 작업했다. CLAUDE.md, 범위 지정 규칙, 서브에이전트 리뷰어는 Claude Code 기능이지만, 계층적 컨텍스트라는 원칙 자체는 어떤 에이전트 코딩 도구에든 그대로 통한다.
에이전트가 잘하는 것
AI 에이전트는 정답이 소프트웨어 관례로 정해지는 작업, 그러니까 하드웨어 동작에 좌우되지 않는 작업에서 진짜 생산적이다. Yocto 레시피, CMakeLists, systemd 유닛 파일, pytest 스캐폴드, CI/CD 파이프라인 같은 것들. 에이전트는 학습 데이터에서 .bb 레시피를 수천 개 봤고, 그 레시피들 사이의 차이는 프로젝트 파일에 글로 적어 줄 수 있는 텍스트다. Jacob Beningo는 Claude Code로 센서 드라이버 작업에서 4시간에서 20시간을 아꼈다고 보고했는데, 깔끔한 하드웨어 추상화 계층이 전제라는 단서를 달았다. 빌드 시스템 작업이나 테스트 스캐폴딩에서는 혼자 하는 개발이 소규모 팀처럼 느껴질 정도다.
에이전트가 틀리는 것
실패는 모든 계층에서 튀어나온다. 레지스터 계층부터 보자. 에이전트가 우리 ICM-42688-P IMU의 CTRL_REG1 주소로 0x11을 썼다. 맞는 주소는 0x4E다. SPI는 레지스터 주소가 틀렸다고 에러를 뱉지 않는다. 그냥 그 위치에 있는 값을 읽어 올 뿐인데, 하필 거기가 0을 돌려주는 예약 레지스터였다. 로직 분석기 붙잡고 40분을 헤맨 뒤에야 레지스터 맵을 덤프해서 주소가 틀린 걸 찾았다. CLAUDE.md에 한 줄이면 막았을 일이다.
커널 계층으로 가면 격차가 훨씬 위험하다. i.MX8M Plus의 센서 드라이버 DMA 완료 콜백이 교과서 같은 사례다. 에이전트는 이런 코드를 뽑는다:
/* 에이전트 출력: DMA 완료 콜백 */
static void sensor_dma_complete(void *arg)
{
struct sensor_dev *sdev = arg;
memcpy(sdev->latest, sdev->dma_buf, SAMPLE_SIZE);
complete(&sdev->data_ready);
}
컴파일된다. 가벼운 트래픽이면 벤치에서도 잘 돈다. 하지만 부하가 꾸준히 걸리면 데이터가 간헐적으로 깨진다. 컨텍스트가 없으면 에이전트가 알 도리가 없는 문제가 둘 있다:
/* 하드웨어가 실제로 필요로 하는 것 */
static void sensor_dma_complete(void *arg)
{
struct sensor_dev *sdev = arg;
/* SDMA가 RAM에 직접 쓰지만 CPU L1/L2에 여전히 stale 데이터가 있음.
이 호출 없이는 캐시 라인이 아직 evict되지 않았을 때
memcpy가 쓰레기를 읽음. */
dma_sync_single_for_cpu(sdev->dev, sdev->dma_addr,
SAMPLE_SIZE, DMA_FROM_DEVICE);
spin_lock(&sdev->lock); /* process-context reader의 spin_lock_bh()와 쌍 */
memcpy(sdev->latest, sdev->dma_buf, SAMPLE_SIZE);
spin_unlock(&sdev->lock);
complete(&sdev->data_ready);
}
빠진 dma_sync_single_for_cpu는, DMA 엔진이 RAM에 직접 쓴 뒤 CPU가 stale 캐시 라인을 읽는다는 뜻이다. Cortex-A53를 쓰는 i.MX8M Plus에서 캐시 eviction은 부하에 따라 비결정적이라, 손상이 메모리 압력에 따라 나타났다 사라졌다 한다. 빠진 spin_lock은 콜백(tasklet/softirq 컨텍스트)과 sdev->latest를 읽는 프로세스 스레드 사이에 레이스를 만든다. 그리고 프로세스 컨텍스트 쪽은 평범한 spin_lock으로는 안 되고 spin_lock_bh가 필요하다. 안 그러면 락을 쥔 채로 softirq가 터질 때 데드락에 빠진다. 인트로에서 커널이 멈춘 게 바로 이 데드락이다.
모델들은 학습 데이터에서 Linux DMA API를 봤다. 하지만 당신의 플랫폼 캐시 토폴로지, 당신의 드라이버 락킹 규칙, 이 콜백이 sleeping lock이 치명적인 softirq 컨텍스트에서 돈다는 사실은 못 봤다. Chalmers/University of Gothenburg 연구(2026년 1월)는 4개 회사의 시니어 소프트웨어 엔지니어 10명을 조사했다. 이들은 비결정성과 인증 격차를 임베디드에서 AI 에이전트 도입을 막는 최대 장벽으로 꼽았다. 한 엔지니어의 요약이 정곡을 찌른다. “10년 전 컴파일러는 체크인할 수 있어도, LLM은 못 한다.”
격차는 부하가 걸릴 때, 스케일이 커질 때, 아니면 하드웨어 동작이 학습 데이터가 암시하던 것과 어긋날 때 드러난다. 그 격차를 메우려면 더 똑똑한 모델이 필요한 게 아니다. 컨텍스트가 필요하다.
컨텍스트 엔지니어링: 동작하는 세 계층
점점 더 미묘한 실패를 잡아 주는 컨텍스트 세 계층으로 수렴했다.
계층 1: 하드웨어 명세로서의 CLAUDE.md
단일 변화 중 효과가 제일 크다. 프로젝트 루트에 놓인 마크다운 파일 하나가 모든 Claude Code 세션에 걸쳐 살아남는다. 30분 적어 두면 그 뒤 모든 상호작용에서 본전을 뽑는다.
ETH Zurich 연구(2026년 2월)는 실세계 작업 138개를 테스트했다. LLM이 생성한 컨텍스트 파일은 오히려 성공률을 3% 낮추고 비용을 20% 늘렸다. 사람이 쓴 컨텍스트 파일은 성공률을 4% 올렸지만 비용도 19% 같이 올랐다. 에이전트가 지시를 너무 곧이곧대로 따르기 때문이다. 교훈은 이렇다. 에이전트가 저장소만 보고는 추론 못 하는 것, 그것만 넣는다. 뻔한 프로젝트 구조 설명은 토큰 낭비다. 코드에서 찾아낼 수 없는 하드웨어 제약이 금값이다.
내 CLAUDE.md는 추론 불가능한 제약에 집중한다:
- 하드웨어 플랫폼: SoC 모델(i.MX8M Plus), 타겟 보드(RPi CM4), 주요 주변장치와 그 레지스터 특이사항
- 레지스터 맵: 에이전트가 전에 틀렸던 핵심 SPI/I2C 레지스터 주소
- DMA 규칙: 캐시 라인 정렬(Cortex-A53에서 64바이트), 필수 sync 호출, 버퍼 할당 제약
- 빌드 시스템: cmake 아니고 meson, 어느 meta-layer가 어느 레시피를 소유하는지, Docker 빌드 패턴
모든 항목은 에이전트가 그게 없어서 특정 실수를 저질렀기 때문에 존재한다. 진짜 효과를 내는 패턴은 “학습된 수정” 섹션이다. 각 줄이 구체적인 실패 하나로 추적된다:
## Learned Corrections
- 2026-01-15: IMU CTRL_REG1은 ICM-42688-P에서 0x4E, 0x11 아님
- 2026-01-18: DMA 버퍼는 Cortex-A53에서 64바이트 정렬 필수
- 2026-02-03: ICM-42688-P는 SPI 모드 3 (CPOL=1, CPHA=1)
각 줄은 에이전트가 도입하고, 진단하고, 다시는 반복하지 않을 버그다. 파일은 유기적으로 자란다. 내 경험상 도구 체인이랑 워크플로 수정이 하드웨어 수정보다 대략 3대 1로 많다. 가장 자주 터지는 실패는 학습 데이터가 못 다루는 빌드 환경이랑 배포 경로 쪽이다.
더 큰 프로젝트에서는 Claude Code가 경로 기반 규칙 파일(.claude/rules/*.md)을 지원한다. 매칭되는 파일을 편집할 때만 로드되는 방식이다. 커널 관련 제약은 드라이버 코드를 건드릴 때만 로드된다. Yocto 관례는 .bb 파일용으로 로드되고. 각 규칙 파일은 작고 집중돼 있다. 에이전트는 지금 작업에 딱 필요한 컨텍스트만 받는다.
계층 2: 리뷰를 위한 특화 서브에이전트
범용 에이전트 하나로 임베디드 코드를 리뷰하면, 한꺼번에 너무 많은 걸 들고 있으려고 한다: 코드 스타일, 동시성 안전, 레지스터 정확성, 변경 영향 분석, 코딩 표준. 컨텍스트 창이 차면 에이전트가 놓치기 시작한다.
해법은 리뷰를 특화 서브에이전트로 쪼개는 것이다. 각각 집중된 시스템 프롬프트를 갖고, 필요한 도구에만 접근한다. 내 BSP 워크플로는 특화 리뷰어 5개에 결과를 집계하는 감독자(supervisor) 하나를 둔다. 여기서 설계 결정 두 개가 중요하다:
조건부 호출. 모든 리뷰어가 모든 변경에 도는 건 아니다. 레지스터 계약 리뷰어는 SPI/I2C 코드나 디바이스 트리 파일이 수정될 때만 실행된다. 동시성 리뷰어는 커널 드라이버 코드가 바뀔 때만 돈다. 테스트만 바뀌었으면 둘 다 건너뛰고 테스트 커버리지 리뷰어만 실행한다. 각 에이전트가 시작할 때 간단한 파일 glob 체크로 실행 여부를 정한다. 이걸로 대부분의 커밋에서 리뷰 시간이 반으로 줄고, 리뷰할 이유가 없는 코드를 분석하다 나오는 오탐도 막는다.
핵심 리뷰어의 중복 인스턴스. 동시성 리뷰어와 레지스터 계약 리뷰어는 각각 같은 프롬프트로 인스턴스 2개를 병렬로 돌리되, 컨텍스트는 독립이다. 두 인스턴스가 같은 이슈를 플래그하면 확신이 높다. 하나만 플래그하면 사람 리뷰로 넘긴다. 단일 LLM 패스는 놓치지만 같은 모델이 두 번째 실행에서는 잡을 수 있는, 그 확률적 맹점 문제를 이렇게 푼다. 비용은 적당하다(하나 대신 병렬 API 호출 두 개). 대신 가장 위험한 버그가 나오는 두 카테고리에서 신뢰성이 확 올라간다. 감독자는 모든 결과를 중복 제거하고 심각도 순위를 매긴다. 같은 이슈에 플래그가 2개 이상이면 높은 확신으로 자동 수정 대상, 1개면 사람 리뷰. 재실행 라운드는 없다. 2-인스턴스 설계가 이미 변동을 잡아내고, 라운드를 더 돌려 봐야 지연만 는다.
Google/MIT 스케일링 연구(2025년 12월, 설정 180개)는 고정 토큰 예산에서 에이전트 3개에서 4개가 상한선이라는 걸 발견했다. 조율 없이 도는 독립 에이전트는 에러를 17.2배 증폭한다. 중앙집중 감독자 아키텍처는 에러를 4.4배로 억제한다. 테스트한 모든 패턴 중 최선이었다. 에이전트 수를 늘리는 게 토론 라운드를 늘리는 것보다 효과적이고, 독립 분석 뒤 중앙집중 집계가 에이전트끼리 토론하는 것보다 낫다.
가장 많은 버그를 잡는 리뷰어 둘:
동시성 리뷰어. 프롬프트가 이렇게 시작한다: “코드 품질, 스타일, 성능은 리뷰하지 않는다. 오직 하나만 답한다: 이 코드가 동시 실행 환경에서 깨질 수 있는가?” spinlock 컨텍스트 위반, IRQ/softirq에서의 sleep, 락 순서, 메모리 배리어, DMA 동기화 규칙을 확인한다. 앞에서 나온 그 DMA 콜백 버그? 이 리뷰어가 첫 패스에서 잡는다. 빠진 캐시 동기화랑 보호 안 된 memcpy를 단계별 인터리빙 시나리오로 플래그한다. 범용 리뷰어는 둘 다 놓친다.
레지스터 계약 리뷰어. SPI/I2C 레지스터 주소, 비트 필드 마스크, 초기화 시퀀스가 데이터시트랑 맞는지 검증한다. 리뷰어가 모든 레지스터 접근 매크로를 grep하고, 주소를 뽑아내고, docs/hardware/의 레지스터 맵과 대조한다.
나머지 리뷰어 셋(부작용/변경 영향, 코딩 표준, 테스트 커버리지)은 단일 인스턴스로 이 둘과 나란히 돈다. 위험이 낮은 관심사라 단일 패스로 충분하다.
정적 분석 출력을 각 리뷰어 프롬프트에 주입하면 효과가 배가된다. 리뷰 패스 전에 cppcheck랑 clang-tidy를 돌리고, 출력을 JSON으로 바꿔서 각 리뷰어 프롬프트에 넣는다. 정적 분석 결과랑 LLM 리뷰를 합치면 둘 중 어느 쪽 혼자보다 더 많이 잡는다.
계층 3: 컴파일러-인-더-루프 피드백
마지막 계층은 에이전트 출력과 실제 도구 체인 결과 사이의 루프를 닫는다. 에이전트 출력을 타겟용으로 크로스 컴파일하고, 정적 분석을 돌리고, 플래시랑 RAM 사용량을 확인하고, 에러를 구조화된 데이터로 되먹인다. 그러면 에이전트가 추측 대신 진짜 컴파일러랑 정적 분석 출력을 놓고 반복한다.
첫 번째 포스트에서 스펙 기반 테스팅 파이프라인 전체를 다뤘다: spec.md에 인수 기준 쓰기, pytest + labgrid를 단일 명령 스크립트로 감싸기, 구조화된 JSON 반환. 빌드 피드백 루프는 코드가 타겟에 닿기 전, 파이프라인 더 앞단에서 같은 원칙을 적용한다. 에이전트는 컴파일러 경고를 스크롤하는 대신 {"check": "cppcheck", "severity": "error", "message": "Buffer access out of bounds"}를 읽고, 커널 로그 대신 {"test": "dma_cache_sync", "outcome": "failed", "reason": "data mismatch after 10000 transfers"}를 읽는다.
세 계층은 서로 복합된다. CLAUDE.md는 알려진 나쁜 패턴으로 코드가 생성되는 걸 막는다. 서브에이전트는 메인 에이전트가 놓칠 도메인 특화 이슈를 잡는다. 빌드 피드백 루프는 현실에 대고 테스트해서 나머지를 잡는다.
이 파이프라인이 만들어내는 것
컨텍스트 엔지니어링 전에는, 에이전트 코드가 컴파일은 돼도 런타임에서 죽었다. 잘못된 레지스터 주소, 빠진 캐시 동기화, 보호 안 된 공유 상태 같은 조용한 실패가 진단마다 30분 넘게 잡아먹었다. 로그에는 근본 원인을 가리키는 게 하나도 없었기 때문이다.
세 계층을 깔고 나니 대부분의 모듈이 첫 번째나 두 번째 반복에서 인수 테스트를 통과한다. 남은 재작업은 코드가 깨진 게 아니라 스펙이 덜 여문 탓이다.
컨텍스트 엔지니어링이 멈추는 지점
이 접근법은 프로덕션 임베디드 드라이버에서 중요한 것의 대략 절반을 커버한다. 나머지는 사람 몫이다.
하드웨어 디버깅. 칩 셀렉트 극성이 뒤집혀서 SPI 버스가 멈췄을 때, 컨텍스트 파일은 아무 도움이 안 된다. 오실로스코프, 로직 분석기, 그리고 CPOL 비트가 반전됐다는 걸 깨닫기 전까지 프로브를 뚫어져라 노려보는 20분. 에이전트가 소프트웨어 장애 주입 스크립트(stress-ng, 강제 모듈 언로드/재로드)는 짜 주지만, 물리적 장애에는 물리적 장비가 필요하다.
컨텍스트 저하. 긴 세션(2시간 이상)을 돌리면 CLAUDE.md가 로드돼 있어도 에이전트가 앞쪽 제약을 놓친다. 해법은 /clear(Claude Code의 컨텍스트 리셋 명령)를 아낌없이 써서 새로 시작하는 것이다.
스펙 품질. 파이프라인은 스펙만큼만 좋다. 첫 번째 포스트에서 다뤘다.
여기서 시작하라
CLAUDE.md를 20줄로 쓴다: 타겟 하드웨어, 빌드 시스템, “절대 하지 마” 규칙 3개. 에이전트가 실수할 때마다 항목을 더한다. 그다음 동시성 리뷰어 서브에이전트를 하나 붙인다. 그다음 크로스 컴파일해서 에러를 JSON으로 돌려주는 bash 스크립트로 피드백 루프를 닫는다. TDD 포스트에 스펙 기반 테스팅 설정 전체가 있다.
더 좋은 모델은 좋은 컨텍스트로 가능한 것의 상한을 끌어올릴 것이다. 그래도 컨텍스트가 필요하다는 사실 자체를 없애지는 못한다. 모델은 계속 똑똑해질 것이다. CLAUDE.md가 절벽이 어디 있는지 알려준다.
이 시리즈의 마지막 포스트에서는 완전한 프로젝트 구조를 한자리에 모아 보여준다: 폴더 구조, 스펙, 4계층 테스트, 리뷰 에이전트, end-to-end 피드백 루프.
댓글
댓글 불러오는 중...