Hotdry.
ai-systems

在7年历史的Rails单体应用中集成AI代理:处理遗留代码库、数据库模式兼容性和实时推理流水线的工程挑战

探索在遗留Rails单体应用中集成AI代理的工程实践,涵盖数据库模式适配、实时推理流水线设计、与现有业务逻辑的无缝集成,以及Active Agent框架的Rails原生解决方案。

引言:当 AI 遇见遗留 Rails 单体

在当今 AI 驱动的开发浪潮中,许多企业面临着一个共同的困境:如何在已有 7 年甚至更久历史的 Rails 单体应用中,安全、高效地集成 AI 代理功能?这些遗留系统往往积累了大量的技术债务,数据库模式复杂多变,业务逻辑紧密耦合,而 AI 代理的引入又带来了全新的挑战 —— 实时推理、工具调用、上下文管理,以及与现有系统的无缝集成。

根据 Evil Martians 团队在 2025 年 9 月的实践分享,Rails 生态系统正在快速适应 AI 革命的需求。从早期的 ruby-openai SDK 到专门的 AI 库如 Raix 和 RubyLLM,再到最新的 Active Agent 框架,Rails 开发者正在寻找符合 Rails 哲学的原生 AI 抽象。然而,在遗留单体中集成 AI 代理远不止选择一个库那么简单,它涉及到数据库模式兼容性、实时推理流水线设计、测试策略、监控体系等一系列工程挑战。

Active Agent:Rails 原生的 AI 抽象框架

Active Agent 框架代表了 Rails 社区对 AI 集成问题的最新思考。它借鉴了 Action Mailer 的设计模式,将 AI 代理封装为类似控制器的对象,支持回调、模板渲染等熟悉的 Rails 特性。这种设计让 Rails 开发者能够以最小的认知成本开始构建 AI 功能。

基础架构设计

Active Agent 的核心思想是将 AI 生成逻辑封装在专门的代理类中。以下是一个简单的示例:

class JokerAgent < ApplicationAgent
  after_generation :notify_user

  def dad_joke(topic = "beer")
    prompt(body: "Generate a dad joke for the topic: #{topic}")
  end

  def nerd_joke(topic = "Apple keynote")
    prompt(body: "Generate a nerd joke for the topic: #{topic}")
  end

  private

  def notify_user
    return unless params[:user]
    
    UserMailer.with(user: params[:user]).joke_generated(response.message.content).deliver_later
  end
end

这种设计模式的优势在于:

  1. 熟悉的开发体验:Rails 开发者立即理解代码结构
  2. 内置异步支持:通过generate_later方法轻松实现后台处理
  3. 回调机制:在生成前后执行自定义逻辑
  4. 模板系统:使用 Action View 渲染复杂的提示模板

与遗留系统的集成策略

在遗留 Rails 单体中,最大的挑战是如何让 AI 代理与现有的业务逻辑协同工作。Active Agent 通过params参数传递上下文信息,允许代理访问当前用户、记录对象等业务实体。这种设计虽然简单,但在遗留系统中需要特别注意副作用的管理。

数据库模式兼容性:适配遗留数据模型

7 年历史的 Rails 应用通常伴随着复杂的数据库演化历史。迁移可能不完整,表结构可能包含废弃字段,关联关系可能以非标准方式实现。AI 代理需要能够理解并适应这些数据模式。

数据访问层的抽象

为了避免 AI 代理直接依赖具体的数据库实现,建议在代理和数据模型之间建立抽象层:

class TranslateAgent < ApplicationAgent
  after_generation :update_record

  def translate(content, locale)
    @locale = locale
    @content = content
    prompt
  end

  private

  def update_record
    return unless params[:record]
    
    record = params[:record]
    result = response.message.content
    
    # 解析AI响应并更新记录
    _, original_locale, locale, content = result.split(/^(\w{2})->(\w{2}):\s*/)
    
    return unless original_locale.present? && locale.present? && content.present?
    
    # 调用模型方法而非直接操作数据库
    record.translated!(original_locale, locale, content)
  end
end

模式演化策略

在遗留系统中集成 AI 代理时,需要考虑以下模式演化策略:

  1. 只读访问优先:初始阶段让 AI 代理以只读方式访问数据,避免直接修改
  2. 版本化 API:为 AI 代理提供版本化的数据访问接口
  3. 数据转换层:在代理和数据源之间建立转换层,处理字段映射和格式转换
  4. 渐进式迁移:逐步将 AI 相关的数据操作迁移到新的表结构中

