원문 캡처 · rubyllm.com
원문 캡처 · rubyllm.com

RubyLLM 2.1은 MCP 클라이언트, 유형이 지정된 판정, 에이전트 평가, OpenTelemetry 추적 기능을 추가하고 호출마다 수행하는 작업을 줄였다. Ruby 3.2 이상에서 사용할 수 있으며, Rails 앱은 업그레이드 명령으로 새 데이터베이스 스키마를 적용해야 한다.

기본 성능 개선

대부분의 성능 개선은 코드를 바꾸지 않아도 적용된다. 스트림 이벤트는 RubyLLM 전용 파서로 처리하고, 긴 텍스트와 추론 내용은 제자리에서 덧붙인다. 각 청크에 반복되는 인용은 한 번에 중복을 제거하며, 비용도 청크마다 계산하는 대신 스트림이 끝날 때 한 번 계산한다. 요청은 메시지마다 다시 확인하지 않고 프로토콜을 한 번 선택하고, 도구 이름을 한 번 도출하며, 성공 응답도 한 번 파싱한다.

응답 객체가 자신이 답한 요청을 더는 보관하지 않아 긴 대화에서 대화 기록을 한 벌만 유지한다. 요청 내용을 읽는 기능은 요청 전 요청 읽기를 참고하면 된다. 여러 스레드가 동시에 모델 레지스트리에 접근하면 한 번만 불러오며, Vertex AI 호출은 각자 토큰을 가져오는 대신 OAuth 토큰 하나를 공유한다. 저장된 대화는 이미 불러온 행을 이용해 다시 구성한다. automatic_scope_inversing 도입 전 앱에서는 데이터베이스 질의가 줄고, 저장 파일은 요청에 실제로 첨부할 때만 내려받는다.

아래 수치는 같은 컴퓨터에서 2.0.0과 2.1을 비교한 결과다. 제공업체는 미리 준비된 응답을 반환하도록 해 RubyLLM 자체 작업 시간을 측정했다.

작업2.02.1
Anthropic에서 텍스트 델타 500개 스트리밍9.8 ms4.1 ms
16 KB 조각으로 도착하는 2 MB 이벤트 스트리밍116 ms2.0 ms
출처 20개씩 인용하는 Perplexity 청크 500개 스트리밍137 ms17 ms
메시지 기록 200개가 있는 Bedrock 대화 요청2.9 ms0.40 ms
텍스트 100개 임베딩 후 2.4 MB 응답 읽기4.5 ms2.2 ms
256 KB 이미지가 포함된 40턴 스트리밍 대화의 메모리 사용량28 MB0.48 MB
스레드 8개가 동시에 모델 레지스트리 불러오기273 ms32 ms
서비스 계정 키로 Vertex AI 20회 호출토큰 요청 20회토큰 요청 1회
저장된 메시지 100개 대화 재구성11.4 ms6.8 ms
automatic_scope_inversing이 없는 같은 대화질의 58회질의 8회
1 MB 이미지 10개가 있는 대화에서 awaiting_approval? 확인다운로드 10회다운로드 없음

추가로 두 가지 성능 개선은 사용자가 직접 설정해야 한다. 호출 간 HTTP 연결을 재사용하므로 연결을 유지하는 어댑터를 쓰면 첫 호출 뒤에는 TCP와 TLS 핸드셰이크를 건너뛸 수 있다. Puma와 Sidekiq처럼 스레드를 사용하는 경우 faraday-net_http_persistent gem의 :net_http_persistent를, Falcon처럼 Async 리액터 안에서는 async-http-faraday의 :async_http를 사용한다.

ruby
RubyLLM.configure do |config|
 config.faraday_adapter = :net_http_persistent
end

로컬 HTTPS 서버에 :net_http_persistent로 20회 호출한 측정에서는 핸드셰이크가 20회에서 1회로 줄었고, 호출당 시간은 2.1 ms에서 0.4 ms로 줄었다. 실제 네트워크에서는 건너뛴 핸드셰이크마다 왕복 시간도 절약된다. 자세한 내용은 연결 재사용을 참고하면 된다.

