본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2026. 1. 31. · 58 Views
Pi Agent 런타임 완벽 가이드
AI 에이전트 런타임 아키텍처를 실무 중심으로 배워봅니다. Pi Agent Core의 구조부터 RPC 모드, 도구 스트리밍까지 초급 개발자도 쉽게 이해할 수 있도록 스토리텔링 방식으로 설명합니다.
목차
- 도입_AI_에이전트_런타임이란
- Pi_Agent_Core_아키텍처
- src_agents_디렉토리_분석
- RPC_모드_구현_원리
- 도구_스트리밍과_블록_스트리밍
block(최종 응답)- 실전_커스텀_에이전트_만들기
1. 도입 AI 에이전트 런타임이란
김개발 씨는 최근 AI 에이전트 프로젝트에 투입되었습니다. 팀장님께서 "Pi Agent 런타임으로 에이전트를 만들어야 해"라고 하셨는데, 도대체 런타임이 뭘까요?
선배 박시니어 씨에게 물어보니 "에이전트가 돌아가는 환경이지"라는 답변만 돌아왔습니다.
AI 에이전트 런타임은 한마디로 AI 에이전트가 실행되는 기반 환경입니다. 마치 자동차 엔진처럼, 에이전트의 모든 동작을 관리하고 제어합니다.
Pi Agent Runtime은 타입스크립트 기반으로 만들어진 현대적인 에이전트 실행 환경으로, 개발자가 쉽게 AI 에이전트를 만들고 배포할 수 있게 해줍니다.
다음 코드를 살펴봅시다.
import { Agent, AgentRuntime } from '@pi-agent/core';
// 간단한 에이전트 생성
const myAgent = new Agent({
name: 'assistant',
model: 'gpt-4',
// 에이전트가 사용할 도구들
tools: [searchTool, calculatorTool],
// 시스템 프롬프트
systemPrompt: '당신은 친절한 어시스턴트입니다.'
});
// 런타임에서 에이전트 실행
const runtime = new AgentRuntime();
const response = await runtime.run(myAgent, '오늘 날씨는?');
김개발 씨는 회의실에서 노트북을 펼쳤습니다. 화면에는 낯선 코드가 가득했습니다.
"에이전트 런타임이라는 게 정확히 뭘 하는 건지 모르겠어요." 답답한 마음에 박시니어 씨를 다시 찾아갔습니다. 박시니어 씨는 커피를 한 모금 마시고 천천히 설명을 시작했습니다.
"쉽게 말하면, 런타임은 에이전트가 살아가는 세계야." 런타임이 왜 필요할까요? AI 모델은 그 자체로는 아무것도 할 수 없습니다. 단순히 텍스트를 받아서 텍스트를 생성할 뿐입니다.
하지만 우리가 원하는 건 "날씨를 검색하고, 계산하고, 파일을 읽는" 똑똑한 에이전트입니다. 바로 이 지점에서 런타임이 등장합니다.
런타임은 마치 무대 감독처럼, AI 모델이 실제 세계와 상호작용할 수 있도록 모든 것을 조율합니다. 런타임의 핵심 역할 첫째, 런타임은 도구 관리를 담당합니다.
AI가 "날씨를 검색해줘"라고 하면, 런타임이 날씨 API를 호출하고 결과를 AI에게 전달합니다. 마치 비서가 상사의 업무를 처리하는 것과 같습니다.
둘째, 대화 흐름 제어를 맡습니다. 사용자 질문 → AI 응답 → 도구 호출 → 결과 확인 → 최종 답변.
이 복잡한 흐름을 런타임이 자동으로 관리합니다. 셋째, 상태 관리도 중요합니다.
대화 기록, 사용한 토큰 수, 현재 진행 중인 작업 등을 추적합니다. Pi Agent Runtime의 특징 Pi Agent는 타입스크립트로 작성되었습니다.
이것은 큰 장점입니다. 타입 안전성 덕분에 개발 중 많은 버그를 미리 잡을 수 있기 때문입니다.
또한 모듈식 설계를 채택했습니다. 필요한 기능만 골라서 사용할 수 있습니다.
마치 레고 블록을 조립하듯이, 원하는 에이전트를 만들 수 있습니다. 실무에서의 활용 실제 서비스를 개발한다고 생각해봅시다.
고객 상담 챗봇을 만들어야 합니다. 런타임 없이 직접 구현하려면 어떻게 해야 할까요?
OpenAI API를 호출하는 코드, 도구 실행 로직, 에러 처리, 대화 기록 저장 등을 모두 직접 작성해야 합니다. 코드가 수백 줄로 늘어나고, 버그가 숨어있기 쉽습니다.
하지만 Pi Agent Runtime을 사용하면 위의 코드처럼 10줄 안팎으로 깔끔하게 구현할 수 있습니다. 런타임이 복잡한 부분을 모두 처리해주기 때문입니다.
코드 살펴보기 처음 코드를 다시 보겠습니다. new Agent()로 에이전트 객체를 생성합니다.
이때 모델 이름, 사용할 도구, 시스템 프롬프트를 설정합니다. 그다음 AgentRuntime()으로 런타임을 초기화합니다.
마지막으로 runtime.run()을 호출하면 에이전트가 실행됩니다. 단 세 단계입니다.
초보자들이 헷갈리는 부분 많은 분들이 "에이전트와 런타임이 뭐가 다른가요?"라고 질문합니다. 간단히 말하면, 에이전트는 설계도이고 런타임은 공장입니다.
에이전트는 "어떤 도구를 쓸지, 어떤 성격일지" 정의합니다. 런타임은 그 정의대로 실제로 동작하게 만듭니다.
다시 김개발 씨의 이야기로 박시니어 씨의 설명을 들은 김개발 씨는 이제 이해가 되기 시작했습니다. "아, 런타임이 엔진이고 에이전트가 자동차 설계도구나!" 맞습니다.
이제 런타임의 개념을 이해했다면, 다음 단계는 내부 구조를 파악하는 것입니다.
실전 팁
💡 - 런타임은 에이전트의 실행 환경이라고 이해하세요
- Pi Agent는 타입스크립트 기반이므로 타입 안전성을 활용하세요
- 작은 에이전트부터 만들어보며 감을 익히세요
2. Pi Agent Core 아키텍처
김개발 씨는 이제 런타임이 뭔지 알았습니다. 하지만 코드베이스를 열어보니 수십 개의 파일이 복잡하게 얽혀있습니다.
"어디서부터 봐야 하지?" 막막했습니다. 박시니어 씨가 말했습니다.
"Core 아키텍처부터 이해하면 돼."
Pi Agent Core는 런타임의 핵심 구조입니다. 크게 Agent Layer, Runtime Layer, Tool Layer 세 계층으로 나뉩니다.
각 레이어는 명확한 책임을 가지며, 서로 독립적으로 동작합니다. 마치 건물의 기초, 골조, 인테리어가 분리되어 있는 것처럼 깔끔하게 설계되었습니다.
다음 코드를 살펴봅시다.
// Core 아키텍처의 세 계층
import { BaseAgent } from './core/agent';
import { RuntimeEngine } from './core/runtime';
import { ToolRegistry } from './core/tools';
// 1. Tool Layer: 도구 등록
const toolRegistry = new ToolRegistry();
toolRegistry.register('search', searchTool);
toolRegistry.register('calculator', calcTool);
// 2. Agent Layer: 에이전트 정의
const agent = new BaseAgent({
model: 'gpt-4',
tools: toolRegistry.getAll()
});
// 3. Runtime Layer: 실행 엔진
const runtime = new RuntimeEngine({ agent });
await runtime.execute('2+2는?');
김개발 씨는 화이트보드 앞에 섰습니다. 박시니어 씨가 마커를 들고 세 개의 박스를 그렸습니다.
"이게 전부야. 복잡해 보이지만, 핵심은 이 세 개뿐이야." 레이어 구조의 철학 소프트웨어 아키텍처에는 오래된 원칙이 하나 있습니다.
**관심사의 분리(Separation of Concerns)**입니다. 각 부분이 한 가지 일만 잘하도록 만들면, 전체 시스템이 유지보수하기 쉬워집니다.
Pi Agent Core도 이 원칙을 따릅니다. 도구 관리는 Tool Layer, 에이전트 설정은 Agent Layer, 실행 로직은 Runtime Layer가 담당합니다.
Tool Layer 깊이 파헤치기 맨 아래 계층은 Tool Layer입니다. 여기서 에이전트가 사용할 모든 도구를 정의하고 관리합니다.
ToolRegistry는 도구들의 등록부입니다. 마치 도서관의 카드 목록처럼, 어떤 도구가 있는지 추적합니다.
새로운 도구를 추가하려면 register() 메서드를 호출하면 됩니다. 중요한 점은, 도구가 표준 인터페이스를 따른다는 것입니다.
모든 도구는 name, description, execute() 메서드를 가져야 합니다. 이렇게 하면 런타임이 어떤 도구든 동일한 방식으로 호출할 수 있습니다.
Agent Layer의 역할 중간 계층은 Agent Layer입니다. 여기서 에이전트의 성격과 능력을 정의합니다.
BaseAgent 클래스는 모든 에이전트의 기본이 됩니다. 어떤 모델을 쓸지, 어떤 도구를 사용할지, 시스템 프롬프트는 무엇인지 설정합니다.
이 레이어는 불변성을 중시합니다. 한번 만들어진 에이전트 설정은 변하지 않습니다.
새로운 설정이 필요하면 새 에이전트를 만듭니다. 이렇게 하면 예측 가능한 동작을 보장할 수 있습니다.
Runtime Layer의 마법 최상위 계층은 Runtime Layer입니다. 여기가 진짜 마법이 일어나는 곳입니다.
RuntimeEngine은 에이전트를 실행하는 엔진입니다. 사용자 입력을 받아서, AI 모델을 호출하고, 필요하면 도구를 실행하고, 최종 결과를 반환합니다.
이 과정은 이벤트 루프로 구현됩니다. "AI 응답 → 도구 호출 확인 → 도구 실행 → 결과를 AI에게 전달 → 최종 응답" 이 순환을 완료될 때까지 반복합니다.
실제 요청 흐름 추적하기 사용자가 "2+2는?"이라고 물었다고 가정해봅시다. 먼저 Runtime Layer가 요청을 받습니다.
Agent Layer에서 설정된 시스템 프롬프트와 함께 AI 모델로 전달됩니다. AI는 "calculator 도구를 사용해야겠다"고 판단합니다.
Runtime Layer는 Tool Layer에서 calculator를 찾아 실행합니다. 결과는 "4"입니다.
이 결과를 다시 AI에게 전달하면, AI는 "2+2는 4입니다"라는 자연어 응답을 생성합니다. 최종적으로 사용자에게 반환됩니다.
왜 이렇게 복잡하게 나눴을까? 처음에는 "그냥 한 파일에 다 넣으면 안 돼?"라고 생각할 수 있습니다. 작은 프로젝트라면 괜찮을 수도 있습니다.
하지만 프로젝트가 커지면 문제가 생깁니다. 도구를 추가할 때마다 전체 코드를 수정해야 합니다.
에이전트 로직과 실행 로직이 뒤섞여 디버깅이 어려워집니다. 레이어를 나누면 확장성이 좋아집니다.
새 도구는 Tool Layer에만 추가하면 됩니다. 다른 레이어는 전혀 건드릴 필요가 없습니다.
테스트하기 쉬운 구조 또 다른 장점은 테스트 용이성입니다. 각 레이어를 독립적으로 테스트할 수 있습니다.
Tool Layer만 테스트하고 싶다면, 가짜 에이전트와 런타임을 만들면 됩니다. Runtime Layer를 테스트할 때는 가짜 도구를 주입하면 됩니다.
김개발 씨의 깨달음 김개발 씨는 화이트보드의 그림을 사진으로 찍었습니다. "이제 이해가 돼요.
각 레이어가 자기 일만 하니까 코드가 깔끔하네요." 박시니어 씨가 웃으며 말했습니다. "맞아.
좋은 아키텍처는 복잡함을 단순함으로 바꿔주지."
실전 팁
💡 - 각 레이어의 책임을 명확히 이해하세요
- 새로운 기능을 추가할 때 어느 레이어에 속하는지 먼저 판단하세요
- 레이어 간 의존성을 최소화하세요
3. src agents 디렉토리 분석
다음 날, 김개발 씨는 src/agents 폴더를 열었습니다. BaseAgent.ts, RPCAgent.ts, StreamingAgent.ts 등 여러 파일이 보였습니다.
"이 파일들은 다 뭐하는 거지?" 또다시 박시니어 씨를 찾아갔습니다. "각 에이전트가 다른 역할을 하거든."
src/agents 디렉토리는 다양한 에이전트 구현체들이 모여있는 곳입니다. BaseAgent는 모든 에이전트의 부모 클래스이고, RPCAgent는 원격 호출을 지원하며, StreamingAgent는 실시간 스트리밍을 처리합니다.
각 에이전트는 특정 사용 사례에 최적화되어 있습니다.
다음 코드를 살펴봅시다.
// src/agents 구조
import { BaseAgent } from './agents/BaseAgent';
import { RPCAgent } from './agents/RPCAgent';
import { StreamingAgent } from './agents/StreamingAgent';
// 기본 에이전트 (동기식)
const basic = new BaseAgent({ model: 'gpt-4' });
const result = await basic.run('안녕?');
// RPC 에이전트 (원격 호출)
const rpc = new RPCAgent({ endpoint: 'http://api.example.com' });
await rpc.connect();
const rpcResult = await rpc.run('계산해줘');
// 스트리밍 에이전트 (실시간)
const streaming = new StreamingAgent({ model: 'gpt-4' });
for await (const chunk of streaming.stream('긴 이야기 써줘')) {
process.stdout.write(chunk);
}
김개발 씨는 에디터로 BaseAgent.ts 파일을 열었습니다. 200줄이 넘는 코드가 화면을 가득 채웠습니다.
"우와, 복잡하다..." 한숨이 나왔습니다. 박시니어 씨가 옆으로 와서 말했습니다.
"걱정 마. 핵심만 보면 단순해." BaseAgent: 모든 것의 시작 BaseAgent는 추상 클래스입니다.
마치 건축의 청사진처럼, 모든 에이전트가 따라야 할 기본 구조를 정의합니다. 이 클래스에는 run() 메서드가 있습니다.
사용자 입력을 받아서 AI 응답을 반환하는 핵심 메서드입니다. 모든 하위 클래스는 이 메서드를 구현해야 합니다.
또한 tools, model, systemPrompt 같은 공통 속성들도 정의되어 있습니다. 어떤 에이전트든 이 속성들은 필요하기 때문입니다.
왜 추상 클래스를 쓸까? 초보 개발자들이 자주 묻는 질문입니다. "그냥 일반 클래스로 만들면 안 돼요?" 추상 클래스를 쓰면 강제성이 생깁니다.
하위 클래스는 반드시 run() 메서드를 구현해야 합니다. 안 그러면 타입스크립트가 컴파일 에러를 냅니다.
이렇게 하면 모든 에이전트가 동일한 인터페이스를 가지게 됩니다. 런타임 입장에서는 어떤 에이전트든 똑같이 사용할 수 있습니다.
RPCAgent: 원격의 세계 RPCAgent는 특별한 에이전트입니다. AI 모델이 로컬이 아니라 원격 서버에 있을 때 사용합니다.
RPC는 Remote Procedure Call의 약자입니다. 쉽게 말해 "네트워크 너머의 함수를 호출하는 것"입니다.
마치 전화를 거는 것처럼, 멀리 있는 서버의 기능을 사용할 수 있습니다. RPCAgent는 connect() 메서드로 서버와 연결을 맺습니다.
그 다음 run()을 호출하면, 실제로는 HTTP 요청이 서버로 전송됩니다. 서버가 AI 모델을 실행하고 결과를 돌려줍니다.
언제 RPC를 쓸까? 실무에서 RPC 패턴이 필요한 경우가 많습니다. 예를 들어 AI 모델이 너무 커서 로컬에서 실행하기 어렵다면, 강력한 GPU 서버에서 돌리고 결과만 받아오면 됩니다.
또한 비용 절감 효과도 있습니다. 모든 클라이언트가 각자 모델을 로드하는 대신, 중앙 서버 하나만 띄워놓으면 됩니다.
StreamingAgent: 실시간의 마법 StreamingAgent는 가장 흥미로운 구현체입니다. 일반 에이전트는 모든 응답이 완성될 때까지 기다립니다.
하지만 스트리밍 에이전트는 생성되는 즉시 텍스트를 전달합니다. ChatGPT를 써보셨나요?
답변이 타이핑하듯이 나타나는 것을 본 적이 있을 겁니다. 바로 스트리밍 방식입니다.
stream() 메서드는 일반 Promise가 아니라 AsyncIterator를 반환합니다. for await...of 루프로 하나씩 받을 수 있습니다.
스트리밍의 장점 첫째, 사용자 경험이 훨씬 좋습니다. 5초 동안 아무것도 안 나오다가 갑자기 답변이 뜨는 것보다, 조금씩 나타나는 게 훨씬 자연스럽습니다.
둘째, 메모리 효율성입니다. 긴 텍스트를 한꺼번에 메모리에 올리지 않고, 조금씩 처리하고 버릴 수 있습니다.
코드 흐름 분석 위 코드를 다시 보겠습니다. 기본 에이전트는 await로 결과를 기다립니다.
간단하지만, 응답이 올 때까지 blocking됩니다. RPC 에이전트는 먼저 connect()로 연결을 맺습니다.
이후 run()은 내부적으로 네트워크 요청을 보냅니다. 스트리밍 에이전트는 for await...of를 씁니다.
각 chunk는 생성된 텍스트 조각입니다. process.stdout.write()로 즉시 출력합니다.
어떤 에이전트를 선택할까? 프로젝트 요구사항에 따라 달라집니다. 간단한 CLI 도구라면 BaseAgent로 충분합니다.
서버-클라이언트 구조라면 RPCAgent가 필요합니다. 실시간 채팅 UI라면 StreamingAgent가 최선입니다.
확장 가능한 설계 중요한 점은, 새로운 에이전트를 쉽게 추가할 수 있다는 것입니다. BaseAgent를 상속받고 run() 메서드만 구현하면 됩니다.
예를 들어 CachedAgent를 만들고 싶다면, 이전 응답을 캐싱하는 로직을 추가하면 됩니다. 기존 코드는 전혀 건드리지 않아도 됩니다.
김개발 씨의 선택 김개발 씨는 자신의 프로젝트를 생각했습니다. "우리는 웹 서비스니까 스트리밍이 필요하겠네요." 박시니어 씨가 고개를 끄덕였습니다.
"맞아. 사용자들은 기다리는 걸 싫어하니까."
실전 팁
💡 - 프로젝트 요구사항에 맞는 에이전트 타입을 선택하세요
- 새로운 에이전트가 필요하면 BaseAgent를 상속받아 만드세요
- 스트리밍은 사용자 경험을 크게 개선합니다
4. RPC 모드 구현 원리
김개발 씨는 RPC 모드를 구현해야 하는 작업을 맡았습니다. "클라이언트와 서버를 어떻게 연결하지?" 막막했습니다.
박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "RPC는 생각보다 단순해.
핵심은 직렬화와 역직렬화야."
RPC 모드는 원격 프로시저 호출을 통해 에이전트를 실행하는 방식입니다. 클라이언트는 요청을 직렬화해서 전송하고, 서버는 역직렬화해서 처리한 뒤 결과를 다시 직렬화해서 반환합니다.
JSON-RPC 프로토콜을 사용하며, HTTP 또는 WebSocket으로 통신합니다.
다음 코드를 살펴봅시다.
// RPC 서버 구현
import { RPCServer } from '@pi-agent/rpc';
const server = new RPCServer({
port: 3000,
agent: myAgent
});
// 요청 처리 핸들러
server.handle('agent.run', async (params) => {
const { message, options } = params;
// 에이전트 실행
const result = await myAgent.run(message, options);
// 결과 직렬화
return { success: true, data: result };
});
server.start();
console.log('RPC 서버가 3000번 포트에서 실행 중...');
김개발 씨는 회의실에서 노트를 펼쳤습니다. "RPC가 정확히 뭔가요?" 박시니어 씨는 커피를 한 모금 마시고 설명을 시작했습니다.
RPC의 기본 개념 RPC는 Remote Procedure Call, 즉 원격 함수 호출입니다. 컴퓨터 A에서 함수를 호출하면, 실제로는 컴퓨터 B에서 실행되는 방식입니다.
마치 전화를 거는 것과 같습니다. 당신이 친구에게 "저녁 뭐 먹을래?"라고 물으면, 친구가 생각하고 답변합니다.
당신은 친구의 뇌가 어떻게 작동하는지 몰라도 됩니다. 그냥 질문하고 답을 듣기만 하면 됩니다.
왜 RPC가 필요할까? AI 모델은 리소스를 많이 먹습니다. GPT-4 같은 모델은 수십 GB의 메모리가 필요합니다.
모든 사용자의 컴퓨터에 이걸 설치할 수는 없습니다. 대신 강력한 서버 한 대에 모델을 올려놓습니다.
사용자들은 가벼운 클라이언트로 서버에 요청만 보냅니다. 서버가 AI를 실행하고 결과를 돌려줍니다.
직렬화와 역직렬화 여기서 중요한 개념이 등장합니다. 네트워크를 통해 데이터를 보낼 때는 **직렬화(Serialization)**가 필요합니다.
직렬화란 객체를 문자열이나 바이트 배열로 변환하는 것입니다. 역직렬화(Deserialization)는 그 반대입니다.
예를 들어, { message: "안녕" } 객체를 JSON 문자열 '{"message":"안녕"}'로 바꾸는 게 직렬화입니다. 네트워크는 문자열만 전송할 수 있기 때문입니다.
JSON-RPC 프로토콜 Pi Agent는 JSON-RPC 2.0 프로토콜을 사용합니다. 이것은 JSON을 이용한 RPC 표준 규격입니다.
요청은 이런 형태입니다: { "jsonrpc": "2.0", "method": "agent.run", "params": { "message": "안녕" }, "id": 1 } 응답은 이렇게 옵니다: { "jsonrpc": "2.0", "result": { "success": true, "data": "안녕하세요!" }, "id": 1 } id 필드가 중요합니다. 여러 요청이 동시에 날아가도, 어떤 응답이 어떤 요청에 대한 것인지 매칭할 수 있습니다.
서버 코드 분석 위의 코드를 보겠습니다. RPCServer를 생성할 때 포트와 에이전트를 지정합니다.
server.handle()로 메서드를 등록합니다. 클라이언트가 'agent.run'을 호출하면, 이 핸들러가 실행됩니다.
핸들러 안에서 params를 받아 myAgent.run()을 호출합니다. 결과를 객체로 감싸서 반환하면, 서버가 자동으로 직렬화해서 클라이언트에게 보냅니다.
클라이언트는 어떻게 생겼을까? 클라이언트 코드도 간단합니다: typescript const client = new RPCClient({ url: 'http://localhost:3000' }); const response = await client.call('agent.run', { message: '날씨 알려줘' }); console.log(response.data); client.call()이 내부적으로 HTTP POST 요청을 보냅니다. 서버에서 응답이 오면 역직렬화해서 반환합니다.
에러 처리 RPC에서 에러 처리는 중요합니다. 네트워크는 언제든 끊길 수 있기 때문입니다.
JSON-RPC는 에러 응답 형식도 정의합니다: { "jsonrpc": "2.0", "error": { "code": -32603, "message": "서버 내부 오류" }, "id": 1 } 클라이언트는 error 필드를 확인해서 예외를 던집니다. 실무 활용 사례 대규모 서비스에서는 이런 구조를 많이 씁니다.
프론트엔드는 가벼운 React 앱입니다. 백엔드 서버에 RPC로 요청을 보내면, AI가 답변을 생성합니다.
예를 들어 고객 상담 챗봇 서비스를 만든다면, 웹 브라우저(클라이언트)가 서버에 "상품 추천해줘"라고 요청합니다. 서버는 AI 에이전트를 실행하고 추천 목록을 반환합니다.
보안 고려사항 RPC 서버를 열 때는 인증이 필수입니다. 아무나 접근하면 비용이 폭탄처럼 나갈 수 있습니다.
API 키, JWT 토큰 등으로 인증 레이어를 추가해야 합니다. Pi Agent는 미들웨어 시스템을 제공해서 쉽게 추가할 수 있습니다.
김개발 씨의 구현 김개발 씨는 서버 코드를 작성했습니다. 포트를 열고 핸들러를 등록했습니다.
클라이언트에서 테스트해보니 잘 작동했습니다. "와, 진짜 되네요!" 박시니어 씨가 웃으며 말했습니다.
"이제 RPC 전문가 됐어."
실전 팁
💡 - JSON-RPC 2.0 표준을 따르면 호환성이 좋습니다
- 에러 처리와 인증을 반드시 구현하세요
- 네트워크 지연을 고려해 타임아웃을 설정하세요
5. 도구 스트리밍과 블록 스트리밍
프로젝트가 진행되면서, 김개발 씨는 새로운 문제에 부딪혔습니다. "AI가 여러 도구를 순서대로 실행하는데, 사용자는 뭐가 진행되는지 모르겠대요." 박시니어 씨가 말했습니다.
"도구 스트리밍을 써야 해. 각 도구 실행을 실시간으로 보여줄 수 있거든."
**도구 스트리밍(Tool Streaming)**은 에이전트가 도구를 실행할 때마다 이벤트를 발생시켜 진행 상황을 실시간으로 전달하는 기법입니다. **블록 스트리밍(Block Streaming)**은 AI 응답을 작은 블록 단위로 쪼개서 보내는 방식입니다.
두 기법을 결합하면 사용자에게 완벽한 투명성을 제공할 수 있습니다.
다음 코드를 살펴봅시다.
import { StreamingAgent } from '@pi-agent/core';
const agent = new StreamingAgent({
model: 'gpt-4',
tools: [searchTool, calculatorTool],
// 도구 스트리밍 활성화
enableToolStreaming: true
});
// 이벤트 리스너 등록
agent.on('tool:start', (data) => {
console.log(`도구 시작: ${data.toolName}`);
});
agent.on('tool:complete', (data) => {
console.log(`도구 완료: ${data.toolName} → ${data.result}`);
});
agent.on('block', (block) => {
console.log(`블록: ${block.type} - ${block.content}`);
});
// 실행
await agent.run('날씨 검색하고 섭씨를 화씨로 변환해줘');
김개발 씨는 사용자 피드백을 받고 고민에 빠졌습니다. "AI가 10초 동안 아무 반응이 없다가 갑자기 답변을 내놓으니까, 사용자들이 멈춘 줄 알고 새로고침을 누른대요." 박시니어 씨가 말했습니다.
"전형적인 문제야. 사용자는 뭔가 진행되고 있다는 걸 알아야 해." 도구 스트리밍의 필요성 에이전트가 복잡한 작업을 수행한다고 생각해봅시다.
날씨를 검색하고, 단위를 변환하고, 결과를 정리합니다. 이 과정에 10초가 걸린다면 사용자는 불안해집니다.
도구 스트리밍을 쓰면 각 단계를 보여줄 수 있습니다: - "날씨 검색 중..." - "검색 완료: 현재 20도" - "단위 변환 중..." - "변환 완료: 68도" 사용자는 진행 상황을 보며 안심합니다. 이벤트 기반 아키텍처 Pi Agent는 EventEmitter 패턴을 사용합니다.
에이전트가 특정 시점에 이벤트를 발생시키면, 리스너가 반응합니다. tool:start 이벤트는 도구 실행이 시작될 때 발생합니다.
tool:complete는 완료될 때 발생합니다. 각 이벤트는 도구 이름, 파라미터, 결과 등의 데이터를 담고 있습니다.
블록 스트리밍이란? 일반 스트리밍은 텍스트를 글자 단위로 보냅니다. 하지만 에이전트 응답은 더 복잡합니다.
텍스트, 도구 호출, 결과 등 여러 종류가 섞여 있습니다. 블록 스트리밍은 의미 있는 단위로 쪼갭니다.
각 블록은 type과 content를 가집니다: - { type: 'text', content: '날씨를 검색하겠습니다' } - { type: 'tool_call', content: { name: 'search', args: {...} } } - { type: 'tool_result', content: { result: '20도' } } 클라이언트는 블록 타입에 따라 다르게 렌더링할 수 있습니다. 실제 코드 흐름 위 코드를 보겠습니다.
enableToolStreaming: true로 기능을 활성화합니다. 그다음 .on() 메서드로 이벤트 리스너를 등록합니다.
tool:start가 발생하면 "도구 시작"을 출력합니다. agent.run()을 호출하면, 내부적으로 여러 이벤트가 순차적으로 발생합니다:
5. block (최종 응답)
실전 팁
💡 - 도구 실행이 1초 이상 걸린다면 반드시 스트리밍을 쓰세요
- 블록 타입에 따라 다른 UI를 보여주면 사용자 경험이 좋아집니다
- 에러도 스트리밍으로 전달해서 사용자에게 피드백하세요
6. 실전 커스텀 에이전트 만들기
마침내 김개발 씨는 자신만의 에이전트를 만들어야 할 순간이 왔습니다. "기존 에이전트로는 우리 요구사항을 충족할 수 없어요." 팀장님이 말했습니다.
박시니어 씨가 웃으며 말했습니다. "그럼 직접 만들면 되지.
어렵지 않아."
커스텀 에이전트는 BaseAgent를 상속받아 특정 비즈니스 로직을 구현한 에이전트입니다. run() 메서드를 오버라이드해서 원하는 동작을 정의하고, 필요한 미들웨어나 **훅(Hook)**을 추가합니다.
이를 통해 완전히 커스터마이징된 에이전트를 만들 수 있습니다.
다음 코드를 살펴봅시다.
import { BaseAgent } from '@pi-agent/core';
import { Logger } from './utils/logger';
// 커스텀 에이전트 클래스
export class CustomerSupportAgent extends BaseAgent {
private logger: Logger;
private sessionHistory: Map<string, Message[]>;
constructor(options) {
super(options);
this.logger = new Logger('CustomerSupport');
this.sessionHistory = new Map();
}
// run 메서드 오버라이드
async run(message: string, sessionId: string): Promise<string> {
// 1. 세션 기록 조회
const history = this.sessionHistory.get(sessionId) || [];
// 2. 로깅
this.logger.info(`[${sessionId}] 요청: ${message}`);
// 3. 컨텍스트 추가
const contextMessage = this.addContext(message, history);
// 4. 부모 클래스의 run 호출
const response = await super.run(contextMessage);
// 5. 기록 저장
history.push({ role: 'user', content: message });
history.push({ role: 'assistant', content: response });
this.sessionHistory.set(sessionId, history);
// 6. 후처리
return this.postProcess(response);
}
private addContext(message: string, history: Message[]): string {
// 이전 대화 3개만 포함
const recentHistory = history.slice(-6);
return `${recentHistory.map(m => `${m.role}: ${m.content}`).join('\n')}\n\nuser: ${message}`;
}
private postProcess(response: string): string {
// 금지어 필터링
return response.replace(/비속어/g, '***');
}
}
// 사용 예시
const agent = new CustomerSupportAgent({
model: 'gpt-4',
tools: [faqTool, ticketTool],
systemPrompt: '당신은 고객 지원 담당자입니다.'
});
const answer = await agent.run('환불 방법이 궁금합니다', 'session-123');
console.log(answer);
김개발 씨는 요구사항 문서를 펼쳤습니다. "고객 상담 에이전트를 만들어야 하는데, 세션 관리, 로깅, 금지어 필터링이 필요합니다." 복잡해 보였습니다.
박시니어 씨가 말했습니다. "걱정 마.
하나씩 추가하면 돼." BaseAgent 상속하기 커스텀 에이전트를 만드는 첫 단계는 상속입니다. extends BaseAgent로 기본 기능을 물려받습니다.
이렇게 하면 tools, model, systemPrompt 같은 기본 속성들을 그대로 쓸 수 있습니다. 바퀴를 다시 발명할 필요가 없습니다.
run() 메서드 오버라이드 핵심은 run() 메서드입니다. 이 메서드가 에이전트의 두뇌입니다.
위 코드를 보면, async run(message, sessionId)로 시그니처를 정의했습니다. 기본 run()은 메시지만 받지만, 우리는 세션 ID도 받습니다.
전처리 로직 추가 run() 메서드 안에서 먼저 전처리를 합니다. 세션 기록을 Map에서 조회합니다.
없으면 빈 배열을 씁니다. 그다음 로거로 요청을 기록합니다.
나중에 디버깅할 때 유용합니다. addContext() 메서드는 이전 대화를 메시지에 추가합니다.
AI가 문맥을 이해할 수 있게 하기 위함입니다. 부모 메서드 호출 중요한 부분입니다.
super.run(contextMessage)로 부모 클래스의 메서드를 호출합니다. 부모 클래스가 실제 AI 모델을 호출하고 도구를 실행합니다.
우리는 그 앞뒤로 커스텀 로직을 끼워 넣는 것입니다. 이것을 템플릿 메서드 패턴이라고 합니다.
기본 흐름은 부모가 제공하고, 세부 동작은 자식이 커스터마이징합니다. 후처리 로직 AI 응답을 받은 후에도 할 일이 있습니다.
먼저 대화 기록을 업데이트합니다. 사용자 메시지와 AI 응답을 모두 저장합니다.
다음 요청에서 이 기록을 쓸 것입니다. postProcess() 메서드는 금지어를 필터링합니다.
만약 AI가 부적절한 단어를 생성했다면, ***로 마스킹합니다. 세션 관리의 중요성 왜 세션을 관리할까요?
고객 상담에서는 문맥이 중요하기 때문입니다. 고객이 "그거 언제 도착하나요?"라고 물었다면, "그거"가 뭔지 알아야 답변할 수 있습니다.
이전 대화에서 "노트북을 주문했어요"라는 내용이 있었다면, AI는 노트북 배송 상태를 조회해야 합니다. 세션 ID로 각 고객의 대화를 분리해서 관리합니다.
로깅 전략 실무에서 로깅은 필수입니다. 문제가 생겼을 때 무슨 일이 있었는지 추적해야 합니다.
Logger 클래스를 만들어서 모든 요청과 응답을 기록합니다. 나중에 "왜 이 고객에게 이상한 답변을 했을까?"를 분석할 수 있습니다.
로그에는 타임스탬프, 세션 ID, 메시지, 응답을 포함시킵니다. 필요하면 외부 로그 서비스(Datadog, Sentry 등)로 전송합니다.
미들웨어 패턴 코드가 더 복잡해지면 미들웨어 패턴을 쓸 수 있습니다: typescript class CustomerSupportAgent extends BaseAgent { private middlewares: Middleware[] = []; use(middleware: Middleware) { this.middlewares.push(middleware); } async run(message: string, sessionId: string) { let context = { message, sessionId }; // 미들웨어 체인 실행 for (const mw of this.middlewares) { context = await mw(context); } return await super.run(context.message); } } // 사용 agent.use(loggingMiddleware); agent.use(contextMiddleware); agent.use(filterMiddleware); 각 미들웨어가 독립적인 기능을 담당합니다. 조합해서 쓸 수 있어서 유연합니다.
테스트하기 커스텀 에이전트는 반드시 테스트해야 합니다. 단위 테스트를 작성합니다: typescript describe('CustomerSupportAgent', () => { it('세션 기록을 저장해야 한다', async () => { const agent = new CustomerSupportAgent({...}); await agent.run('안녕하세요', 'session-1'); await agent.run('환불해주세요', 'session-1'); const history = agent.getHistory('session-1'); expect(history).toHaveLength(4); // 2개 요청 + 2개 응답 }); }); 실무 배포 전 체크리스트 커스텀 에이전트를 배포하기 전에 확인할 것들: - 에러 처리가 제대로 되어 있나?
- 로그가 민감 정보를 포함하지 않나? - 세션 기록이 무한정 쌓이지 않나?
(메모리 누수 방지) - 동시 요청을 처리할 수 있나? 김개발 씨의 완성 김개발 씨는 CustomerSupportAgent를 완성했습니다.
테스트를 돌려보니 모두 통과했습니다. "와, 정말 작동하네요!" 박시니어 씨가 웃으며 말했습니다.
"이제 진짜 에이전트 개발자 됐어. 축하해!" 김개발 씨는 뿌듯했습니다.
처음엔 막막했던 Pi Agent Runtime이 이제는 익숙해졌습니다. 앞으로 어떤 에이전트든 만들 수 있을 것 같았습니다.
마치며 Pi Agent Runtime은 강력하면서도 유연한 프레임워크입니다. BaseAgent를 상속받기만 하면, 무한한 가능성이 열립니다.
여러분도 오늘 배운 내용을 바탕으로 자신만의 에이전트를 만들어보세요. 처음에는 작고 간단하게 시작하세요.
점진적으로 기능을 추가하다 보면, 어느새 멋진 AI 에이전트가 완성되어 있을 것입니다.
실전 팁
💡 - 처음엔 간단한 기능부터 구현하고 점진적으로 확장하세요
- 미들웨어 패턴을 활용하면 코드가 깔끔해집니다
- 반드시 테스트 코드를 작성하세요
- 세션 관리 시 메모리 누수에 주의하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
UX와 협업 패턴 완벽 가이드
AI 에이전트와 사용자 간의 효과적인 협업을 위한 UX 패턴을 다룹니다. 프롬프트 핸드오프부터 인터럽트 처리까지, 현대적인 에이전트 시스템 설계의 핵심을 배웁니다.
Dual-Track 스트리밍 생성 완벽 가이드
실시간 음성 대화 시스템의 핵심인 Dual-Track 스트리밍 아키텍처를 다룹니다. 97ms의 초저지연을 달성하는 방법과 스트리밍 TTS 구현 기법을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다.
AI 에이전트의 Task Decomposition & Planning 완벽 가이드
AI 에이전트가 복잡한 작업을 어떻게 분해하고 계획하는지 알아봅니다. 작업 분해 전략부터 동적 재계획까지, 에이전트 개발의 핵심 개념을 실무 예제와 함께 쉽게 설명합니다.
에이전트 강화 미세조정 RFT 완벽 가이드
AI 에이전트가 스스로 학습하고 적응하는 강화 미세조정(RFT) 기법을 알아봅니다. 온라인/오프라인 학습부터 A/B 테스팅까지 실무에서 바로 적용할 수 있는 핵심 개념을 다룹니다.
자가 치유 및 재시도 패턴 완벽 가이드
AI 에이전트와 분산 시스템에서 필수적인 자가 치유 패턴을 다룹니다. 에러 감지부터 서킷 브레이커까지, 시스템을 스스로 복구하는 탄력적인 코드 작성법을 배워봅니다.