1. 왜 에이전트에 “프로덕션 사고방식”이 필요한가
일반적인 백엔드를 작성할 때 “프로덕션에 배포한다”는 아이디어만으로도 자동으로 편집증 모드가 켜집니다: 인가, 로깅, 에러 처리, 리밋, 시크릿은 코드가 아닌 .env에.
에이전트에도 같은 모드를 켜야 하지만, 훨씬 더 엄격하게 해야 합니다. 이유는 간단합니다: 보통의 백엔드는 당신이 작성한 그대로만 실행하지만, 에이전트는 주어진 도구와 지시 범위 안에서 모델이 스스로 판단해 하는 일을 수행합니다. 여기서는 통제의 착시가 고전적인 코드보다 더 강합니다: 프롬프트가 모든 것을 규정해 주는 듯 보이지만, 실제로는 모델의 모든 생각을 통제하는 것이 아니라 오직 환경과 가능한 행동만 통제할 뿐입니다.
그래서 이번 강의에서는 에이전트를 “보호 레이어”들로 차근차근 감싸 나가겠습니다:
- 먼저 그가 정확히 무엇을 할 수 있는지 제한합니다(도구 권한과 에이전트 분리),
- 그다음 실행 환경을 격리합니다(sandbox와 리밋),
- 시크릿과 PII를 정리합니다,
- 마지막으로 관찰성을 켭니다: 로그, 메트릭, 기본 트레이싱.
구체적으로 보기 위해, 우리의 GiftGenius 이야기를 이어갑니다: 선물 추천을 도와주고 커머스 세계에 조금 발을 담그는(주문과 체크아웃을 거치지만, ACP의 자세한 내용은 나중에) 에이전트입니다.
2. 권한: 에이전트에게 “세상 모든 버튼”은 필요 없다
최소 권한 원칙 (Least Privilege)
첫 번째 규칙: 에이전트는 모든 걸 할 필요가 없습니다. 도구가 많을수록 “그게 아닌” 함수를 “그때가 아닌” 순간에 호출할 가능성이 커집니다. 무엇이든 읽고 쓰는 거대한 manageEverything() 대신, 최소한 읽기와 쓰기를 분리한 작고 명확한 함수들로 설계합니다.
GiftGenius에서는 특히 분명합니다: 선물 목록과 사용자 선호를 읽는 일과 주문을 생성하거나 확정하는 일(여기에는 돈이 걸려 있습니다)은 다릅니다. 그래서 보통은 다음처럼 합니다:
- 안전한 “read‑only” 도구 세트(선물 검색, 상세 보기),
- 별도의 “write” 도구(주문 초안 생성, 주문 취소),
- 필요하다면 특히 위험한 작업을 위한 또 하나의 레벨(결제 확정, 대량 변경).
업무별로 에이전트를 분리
또 하나의 강력한 기법은 책임 영역별로 에이전트를 분리하는 것입니다. 한 에이전트는 “선물 추천”, 다른 에이전트는 “주문 관리”. 그러면 gift 에이전트의 모델이 조금 “탈선”하더라도, 그 에이전트의 설정에 결제 도구가 없기 때문에 물리적으로 결제 도구를 호출할 수 없습니다.
에이전트와 도구 구성의 최소 타입을 가정해 봅시다:
// 아이디어 설명을 위한 단순화된 타입
type ToolName = 'suggest_gifts' | 'get_gift_details' |
'create_order_draft' | 'confirm_order';
type AgentConfig = {
id: string;
allowedTools: ToolName[];
maxSteps: number;
};
이제 GiftGenius의 두 에이전트를 정의해 보겠습니다:
export const giftPlannerAgent: AgentConfig = {
id: 'gift-planner',
allowedTools: ['suggest_gifts', 'get_gift_details'],
maxSteps: 6,
};
export const orderAgent: AgentConfig = {
id: 'order-manager',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 4,
};
네, 아직은 추상화에 가깝지만 요지는 간단합니다: 코드에 네 개의 도구가 다 있더라도, 특정 에이전트에는 필요한 부분집합만 부여합니다.
권한을 사용자와 역할에 묶기
서로 다른 두 개체를 기억하는 것이 중요합니다:
- 사용자와 그의 권한(해당 user_id가 구매/취소/히스토리 보기 등을 할 수 있는가),
- 에이전트와 그에게 허용된 도구.
이상적으로는 모든 도구 호출이 두 가지 검사를 모두 통과해야 합니다: “에이전트에게 허용되었는가?” 그리고 “사용자에게도 허용되었는가?”.
의사 코드로:
type UserRole = 'guest' | 'customer' | 'admin';
function canUserCallTool(role: UserRole, tool: ToolName): boolean {
if (tool === 'confirm_order') {
return role === 'customer' || role === 'admin';
}
if (tool === 'create_order_draft') {
return role !== 'guest';
}
return true; // 읽기는 모두 허용
}
MCP/백엔드 쪽에서 tool 호출을 처리할 때 이중 검사를 할 수 있습니다:
function assertToolAllowed(
agent: AgentConfig,
userRole: UserRole,
tool: ToolName,
) {
if (!agent.allowedTools.includes(tool)) {
throw new Error(`에이전트 ${agent.id}에 대해 도구 ${tool}는(은) 허용되지 않습니다`);
}
if (!canUserCallTool(userRole, tool)) {
throw new Error(`역할이 ${userRole}인 사용자는 ${tool}를(을) 호출할 수 없습니다`);
}
}
결국 모델이 잘못된 에이전트나 게스트 사용자로 confirm_order를 호출하려 해도 — 이 검사에서 막혀 통제 가능한 오류로 바뀌며, 계획되지 않은 결제로 이어지지 않습니다.
환경별 서로 다른 구성
dev와 staging 환경에서는 더 많은 자유를 주고 싶을 때가 흔합니다: 테스트 도구, 가짜 결제 서비스, 실험적 기능들. 반대로 production에서는 구성은 최대한 엄격합니다: 일부 도구는 비활성, 엔드포인트는 실운영만, 토큰은 실제만.
가장 단순한 스키마:
type Env = 'dev' | 'staging' | 'production';
const env = (process.env.APP_ENV as Env) ?? 'dev';
const orderAgentByEnv: Record<Env, AgentConfig> = {
dev: {
id: 'order-manager-dev',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 8,
},
staging: {
id: 'order-manager-staging',
allowedTools: ['create_order_draft', 'confirm_order'],
maxSteps: 6,
},
production: {
id: 'order-manager-prod',
allowedTools: ['create_order_draft'], // confirm은 별도의 경로로만 수행
maxSteps: 4,
},
};
export const currentOrderAgent = orderAgentByEnv[env];
프로덕션에서는 confirm_order를 아예 별도의 “위험한” 에이전트로 분리할 수도 있습니다. 이는 위젯에서 “주문 확인”을 명시적으로 클릭하고 추가 검증을 거친 뒤에만 호출합니다.
3. Sandbox: 에이전트에 당신의 우주에 대한 root 권한은 필요 없다
격리 수준
에이전트와 사용자에 대한 권한을 정의한 뒤에는, 다음 보호 레벨로 넘어갑니다 — sandbox와 실행 환경 격리입니다.
에이전트와 그 도구에 대한 sandbox는 대략 다음과 같은 몇 가지 수준으로 나눌 수 있습니다:
- 도구 코드 레벨. 파일 시스템, 네트워크, 프로세스 자원 접근을 제한합니다: 아무 데나 쓰기 금지, 임의 도메인 접근 금지, CPU를 무한 점유하거나 메모리를 기가바이트 단위로 먹지 못하게 합니다.
- Agents SDK 레벨. run 사이클의 단계 수, tool 호출 수, 컨텍스트 크기(token limit)에 리밋을 둡니다. 모델은 무한히 “생각”하거나 tool-call을 양산할 수 없고, 어느 시점에서 “단계 제한” 또는 “시간 제한” 오류로 run이 종료됩니다.
이 모든 것이 고전적인 “방어적 아키텍처”를 구성하며, 아래와 같은 그림으로 표현하면 이해하기 쉽습니다.
graph TD
A[프롬프트 / system 지시문] --> B[도구의 JSON Schema]
B --> C[에이전트와 사용자 권한]
C --> D[인프라 샌드박스]
D --> E[외부 서비스 / DB]
subgraph 에이전트
A
B
C
end
subgraph 인프라
D
end
프롬프트는 가장 약한 보호입니다; 진짜 힘은 당신이 코드가 무엇을 할 수 있고 어떤 API에 접근 가능한지를 물리적으로 제한하는 곳에서 시작됩니다.
런 사이클 제한: 단계, 시간, tool‑calls
sandbox의 일부는 에이전트의 구성에 바로 표현할 수 있습니다: 최대 단계 수, 전체 실행 시간, tool 호출 수 제한. 이는 runaway 사이클로부터의 보호일 뿐 아니라 비용 통제이기도 합니다.
런 옵션의 추상적인 구성 예시:
type RunLimits = {
maxSteps: number;
maxToolCalls: number;
timeoutMs: number;
};
const defaultLimits: RunLimits = {
maxSteps: 8,
maxToolCalls: 10,
timeoutMs: 30_000,
};
이런 리밋을 에이전트를 실행하는 래퍼에 전달합니다. 모델이 11번째로 도구를 호출하려 하면 — 에이전트 실행을 중단하고 과제가 너무 복잡하다고 사용자에게 정직하게 알립니다. 무제한으로 예산을 태우게 두지 않습니다.
코드와 네트워크 격리
컨테이너/프로세스 레벨에서의 일반적인 실천은 다음과 같습니다:
MCP 서버 및/또는 에이전트 서비스의 코드는 읽기 전용 파일 시스템(특별히 지정한 작업 디렉터리를 제외)과 제한된 자원(CPU, RAM)으로 컨테이너에서 실행됩니다. 네트워크는 allow‑list로 설정됩니다: 필요한 외부 서비스(커머스 백엔드, 결제, 몇 개의 외부 API)로만 갈 수 있고, 임의의 인터넷으로는 갈 수 없습니다.
에이전트 시나리오에서는 특히 중요합니다: 모델이 “이상한” API로 나가거나 예상치 못한 파일을 읽으려 할 수 있으며, 그런 시도에도 물리적으로 여분의 리소스에 접근 권한이 없도록 해야 합니다.
코드에서는 보통 이것이 “마법의 TypeScript 한 줄”이 아니라, 오케스트레이터 설정(Docker Compose, Kubernetes, Vercel, Fly.io 등)로 나타납니다. 하지만 설계 단계에서부터 다음을 염두에 두는 것이 유익합니다:
- 외부 코드를 실행하는 도구(예: 셸 명령으로 리포트 생성)는 별도의 강한 격리 환경에서 동작해야 합니다;
- 도구는 남의 파일, 시크릿, 설정을 읽을 수 없어야 합니다;
- 네트워크 접근은 도메인 또는 IP 기준으로 명시적으로 제한하는 것이 좋습니다.
4. 시크릿과 민감 정보: 에이전트가 알 필요 없는 것
시크릿은 어디에 두고 어디에 두지 말아야 하는가
기본 규칙: 어떤 시크릿 — API 키, 비밀번호, access 토큰 — 도 모델 프롬프트, 위젯, 로그, 리포지토리에 들어가서는 안 됩니다. 이들은 다음에 둡니다:
- 환경 변수(process.env.SOMETHING),
- 시크릿 매니저(AWS Secrets Manager, GCP Secret Manager, Vault 등),
- 접근이 엄격히 통제되는 별도의 암호화 스토어.
예를 들어 GiftGenius에는 상점의 커머스 API 키가 있습니다. 에이전트가 MCP 도구를 통해 주문 초안을 만들 수 있어야 하지만, 모델은 그 키를 보지 못해야 합니다.
// mcp/tools/createOrderDraft.ts
const COMMERCE_API_KEY = process.env.COMMERCE_API_KEY!;
export async function createOrderDraft(args: {
userId: string;
giftId: string;
quantity: number;
}) {
// 모델은 COMMERCE_API_KEY를 절대 볼 수 없습니다 — 여기 서버에서만 사용됩니다
const res = await fetch(`${process.env.COMMERCE_API_URL}/orders/draft`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${COMMERCE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
if (!res.ok) {
throw new Error(`Commerce API returned ${res.status}`);
}
return res.json(); // 에이전트에게는 이미 안전한 객체만 돌려줍니다
}
중요: 도구의 응답에서 키나 다른 민감한 상세 정보를 “끌고 나가면” 안 됩니다. 에이전트는 draftOrderId, 항목 목록, 그리고 필요하다면 상태 정도만 알면 됩니다.
PII와 컨텍스트 데이터 최소화
시크릿 외에도 PII(개인정보)라는 범주가 있습니다: 이름, 전화번호, 배송 주소, 이메일 등. 에이전트는 이런 “원문” 텍스트 전부가 필요하지 않은 경우가 많습니다. 구조화된 프로필이면 충분합니다: “보드게임을 좋아함”, “나이 30–35”, “대략 예산 50–70달러”.
사용자의 전체 주문 이력을 프롬프트에 던지는 대신, 이미 집계되고 비식별화된 프로필을 반환하는 get_user_profile_summary 같은 도구를 두면 됩니다.
type ProfileSummary = {
ageRange: '18-25' | '26-35' | '36-50' | '50+';
interests: string[];
preferredBudget: { min: number; max: number };
};
export async function getUserProfileSummary(userId: string): Promise<ProfileSummary> {
// 여기서는 DB를 조회하지만, 밖으로는 집계된 정보만 반환합니다
return {
ageRange: '26-35',
interests: ['보드게임', '전자기기'],
preferredBudget: { min: 30, max: 80 },
};
}
모델은 선물 추천에 필요한 만큼만 보고, 그 이상은 보지 않습니다.
로그 스크러빙
로그는 시크릿과 PII가 우연히 드러나기 쉬운 자연스러운 장소입니다. 특히 console.log(...) 같은 “편한” 로거를 만들어 이것저것 “모두 출력”하게 하면 더 그렇습니다.
좋은 접근은 중앙 로거를 두고, 출력 전에 페이로드를 훑어 민감한 필드를 마스킹하는 것입니다.
type LogPayload = Record<string, unknown>;
const SENSITIVE_KEYS = ['email', 'phone', 'cardNumber', 'token'];
function scrub(payload: LogPayload): LogPayload {
const result: LogPayload = {};
for (const [key, value] of Object.entries(payload)) {
if (SENSITIVE_KEYS.includes(key)) {
result[key] = '***redacted***';
} else {
result[key] = value;
}
}
return result;
}
export function logEvent(event: string, payload: LogPayload) {
const safe = scrub(payload);
console.log(JSON.stringify({ event, ...safe }));
}
이제 “우리는 이미 반년 동안 고객의 전화번호와 토큰을 로그로 남기고 있었다” 같은 프로덕션 사고를 언젠가 맞닥뜨리기보다는, 애초에 그런 일이 불가능한 구조를 갖게 됩니다. 이는 단지 꼼꼼함의 문제가 아니라, 향후 컴플라이언스(GDPR 및 각국 법규) 요구사항의 문제이기도 합니다: 로그의 PII가 적을수록 제품은 더 편합니다.
5. 에이전트 모니터링과 관찰성
무엇을 봐야 하는가
우리는 에이전트가 할 수 있는 일, 그가 볼 수 있는 데이터, 로그에 들어가는 내용을 제한했습니다. 다음 질문은 — 이 “동물원” 속에서 에이전트가 프로덕션에서 우리가 의도한 대로 행동하고 있는지 어떻게 알 수 있을까요?
일반적인 “서비스 살아있음/죽음” 모니터링은 에이전트에게 거의 쓸모가 없습니다. 프로세스가 살아 있는지만이 아니라, 그 행태를 이해하는 것이 중요합니다: 어떤 단계를 수행하는지, 어떤 도구를 호출하는지, 어디서 실패하는지, 어디서 루프에 빠지는지.
각 run에 대해 최소한으로 필요한 데이터:
- agent_run_id — 실행의 고유 식별자;
- 익명 user_id 또는 세션 ID;
- 에이전트 이름과 환경;
- 호출된 도구 목록: 이름, 횟수, 총 소요 시간;
- 워크플로 단계와 어디에서 멈췄는지;
- 최종 상태: success, partial_success, failed, canceled, timeout, limits_exceeded.
이를 구조로 정리하면 다음과 같습니다:
type RunStatus =
| 'success'
| 'partial_success'
| 'failed'
| 'canceled'
| 'timeout'
| 'limits_exceeded';
type ToolCallLog = {
name: ToolName;
durationMs: number;
success: boolean;
};
type AgentRunLog = {
runId: string;
agentId: string;
userId: string;
env: Env;
startedAt: string;
finishedAt: string;
status: RunStatus;
toolCalls: ToolCallLog[];
errorMessage?: string;
};
에이전트 실행 래퍼 예시
실제 Agents SDK 호출을 캡슐화하는 runAgent 함수가 있다고 가정해 봅시다. 여기에 모니터링을 입혀 보겠습니다:
async function runAgentWithLogging(
agent: AgentConfig,
input: string,
userId: string,
): Promise<string> {
const runId = crypto.randomUUID();
const startedAt = new Date();
const toolCalls: ToolCallLog[] = [];
try {
const result = await runAgent(agent, input, {
userId,
limits: defaultLimits,
onToolCall: (name, durationMs, success) => {
toolCalls.push({ name, durationMs, success });
},
});
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'success',
toolCalls,
};
logEvent('agent_run', log);
return result;
} catch (err) {
const finishedAt = new Date();
const log: AgentRunLog = {
runId,
agentId: agent.id,
userId,
env,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
status: 'failed',
toolCalls,
errorMessage: (err as Error).message,
};
logEvent('agent_run', log);
throw err;
}
}
여기서 runAgent는 실제 Agents SDK로 구현될 수 있는 블랙박스입니다. 우리는 특정 API에 묶이지 않고 관찰성을 추가하는 방법을 보여 주고 있습니다.
로그 vs 메트릭 vs 트레이싱
관찰성에는 보통 세 가지 레벨을 구분해 두면 편합니다:
| 레벨 | 정의 | GiftGenius 에이전트 예시 |
|---|---|---|
| 로그 | 개별 run에 대한 “스토리” | 단계와 도구가 담긴 상세 AgentRunLog |
| 메트릭 | 집계된 수치 지표 | p95 run 지속 시간, 평균 tool‑call 수, error‑rate |
| 트레이싱 | 요청과 하위 요청의 트리/그래프 | Run → 단계 → tool‑calls → 외부 API 호출(commerce, DB 등) |
메트릭은 “전반적으로 괜찮은가?”를 파악하기 위한 것입니다(예: 지난 1시간 error‑rate). 로그와 트레이싱은 “왜 여기서 문제가 생겼는가?”를 파악하고 문제의 run을 재현하기 위한 것입니다.
메트릭의 초석은 로그 위에 구현할 수 있습니다: 주기적인 작업이 agent_run 이벤트를 집계해 p95 지속 시간, 오류 수 등을 계산합니다.
6. GiftGenius 전체 흐름에서는 이렇게 보인다
추상화의 나열처럼 보이지 않도록, 우리의 학습용 애플리케이션에 이를 모아 보겠습니다.
프로덕션 환경의 gift-planner 에이전트는 안전한 도구만 가집니다: 선물 추천과 상세 조회. 결제나 주문 관리는 보지 못합니다. system 지시문은 “당신이 모든 비용을 대신 지불하겠다” 같은 약속을 하지 말고, 최대한 추천을 준비하고, 필요하다면 선물 목록 초안을 준비하도록 안내합니다.
order-manager 에이전트는 별도로 존재하며 주문만 다룰 수 있습니다. 프로덕션에서는 주문 초안(create_order_draft)만 생성할 수 있으며, 주문 확정(confirm_order)은 위젯의 명시적 UI 트리거를 통해 사람이 수행하거나, dev/staging에서만 가능하도록 합니다. 그 도구들은 상점의 API 키 같은 시크릿을 오직 백엔드 측에서만 사용하고, 응답에는 필요한 필드만 프록시합니다.
두 에이전트 모두 runAgentWithLogging 래퍼를 통해 실행되며, 이 래퍼는 리밋을 적용하고 agent_run_id, userId, 환경, 도구 목록과 함께 로그를 기록합니다. 로그에는 이메일과 전화번호가 없습니다; 이런 필드는 스크러버로 미리 제거됩니다. 사용자 프로필은 비식별화 형태로 사용합니다: 연령대, 관심사, 예산 등이며, 전체 구매 이력의 원문은 아닙니다.
MCP 서버와 에이전트 서비스가 사는 인프라는 격리되어 있습니다: 읽기 전용 파일 시스템(특별히 할당된 디렉터리나 /tmp를 제외), CPU/RAM 제한, 도메인 allow‑list 네트워크. 에이전트가 “이상한 곳”을 치려고 해도 물리적으로 접근할 수 없습니다.
어느 순간 limits_exceeded 상태의 run 비율이 튀거나 “평균 tool‑call 수 > 10” 같은 메트릭이 보이면, 프롬프트가 지나치게 말이 많아졌거나 특정 도구가 문제를 일으켜 에이전트가 단계를 재시도하게 만들고 있음을 이해할 수 있습니다.
이것이 바로 “대충 되는” 실험용 에이전트가 아니라, 성숙한 서비스의 행동입니다.
7. 프로덕션 배포 시 흔한 실수
지금까지 논의한 것은 “올바른” 프로덕션 에이전트의 모습입니다. 실제로는 흔한 함정들이 더 자주 등장합니다. 다음 리스트로 모아 봅니다: 이 오류들만 피해도 프로덕션 배포가 훨씬 평온해집니다.
오류 1: 에이전트에게 “모두 허용”.
흔한 시나리오: 검색/수정/삭제/결제 같은 MCP 도구를 잔뜩 정의해 놓고, 에이전트를 만들 때 그냥 전체 목록을 먹입니다. 결과적으로 모델은 읽기만 원했던 지점에서 삭제나 결제를 실수로 호출할 수 있습니다. 도구를 역할별로 분리하고, 각자 고유의 allowedTools를 가진 더 좁은 에이전트 여러 개로 나누는 것으로 해결합니다.
오류 2: 권한 검증을 프롬프트에만 의존.
개발자가 system 지시문에 “사용자 확인 없이 아무것도 구매하지 마”라고 적고 안심하는 경우가 있습니다. 그러나 프롬프트는 약한 보호이며, 탈옥(jailbreak)이나 단순 실수를 막아 주지 못합니다. 백엔드 수준의 실제 검증이 필요합니다: “에이전트에게 이 도구가 허용되었는가”와 “사용자에게 이 도구가 허용되었는가”. 그렇지 않으면 한 번의 부정확한 생성으로 아무도 의도하지 않은 행동이 일어날 수 있습니다.
오류 3: 프롬프트와 로그에 시크릿.
통합을 “빨리” 하고 싶어 system 프롬프트에 API 키를 넣거나 도구 인자로 전달해, 에이전트가 직접 외부 API를 호출하게 하고 싶어질 수 있습니다. 결국 키는 모델 로그와 잠재적으로 외부 시스템에 남습니다. 이는 유출과 스토어 차단으로 가는 지름길입니다. 시크릿은 서버 측, 환경 변수 또는 시크릿 매니저에만 존재해야 하며, 모델 컨텍스트로는 절대 들어가면 안 됩니다.
오류 4: 스크러빙 없는 “원문” 로그.
디버깅할 때는 console.log(...)로 “일단 찍자” 하고 잊어버리기 쉽습니다. 몇 달 뒤 보면 로그에 사용자 주소, 전화번호, PII가 포함된 주문 번호가 누적되어 있는 경우가 있습니다. GDPR 등 규제 환경에서는 특히 곤란합니다. “우리는 dev에서만 로그 찍어”라고 느껴지더라도, 중앙 로거를 두고 민감 필드를 자동 마스킹하는 것을 처음부터 도입하는 것이 좋습니다.
오류 5: 에이전트 행태에 대한 리밋 부재.
단계 수, 시간, tool 호출 수 제한이 없으면 에이전트가 루프에 빠질 수 있습니다: 같은 도구를 반복 호출하거나, 같은 오류를 무한히 고치려 하고, 토큰을 엄청 소비하며, 외부 API를 과부하 시킵니다. 최선의 경우 모델 요금 폭탄, 최악의 경우 백엔드가 다운되고 사용자 모두가 화가 납니다. 런 사이클 리밋과 합리적인 타임아웃 디폴트는 필수 구성입니다.
오류 6: read와 write를 하나의 도구로 섞기.
가끔 getOrCreateOrder처럼 “편리한” 메서드를 만들어, 주문이 없으면 새로 생성하게 하기도 합니다. 고전 백엔드에서는 허용 가능한 패턴일 수 있지만, 에이전트 세계에서는 예상치 못한 부작용을 낳을 수 있습니다: 모델은 상태만 알고 싶었는데, 도구가 무언가를 만들어 버립니다. get_order_details와 create_order_draft를 분리하는 것이 훨씬 안전합니다. 그러면 반복 호출에도 결과를 더 잘 통제할 수 있습니다.
오류 7: 관찰성 무시.
많은 팀이 “나중에 로그와 메트릭 붙이자, 지금은 일단 되게 하자”로 시작합니다. 관찰성 없는 에이전트는 블랙박스입니다: 어떤 도구를 호출하고, 몇 단계나 밟고, 어디서 실패했는지 모릅니다. 사용자 불만 하나가 어두운 방의 수사로 변합니다. 차라리 처음부터 agent_run_id, 도구, 상태 같은 로그 구조와 기본 메트릭을 설계해 두는 편이, 뒤늦게 혼란스러운 코드 위에 얹는 것보다 훨씬 쉽습니다.
GO TO FULL VERSION