본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 28. 07:11

AI 에이전트의 내부 구조: 계획(Planning), 도구 사용(Tool Use), 메모리(Memory), 제약 조건(Constraints)

요약

AI 에이전트의 성능은 모델 자체보다 이를 둘러싼 소프트웨어 워크플로우에 의해 결정됩니다. 계획, 도구 사용, 메모리, 제약 조건, 검증이라는 5가지 핵심 요소를 통해 안정적인 프로덕션급 에이전트를 구축하는 방법을 다룹니다.

핵심 포인트

  • 에이전트의 핵심은 모델이 아닌 프롬프트와 오케스트레이션 워크플로우임
  • 성공적인 에이전트를 위한 5대 요소: 계획, 도구 사용, 메모리, 제약 조건, 검증
  • 에이전트는 목표 읽기, 행동 결정, 도구 호출, 메모리 업데이트, 검증의 루프를 반복함
  • 프레임워크와 관계없이 에이전트의 본질은 동일한 실행 루프를 가짐

“AI 에이전트”의 모든 데모가 영상에서는 인상적으로 보이지만, 더 날카로운 질문을 던지는 순간 무너지는 것을 목격한 적이 있나요?

에이전트는 자신 있게 잘못된 행동을 합니다. 방금 결정한 내용을 잊어버립니다. 존재하지 않는 도구(tool)를 호출하려고 시도합니다. 같은 파일을 계속해서 다시 쓰는 무한 루프에 빠집니다. 배포가 성공하지 않았는데도 차분하게 성공했다고 말합니다.

이것들은 모델의 실패가 아닙니다. 모델을 둘러싼 워크플로우(workflow)의 실패입니다.

왜냐하면 에이전트의 실체는 바로 그것이기 때문입니다: 언어 모델(language model)이 다음 단계를 선택하고 도구(tool)를 호출할 수 있는 소프트웨어 워크플로우(software workflow). "지능"은 어떤 비밀스러운 에이전트용 요정 가루에 들어있는 것이 아니라, 프롬프트(prompt)와 그 주변의 오케스트레이션(orchestration)에 들어있습니다. "에이전트"라는 단어를 걷어내면 다섯 가지의 배관(plumbing) 요소가 남습니다: 계획(planning), 도구 사용(tool use), 메모리(memory), 제약 조건(constraints), 검증(verification). 모든 프로덕션급(production-grade) 에이전트는 이 다섯 가지에 의해 성패가 결정됩니다.

이 글은 이 각각의 요소들을 심도 있게 살펴보는 긴 여정입니다. 마케팅용 버전이 아닙니다. 당신의 데이터베이스와 통신하는 무언가를 출시하기 전에 실제로 필요로 하는 수준의 상세한 내용입니다.

당신이 실제로 구축하고 있는 루프(Loop)

각 기둥을 개별적으로 다루기 전에, 전체 루프를 머릿속에 그려보세요.

유용한 에이전트는 매 턴마다 대략 다음과 같은 과정을 수행합니다:

  1. 목표를 읽습니다 (그리고 그와 관련된 모든 메모리(memory)를 읽습니다).
  2. 다음 행동을 결정합니다: 직접 답변하기, 도구(tool) 호출하기, 명확한 질문 던지기, 또는 중단하기.
  3. 도구를 호출했다면, 도구의 결과를 관찰하고 이를 다시 피드백합니다.
  4. 기억할 가치가 있는 것이 있다면 메모리(memory)를 업데이트합니다.
  5. 제약 조건(constraints)을 확인합니다: 예산을 초과했는가, 반복 횟수를 다 썼는가, 금지된 영역을 건드리고 있는가?
  6. 성공을 선언하기 전에 출력을 검증(verify)합니다.
  7. 완료되거나 중단될 때까지 루프를 반복합니다.

그게 전부입니다. 모든 프레임워크(LangGraph, OpenAI Agents SDK, Claude Agent SDK, smolagents, 혹은 다음 달에 출시될 그 무엇이든)는 서로 다른 기본값을 가진 동일한 루프의 다른 형태일 뿐입니다.

