ce-dspy-ruby

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DSPy.rb

DSPy.rb

Build LLM apps like you build software. Type-safe, modular, testable.
DSPy.rb brings software engineering best practices to LLM development. Instead of tweaking prompts, define what you want with Ruby types and let DSPy handle the rest.
像构建普通软件一样构建LLM应用。类型安全、模块化、可测试。
DSPy.rb将软件工程最佳实践引入LLM开发。无需反复调整提示词,只需用Ruby类型定义需求,剩下的交给DSPy处理。

Overview

概述

DSPy.rb is a Ruby framework for building language model applications with programmatic prompts. It provides:
  • Type-safe signatures — Define inputs/outputs with Sorbet types
  • Modular components — Compose and reuse LLM logic
  • Automatic optimization — Use data to improve prompts, not guesswork
  • Production-ready — Built-in observability, testing, and error handling
DSPy.rb是一款用于构建大语言模型应用的Ruby框架,支持程序化提示。它提供以下功能:
  • 类型安全签名 —— 使用Sorbet类型定义输入/输出
  • 模块化组件 —— 组合并复用LLM逻辑
  • 自动优化 —— 借助数据改进提示词,而非凭经验猜测
  • 生产就绪 —— 内置可观测性、测试和错误处理机制

Core Concepts

核心概念

1. Signatures

1. 签名

Define interfaces between your app and LLMs using Ruby types:
ruby
class EmailClassifier < DSPy::Signature
  description "Classify customer support emails by category and priority"

  class Priority < T::Enum
    enums do
      Low = new('low')
      Medium = new('medium')
      High = new('high')
      Urgent = new('urgent')
    end
  end

  input do
    const :email_content, String
    const :sender, String
  end

  output do
    const :category, String
    const :priority, Priority  # Type-safe enum with defined values
    const :confidence, Float
  end
end
使用Ruby类型定义应用与LLM之间的接口:
ruby
class EmailClassifier < DSPy::Signature
  description "Classify customer support emails by category and priority"

  class Priority < T::Enum
    enums do
      Low = new('low')
      Medium = new('medium')
      High = new('high')
      Urgent = new('urgent')
    end
  end

  input do
    const :email_content, String
    const :sender, String
  end

  output do
    const :category, String
    const :priority, Priority  # Type-safe enum with defined values
    const :confidence, Float
  end
end

2. Modules

2. 模块

Build complex workflows from simple building blocks:
  • Predict — Basic LLM calls with signatures
  • ChainOfThought — Step-by-step reasoning
  • ReAct — Tool-using agents
  • CodeAct — Dynamic code generation agents (install the
    dspy-code_act
    gem)
通过简单的构建块搭建复杂工作流:
  • Predict —— 带签名的基础LLM调用
  • ChainOfThought —— 分步推理
  • ReAct —— 可使用工具的智能体
  • CodeAct —— 动态代码生成智能体(需安装
    dspy-code_act
    gem)

3. Tools & Toolsets

3. 工具与工具集

Create type-safe tools for agents with comprehensive Sorbet support:
ruby
undefined
创建具备全面Sorbet支持的类型安全工具供智能体使用:
ruby
undefined

Enum-based tool with automatic type conversion

Enum-based tool with automatic type conversion

class CalculatorTool < DSPy::Tools::Base tool_name 'calculator' tool_description 'Performs arithmetic operations with type-safe enum inputs'
class Operation < T::Enum enums do Add = new('add') Subtract = new('subtract') Multiply = new('multiply') Divide = new('divide') end end
sig { params(operation: Operation, num1: Float, num2: Float).returns(T.any(Float, String)) } def call(operation:, num1:, num2:) case operation when Operation::Add then num1 + num2 when Operation::Subtract then num1 - num2 when Operation::Multiply then num1 * num2 when Operation::Divide return "Error: Division by zero" if num2 == 0 num1 / num2 end end end
class CalculatorTool < DSPy::Tools::Base tool_name 'calculator' tool_description 'Performs arithmetic operations with type-safe enum inputs'
class Operation < T::Enum enums do Add = new('add') Subtract = new('subtract') Multiply = new('multiply') Divide = new('divide') end end
sig { params(operation: Operation, num1: Float, num2: Float).returns(T.any(Float, String)) } def call(operation:, num1:, num2:) case operation when Operation::Add then num1 + num2 when Operation::Subtract then num1 - num2 when Operation::Multiply then num1 * num2 when Operation::Divide return "Error: Division by zero" if num2 == 0 num1 / num2 end end end

Multi-tool toolset with rich types

Multi-tool toolset with rich types

class DataToolset < DSPy::Tools::Toolset toolset_name "data_processing"
class Format < T::Enum enums do JSON = new('json') CSV = new('csv') XML = new('xml') end end
tool :convert, description: "Convert data between formats" tool :validate, description: "Validate data structure"
sig { params(data: String, from: Format, to: Format).returns(String) } def convert(data:, from:, to:) "Converted from #{from.serialize} to #{to.serialize}" end
sig { params(data: String, format: Format).returns(T::Hash[String, T.any(String, Integer, T::Boolean)]) } def validate(data:, format:) { valid: true, format: format.serialize, row_count: 42, message: "Data validation passed" } end end
undefined
class DataToolset < DSPy::Tools::Toolset toolset_name "data_processing"
class Format < T::Enum enums do JSON = new('json') CSV = new('csv') XML = new('xml') end end
tool :convert, description: "Convert data between formats" tool :validate, description: "Validate data structure"
sig { params(data: String, from: Format, to: Format).returns(String) } def convert(data:, from:, to:) "Converted from #{from.serialize} to #{to.serialize}" end
sig { params(data: String, format: Format).returns(T::Hash[String, T.any(String, Integer, T::Boolean)]) } def validate(data:, format:) { valid: true, format: format.serialize, row_count: 42, message: "Data validation passed" } end end
undefined

