도구를 부를 수 있다는 것과 에이전트는 다르다. 조회 결과를 받은 뒤 끝낼지, 다른 자료를 찾을지, 확인을 요청할지 누군가 정해야 한다. 그 결정을 모델에 맡기면 입력에 따라 유연하게 움직이지만 호출 횟수와 실패 경로도 늘어난다. 고정된 티켓 업무 흐름과 제한된 도구 선택 루프를 나누고, 제어 로직은 대역 계획기로 검증한다. 실제 모델이 어떤 행동을 제안하든 서버가 허용하지 않은 실행은 일어나지 않게 만드는 것이 목표다. 대역 실험의 성공은 모델의 계획 능력과 무관하다.
정해진 경로와 동적인 경로
워크플로는 다음 단계가 코드의 규칙으로 정해진 흐름이고, 이 책의 에이전트 루프는 관찰 결과를 받아 모델이 다음 행동을 제안하는 구조다. 이 구분은 Anthropic의 설계 안내에서도 쓴다. 제품 이름보다 다음 단계의 결정권이 어디에 있는지가 중요하다. 한울연구소의 티켓 등록은 고정 흐름이다. 초안 생성 뒤에는 무조건 확인 대기로 멈추고, 확인 이벤트 뒤에 호스트가 등록 메서드를 부른다. 조회 단계에서만 계획기가 종료 여부를 제안한다. 허용 행동은 티켓 조회, 티켓 초안 작성, 종료 셋이고, 도구 이름이 목록에 없으면 실행하지 않는다. 워크플로와 에이전트의 구분
한 단계의 순서
class Action(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
kind: Literal["get_ticket", "prepare_ticket", "finish"]
arguments: dict
ReAct 연구는 추론과 행동을 교차시키며 환경의 결과를 쓰는 접근을 제시했다. 이 예제는 그 벤치마크나 내부 추론 서술을 재현하지 않고, 밖에서 확인할 수 있는 행동 요청과 도구 결과만으로 제어를 구현한다. 실행을 추적하는 데 긴 사고 과정의 저장은 필요 없다. agent_loop.py의 행동 스키마는 추가 필드를 거부하고 도구 인자는 선택된 도구의 스키마로 다시 검사한다. 등록 도구를 요청하거나 중복 JSON 키로 도구 이름을 덮어쓰려는 출력도 거부한다. ReAct 논문
도구 결과도 그대로 관찰에 넣지 않는다. 티켓 조회는 티켓 ID·접수 상태·분류·제목·설명을, 초안 작성은 초안 ID·미리보기·확인 대기 상태를 검사하고 결과 크기에도 한도를 둔다. 계획기에 넘기는 관찰은 복사본이라 계획기 함수가 접수 상태를 해결 완료로 고쳐도 원본은 그대로다. 종료 행동은 자유로운 성공 문장을 받지 않는다. 관찰이 있으면 마지막으로 검증한 도구 결과를 돌려주고, 아무 도구도 실행하지 않았으면 no_action이다. completed는 이 제한된 루프가 결과를 얻고 끝났다는 뜻이고, 사용자의 복합 요구가 다 충족됐다는 증명과는 다르다.
네 가지 한도
result = run(
question, planner, tools,
max_steps=4,
max_units=12,
timeout_seconds=30,
)
루프를 멈추는 기준은 모델의 종료 판단과 별개로 있어야 한다. 크레딧은 제어 로직을 검증하는 정책 예산이고 API 가격이나 실제 토큰 비용과 다르다. 계획 요청 전에 1단위가 남았는지, 계획을 받은 뒤 도구 실행에 필요한 2단위가 있는지 검사한다. 실행한 뒤에 잔액을 줄이면 한도를 넘긴 작업이 이미 수행된다. 실제 금액 예산으로 바꾸려면 사용량·단가 계산에 예상 최대 비용의 예약과 정산을 더한다. 단계 한도는 제안 횟수를, 반복 지문은 같은 도구·같은 인자의 요청을 잡는다. 인자가 달라지는 반복은 지문으로 못 잡아서 단계 한도가 함께 있다. 시간은 단조 증가 시계로 마감을 계산하고 계획과 도구 호출의 앞뒤에서 확인한다.
마감 검사와 강제 취소는 다르다. 계획기가 60초 동안 멈추면 30초에 함수가 돌아오는 것이 아니라, 호출이 끝난 뒤 마감 초과를 확인해 다음 도구를 실행하지 않는다. 실행 시간을 강제하려면 클라이언트 타임아웃, 취소 가능한 비동기 작업, 프로세스 격리를 함께 설계한다. 도구가 늦게 돌아오면 execution_unknown이다. 시간을 넘긴 결과는 정상 완료로 내보내지 않고, 외부 부작용이 없었다고 볼 수도 없다.
| 종료 상태 | 뜻 |
|---|---|
| awaiting_confirmation | 초안 생성 후 사용자 확인을 기다린다 |
| completed | 검증한 도구 결과를 얻고 종료했다 |
| no_action | 도구 결과 없이 종료를 제안했다 |
| repeated_action | 같은 도구·인자 요청이 반복됐다 |
| step_limit | 허용한 계획 횟수를 소진했다 |
| budget | 다음 계획 또는 도구 실행 예산이 부족하다 |
| deadline | 마감 이후 다음 단계를 시작하지 않는다 |
| execution_unknown | 호출 이후 결과를 확정할 수 없다 |
부분 실패와 복구
재시도를 정하려면 결과가 불명확한 작업과 실행하지 않은 작업을 나눠야 한다. 예산 부족으로 도구를 부르지 않았으면 미실행이 확실하고, 도구 함수가 예외를 던졌으면 내부에서 상태를 바꾼 뒤 실패했을 수도 있다. 이 루프는 도구 예외 뒤 자동으로 재시도하지 않고 execution_unknown과 실행 시작 기록을 남긴다. 업무 상태를 조회하거나 같은 멱등성 키로 복구할 길이 있을 때 별도 경로를 고른다. 오류 문자열을 모델에게 주고 아무 도구나 다시 고르게 하는 것은 복구와 다르다.
초안 작성은 티켓 등록과 다른 상태 변화지만, 초안을 만든 직후 연결이 끊기면 확인되지 않은 초안이 남고 작업을 재시작하며 새 초안을 만들면 초안이 겹친다. 이 메모리 루프는 프로세스 재시작 뒤 상태를 복원하지 않는다. 운영 구현에서는 요청 ID·단계·선택 행동·도구 실행 식별자·확인 대기 상태와 결과를 체크포인트로 남기되, 저장된 단계에서 다시 시작할 때 외부 쓰기를 무조건 재실행하지 않고 완료 결과의 저장 여부와 실제 업무 상태를 대조한다. 확인을 기다리는 동안 권한이 바뀔 수 있어서 재개 시 현재 권한을 다시 검사하고, 승인된 초안의 본문이 달라졌으면 새 확인을 받는다.
실행 기록과 평가
python books/ai-engineering/examples/agent_demo.py
python -m unittest discover -s books/ai-engineering/examples -p 'test_*.py' -q
실행 기록은 무엇을 시도했는지와 무엇을 확인했는지를 나눈다. 이벤트에는 단계, 도구 이름, started·returned·unknown·late 같은 상태가 들어가고, 계획기의 내부 사고나 사용자 본문은 감사 로그에 복사하지 않는다. 도구 결과는 다음 계획의 관찰이지만 운영 로그와는 별개고, 권한이 다른 작업에 전체 관찰을 재사용하면 이전 요청의 티켓 정보가 섞이니 요청별로 격리한다.
평가는 업무 성공과 제어 준수를 따로 본다. 올바른 티켓 상태를 돌려줬는가, 확인 전에 등록이 일어나지 않았는가, 반복과 예산 초과를 막았는가, 실패 뒤 중복 상태를 만들지 않았는가가 각각 다른 항목이다. agent_demo.py는 대역 계획기를 실제 SQLite 티켓 구현에 붙여 초안 작성 뒤 티켓 행이 0개인지 보고, 테스트가 확인 이벤트를 넣어 등록한 뒤 조회와 종료 두 단계를 돌린다. 결과는 experiments/agent-control.json에 있다. 조회 후 종료는 2단계·4크레딧, 반복 조회는 두 번째 실제 호출 전에 멈췄고, 크레딧이 1단위뿐인 초안 요청은 계획 뒤 도구를 부르지 않았다. 대역 계획기는 테스트에 필요한 행동을 일부러 돌려주므로 이 실험은 모델이 자연어 요청에서 적절한 도구를 고르는 능력과 무관하다. 실제 모델을 붙일 때는 같은 제어 테스트를 유지하고 도구 선택과 인자 정확성 평가를 따로 둔다.
에이전트가 필요 없는 경우
단일 티켓의 상태 조회는 입력한 ID로 조회하고 결과를 보여 주면 된다. 사용자가 이미 명확한 ID를 줬는데 모델이 조회 여부와 종료 여부를 매번 판단하면 호출만 늘어난다. 초안 확인·등록도 고정 경로로 충분하다. 모델은 증상 설명을 정리하는 데만 쓰고 확인과 등록은 애플리케이션이 한다. 요청을 풀려면 필요한 자료나 도구가 입력에 따라 달라지고 관찰을 보고 다음 행동을 바꿔야 할 때 제한된 동적 선택을 검토하되, 작은 허용 목록과 단계 한도로 시작해 고정 흐름과 업무 성공·지연·운영 복잡도를 비교한다. 에이전트라는 이름은 채택 근거가 되지 못한다. 같은 평가 질문에서 무엇을 더 잘 처리하고 어떤 비용을 더 내는지 말할 수 없으면 더 단순한 경로가 답이다.
다중 에이전트
여러 에이전트로 나누는 설계는 작업을 독립적으로 분해할 수 있을 때 검토한다. 두 역할이 같은 전체 대화와 같은 넓은 도구 권한을 공유하면 이름만 나눈 셈이다. 문서 검색 담당에게 티켓 등록 권한을 줄 이유가 없고, 하위 작업의 결과도 검증한 뒤 상위 문맥에 넣는다. 한 에이전트의 주장이 원문이나 업무 시스템보다 우선하지 않는다. 예산은 상위 요청 전체에서 관리한다. 하위 작업에 배분한 비용·시간·동시 실행 수를 합산하고 남은 예산에서만 추가 위임하며, 종료한 작업의 결과와 취소된 작업의 부작용을 구분한다. 격리에는 문맥뿐 아니라 자격 증명과 저장 공간이 들어간다. 이 실습은 단일 루프고 다중 에이전트 실행은 구현하지 않았다. 심화로는 한 요청을 두 개의 독립 조회로 나누고 같은 권한·전체 예산으로 단일 실행과 비교한다.
직접 구현과 SDK
| 확인할 것 | 같은 이름이 가리킬 수 있는 다른 범위 |
|---|---|
| 최대 턴 | 모델 호출 수인가, 도구 호출 수인가 |
| 타임아웃 | 전체 요청인가, 개별 호출인가 |
| 자동 재시도 | 어느 단계에 붙는가, 외부 쓰기에도 붙는가 |
| 체크포인트 | 어느 단계에 기록되고 재개 시 무엇이 다시 실행되는가 |
직접 구현한 루프는 허용 행동과 종료 조건이 코드에 드러나는 대신 실행 상태의 저장, 재개, 스트리밍, 도구 프로토콜 연결을 직접 관리한다. SDK나 워크플로 엔진은 이런 공통 기능을 주지만 업무별 멱등성·확인·권한 정책을 대신 정하지 않는다. LangGraph는 체크포인트로 실행 상태를 보존하는 기능을 문서화하는데, 저장된 상태가 외부 티켓 API의 커밋 여부와 항상 같다고 볼 수 없다. 설정값만 옮기지 말고 기존 실패 시나리오를 새 구현에서 다시 돌린다. 이 장은 SDK를 설치해 동적 실행을 하지 않았고 대표 구현은 Python 루프다. MCP 연결과 결합하려면 비동기 호출·연결 종료·시간 제한·오류 변환이 더 필요하다. LangGraph 실행 상태 보존 안내
실습: 제어 조건을 깨뜨려 보기
- 계획기가 같은 조회를 계속 돌려주게 바꾸고 실제 도구 호출이 한 번인지 본다. 다음에는 인자를 매번 바꿔 반복 지문을 피하게 하고 단계 한도에서 끝나는지 본다. 두 검사는 다른 실패를 잡는다.
- 주입한 시계를 계획 뒤 마감 이후로 옮겨 도구가 실행되지 않는지 본다. 도구 안에서 시계를 옮기면 결과가 정상으로 표시되지 않는지도 본다. 이 테스트는 마감 판단을 검증하고, 멈춘 네트워크 호출의 강제 취소는 검증하지 않는다.
- 도구가 성공한 뒤 잘못된 구조를 돌려주게 한다. 실행기는
execution_unknown으로 멈추고 성공인지 미실행인지 알 수 없는 상태로 둔다. 실제 복구는 업무 시스템의 상태 확인과 멱등성 키로 한다.
도구 실행을 반복하는 경로에도 제어 장치가 생겼다.