mcpField
D
dspy-ruby

dspy-ruby

github.com/everyinc开发25.8k494 次安装今天更新
代码仓库

关于 dspy-ruby

DSPy.rb 是用于构建模块化、类型安全的大模型应用的 Ruby 框架,支持程序化提示词。它提供 Agent 组件、评估与优化工具、事件观测,以及优化程序存储功能。

主要功能

  1. 01定义类型化签名与可复用模块
  2. 02构建工具调用 Agent 和工作流
  3. 03使用内置或自定义指标评估输出
  4. 04通过 MIPROv2 和 GEPA 优化提示词
  5. 05借助结构化事件观测运行情况
  6. 06保存优化程序与历史记录

适用场景

  1. 01在 Ruby 中构建类型化大模型应用
  2. 02组合工具调用 Agent 工作流
  3. 03衡量并改进预测质量
  4. 04跟踪模型与模块的运行事件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/everyinc/compound-engineering-plugin --skill dspy-ruby

安装后在 Claude Code 中输入/dspy-ruby确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
config-template.rb
module-template.rb
signature-template.rb
references/
core-concepts.md
observability.md
optimization.md
providers.md
toolsets.md

详细介绍

DSPy.rb

像构建软件一样构建 LLM 应用。类型安全、模块化、可测试。

DSPy.rb 将软件工程最佳实践引入 LLM 开发。无需反复调整提示词,只需用 Ruby 类型定义所需结果,其余工作交给 DSPy。

概览

DSPy.rb 是一个用于通过程序化提示词构建语言模型应用的 Ruby 框架。它提供:

  • 类型安全的签名 — 使用 Sorbet 类型定义输入和输出
  • 模块化组件 — 组合并复用 LLM 逻辑
  • 自动优化 — 利用数据改进提示词,而不是靠猜测
  • 适用于生产环境 — 内置可观测性、测试和错误处理

核心概念

1. 签名

使用 Ruby 类型定义应用与 LLM 之间的接口:

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. 模块

通过简单的构建模块构建复杂的工作流:

  • Predict — 使用签名进行基本 LLM 调用
  • ChainOfThought — 逐步推理
  • ReAct — 使用工具的 Agent
  • CodeAct — 动态代码生成 Agent(安装 dspy-code_act gem)
3. 工具与工具集

为 Agent 创建类型安全的工具,并获得全面的 Sorbet 支持:

# 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

# 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
4. 类型系统与判别器

DSPy.rb 使用高级类型判别机制处理复杂数据结构:

  • 自动注入 _type 字段 — DSPy 会向结构体添加判别字段,以确保类型安全
  • 联合类型支持 — T.any() 类型会根据 _type 自动消除歧义
  • 保留字段名 — 请勿在结构体中自行定义 _type 字段
  • 递归过滤 — 反序列化时会在所有嵌套层级过滤 _type 字段
5. 优化

利用真实数据提高准确性:

  • MIPROv2 — 高级多提示词优化,结合自举采样和贝叶斯优化
  • GEPA — 遗传帕累托反思式提示词演进,支持反馈映射、实验跟踪和遥测
  • 评估 — 全面的评估框架,支持内置和自定义指标、错误处理及批处理

快速开始

# Install
gem 'dspy'

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

# 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

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

提供商适配器 gem

连接 LLM 提供商的两种策略:

按提供商划分的适配器(直接访问 SDK)
# Gemfile
gem 'dspy'
gem 'dspy-openai'    # OpenAI, OpenRouter, Ollama
gem 'dspy-anthropic' # Claude
gem 'dspy-gemini'    # Gemini

每个适配器 gem 都会引入对应的官方 SDK(openai、anthropic、gemini-ai)。

通过 RubyLLM 使用统一适配器(推荐用于多提供商场景)
# Gemfile
gem 'dspy'
gem 'dspy-ruby_llm'  # Routes to any provider via ruby_llm
gem 'ruby_llm'

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

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

事件系统

DSPy.rb 内置结构化事件总线,用于观测运行时行为。

模块级订阅(Agent 的首选方式)
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
全局订阅(用于可观测性和集成)
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]}" }

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

生命周期回调

每个 DSPy::Module 都内置 Rails 风格的生命周期钩子:

  • before — 在 forward 之前运行,用于准备工作(指标、上下文加载)
  • around — 包装 forward,调用 yield,并支持成对执行准备和清理逻辑
  • after — forward 返回后触发,用于清理或持久化
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 本地 LM 上下文

使用 fiber 本地存储临时覆盖语言模型:

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
# Back to global LM outside the block

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

使用 configure_predictor 精细控制 Agent 内部行为:

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

评估框架

使用 DSPy::Evals 系统地测试大模型应用的性能:

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 优化

GEPA(遗传-帕累托反思式提示词演化)使用反思驱动的指令重写:

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 定位单个预测器。

类型化上下文模式

使用 T::Struct 输入替换不透明的字符串上下文块。每个字段都会在大模型看到的 JSON Schema 中获得单独的 description: 注解:

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

优点:编译时类型安全、在大模型 Schema 中为每个字段提供描述、易于作为值对象测试,并且可通过添加 const 声明来扩展。

Schema 格式(BAML / TOON)

控制 DSPy 如何向大模型描述签名结构:

  • 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。

存储系统

使用 DSPy::Storage::ProgramStorage 持久化并重新加载优化后的程序:

storage = DSPy::Storage::ProgramStorage.new(storage_path: "./dspy_storage")
storage.save_program(result.optimized_program, result, metadata: { optimizer: 'MIPROv2' })