Rails에서는 2.1 업그레이드를 실행해야 대화가 저장 파일의 제공업체 업로드 정보를 기억한다. 그러면 다른 프로세스에서 불러온 대화가 큰 파일을 다시 내려받고 업로드하는 대신 제공업체에 이미 올라간 파일을 보낸다. 새 프로세스에서 각각 처리한 30 MB PDF 대화 5턴의 경우, 2.0은 총 150 MB를 업로드했지만 2.1은 업로드하지 않았다.

자신의 컴퓨터에서 측정하려면 RubyLLM을 복제한 뒤 원하는 버전과 현재 체크아웃을 비교할 수 있다. 벤치마크는 API 키나 네트워크 연결이 필요하지 않다.

sh
bundle exec rake "benchmark:compare[v2.0.0]"

위 수치는 Ruby 4.0.7을 실행하는 AMD Ryzen 9 9900X에서 측정한 중앙값이다. 실행 시간은 컴퓨터마다 다르지만 질의·다운로드·업로드 횟수 같은 수치는 달라지지 않는다.

MCP 클라이언트와 도구 진행 상황

Linear, GitHub, Notion, Slack, Dropbox 같은 서비스는 API를 Model Context Protocol(MCP) 서버로 제공한다. RubyLLM 2.1에는 MCP 클라이언트가 포함됐다. 도구나 에이전트를 정의하듯 클래스로 연결할 서버를 선언할 수 있다.

ruby
class Linear < RubyLLM::MCP
 url "https://mcp.linear.app/mcp"
 inputs :user
 oauth owner: :user

 only :list_issues, :get_issue, :create_issue
 requires_approval :create_issue
end

chat = RubyLLM.chat.with_mcp(Linear.new(user: current_user))
chat.ask "What's blocking the release?"

서버의 각 도구는 Ruby 메서드로도 쓸 수 있어 콘솔에서 서버를 살펴볼 수 있다.

ruby
docs = RubyLLM.mcp(url: "https://learn.microsoft.com/api/mcp")
docs.tools
docs.microsoft_docs_search(query: "Azure Blob Storage").text

모델에 어떤 도구를 보여줄지는 앱이 결정한다. 도구 이름과 설명을 바꾸고, 모델이 선택해서는 안 되는 인수를 고정하거나, 결과를 자체 메서드로 전달할 수 있다. 일반 RubyLLM::Tool로 서버의 기본 기능을 조합해 상위 수준 도구를 만들 수도 있다. 리소스는 첨부 파일처럼, 프롬프트는 ask와 함께 쓸 수 있다. 서버가 입력을 요청하면 도구 승인 때처럼 대화가 멈춘다. Rails에서는 재시작 뒤에도 이 상태가 유지된다.

사용 중 서버의 도구가 바뀌면 대화의 다음 턴부터 새 목록을 사용한다. listen으로 도구 변경뿐 아니라 관심 있는 리소스와 작업의 업데이트도 받을 수 있으며, after_change로 처리할 수 있다.

ruby
class Handbook < RubyLLM::MCP
 url "https://handbook.example.com/mcp"
 after_change { |change| ReindexPolicyJob.perform_later(change.uri) if change.is_a?(RubyLLM::MCP::Resource) }
end

Handbook.new.listen(resources: ["handbook://policies"])

자세한 내용은 변경 사항 듣기를 참고하면 된다.

클라이언트는 프로토콜의 2026-07-28 개정판을 사용하며 이전 버전의 서버에는 호환 방식으로 연결한다. OAuth는 MCP 인증 사양을 따르고, Rails에서는 인증 정보를 암호화해 저장한다. 백그라운드 작업은 클라이언트 자격 증명, 개인 키 또는 워크로드 아이덴티티 토큰을 이용해 앱 자체 계정으로 연결할 수 있다. 기업은 자체 아이덴티티 제공업체를 통해 사용자를 인증할 수 있으며, DPoP가 필요한 서버에는 키에 바인딩된 토큰을 제공한다.

앱이 지원하는 프로토콜 확장은 extension으로 선언한다. RubyLLM은 두 가지 확장을 구현한다. MCP Apps를 사용하면 도구가 결과 옆에 앱이 렌더링할 UI를 제공한다. 각 도구 호출은 UI에 필요한 결과도 저장해 페이지를 새로고침한 뒤 UI를 다시 그릴 수 있다. Tasks에서는 서버가 긴 도구 호출을 백그라운드에서 실행하고 대화는 승인 대기처럼 멈춘다. 따라서 작업이 끝날 때까지 잡을 붙잡아 두지 않는다.