实时推理流水线:同步与异步的权衡

AI 代理的推理过程可能耗时较长,特别是在处理复杂任务或调用外部工具时。在 Web 请求中直接进行同步推理可能导致超时和糟糕的用户体验。然而,某些场景又需要实时响应。

同步推理模式

对于简单的、低延迟的任务,可以使用同步推理:

class TranslationsController < ApplicationController
  def create
    comment = Comment.find(params[:id])
    unless comment.translated_to?(params[:locale])
      TranslateAgent.with(record: comment)
        .translate(comment.body, params[:locale]).generate_now
    end
    
    render json: comment
  end
end

这种模式的优点是简单直接,但需要注意:

  • 设置合理的超时时间
  • 监控推理延迟
  • 考虑使用连接池管理 AI 服务连接

异步推理模式

对于耗时的任务,应该使用异步处理:

def regenerate_translations
  return if translations.empty?
  
  translations.each do |translation|
    TranslateAgent.with(record: self)
      .translate(translatable_content.to_s, translation.locale)
      .generate_later
  end
end

异步模式的关键考虑因素:

  1. 作业队列选择:Active Job 支持多种后端(Sidekiq、Resque 等)
  2. 重试策略:AI 服务可能不稳定,需要合理的重试机制
  3. 进度通知:通过 Action Cable 或轮询 API 通知用户处理进度
  4. 结果缓存:缓存 AI 生成结果,避免重复计算

混合模式:流式响应

对于需要逐步显示结果的场景,可以考虑流式响应。虽然 Active Agent 本身不直接支持流式输出,但可以通过以下方式实现:

  1. 使用 Server-Sent Events(SSE)逐步推送部分结果
  2. 将复杂任务分解为多个子任务
  3. 在前端展示处理进度

工程化实践:测试、监控与安全

在遗留系统中引入 AI 功能,必须建立完善的工程实践体系,确保系统的可靠性、可维护性和安全性。

测试策略

测试 AI 功能面临独特挑战:LLM 输出具有不确定性,外部 API 可能不稳定。Active Agent 社区推荐使用专门的测试提供者:

# 自定义的测试LLM提供者
module ActiveAgent
  module GenerationProvider
    class FakeLLMProvider < Base
      attr_reader :response
      
      class << self
        def generate_response_content
          raise NotImplementedError, "必须通过stub指定响应内容"
        end
        
        def generations
          Thread.current[:generations] ||= []
        end
      end
      
      def initialize(*)
      end
      
      def generate(prompt)
        @prompt = prompt
        
        message = ActiveAgent::ActionPrompt::Message.new(
          content: self.class.generate_response_content,
          role: "assistant"
        )
        
        # 记录执行的提示,用于验证
        self.class.generations << prompt
        
        @response = ActiveAgent::GenerationProvider::Response.new(
          prompt: prompt, 
          message: message, 
          raw_response: {}
        )
      end
    end
  end
end

测试辅助方法:

module LLMHelpers
  def stub_llm_response(content)
    allow(ActiveAgent::GenerationProvider::FakeLLMProvider)
      .to receive(:generate_response_content).and_return(content)
  end
  
  def assert_llm_has_been_called(times: 1)
    expect(ActiveAgent::GenerationProvider::FakeLLMProvider)
      .to have_received(:generate_response_content).exactly(times).times
  end
end

使用监控与配额管理

AI 服务通常按使用量计费,需要建立完善的监控和配额体系:

class ApplicationAgent < ActiveAgent::Base
  before_generation :ensure_has_credits
  before_generation :track_start_time
  after_generation :track_usage
  
  private
  
  def identity = (params[:account] || Current.account)&.ai_identity
  
  def ensure_has_credits
    return unless identity
    
    raise "No credits available" unless identity.has_credits?
  end
  
  def track_start_time = @start_time = Time.current
  
  def track_usage
    return unless response
    
    if identity
      identity.generations.create!(
        purpose: [agent_name.underscore, action_name].join("/"),
        time_spent: (Time.current - @start_time).to_i,
        tokens_used: response.tokens_used,
        model: generation_provider.config["model"],
        provider: generation_provider.config["service"]
      )
    end
  end