支持检查点管理、优化历史跟踪,以及在不同环境之间导入/导出。

Rails 集成

目录结构

按照 Rails 约定组织 DSPy 组件:

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
初始化程序
# 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
通过功能标志选择模型

为不同角色使用不同模型(分类使用快速/低成本模型,综合使用强大模型):

# 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

然后按工具或预测器进行单独覆盖:

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 驱动的签名

优先使用类型化 Schema,而非字符串描述。 让类型系统向大模型传达结构,而不是在签名描述中使用散文。

将实体用作共享类型

在 app/entities/ 中定义可复用的 T::Struct 和 T::Enum 类型,并在不同签名中引用:

# app/entities/search_strategy.rb
class SearchStrategy < T::Enum
  enums do
    SingleSearch = new("single_search")
    DateDecomposition = new("date_decomposition")
  end
end

# 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
Schema 与描述:分别适用于何时

以下情况使用 Schema(T::Struct/T::Enum):

  • 具有特定类型的多字段输出
  • 大模型必须从中选择已定义值的枚举
  • 嵌套结构、类型化对象数组
  • 由代码使用(而非展示给用户)的输出

以下情况使用字符串描述:

  • 类型为 String 的简单单字段输出
  • 自然语言生成(摘要、回答)
  • 提供约束指导有帮助的字段(例如,description: "YYYY-MM-DD format")

经验法则:如果你会对输出编写 case 语句,它就应该是 T::Enum。如果你会对它调用 .each,它就应该是 T::Array[SomeStruct]。

工具模式

封装预测的工具

一种常见模式:工具封装 DSPy 预测,并添加错误处理、模型选择和序列化:

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

关键模式:

  • 在不需要时跳过大模型调用(数据量小、情况简单)
  • 限制输入大小,防止 token 溢出
  • 通过 configure 为每个工具单独选择模型
  • 使用备用数据进行稳健的错误处理
错误处理注意事项
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

可观测性

使用 DSPy::Context 进行追踪

将操作包装在 span 中,以便通过 Langfuse/OpenTelemetry 查看:

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
Langfuse 设置
# Gemfile
gem 'dspy-o11y'
gem 'dspy-o11y-langfuse'

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

配置可观测性后,每次 DSPy::Predict、DSPy::ReAct 和工具调用都会自动进行追踪。

分数报告

将评估分数报告到 Langfuse:

DSPy.score(name: "relevance", value: 0.85, trace_id: current_trace_id)

测试

Rails 的 VCR 设置
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
签名 Schema 测试

测试签名能否生成有效的 schema,而无需调用任何大模型:

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
使用模拟预测结果的工具测试
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

资源

  • references/core-concepts.md — 签名、模块、预测器、类型系统深入解析
  • references/toolsets.md — Tools::Base、Tools::Toolset DSL、类型安全、测试
  • references/providers.md — 提供方适配器、RubyLLM、fiber-local LM 上下文、兼容性矩阵
  • references/optimization.md — MIPROv2、GEPA、评估框架、存储系统
  • references/observability.md — 事件系统、dspy-o11y gems、Langfuse、分数报告
  • assets/signature-template.rb — 签名脚手架,包含 T::Enum、Date/Time、默认值、联合类型
  • assets/module-template.rb — 模块脚手架,包含 .call()、生命周期回调、fiber-local LM
  • assets/config-template.rb — Rails 初始化器,包含 RubyLLM、可观测性、功能标志

关键 URL

Claude 指南

为用户提供 DSPy.rb 帮助时:

  1. 优先使用 Schema,而不是散文描述 — 使用 T::Struct 和 T::Enum 类型定义输出结构,而不是字符串描述
  2. 将实体放在 app/entities/ 中 — 提取共享类型,让签名保持精简
  3. 为每个工具选择模型 — 使用 predictor.configure { |c| c.lm = ... },为每项任务选择合适的模型
  4. 短路跳过大模型调用 — 对简单情况(少量数据、缓存结果)跳过大模型
  5. 限制输入大小 — 发送给大模型前限制数组大小,避免 token 溢出
  6. 无需大模型即可测试 Schema — 在单元测试中验证 input_json_schema 和 output_json_schema
  7. 使用 VCR 进行集成测试 — 记录真实的 HTTP 交互,绝不手动模拟大模型响应
  8. 使用 span 跟踪 — 用 DSPy::Context.with_span 包装工具调用,以便进行可观测性监控
  9. 优雅降级 — 始终捕获大模型错误并返回备用数据
签名最佳实践

保持描述简洁 — 签名的 description 应说明目标,而非字段细节:

# 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

优先使用默认值,而不是可为 nil 的数组 — 以兼容 OpenAI 结构化输出:

# Good — works with OpenAI structured outputs
class ASTNode < T::Struct
  const :children, T::Array[ASTNode], default: []
end
使用 $defs 定义递归类型

DSPy.rb 支持在结构化输出中使用 JSON Schema $defs 定义递归类型:

class TreeNode < T::Struct
  const :value, String
  const :children, T::Array[TreeNode], default: []  # Self-reference
end

Schema 生成器会自动为递归类型创建 #/$defs/TreeNode 引用,兼容 OpenAI 和 Gemini 结构化输出。

T::Struct 的字段描述

DSPy.rb 扩展了 T::Struct,支持字段级别的 description: 关键字参数,并将其传递到 JSON Schema:

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

适合使用字段描述的情况:复杂的字段语义、类似枚举的字符串、受约束的值、名称含义模糊的嵌套 struct。适合省略的情况:name、id、url 等含义明确的字段或布尔标志。

版本

当前版本:0.34.3

评论

支持 Markdown