4. Type System & Discriminators

4. 类型系统与判别器

DSPy.rb uses sophisticated type discrimination for complex data structures:
  • Automatic
    _type
    field injection
    — DSPy adds discriminator fields to structs for type safety
  • Union type support
    T.any()
    types automatically disambiguated by
    _type
  • Reserved field name — Avoid defining your own
    _type
    fields in structs
  • Recursive filtering
    _type
    fields filtered during deserialization at all nesting levels
DSPy.rb为复杂数据结构提供了完善的类型判别机制:
  • 自动注入
    _type
    字段
    —— DSPy会为结构体添加判别器字段以保证类型安全
  • 联合类型支持 ——
    T.any()
    类型会通过
    _type
    自动区分
  • 保留字段名 —— 避免在结构体中自定义
    _type
    字段
  • 递归过滤 —— 反序列化时会在所有嵌套层级过滤
    _type
    字段

5. Optimization

5. 优化

Improve accuracy with real data:
  • MIPROv2 — Advanced multi-prompt optimization with bootstrap sampling and Bayesian optimization
  • GEPA — Genetic-Pareto Reflective Prompt Evolution with feedback maps, experiment tracking, and telemetry
  • Evaluation — Comprehensive framework with built-in and custom metrics, error handling, and batch processing
借助真实数据提升准确率:
  • MIPROv2 —— 高级多提示词优化,支持引导采样和贝叶斯优化
  • GEPA —— 遗传帕累托反射式提示词进化,具备反馈映射、实验跟踪和遥测功能
  • 评估 —— 全面的框架,内置及自定义指标、错误处理和批处理支持

Quick Start

快速开始

ruby
undefined
ruby
undefined

Install

Install

gem 'dspy'
gem 'dspy'

Configure

Configure

DSPy.configure do |c| c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY']) end
DSPy.configure do |c| c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY']) end

Define a task

Define a task

class SentimentAnalysis < DSPy::Signature description "Analyze sentiment of text"
input do const :text, String end
output do const :sentiment, String # positive, negative, neutral const :score, Float # 0.0 to 1.0 end end
class SentimentAnalysis < DSPy::Signature description "Analyze sentiment of text"
input do const :text, String end
output do const :sentiment, String # positive, negative, neutral const :score, Float # 0.0 to 1.0 end end

Use it

Use it

analyzer = DSPy::Predict.new(SentimentAnalysis) result = analyzer.call(text: "This product is amazing!") puts result.sentiment # => "positive" puts result.score # => 0.92
undefined
analyzer = DSPy::Predict.new(SentimentAnalysis) result = analyzer.call(text: "This product is amazing!") puts result.sentiment # => "positive" puts result.score # => 0.92
undefined

Provider Adapter Gems

提供商适配Gem

Two strategies for connecting to LLM providers:
连接LLM提供商的两种策略:

Per-provider adapters (direct SDK access)

按提供商适配(直接SDK访问)

ruby
undefined
ruby
undefined

Gemfile

Gemfile

gem 'dspy' gem 'dspy-openai' # OpenAI, OpenRouter, Ollama gem 'dspy-anthropic' # Claude gem 'dspy-gemini' # Gemini

Each adapter gem pulls in the official SDK (`openai`, `anthropic`, `gemini-ai`).
gem 'dspy' gem 'dspy-openai' # OpenAI, OpenRouter, Ollama gem 'dspy-anthropic' # Claude gem 'dspy-gemini' # Gemini

每个适配Gem都会引入官方SDK(`openai`、`anthropic`、`gemini-ai`)。

Unified adapter via RubyLLM (recommended for multi-provider)

通过RubyLLM统一适配(多提供商推荐)

ruby
undefined
ruby
undefined

Gemfile

Gemfile

gem 'dspy' gem 'dspy-ruby_llm' # Routes to any provider via ruby_llm gem 'ruby_llm'

RubyLLM handles provider routing based on the model name. Use the `ruby_llm/` prefix:

```ruby
DSPy.configure do |c|
  c.lm = DSPy::LM.new('ruby_llm/gemini-2.5-flash', structured_outputs: true)
  # c.lm = DSPy::LM.new('ruby_llm/claude-sonnet-4-20250514', structured_outputs: true)
  # c.lm = DSPy::LM.new('ruby_llm/gpt-4o-mini', structured_outputs: true)
end
gem 'dspy' gem 'dspy-ruby_llm' # 通过ruby_llm路由到任意提供商 gem 'ruby_llm'

RubyLLM会根据模型名称处理提供商路由,使用`ruby_llm/`前缀:

```ruby
DSPy.configure do |c|
  c.lm = DSPy::LM.new('ruby_llm/gemini-2.5-flash', structured_outputs: true)
  # c.lm = DSPy::LM.new('ruby_llm/claude-sonnet-4-20250514', structured_outputs: true)
  # c.lm = DSPy::LM.new('ruby_llm/gpt-4o-mini', structured_outputs: true)
end

Events System

事件系统

DSPy.rb ships with a structured event bus for observing runtime behavior.
DSPy.rb内置结构化事件总线,用于监控运行时行为。

Module-Scoped Subscriptions (preferred for agents)

模块范围订阅(智能体推荐使用)

