Step 01 — Deep Agent란 무엇인가
학습 목표
- 얕은(shallow) 에이전트가 왜 긴 작업에서 무너지는지 재현하고 설명한다
- Deep Agent 의 4대 기둥(계획 / 파일시스템 / 서브에이전트 / 시스템 프롬프트)이 각각 어떤 실패를 고치는지 짝지어 말한다
createDeepAgent 가 마법이 아니라 createAgent + 미들웨어 묶음임을 코드로 확인한다
createAgent 와 createDeepAgent 를 언제 무엇을 쓸지 기준을 세운다
- 컨텍스트 윈도우가 유한 자원이라는 사실이 Deep Agent 의 모든 설계를 지배함을 이해한다
선행 스텝: 실습 환경 (project/)
예상 소요: 50분
에이전트를 처음 만들면 대개 잘 돌아갑니다. "이 도시 날씨 알려 줘" 같은 요청은 도구 한 번 부르고 끝나니까요. 그런데 "리서치 보고서를 써 줘" 처럼 수십 단계가 필요한 일을 시키는 순간 무너집니다. 중간에 자기가 뭘 하려 했는지 잊고, 앞에서 찾은 자료를 잃어버리고, 결국 그럴듯하지만 얕은 글 한 덩이를 뱉고 끝냅니다.
Deep Agent 는 이 문제를 풀려고 나온 에이전트 하네스(harness) 입니다. 이 스텝에서는 새 API 를 배우지 않습니다. 대신 얕은 에이전트가 정확히 어디서 무너지는지를 보고, Deep Agent 가 그 자리에 무엇을 채워 넣는지를 봅니다. 앞으로 11개 스텝에서 배울 모든 것이 여기서 본 4개의 기둥을 하나씩 분해하는 작업입니다.
1-1. 얕은 에이전트의 한계
createAgent 로 에이전트를 하나 만들고 리서치 보고서를 시켜 봅시다. 도구는 주지 않습니다.
import { createAgent } from "langchain";
const RESEARCH_TASK =
"LangGraph, CrewAI, AutoGen 세 프레임워크를 비교하는 리서치 보고서를 써 주세요. " +
"각각의 아키텍처, 상태 관리 방식, 적합한 사용처를 다루고, 마지막에 선택 가이드를 붙여 주세요.";
const shallowAgent = createAgent({
model: "anthropic:claude-sonnet-4-6",
tools: [],
});
const result = await shallowAgent.invoke({
messages: [{ role: "user", content: RESEARCH_TASK }],
});
console.log(`메시지 개수: ${result.messages.length}`);
출력 예시 (모델 응답이므로 매번 다릅니다)
메시지 개수: 2
[ 0] human LangGraph, CrewAI, AutoGen 세 프레임워크를 비교하는 리서치 보고서를 써 주세요. …
[ 1] ai # 에이전트 프레임워크 비교 보고서 ## 1. 개요 LangGraph는 그래프 기반의 상태 머신…
메시지가 2개입니다. 사람이 한 번 묻고, AI 가 한 번 답했습니다. 이게 전부입니다.
내용 자체는 그럴듯합니다. 문단도 나뉘어 있고 표도 있습니다. 그런데 이 결과물에는 다음이 전부 없습니다.
| 없는 것 | 왜 문제인가 |
|---|
| 계획 | 무엇을 조사할지 정하지 않고 바로 쓰기 시작했습니다. 빠뜨린 항목이 있어도 본인이 모릅니다. |
| 자료 | 검색을 안 했습니다. 전부 모델이 학습 때 외운 것이고, 최신이 아닐 수 있으며, 출처가 없습니다. |
| 중간 산출물 | 초안이 없습니다. 한 번에 다 쏟아냈으니 고칠 대상도 없습니다. |
| 검증 | 자기 글을 다시 읽어 본 적이 없습니다. |
가장 중요한 건 한 턴에 끝났다는 사실입니다. 모델이 가진 유일한 전략은 "지금 아는 걸 최대한 쏟아내기" 였습니다. 실제로 사람이 이 일을 한다면 자료를 찾고, 메모하고, 초안을 쓰고, 다시 읽고 고칩니다. 얕은 에이전트에겐 메모할 곳도, 계획을 적을 곳도 없습니다.
⚠️ 함정 — "결과가 나왔으니 성공"이 아니다
이 함정이 이 코스 전체의 출발점입니다. 얕은 에이전트는 에러를 내지 않습니다. 보고서를 달라면 보고서 모양의 글을 줍니다. 그래서 문제를 발견하기가 어렵습니다.
에이전트가 실패했는지 보려면 결과물이 아니라 과정을 봐야 합니다. result.messages.length 가 2 라는 건 "도구를 한 번도 안 썼고, 스스로 점검한 적도 없다"는 뜻입니다. 이 숫자가 앞으로 우리가 볼 첫 번째 계기판입니다.
여기서 흔한 오해 하나를 짚고 갑시다. "프롬프트를 더 잘 쓰면 되지 않나?" — 어느 정도는 맞습니다. "먼저 계획을 세우고, 단계별로 진행하세요" 라고 시키면 조금 나아집니다. 하지만 모델이 세운 계획은 대화 기록 안의 텍스트일 뿐이라, 대화가 길어지면 앞쪽으로 밀려나 잊힙니다. 계획을 상태로 저장하고 갱신할 자리가 없다는 게 구조적 한계입니다. 그 자리를 만들어 주는 게 Deep Agent 입니다.
1-2. Deep Agent 의 4대 기둥
같은 요청을 createDeepAgent 에 던져 봅시다. 도구는 여전히 하나도 안 줍니다.
import { createDeepAgent } from "deepagents";
const deepAgent = createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
// tools 없음
});
const deep = await deepAgent.invoke({
messages: [{ role: "user", content: RESEARCH_TASK }],
});
console.log(`메시지 개수: ${deep.messages.length}`);
console.log(deep.todos); // ← 계획
console.log(deep.files); // ← 파일
출력 예시 (모델 응답이므로 매번 다릅니다 — 개수와 파일명은 실행마다 달라집니다)
메시지 개수: 34
[ 1] ai → 도구 호출: write_todos
[ 2] tool ← write_todos: Updated todo list to [...]
[ 3] ai → 도구 호출: task
[ 4] tool ← task: LangGraph는 상태 그래프 기반으로…
[ 5] ai → 도구 호출: write_file
...
── 에이전트가 세운 계획(todos) ──
☑ 세 프레임워크의 아키텍처 조사 (completed)
☑ 상태 관리 방식 비교 (completed)
▶ 보고서 초안 작성 (in_progress)
☐ 선택 가이드 추가 (pending)
── 에이전트가 만든 파일(files) ──
/report.md 4821자
/notes.md 1203자
메시지가 2개에서 수십 개로 늘었습니다. 도구를 하나도 안 줬는데 write_todos, task, write_file 을 부르고 있습니다. 이 넷이 Deep Agent 의 4대 기둥입니다.
기둥 1 — 계획 (planning)
write_todos 도구가 할 일 목록을 상태(state)로 만듭니다. 앞서 말한 "계획이 대화에 묻혀 잊히는" 문제를 고칩니다. 계획이 todos 라는 별도 필드에 살아 있으므로, 대화가 아무리 길어져도 밀려나지 않습니다. 상태가 pending → in_progress → completed 로 바뀌는 것도 중요합니다 — 에이전트가 자기가 어디까지 했는지 매 턴 다시 확인할 수 있습니다.
고치는 실패: "중간에 자기가 뭘 하려 했는지 잊는다"
기둥 2 — 파일시스템 (context offloading)
ls, read_file, write_file, edit_file, glob, grep 이 가상 파일시스템을 다룹니다. 핵심 개념은 오프로딩(offloading) 입니다.
검색 결과 3만 토큰을 대화에 그대로 쌓으면 컨텍스트가 순식간에 찹니다. 대신 /notes.md 에 써 두고 대화에는 "/notes.md 에 저장했음" 한 줄만 남기면, 컨텍스트는 거의 안 쓰면서 내용은 언제든 read_file 로 되찾을 수 있습니다. 컨텍스트를 메모리가 아니라 디스크처럼 쓰는 것입니다.
초안을 파일에 두는 것도 같은 이유입니다. 파일이면 edit_file 로 고칠 수 있습니다. 대화에 뱉은 글은 고칠 수 없습니다.
고치는 실패: "앞에서 찾은 자료를 잃어버린다", "한 번 뱉으면 못 고친다"
기둥 3 — 서브에이전트 (context isolation)
task 도구가 일회용 서브에이전트를 띄웁니다. 핵심은 격리(isolation) 입니다.
"LangGraph 를 조사해" 를 서브에이전트에 맡기면, 그 서브에이전트는 자기만의 깨끗한 컨텍스트에서 검색을 20번 하든 파일을 10개 읽든 마음대로 합니다. 그리고 부모에게는 최종 요약 한 덩이만 돌려줍니다. 부모의 컨텍스트에는 20번의 검색 기록이 아니라 결과 한 줄만 남습니다.
공식 문서의 표현을 그대로 옮기면 이렇습니다:
"the main agent receives only the final result, not the dozens of tool calls that produced it"
고치는 실패: "탐색 과정이 컨텍스트를 다 잡아먹는다"
기둥 4 — 상세 시스템 프롬프트
위 도구들을 언제 어떻게 쓰는지를 가르치는 긴 지침입니다. 도구만 쥐여 주고 "알아서 써" 하면 모델은 잘 안 씁니다. write_todos 를 언제 쓰고 언제 쓰지 말아야 하는지, task 로 뭘 위임해야 하는지를 수천 자에 걸쳐 설명해 줘야 비로소 제대로 씁니다.
고치는 실패: "도구는 있는데 모델이 안 쓴다"
💡 실무 팁 — 4대 기둥은 서로를 필요로 합니다
넷 중 하나만 켜면 효과가 잘 안 납니다. 계획만 있고 파일이 없으면 계획을 실행한 결과를 둘 곳이 없고, 파일만 있고 계획이 없으면 뭘 써야 할지 모릅니다. 서브에이전트만 있고 프롬프트가 부실하면 모델이 위임을 아예 안 합니다.
createDeepAgent 의 가치는 개별 기능이 아니라 넷을 같이 켜 주고 서로 맞물리게 조율해 준다는 데 있습니다. 그래서 "하네스"라고 부릅니다.
1-3. Deep Agent = 에이전트 하네스 (마법이 아니다)
여기서 정체를 밝히고 갑시다. createDeepAgent 는 새로운 종류의 에이전트가 아닙니다. createAgent 에 엄선된 미들웨어 묶음을 얹은 것입니다. 공식 문서도 스스로를 "agent harness"라고 부릅니다.
증명해 봅시다. 모델을 부르기 직전에 가로채는 미들웨어를 하나 만들어서, 이번 호출에 실제로 무엇이 실리는지 찍어 보겠습니다.
import { createMiddleware } from "langchain";
import { AIMessage } from "@langchain/core/messages";
type Observed = { tools: string[]; systemPrompt: string };
function createSpy(sink: Observed) {
return createMiddleware({
name: "Spy",
wrapModelCall: async (request) => {
sink.tools = (request.tools ?? []).map((t) => (t as { name: string }).name);
sink.systemPrompt =
typeof request.systemPrompt === "string" ? request.systemPrompt : "";
// handler(request) 를 부르지 않는다 = 진짜 모델을 부르지 않는다.
return new AIMessage("(스파이가 가로챘습니다 — 모델 호출 없음)");
},
});
}
wrapModelCall 은 "모델을 부르기 직전"에 끼어드는 훅입니다. request 안에 이번 호출에 실릴 도구 목록과 시스템 프롬프트가 전부 들어 있습니다. 우리는 handler(request) 를 부르지 않고 가짜 AIMessage 를 돌려줍니다 — 그래서 모델 호출이 0번이고, API 키 없이도 돌아가며, 토큰도 안 씁니다.
이제 도구를 하나도 안 준 Deep Agent 에 붙여 봅시다.
const sink: Observed = { tools: [], systemPrompt: "" };
const agent = createDeepAgent({ model: MODEL, middleware: [createSpy(sink)] });
await agent.invoke({ messages: [{ role: "user", content: "안녕" }] });
console.log(`도구: ${sink.tools.length}개`);
console.log(`시스템 프롬프트 길이: ${sink.systemPrompt.length}자`);
결과 (이 값들은 결정적입니다 — deepagents@1.11.0 에서 그대로 재현됩니다)
createDeepAgent 가 모델에 실은 도구: 8개
- edit_file
- glob
- grep
- ls
- read_file
- task
- write_file
- write_todos
시스템 프롬프트 길이: 6,979자
시스템 프롬프트 앞 120자:
"You are a Deep Agent, an AI assistant that helps users accomplish tasks using tools. You respond with text and tool call"
tools: [] 를 줬는데 도구가 8개입니다. 그리고 시스템 프롬프트를 한 글자도 안 썼는데 6,979자가 실려 있습니다.
이게 하네스의 정체입니다. createDeepAgent 가 한 일은:
| 미들웨어 | 얹어 주는 것 | 대응하는 기둥 |
|---|
TodoListMiddleware | write_todos 도구 + 사용 지침 | 기둥 1 (계획) |
FilesystemMiddleware | ls read_file write_file edit_file glob grep + 지침 | 기둥 2 (파일) |
SubAgentMiddleware | task 도구 + 위임 지침 | 기둥 3 (서브에이전트) |
SkillsMiddleware | 스킬을 필요할 때만 불러오는 장치 | (Step 10) |
SummarizationMiddleware | 컨텍스트가 찰 때 자동 요약 | (Step 08) |
PatchToolCallsMiddleware | 중단된 도구 호출 복구 | (Step 09) |
여기에 Anthropic/Bedrock 모델이면 프롬프트 캐싱 미들웨어가 자동으로 더 붙습니다.
💡 실무 팁 — 이 사실이 중요한 이유
"Deep Agent = createAgent + 미들웨어"를 이해하면 세 가지가 따라옵니다.
- 직접 조립할 수 있습니다.
createFilesystemMiddleware 같은 함수가 전부 deepagents 에서 export 되어 있습니다. 파일시스템만 필요하면 createAgent 에 그것만 붙이면 됩니다 (Step 08).
- 디버깅 방법이 같습니다. Deep Agent 가 이상하게 굴면 "미들웨어 중 누가 그랬나"를 보면 됩니다. 방금 만든 스파이가 바로 그 도구입니다.
- 비용을 예측할 수 있습니다. 6,979자는 공짜가 아닙니다. 1-5 에서 계산합니다.
⚠️ 함정 — 기본 general-purpose 서브에이전트가 이미 켜져 있다
위 목록의 task 도구를 눈여겨보세요. subagents 를 하나도 지정하지 않았는데 task 가 실렸습니다. Deep Agent 는 general-purpose 라는 기본 서브에이전트를 자동으로 켭니다.
그 서브에이전트의 설명은 이렇습니다(실제 문자열):
general-purpose: General-purpose agent for researching complex questions,
searching for files and content, and executing multi-step tasks. …
This agent has access to all tools as the main agent.
"메인 에이전트와 똑같은 도구를 전부 가진다"는 뜻입니다. 즉 모델은 언제든 task 를 불러 자기와 동급의 에이전트를 통째로 하나 더 띄울 수 있습니다. 그 서브에이전트도 6,979자 프롬프트를 다시 싣고 시작합니다.
여기서 예상 못 한 토큰이 샙니다. 간단한 질문에도 모델이 "이건 복잡하니 위임하자"고 판단해 버리면 비용이 몇 배가 됩니다. 에러는 안 납니다 — 청구서에만 나타납니다. 끄고 켜는 법은 Step 06 에서 다룹니다.
1-4. createAgent vs createDeepAgent
같은 스파이를 createAgent 에도 붙여서 둘을 나란히 놓아 봅시다.
const shallowObs = await observe((spy) =>
createAgent({ model: MODEL, tools: [], middleware: [spy] }),
);
const deepObs = await observe((spy) =>
createDeepAgent({ model: MODEL, middleware: [spy] }),
);
console.table({
createAgent: { 도구수: shallowObs.tools.length, 시스템프롬프트길이: shallowObs.systemPrompt.length },
createDeepAgent: { 도구수: deepObs.tools.length, 시스템프롬프트길이: deepObs.systemPrompt.length },
});
결과 (결정적입니다)
┌─────────────────┬────────┬────────────────────┐
│ (index) │ 도구수 │ 시스템프롬프트길이 │
├─────────────────┼────────┼────────────────────┤
│ createAgent │ 0 │ 0 │
│ createDeepAgent │ 8 │ 6979 │
└─────────────────┴────────┴────────────────────┘
0 대 8, 0자 대 6,979자. 이게 두 함수의 차이 전부입니다. 다른 마법은 없습니다.
비교표
| 항목 | createAgent | createDeepAgent |
|---|
| 출처 | langchain | deepagents |
| 기본 도구 | 0개 | 8개 (ls read_file write_file edit_file glob grep task write_todos) |
| 기본 시스템 프롬프트 | 없음(0자) | 약 7천 자 |
| 계획 수단 | 없음 (직접 todoListMiddleware 추가) | write_todos 내장 |
| 파일시스템 | 없음 | 가상 FS 내장 (백엔드 교체 가능) |
| 서브에이전트 | 없음 (직접 구성) | task + general-purpose 자동 활성화 |
| 자동 요약 | 없음 | SummarizationMiddleware 내장 |
| 기본 모델 | 지정 필수 | 생략 시 claude-sonnet-4-5-20250929 |
| 시작 토큰 비용 | 거의 0 | 약 1,700 토큰 |
| 적합한 작업 | 1~5턴짜리 정해진 일 | 수십 턴짜리 열린 일 |
언제 무엇을
createAgent 를 쓰세요:
- 턴 수가 예측 가능합니다 (도구 한두 번 부르고 끝).
- 무슨 도구를 언제 쓸지 여러분이 정해 놨습니다.
- 지연 시간과 토큰이 빠듯합니다. 챗봇, 분류기, 단순 질의응답.
- 흐름을 완전히 통제하고 싶습니다. 에이전트가 멋대로 파일을 쓰거나 서브에이전트를 띄우면 곤란합니다.
createDeepAgent 를 쓰세요:
- 턴 수를 예측할 수 없습니다. 리서치, 코드베이스 분석, 다단계 데이터 처리.
- 작업 중 만들어지는 중간 산출물이 많고, 그걸 다시 읽거나 고쳐야 합니다.
- 탐색이 컨텍스트를 다 잡아먹을 위험이 있습니다.
- 무엇을 해야 할지 에이전트가 스스로 정해야 합니다.
💡 실무 팁 — 애매하면 createAgent 부터
"Deep 이 더 좋아 보이니까 Deep 으로 시작하자"는 흔한 실수입니다. 날씨 챗봇에 createDeepAgent 를 쓰면 매 요청마다 1,700 토큰을 헛되이 태우고, 모델이 간단한 질문에도 write_todos 를 부르며 뜸을 들입니다. 사용자는 "왜 이렇게 느리지?" 합니다.
작업이 몇 턴 만에 끝나는가를 먼저 재 보세요. createAgent 로 만들었는데 result.messages.length 가 계속 20을 넘고 컨텍스트가 부족해지기 시작하면, 그때가 Deep 으로 옮길 때입니다. 반대 방향(Deep → 얕게)으로 가는 리팩터링이 훨씬 귀찮습니다.
⚠️ 함정 — 공식 문서는 await createDeepAgent(...) 라고 쓰는데, 이 함수는 동기다
공식 문서와 이 코스의 예제는 전부 await createDeepAgent({...}) 로 씁니다. 그래서 "비동기 함수구나, await 를 빼면 Promise 에 .invoke 를 부르게 되겠구나" 하고 생각하기 쉽습니다. 직접 찍어 보면 그렇지 않습니다.
const ret = createDeepAgent({ model: MODEL });
console.log(ret instanceof Promise); // false
console.log(ret.constructor.name); // "ReactAgent"
deepagents@1.11.0 의 타입 선언도 Promise<DeepAgent> 가 아니라 DeepAgent 를 반환한다고 되어 있고, memory / skills / subagents 를 줘도 Promise 가 되지 않습니다.
그래도 await 를 붙이세요. 이유는 두 가지입니다. (1) await 를 Promise 가 아닌 값에 붙이면 JS 는 그냥 그 값을 돌려줍니다 — 손해가 없습니다. (2) 문서 표기가 await 이므로, 향후 버전이 실제로 비동기가 되어도 여러분 코드는 안 깨집니다.
진짜 함정은 반대쪽입니다. agent.invoke(...) 앞의 await 를 빠뜨리는 것. 이건 진짜 비동기입니다. 빠뜨리면 result.messages 가 undefined 라며 엉뚱한 곳에서 터집니다.
1-5. 컨텍스트 엔지니어링 — 유한 자원이 모든 설계를 지배한다
Deep Agent 의 설계를 이해하는 열쇠는 하나입니다. 컨텍스트 윈도우는 유한한 자원이다.
RAM 이 무한하면 가비지 컬렉터가 필요 없고, 디스크가 무한하면 압축이 필요 없습니다. 컨텍스트가 무한하면 Deep Agent 도 필요 없습니다. 전부 대화에 쌓아 두면 되니까요. 유한하기 때문에 계획을 상태로 빼고, 자료를 파일로 밀어내고, 탐색을 서브에이전트에 격리합니다.
방금 잰 6,979자가 얼마인지 계산해 봅시다.
const roughTokens = (s: string) => Math.round(s.length / 4); // 영어 대략 4자 = 1토큰
const promptTokens = roughTokens(deepObs.systemPrompt);
const WINDOW = 200_000;
console.log(`시스템 프롬프트: 약 ${promptTokens.toLocaleString()} 토큰 (어림)`);
console.log(`→ 사용자가 한 글자도 치기 전에 약 ${((promptTokens / WINDOW) * 100).toFixed(1)}% 를 씁니다.`);
결과
시스템 프롬프트: 약 1,745 토큰 (어림)
컨텍스트 윈도우: 200,000 토큰
→ 사용자가 한 글자도 치기 전에 약 0.9% 를 씁니다.
0.9% 는 싸 보입니다. 하지만 이건 시작값일 뿐입니다. 실제로 컨텍스트를 먹는 건 이쪽입니다.
| 먹는 놈 | 규모 |
|---|
| 시스템 프롬프트 | 약 1,700 토큰 (고정) |
| 도구 스키마 8개 | 수백 토큰 (고정) |
| 검색 결과 1건 | 2,000~10,000 토큰 |
| 읽은 파일 1개 | 1,000~20,000 토큰 |
| 서브에이전트를 안 쓴 탐색 20회 | 100,000 토큰+ |
리서치 한 번에 검색 20번이면 고정 비용은 반올림 오차입니다. 변동 비용이 전부입니다. 그래서 Deep Agent 의 대응책도 전부 변동 비용을 겨눕니다.
Deep Agent 의 세 가지 대응책
| 전략 | 무엇을 하는가 | 언제 작동하는가 |
|---|
| 오프로딩 (offloading) | 도구 입출력이 크면 파일시스템에 저장하고 대화에는 참조만 남긴다 | 도구 결과가 20,000 토큰을 넘을 때 |
| 요약 (summarization) | 대화 전체를 "의도 / 만든 것 / 다음 할 일" 구조로 압축해 원본을 대체한다 | 컨텍스트가 max_input_tokens 의 85% 에 이를 때 |
| 격리 (isolation) | 서브에이전트가 자기 컨텍스트에서 일하고 결과만 돌려준다 | task 도구를 부를 때 |
세 숫자(20,000 / 85% / 10% 보존)는 공식 문서에 명시된 기본값입니다. 요약이 걸리면 최근 토큰의 10% 는 남겨 둡니다. 모델 프로필을 못 찾으면 170,000 토큰을 기준으로 대신 씁니다.
⚠️ 함정 — 요약은 공짜가 아니고, 되돌릴 수 없다
85% 에서 자동 요약이 걸리는 건 편리해 보이지만, 요약은 손실 압축입니다. 원본 대화가 요약본으로 대체되고, 사라진 세부사항은 영영 못 돌아옵니다.
여기서 조용한 사고가 납니다. 에이전트가 20턴째에 찾아낸 중요한 수치가 요약 과정에서 "관련 자료를 조사했음" 한 줄로 뭉개지면, 30턴째의 에이전트는 그 수치를 모르는 채로 보고서를 씁니다. 에러는 없습니다. 그냥 틀린 보고서가 나옵니다.
그래서 4대 기둥 중 파일시스템이 결정적입니다. 중요한 걸 파일에 써 두면 요약이 대화를 뭉개도 파일은 그대로 남고, read_file 로 언제든 되찾습니다. "중요한 건 대화에 두지 말고 파일에 둬라" — 이게 Deep Agent 를 잘 쓰는 첫 번째 원칙입니다. Step 04 에서 본격적으로 다룹니다.
💡 실무 팁 — 컨텍스트를 예산처럼 다루세요
숙련된 에이전트 개발자는 "이 작업에 컨텍스트 예산이 얼마인가"를 먼저 생각합니다. 200,000 토큰 중 시스템이 2,000 을 쓰고, 요약이 170,000 에서 걸린다면, 실질 작업 공간은 약 168,000 토큰입니다. 검색 한 번에 5,000 토큰이면 33번이 한계입니다.
이 계산을 해 두면 설계가 달라집니다. 33번으로 부족하면 검색을 서브에이전트에 격리해야 합니다. 그러면 부모 컨텍스트에는 요약 500 토큰만 남으니 330번도 가능해집니다. 이런 판단을 하려면 지금 뭐가 컨텍스트를 먹고 있는지 봐야 하고, 그 관측 도구가 바로 Step 11 의 LangSmith 입니다.
1-6. 코스 로드맵
이 스텝에서 본 4대 기둥을 앞으로 하나씩 분해합니다.
| Step | 주제 | 이 스텝과의 관계 |
|---|
| 01 | Deep Agent란 무엇인가 | ← 지금 여기 |
| 02 | 첫 Deep Agent | 옵션 전체 지도. 어느 스텝에서 뭘 다루는지 |
| 03 | 계획 도구 (write_todos) | 기둥 1 분해 |
| 04 | 가상 파일시스템 | 기둥 2 분해 — 오프로딩 |
| 05 | 백엔드와 권한 | 파일이 실제로 어디에 저장되는가 |
| 06 | 서브에이전트 | 기둥 3 분해 — 격리. general-purpose 끄기 |
| 07 | 시스템 프롬프트 설계 | 기둥 4 분해. 6,979자를 뜯어본다 |
| 08 | 미들웨어 조합 | 1-3 에서 본 하네스를 직접 조립 |
| 09 | HITL과 권한 제어 | 위험한 도구 실행 전에 사람이 승인 |
| 10 | 장기 메모리와 스킬 | 스레드를 넘어 기억하기 |
| 11 | 스트리밍과 프로덕션 | 안에서 무슨 일이 벌어지는지 보기 |
| 12 | 종합 — 딥 리서치 에이전트 | 1-1 의 실패한 요청을 제대로 해내기 |
Step 12 에서 다시 만날 요청이 바로 1-1 의 그 리서치 보고서입니다. 그때는 계획을 세우고, 자료를 파일에 모으고, 조사를 서브에이전트에 나눠 맡기고, 초안을 고쳐 쓰는 에이전트가 되어 있을 겁니다.
정리
| 개념 | 한 줄 |
|---|
| 얕은 에이전트 | 도구도 메모장도 없이 한 턴에 다 쏟아낸다. 에러 없이 얕은 결과를 낸다 |
| Deep Agent | createAgent + 엄선된 미들웨어 묶음 = 하네스. 마법 아님 |
| 기둥 1 — 계획 | write_todos. 계획을 상태로 만들어 대화에 묻히지 않게 |
| 기둥 2 — 파일시스템 | read_file/write_file 등. 큰 자료를 오프로딩하고 초안을 고칠 수 있게 |
| 기둥 3 — 서브에이전트 | task. 탐색을 격리해 부모 컨텍스트를 지킨다 |
| 기둥 4 — 시스템 프롬프트 | 약 7천 자. 도구를 언제 쓰는지 가르친다 |
| 컨텍스트 엔지니어링 | 윈도우는 유한 자원. 오프로딩 / 요약 / 격리가 그 답 |
핵심 함정 3가지
- "결과가 나왔으니 성공"이 아니다. 얕은 에이전트는 에러 없이 얕은 결과를 냅니다. 결과물이 아니라 과정(
messages.length, todos, files)을 보세요.
- 기본
general-purpose 서브에이전트가 이미 켜져 있다. subagents 를 안 줘도 task 가 실리고, 모델이 자기와 동급의 에이전트를 띄워 프롬프트 7천 자를 다시 태울 수 있습니다. 에러 대신 청구서로 나타납니다.
- 자동 요약은 손실 압축이고 되돌릴 수 없다. 85% 에서 걸리면 원본 대화가 요약본으로 대체됩니다. 중요한 건 대화가 아니라 파일에 두세요.
버전 특이사항: createDeepAgent 는 문서 표기와 달리 deepagents@1.11.0 에서 동기 함수입니다(Promise 를 반환하지 않음). 그래도 문서를 따라 await 를 붙이는 편이 안전합니다 — 손해가 없고, 향후 버전 변경에도 안 깨집니다.
연습문제
createAgent 로 "피보나치 수열 10번째 항을 구해 줘" 를 시키고 result.messages.length 를 찍으세요. 이어서 같은 요청을 createDeepAgent 로 시키고 두 숫자를 비교하세요. 어느 쪽이 이 작업에 적합한가요? 이유를 주석으로 적으세요.
- 본문의
createSpy 를 그대로 써서, createDeepAgent({ tools: [myTool] }) 처럼 커스텀 도구를 1개 줬을 때 모델에 실리는 도구가 몇 개인지 찍으세요. 커스텀 도구는 내장 도구를 대체하나요, 더해지나요?
createSpy 로 createDeepAgent 의 시스템 프롬프트 전문을 파일로 저장한 뒤(fs.writeFileSync), write_todos / task / read_file 이라는 단어가 각각 몇 번 등장하는지 세세요. 어느 기둥에 가장 많은 지면을 할애했나요?
createDeepAgent({ model: MODEL }) 의 반환값에 대해 instanceof Promise 와 constructor.name 을 찍어, 본문 1-4 의 함정을 직접 재현하세요. 이어서 await 를 붙인 결과와 안 붙인 결과가 같은지 확인하세요.
- (컨텍스트 예산) 컨텍스트 윈도우가 200,000 토큰이고 자동 요약이 170,000 에서 걸린다고 할 때, 검색 결과 1건이 6,000 토큰이면 요약 전에 검색을 몇 번 할 수 있나요? 시스템 프롬프트 약 1,700 토큰을 빼고 계산하는 코드를 쓰세요.
- (설계 판단) 다음 세 작업에
createAgent 와 createDeepAgent 중 무엇을 쓸지 고르고, 이유를 한 줄씩 적으세요.
- (a) 고객 문의를 "환불 / 배송 / 기타" 셋 중 하나로 분류
- (b) 저장소 전체를 읽고 아키텍처 문서를 작성
- (c) 사내 위키를 검색해 질문에 답하는 챗봇
- (기둥 짝짓기) 아래 실패 증상 4개를 4대 기둥 중 하나씩에 짝지으세요.
- (a) 검색 결과를 15개 쌓았더니 컨텍스트가 넘쳤다
- (b) 30턴째에 처음 세운 계획을 잊고 엉뚱한 걸 하고 있다
- (c) 도구는 다 붙여 줬는데 모델이 한 번도 안 부른다
- (d) 초안을 뱉었는데 고쳐 달라니까 처음부터 다시 쓴다
- (심화)
createSpy 를 고쳐서 handler(request) 를 실제로 호출하되 도구 목록과 프롬프트 길이도 찍게 만드세요. 그리고 RUN_LIVE=1 로 1-2 를 돌려, 실행 중 모델이 몇 번 호출되고 매번 프롬프트 길이가 어떻게 변하는지 관찰하세요. (힌트: 대화가 길어지면 프롬프트가 아니라 messages 가 커집니다)
문제만 담긴 파일은 exercise.ts, 정답과 해설은 solution.ts 입니다. 두 파일 모두 아래 실습 파일 섹션에 전문이 실려 있습니다.
다음 단계
→ Step 02 — 첫 Deep Agent
createDeepAgent 의 옵션 16개 전체 지도를 펼치고, 어느 스텝에서 무엇을 다루는지 짚습니다. 그리고 이 스텝에서 스파이로 훔쳐본 것들을 정식으로 다뤄 봅니다.
실습 파일
이 스텝은 TypeScript 파일 3개로 구성됩니다. 본문(1-1 ~ 1-6)의 예제를 순서대로 담은 practice.ts 를 먼저 실행해 결과를 눈으로 확인하고, 그다음 exercise.ts 의 8개 문제를 직접 풀어본 뒤, 마지막으로 solution.ts 로 채점하고 해설을 읽는 흐름입니다.
세 파일 모두 project/ 폴더에서 실행합니다.
cd docs/reference/deepagent/project
npx tsx ../step-01-why-deep-agents/practice.ts
이 스텝의 실습 파일은 대부분 API 키 없이 돌아갑니다. 1-3 에서 만든 스파이 미들웨어가 모델 호출을 가로채기 때문입니다. 실제 호출이 필요한 [1-1] [1-2] 는 기본적으로 꺼져 있고, 켜려면 앞에 RUN_LIVE=1 을 붙입니다.
RUN_LIVE=1 npx tsx ../step-01-why-deep-agents/practice.ts # 실제 API 호출 (토큰 소모)
practice.ts
본문 강의를 따라가며 손으로 쳐볼 예제를 [1-1] ~ [1-6] 주석 번호로 묶어 놓은 파일입니다. 절 번호가 본문 소제목과 1:1 로 대응하므로, 본문을 읽다가 막히면 같은 번호의 블록을 찾아 실행해 보면 됩니다.
[1-3] 의 createSpy 가 이 파일의 심장입니다. wrapModelCall 안에서 handler(request) 를 부르지 않는 것이 핵심입니다 — 그래서 모델 호출이 0번이고 토큰이 안 듭니다. 주석에도 적어 뒀지만, wrapModelCall 은 반드시 AIMessage 나 Command 를 돌려줘야 합니다. { result: [...] } 같은 객체를 돌려주면 MiddlewareError: … expected AIMessage or Command, got object 가 납니다.
observe() 헬퍼의 Invokable 타입이 invoke 를 메서드 표기로 선언한 것은 의도적입니다. 프로퍼티 표기({ invoke: (i: unknown) => … })로 쓰면 strictFunctionTypes 가 인자를 반공변으로 검사해서 createAgent 와 createDeepAgent 를 같은 자리에 못 넣습니다. 바꿔 보면 바로 타입 에러가 나니 한번 해 보세요.
[1-4] 의 console.table 이 0 / 0 과 8 / 6979 를 나란히 보여 줍니다. 이 네 숫자가 이 스텝의 결론입니다.
[1-4] 끝의 ret instanceof Promise → false 는 본문의 함정을 직접 재현하는 부분입니다. 공식 문서가 await createDeepAgent(...) 라고 쓰는 것과 실제 동작이 다르다는 걸 눈으로 확인시킵니다.
- 파일 맨 끝의 "참고" 블록은 커스텀 도구를 1개 줬을 때 도구가 9개(8+1)가 되는 걸 보여 줍니다. 연습문제 2번의 답이 여기 있으니, 문제를 풀기 전이라면 스크롤을 멈추세요.
/**
* Step 01 — Deep Agent란 무엇인가
* 실행: npx tsx docs/reference/deepagent/step-01-why-deep-agents/practice.ts
*
* 이 파일의 절반은 API 키가 없어도 돌아갑니다.
* [1-3] [1-4] [1-5] 는 "모델을 부르기 직전에 가로채서 관찰"하는 방식이라
* 실제 모델 호출이 일어나지 않습니다. 토큰도 안 씁니다.
* [1-1] [1-2] 만 실제 API 호출이 필요합니다.
*/
import "dotenv/config";
import { createAgent, createMiddleware, tool } from "langchain";
import { createDeepAgent } from "deepagents";
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
import * as z from "zod";
import { printSection, printMessages, printTodos, printFiles } from "../project/src/lib/print.js";
/* ── 이 파일 전체에서 쓰는 모델 ───────────────────────────────
문자열 형식은 "provider:model" 입니다. OpenAI 로 바꾸려면
"openai:gpt-5.5" 로 두고 @langchain/openai 를 설치하세요. */
const MODEL = "anthropic:claude-sonnet-4-6";
/* ── 실제 API 호출을 켤지 스위치 ──────────────────────────────
기본은 꺼짐입니다. 리서치 보고서 한 번이 수만 토큰을 태우므로,
의도적으로 켰을 때만 돌게 했습니다.
RUN_LIVE=1 npx tsx .../practice.ts */
const RUN_LIVE = process.env["RUN_LIVE"] === "1";
/* ===== [1-3] 관찰 도구 — 모델 호출을 가로채는 스파이 미들웨어 =====
*
* wrapModelCall 은 "모델을 부르기 직전"에 끼어듭니다. request 안에는
* 이번 호출에 실제로 실릴 도구 목록과 시스템 프롬프트가 다 들어 있습니다.
* 여기서 handler(request) 를 부르면 진짜 모델이 호출되지만,
* 우리는 부르지 않고 가짜 AIMessage 를 돌려줍니다. → 모델 호출 0회.
*
* 이게 이 스텝의 핵심 관찰 장치입니다. "Deep Agent 가 마법이 아니라
* 그냥 도구와 프롬프트를 얹어 준 것"임을 눈으로 확인시켜 줍니다.
*
* 주의: wrapModelCall 은 반드시 AIMessage 나 Command 를 돌려줘야 합니다.
* { result: [...] } 같은 객체를 돌려주면 이런 에러가 납니다:
* MiddlewareError: Invalid response from "wrapModelCall" in middleware
* "Spy": expected AIMessage or Command, got object
*/
type Observed = { tools: string[]; systemPrompt: string };
function createSpy(sink: Observed) {
return createMiddleware({
name: "Spy",
wrapModelCall: async (request) => {
sink.tools = (request.tools ?? []).map((t) => (t as { name: string }).name);
sink.systemPrompt =
typeof request.systemPrompt === "string" ? request.systemPrompt : "";
// handler 를 부르지 않는다 = 모델을 부르지 않는다.
return new AIMessage("(스파이가 가로챘습니다 — 모델 호출 없음)");
},
});
}
/* invoke 를 "메서드 표기"로 선언한 것이 의도적입니다. 프로퍼티 표기
({ invoke: (i: unknown) => ... })로 쓰면 strictFunctionTypes 가 인자를
반공변으로 검사해서 DeepAgent 를 못 받습니다. 메서드 표기는 양공변이라
createAgent 와 createDeepAgent 를 같은 자리에 넣을 수 있습니다. */
type Invokable = { invoke(input: { messages: { role: string; content: string }[] }): Promise<unknown> };
/** 에이전트를 한 번 굴려서 "모델에 실릴 뻔한 것"만 관찰합니다. */
async function observe(make: (spy: ReturnType<typeof createSpy>) => Invokable): Promise<Observed> {
const sink: Observed = { tools: [], systemPrompt: "" };
const agent = make(createSpy(sink));
await agent.invoke({ messages: [{ role: "user", content: "안녕" }] });
return sink;
}
/* ===== [1-1] 얕은 에이전트의 한계 =====
*
* createAgent 로 "리서치 보고서를 써 줘" 를 시켜 봅니다.
* 도구도, 계획 수단도, 메모장도 없습니다. 모델이 가진 것은
* "한 번의 응답으로 전부 쏟아내기" 뿐입니다.
*/
printSection("[1-1] 얕은 에이전트 — createAgent 로 리서치 보고서 시키기");
const RESEARCH_TASK =
"LangGraph, CrewAI, AutoGen 세 프레임워크를 비교하는 리서치 보고서를 써 주세요. " +
"각각의 아키텍처, 상태 관리 방식, 적합한 사용처를 다루고, 마지막에 선택 가이드를 붙여 주세요.";
const shallowAgent = createAgent({
model: MODEL,
tools: [], // 도구 없음
// systemPrompt 도 없음 — 기본값은 "비어 있음"입니다. 아래 [1-4] 에서 확인합니다.
});
if (RUN_LIVE) {
const shallow = await shallowAgent.invoke({
messages: [{ role: "user", content: RESEARCH_TASK }],
});
const msgs = shallow.messages as BaseMessage[];
console.log(`메시지 개수: ${msgs.length}`); // 보통 2 — 사람 1, AI 1
printMessages(msgs, 160);
console.log("\n관찰: 턴이 한 번에 끝났습니다. 계획도, 중간 산출물도, 검증도 없습니다.");
} else {
console.log("RUN_LIVE=1 을 붙이면 실제로 호출합니다. 여기서는 구조만 봅니다.");
console.log(`요청: ${RESEARCH_TASK.slice(0, 40)}…`);
}
/* ===== [1-2] Deep Agent 의 4대 기둥 =====
*
* 같은 요청을 createDeepAgent 에 던집니다. 도구를 하나도 안 줬는데도
* 계획(write_todos)을 세우고, 파일에 초안을 쓰고, 서브에이전트를 띄웁니다.
* 그 넷이 이 스텝에서 말하는 4대 기둥입니다.
*/
printSection("[1-2] Deep Agent — 같은 요청을 createDeepAgent 로");
const deepAgent = createDeepAgent({
model: MODEL,
// tools 를 안 줍니다. 그런데도 도구 8개가 생깁니다 → [1-3] 에서 확인.
});
if (RUN_LIVE) {
const deep = await deepAgent.invoke({
messages: [{ role: "user", content: RESEARCH_TASK }],
});
const msgs = deep.messages as BaseMessage[];
console.log(`메시지 개수: ${msgs.length}`); // 보통 수십 개
printMessages(msgs, 80);
console.log("\n── 에이전트가 세운 계획(todos) ──");
printTodos(deep.todos);
console.log("\n── 에이전트가 만든 파일(files) ──");
printFiles(deep.files);
console.log("\n관찰: 계획 → 파일 쓰기 → 서브에이전트 위임이 알아서 일어났습니다.");
} else {
console.log("RUN_LIVE=1 을 붙이면 실제로 호출합니다.");
}
/* ===== [1-3] Deep Agent = 하네스 — 마법이 아님을 증명 =====
*
* "아무 도구도 안 줬는데 왜 도구가 8개인가?" 를 직접 찍어 봅니다.
* 여기부터는 API 키 없이 돌아갑니다.
*/
printSection("[1-3] 하네스의 정체 — 도구를 안 줘도 8개가 실린다");
const deepObs = await observe((spy) => createDeepAgent({ model: MODEL, middleware: [spy] }));
console.log(`createDeepAgent 가 모델에 실은 도구: ${deepObs.tools.length}개`);
for (const name of [...deepObs.tools].sort()) console.log(` - ${name}`);
console.log(`\n시스템 프롬프트 길이: ${deepObs.systemPrompt.length.toLocaleString()}자`);
console.log("시스템 프롬프트 앞 120자:");
console.log(` ${JSON.stringify(deepObs.systemPrompt.slice(0, 120))}`);
/* ===== [1-4] createAgent vs createDeepAgent 나란히 =====
*
* 같은 스파이를 createAgent 에도 붙여서 둘을 나란히 놓습니다.
* 차이는 "무엇이 기본으로 실려 있는가" 하나뿐입니다.
*/
printSection("[1-4] createAgent vs createDeepAgent — 같은 스파이로 나란히");
const shallowObs = await observe((spy) =>
createAgent({ model: MODEL, tools: [], middleware: [spy] }),
);
console.table({
createAgent: {
도구수: shallowObs.tools.length,
시스템프롬프트길이: shallowObs.systemPrompt.length,
},
createDeepAgent: {
도구수: deepObs.tools.length,
시스템프롬프트길이: deepObs.systemPrompt.length,
},
});
console.log("createAgent 는 빈 손으로 시작합니다 — 도구 0개, 프롬프트 0자.");
console.log("createDeepAgent 는 도구 8개와 프롬프트 약 7천 자를 이미 얹고 시작합니다.");
/* 반환값도 확인합니다. 공식 문서는 `await createDeepAgent(...)` 로 쓰지만,
1.11.0 에서 이 함수는 실제로는 동기 함수입니다. 직접 찍어 봅니다. */
const ret = createDeepAgent({ model: MODEL });
console.log(`\ncreateDeepAgent 반환값이 Promise 인가? ${ret instanceof Promise}`);
console.log(`반환값 생성자 이름: ${ret.constructor.name}`);
console.log("→ await 를 붙여도 손해는 없습니다. 문서 표기를 따라 붙이는 걸 권합니다.");
/* ===== [1-5] 컨텍스트 엔지니어링 — 유한 자원 =====
*
* Deep Agent 의 모든 설계는 "컨텍스트 윈도우가 유한하다" 에서 나옵니다.
* 시작도 하기 전에 프롬프트만으로 얼마를 쓰는지 재 봅니다.
*/
printSection("[1-5] 컨텍스트는 유한 자원 — 시작 전에 이미 얼마를 쓰는가");
// 영어 기준 대략 4자 = 1토큰. 정확한 수치가 아니라 감을 잡기 위한 어림입니다.
const roughTokens = (s: string) => Math.round(s.length / 4);
const promptTokens = roughTokens(deepObs.systemPrompt);
const WINDOW = 200_000; // claude-sonnet-4-6 의 대략적인 컨텍스트 윈도우
console.log(`시스템 프롬프트: 약 ${promptTokens.toLocaleString()} 토큰 (어림)`);
console.log(`컨텍스트 윈도우: ${WINDOW.toLocaleString()} 토큰`);
console.log(`→ 사용자가 한 글자도 치기 전에 약 ${((promptTokens / WINDOW) * 100).toFixed(1)}% 를 씁니다.`);
console.log("\n이 비용을 내고 사는 것이 계획/파일/서브에이전트 능력입니다.");
console.log("Deep Agent 의 나머지 설계(오프로딩, 요약, 격리)는 전부");
console.log("'남은 컨텍스트를 어떻게 아낄 것인가' 에 대한 답입니다.");
/* ===== [1-6] 코스 로드맵 =====
*
* 여기서 본 4대 기둥을 앞으로 하나씩 분해합니다.
*
* Step 02 첫 Deep Agent — 옵션 전체 지도
* Step 03 계획 → write_todos (기둥 1)
* Step 04 파일시스템 → ls/read/write/edit (기둥 2)
* Step 05 백엔드와 권한 — 파일이 어디에 저장되는가
* Step 06 서브에이전트 → task (기둥 3)
* Step 07 시스템 프롬프트 설계 (기둥 4)
* Step 08 미들웨어 조합 — 하네스를 직접 만들기
* Step 09 HITL 과 권한 제어
* Step 10 장기 메모리와 스킬
* Step 11 스트리밍과 프로덕션
* Step 12 종합 — 딥 리서치 에이전트
*/
printSection("[1-6] 코스 로드맵");
console.log("Step 03 계획 / Step 04 파일시스템 / Step 06 서브에이전트 / Step 07 프롬프트");
console.log("→ 이 스텝에서 본 4대 기둥을 하나씩 분해합니다. 다음은 Step 02 입니다.");
/* ===== 참고: 커스텀 도구를 얹으면? =====
* 내장 8개에 내 도구가 "더해집니다"(대체가 아닙니다). Step 02 의 2-6 에서 다룹니다. */
const getWeather = tool(({ city }: { city: string }) => `${city}는 언제나 맑음!`, {
name: "get_weather",
description: "도시의 날씨를 알려 줍니다",
schema: z.object({ city: z.string() }),
});
const withCustom = await observe((spy) =>
createDeepAgent({ model: MODEL, tools: [getWeather], middleware: [spy] }),
);
console.log(`\n커스텀 도구 1개를 준 Deep Agent 의 도구 수: ${withCustom.tools.length}개 (8 + 1)`);
console.log(` → ${[...withCustom.tools].sort().join(", ")}`);
exercise.ts
본문 "연습문제" 8개를 그대로 옮겨 담은 빈칸 채우기용 파일입니다. 각 문제는 [문제 N] 주석 블록으로 구분되어 있고 그 아래가 비어 있으니, 거기에 직접 코드를 써 넣고 파일을 실행해 검증하면 됩니다.
- 파일 상단에
createSpy 와 observe 가 이미 준비되어 있습니다. 문제 2, 3, 8 은 이걸 그대로 쓰면 됩니다. 스파이를 처음부터 다시 만들 필요 없습니다.
[문제 5] [문제 6] [문제 7] 은 코드가 아니라 계산과 판단을 묻습니다. 5번만 코드로 풀고, 6번과 7번은 주석으로 답을 적으면 됩니다.
[문제 1] 과 [문제 8] 은 실제 API 호출이 필요합니다. 나머지는 키 없이 풀립니다.
- 파일을 그대로 실행하면 문제 번호만 출력되고 결과는 안 나옵니다. 정상입니다.
/**
* Step 01 — Deep Agent란 무엇인가 · 연습문제
* 실행: npx tsx docs/reference/deepagent/step-01-why-deep-agents/exercise.ts
*
* 아래 [문제 N] 블록 밑을 채우세요.
* 문제 1 과 8 만 실제 API 호출이 필요합니다 (RUN_LIVE=1 로 켜세요).
* 나머지는 스파이 미들웨어 덕분에 API 키 없이 풀립니다.
*/
import "dotenv/config";
import { createAgent, createMiddleware, tool } from "langchain";
import { createDeepAgent } from "deepagents";
import { AIMessage } from "@langchain/core/messages";
import * as z from "zod";
import { printSection } from "../project/src/lib/print.js";
const MODEL = "anthropic:claude-sonnet-4-6";
const RUN_LIVE = process.env["RUN_LIVE"] === "1";
/* ── 준비물: 본문 1-3 의 스파이 미들웨어 ──────────────────────
문제 2, 3, 8 에서 그대로 씁니다. 다시 만들 필요 없습니다. */
type Observed = { tools: string[]; systemPrompt: string };
function createSpy(sink: Observed) {
return createMiddleware({
name: "Spy",
wrapModelCall: async (request) => {
sink.tools = (request.tools ?? []).map((t) => (t as { name: string }).name);
sink.systemPrompt =
typeof request.systemPrompt === "string" ? request.systemPrompt : "";
return new AIMessage("(스파이가 가로챘습니다)");
},
});
}
type Invokable = { invoke(input: { messages: { role: string; content: string }[] }): Promise<unknown> };
async function observe(make: (spy: ReturnType<typeof createSpy>) => Invokable): Promise<Observed> {
const sink: Observed = { tools: [], systemPrompt: "" };
const agent = make(createSpy(sink));
await agent.invoke({ messages: [{ role: "user", content: "안녕" }] });
return sink;
}
/* ===== [문제 1] 얕은 에이전트 vs Deep Agent — 턴 수 비교 =====
*
* "피보나치 수열 10번째 항을 구해 줘" 를
* (A) createAgent 로 시키고 result.messages.length 를 찍으세요.
* (B) 같은 요청을 createDeepAgent 로 시키고 같은 값을 찍으세요.
* 두 숫자를 비교하고, 이 작업에 어느 쪽이 적합한지 이유를 주석으로 적으세요.
*
* RUN_LIVE=1 이 필요합니다.
*/
printSection("[문제 1] 턴 수 비교");
// 여기에 작성하세요
// → 어느 쪽이 적합한가요? 왜?
// (여기에 답을 주석으로)
/* ===== [문제 2] 커스텀 도구는 대체인가 추가인가 =====
*
* 위의 observe() 와 createSpy() 를 써서,
* createDeepAgent({ tools: [myTool] }) 처럼 커스텀 도구를 1개 줬을 때
* 모델에 실리는 도구가 몇 개인지 찍으세요.
* 커스텀 도구는 내장 도구를 대체하나요, 더해지나요?
*/
printSection("[문제 2] 커스텀 도구 1개를 주면 도구가 몇 개?");
const myTool = tool(({ city }: { city: string }) => `${city}는 맑음`, {
name: "get_weather",
description: "도시의 날씨를 알려 줍니다",
schema: z.object({ city: z.string() }),
});
// 여기에 작성하세요
// → 대체인가요, 추가인가요?
// (여기에 답을 주석으로)
/* ===== [문제 3] 시스템 프롬프트 해부 =====
*
* observe() 로 createDeepAgent 의 시스템 프롬프트를 얻어
* (A) 파일로 저장하세요 (fs.writeFileSync, 예: "./prompt.txt")
* (B) "write_todos", "task", "read_file" 이 각각 몇 번 등장하는지 세세요
* 어느 기둥에 가장 많은 지면을 할애했나요?
*
* 힌트: 개수 세기는 (s.match(/task/g) ?? []).length
*/
printSection("[문제 3] 프롬프트에서 어느 기둥이 가장 긴가");
// 여기에 작성하세요
// → 어느 기둥에 지면이 가장 많이 갔나요? 왜 그럴까요?
// (여기에 답을 주석으로)
/* ===== [문제 4] createDeepAgent 는 정말 비동기인가 =====
*
* createDeepAgent({ model: MODEL }) 의 반환값에 대해
* (A) instanceof Promise
* (B) constructor.name
* 을 찍어 본문 1-4 의 함정을 재현하세요.
* 이어서 await 를 붙인 결과와 안 붙인 결과가 같은 객체인지 확인하세요.
*/
printSection("[문제 4] createDeepAgent 의 반환값 정체");
// 여기에 작성하세요
/* ===== [문제 5] 컨텍스트 예산 계산 =====
*
* 컨텍스트 윈도우 200,000 토큰, 자동 요약은 170,000 에서 걸린다고 합시다.
* 시스템 프롬프트가 약 1,700 토큰이고 검색 결과 1건이 6,000 토큰이면,
* 요약이 걸리기 전에 검색을 몇 번 할 수 있나요? 계산하는 코드를 쓰세요.
*/
printSection("[문제 5] 요약 전에 검색을 몇 번 할 수 있나");
const SUMMARIZE_AT = 170_000;
const SYSTEM_TOKENS = 1_700;
const SEARCH_TOKENS = 6_000;
// 여기에 작성하세요
/* ===== [문제 6] 설계 판단 — 무엇을 쓸 것인가 =====
*
* 다음 세 작업에 createAgent 와 createDeepAgent 중 무엇을 쓸지 고르고
* 이유를 한 줄씩 주석으로 적으세요. (코드 없이 주석만)
*
* (a) 고객 문의를 "환불 / 배송 / 기타" 셋 중 하나로 분류
* (b) 저장소 전체를 읽고 아키텍처 문서를 작성
* (c) 사내 위키를 검색해 질문에 답하는 챗봇
*/
printSection("[문제 6] 설계 판단");
// (a) →
// (b) →
// (c) →
/* ===== [문제 7] 실패 증상과 기둥 짝짓기 =====
*
* 아래 실패 증상 4개를 4대 기둥 중 하나씩에 짝지으세요. (주석으로)
* 기둥 1 = 계획(write_todos)
* 기둥 2 = 파일시스템(오프로딩)
* 기둥 3 = 서브에이전트(격리)
* 기둥 4 = 상세 시스템 프롬프트
*
* (a) 검색 결과를 15개 쌓았더니 컨텍스트가 넘쳤다
* (b) 30턴째에 처음 세운 계획을 잊고 엉뚱한 걸 하고 있다
* (c) 도구는 다 붙여 줬는데 모델이 한 번도 안 부른다
* (d) 초안을 뱉었는데 고쳐 달라니까 처음부터 다시 쓴다
*/
printSection("[문제 7] 증상 → 기둥 짝짓기");
// (a) →
// (b) →
// (c) →
// (d) →
/* ===== [문제 8] (심화) 스파이를 통과시키기 =====
*
* createSpy 를 고쳐서 handler(request) 를 "실제로 호출하되"
* 매 호출마다 도구 개수와 프롬프트 길이를 찍는 미들웨어를 만드세요.
* 그리고 RUN_LIVE=1 로 Deep Agent 를 돌려
* - 모델이 몇 번 호출되는지
* - 매번 시스템 프롬프트 길이가 변하는지
* 관찰하세요.
*
* 힌트: wrapModelCall 의 두 번째 인자가 handler 입니다.
* return await handler(request); 로 통과시키면 됩니다.
* 대화가 길어질 때 커지는 건 systemPrompt 가 아니라 messages 입니다.
*/
printSection("[문제 8] 통과시키는 스파이로 호출 횟수 세기");
// 여기에 작성하세요
// → 시스템 프롬프트 길이는 매번 변하나요? 그럼 뭐가 커지나요?
// (여기에 답을 주석으로)
console.log("\n(문제를 다 풀었으면 solution.ts 로 채점하세요)");
solution.ts
8문제의 정답 코드와 해설 주석을 담은 파일입니다. exercise.ts 를 스스로 풀어본 뒤에 열어보세요.
[정답 2] 의 답은 9개입니다. 커스텀 도구는 내장 8개를 대체하지 않고 더해집니다. 이건 Deep Agent 의 기본 도구를 끌 수 없다는 뜻이기도 합니다 — 끄려면 createAgent 로 내려가 미들웨어를 직접 고르는 수밖에 없습니다(Step 08).
[정답 3] 이 이 파일에서 가장 재미있는 대목입니다. 6,979자 프롬프트에서 task 관련 서술이 압도적으로 깁니다. 서브에이전트 위임은 모델이 가장 안 하려는 행동이라 가장 많이 설득해야 하기 때문입니다. 반대로 write_todos 는 짧습니다 — 모델이 이미 계획 세우기를 좋아합니다.
[정답 5] 의 답은 28회입니다. (170,000 − 1,700) ÷ 6,000 = 28.05. 이 숫자가 작다는 게 요점입니다. 서브에이전트로 격리하면 부모 컨텍스트에는 요약만 남으므로 훨씬 많이 할 수 있습니다.
[정답 6] 의 답은 (a) createAgent, (b) createDeepAgent, (c) createAgent 입니다. (c) 가 헷갈릴 수 있는데, 위키 검색 챗봇은 "검색 → 답변" 2턴이면 끝나므로 얕은 쪽이 맞습니다. 만약 "위키를 다 뒤져서 종합 리포트를 써라" 였다면 Deep 입니다.
[정답 7] 의 짝은 (a) 기둥 3(서브에이전트/격리), (b) 기둥 1(계획), (c) 기둥 4(시스템 프롬프트), (d) 기둥 2(파일시스템) 입니다. (d) 를 기둥 4로 착각하기 쉬운데, "고칠 수 있으려면 고칠 대상이 파일로 있어야 한다"가 핵심입니다.
/**
* Step 01 — Deep Agent란 무엇인가 · 정답과 해설
* 실행: npx tsx docs/reference/deepagent/step-01-why-deep-agents/solution.ts
*
* exercise.ts 를 스스로 풀어본 뒤에 열어보세요.
* 문제 1 과 8 만 RUN_LIVE=1 이 필요합니다.
*/
import "dotenv/config";
import { createAgent, createMiddleware, tool } from "langchain";
import { createDeepAgent } from "deepagents";
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
import * as z from "zod";
import { writeFileSync } from "node:fs";
import { printSection } from "../project/src/lib/print.js";
const MODEL = "anthropic:claude-sonnet-4-6";
const RUN_LIVE = process.env["RUN_LIVE"] === "1";
type Observed = { tools: string[]; systemPrompt: string };
function createSpy(sink: Observed) {
return createMiddleware({
name: "Spy",
wrapModelCall: async (request) => {
sink.tools = (request.tools ?? []).map((t) => (t as { name: string }).name);
sink.systemPrompt =
typeof request.systemPrompt === "string" ? request.systemPrompt : "";
return new AIMessage("(스파이가 가로챘습니다)");
},
});
}
type Invokable = { invoke(input: { messages: { role: string; content: string }[] }): Promise<unknown> };
async function observe(make: (spy: ReturnType<typeof createSpy>) => Invokable): Promise<Observed> {
const sink: Observed = { tools: [], systemPrompt: "" };
const agent = make(createSpy(sink));
await agent.invoke({ messages: [{ role: "user", content: "안녕" }] });
return sink;
}
/* ===== [정답 1] 얕은 에이전트 vs Deep Agent — 턴 수 비교 =====
*
* 기대 결과 (모델 응답이므로 매번 다릅니다):
* createAgent → messages.length = 2
* createDeepAgent → messages.length = 2 ~ 8
*
* 답: 이 작업에는 createAgent 가 적합합니다.
*
* 해설: 피보나치 10번째 항은 모델이 암산으로 즉시 답합니다(55).
* 계획도, 파일도, 서브에이전트도 필요 없습니다. 그런데 createDeepAgent 로
* 시키면 프롬프트 약 1,700 토큰을 헛되이 태우고, 운이 나쁘면 모델이
* "단계별로 해 보자" 며 write_todos 까지 부릅니다 — 55 하나 얻으려고요.
*
* 이게 본문 1-4 의 "애매하면 createAgent 부터" 팁의 실물입니다.
* Deep 은 공짜가 아니라 고정비를 내고 사는 능력입니다.
*/
printSection("[정답 1] 턴 수 비교");
const FIB_TASK = "피보나치 수열 10번째 항을 구해 줘";
if (RUN_LIVE) {
const shallow = createAgent({ model: MODEL, tools: [] });
const a = await shallow.invoke({ messages: [{ role: "user", content: FIB_TASK }] });
console.log(`(A) createAgent messages.length = ${(a.messages as BaseMessage[]).length}`);
const deep = createDeepAgent({ model: MODEL });
const b = await deep.invoke({ messages: [{ role: "user", content: FIB_TASK }] });
console.log(`(B) createDeepAgent messages.length = ${(b.messages as BaseMessage[]).length}`);
console.log("→ 간단한 계산에 Deep 을 쓰면 고정비만 더 냅니다. createAgent 가 정답.");
} else {
console.log("RUN_LIVE=1 로 실행하면 실제 숫자를 볼 수 있습니다.");
console.log("기대: createAgent = 2, createDeepAgent = 2~8 → 이 작업엔 createAgent 가 적합");
}
/* ===== [정답 2] 커스텀 도구는 대체인가 추가인가 =====
*
* 답: 9개 — 추가입니다. 대체가 아닙니다.
*
* 해설: 내장 8개(ls, read_file, write_file, edit_file, glob, grep, task,
* write_todos)에 내 도구가 "더해져" 9개가 됩니다.
*
* 이 사실의 함의가 중요합니다. createDeepAgent 를 쓰는 한 내장 도구를
* 뺄 수 없습니다. "파일시스템은 필요 없고 계획만 쓰고 싶다" 같은 요구가
* 있어도 옵션으로는 안 됩니다. 그러려면 createAgent 로 내려가서
* 미들웨어를 직접 골라 붙여야 합니다 (Step 08).
*
* 즉 createDeepAgent 는 "전부 아니면 전무" 에 가까운 묶음 상품입니다.
*/
printSection("[정답 2] 커스텀 도구 1개를 주면 도구가 몇 개?");
const myTool = tool(({ city }: { city: string }) => `${city}는 맑음`, {
name: "get_weather",
description: "도시의 날씨를 알려 줍니다",
schema: z.object({ city: z.string() }),
});
const withCustom = await observe((spy) =>
createDeepAgent({ model: MODEL, tools: [myTool], middleware: [spy] }),
);
console.log(`도구 개수: ${withCustom.tools.length}개`);
console.log(` ${[...withCustom.tools].sort().join(", ")}`);
console.log("→ 8 + 1 = 9. 대체가 아니라 추가입니다.");
/* ===== [정답 3] 시스템 프롬프트 해부 =====
*
* 답: `task`(서브에이전트)에 지면이 압도적으로 많이 갑니다.
*
* 실제 측정값 (deepagents@1.11.0, 결정적):
* 등장 횟수: task 26회 / write_todos 3회 / read_file 2회
* 섹션 길이: task 약 2,140자 / write_todos 약 1,239자 / filesystem 약 483자
*
* 왜? 서브에이전트 위임은 모델이 "가장 안 하려는" 행동이기 때문입니다.
* 모델의 기본 성향은 "내가 직접 다 하기" 입니다. 남에게 맡기고 결과만
* 받는 건 부자연스러운 행동이라, 언제 위임해야 하는지 / 언제 하면 안 되는지 /
* 왜 그게 이득인지를 예시까지 들어 가며 길게 설득해야 합니다.
*
* 반대로 write_todos 는 짧습니다 — 모델은 이미 계획 세우기를 좋아합니다.
* 오히려 프롬프트가 "간단한 일엔 쓰지 마라" 고 말리는 쪽입니다.
*
* ⚠️ 함정: "ls" 를 단순 substring 으로 세면 23회가 나옵니다. tooLS, calLS
* 같은 단어에 걸리기 때문입니다. 짧은 도구 이름을 셀 때는 단어 경계(\b)나
* 백틱(`ls`)까지 포함해서 세야 합니다. 이 문제의 정답 코드가 왜 긴 이름만
* 골랐는지가 여기 있습니다.
*/
printSection("[정답 3] 프롬프트에서 어느 기둥이 가장 긴가");
const deepObs = await observe((spy) => createDeepAgent({ model: MODEL, middleware: [spy] }));
const sys = deepObs.systemPrompt;
// (A) 파일로 저장
writeFileSync("./prompt.txt", sys, "utf8");
console.log(`시스템 프롬프트 ${sys.length.toLocaleString()}자를 ./prompt.txt 에 저장했습니다.`);
// (B) 등장 횟수 세기
const countOf = (needle: string) => (sys.match(new RegExp(needle, "g")) ?? []).length;
for (const w of ["write_todos", "task", "read_file"]) {
console.log(` ${w.padEnd(12)} ${countOf(w)}회`);
}
// 섹션 길이로도 확인 — 이게 "지면" 을 재는 더 정확한 방법입니다.
const iTodo = sys.indexOf("## `write_todos`");
const iFs = sys.indexOf("## Filesystem Tools");
const iTask = sys.indexOf("## `task`");
console.log("\n섹션 길이:");
console.log(` write_todos 약 ${iFs - iTodo}자`);
console.log(` filesystem 약 ${iTask - iFs}자`);
console.log(` task 약 ${sys.length - iTask}자 ← 압도적`);
console.log("\n→ 위임은 모델이 가장 안 하려는 행동이라 가장 길게 설득해야 합니다.");
/* ===== [정답 4] createDeepAgent 는 정말 비동기인가 =====
*
* 답: 비동기가 아닙니다. instanceof Promise 는 false, 생성자는 ReactAgent.
*
* 해설: deepagents@1.11.0 의 타입 선언은
* declare function createDeepAgent<...>(params?): DeepAgent<...>
* 로, Promise<DeepAgent> 가 아닙니다. memory / skills / subagents 를 줘도
* 마찬가지입니다.
*
* 그런데 공식 문서는 전부 `await createDeepAgent(...)` 로 씁니다.
* 모순처럼 보이지만 문제가 안 되는 이유는, JS 의 await 가 Promise 가 아닌
* 값에 붙으면 그냥 그 값을 돌려주기 때문입니다. 그래서 아래 두 줄은 같습니다.
*
* 결론: await 를 붙이세요. 손해가 없고, 향후 버전이 비동기로 바뀌어도 안 깨집니다.
*
* 진짜 조심할 건 agent.invoke() 앞의 await 입니다. 이건 진짜 Promise 라
* 빠뜨리면 result.messages 가 undefined 라며 엉뚱한 데서 터집니다.
*/
printSection("[정답 4] createDeepAgent 의 반환값 정체");
const ret = createDeepAgent({ model: MODEL });
console.log(`(A) instanceof Promise : ${ret instanceof Promise}`);
console.log(`(B) constructor.name : ${ret.constructor.name}`);
const awaited = await createDeepAgent({ model: MODEL });
console.log(`await 를 붙인 것도 같은 타입인가? ${awaited.constructor.name === ret.constructor.name}`);
console.log("→ await 는 붙여도 그만, 안 붙여도 그만. 문서를 따라 붙이는 걸 권합니다.");
/* ===== [정답 5] 컨텍스트 예산 계산 =====
*
* 답: 28회.
* (170,000 − 1,700) ÷ 6,000 = 28.05 → 28회
*
* 해설: 이 숫자가 "작다" 는 게 요점입니다. 리서치 한 번에 검색 28번이면
* 넉넉해 보이지만, 실제로는 파일 읽기와 모델의 응답도 같은 예산을 씁니다.
* 게다가 28회를 넘기는 순간 자동 요약이 걸려서 앞쪽 대화가 손실 압축됩니다.
*
* 서브에이전트로 격리하면? 검색 20번을 서브에이전트가 자기 컨텍스트에서
* 하고 부모에겐 요약 500 토큰만 돌려줍니다. 부모 기준으로는 검색 20번이
* 500 토큰이 되므로, 같은 예산으로 수백 번도 가능해집니다.
*
* 이게 본문 1-5 "컨텍스트를 예산처럼 다루세요" 팁의 계산 근거입니다.
*/
printSection("[정답 5] 요약 전에 검색을 몇 번 할 수 있나");
const SUMMARIZE_AT = 170_000;
const SYSTEM_TOKENS = 1_700;
const SEARCH_TOKENS = 6_000;
const budget = SUMMARIZE_AT - SYSTEM_TOKENS;
const maxSearches = Math.floor(budget / SEARCH_TOKENS);
console.log(`작업 예산: ${SUMMARIZE_AT.toLocaleString()} − ${SYSTEM_TOKENS.toLocaleString()} = ${budget.toLocaleString()} 토큰`);
console.log(`검색 1건 = ${SEARCH_TOKENS.toLocaleString()} 토큰`);
console.log(`→ 최대 ${maxSearches}회. 그 다음엔 자동 요약이 걸립니다.`);
console.log("\n서브에이전트로 격리하면 부모엔 요약만 남으므로 훨씬 많이 할 수 있습니다.");
/* ===== [정답 6] 설계 판단 =====
*
* (a) 고객 문의 분류 → createAgent
* 한 턴이면 끝나는 분류 작업. 계획도 파일도 필요 없습니다.
* responseFormat 으로 enum 을 주면 그걸로 완성입니다.
*
* (b) 저장소 읽고 아키텍처 문서 작성 → createDeepAgent
* 파일이 몇 개인지 미리 모르고(턴 수 예측 불가), 읽은 내용이
* 컨텍스트를 다 먹으며(오프로딩 필요), 초안을 고쳐야 합니다(파일 필요).
* 4대 기둥이 전부 필요한 교과서적 사례입니다.
*
* (c) 사내 위키 검색 챗봇 → createAgent
* ⚠️ 여기서 많이 틀립니다. "검색" 이 들어가니 Deep 일 것 같지만,
* 이 작업은 "검색 1번 → 답변" 2턴이면 끝납니다. 챗봇이라 지연 시간도
* 중요한데 Deep 을 쓰면 매 질문마다 1,700 토큰을 더 태우고 느려집니다.
*
* 만약 요구가 "위키를 전부 뒤져서 종합 리포트를 써라" 였다면 Deep 입니다.
* 구분 기준은 "검색이 있느냐" 가 아니라 "턴 수를 예측할 수 있느냐" 입니다.
*/
printSection("[정답 6] 설계 판단");
console.log("(a) 문의 분류 → createAgent (한 턴, 예측 가능)");
console.log("(b) 저장소 → 아키텍처 문서 → createDeepAgent (턴 예측 불가, 오프로딩·초안 필요)");
console.log("(c) 위키 검색 챗봇 → createAgent (검색 1번 + 답변 = 2턴, 지연 중요)");
console.log("\n기준은 '검색이 있느냐' 가 아니라 '턴 수를 예측할 수 있느냐' 입니다.");
/* ===== [정답 7] 실패 증상과 기둥 짝짓기 =====
*
* (a) 검색 결과 15개로 컨텍스트가 넘쳤다
* → 기둥 3 (서브에이전트 / 격리)
* 검색을 서브에이전트에 맡기면 부모엔 요약만 옵니다.
* (기둥 2 오프로딩도 부분적으로 답이 됩니다 — 결과를 파일에 쓰면
* 대화엔 참조만 남으니까요. 하지만 "탐색 과정 자체" 를 없애는 건
* 격리 쪽입니다.)
*
* (b) 30턴째에 처음 계획을 잊었다
* → 기둥 1 (계획 / write_todos)
* 계획이 대화 텍스트면 앞쪽으로 밀려 잊힙니다. todos 는 별도 상태라
* 안 밀려납니다.
*
* (c) 도구는 붙였는데 모델이 안 부른다
* → 기둥 4 (상세 시스템 프롬프트)
* 도구를 쥐여 주는 것과 쓰게 만드는 건 다른 문제입니다.
* 정답 3 에서 본 대로, 위임 하나 시키려고 2,140자를 씁니다.
*
* (d) 초안을 고쳐 달라니까 처음부터 다시 쓴다
* → 기둥 2 (파일시스템)
* ⚠️ 기둥 4(프롬프트)로 착각하기 쉽습니다. "고쳐 써" 라고 잘 시키면
* 될 것 같지만 아닙니다. 대화에 뱉은 글은 고칠 대상이 없습니다.
* 초안이 /report.md 라는 파일로 있어야 edit_file 로 그 부분만 고칩니다.
* "고칠 수 있으려면 고칠 대상이 파일로 존재해야 한다" 가 핵심입니다.
*/
printSection("[정답 7] 증상 → 기둥 짝짓기");
console.log("(a) 컨텍스트 넘침 → 기둥 3 (서브에이전트 / 격리)");
console.log("(b) 계획을 잊음 → 기둥 1 (계획 / write_todos)");
console.log("(c) 도구를 안 부름 → 기둥 4 (상세 시스템 프롬프트)");
console.log("(d) 처음부터 다시 씀 → 기둥 2 (파일시스템) ← 기둥 4로 착각하기 쉬움");
/* ===== [정답 8] (심화) 통과시키는 스파이 =====
*
* 핵심은 wrapModelCall 의 두 번째 인자 handler 입니다.
* handler(request) 를 부르면 진짜 모델이 호출되고, 그 결과를 그대로
* 돌려주면 에이전트는 아무 일 없었다는 듯 계속 굴러갑니다.
*
* 답: 시스템 프롬프트 길이는 매 호출마다 "거의 변하지 않습니다"(6,979자 고정).
* 커지는 건 messages 입니다. 대화가 쌓일수록 messages 배열이 길어지고,
* 그게 컨텍스트를 먹습니다.
*
* 이게 본문 1-5 의 "고정 비용은 반올림 오차, 변동 비용이 전부" 를
* 눈으로 확인하는 실험입니다. 시스템 프롬프트 7천 자를 아끼려고 애쓰는 건
* 의미가 없고, 검색 결과 하나를 파일로 밀어내는 게 훨씬 큽니다.
*/
printSection("[정답 8] 통과시키는 스파이로 호출 횟수 세기");
let callCount = 0;
const passThroughSpy = createMiddleware({
name: "PassThroughSpy",
wrapModelCall: async (request, handler) => {
callCount += 1;
const sysLen = typeof request.systemPrompt === "string" ? request.systemPrompt.length : 0;
const msgCount = request.messages?.length ?? 0;
console.log(
` 호출 #${callCount}: 도구 ${request.tools?.length ?? 0}개 / ` +
`시스템 프롬프트 ${sysLen.toLocaleString()}자 / messages ${msgCount}개`,
);
return await handler(request); // ← 진짜 모델을 부른다
},
});
if (RUN_LIVE) {
const agent = createDeepAgent({ model: MODEL, middleware: [passThroughSpy] });
await agent.invoke({
messages: [{ role: "user", content: "/hello.md 파일에 인사말을 써 줘" }],
});
console.log(`\n총 모델 호출 횟수: ${callCount}회`);
console.log("→ 시스템 프롬프트 길이는 고정. 커지는 건 messages 입니다.");
} else {
console.log("RUN_LIVE=1 로 실행하면 호출마다 한 줄씩 찍힙니다.");
console.log("기대: 시스템 프롬프트는 6,979자로 고정, messages 개수만 늘어남");
}
console.log("\n(정답 확인 끝. Step 02 로 넘어가세요)");