end

监控指标应该包括:

  • 每次调用的 token 消耗
  • 推理延迟分布
  • 错误率和重试次数
  • 成本分析和预测

安全防护

AI 代理引入了新的安全风险,需要特别注意:

  1. 提示注入防护:验证和清理用户输入,避免恶意提示覆盖系统指令
  2. 输出验证:对 AI 生成的内容进行格式和内容验证
  3. 访问控制:确保 AI 代理只能访问授权数据
  4. 审计日志:记录所有 AI 操作,便于安全审计

工具集成与结构化输出

复杂的 AI 代理需要调用外部工具和返回结构化数据。Active Agent 提供了相应的机制,但在遗留系统中需要特别注意集成方式。

工具调用

class ReviewAgent < ApplicationAgent
  def review(proposal)
    @proposal = proposal
    prompt(output_schema: "review_schema")
  end
  
  def search_talks
    query = params[:query]
    results = TrieveClient.search(query)
    
    prompt(instructions: "") do |format|
      format.html { render partial: "search_talks_results", locals: {results: results} }
    end
  end
end

工具定义需要 JSON Schema,这在一定程度上违背了 Rails 的约定优于配置原则。社区正在探索使用 RBS 类型签名等更优雅的方案。

结构化输出

通过指定输出模式,可以让 LLM 返回结构化数据:

result = ReviewAgent.review(proposal).generate_now
JSON.parse(result.message.content)
#=> {
#     "scores": {"novelty":4,"relevance":4,"quality":5},
#     "feedback": "This one is a good fit for the conference",
#     "notes": "I've searched for the topic and couldn't find a lot of talks like this one"
#   }

未来可能支持更优雅的结构化输出方式,如直接返回 Ruby 对象。

未来方向与最佳实践

在遗留 Rails 单体中集成 AI 代理是一个持续演进的过程。基于当前实践,我们总结出以下最佳实践:

渐进式集成策略

  1. 从只读功能开始:先实现不修改数据的 AI 功能,如内容分析、分类等
  2. 逐步引入写操作:在验证可靠性和安全性后,逐步引入数据修改功能
  3. 建立回滚机制:确保每个 AI 功能都有对应的手动回滚路径
  4. A/B 测试:通过 A/B 测试验证 AI 功能的实际效果

架构演进建议

  1. 服务边界清晰化:明确 AI 代理的职责边界,避免成为新的上帝对象
  2. 依赖倒置:通过接口抽象 AI 服务依赖,便于切换提供商
  3. 可观测性优先:在开发初期就建立完善的监控和日志体系
  4. 文档驱动:为每个 AI 代理编写清晰的文档,说明其输入、输出和行为

团队能力建设

  1. 跨职能协作:AI 开发需要产品、工程、数据科学等多方协作
  2. 持续学习:AI 技术快速演进,团队需要建立持续学习机制
  3. 经验分享:建立内部知识库,分享 AI 集成的最佳实践和教训

结论

在 7 年历史的 Rails 单体应用中集成 AI 代理是一项充满挑战但回报丰厚的工程实践。通过采用 Active Agent 等 Rails 原生框架,遵循渐进式集成策略,建立完善的测试和监控体系,团队可以在不破坏现有系统稳定性的前提下,逐步引入 AI 能力。

关键的成功因素包括:对遗留系统的深入理解、谨慎的架构决策、完善的工程实践,以及团队的持续学习能力。随着 AI 技术的不断成熟和 Rails 生态的持续演进,我们有理由相信,更多的遗留系统将成功转型为智能化的现代应用。

正如 Evil Martians 团队所观察到的,Rails AI 生态系统仍处于早期阶段,但 Active Agent 等框架已经展示了将 AI 功能以 Rails 方式集成的巨大潜力。对于面临类似挑战的团队,现在正是开始探索和实践的最佳时机。

资料来源

  1. Evil Martians, "Exploring Active Agent, or can we build AI features the Rails way?", September 2025
  2. 关于 Rails 单体应用现代化和数据库迁移的最佳实践指南
查看归档
在7年历史的Rails单体应用中集成AI代理:处理遗留代码库、数据库模式兼容性和实时推理流水线的工程挑战 | Hotdry Blog