ruby
class MyAgent < DSPy::Module
  subscribe 'lm.tokens', :track_tokens, scope: :descendants

  def track_tokens(_event, attrs)
    @total_tokens += attrs.fetch(:total_tokens, 0)
  end
end
ruby
class MyAgent < DSPy::Module
  subscribe 'lm.tokens', :track_tokens, scope: :descendants

  def track_tokens(_event, attrs)
    @total_tokens += attrs.fetch(:total_tokens, 0)
  end
end

Global Subscriptions (for observability/integrations)

全局订阅(用于可观测性/集成)

ruby
subscription_id = DSPy.events.subscribe('score.create') do |event, attrs|
  Langfuse.export_score(attrs)
end
ruby
subscription_id = DSPy.events.subscribe('score.create') do |event, attrs|
  Langfuse.export_score(attrs)
end

Wildcards supported

支持通配符

DSPy.events.subscribe('llm.*') { |name, attrs| puts "[#{name}] tokens=#{attrs[:total_tokens]}" }

Event names use dot-separated namespaces (`llm.generate`, `react.iteration_complete`). Every event includes module metadata (`module_path`, `module_leaf`, `module_scope.ancestry_token`) for filtering.
DSPy.events.subscribe('llm.*') { |name, attrs| puts "[#{name}] tokens=#{attrs[:total_tokens]}" }

事件名称使用点分隔的命名空间(如`llm.generate`、`react.iteration_complete`)。每个事件都包含模块元数据(`module_path`、`module_leaf`、`module_scope.ancestry_token`)用于过滤。

Lifecycle Callbacks

生命周期回调

Rails-style lifecycle hooks ship with every
DSPy::Module
:
  • before
    — Runs ahead of
    forward
    for setup (metrics, context loading)
  • around
    — Wraps
    forward
    , calls
    yield
    , and lets you pair setup/teardown logic
  • after
    — Fires after
    forward
    returns for cleanup or persistence
ruby
class InstrumentedModule < DSPy::Module
  before :setup_metrics
  around :manage_context
  after :log_metrics

  def forward(question:)
    @predictor.call(question: question)
  end

  private

  def setup_metrics
    @start_time = Time.now
  end

  def manage_context
    load_context
    result = yield
    save_context
    result
  end

  def log_metrics
    duration = Time.now - @start_time
    Rails.logger.info "Prediction completed in #{duration}s"
  end
end
Execution order: before → around (before yield) → forward → around (after yield) → after. Callbacks are inherited from parent classes and execute in registration order.
每个
DSPy::Module
都支持Rails风格的生命周期钩子:
  • before
    —— 在
    forward
    之前运行,用于初始化(如指标、上下文加载)
  • around
    —— 包裹
    forward
    ,调用
    yield
    ,可配对初始化/清理逻辑
  • after
    —— 在
    forward
    返回后触发,用于清理或持久化
ruby
class InstrumentedModule < DSPy::Module
  before :setup_metrics
  around :manage_context
  after :log_metrics

  def forward(question:)
    @predictor.call(question: question)
  end

  private

  def setup_metrics
    @start_time = Time.now
  end

  def manage_context
    load_context
    result = yield
    save_context
    result
  end

  def log_metrics
    duration = Time.now - @start_time
    Rails.logger.info "Prediction completed in #{duration}s"
  end
end
执行顺序:before → around(yield之前)→ forward → around(yield之后)→ after。回调会从父类继承,并按注册顺序执行。

Fiber-Local LM Context

纤程本地LM上下文

Override the language model temporarily using fiber-local storage:
ruby
fast_model = DSPy::LM.new("openai/gpt-4o-mini", api_key: ENV['OPENAI_API_KEY'])

DSPy.with_lm(fast_model) do
  result = classifier.call(text: "test")  # Uses fast_model inside this block
end
使用纤程本地存储临时覆盖语言模型:
ruby
fast_model = DSPy::LM.new("openai/gpt-4o-mini", api_key: ENV['OPENAI_API_KEY'])

DSPy.with_lm(fast_model) do
  result = classifier.call(text: "test")  # 此代码块内使用fast_model
end

Back to global LM outside the block

代码块外恢复为全局LM


**LM resolution hierarchy**: Instance-level LM → Fiber-local LM (`DSPy.with_lm`) → Global LM (`DSPy.configure`).

Use `configure_predictor` for fine-grained control over agent internals:

```ruby
agent = DSPy::ReAct.new(MySignature, tools: tools)
agent.configure { |c| c.lm = default_model }
agent.configure_predictor('thought_generator') { |c| c.lm = powerful_model }

**LM解析优先级**:实例级LM → 纤程本地LM(`DSPy.with_lm`)→ 全局LM(`DSPy.configure`)。

使用`configure_predictor`对智能体内部进行细粒度控制:

```ruby
agent = DSPy::ReAct.new(MySignature, tools: tools)
agent.configure { |c| c.lm = default_model }
agent.configure_predictor('thought_generator') { |c| c.lm = powerful_model }

Evaluation Framework

评估框架