ruby
class Reports < RubyLLM::MCP
 url "https://reports.example.com/mcp"
 extension :apps
 extension :tasks
end

도구·결과·리소스는 확장이 기록한 _meta도 노출하며, log_level을 사용하면 서버 로그 메시지를 앱 로그에 포함할 수 있다. 자세한 내용은 MCP 클라이언트를 참고하면 된다.

큰 파일을 내려받거나 스캔 문서를 읽는 도구는 실행에 시간이 걸릴 수 있다. 이제 도구가 처리 중인 내용을 알리고 앱은 결과가 오기 전에 이를 표시할 수 있다.

ruby
class ReadReport < RubyLLM::Tool
 def execute(url:)
 progress "Downloading #{File.basename(url)}"
 pages = Scanner.pages(url)
 pages.each_with_index.map do |page, index|
 progress "Reading page #{index + 1} of #{pages.size}", value: index + 1, total: pages.size
 page.text
 end.join("\n")
 end
end

chat.with_tools(ReadReport).after_tool_progress do |tool_call, progress|
 puts "#{tool_call.name}: #{progress.message}"
end

MCP 서버 도구의 진행 상황도 같은 콜백으로 전달된다. 동시 도구 실행, 에이전트, Rails 대화 레코드에서 사용할 수 있다. 자세한 내용은 진행 상황 보고를 참고하면 된다.

도구 검색과 대화 중 제공업체 전환

일부 MCP 서버는 수백 개의 도구 정의를 매 턴 모델에 전달할 수 있다. 도구를 미리 전달하지 않고 모델이 필요한 도구를 검색하게 할 수 있다.

ruby
chat.with_mcp(GitHub, Linear, defer: true)
chat.ask "Which open issues mention the flaky login spec?"

with_tools, 에이전트의 tools·mcp 매크로, Rails 대화 레코드도 같은 defer: 옵션을 지원한다. 도구 클래스와 MCP 클래스에서는 defer로 선언할 수 있다. Anthropic과 OpenAI Responses API는 자체 검색 기능을 사용한다. 다른 제공업체에는 지연된 도구를 일반 도구로 전달하므로 같은 코드를 여러 제공업체에서 실행할 수 있다. 자세한 내용은 도구 검색을 참고하면 된다.

대화는 도구 실행 단계 중간에도 다른 모델이나 제공업체로 옮길 수 있다.

ruby
chat = RubyLLM.chat(model: "claude-sonnet-5").with_provider_tools(:web_search)
chat.ask "What changed in the latest Ruby release?"

chat.with_model("gpt-5.6-luna")
chat.ask "Which of those changes affect Rails apps?"

추론 서명, 제공업체별 도구 단계, 압축 데이터와 그 밖의 제공업체별 형식은 이를 만든 모델에만 다시 전달된다. 다른 모델에는 같은 대화를 텍스트와 도구 호출 형태로 보내므로 읽을 수 없는 블록을 받지 않는다. 이는 한 제공업체 안에서도 중요하다. Bedrock, Vertex AI, Azure, OpenRouter는 서로의 추론 형식을 거부하는 모델 계열을 제공한다. Gemini는 다른 제공업체에서 시작한 도구 실행 단계도 이어받을 수 있다. 예를 들어 승인을 기다리는 동안 사용자가 모델을 바꾼 경우다.

작업자가 턴 중간에 종료돼도 제공업체가 거부하는 대화 상태가 남지 않는다. 요청에서 저장된 빈 응답을 제외하고, 대화가 이미 지나온 턴의 도구 호출은 완료되지 않은 상태로 모델에 전달한다. 자세한 내용은 지속성 흐름 이해하기를 참고하면 된다.

첨부 파일, 이미지 해상도, 캐시

일부 프로토콜은 Word 문서처럼 모든 파일 형식을 전송할 수 없다. 직접 변환하면 RubyLLM은 변환한 파일을 보내고 대화에는 원본을 유지한다.

