· 6AI

하네스 엔지니어링 알아보기(3)

에이전트에게 넓은 범위의 작업을 맡기면 최소 세 가지 문제가 발생한다.(물론 컨텍스트가 커진 최신 프론티어 모델은 이 문제가 잘 안느껴진다. 돈이 최고)

#AI#harness
목차 보기

에이전트에게 넓은 범위의 작업을 맡기면 최소 세 가지 문제가 발생한다.(물론 컨텍스트가 커진 최신 프론티어 모델은 이 문제가 잘 안느껴진다. 돈이 최고)

  1. 현재 기능을 끝내기 전에 주변 기능과 리팩터링까지 건드린다.
  2. 여러 작업이 동시에 진행되면서 무엇이 완료됐는지 알기 어려워진다.
  3. 일부 코드나 테스트만 확인한 뒤 전체 작업이 끝났다고 판단한다.

에이전트가 작업 범위를 계속 넓히면 동시에 진행하는 작업이 많아지고 그러면 각 기능을 충분히 구현하고 검증하기 어려워진다. 가끔은 코드가 어느 정도 작성됐다는 이유로 작업이 완료됐다고 선언하기도 했다... 물론 이런건 task.md 같은거로 제어할 수 있긴한데.

따라서 하네스는 다음 세 가지를 강제해야 한다.

한 번에 하나의 기능만 작업한다.
현재 작업의 상태를 저장소에 기록한다.
검증을 통과한 경우에만 완료로 인정한다.

에이전트는 관련된 작업까지 범위를 넓힐 수도 있다.

에이전트에게 회원가입 기능을 구현하라고 하면 회원가입 코드이 범위도 건드릴 수 있다. 사용자 모델을 만들다가 인증 구조를 건드리고, 예외 처리 방식이 일관적이지 않다는 이유로 공통 예외 처리기를 리팩터링할 수 있다. 테스트 구조가 마음에 들지 않으면 테스트 코드까지 재구성한다.

에이전트가 생각하기엔 각각의 변경은 필요해 보일 수 있지만 현재 기능을 끝내기 전에 새로운 작업을 계속 시작해서 코어 기능을 개발하는 데 사용해야할 context가 부족해진다는 점이다...

그래서 수정한 파일과 코드는 늘어나지만 실제로 검증된 기능은 하나도 없을 수 있다. 이 문제를 막으려면 동시에 진행할 수 있는 작업 수를 하나로 제한해야 한다. 이를 WIP=1이라고 한다.

아래와 같은 규칙을 정의해서 WIP를 구현할 수 있따.

## 작업 범위

- 한 번에 하나의 기능만 active 상태로 둔다.
- 현재 기능이 검증을 통과하기 전에는 다음 기능을 시작하지 않는다.
- 현재 기능과 무관한 리팩터링은 별도 작업으로 기록한다.

작업은 사용자 동작 단위로 나눈다

WIP를 하나로 제한해도 작업 범위 자체가 너무 크면 의미가 없다. 이거도 context 제한 때문이다.

사용자 인증 구현
검색 기능 완성
관리자 페이지 개발

반대로 구현 세부 사항을 지나치게 잘게 나누는 것도 좋지 않다.

// 사용자 인증 구현을 아래처럼 풀어버렸다고 가정
User 클래스 생성
email 필드 추가
Repository 인터페이스 작성

적절한 작업 단위는 외부에서 관찰할 수 있는 하나의 동작과 하나의 검증 방법으로 설명할 수 있어야 한다.

유효한 이메일과 비밀번호로 회원가입하면 201을 반환한다.
중복된 이메일로 회원가입하면 409를 반환한다.
잘못된 비밀번호로 로그인하면 401을 반환한다.

하나의 작업은 가능하면 한 세션이나 하나의 명확한 체크포인트로 구현하고 검증할 수 있을 정도로 작아야 한다.

기능 목록은 하네스의 상태 저장소다

- [x] 회원가입
- [ ] 로그인
- [ ] 비밀번호 재설정

여기서 회원가입 완료가 무엇을 의미하는지 한번에 알 수 없다.

API 코드만 작성됐다는 것인지, 데이터베이스에 사용자가 실제로 저장되는지, 중복 이메일 검증과 테스트까지 통과했다는 것인지 불분명하다. 이렇게 완료 기준이 없으면 에이전트는 자신이 생각하는 기준으로 완료를 판단한다.