Systematically test LLM application performance with
DSPy::Evals
:
ruby
metric = DSPy::Metrics.exact_match(field: :answer, case_sensitive: false)
evaluator = DSPy::Evals.new(predictor, metric: metric)
result = evaluator.evaluate(test_examples, display_table: true)
puts "Pass Rate: #{(result.pass_rate * 100).round(1)}%"
Built-in metrics:
exact_match
,
contains
,
numeric_difference
,
composite_and
. Custom metrics return
true
/
false
or a
DSPy::Prediction
with
score:
and
feedback:
fields.
Use
DSPy::Example
for typed test data and
export_scores: true
to push results to Langfuse.
使用
DSPy::Evals
系统地测试LLM应用性能:
ruby
metric = DSPy::Metrics.exact_match(field: :answer, case_sensitive: false)
evaluator = DSPy::Evals.new(predictor, metric: metric)
result = evaluator.evaluate(test_examples, display_table: true)
puts "Pass Rate: #{(result.pass_rate * 100).round(1)}%"
内置指标:
exact_match
contains
numeric_difference
composite_and
。自定义指标返回
true
/
false
或带
score:
feedback:
字段的
DSPy::Prediction
使用
DSPy::Example
定义类型化测试数据,设置
export_scores: true
可将结果推送到Langfuse。

GEPA Optimization

GEPA优化

GEPA (Genetic-Pareto Reflective Prompt Evolution) uses reflection-driven instruction rewrites:
ruby
gem 'dspy-gepa'

teleprompter = DSPy::Teleprompt::GEPA.new(
  metric: metric,
  reflection_lm: DSPy::ReflectionLM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY']),
  feedback_map: feedback_map,
  config: { max_metric_calls: 600, minibatch_size: 6 }
)

result = teleprompter.compile(program, trainset: train, valset: val)
optimized_program = result.optimized_program
The metric must return
DSPy::Prediction.new(score:, feedback:)
so the reflection model can reason about failures. Use
feedback_map
to target individual predictors in composite modules.
GEPA(遗传帕累托反射式提示词进化)使用反射驱动的指令重写:
ruby
gem 'dspy-gepa'

teleprompter = DSPy::Teleprompt::GEPA.new(
  metric: metric,
  reflection_lm: DSPy::ReflectionLM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY']),
  feedback_map: feedback_map,
  config: { max_metric_calls: 600, minibatch_size: 6 }
)

result = teleprompter.compile(program, trainset: train, valset: val)
optimized_program = result.optimized_program
指标必须返回
DSPy::Prediction.new(score:, feedback:)
,以便反射模型分析失败原因。使用
feedback_map
可针对复合模块中的特定预测器进行优化。

Typed Context Pattern

类型化上下文模式

Replace opaque string context blobs with
T::Struct
inputs. Each field gets its own
description:
annotation in the JSON schema the LLM sees:
ruby
class NavigationContext < T::Struct
  const :workflow_hint, T.nilable(String),
        description: "Current workflow phase guidance for the agent"
  const :action_log, T::Array[String], default: [],
        description: "Compact one-line-per-action history of research steps taken"
  const :iterations_remaining, Integer,
        description: "Budget remaining. Each tool call costs 1 iteration."
end

class ToolSelectionSignature < DSPy::Signature
  input do
    const :query, String
    const :context, NavigationContext  # Structured, not an opaque string
  end

  output do
    const :tool_name, String
    const :tool_args, String, description: "JSON-encoded arguments"
  end
end
Benefits: type safety at compile time, per-field descriptions in the LLM schema, easy to test as value objects, extensible by adding
const
declarations.
T::Struct
输入替代不透明的字符串上下文。每个字段在LLM看到的JSON Schema中都会有自己的
description:
注释:
ruby
class NavigationContext < T::Struct
  const :workflow_hint, T.nilable(String),
        description: "Current workflow phase guidance for the agent"
  const :action_log, T::Array[String], default: [],
        description: "Compact one-line-per-action history of research steps taken"
  const :iterations_remaining, Integer,
        description: "Budget remaining. Each tool call costs 1 iteration."
end

class ToolSelectionSignature < DSPy::Signature
  input do
    const :query, String
    const :context, NavigationContext  # 结构化,而非不透明字符串
  end

  output do
    const :tool_name, String
    const :tool_args, String, description: "JSON-encoded arguments"
  end
end
优势:编译时类型安全、LLM Schema中的字段级描述、可作为值对象轻松测试、通过添加
const
声明实现扩展。

Schema Formats (BAML / TOON)

Schema格式(BAML / TOON)

Control how DSPy describes signature structure to the LLM:
  • JSON Schema (default) — Standard format, works with
    structured_outputs: true
  • BAML (
    schema_format: :baml
    ) — 84% token reduction for Enhanced Prompting mode. Requires
    sorbet-baml
    gem.
  • TOON (
    schema_format: :toon, data_format: :toon
    ) — Table-oriented format for both schemas and data. Enhanced Prompting mode only.
BAML and TOON apply only when
structured_outputs: false
. With
structured_outputs: true
, the provider receives JSON Schema directly.
控制DSPy向LLM描述签名结构的方式:
  • JSON Schema(默认)—— 标准格式,支持
    structured_outputs: true
  • BAML
    schema_format: :baml
    )—— 在增强提示模式下可减少84%的Token消耗,需安装
    sorbet-baml
    gem
  • TOON
    schema_format: :toon, data_format: :toon
    )—— 面向表格的格式,同时支持Schema和数据,仅适用于增强提示模式
BAML和TOON仅在
structured_outputs: false
时生效。当
structured_outputs: true
时,提供商会直接接收JSON Schema。

Storage System

存储系统

Persist and reload optimized programs with
DSPy::Storage::ProgramStorage
:
ruby
storage = DSPy::Storage::ProgramStorage.new(storage_path: "./dspy_storage")
storage.save_program(result.optimized_program, result, metadata: { optimizer: 'MIPROv2' })
Supports checkpoint management, optimization history tracking, and import/export between environments.
使用
DSPy::Storage::ProgramStorage
持久化和重新加载优化后的程序:
ruby
storage = DSPy::Storage::ProgramStorage.new(storage_path: "./dspy_storage")
storage.save_program(result.optimized_program, result, metadata: { optimizer: 'MIPROv2' })
支持检查点管理、优化历史跟踪,以及环境间的导入/导出。