ruby
chat.convert_unsupported_attachments do |attachment|
 text = TextExtraction.call(attachment)
 RubyLLM::Attachment.new(StringIO.new(text), filename: "extracted.txt") if text
end

chat.ask "Summarize this report.", with: "report.docx"

블록은 현재 프로토콜이 전송할 수 없는 파일에만, 파일마다 한 번 실행된다. 원본을 읽을 수 있는 제공업체로 바꾸면 원본 파일을 보낸다. 자세한 내용은 지원되지 않는 첨부 파일 변환을 참고하면 된다.

작은 글씨를 읽으려면 사진보다 더 세밀한 이미지가 필요할 수 있다. 첨부 파일에 resolution:을 설정하면 토큰 사용량과 세부 묘사 수준을 조정할 수 있다. :low부터 :ultra_high까지 설정하거나 이미지를 있는 그대로 전달하는 :original을 쓸 수 있다.

ruby
page = RubyLLM::Attachment.new("page-3.png", resolution: :original)
chat.ask "What is the revenue figure?", with: page

Gemini, OpenAI, Azure는 이 설정을 자체 방식으로 변환한다. 다른 제공업체는 무시하며 Rails는 저장 파일과 함께 설정을 보관한다. 자세한 내용은 미디어 해상도 선택을 참고하면 된다.

안정적인 프롬프트 앞부분과 계속 늘어나는 대화에는 서로 다른 캐시 수명이 적합할 수 있다. 경계마다 ttl:을 설정하면 나머지 대화에는 with_caching의 수명이 적용된다.

ruby
chat = RubyLLM.chat.with_caching
chat.with_instructions(policy).cache_until_here(ttl: "1h")
chat.ask "Summarize today's tickets."

Anthropic, OpenRouter, Bedrock Converse는 각 경계에 수명을 설정하며 Rails는 이를 메시지와 함께 보관한다. 1시간 캐시 쓰기는 Anthropic, Vertex AI의 Claude, Bedrock에서 입력 가격의 두 배가 든다. tokens.cache_write_by_ttl은 수명별 쓰기 토큰을 나누고, cost.cache_write는 각 쓰기에 해당 요금을 적용한다. 자세한 내용은 명시적 캐시 경계를 참고하면 된다.

프롬프트 부분 템플릿과 유형이 지정된 판정

프롬프트 템플릿은 Action View처럼 부분 템플릿을 렌더링하므로 지시문을 여러 템플릿에서 공유할 수 있다.

erb
<%# app/prompts/work_assistant/instructions.txt.erb %>
<%= render "tone", display_name: display_name %>
<%= render "shared/safety" %>

경로가 없는 이름은 현재 프롬프트 옆에서 찾고, 경로가 있는 이름은 프롬프트 루트에서 찾는다. local_assigns로 선택적 지역 변수를 읽을 수 있다. 자세한 내용은 부분 템플릿을 참고하면 된다.

앱 데이터에 관해 질문을 정의하고 확률, 선택지, 점수를 읽을 수 있다.

ruby
class TicketTriage < RubyLLM::Judge
 probability :urgent, "Does this need attention today?"

 choice :department, "Which team should handle this?" do
 billing "Payments and refunds"
 technical "Bugs and integrations"
 other "Everything else"
 end
end

judgment = TicketTriage.judge("Please refund the duplicate charge today.")
judgment.urgent.probability
judgment.department.choice

모델을 별도로 지정하지 않으면 판정기는 config.default_judgment_model을 사용한다. 구조화된 입력, 재사용 가능한 정의, 실행 시점의 프로시저를 받을 수 있다. 선택지와 점수 응답에는 전체 분포와 신뢰도가 포함돼 앱이 처리 방식을 결정할 수 있다. 자세한 내용은 판정을 참고하면 된다.

TypeSafe가 기본 제공업체 목록에 들어오면서 제공업체 수는 18개가 됐다. 호스팅 Jev 모델이나 Jev 호환 로컬 서버를 같은 판정 API로 사용할 수 있다. OpenAI의 gpt-6-luna는 OpenAI Decisions를 통해 같은 질문에 답하며, with:로 전달한 이미지 관련 질문도 지원한다. 로컬 판정을 원하면 Cloudflare의 Clef를 Ollama에서 provider: :ollama로 실행할 수 있다.

