Hotdry.

Article

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

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

2025-12-26ai-systems

引言:当 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 单体应用现代化和数据库迁移的最佳实践指南

ai-systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。