본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 06. 15. 08:50

기초적인 AI 에이전트 처음부터 만들기: 긴 작업 계획 (Long Task Planning)

요약

AI 에이전트가 길고 복잡한 작업을 수행할 수 있도록 '긴 작업 계획(Long Task Planning)' 능력을 부여하는 방법을 다룹니다. 모델이 목표를 이해하고 단계를 나누며 계획을 수정할 수 있도록 스크래치패드(Scratchpad) 도구를 구현하는 과정을 설명합니다.

핵심 포인트

  • LLM의 대화형 특성으로 인한 장기 작업 수행의 한계 분석
  • 작업 목표 이해, 단계 분할, 진행 상황 추적 등 계획 능력의 필요성
  • 모델의 사고 과정을 기록하고 재사용하는 스크래치패드 도구 활용
  • Python을 이용한 메모리 기반 스크래치패드 구현 방법 제시

Build A Basic AI Agent From Scratch 시리즈의 이전 파트에서, 우리는 에이전트가 우리를 대신해 자율적으로 작동할 수 있도록 필수적인 도구들을 추가했습니다. 우리는 에이전트에게 파일을 찾고, 파일을 읽고 쓰고, bash 명령어를 실행하며, 웹에서 콘텐츠를 가져올 수 있는 능력을 부여했습니다. 이 도구들만으로도 매우 유능한 에이전트를 확보할 수 있었습니다.

에이전트가 길고 복잡한 작업을 수행할 때 어떤 일이 발생할까요?

현재의 에이전트는 매우 잘 작동하지만, 우리는 에이전트가 많은 양의 업무를 완수하기를 원하며, 이를 위해서는 긴 시간 동안 작업에 집중하는 것이 필요합니다. 현재로서는 에이전트에게 길고 복잡한 작업을 주려고 하면, 에이전트가 장기적인 관점에서 생각하지 못하고 아주 작은 진전이 있은 후 바로 작동을 멈추는 것을 발견하게 될 것입니다.

이는 LLM (Large Language Model)이 대화형으로 행동하도록 훈련되었기 때문에 예상 가능한 결과입니다. LLM은 질문과 답변을 기반으로 대화를 주고받는 것을 기대합니다. 이는 단순한 챗봇에게는 괜찮지만, 우리의 에이전트는 요청을 받은 후 결과를 반환하기 전까지 오랫동안 그 작업에 매달릴 수 있어야 합니다.

긴 작업 계획 (Long task planning)

우리가 에이전트에게 부여할 다음 능력은 길고 복잡한 작업을 계획하는 능력입니다.

에이전트에게 필요한 능력은 다음과 같습니다:

  • 작업의 목표를 이해함
  • 작업을 해결할 방법을 사전에 계획함
  • 작업을 구체적인 단계로 나눔
  • 대기 중, 진행 중, 완료된 작업을 추적함
  • 현재 계획에 문제가 생기면 접근 방식을 재고함
  • 중단하기 전에 계획된 모든 것이 실제로 완료되었는지 확인함

에이전트에게 이러한 능력을 부여하기 위해, 우리는 지난 파트에서 추가한 **도구 (tools)**에 의존할 것입니다. 또한 모델의 **시스템 프롬프트 (system prompt)**를 통해 모델에게 긴 작업 계획을 사용하는 방법을 설명할 것입니다.

새로운 도구: 스크래치패드 (Scratchpad)

이것은 매우 단순하지만 강력한 도구입니다. 우리는 모델에게 자신의 생각을 적고 나중에 다시 읽을 수 있는 장소를 제공하는 것뿐입니다.

이 도구의 주요 이점은 모델이 작업을 시작하기 전에 목표를 철저히 생각하고 전체적인 접근 방식을 계획하도록 강제한다는 점입니다.

이 도구는 스크래치패드 (scratchpad) 내용을 파일이나 데이터베이스 대신 메모리에 저장합니다. 세션 간에 스크래치패드 내용을 공유할 필요가 없으므로 이 방식은 적절합니다.

다음은 Python 구현 코드입니다:

class Scratchpad:

"""메모리 내 스크래치패드에서 읽기 및 쓰기 수행"""

def init(self):

self._content = ""

def read(self) -> str:

if self._content == "":

    return "(empty)"

return self._content

def write(self, content: str) -> str:

self._content = str(content).strip()