Rails Integration

Rails集成

Directory Structure

目录结构

Organize DSPy components using Rails conventions:
app/
  entities/          # T::Struct types shared across signatures
  signatures/        # DSPy::Signature definitions
  tools/             # DSPy::Tools::Base implementations
    concerns/        # Shared tool behaviors (error handling, etc.)
  modules/           # DSPy::Module orchestrators
  services/          # Plain Ruby services that compose DSPy modules
config/
  initializers/
    dspy.rb          # DSPy + provider configuration
    feature_flags.rb # Model selection per role
spec/
  signatures/        # Schema validation tests
  tools/             # Tool unit tests
  modules/           # Integration tests with VCR
  vcr_cassettes/     # Recorded HTTP interactions
使用Rails约定组织DSPy组件:
app/
  entities/          # 签名间共享的T::Struct类型
  signatures/        # DSPy::Signature定义
  tools/             # DSPy::Tools::Base实现
    concerns/        # 共享工具行为(如错误处理)
  modules/           # DSPy::Module编排器
  services/          # 组合DSPy模块的普通Ruby服务
config/
  initializers/
    dspy.rb          # DSPy + 提供商配置
    feature_flags.rb # 按角色选择模型
spec/
  signatures/        # Schema验证测试
  tools/             # 工具单元测试
  modules/           # 带VCR的集成测试
  vcr_cassettes/     # 录制的HTTP交互

Initializer

初始化器

ruby
undefined
ruby
undefined

config/initializers/dspy.rb

config/initializers/dspy.rb

Rails.application.config.after_initialize do next if Rails.env.test? && ENV["DSPY_ENABLE_IN_TEST"].blank?
RubyLLM.configure do |config| config.gemini_api_key = ENV["GEMINI_API_KEY"] if ENV["GEMINI_API_KEY"].present? config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] if ENV["ANTHROPIC_API_KEY"].present? config.openai_api_key = ENV["OPENAI_API_KEY"] if ENV["OPENAI_API_KEY"].present? end
model = ENV.fetch("DSPY_MODEL", "ruby_llm/gemini-2.5-flash") DSPy.configure do |config| config.lm = DSPy::LM.new(model, structured_outputs: true) config.logger = Rails.logger end

Langfuse observability (optional)

if ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present? DSPy::Observability.configure! end end
undefined
Rails.application.config.after_initialize do next if Rails.env.test? && ENV["DSPY_ENABLE_IN_TEST"].blank?
RubyLLM.configure do |config| config.gemini_api_key = ENV["GEMINI_API_KEY"] if ENV["GEMINI_API_KEY"].present? config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] if ENV["ANTHROPIC_API_KEY"].present? config.openai_api_key = ENV["OPENAI_API_KEY"] if ENV["OPENAI_API_KEY"].present? end
model = ENV.fetch("DSPY_MODEL", "ruby_llm/gemini-2.5-flash") DSPy.configure do |config| config.lm = DSPy::LM.new(model, structured_outputs: true) config.logger = Rails.logger end

Langfuse可观测性(可选)

if ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present? DSPy::Observability.configure! end end
undefined

Feature-Flagged Model Selection

功能标记模型选择

Use different models for different roles (fast/cheap for classification, powerful for synthesis):
ruby
undefined
为不同角色使用不同模型(快速/廉价模型用于分类,高性能模型用于合成):
ruby
undefined

config/initializers/feature_flags.rb

config/initializers/feature_flags.rb

module FeatureFlags SELECTOR_MODEL = ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite") SYNTHESIZER_MODEL = ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash") end

Then override per-tool or per-predictor:

```ruby
class ClassifyTool < DSPy::Tools::Base
  def call(query:)
    predictor = DSPy::Predict.new(ClassifyQuery)
    predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SELECTOR_MODEL, structured_outputs: true) }
    predictor.call(query: query)
  end
end
module FeatureFlags SELECTOR_MODEL = ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite") SYNTHESIZER_MODEL = ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash") end

然后按工具或预测器覆盖:

```ruby
class ClassifyTool < DSPy::Tools::Base
  def call(query:)
    predictor = DSPy::Predict.new(ClassifyQuery)
    predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SELECTOR_MODEL, structured_outputs: true) }
    predictor.call(query: query)
  end
end

Schema-Driven Signatures

Schema驱动的签名

Prefer typed schemas over string descriptions. Let the type system communicate structure to the LLM rather than prose in the signature description.
优先使用类型化Schema而非字符串描述。让类型系统向LLM传达结构,而非签名描述中的散文式内容。

Entities as Shared Types

作为共享类型的实体

Define reusable
T::Struct
and
T::Enum
types in
app/entities/
and reference them across signatures:
ruby
undefined
app/entities/
中定义可复用的
T::Struct
T::Enum
类型,并在多个签名中引用:
ruby
undefined

app/entities/search_strategy.rb

app/entities/search_strategy.rb

class SearchStrategy < T::Enum enums do SingleSearch = new("single_search") DateDecomposition = new("date_decomposition") end end
class SearchStrategy < T::Enum enums do SingleSearch = new("single_search") DateDecomposition = new("date_decomposition") end end

app/entities/scored_item.rb

app/entities/scored_item.rb