에이전트 평가와 추적

에이전트가 정답을 얼마나 자주 맞히는지 평가하려면 에이전트를 실행하는 클래스와 기준 답변이 있는 사례 데이터셋을 작성한다.

ruby
# app/evals/support_evaluation.rb
class SupportEvaluation < RubyLLM::Evaluation
 def perform(input)
 SupportAgent.new.ask(input)
 end
end
yaml
# app/evals/support_evaluation.yml
cases:
 - name: unopened_return
 inputs: Can I return an unopened item after 14 days?
 expected_output: Yes, unopened items can be returned within 30 days.
sh
bin/rails "ruby_llm:eval[SupportEvaluation]"

RubyLLM은 모델을 사용해 각 답변을 기준 답변과 비교하며, 의미가 같으면 표현이 달라도 정답으로 처리한다. 자체 평가 기준을 선언하고 Agent나 Judge로 채점하거나 도구 호출에 Ruby 어설션을 추가할 수 있다. 같은 사례를 RSpec 또는 Minitest 테스트로 실행하는 것도 가능하다. 보고서에는 각 판정과 이유, 실행에 사용한 토큰과 비용이 포함된다. 자세한 내용은 평가를 참고하면 된다.

모델 호출, 도구 실행, 워크플로를 OpenTelemetry GenAI 규약에 맞는 스팬으로 추적 백엔드에 보낼 수 있다.

ruby
RubyLLM::OpenTelemetry.enable

RubyLLM은 앱이 설정한 SDK와 익스포터를 사용한다. 모델, 토큰 수, 종료 이유 등의 메타데이터를 내보내지만 프롬프트와 응답은 내보내지 않는다. 자세한 내용은 OpenTelemetry를 참고하면 된다.

제공업체와 설정 변경

Hetzner가 기본 제공업체에 추가되면서 제공업체 수는 19개가 됐다. Hetzner의 실험용 Inference API는 Hetzner 데이터센터에서 오픈 웨이트 모델을 제공한다.

ruby
RubyLLM.chat(model: "Qwen3.8-27B", provider: :hetzner).ask("Hello from Hetzner")

Hetzner Console에서 받은 토큰을 hetzner_api_key에 설정해야 한다. 자세한 내용은 제공업체 설정을 참고하면 된다.

Azure 모델의 가격과 컨텍스트 길이는 이제 models.dev에서 가져오므로 Azure 대화 비용을 보고할 수 있다. 배포 이름이 실제 모델 이름과 다르면 이를 선언해야 해당 모델의 가격, 한도, 기능을 대화에 적용한다.

ruby
RubyLLM.configure do |config|
 config.azure_deployments = { "gpt-4o-global" => "gpt-4o" }
end

RubyLLM.chat(model: "gpt-4o-global", provider: :azure)

GPT-6 배포는 Responses API에서 실행된다. 자세한 내용은 Azure 배포를 참고하면 된다.

Perplexity는 2026년 9월 27일 Sonar를 종료할 예정이다. 이에 따라 Perplexity 채팅은 이제 fast, high 같은 프리셋을 통해 Agent API를 사용한다. Sonar 모델을 지정한 기존 대화는 Perplexity가 권장하는 프리셋과 지원 중단 경고를 사용해 계속 작동한다. 자세한 내용은 Perplexity 채팅을 프리셋으로 이전을 참고하면 된다.

에이전트는 입력값에 따라 대화마다 설정을 만들 수 있다. 따라서 각 테넌트가 자체 자격 증명이나 엔드포인트를 사용할 수 있다. 인수를 받지 않는 context 블록은 대화를 만들 때 실행된다.

ruby
class SupportAgent < RubyLLM::Agent
 model "gpt-5.6-luna", provider: :openai
 inputs :workspace

 context do
 RubyLLM.context { |config| config.openai_api_key = workspace.openai_api_key }
 end
end

SupportAgent.chat(workspace: current_workspace)

이 설정은 SupportAgent.chat과 Rails 기반 에이전트에서 쓸 수 있으며, Rails에서는 블록이 대화 레코드도 볼 수 있다. 설정을 인수로 받는 기존 형식의 블록은 계속 클래스 정의 시점에 한 번 실행된다. 자세한 내용은 설정 컨텍스트를 참고하면 된다.