그래서 이런 기능 목록 작성 시에는 각 기능에 대해 다음과 같은 최소한의 정보를 넣어줘야 좋다.

  1. 사용자 관점의 동작
  2. 검증 방법
  3. 현재 상태

예를 들면 다음과 같다.

나쁜 예:
RegisterService를 구현한다.

좋은 예:
유효한 회원가입 요청을 보내면 사용자가 저장되고 201을 반환한다.
{
  "id": "F01",
  "behavior": "유효한 회원가입 요청을 보내면 사용자가 생성되고 201을 반환한다.",
  "verification": "./scripts/verify-feature.sh F01",
  "state": "active"
}

강의에서는 json으로 작성하는데, yaml, md, json 어떤 파일이어도 상관 없다. 어쩌면 주석을 달 수 있는 yaml이 좋을 수도 있겠다.

기능 상태 머신은 최대한 간결히

기능에 대한 상태는 다음 정도면 충분하다.

not_started
아직 시작하지 않은 기능

active
현재 구현 중인 유일한 기능

blocked
외부 의존성이나 선행 작업 때문에 진행할 수 없는 기능

passing
등록된 검증을 통과한 기능

에이전트가 자신의 판단만으로 passing 상태를 기록하지만 않으면 된다.

./scripts/verify-feature.sh F01

검증 절차에 스크립트를 실행시켜 검증이 성공하면 상태를 passing으로 변경하고, 실패하면 active 상태를 유지하도록 규칙을 달아두는 방향으로 제어한다.

에이전트의 완료 선언을 믿으면 안 된다

에이전트는 자신이 작성한 코드를 스스로 평가한다.

처음에 추론 과정을 거쳐 구현 의도를 들고 있기 때문에 누락된 요구사항보다 코드가 의도대로 작성됐는지를 중심으로 확인 프로세스가 동작되기 쉽다.

- 필요한 코드를 모두 추가했습니다.
- 단위 테스트가 통과했습니다.
- 정상적으로 동작할 것으로 보입니다.

~처럼 보인다라는 말이 얼마나 불안한지 모른다. 그래서 정적 검증, 통합 검증 절차를 둬서 정량 평가 시스템을 구축해야한다.

정적 검증은 코드가 빌드 가능한 상태인지 확인하는 단계다. format, lint, type check, compile 이런 게 여기 속한다.

통합 검증은 프로그램을 실제로 실행하고 구성 요소가 연결되는지 확인하는 과정이다.

애플리케이션이 시작되는가?
데이터베이스와 연결되는가?
API 요청이 실제 데이터에 반영되는가?
필요한 외부 서비스와 연결되는가?

여기서 좀 더 나아가면 end-to-end 검증이 된다. 엔드 투 엔드는 사용자 시나리오를 처음부터 끝까지 실행하는 테스트다.

예를 들어 비밀번호 재설정 기능이라면 다음 flow를 모두 동작해야 한다.

1. 사용자가 비밀번호 재설정을 요청한다.
2. 서버가 재설정 토큰을 생성한다.
3. 재설정 링크가 전달된다.
4. 사용자가 새 비밀번호를 설정한다.
5. 기존 비밀번호로 로그인이 실패한다.
6. 새 비밀번호로 로그인이 성공한다.

그리고 가능하면 코드를 작성하는 역할과 완료 여부를 판단하는 역할을 분리하는 것이 좋다. 이런 걸 하라고 tmux도 쓰고 하는 것이다.

opencode를 사용할 때 처음 알게된 것들인데, 역할을 나눠서 처리하는 게 생각보다 유효한 전략같다.

Planner
요구사항을 기능 단위로 나눈다.

Worker
현재 active 기능을 구현한다.

Verifier
등록된 검증을 실행한다.

Harness
검증 결과에 따라 상태를 변경한다.

기능 구현 중 코드 중복이나 구조 문제를 발견하면 에이전트가 바로 리팩터링하려는 경향이 있는데, 기능이 한 번도 정상 동작한 적이 없는 상태에서 구조까지 변경하면 실패 원인을 구분하기 어려워진다.

코드가 길어져 버리면 고치는 데 돈이 더 많이 들어가게 된다ㅠ.

그래서 이번 7,8,9 강을 정리하면 아래 순서로 계획을 세우는 게 좋다는 결론이 나온다.

1. 기능 구현
2. 통합 및 엔드투엔드 검증
3. 회귀 테스트
4. 리팩터링과 최적화
공유

댓글