오픈 소스 CRM을 AI-Native로 만들기: 프로덕션 환경에서의 laravel/ai 활용
요약
오픈 소스 CRM인 Relaticle에 laravel/ai 패키지를 활용하여 AI 에이전트를 통합하는 실전 사례를 다룹니다. 단순한 프롬프트 작성을 넘어, 프로덕션 환경에서 스트리밍 안정성, 재연결 처리, 그리고 인간의 승인을 거치는 신뢰할 수 있는 쓰기 작업 구현의 중요성을 설명합니다.
핵심 포인트
- laravel/ai를 활용한 앱 내 에이전트(in-app agent) 구현 방법
- REST API, MCP, 에이전트가 동일한 액션 클래스를 공유하는 아키텍처 설계
- 프로덕션 환경에서의 스트리밍 안정성 및 재연결(resumable) 처리 전략
- AI의 쓰기 작업에 대한 인간의 승인 프로세스 구축
오픈 소스 CRM을 AI-Native로 만들기: 프로덕션 환경에서의 laravel/ai 활용
1년 전, 저는 Laravel과 Filament를 기반으로 한 무료 오픈 소스 CRM인 Relaticle을 구축하는 것에 대해 글을 썼습니다. 그 이후로 모든 채널에서 끊임없이 돌아오는 요청이 하나 있었습니다. 바로 AI였습니다.
지난주에 우리는 v3.3을 출시했습니다. 바로 "Ask Relaticle"입니다. 이는 앱 내 에이전트(in-app agent)로, 모든 쓰기 작업에 대해 인간의 승인을 거친 후 CRM을 읽고 쓸 수 있습니다. 이 글은 우리가 시작했을 때 존재했으면 좋았을 내용입니다. 즉, 완전히 새로운 퍼스트 파티(first-party) laravel/ai 패키지를 프로덕션(production) 환경에 적용하는 데 실제로 무엇이 필요한지, 그리고 진짜 어려움이 어디에 있는지에 대한 내용입니다 (스포일러: 프롬프트(prompts)가 아닙니다).
MCP가 존재하는데 왜 내장형 에이전트가 필요한가
Relaticle은 이미 MCP 서버(Sanctum을 통한 30개의 도구 및 팀별 격리 기능 제공)를 노출하고 있으므로, Claude나 ChatGPT가 원격으로 이를 제어할 수 있습니다. 이는 에이전트 네이티브(agent-native)를 사용하는 소수 사용자를 위한 기능입니다. 내장형 채팅은 그 외의 모든 사람을 위해 존재합니다. 즉, Claude Desktop을 절대 열지 않겠지만, Cmd+J를 누르고 "회사를 생성하고 연락처를 추가해줘"라고 기꺼이 입력할 영업 사원들을 위한 것입니다.
이 시스템을 안정적으로 유지해 준 아키텍처 규칙은 다음과 같습니다: REST API, MCP 도구, 그리고 앱 내 에이전트가 모두 동일한 액션 클래스(action classes)를 호출한다는 점입니다. 요청자가 Filament 폼의 인간이든, HTTP 클라이언트든, 혹은 변경을 제안하는 모델이든 상관없이 비즈니스 로직, 활동 로깅(activity logging), 알림(notifications), 그리고 테넌트 스코핑(tenant scoping)을 위한 단일 코드 경로를 사용합니다.
에이전트 자체는 쉬운 부분이다
에이전트 정의 전체는 단 하나의 클래스로 이루어집니다. laravel/ai는 어트리뷰트(attributes)와 컨트랙트(contracts)를 통해 핵심적인 작업을 수행합니다:
#[Provider(['anthropic', 'openai'])]
#[MaxSteps(15)]
...
RemembersConversations는 즉시 지속적인 히스토리(persistent history)를 제공합니다. #[Provider]는 에이전트를 프로바이더에 구애받지 않게(provider-agnostic) 만듭니다. 사용자는 대화마다 Claude 또는 GPT를 선택할 수 있으며, 셀프 호스팅(self-hosting) 시에는 자신의 키를 가져와 사용할 수 있습니다.
이것은 아마 하루 정도의 작업일 것입니다. 아래의 모든 내용은 나머지 6주 동안 진행된 작업들입니다.
현실에서 살아남는 스트리밍(Streaming)
채팅은 Reverb를 통해 스트리밍되는 큐 작업(queued job, Horizon)으로 실행됩니다. 데모 영상의 세계에서는 그것으로 이야기가 끝납니다. 하지만 프로덕션 환경에서는 다음과 같습니다:
- 사용자가 스트리밍 도중 페이지를 새로고침합니다.
- Websockets 연결이 끊겼다가 재연결됩니다.
- Livewire가 불편한 순간에 다시 렌더링됩니다.
모든 스트림은 클라이언트가 재연결 후 이미 렌더링된 내용을 조정할 수 있도록 **식별자(identity)**가 필요하며, 연속적인 작업은 **재개 가능(resumable)**해야 합니다. 답변 도중에 새로고침이 발생하더라도, 절반만 작성된 응답을 유기하는 것이 아니라 스트림을 다시 시작해야 합니다.
신뢰할 수 있는 쓰기: 승인 파이프라인
에이전트는 직접 글을 쓰지 않습니다. 툴(Tools)은 _제안(proposals)_을 방출하고, 사용자는 승인 카드(approval card)를 보고 결정합니다. 간단하게 들리지만, 눈에 띄지 않는 부분이 있습니다:
멱등성 있는 승인(Idempotent approvals). 승인은 HTTP 요청이며, HTTP 요청은 재시도됩니다. 만약
Relaticle의 레코드는 사용자 정의 커스텀 필드(custom fields)를 가지고 있으며, 모든 팀의 스키마(schema)는 서로 다릅니다. 따라서 대부분의 에이전트 데모(agent demos)처럼 도구의 JSON 스키마를 하드코딩할 수 없습니다.
우리의 접근 방식은 다음과 같습니다. 런타임(runtime) 시점에 테넌트(tenant)별 커스텀 필드 스키마의 설명(필드 코드, 타입, 옵션 레이블 등)을 프롬프트(prompt)에 인라인(inline)으로 삽입하고, 검증(validation) 시점에 옵션 레이블을 다시 옵션 ID로 변환합니다. 이를 통해 얻는 이점은 관리자 UI에서 필드를 추가하기만 하면, 필드별 추가 코드 작성 없이 즉시 채팅(및 MCP 클라이언트)에서 사용할 수 있다는 점입니다.
현장에서 얻은 프로바이더(Provider) 관련 참고 사항
Gemini는 의도적으로 제외되었습니다. 현재 드라이버(driver)가 프로바이더 옵션을 generationConfig로 병합하기 때문에 function_calling_config를 설정할 수 없습니다. 그리고 이 설정 없이는 우리의 순차적 쓰기 가드(sequential-write guard)를 강제할 수 없습니다. 프로바이더마다 다르게 동작하는 에이전트를 출시하기보다는, 드라이버가 이를 지원할 때까지 목록을 제한했습니다.
Anthropic의 프롬프트 캐싱(prompt caching)은 설정 플래그 하나로 가능하며 그만한 가치가 있습니다. 멀티 턴(multi-turn) 에이전트 대화는 대규모 시스템 프롬프트(특히 테넌트별 스키마 주입이 포함된 경우)를 반복해서 전송합니다. 캐싱을 활성화하면 멀티 턴 입력 토큰(input tokens)을 획기적으로 줄일 수 있습니다:
'anthropic_prompt_caching' => (bool) env('CHAT_ANTHROPIC_PROMPT_CACHING', true),
정직한 실패가 조용한 실패보다 낫다
속도 제한(Rate limits)은 발생하기 마련입니다. 프로바이더(Providers)는 가끔 문제를 일으킵니다. 에이전트가 할 수 있는 최악의 행동은 에러를 삼켜버리고 사용자가 멈춰버린 커서만 바라보게 만드는 것입니다.
모든 실패 모드는 UI에서 "재시도 중(retrying)", "실패 — 재개하시겠습니까?(failed — resume?)"와 같은 명시적인 상태로 나타나며, 재개 시 대화를 처음부터 다시 시작하는 대신 이어서 진행합니다. 작업이 중간에 완료되지 않은 채 조용히 사라지는 것은 에이전트가 아예 없는 것보다 에이전트에 대한 신뢰를 더 빠르게 무너뜨립니다. 이는 사후 고려 사항이 아닌 설계 단계에서의 결정이었으며, 이벤트 모델(이벤트 스트림 실패 / 스트림 재시도 / 채팅 일시 중지는 로그 라인이 아닌 일급 객체(first-class events)임)을 형성하는 데 영향을 미쳤습니다.
시작하기 전에 당신에게 해주고 싶은 말
- 분산 시스템의 위생(distributed-systems hygiene)에 대부분의 시간을 할애하세요: 스트림 식별(stream identity), 재개 가능성(resumability), 멱등성(idempotency), 테넌트 스코핑(tenant scoping) 등이 해당됩니다. LLM 부분은 데모일 뿐이며, 이것이 바로 제품입니다.
- 쓰기 작업(writes)에 대해서는 기본적으로 사람의 승인을 거치도록 하세요. 모델이 잘못된 레코드 업데이트를 자신 있게 제안하는 것을 본 이후로, 우리는 타인의 매출 데이터에 대해 '기본 승인'을 유일하게 정직한 기본 설정으로 간주합니다.
- 실패 상태(failure states)를 일급 객체(first-class)로 만드세요. 사용자는 첫날부터 속도 제한(rate limits)에 걸리게 될 것입니다.
- 도메인에 동적 스키마(dynamic schemas)가 있다면, 프롬프트 인젝션(prompt injection)을 조기에 설계하세요. 이는 도구 정의(tool definitions)를 생각하는 방식을 변화시킵니다.
이 포스트의 모든 내용은 리포지토리(repo)에서 읽어볼 수 있습니다. 채팅 기능은 packages/Chat에 있으며, 실제로 오후 한나절이면 다 읽을 수 있는 분량입니다: https://github.com/relaticle/relaticle
프로덕션 환경에서의 laravel/ai에 대한 질문은 언제든 환영합니다. 아직 이에 대한 실제 사례 자료가 많지 않기에, 위의 내용 중 어떤 부분이라도 더 깊이 있게 다룰 준비가 되어 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기