어이, openai 이렇게 대충 만들거야?
AI Agent를 요즘 공부하고 구현해보고 있습니다. 자료들 찾아보던 중 OpenAI에서 codex의 에이전트 루프를 어떻게 구현하였는 지에 대한 글을 발견하고 정리를 해보고자 합니다. openai 는 못참지..
저는 알게된 사실 위주로 정리해보고자해서 전문은 아래 링크를 참고하시면 됩니다.
https://openai.com/ko-KR/index/unrolling-the-codex-agent-loop/
Codex 에이전트 루프 풀어보기
Responses API를 사용해 Codex CLI가 모델, 툴, 프롬프트, 성능을 어떻게 오케스트레이션하는지 설명하는 Codex 에이전트 루프에 대한 기술적 심층 분석입니다.
openai.com
제목으로 어그로를 좀 끌어봤는데요, 읽어보면서 느낀 점은 생각보다 단순한데? 입니다
AI Agent라 하면 주어진 상황에서 스스로 판단하고 사전 정의된 툴/도구 를 활용해서 목적을 달성하는 인공지능 시스템? 정도라고 할 수 있을 것 같습니다. 정확한 정의는 아니겠지만요. 저희가 가장 흔하게 접하는 것으로는 아마 이 글에서 소개하는 codex, claude code 같은 코딩 에이전트 들이 있을 것 같아요. 요즘에는 사실 그냥 chatgpt 이런 것도 다 단순히 llm 이 하는 게 아니라 에이전트처럼 작동을 하죠.
AGENT LOOP
처음 이 글을 읽어보기 전에는 codex? 얼마나 복잡하게 구현이 되어 있으려나... 이럴 때는 이 툴 저럴때는 저 툴 이런 상황에서는 어떻게 뭐 어떤 루프를 통해 구성될까 싶었습니다.
그런데 음?
이게 끝?
정말 간단 하더라고요.
쉽게 말하면 아니 사실 있는 그대로 말하면 USER INPUT 그러니까 우리가 그냥 하는 질문을 받아서 MODEL INFERENCE 즉 모델이 고민해서 결정을 합니다. 그냥 응답을 할지? 아니면 도구를 호출할지. TOOL CALLS 즉 도구가 필요하다고 판단하면 도구 호출한 결과를 다시 모델에게 전달을 합니다. 그러면 도구 USER INPUT 그 밑에 TOOL CALLS 결과 까지 포함해서 다시 모델에게 주는거에요. 그 과정을 반복하다가 모델이 아. 이제 끝 결과 나왔다 판단하면 AGENT RESPONSE 로 우리가 받는 응답이 나오는 거죠.
하지만 우리는 AI에게 한 번만 질문을 하지 않습니다. "음 알겠는데, 이건 뭐야?" "이건 별론데 이렇게 고쳐줘" 그러면 AI 가 다시 기존 답변을 받고 응답을 합니다.
맞습니다.
그런 응답들도 다시 모델에게 넣어줘야겠죠? 방금까지는 싱글 턴, 즉 한 번의 질문과 대답을 루프로 나타낸 것이고 일반적으로는 multi-turn 그러니까 여러번 질문, 응답을 하는 우리가 흔히 생각하는 구조에요.
llm에 관심이 있는 분들은 모두 아시겠지만, llm에는 정해진 input이 있고 한번의 입력 그리고 하나의 출력으로 구성됩니다. 즉 우리가 했던 대화들을 모조리 모아서 다시 모델에 넣어서 출력을 받는 것이에요. 그러니까 대화가 길어질 수록 ai들이 헛소리를 많이 하는 거죠. 읽어야 할 것들이 많아지니까..(그러니 대화기록은 제때 지웁시다)
한 번에 모델에 넣어줄 수있는 내용의 크기를 context window라고 합니다. 이 컨텍스트 윈도우는 agent를 개발하는 데 매우 중요한 요소 중 하나입니다.
아무튼, loop는 이정도로 구성되었고 다음은 model inference 관련된 부분입니다.
MODEL INFERENCE
codex CLI는 모델 추론을 실행하기 위해 Responses API 로 HTTP 요청을 보낸다고 합니다. API를 쏘고 받고 하는 과정에서 프롬프트를 어떻게 관리하는 지 설명을 하고있는데 한 번 알아보겠습니다.
Responses API는 여러 매개변수를 포함한 JSON 페이로드를 받는데, 여기서는 크게 세 가지를 중심으로 설명하고 있습니다.
- instructions : 모델 컨텍스트에 삽입되는 system 메세지
- tools : 모델이 호출할 수 있는 도구 리스트
- input : 모델에 전달되는 텍스트, 이미지
이미지로 보면
이런식으로 구성되는 거죠.
그럼 멀티턴 구조로 생각을 해보겠습니다.
첫 번째 턴, 우리가 모델에게 어떠한 요청을 한 경우입니다. HTTP request를 Responses API에 쏘는게 시작입니다. 서버는 SSE(Server-Sent Event) 로 응답한다고 합니다. 그래서 이벤트들로 구성이 되어있는데, "response" 로 시작하는 "type" 을 가진 JSON페이로드 라고 합니다.
data: {"type":"response.reasoning_summary_text.delta","delta":"ah ", ...}
data: {"type":"response.reasoning_summary_text.delta","delta":"ha!", ...}
data: {"type":"response.reasoning_summary_text.done", "item_id":...}
data: {"type":"response.output_item.added", "item":{...}}
data: {"type":"response.output_text.delta", "delta":"forty-", ...}
data: {"type":"response.output_text.delta", "delta":"two!", ...}
data: {"type":"response.completed","response":{...}}
이런 느낌으로 쏴지는거죠
여기서 특히 response.output_item.added 같은 경우 아래와 같은 것들을 포함합니다.
[
/* ... original 5 items from the input array ... */
{
"type": "reasoning",
"summary": [
"type": "summary_text",
"text": "**Adding an architecture diagram for README.md**\n\nI need to..."
],
"encrypted_content": "gAAAAABpaDWNMxMeLw..."
},
{
"type": "function_call",
"name": "shell",
"arguments": "{\"command\":\"cat README.md\",\"workdir\":\"/Users/mbolin/code/codex5\"}",
"call_id": "call_8675309..."
},
{
"type": "function_call_output",
"call_id": "call_8675309...",
"output": "<p align=\"center\"><code>npm i -g @openai/codex</code>..."
}
]
요런 친구들은 다음 Repsonses API 호출의 input 배열에 들어가야 한다는 것이죠.
결국 아래와 같은 형태가 됩니다.
이 구조에서 주목해야할 점은 바로 기존 input 그러니까 첫 번째 턴에서 입력했던 input 그대로 가 두번째 input의 prefix 즉 앞부분이 된다는 것입니다. 이렇게 동일하게 들어가는 이유는 바로 caching(캐싱) 때문입니다. 간단하게 자주 사용하는 데이터의 값을 저장해두고 다시 계산하지 않고 불러와서 사용한다는 것이죠. 하지만 이것은 다음 섹션에서 다룹니다.
아무튼 여기서 핵심은 지속해서 프롬프트가 반복된다는 것입니다. 이전의 프롬프트가 llm의 도구 호출 뒤에 다음 프롬프트의 prefix 가 되고 또다시 prefix가 되면서 모델에 넣어주어야 하는 컨텐츠의 양이 늘어납니다.
그럼 여기서 어느정도 의문부호가 들 것 같은데요.
한 두번 반복하면 모르겠지만 4번 5번 아니 계속 지속되면?
"프롬프트가 너무 길어지는 것 아니야?"
바로 그 생각이 맞습니다. openai에서도 이 부분을 짚고 넘어가고 있습니다. ai 모델에서 모델 샘플링 비용이 가장 큰 부분을 차지하고 있기 때문에 프롬프트에 넣어줘야 하는 컨텐츠가 엄청나게 늘어나면 문제가 됩니다.
하지만, 당연히 openai 가 바보도 아니고 그렇게 두지 않겠죠.
아까 캐싱이라고 잠깐 언급했는데요, 여기서 바로 캐싱이 중요한 역할을 합니다. 매번 프롬프트에 prefix가 이전의 프롬프트가 되기때문에 즉 동일한 부분이 들어가기 때문에 이전에 계산했던 값을 그대로 이용할 수 있는 것입니다.
정확히 동일한 부분이 있어야 cache가 hit 되는데요, 그래서 openai는 cache를 hit하기위해 많은 노력을 하고 있습니다.
지침이나 예시 같은 정적인 콘텐츠를 먼저 배치하고 사용자 정보와 같이 변하는 콘텐츠는 뒤로 배치합니다.
하지만 쉽지 않은 경우들도 있습니다. 모델에서 사용가능한 도구가 바뀌거나 Responses API에서 요청의 대상이 되는 모델을 바꾸는 등의 경우는 도구의 순서가 바뀌거나 등의 이유로 캐시가 hit되지 못합니다. 그래서 해당 부분 같은 경우 openai에서는 가능한 수정이 있는 부분은 해당 내용에서 수정하지 않고 뒤에서 내용을 추가하는 방식으로 진행을 한다고 합니다.
이런 식으로 model sampling 비용을 줄이기 위해 많은 노력을 하고 있음을 알 수 있었습니다.
context window에 대해 생각하다 보면, model sampling뿐만 아니라 넣어줘야 하는 내용이 context window를 초과하는 경우도 생각할 수 있습니다. 이만큼 지금 정보를 넣어줘야하는데 그게 모델의 한도보다 크다면? 하지만 그 작업을 계속 시행해야 한다면?
그럴때 사용하는 것이 /compact입니다. 기존에는 수동으로 /compact를 사용해 새로운 세션에서 진행하도록 구현하였지만 현재는 /responses/compact endpoint 를 구현하여 context window가 auto_compact_limit 을 초과하면 엔드포인트를 자동으로 사용하여 대화를 압축합니다.
여기까지가 Urolling the codex agent loop 에 대한 내용입니다. 이어지는 글에서는 CLI 아키텍처, 툴 사용이 어떻게 구현되는지 그리고 codex의 샌드박싱 모델을 자세히 다룬다고 하니 또 리뷰를 해보도록 하겠습니다.
REFRENCE
'식빵 > AI' 카테고리의 다른 글
| Claude opus 4.6 시스템 프롬프트 유출 (0) | 2026.02.09 |
|---|---|
| SSE(Sever-Sent Event) 로 구현하는 LLM - 실습까지 (0) | 2026.02.01 |