return self._content

scratchpad = Scratchpad()

def read_scratchpad():

"""스크래치패드의 내용을 읽음"""

return scratchpad.read()

def write_scratchpad(content: str):

"""

스크래치패드에 내용을 작성합니다. 이전 내용은

덮어씌워집니다.

"""

scratchpad.write(content)

return "Successfully written content into scratchpad"

이 코드는 이 블로그 시리즈의 <a href="https://github.com/rogiia/basic-agent-harness" target="_blank">Github 저장소 (repo)</a>에서 찾아 클론할 수 있습니다.

새로운 도구: 할 일 목록 (To-do list)

할 일 목록 (To-do list)을 사용하면 에이전트가 작업을 태스크 (tasks)로 분해하고, 무엇이 남아 있는지 (pending, 대기 중), 현재 무엇을 하고 있는지 (in progress, 진행 중), 그리고 무엇이 이미 완료되었는지 (done, 완료)를 추적할 수 있습니다.

이 도구는 또한 몇 가지 좋은 관행을 강제합니다. 즉, 동시에 여러 태스크가 진행 중인 상태를 허용하지 않으며, 유효하지 않은 태스크 상태를 허용하지 않고, 중복된 태스크를 허용하지 않습니다.

스크래치패드와 마찬가지로, 이 도구는 할 일 목록을 파일이나 데이터베이스 대신 메모리에 저장합니다. 에이전트 세션 간에 할 일 목록을 공유할 필요가 없으므로 이 방식 또한 적절합니다.

RETRY_LIMIT = 3

class ToDoList:

"""메모리에 할 일 목록을 보관하는 헬퍼 클래스"""

statuses = ["pending", "in_progress", "done", "cancelled", "failed"]

def __init__(self):

    self._items = []

def read(self, include_completed=False):

    """할 일 목록을 읽음"""

    if include_completed:

        return [item.copy() for item in self._items]

    else:

        return [item.copy() for item in self._items

if item["status"] != "done" and item["status"] != "cancelled"]

def append(self, id, content, status):

if status not in ToDoList.statuses:

raise Exception(f"Invalid status {status}. "

"Valid to-do statuses: pending, in_progress, done, "

"cancelled, failed")

if self.contains(id):

raise Exception(f"To do item {id} already exists!")

new_item = {"id": id, "content": content,
"status": status, "retries": 0}

self._items.append(new_item)

return new_item.copy()

def contains(self, id) -> bool:

"""Check if the to do list contains an item with a specific id"""

for item in self._items:

if item["id"] == id:

return True

return False

def update(self, id, content, status):

if status is not None and status not in ToDoList.statuses:

raise Exception(f"Invalid status {status}. "

"Valid to-do statuses: pending, in_progress, done, "

"cancelled, failed")

idx = 0

while idx < len(self._items):

if self._items[idx]["id"] == id:

if content is not None:

self._items[idx]["content"] = content

if status is not None:

prev_status = self._items[idx]["status"]
self._items[idx]["status"] = status

A failed task being set back to in_progress is a retry attempt.

if prev_status == "failed" and status == "in_progress":

self._items[idx]["retries"] += 1

return self._items[idx].copy()

idx += 1

raise Exception(f"To do item with id {id} not found")

todo_store = ToDoList()

def todo_append(id, content, status) -> str:

"""Append a new to do item to the to do list"""

id_str = str(id)
content_str = str(content)
status_str = str(status)

try:
todo_store.append(id_str, content_str, status_str)
return f"Successfully appended to do item {id_str} in to do list!"
except Exception as e:
return f"Failed to append to do item: {e}"

def todo_list(include_completed=False) -> str:

"""List all the items in the to do list"""

items = todo_store.read(include_completed)
result = f"To Do List ({len(items)} items)\n"
for status in ToDoList.statuses:

count = sum(1 for i in items if i["status"] == status)

result += f"{count} {status} items\n"

result += "-----\n"

for item in items:

retry_note = f", {item['retries']}
} retries" if item["retries"] > 0 else ""

result += f"- [{item['id']}] {item['content']}
} ({item['status']}{retry_note})\n"

return result

def todo_update(id, content=None, status=None) -> str:

if content is None and status is None:
return "No content or status was given to update. Nothing to do."

try:
item = todo_store.update(id, content, status)