class ScoredItem < T::Struct const :id, String const :score, Float, description: "Relevance score 0.0-1.0" const :verdict, String, description: "relevant, maybe, or irrelevant" const :reason, String, default: "" end
undefined
class ScoredItem < T::Struct const :id, String const :score, Float, description: "Relevance score 0.0-1.0" const :verdict, String, description: "relevant, maybe, or irrelevant" const :reason, String, default: "" end
undefined

Schema vs Description: When to Use Each

Schema vs 描述:何时使用哪种

Use schemas (T::Struct/T::Enum) for:
  • Multi-field outputs with specific types
  • Enums with defined values the LLM must pick from
  • Nested structures, arrays of typed objects
  • Outputs consumed by code (not displayed to users)
Use string descriptions for:
  • Simple single-field outputs where the type is
    String
  • Natural language generation (summaries, answers)
  • Fields where constraint guidance helps (e.g.,
    description: "YYYY-MM-DD format"
    )
Rule of thumb: If you'd write a
case
statement on the output, it should be a
T::Enum
. If you'd call
.each
on it, it should be
T::Array[SomeStruct]
.
使用Schema(T::Struct/T::Enum)的场景
  • 具有特定类型的多字段输出
  • LLM必须从定义值中选择的枚举
  • 嵌套结构、类型化对象数组
  • 供代码消费的输出(而非展示给用户)