RubyLLM의 보조 레코드를 대화와 메시지가 있는 보조 데이터베이스에 함께 저장할 수도 있다. 두 모델이 같은 연결을 사용하도록 설정한다.

ruby
Rails.application.config.to_prepare do
 RubyLLM::ActiveRecord::Record.connection_specification_name =
 LlmRecord.connection_specification_name
end

자세한 내용은 Rails 고급 설정을 참고하면 된다.

사용량 기록과 오류 처리

Rails에서는 RubyLLM.transcribe, RubyLLM.embed 같은 단발성 작업도 사용량 장부에 기록한다. owner: current_user를 지정하거나 RubyLLM.with_usage_owner(current_user) { ... }를 사용해 사용자별로 기록할 수 있다. 비디오와 리서치 작업은 끝날 때 행을 기록하며, 비디오 비용에는 xAI와 OpenRouter가 보고한 금액을 사용한다. 자세한 내용은 단발성 작업을 참고하면 된다.

제공업체 도구 사용량은 모든 곳에서 같은 방식으로 집계된다. 검색을 실행하는 모든 제공업체에서 tokens.server_tool_use가 web_search_requests를 보고한다. 여기에는 OpenAI, Gemini, Perplexity가 포함된다. Rails는 사용량 행마다 횟수를 저장하므로 자체 기록을 바탕으로 비용을 계산할 수 있다. 자세한 내용은 도구 사용량 요금 계산을 참고하면 된다.

제공업체마다 같은 오류를 다르게 알리던 문제를 정리해 RubyLLM이 오류 유형별로 같은 클래스를 발생시킨다.

  • 거부된 API 키는 Gemini, xAI, Bedrock을 포함한 모든 제공업체에서 RubyLLM::UnauthorizedError를 발생시킨다. 이들 제공업체는 오류를 400 또는 403으로 응답하기도 한다.
  • Anthropic 계정의 크레딧이 소진되면 RubyLLM::PaymentRequiredError가 발생한다.
  • 서버 과부하를 나타내는 400 응답은 RubyLLM::OverloadedError가 된다. 따라서 재시도와 대체 제공업체 처리에서 용량 문제로 다룰 수 있다.
  • 토큰 할당량 초과 429 응답은 RubyLLM::RateLimitError가 되고, 모델이 처리하기에 너무 큰 요청은 RubyLLM::ContextLengthExceededError가 된다.

재시도는 제공업체의 retry-after-ms를 따르며, Bedrock은 재시도할 때 요청에 다시 서명한다. 따라서 첫 시도가 오래 걸려도 서명 만료로 재시도가 끝나지 않는 문제를 피한다.

제공업체가 이유를 밝히지 않고 대화를 거부하면 error.request_shape로 각 턴의 구성 요소와 크기를 확인할 수 있다. 내용 자체는 표시하지 않으며, 데이터가 없는 구성 요소처럼 제공업체가 거부하는 것으로 알려진 문제를 보여준다. 자세한 내용은 거부된 요청 설명을 참고하면 된다.

업그레이드와 설치

2.1부터 각 릴리스에는 직전 릴리스에서 올리는 업그레이드가 포함된다. 2.1에서 bin/rails generate ruby_llm:upgrade를 실행하면 MCP 자격 증명 테이블, 입력이나 작업을 기다리는 도구 호출 및 MCP Apps 렌더링 결과를 위한 열, 저장 파일의 제공업체 업로드를 대화가 기억하는 테이블, 각 시도의 제공업체 도구 사용량을 기록하는 열, 메시지별 캐시 수명 열, 대화 밖 사용량의 소유자가 추가된다. 1.x를 사용하는 앱은 먼저 2.0으로 업그레이드해야 한다. 자세한 내용은 업그레이드를 참고하면 된다.

RubyLLM 2.1에는 Ruby 3.2 이상이 필요하다. 다음 명령으로 설치할 수 있다.

sh
bundle add ruby_llm --version "~> 2.1.0"

시작하기를 참고하거나 업그레이드 안내에 따라 2.0 앱을 업데이트하면 된다.