retries = item["retries"]
if item["status"] == "in_progress" and retries > 0:
	if retries >= RETRY_LIMIT:
		return (
	f"Updated to do item {id} to in_progress - "
	f"but this is retry {retries} of {RETRY_LIMIT} (retry limit reached). "
	f"Do not retry again. Escalate to the user instead.\n"
	)
	return (
	f"Successfully updated to do item {id}! "
"Retry attempt {retries} of {RETRY_LIMIT}."
	)
return f"Successfully updated to do item {id}!"

except Exception as e:
return f"Failed to update to do item {id}: {e}"

새로운 시스템 프롬프트

도구에 구현할 수 없는 장기 작업 계획(long term task planning)을 위한 모든 전략은 시스템 프롬프트에 모델에게 설명됩니다. 여기서는 모델에게 기사 초반부에 설명된 프로세스를 사용하여 계획하는 방법과 또한 계획 과정에서 도움을 받을 수 있는 새로운 도구를 사용하는 방법을 설명할 것입니다.

더 자세한 내용은 아래의 시스템 프롬프트를 읽어주세요.

저는 또한 시스템 프롬프트에 모델에게 명시되지 않은 경우 작업해야 할 프로젝트는 현재 디렉터리에 있다는 것을 설명하는 작은 주석을 추가했습니다.

