关于 dspy-ruby
DSPy.rb 是用于构建模块化、类型安全的大模型应用的 Ruby 框架,支持程序化提示词。它提供 Agent 组件、评估与优化工具、事件观测,以及优化程序存储功能。
主要功能
- 01定义类型化签名与可复用模块
- 02构建工具调用 Agent 和工作流
- 03使用内置或自定义指标评估输出
- 04通过 MIPROv2 和 GEPA 优化提示词
- 05借助结构化事件观测运行情况
- 06保存优化程序与历史记录
适用场景
- 01在 Ruby 中构建类型化大模型应用
- 02组合工具调用 Agent 工作流
- 03衡量并改进预测质量
- 04跟踪模型与模块的运行事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ everyinc/ compound-engineering-plugin --skill dspy-ruby安装后在 Claude Code 中输入/dspy-ruby确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/config-template.rbmodule-template.rbsignature-template.rbreferences/core-concepts.mdobservability.mdoptimization.mdproviders.mdtoolsets.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_actgem)
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-bamlgem。 - 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 LMassets/config-template.rb— Rails 初始化器,包含 RubyLLM、可观测性、功能标志
关键 URL
- 主页:https://oss.vicente.services/dspy.rb/
- GitHub:https://github.com/vicentereig/dspy.rb
- 文档:https://oss.vicente.services/dspy.rb/getting-started/
Claude 指南
为用户提供 DSPy.rb 帮助时:
- 优先使用 Schema,而不是散文描述 — 使用
T::Struct和T::Enum类型定义输出结构,而不是字符串描述 - 将实体放在
app/entities/中 — 提取共享类型,让签名保持精简 - 为每个工具选择模型 — 使用
predictor.configure { |c| c.lm = ... },为每项任务选择合适的模型 - 短路跳过大模型调用 — 对简单情况(少量数据、缓存结果)跳过大模型
- 限制输入大小 — 发送给大模型前限制数组大小,避免 token 溢出
- 无需大模型即可测试 Schema — 在单元测试中验证
input_json_schema和output_json_schema - 使用 VCR 进行集成测试 — 记录真实的 HTTP 交互,绝不手动模拟大模型响应
- 使用 span 跟踪 — 用
DSPy::Context.with_span包装工具调用,以便进行可观测性监控 - 优雅降级 — 始终捕获大模型错误并返回备用数据
签名最佳实践
保持描述简洁 — 签名的 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