AI-SDLC 실천: 워크플로를 Agent로 묶을까, 스크립트로 묶을까
두 방식을 모두 시도한 끝에 Agent가 CLI를 조작하는 구조를 택했고, 워크플로·단계·상태·승인 지점 설계를 정리했다.
중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.
한 번의 딜리버리는 요구, 설계, 개발, 검증, 전달의 다섯 단계를 거치며, 여러 세션에 걸쳐 진행되고, 중간에 컨텍스트 압축과 세션 중단도 마주하게 된다. 단계와 단계를 무엇으로 연결할지가 워크플로를 설계할 때 가장 먼저 답해야 할 질문이다.
우리의 답은 Agent가 CLI를 조작하는 것이다. 흐름 정의와 진행 상황, 검사는 CLI에 두고, Agent가 각 단계를 어떻게 할지 결정하며 CLI 명령을 통해 진행 상황을 조회하고 산출물을 등록하고 진행을 요청한다.
이 글에서는 다섯 가지를 다룬다.
- 왜 전부 Agent에 맡기지도, 전부 스크립트에 맡기지도 않는가;
- 워크플로의 세 가지 핵심 개념인 Workflow, Phase, State가 각각 무엇이며, skill이 어떻게 단계에 묶이는가;
- 단계 사이를 어떻게 인계하고, State를 어떻게 진행시키는가;
- 사람이 워크플로 안에서 어떤 몇 가지를 확인하는가;
- 압축하고 세션을 바꾼 뒤에 어떻게 이어서 가며, hook을 어느 몇 가지 이벤트에 거는가.
제 01편의 끝에서 말했듯, 흐름의 정확성을 「hook이 반드시 실행된다」는 것 위에 세울 수는 없다. 이 글에서는 hook 이외에 흐름을 무엇으로 연결하는지를 다룬다.
전부 Agent에 맡기기, 전부 CLI에 맡기기, 그리고 Agent가 CLI를 조작하기
두 가지 극단적인 방식은 우리 모두 시도해 보았다.
전부 Agent에 맡겨 연결하기. 단계 순서와 방식, 산출물 요구 사항을 모두 skill과 프롬프트에 적어 두고 모델이 기억하며 진행하게 한다. 문제는 대형 모델이 지나치게 자유분방하게 나서고, 중간 노드의 산출물이 제약을 받지 않는다는 점이다.
전부 CLI에 맡겨 연결하기. 각 단계에서 무엇을 하고 무엇을 호출하고 무엇을 산출할지를 모두 스크립트가 결정하고, 모델은 빈칸만 채우면 된다. 흐름은 안정되었지만 지나치게 경직되어 제약을 받기 쉽다.
현재의 방식은 둘을 결합한 것으로, Agent가 CLI를 조작한다.
- CLI는 흐름 정의와 매 딜리버리의 진행 상황, 확정적 검사를 보유하여 산출물이 무엇인지, 어디에 두는지, 어떻게 되어야 합격인지, 다음 단계는 어디로 갈 수 있는지를 제약한다;
- Agent는 단계 안에서 어떻게 할지를 결정하고, CLI 명령을 통해 진행 상황을 조회하고 산출물을 등록하고 진행을 요청한다;
- 사람은 영향 범위가 걸린 몇 가지 지점에서만 결정한다;
- hook은 정해진 시점에 Agent에게 진행 상황을 읽으라고 알리되, 대신 결정하지는 않는다.
아래에서는 먼저 CLI가 보유한 세 가지 개념을 설명하고, 이어서 인계와 진행, 사람의 역할과 복구를 설명한 뒤 마지막으로 hook을 설명한다.
Workflow, Phase, State 세 개념의 정의
먼저 각각을 한 문장으로 정의한다.
- Workflow는 흐름 정의로, 어떤 단계 노드들이 있는지, 누가 누구에게 의존하는지, 각 노드의 입력이 어느 상류에서 오는지만 기술하며, 구체적인 방식은 전혀 포함하지 않는다;
- Phase는 단계 정의로, 한 단계가 무엇을 읽고, 누가 수행하며, 무엇을 산출하고, 어떻게 되어야 합격인지를 기술한다;
- State는 한 워크플로 인스턴스의 현재 사실로, 어디까지 왔는지, 어디서 막혔는지, 무엇을 산출했는지를 기록한다.
세 가지의 관계: Workflow의 각 노드는 하나의 Phase를 참조한다. 딜리버리가 시작될 때마다 CLI는 Workflow에 따라 워크플로 인스턴스(이하 인스턴스)를 만들고, State는 인스턴스 디렉터리에 기록된다. Phase 사이는 결과를 직접 전달하지 않고, State에 등록된 인계 파일을 통해서만 전달한다.
Workflow: 노드, 의존성, 입력 출처
Workflow의 각 노드에는 네 가지를 적는다. 노드 ID, 어떤 Phase를 참조하는지, 직접 의존성, 입력이 어느 상류 노드의 어떤 산출물에서 오는지. 흐름 전체는 이와 별도로 흐름 입력(사용자 요청)과 최종 출력(마지막 노드의 인계)을 선언한다.
아래는 잘라낸 간략 흐름 정의로, 개발, 검증, 전달 세 개 노드만 있다.
kind: workflow id: quick-delivery inputs: request: { contract: quick-request } nodes: - id: develop phase: quick-develop needs: [] inputs: { request: { input: request } } - id: verify phase: quick-verify needs: [ develop ] inputs: { previous: { node: develop , output: implementation } } - id: delivery phase: quick-delivery needs: [ verify ] inputs: { previous: { node: verify , output: verification } } outputs: delivery: { node: delivery , output: delivery }
단계의 선후는 오직 노드 사이의 의존성에 의해서만 결정된다. 전체 흐름은 「요구 → 설계 → 개발 → 검증 → 전달」이고, 간략 흐름은 「개발 → 검증 → 전달」이며, 둘은 두 개의 Workflow 정의다. 간략 흐름을 탈 때는 단계를 다 갖추기 위해 요구와 설계 문서를 억지로 만들지 않는다.
여기서 「노드」는 특정하게 한 번의 단계 호출을 가리킨다. 개발 단계 내부에는 또 하나의 작업 그래프가 있고, 그 그래프의 노드는 업무 작업 단위이며, 이는 제 04, 05편에서 다룬다.
Phase: 입력, 실행, 출력 세 구간
Phase는 세 구간으로 선언한다.
- 입력 : 출처는 상류 노드의 인계이며, 첫 번째 단계는 사용자 요청을 읽는다;
- 실행 : 단계 목표, 수행자, 로드할 skill, 그리고 읽기가 허용된 파일, 템플릿, 예시;
- 출력 : 산출물 목록으로, 어느 것이 필수인지, 어느 것이 다운스트림에 넘기는 인계인지를 표시하며, 인계는 계약에 따라 검증된다.
수행자에는 두 가지가 있다. 대부분의 단계는 메인 세션의 오케스트레이터, 즉 Agent 자체가 수행하며, 순수 검사류의 단계는 확정적 명령 한 줄일 수 있어 모델의 참여가 필요 없다.
아래는 잘라낸 설계 단계 정의다:
```yaml kind: phase id: full-design input: ports: previous: { contract: full-requirements } execution: goal: 形成可执行、可评审的技术方案,并明确开发范围 executor: { kind: orchestrator } skills: - { id: design , path: skills/design/SKILL.md , load: entry , required: true } - id: component-reuse path: skills/component-reuse/SKILL.md load: on-demand required: false when: { platforms: [ android , ios ] } output: ports: implementation-plan: { contract: full-design } artifacts: - { id: plan , path: design/plan.md , required: true } - { id: task-graph , path: design/tasks.md , required: true } ```
산출물의 내용은 Agent가 작성하고, 산출물이 빠짐없이 갖춰졌는지, 인계가 계약을 충족하는지는 CLI가 판단한다. 단계 출구의 분업은 바로 이 한 가지다.
단계 skill과 원자 skill의 바인딩
Phase의 실행 구간은 이 단계에서 사용할 수 있는 skill을 나열하며, 각 항목에 ID, 파일 경로, 로드 방식, 필수 여부, 적용 조건을 적는다. 로드 방식은 두 가지뿐이다:
- 진입(入口): 각 단계마다 정확히 하나이며 필수이고, 이 단계의 주 skill이다. 이하 '단계 skill'이라 부른다;
- 필요 시(按需): 원자 skill로, 단계에서 구체적 상황을 만났을 때만 로드한다.
우리의 각 단계는 대개 진입 1개에 필요 시 로드 10여 개가 붙는다.
skill 간의 호출 관계는 별도의 skill 그래프에 둔다. 각 간선에는 어느 단계 skill이 어떤 상황에서 어떤 원자 skill을 로드하는지, 그리고 그 이유를 적는다:
```yaml edges: - from: design to: component-reuse type: dynamic_load reason: 发现可复用组件缺口时加载,先盘点目标仓已有组件,再决定复用还是新增 ```
인스턴스 시작 시 CLI는 단계마다 skill 선언을 하나씩 해석하고, 적용 조건에 따라 필터링하며(예: 이번 납품이 어떤 플랫폼을 포함하는지), 결과를 인스턴스의 구성 스냅숏에 기록한다:
- 각 단계가 어느 단계 skill에 대응하는지;
- 바인딩된 각 skill의 ID, 경로, hash. hash는 출처 추적에 쓴다.
이후 매 라운드 제출 시 hook은 스냅숏에서 현재 단계에 해당하는 부분을 꺼내 Agent에게 넘긴다: 단계 skill의 경로, 그것이 로드할 수 있는 원자 skill, skill 세션 기록의 위치. 로드할 수 있는 원자 skill은 skill 그래프에 있고 이 인스턴스에 이미 바인딩된 것들이다.
동시에 한 가지 요구 사항을 덧붙인다: 먼저 단계 skill을 완전히 읽어 들여 실행하고, 그다음 단계 skill이 skill 그래프에 따라 원자 skill을 명시적으로 호출해야 하며, 단계 skill을 '산출물 한 부 생성'으로 격하한 일반 작업으로 만들어서는 안 된다.
사람과 상호작용해야 하는 원자 skill(예: 요구사항 명확화)에는 별도의 세션 기록이 있으며, 단계 디렉터리에 두고 현재 활성 원자 skill, 상태, 답변 대기 중인 질문, 이미 내린 결정을 기록한다. 규칙은 세 가지다:
- 한 번에 한 질문만 한다;
- 사용자가 답하면 같은 원자 skill로 돌아가 계속하고, 하류로 건너뛰지 않는다;
- 활성 원자 skill이 완료를 반환해야 단계 skill이 다음을 호출한다.
압축하거나 세션을 바꾼 뒤에도 Agent는 이 기록으로 어느 질문까지 했는지 안다.
01편에서 말한 컨텍스트 윈도로 돌아가면: skill 파일은 평소처럼 두 호스트에 프로젝션되지만, 매 라운드에는 현재 단계의 skill만 Agent에게 넘기므로 윈도 안에 다른 단계의 방법이 나타나지 않는다.
State: 인스턴스 디렉터리의 상태 파일
각 인스턴스마다 독립된 디렉터리가 있고, 그 안에 상태 파일 하나, 단계마다 하위 디렉터리 하나, 그 밖에 동결된 구성 스냅숏, 현재 블로킹 설명, 체크포인트, 과정 기록이 있다.
상태 파일은 다음 내용을 기록한다:
- 목표;
- 현재 단계와 전체 상태;
- 각 단계의 상태, 시작·종료 시각, 검사 결과, 산출물;
- 블로킹 목록과 다음 단계;
- 산출물 등록표;
- 간단한 이력.
읽기·쓰기 분업은 Agent가 CLI를 조작하는 핵심이다:
- State는 CLI만 변경한다;
- Agent는 CLI의 조회 명령으로 State를 읽고, 진행이 필요하면 명시적으로 요청을 보낸다;
- hook은 State를 변경하지 않고, 인스턴스의 이벤트 기록에 관찰 한 건을 덧붙일 뿐이다.
납품은 워크플로 진입 skill에서 시작한다: 이 skill은 프로세스 선택, CLI를 호출한 인스턴스 생성, 중단 후 복구를 맡는다. 각 단계 skill의 서두에는 진행 중인 납품이 없으면 먼저 진입을 통해 시작하라고 적혀 있다.
단계 사이에는 경로와 hash만 전달한다
한 단계가 입력을 받아 결과를 넘길 때까지 순서는 고정이다: 최소한의 권위 있는 입력 → 집중 실행 → 결정론적 검사 → 인계 → 다음 단계.
인계는 작은 색인일 뿐이며, 인계 파일의 경로와 hash를 적고 본문은 복사하지 않는다. 하류는 Phase에서 선언되었고 이미 State에 등록된 인계 파일만 읽는다. 세 가지 내용은 인계로 쓸 수 없다:
- 채팅 속 요약;
- 전체 대화 이력;
- 서브 agent의 자기 진술, 예컨대 「완료했고 산출물은 어딘가에 있다」.
분업 원칙은 다음과 같다: 의존성 그래프, 명령 결과, 검사 결론은 결정론적 로직으로 처리하고, 모델은 의미 추출·종합·판단만 한다.
이것이 Agent가 CLI를 조작한다는 것의 단계 간 구체적 의미다: 인계의 형식과 행선지는 CLI가 제약하고, 인계의 내용은 Agent가 산출한다. 전부 Agent에게 맡길 때 빠지는 것이 바로 이 층의 제약이다.
State 진행의 세 가지 규칙
규칙 하나: 검사 통과가 상태 진행을 뜻하지는 않는다.
CLI의 검사는 결론만 제시한다. 단계가 다음으로 갈지는 Agent가 명시적으로 진행 요청을 보내고, CLI가 검증한 뒤 기록한다.
읽기 전용 조회 역시 아무것도 바꾸지 않는다. 상태를 한 번 조회한 것이 산출물을 이미 읽었거나, 이미 검수했거나, 권한을 받았다는 뜻은 아니다.
규칙 둘: 변경은 덮어쓰지 않는다.
단계를 다시 열 때는 그 단계 자신과 그것에 의존하는 하류의 인계 기록을 지우고, 무관한 분기는 계속 진행된다.
사용자가 결정, 범위 또는 권한을 바꾸면 새 개정판을 열고, 이전 버전은 조회용으로 보존하며, 이미 다운스트림에서 사용된 사실을 조용히 다시 쓰지 않는다.
규칙 3: 블로킹은 영향을 받는 부분만 막는다.
입력이 없거나 의존성이 완료되지 않았을 때는 관련 작업과 그 다운스트림을 일시 중지하고, 다른 독립적인 작업은 계속 진행한다. 블로킹에는 사유와 다음 단계를 명시하고 State에 기록한다.
이 세 가지는 Agent를 제약한다. CLI의 제약에도 마찬가지로 경계가 있어야 한다. 그렇지 않으면 모든 것을 CLI에 맡겼을 때의 경직성으로 되돌아간다.
우리는 이 부분에서 시행착오를 겪었다. schema와 hash를 강한 검증으로 만들고 나니, 초기 빠른 반복 단계에서는 산출물이 불안정하고 자주 수정해야 했으며, 수정할 때마다 대량의 호환 작업을 해야 했고, 하지 않으면 워크플로 진행이 막혔다.
현재 규칙은 다음과 같다:
- schema와 hash는 차단 조건으로 삼지 않으며, hash는 추적 용도로만 사용한다;
- 설정, 지식, 일반 산출물의 hash가 바뀌어도 사용을 거부하지 않고, 다운스트림 결과를 연쇄적으로 무효화하지 않는다;
- 실제로 차단하는 것은 네 가지뿐이다: 입력 누락, 비즈니스 실패, 권한 초과, 쓰기 충돌.
또한 인스턴스가 시작될 때 사용하는 버전과 설정을 동결하고, 이후 복구 시에는 동결된 근거에 따라 진행하며, 현재 설치된 기본값으로 보충해 쓰지 않는다. 동결 여부를 무엇으로 판단하든 hash는 애초에 같은 것인지만 답하며, 둘은 충돌하지 않는다.
워크플로 안의 사람 확인 지점 여섯 개
사람도 직렬 연결의 한 축이지만, 영향 범위가 있는 곳에서만 등장한다. 아래 여섯 지점에서 워크플로는 멈추고 사람의 확인을 기다린다:
확인 지점 | 발생 시점 | 사람이 판단하는 것 | 흐름 유형과 목표 범위 | 납품 시작 시 | 전체 흐름을 탈지 빠른 흐름을 탈지, 이번 납품이 무엇을 하는지, 어떤 저장소가 관련되는지 | 요구 사양 | 요구 단계 종료 시 | 사양이 정확한지, 확인 후에 설계로 진입 | 기술 방안 | 설계 단계 종료 시 | 방안을 개발 근거로 삼을 수 있는지 | 목표 저장소 쓰기 | 개발 진입 전 | 목표 저장소 수정을 승인하는지 | 리스크와 증거 생략 | 발생 시 | 명확한 리스크를 수용할지, 아니면 특정 증거를 생략할지 | 외부 쓰기 | 납품 단계 | 제출, 푸시, 배포, 또는 외부 시스템 쓰기를 할지
사람에게 묻지 않는 사항도 규칙에 적어 두고 Agent가 스스로 정한다:
- 기본적으로 어느 경로를 탈지;
- 단계를 어떻게 해석할지;
- 단계에 필수인 역량;
- 모호함이 없는 구현 세부 사항.
선택 가능한 역량은 먼저 가치와 영향을 설명한 뒤 묻는다. 범위 확대, 리스크 수용, 외부 쓰기는 각 항목을 개별 승인하며, 승인은 다음 항목으로 이어지지 않는다.
질문 방식은 앞서 설명한 세션 기록을 따른다. 한 번에 질문은 하나만 하고, 답변은 인스턴스에 기록한다. 이후 마음이 바뀌면 개정판을 통해 처리하고, 다운스트림에서 이미 사용한 결정을 덮어쓰지 않는다.
압축과 세션 교체 후의 복구
복구의 총원칙: 현재 인스턴스 디렉터리에 있는 파일만 읽고, 채팅 기록에서 추론하지 않으며, 다른 인스턴스에서 사실을 빌려오지 않는다; 이미 완료된 노드는 다시 하지 않는다.
두 가지 중단을 분리해 처리한다.
세션 내 압축. 호스트가 컨텍스트를 압축한 뒤 세션 시작 이벤트를 다시 트리거하며, 출처는 compact로 표시된다. hook은 이를 근거로 State에서 간결한 복구 인덱스를 생성해 윈도에 주입하며, 내용은 다음 절에서 다룬다.
새 세션으로 교체. hook은 현재 세션의 바인딩에 따라 해당 인스턴스를 찾는다. 새 세션이 아직 바인딩되지 않았다면 Agent는 상태 조회 명령에 인스턴스 ID를 붙여 복구한다. 진행 중인 인스턴스가 여러 개인데 사용자가 어느 것인지 말하지 않았다면 hook은 추측하지 않고, Agent가 사용자에게 한 번 묻는다.
어떤 중단이든 hook은 단지 지름길일 뿐이다. 컨텍스트가 불분명할 때 단계 skill은 Agent에게 먼저 상태 조회를 한 번 실행해 현재 단계, 상태, 블로킹 및 다음 단계를 얻은 뒤 무엇을 할지 결정하도록 요구한다. hook이 실행되지 않아도 이 경로로 진행할 수 있다.
hook은 SessionStart와 UserPromptSubmit에 연결된다
두 호스트의 설정에서 단계 컨텍스트를 주입하는 hook은 같은 진입점을 호출하며, 이벤트 이름을 함께 전달해 CLI에서 분기한다.
단계 컨텍스트와 관련된 이벤트는 두 가지다:
- SessionStart(compact 출처): 복구 인덱스를 주입하며, 인스턴스, 목표, 현재 단계, 단계 디렉터리, 다음 단계, 블로킹 사유, 단계 입력, 이전 단계 산출물의 경로와 요약을 포함한다;
- UserPromptSubmit: 현재 단계, 상태, 다음 단계, 블로킹 사유, 서브 agent의 필독 목록, 그리고 앞서 설명한 이번 턴의 skill 호출을 주입한다.
나머지 두 시점은 다른 일을 한다: 새로 시작할 때의 SessionStart는 환경과 버전 검사를 하고, Stop은 회고 단서를 정리하며, 이는 11편에 남긴다.
매 턴 제출 시 주입되는 내용은 대략 다음과 같다(축약했으며 중립적으로 명명):
<workflow-state> 현재 단계: 설계 status: active next_action: 리뷰 의견에 따라 방안을 수정한 뒤 다시 검사를 제출 subagent_readlists: ... </workflow-state> <skill-invocation> phase_skill: design(경로와 hash) atom_skills: component-reuse, code-search skill_session: <단계 디렉터리>/<세션 기록> 먼저 단계 skill을 완전히 읽고 실행한 뒤, 그것이 skill 그래프에 따라 적용 가능한 원자 skill을 명시적으로 호출한다. </skill-invocation>
hook의 경계는 네 가지다:
- 실패 시 통과: 명령 끝에서 오류를 삼키고 항상 정상 종료하며, 호스트의 실패 기본 동작에 의존하지 않는다; 진입점 내부에서 오류가 발생하면 경고를 주입해 Agent가 채팅 기록이나 다른 인스턴스의 상태로 계속하지 말고 먼저 바인딩 또는 단계 상태를 고치도록 요구한다;
- 짧은 타임아웃: 단계 컨텍스트 hook은 5초로 설정한다;
- 경로와 요약만 출력: 각 구간에는 길이 상한이 있다;
- 상태 변경 없음: 서브 agent를 파생시키지 않고, 빌드를 돌리지 않는다.
길이 상한은 필수다. 두 호스트 모두 hook 주입 길이를 제한하며, 초과분은 파일에 쓰고 모델에는 미리보기만 준다:
Claude Code | Codex | 압축 후 세션 시작 인식 | matcher에 compact 기재 | matcher에 compact 기재 | hook 주입 내용 상한 | 10,000자, 초과 시 파일 저장, 경로와 앞 2,000자 미리보기 제공, 조정 불가 | 기본 약 2,500 token, hook에 따라 조정 가능, 초과 시 파일 저장, 앞뒤 미리보기 제공 | UserPromptSubmit 기본 타임아웃 | 30초 | 600초 | 동일 이벤트의 여러 hook 병렬 실행 | 동시 시작 | hook 신뢰 | 워크스페이스 신뢰를 수락한 후에만 실행 | hook 정의의 hash로 신뢰를 기록하며, 변경 시 재검토 필요
복구 인덱스는 구간별로 잘리며, 구간 상한은 수백 자에서 3천여 자까지 다양하고, 전체는 양쪽 상한 안에 들어간다. 상한을 넘으면 모델은 미리보기만 보게 되고, 인덱스 뒤쪽 내용은 반드시 읽힌다고 볼 수 없다.
제목으로 돌아가서: Agent가 CLI를 조작한다
단계 사이는 CLI가 연결한다. Workflow는 순서와 의존성을 정의하고, Phase는 각 단계의 입력·실행·출력을 정의하며, State는 진행 상황을 기록하고, 검사는 결론을 제시한다.
단계 내부는 Agent가 연결한다. 단계 skill은 skill 그래프에 따라 원자 skill을 호출하고, Agent는 CLI를 통해 조회·등록·진행 요청을 한다.
사람은 여섯 지점에서 결정을 내리고, hook은 두 이벤트에서 알린다.
이러한 분업은 양쪽 모두에 경계가 있다. CLI는 중간 산출물의 형식과 행선지, 진행 조건을 제약하여 Agent가 지나치게 자유롭게 나아가는 것을 막고, schema와 hash는 카드 제어를 하지 않아 CLI가 지나치게 경직되는 것을 막는다.
제 03편부터 단계별로 하나씩 전개하며, 첫 번째는 요구사항 이해다.
ZzT
모바일 @水团子
111
글
155k
조회
166
팔로워