루프 제어
루프 제어 (Loop Control)
에이전트 루프의 실행 흐름과 각 단계의 설정을 제어하는 방법을 설명하는 문서예요.
출처: 문서
본문
에이전트 루프의 실행 흐름과 각 단계의 설정을 모두 제어할 수 있어요. 루프는 다음 중 하나가 될 때까지 계속돼요:
- 툴 호출(tool-calls)이 아닌 종료 reasoning이 반환되거나,
- 호출된 툴에
execute함수가 없거나, - 툴 호출이 승인을 필요로 하거나,
- 중지 조건(stop condition)이 충족되거나
AI SDK는 두 매개변수를 통해 내장 루프 제어를 제공해요: 중지 조건을 정의하는 stopWhen과 단계 사이에 설정(모델, 툴, 메시지 등)을 수정하는 prepareStep.
중지 조건 (Stop Conditions)
stopWhen 매개변수는 마지막 단계에 툴 결과가 있을 때 실행을 언제 중지할지 제어해요. 기본적으로 ToolLoopAgent는 isStepCount(20)을 사용해 20단계 후에 중지해요. 이 기본값은 과도한 API 호출과 비용을 초래할 수 있는 무한 루프를 방지하기 위한 안전장치예요.
WorkflowAgent는 기본 단계 제한을 적용하지 않아요. 모델이 툴 호출을 멈추거나 다른 자연스러운 종료 조건이 충족될 때까지 계속돼요. 모델 호출을 제한해야 할 때는 isStepCount(20) 같은 명시적 조건을 구성하세요. 자세한 내용은 WorkflowAgent 루프 제어를 참고하세요.
stopWhen을 제공하면 에이전트는 중지 조건이 충족될 때까지 툴 호출 후 실행을 계속해요. 조건이 배열이면 그중 어떤 조건이라도 충족되면 실행을 중지해요.
내장 조건 사용하기 (Use Built-in Conditions)
AI SDK는 몇 가지 내장 중지 조건을 제공해요:
isStepCount(count)— 지정된 단계 수 후에 중지해요hasToolCall(...toolNames)— 지정된 툴 중 하나가 호출되면 중지해요isLoopFinished()— 절대 트리거하지 않고, 에이전트가 자연스럽게 끝날 때까지 루프를 실행해요
최대 단계까지 실행 (Run Up to a Maximum Number of Steps)
import { ToolLoopAgent, isStepCount } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
stopWhen: isStepCount(50), // Increase ToolLoopAgent's default from 20 to 50.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});
끝날 때까지 실행 (Run Until Finished)
ToolLoopAgent가 모델이 자연스럽게 툴 호출을 멈출 때까지 실행되게 하려면 isLoopFinished()를 사용하세요. 이러면 기본 단계 제한이 제거돼요:
import { ToolLoopAgent, isLoopFinished } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
stopWhen: isLoopFinished(), // No maximum step limit.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});
참고:
isLoopFinished()는 주의해서 사용하세요. 단계 제한이 없으면 모델이 계속 툴 호출을 한다면 에이전트가 잠재적으로 무한히 실행되거나 상당한 비용이 발생할 수 있어요.
여러 조건 결합 (Combine Multiple Conditions)
여러 중지 조건을 결합해요. 루프는 어떤 조건이라도 충족되면 중지돼요:
import { ToolLoopAgent, isStepCount, hasToolCall } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
stopWhen: [
isStepCount(20), // Maximum 20 steps
hasToolCall('someTool', 'done'), // Stop after calling either tool
],
});
const result = await agent.generate({
prompt: 'Research and analyze the topic',
});
커스텀 조건 만들기 (Create Custom Conditions)
특정 요구사항을 위한 커스텀 중지 조건을 만드세요:
import { ToolLoopAgent, StopCondition, ToolSet } from 'ai';
__PROVIDER_IMPORT__;
const tools = {
// your tools
} satisfies ToolSet;
const hasAnswer: StopCondition<typeof tools> = ({ steps }) => {
// Stop when the model generates text containing "ANSWER:"
return steps.some(step => step.text?.includes('ANSWER:')) ?? false;
};
const agent = new ToolLoopAgent({
model: __MODEL__,
tools,
stopWhen: hasAnswer,
});
const result = await agent.generate({
prompt: 'Find the answer and respond with "ANSWER: [your answer]"',
});
커스텀 조건은 모든 단계에 걸친 단계 정보를 받아요:
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const totalUsage = steps.reduce(
(acc, step) => ({
inputTokens: acc.inputTokens + (step.usage?.inputTokens ?? 0),
outputTokens: acc.outputTokens + (step.usage?.outputTokens ?? 0),
}),
{ inputTokens: 0, outputTokens: 0 },
);
const costEstimate =
(totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000;
return costEstimate > 0.5; // Stop if cost exceeds $0.50
};
단계 준비 (Prepare Step)
prepareStep 콜백은 루프의 각 단계 전에 실행되며, 변경 사항을 반환하지 않으면 초기 설정이 기본값으로 사용돼요. 이를 사용해 설정을 수정하고, 컨텍스트를 관리하며, 실행 기록을 기반으로 동적 동작을 구현할 수 있어요.
그것은 현재 단계의 messages를 받고, 원본 입력과 이전 단계에서 누적된 어시스턴트/툴 메시지를 구분해야 할 때는 initialMessages와 responseMessages를 받아요. messages를 루프의 현재 메시지 상태로 취급하세요: messages 오버라이드를 반환하면 그 오버라이드가 완료된 각 단계의 응답 메시지와 함께 이후 단계의 기준(base)으로 유지돼요.
동적 모델 선택 (Dynamic Model Selection)
단계 요구사항에 따라 모델을 전환해요:
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: 'openai/gpt-6-luna', // Default model
tools: {
// your tools
},
prepareStep: async ({ stepNumber, messages }) => {
// Use a stronger model for complex reasoning after initial steps
if (stepNumber > 2 && messages.length > 10) {
return {
model: __MODEL__,
};
}
// Continue with default settings
return {};
},
});
const result = await agent.generate({
prompt: '...',
});
모델 호출 설정 (Model Call Settings)
개별 단계에 대해 프로바이더에 구애받지 않는 모델 호출 설정을 오버라이드해요. 툴 호출 단계가 최종 응답보다 더 결정적인 샘플링을 필요로 할 때 유용할 수 있어요:
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
temperature: 0.7,
tools: {
// your tools
},
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
temperature: 0,
maxOutputTokens: 300,
};
}
return {};
},
});
const result = await agent.generate({
prompt: '...',
});
prepareStep은 maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, reasoning을 오버라이드할 수 있어요. 이 오버라이드는 현재 단계에만 적용돼요. 설정이 생략되거나 undefined면 그 단계에는 최상위 값이 사용돼요. temperature: 0, seed: 0, 빈 stopSequences 배열 같은 정의된 falsy 값은 보존돼요.
컨텍스트 관리 (Context Management)
오래 실행되는 에이전트는 큰 툴 결과, reasoning 부분, 어시스턴트 메시지를 누적할 수 있어요. prepareStep을 사용해 이후 단계에서 사용될 메시지 상태를 변경하세요. 이는 컴팩션(압축)에 유용하며, 언제 컴팩션이 일어나야 하는지 결정하는 것은 여러분이 해요.
messages 값은 현재 단계로 보내질 메시지를 담고 있어요. prepareStep에서 messages 오버라이드를 반환하면 그 변경된 목록이 이후 단계의 기준이 돼요. 루프가 계속될수록 새 어시스턴트와 툴 응답 메시지가 거기에 추가돼요. 유지된 메시지 상태 대신 분리된 조각으로 단계를 다시 구성해야 한다면, 원본 입력에는 initialMessages, 지금까지 누적된 모델/툴 응답 메시지에는 responseMessages를 사용하세요.
pruneMessages 헬퍼는 선택된 메시지를 제거하는 내장 방법을 제공해요. 간단한 컴팩션 전략을 원할 때 prepareStep 안에서 사용할 수 있어요.
import { ToolLoopAgent, pruneMessages, type ModelMessage } from 'ai';
__PROVIDER_IMPORT__;
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => {
return JSON.stringify(messages).length / 4;
};
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
prepareStep: async ({ messages }) => {
if (estimateTokens(messages) > COMPACTION_THRESHOLD) {
return {
messages: pruneMessages({
messages,
reasoning: 'all',
toolCalls: 'before-last-3-messages',
emptyMessages: 'remove',
}),
};
}
},
});
const result = await agent.generate({
prompt: '...',
});
위 토큰 추정기는 의도적으로 단순하며, 언제 컴팩션할지 결정하는 한 가지 방법을 보여줄 뿐이에요. 핵심은 prepareStep이 새 messages 배열을 반환할 수 있고, 그 배열이 이후 단계의 메시지 상태가 된다는 점이에요. 같은 패턴은 generateText와 streamText에서도 동작해요.
툴 선택 (Tool Selection)
각 단계에서 사용할 수 있는 툴을 제어해요:
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
prepareStep: async ({ stepNumber, steps }) => {
// Search phase (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ['search'],
toolChoice: 'required',
};
}
// Analysis phase (steps 3-5)
if (stepNumber <= 5) {
return {
activeTools: ['analyze'],
};
}
// Summary phase (step 6+)
return {
activeTools: ['summarize'],
toolChoice: 'required',
};
},
});
const result = await agent.generate({
prompt: '...',
});
특정 툴을 강제로 사용하게 할 수도 있어요:
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
// Force the search tool to be used first
return {
toolChoice: { type: 'tool', toolName: 'search' },
};
}
if (stepNumber === 5) {
// Force the summarize tool after analysis
return {
toolChoice: { type: 'tool', toolName: 'summarize' },
};
}
return {};
};
메시지 수정 (Message Modification)
모델로 보내기 전에 메시지를 변환해요. 반환된 메시지는 이후 단계로 전달되므로, 이후의 messages 값에는 변환된 메시지와 완료된 단계의 어시스턴트/툴 응답 메시지가 포함돼요:
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
prepareStep: async ({ messages, stepNumber }) => {
// Summarize tool results to reduce token usage
const processedMessages = messages.map(msg => {
if (msg.role === 'tool' && msg.content.length > 1000) {
return {
...msg,
content: summarizeToolResult(msg.content),
};
}
return msg;
});
return { messages: processedMessages };
},
});
const result = await agent.generate({
prompt: '...',
});
실험적 샌드박스 선택 (Experimental Sandbox Selection)
단일 단계에서 툴 실행에 사용되는 실험적 샌드박스를 전환해요:
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
runCommand,
},
experimental_sandbox: defaultSandbox,
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
experimental_sandbox: setupSandbox,
};
}
return {};
},
});
const result = await agent.generate({
prompt: '...',
});
runtimeContext와 toolsContext와 달리, prepareStep에서 반환된 실험적 샌드박스는 해당 단계의 툴 실행에만 적용돼요. 이후 단계는 자체 실험적 샌드박스 오버라이드를 반환하지 않는 한 최상위 experimental_sandbox를 사용해요.
단계 정보 접근 (Access Step Information)
stopWhen과 prepareStep 모두 현재 실행에 대한 상세 정보를 받아요:
prepareStep: async ({
model, // Current model configuration
stepNumber, // Current step number (0-indexed)
steps, // All previous steps with their results
messages, // Messages to be sent to the model
}) => {
// Access previous tool calls and results
const previousToolCalls = steps.flatMap(step => step.toolCalls);
const previousResults = steps.flatMap(step => step.toolResults);
// Make decisions based on execution history
if (previousToolCalls.some(call => call.toolName === 'dataAnalysis')) {
return {
toolChoice: { type: 'tool', toolName: 'reportGenerator' },
};
}
return {};
},
강제 툴 호출 (Forced Tool Calling)
toolChoice: 'required'를 execute 함수가 없는 done 툴과 결합해 에이전트가 항상 툴을 사용하게 강제할 수 있어요. 이 패턴은 에이전트가 매 단계 툴을 사용하고, 명시적으로 완료를 알릴 때만 중지하게 보장해요.
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
search: searchTool,
analyze: analyzeTool,
done: tool({
description: 'Signal that you have finished your work',
inputSchema: z.object({
answer: z.string().describe('The final answer'),
}),
// No execute function - stops the agent when called
}),
},
toolChoice: 'required', // Force tool calls at every step
});
const result = await agent.generate({
prompt: 'Research and analyze this topic, then provide your answer.',
});
// extract answer from done tool call
const toolCall = result.staticToolCalls[0]; // tool call from final step
if (toolCall?.toolName === 'done') {
console.log(toolCall.input.answer);
}
이 패턴의 핵심 측면:
toolChoice: 'required': 텍스트를 직접 생성하는 대신 매 단계 모델이 툴을 호출하도록 강제해요. 이렇게 하면 에이전트가 구조화된 워크플로를 따르게 보장돼요.execute가 없는done툴:execute함수가 없는 툴은 종료 신호로 동작해요. 에이전트가 이 툴을 호출하면 실행할 함수가 없으므로 루프가 중지돼요.- 결과 접근: 최종 답변은 실행되지 않은 툴 호출을 담고 있는
result.staticToolCalls에서 확인할 수 있어요.
이 패턴은 에이전트가 직접 답하려고 시도하기보다 특정 툴을 항상 작업(예: 코드 실행이나 데이터 검색)에 사용하게 하고 싶을 때 유용해요.
수동 루프 제어 (Manual Loop Control)
에이전트 루프를 완전히 제어해야 하는 시나리오에서는 stopWhen과 prepareStep 대신 AI SDK Core 함수(generateText, streamText)를 사용해 자체 루프 관리를 구현할 수 있어요. 이 접근 방식은 복잡한 워크플로에 최대한의 유연성을 제공해요.
수동 루프 구현 (Implementing a Manual Loop)
실행을 완전히 제어해야 할 때 자체 에이전트 루프를 구축하세요:
import { generateText, ModelMessage } from 'ai';
__PROVIDER_IMPORT__;
const messages: ModelMessage[] = [{ role: 'user', content: '...' }];
let step = 0;
const maxSteps = 10;
while (step < maxSteps) {
const result = await generateText({
model: __MODEL__,
messages,
tools: {
// your tools here
},
});
messages.push(...result.responseMessages);
if (result.text) {
break; // Stop when model generates text
}
step++;
}
이 수동 접근 방식은 다음을 완전히 제어할 수 있게 해 줘요:
- 메시지 기록 관리
- 단계별 의사 결정
- 커스텀 중지 조건
- 동적 툴과 모델 선택
- 오류 처리와 복구