使用字符串描述的场景
  • 类型为
    String
    的简单单字段输出
  • 自然语言生成(摘要、回答)
  • 约束指导有帮助的字段(如
    description: "YYYY-MM-DD格式"
经验法则:如果要对输出写
case
语句,那么它应该是
T::Enum
;如果要对它调用
.each
,那么它应该是
T::Array[SomeStruct]

Tool Patterns

工具模式

Tools That Wrap Predictions

包裹预测的工具

A common pattern: tools encapsulate a DSPy prediction, adding error handling, model selection, and serialization:
ruby
class RerankTool < DSPy::Tools::Base
  tool_name "rerank"
  tool_description "Score and rank search results by relevance"

  MAX_ITEMS = 200
  MIN_ITEMS_FOR_LLM = 5

  sig { params(query: String, items: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
  def call(query:, items: [])
    return { scored_items: items, reranked: false } if items.size < MIN_ITEMS_FOR_LLM

    capped_items = items.first(MAX_ITEMS)
    predictor = DSPy::Predict.new(RerankSignature)
    predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SYNTHESIZER_MODEL, structured_outputs: true) }

    result = predictor.call(query: query, items: capped_items)
    { scored_items: result.scored_items, reranked: true }
  rescue => e
    Rails.logger.warn "[RerankTool] LLM rerank failed: #{e.message}"
    { error: "Rerank failed: #{e.message}", scored_items: items, reranked: false }
  end
end
Key patterns:
  • Short-circuit LLM calls when unnecessary (small data, trivial cases)
  • Cap input size to prevent token overflow
  • Per-tool model selection via
    configure
  • Graceful error handling with fallback data
常见模式:工具封装DSPy预测,添加错误处理、模型选择和序列化:
ruby
class RerankTool < DSPy::Tools::Base
  tool_name "rerank"
  tool_description "Score and rank search results by relevance"

  MAX_ITEMS = 200
  MIN_ITEMS_FOR_LLM = 5

  sig { params(query: String, items: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
  def call(query:, items: [])
    return { scored_items: items, reranked: false } if items.size < MIN_ITEMS_FOR_LLM

    capped_items = items.first(MAX_ITEMS)
    predictor = DSPy::Predict.new(RerankSignature)
    predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SYNTHESIZER_MODEL, structured_outputs: true) }

    result = predictor.call(query: query, items: capped_items)
    { scored_items: result.scored_items, reranked: true }
  rescue => e
    Rails.logger.warn "[RerankTool] LLM rerank failed: #{e.message}"
    { error: "Rerank failed: #{e.message}", scored_items: items, reranked: false }
  end
end
关键模式
  • 在不必要时跳过LLM调用(如数据量小、 trivial场景)
  • 限制输入大小以防止Token溢出
  • 通过
    configure
    按工具选择模型
  • 优雅的错误处理,提供回退数据

Error Handling Concern

错误处理关注点

ruby
module ErrorHandling
  extend ActiveSupport::Concern

  private

  def safe_predict(signature_class, **inputs)
    predictor = DSPy::Predict.new(signature_class)
    yield predictor if block_given?
    predictor.call(**inputs)
  rescue Faraday::Error, Net::HTTPError => e
    Rails.logger.error "[#{self.class.name}] API error: #{e.message}"
    nil
  rescue JSON::ParserError => e
    Rails.logger.error "[#{self.class.name}] Invalid LLM output: #{e.message}"
    nil
  end
end
ruby
module ErrorHandling
  extend ActiveSupport::Concern

  private

  def safe_predict(signature_class, **inputs)
    predictor = DSPy::Predict.new(signature_class)
    yield predictor if block_given?
    predictor.call(**inputs)
  rescue Faraday::Error, Net::HTTPError => e
    Rails.logger.error "[#{self.class.name}] API error: #{e.message}"
    nil
  rescue JSON::ParserError => e
    Rails.logger.error "[#{self.class.name}] Invalid LLM output: #{e.message}"
    nil
  end
end

Observability

可观测性

Tracing with DSPy::Context

使用DSPy::Context追踪

Wrap operations in spans for Langfuse/OpenTelemetry visibility:
ruby
result = DSPy::Context.with_span(
  operation: "tool_selector.select",
  "dspy.module" => "ToolSelector",
  "tool_selector.tools" => tool_names.join(",")
) do
  @predictor.call(query: query, context: context, available_tools: schemas)
end
将操作包裹在Span中,以便Langfuse/OpenTelemetry可见:
ruby
result = DSPy::Context.with_span(
  operation: "tool_selector.select",
  "dspy.module" => "ToolSelector",
  "tool_selector.tools" => tool_names.join(",")
) do
  @predictor.call(query: query, context: context, available_tools: schemas)
end

Setup for Langfuse

Langfuse设置

ruby
undefined
ruby
undefined

Gemfile

Gemfile

gem 'dspy-o11y' gem 'dspy-o11y-langfuse'
gem 'dspy-o11y' gem 'dspy-o11y-langfuse'

.env

.env

LANGFUSE_PUBLIC_KEY=pk-... LANGFUSE_SECRET_KEY=sk-... DSPY_TELEMETRY_BATCH_SIZE=5

Every `DSPy::Predict`, `DSPy::ReAct`, and tool call is automatically traced when observability is configured.
LANGFUSE_PUBLIC_KEY=pk-... LANGFUSE_SECRET_KEY=sk-... DSPY_TELEMETRY_BATCH_SIZE=5

配置可观测性后,所有`DSPy::Predict`、`DSPy::ReAct`和工具调用都会被自动追踪。

Score Reporting

分数上报

Report evaluation scores to Langfuse:
ruby
DSPy.score(name: "relevance", value: 0.85, trace_id: current_trace_id)
将评估分数上报到Langfuse:
ruby
DSPy.score(name: "relevance", value: 0.85, trace_id: current_trace_id)

Testing

测试

VCR Setup for Rails

Rails的VCR设置

ruby
VCR.configure do |config|
  config.cassette_library_dir = "spec/vcr_cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
  config.filter_sensitive_data('<GEMINI_API_KEY>') { ENV['GEMINI_API_KEY'] }
  config.filter_sensitive_data('<OPENAI_API_KEY>') { ENV['OPENAI_API_KEY'] }
end
ruby
VCR.configure do |config|
  config.cassette_library_dir = "spec/vcr_cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
  config.filter_sensitive_data('<GEMINI_API_KEY>') { ENV['GEMINI_API_KEY'] }
  config.filter_sensitive_data('<OPENAI_API_KEY>') { ENV['OPENAI_API_KEY'] }
end

Signature Schema Tests

签名Schema测试

Test that signatures produce valid schemas without calling any LLM:
ruby
RSpec.describe ClassifyResearchQuery do
  it "has required input fields" do
    schema = described_class.input_json_schema
    expect(schema[:required]).to include("query")
  end

  it "has typed output fields" do
    schema = described_class.output_json_schema
    expect(schema[:properties]).to have_key(:search_strategy)
  end
end
无需调用LLM,测试签名是否生成有效Schema:
ruby
RSpec.describe ClassifyResearchQuery do
  it "has required input fields" do
    schema = described_class.input_json_schema
    expect(schema[:required]).to include("query")
  end

  it "has typed output fields" do
    schema = described_class.output_json_schema
    expect(schema[:properties]).to have_key(:search_strategy)
  end
end

Tool Tests with Mocked Predictions

带模拟预测的工具测试

ruby
RSpec.describe RerankTool do
  let(:tool) { described_class.new }

  it "skips LLM for small result sets" do
    expect(DSPy::Predict).not_to receive(:new)
    result = tool.call(query: "test", items: [{ id: "1" }])
    expect(result[:reranked]).to be false
  end

  it "calls LLM for large result sets", :vcr do
    items = 10.times.map { |i| { id: i.to_s, title: "Item #{i}" } }
    result = tool.call(query: "relevant items", items: items)
    expect(result[:reranked]).to be true
  end
end
ruby
RSpec.describe RerankTool do
  let(:tool) { described_class.new }

  it "skips LLM for small result sets" do
    expect(DSPy::Predict).not_to receive(:new)
    result = tool.call(query: "test", items: [{ id: "1" }])
    expect(result[:reranked]).to be false
  end

  it "calls LLM for large result sets", :vcr do
    items = 10.times.map { |i| { id: i.to_s, title: "Item #{i}" } }
    result = tool.call(query: "relevant items", items: items)
    expect(result[:reranked]).to be true
  end
end

Resources

资源

  • references/core-concepts.md
    — Signatures, modules, predictors, type system deep-dive
  • references/toolsets.md
    — Tools::Base, Tools::Toolset DSL, type safety, testing
  • references/providers.md
    — Provider adapters, RubyLLM, fiber-local LM context, compatibility matrix
  • references/optimization.md
    — MIPROv2, GEPA, evaluation framework, storage system
  • references/observability.md
    — Event system, dspy-o11y gems, Langfuse, score reporting
  • assets/signature-template.rb
    — Signature scaffold with T::Enum, Date/Time, defaults, union types
  • assets/module-template.rb
    — Module scaffold with .call(), lifecycle callbacks, fiber-local LM
  • assets/config-template.rb
    — Rails initializer with RubyLLM, observability, feature flags
  • references/core-concepts.md
    —— 签名、模块、预测器、类型系统深度解析
  • references/toolsets.md
    —— Tools::Base、Tools::Toolset DSL、类型安全、测试
  • references/providers.md
    —— 提供商适配、RubyLLM、纤程本地LM上下文、兼容性矩阵
  • references/optimization.md
    —— MIPROv2、GEPA、评估框架、存储系统
  • references/observability.md
    —— 事件系统、dspy-o11y gems、Langfuse、分数上报
  • assets/signature-template.rb
    —— 带T::Enum、日期/时间、默认值、联合类型的签名模板
  • assets/module-template.rb
    —— 带.call()、生命周期回调、纤程本地LM的模块模板
  • assets/config-template.rb
    —— 带RubyLLM、可观测性、功能标记的Rails初始化器模板

Key URLs

关键链接

Guidelines for Claude

Claude使用指南

When helping users with DSPy.rb:
  1. Schema over prose — Define output structure with
    T::Struct
    and
    T::Enum
    types, not string descriptions
  2. Entities in
    app/entities/
    — Extract shared types so signatures stay thin
  3. Per-tool model selection — Use
    predictor.configure { |c| c.lm = ... }
    to pick the right model per task
  4. Short-circuit LLM calls — Skip the LLM for trivial cases (small data, cached results)
  5. Cap input sizes — Prevent token overflow by limiting array sizes before sending to LLM
  6. Test schemas without LLM — Validate
    input_json_schema
    and
    output_json_schema
    in unit tests
  7. VCR for integration tests — Record real HTTP interactions, never mock LLM responses by hand
  8. Trace with spans — Wrap tool calls in
    DSPy::Context.with_span
    for observability
  9. Graceful degradation — Always rescue LLM errors and return fallback data
当帮助用户处理DSPy.rb相关问题时:
  1. 优先使用Schema而非散文 —— 用
    T::Struct
    T::Enum
    类型定义输出结构,而非字符串描述
  2. 实体放在
    app/entities/
    —— 提取共享类型,让签名保持简洁
  3. 按工具选择模型 —— 使用
    predictor.configure { |c| c.lm = ... }
    为每个任务选择合适的模型
  4. 短路LLM调用 —— 对于trivial场景(如小数据、缓存结果)跳过LLM
  5. 限制输入大小 —— 在发送给LLM前限制数组大小,防止Token溢出
  6. 无需LLM即可测试Schema —— 在单元测试中验证
    input_json_schema
    output_json_schema
  7. 集成测试使用VCR —— 录制真实HTTP交互,切勿手动模拟LLM响应
  8. 用Span追踪 —— 将工具调用包裹在
    DSPy::Context.with_span
    中以实现可观测性
  9. 优雅降级 —— 始终捕获LLM错误并返回回退数据

Signature Best Practices

签名最佳实践

Keep description concise — The signature
description
should state the goal, not the field details:
ruby
undefined
保持描述简洁 —— 签名的
description
应说明目标,而非字段细节:
ruby
undefined

Good — concise goal

良好示例——简洁的目标

class ParseOutline < DSPy::Signature description 'Extract block-level structure from HTML as a flat list of skeleton sections.'
input do const :html, String, description: 'Raw HTML to parse' end
output do const :sections, T::Array[Section], description: 'Block elements: headings, paragraphs, code blocks, lists' end end

**Use defaults over nilable arrays** — For OpenAI structured outputs compatibility:

```ruby
class ParseOutline < DSPy::Signature description 'Extract block-level structure from HTML as a flat list of skeleton sections.'
input do const :html, String, description: 'Raw HTML to parse' end
output do const :sections, T::Array[Section], description: 'Block elements: headings, paragraphs, code blocks, lists' end end

**使用默认值而非可空数组** —— 为了兼容OpenAI结构化输出:

```ruby

Good — works with OpenAI structured outputs

良好示例——兼容OpenAI结构化输出

class ASTNode < T::Struct const :children, T::Array[ASTNode], default: [] end
undefined
class ASTNode < T::Struct const :children, T::Array[ASTNode], default: [] end
undefined

Recursive Types with
$defs

$defs
的递归类型

DSPy.rb supports recursive types in structured outputs using JSON Schema
$defs
:
ruby
class TreeNode < T::Struct
  const :value, String
  const :children, T::Array[TreeNode], default: []  # Self-reference
end
The schema generator automatically creates
#/$defs/TreeNode
references for recursive types, compatible with OpenAI and Gemini structured outputs.
DSPy.rb使用JSON Schema的
$defs
支持结构化输出中的递归类型:
ruby
class TreeNode < T::Struct
  const :value, String
  const :children, T::Array[TreeNode], default: []  # 自引用
end
Schema生成器会自动为递归类型创建
#/$defs/TreeNode
引用,兼容OpenAI和Gemini的结构化输出。

Field Descriptions for T::Struct

T::Struct的字段描述

DSPy.rb extends T::Struct to support field-level
description:
kwargs that flow to JSON Schema:
ruby
class ASTNode < T::Struct
  const :node_type, NodeType, description: 'The type of node (heading, paragraph, etc.)'
  const :text, String, default: "", description: 'Text content of the node'
  const :level, Integer, default: 0  # No description — field is self-explanatory
  const :children, T::Array[ASTNode], default: []
end
When to use field descriptions: complex field semantics, enum-like strings, constrained values, nested structs with ambiguous names. When to skip: self-explanatory fields like
name
,
id
,
url
, or boolean flags.
DSPy.rb扩展了T::Struct,支持字段级
description:
关键字参数,这些参数会同步到JSON Schema:
ruby
class ASTNode < T::Struct
  const :node_type, NodeType, description: 'The type of node (heading, paragraph, etc.)'
  const :text, String, default: "", description: 'Text content of the node'
  const :level, Integer, default: 0  # 无需描述——字段含义自明
  const :children, T::Array[ASTNode], default: []
end
何时使用字段描述:复杂字段语义、类枚举字符串、约束值、名称模糊的嵌套结构体。何时跳过:含义自明的字段,如
name
id
url
或布尔标志。

Version

版本

Current: 0.34.3
当前版本:0.34.3