{

"role": "system",

"content": (

"당신은 유능한 코딩 및 연구 조수입니다.\n
"

"## 사용 가능한 도구\n
"Action tools: read_file, write_file, edit_file, glob_files, grep, run_bash, webfetch\n
"

"Planning tools:\n"

"- Scratchpad (read_scratchpad / write_scratchpad): 당신의 개인 작업 메모리."
}{

접근 방식을 구상하거나, 중간 결과물을 저장하거나, 확정하기 전에 내용을 초안으로 작성하는 데 사용하세요. 각 write 작업은 이전 내용을 완전히 대체합니다.

  • 할 일 목록 (todo_append / todo_list / todo_update): 지속적인 작업 추적기입니다. 항목은 pending (대기 중), in_progress (진행 중), done (완료), cancelled (취소됨), 또는 failed (실패함) 상태를 가집니다.

작업 디렉토리 (Working directory)

현재 작업 디렉토리는 항상 사용자의 프로젝트 루트입니다. 특정 경로가 지정되지 않은 상태에서 프로젝트나 코드베이스 작업을 요청받으면, glob_files 또는 run_bash를 사용하여 '.'를 탐색하는 것부터 시작하세요. 사용자에게 경로를 제공해 달라고 요청하지 마세요.

계획 세우는 법 (How to plan)

복잡하거나 다단계 작업(대략 3단계 이상의 별도 단계가 있거나, 진행 경로가 불분명한 경우)의 경우:

  1. 행동하기 전에 초기 생각과 접근 방식을 스크래치패드 (scratchpad)에 작성하세요.
  2. 작업을 구체적인 단계로 나누고, todo_append를 사용하여 각 단계를 할 일 목록에 추가하세요 (상태: pending).
  3. 단계를 시작하기 전에 todo_update를 사용하여 해당 단계를 in_progress로 표시하세요. 한 번에 하나의 항목만 in_progress 상태로 유지해야 합니다.
  4. 항목을 완료한 직후에 즉시 done으로 표시하세요. 완료 처리를 모아서 하지 마세요.
  5. 다음 단계로 넘어가기 전에 todo_list를 호출하여 남은 작업을 검토하세요.
  6. 작업이 불필요해지면 작업을 cancelled로 표시하세요.

단순한 단일 단계 작업의 경우: 할 일 목록을 만들지 않고 직접 수행하세요.

계획 도구 호출 (write_scratchpad, todo_append, todo_update, todo_list)은 내부적인 장부 기록(bookkeeping)이며, 사용자에 대한 응답이 아닙니다. 계획 도구를 호출한 후에는 항상 즉시 작업을 계속하세요. 다음 도구를 호출하거나, 작업이 완전히 완료되면 실질적인 최종 답변을 제공하세요. 빈 메시지나 공백만 있는 메시지를 출력하지 마세요.

재계획 (Replanning)

모든 도구 실행 결과 후에, 결과가 예상과 일치하는지 확인하세요. 만약 도구가 에러를 반환하거나, 예상치 못한 출력을 내놓거나, 작업에 대한 이해를 바꾸는 정보를 드러낸다면, 계획된 다음 단계로 넘어가지 말고 먼저 재계획을 세우세요.

"단계가 실패했을 때 (When a step fails):
"
"1. 스크래치패드에서 진단하기 - 이것이 복구 가능한 입력 오류(잘못된 경로, 오타, 잘못된 인자)인지 아니면 더 깊은 문제(잘못된 접근 방식, 잘못된 가정)인지 확인합니다."

"2. 작업 실패 표시: todo_update(id, status='failed')."

"3. 복구 조치 선택:
"
" - 재시도 (Retry): 실패가 수정 가능한 경우입니다. 입력을 수정하고 작업을 다시 'in_progress' 상태로 설정합니다. 도구는 몇 번째 재시도 시도인지 보고할 것입니다."
" - 대체 (Replace): 접근 방식이 잘못된 경우입니다. 작업을 취소하고 개정된 작업을 추가합니다."
" - 순서 변경 (Reorder): 새로운 정보가 다른 작업의 우선순위를 높이는 경우입니다. 계속하기 전에 보류 중인 항목을 업데이트합니다."

"4. todo_update가 재시도 한계에 도달했다고 보고하면, 재시도를 멈춥니다. 스크래치패드에 명확한 진단을 작성하세요 - 무엇을 시도했는지, 매번 무엇이 실패했는지, 그리고 무엇이 필요한지 - 그런 다음 사용자에게 간결한 에스컬레이션 메시지를 전달하고 입력을 기다립니다."

"도구가 성공했지만 상황을 바꾸는 정보를 반환할 때는 행동하기 전에 멈춥니다. todo_list를 호출하여 스크래치패드에 있는 모든 보류 중인 항목을 재평가하고, 더 이상 의미가 없는 작업은 취소하거나 대체합니다."

"## 스크래치패드 사용 방법

"
"복잡한 작업을 수행하는 동안 각 도구 호출 전에 현재의 사고 과정을 스크래치패드에 업데이트하세요. 각 항목은 다음 다섯 단계를 중심으로 구조화되어야 합니다:

"1. 목표 재진술 (Restate the goal) - 작업이 무엇인지 이해한 바를 자신의 말로 작성합니다. 이는 잘못된 해석이 누적되어 낭비되는 작업을 만들기 전에 포착해줍니다."
"2. 아는 것 조사 (Survey what you know) - 어떤 파일을 보았는지, 코드 구조가 어떻게 생겼는지, 그리고 적용되는 제약 조건이나 요구 사항은 무엇인지 기록합니다."
"3. 옵션 평가 (Evaluate options) - 최소한 두 가지 접근 방식을 추론하고 왜 하나를 다른 것보다 선택했는지 설명합니다 (예: '미들웨어를 다시 작성할 수도 있고, 래핑(Wrapping)할 수도 있습니다. 기존 호출 사이트를 건드리지 않는 것이 더 안전하기 때문에 래핑을 선택하겠습니다.')."
"4. 실패 모드 예상 (Anticipate failure modes) - 선택한 것에서 무엇이 잘못될 수 있는지 적어봅니다."
}<tool_call|>jsonjson
{

접근 방식과 이를 어떻게 진단할 것인지(예: '이후 테스트가 실패한다면, 가장 가능성 높은 원인은 세션 쿠키(session cookie) 이름이 변경되었기 때문입니다.')를 적어봅니다.

  1. 다음 단일 행동 결정 - 정확히 하나의 도구 호출(tool call)만을 결정합니다. 한 번에 여러 번의 호출을 계획하지 마세요. 오직 다음 단계만을 결정합니다.

도구 결과(tool result)를 받은 후 다시 시작할 때마다, 지금까지 학습한 내용에 근거하여 추론을 유지할 수 있도록 스크래치패드(scratchpad)를 다시 읽으세요.

완료 감지 (Done detection)

단순히 작업 목록(task list)이 비어 있다는 이유만으로 최종 답변을 내놓지 마세요. 작업을 완료했다고 선언하기 전에, 다음 세 가지 사항을 모두 확인하십시오:

  1. 구조적 완료(Structural completion) - todo_list를 호출하여 보류 중(pending), 진행 중(in_progress), 또는 실패(failed) 상태인 항목이 없는지 확인합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0