agent-loop.ts

async function runAgent(goal: string, ctx: AgentContext) {
  const state = ctx.startState(goal);

...

저 루프를 주의 깊게 살펴보세요. 에이전트 시스템에서 발생하는 모든 흥미로운 버그는 decide, check, assertAllowed, run, maybeStore라는 다섯 가지 메서드 호출 중 하나에서 발생합니다. 나머지는 단순한 장부 기록(bookkeeping)일 뿐입니다.

이제 각 요소를 하나씩 분해해 보겠습니다.

Loop diagram with a central model node connected to five pillars: planner, tool bus, memory, permission gate, and verifier

Planning (계획): 단계를 밟기 전의 단계

원샷 프롬프트(one-shot prompt)와 에이전트의 가장 큰 차이점은, 에이전트는 실행하기 전에 무엇을 할 것인지를 생각한다는 점입니다.

단순한 설정은 다음과 같습니다:

const reply = await model.complete(`사용자 요구사항: ${goal}. 실행하세요.`);

모델은 목표를 확인하자마자 즉시 행동으로 뛰어듭니다. 당신은 5단계가 필요할 수도 있는 작업에 대해 모델의 첫 번째 본능을 신뢰하고 있는 셈입니다. 사소한 작업이라면 괜찮습니다. 하지만 다단계(multi-step) 작업이 되면 무너집니다. 모델은 도구를 선택하고, 혼란스러운 결과를 얻으면, 당황하여 진행 상황을 환각(hallucination)하기 시작합니다.

계획(planning) 단계는 게임의 판도를 바꿉니다:

planning.ts

const plan = await model.complete({
  system: PLANNER_SYSTEM,
  user: `목표: ${goal}\n\n짧은 번호 매기기 방식의 계획을 생성하세요. 각 단계는 도구 호출(이름 + 인자)이거나 직접적인 답변이어야 합니다. 아직 아무것도 실행하지 마세요.`,
...

당신은 모델이 무엇인가를 건드리기 전에 계획을 확정하도록 요청하고 있습니다. 이 계획은 감사(auditable)가 가능해집니다. 사용자에게 보여주거나, 로그를 남기거나, 심지어 다른 모델이 검토하게 할 수도 있습니다. 무언가 잘못되었을 때, 에이전트가 의도한 것과 실제로 수행한 것 사이의 기록을 가질 수 있습니다.

Plan-Then-Execute(계획 후 실행) 대 ReAct

두 가지 지배적인 계획 스타일이 있으며, 이들은 매우 다른 사용성(ergonomics)을 가집니다.

**Plan-then-execute (계획 후 실행)**는 방금 우리가 작성한 방식입니다. 모델이 사전에 전체 계획을 생성하면, 러너(runner)가 이를 단계별로 수행합니다. 디버깅이 깔끔하고 로그를 남기기 쉽지만, 현실이 계획과 다를 때 복구하기가 어렵습니다. 모델은 파일 크기가 500MB가 될 것이라는 사실을 몰랐습니다. API가 다른 스키마 (schema)를 반환할 것이라는 사실도 몰랐습니다. 이제 계획은 틀렸고, 러너는 어떻게 적응해야 할지 모릅니다.

ReAct (reason + act, 추론 + 행동)는 사고(thinking)와 행동(acting)을 교차시킵니다. 매 턴마다 모델은 짧은 근거(rationale)를 작성하고, 하나의 도구 호출 (tool call)을 선택하며, 결과를 관찰한 뒤, 다음 근거를 작성합니다. 모델은 학습하면서 조정할 수 있습니다. 토큰과 지연 시간 (latency) 측면에서 비용이 발생하지만 (매 턴마다 전체 컨텍스트 비용이 발생함), 에이전트는 현실에 대해 정직한 상태를 유지합니다.

react_loop.py

def react_step(state):
    response = model.complete(
        system=REACT_SYSTEM,
...

하나의 스타일만 영원히 선택해야 하는 것은 아닙니다. 많은 유용한 에이전트들은 재계획 트리거가 있는 계획 후 실행 (plan-then-execute with a re-plan trigger) 방식을 사용합니다. 모델이 계획을 작성하면 러너가 예상치 못한 상황에 부딪힐 때까지 실행하고, 그 후 러너가 현재 상태를 바탕으로 모델에게 새로운 계획을 요청합니다. 순수한 ReAct 방식보다 저렴하며, 순수한 계획 후 실행 방식보다 적응력이 높습니다.

좋은 계획이란 실제로 어떤 모습인가

여기서 흔히 발생하는 실패는 모델이 너무 추상적인 계획을 작성하도록 내버려 두는 것입니다.

  1. 사용자의 요청을 이해한다.
  2. 관련 정보를 수집한다.
  3. 도움이 되는 응답을 제공한다.

이러한 계획은 쓸모가 없습니다. 이는 에이전트 버전의 "여러 사항을 논의함"이라고 적힌 회의 의제와 같습니다. 유용한 계획은 도구 (tools)와 인자 (arguments)를 명시합니다:

  1. ./src/api에서 list_files를 호출한다.
  2. *_handler.go와 일치하는 각 파일에 대해 read_file을 호출한다.
  3. 결과 전체에서 `

당신은 시스템 프롬프트(system prompt)와 예시를 통해 이러한 형태를 강제합니다. _"각 단계는 반드시 도구 목록에 있는 도구를 참조해야 하며 구체적인 인자(arguments)를 포함해야 합니다. '이해하다' 또는 '분석하다'라고 말하는 단계는 거부됩니다."_와 같은 한 줄의 문구는 사람들이 예상하는 것보다 훨씬 더 큰 역할을 합니다.

도구 사용 (Tool Use): 에이전트가 실제로 세상과 접촉하는 지점

도구가 없다면 에이전트는 챗봇에 불과합니다. 하지만 도구가 있다면 파일을 읽고, API를 호출하고, 데이터베이스를 쿼리하고, 메시지를 보내고, 명령어를 실행하는 등 실제로 무언가를 할 수 있습니다. 이것이 모든 흥미로운 기능이 탄생하는 지점이자, 가장 위험한 실패가 발생하는 지점이기도 합니다.

기계적인 관점에서 도구는 세 가지 요소로 구성됩니다: 이름, 인자(arguments)를 위한 JSON 스키마(schema), 그리고 모델이 도구를 선택했을 때 런타임(runtime)이 호출하는 함수입니다.

tool-definition.ts

const readFile = {
  name: "read_file",
  description: 
...

여기서 핵심적인 역할을 하는 네 가지 요소가 있는데, 그중 세 가지는 코드가 아닙니다.

설명(Description)이 곧 프롬프트다

모델은 이름이 아니라 설명을 바탕으로 도구를 선택합니다. 모호한 설명을 가진 read_file이라는 이름의 도구는 "사용자의 이메일을 찾아줘"라는 요청에 호출될 수 있습니다. 모델이 "음, 이메일은 아마 어딘가 파일 안에 있겠지"라고 생각하기 때문입니다. 반면 _"파일 경로를 이미 알고 있을 때 단일 파일을 읽는 데 사용하십시오. 검색 용도로 사용하지 마십시오. 검색에는 grep_repo를 사용하십시오."_라고 명시된 설명은 수백 번의 잘못된 도구 호출을 방지해 줄 것입니다.

도구 설명을 작은 사양서(spec sheets)처럼 취급하십시오. 도구의 용도와 용도가 아닌 것, 유효한 입력의 형태, 유효한 출력의 형태, 그리고 모델이 알아야 할 모든 예외 상황(edge cases)을 나열하십시오.

스키마(Schemas)는 제안 사항이 아니다

JSON 스키마는 당신의 유일한 계약(contract)입니다. 만약 모델이 스키마에 없는 인자를 만들어낸다면, 검증기(validator)는 해당 호출이 핸들러(handler)에 도달하기 전에 거부해야 합니다. 필수 필드가 누락된 경우도 마찬가지입니다. 만약 문자열이 ["read", "write", "delete"] 중 하나여야 하는데 모델이 "REMOVE"를 보낸다면, 거부하십시오.

모델은 뛰어나지만 제멋대로 행동하기도 합니다. 잘못된 도구 호출 (tool calls)을 거부하고 에러를 모델에 다시 피드백하는 것이 이를 그냥 수용하는 것보다 더 낫습니다. 모델은 루프 중간에 이를 학습하고 조정하기 때문입니다.

function validateToolCall(call: ToolCall, schema: JSONSchema) {
  const result = ajv.validate(schema, call.args);
  if (!result) {
...

부작용 (Side Effects)에는 다른 종류의 도구가 필요합니다

프레임워크들이 종종 모호하게 다루지만, 여러분은 구분해야 할 범주가 있습니다. 바로 **읽기 도구 (read tools)**와 **쓰기 도구 (write tools)**는 서로 다른 성격의 존재라는 점입니다.

읽기 도구는 재시도 비용이 저렴합니다. 만약 list_files가 아무것도 반환하지 않는다면, 다른 인자 (args)를 사용하여 다시 호출하면 됩니다. 해가 될 것이 없습니다.

쓰기 도구 (apply_patch, send_email, deploy_service, run_sql)는 되돌리는 데 비용이 많이 들며, 때로는 불가능하기도 합니다. 이러한 도구들은 별도의 권한 계층 (permission tier), 별도의 로깅 (logging), 그리고 종종 별도의 승인 단계 (approval step)를 가져야 합니다. 이 내용은 제약 조건 (constraints) 섹션에서 다시 다루겠지만, 도구를 설계할 때 그것이 어느 쪽에 속하는지 인지하고 설계하십시오.

Switch 문이 아닌 도구 버스 (Tool Bus)

도구가 세 개라면 switch 문으로도 충분합니다. 하지만 도구가 서른 개라면, 스키마 검증 (schema validation), 로깅 (logging), 타임아웃 강제 (timeout enforcement), 그리고 부작용 분류 (side-effect classification)를 한곳에서 처리하는 도구 레지스트리 (tool registry)가 필요합니다.

tool_bus.py

class ToolBus:
    def __init__(self):
        self.tools: dict[str, Tool] = {}
...

이 단일 클래스는 나중에 속도 제한 (rate limits), 감사 추적 (audit trails), 드라이 런 모드 (dry-run mode), 그리고 비용 추적 (cost tracking)을 추가하게 될 공간입니다. 과해 보일지라도 첫날부터 이를 구축하십시오. 그렇지 않으면 나중에 여기저기 흩어진 일회성 도구 핸들러 (tool handlers)에 이러한 기능들을 억지로 끼워 맞춰야 하는데, 이는 훨씬 더 나쁜 상황을 초래합니다.

메모리 (Memory): 세 가지 서로 다른 것을 숨기고 있는 단어

"메모리 (Memory)"는 에이전트 어휘에서 가장 과하게 사용되는 단어입니다. 이는 보통 최소 세 가지의 서로 다른 메커니즘이 결합된 것을 의미하며, 이들을 혼동하는 것이 "왜 에이전트가 방금 내가 말한 것을 잊어버렸지?"와 같은 버그를 일으키는 주요 원인입니다.

작업 메모리 (Working Memory) (컨텍스트 윈도우 (The Context Window))

이것은 지금까지의 대화 내용, 도구 결과(tool results), 그리고 시스템 프롬프트(system prompt)를 포함합니다. 이는 모델의 컨텍스트 윈도우 (Context Window) 내에 존재하며, 요청이 반환되는 즉시 사라집니다. 이는 모델의 컨텍스트 길이(context length)와 여러분의 지갑 사정에 의해 제한됩니다.

"에이전트가 잊어버렸다"라는 대부분의 불만은 작업 메모리 (Working Memory)에 관한 것입니다. 두 개의 별도 요청을 실행했는데 두 번째 요청에 관련 히스토리가 포함되지 않았다면, 모델은 여러분이 무엇에 대해 말하고 있는지 진심으로 알 수 없습니다. 해결책은 벡터 데이터베이스 (Vector Database)를 사용하는 것이 아닙니다. 해결책은 히스토리를 포함하는 것입니다.

스크래치패드 메모리 (Scratchpad Memory) (실행 내에서)

단일 에이전트 실행 (agent run) 내에서, 모델은 종종 "자신에게 메모를 남길" 수 있는 공간을 가질 때 이득을 얻습니다. 이것은 단순히 구조화된 작업 메모리 (working memory)입니다. 즉, 관찰 내용(observations), 중간 결과(intermediate results), 결정(decisions) 및 그에 따른 추론(reasoning)의 목록입니다.

scratchpad.ts

type ScratchpadEntry =
  | { kind: "observation"; toolName: string; result: unknown }
  | { kind: "decision"; rationale: string; choice: string }
...

스크래치패드 (scratchpad)는 다음 턴(turn)에 모델로 다시 피드백되는 정보입니다. 이것은 마법이 아닙니다. 에이전트가 수행한 작업 자체를 구조적으로 재현(replay)하는 것입니다. 핵심은 (컨텍스트에) 들어갈 수 있을 만큼 충분히 짧게 유지하는 것입니다. 계속해서 내용만 추가하기만 하는 스크래치패드는 에이전트가 긴 작업을 수행할 때 이성을 잃게 만드는 원인이 됩니다.

장기 메모리 (Long-Term Memory) (실행 간에)

이것이 사람들이 보통 "메모리"라고 말할 때 의미하는 것입니다. 즉, 에이전트가 향후 대화에서 회상할 수 있는 사실들의 저장소입니다. 사용자 선호도, 프로젝트 컨텍스트, 비용이 많이 드는 계산 결과, 과거 실패로부터 얻은 교훈 등이 여기에 해당합니다.

여기에는 세 가지 대중적인 형태가 있습니다:

형태 (Shape)형태 (Looks like)용도 (Good for)취약점 (Bad at)
키/값 (Key/value)Redis 또는 플랫 파일 (flat file)안정적인 사실 (사용자 역할, 선호 언어)모호하거나 의미론적인(semantic) 내용
...

파일 기반 메모리(File-based memory)는 과소평가되어 있습니다. Claude Code와 몇몇 다른 에이전트 도구들은 정확히 이 방식을 사용합니다: 작은 MEMORY.md에 의해 인덱싱된 마크다운(markdown) 파일 디렉토리입니다. 에이전트는 파일을 읽고, 쓰고, 편집합니다. 마이그레이션할 것이 없으며, git diff로 확인할 수 있고, 사용자는 파일을 삭제함으로써 메모리를 삭제할 수 있습니다. 벡터(vectors) 방식보다 확장성은 떨어지지만, 추론하기가 훨씬 더 쉽습니다. 또한 실패 모드도 "우리가 올바른 사실을 의미론적으로 검색하여 에이전트가 확신을 가지고 잘못 사용했다"가 아니라, "올바른 파일을 찾을 수 없었다"가 됩니다.

도구로서의 메모리, 배경 프로세스가 아닌 (Memory As A Tool, Not A Background Process)

이 분야 전체에서 가장 깔끔한 설계 결정은 다음과 같습니다: 메모리는 단지 두 개의 도구일 뿐입니다. recall(query)(회상)와 remember(fact)(기억)입니다. 모델은 파일을 읽거나 메시지를 보낼 때를 결정하는 것과 동일한 방식으로, 언제 회상하고 언제 기억할지를 스스로 결정합니다.

그 대안인, 모든 프롬프트에 "관련된 메모리"를 마법처럼 주입하는 배경 프로세스(background process)는 편리하게 들리지만, 실제로 디버깅하기에는 악몽과 같습니다. 검색을 자동화해서 아낀 시간보다, 에이전트가 왜 사용자의 오래된 API 키를 무작위로 언급했는지 설명하는 데 더 많은 시간을 쓰게 될 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0