探索Active Agent:用Rails方式构建AI功能的技术实践

本文深入探讨了如何在Ruby on Rails应用中集成AI功能,重点介绍了Active Agent框架的技术架构、实际应用案例和未来发展方向,包括工具集成、测试策略和架构设计考量。

探索Active Agent:用Rails方式构建AI功能

在AI技术渗透到每个角落的快节奏世界中,框架和生态系统必须快速适应不断演变的业务需求。每个应用都需要智能功能。在使用某些框架构建的项目中添加AI驱动功能的便捷性,直接决定了该框架能否成为首选工具。假设"某些框架"是"Ruby on Rails",让我们探索这个生态系统的AI就绪程度!

Ruby和Rails的AI生态系统

Ruby和Rails的AI生态系统在AI革命爆发后迅速起飞。从非官方的LLM提供商SDK(如ruby-openai)发展到专门的库,如Raix和RubyLLM。每个库都提供了将大语言模型集成到Ruby应用程序的不同方法,但对我来说,它们都没有提供Rails所缺失的AI抽象层。

我更喜欢遵循Rails约定和设计原则的抽象,能带来熟悉的用户体验。Raix和RubyLLM都专注于为与AI聊天提供通用接口(它们做得很好),但没有超越这个层面(在分层架构意义上,它们主要覆盖基础设施层)。

Active Agent的承诺

另一方面,Active Agent承诺填补Rails抽象堆栈中的空白,并以Rails的方式带来LLM驱动的功能。

当我从朋友Justin那里听说Active Agent时,这正是吸引我的地方。巧合的是,那时我开始考虑《Ruby on Rails应用程序分层设计》第二版的想法,而Active Agent背后的概念听起来正是我为相应新章节所设想的。因此,我决定成为早期采用者,并在Evil Martians推广Active Agent。

Active Agent是否兑现了作为Rails原生AI库的承诺?让我们通过实际示例尝试回答这个问题,并探索Rails中AI的未来可能。

Active Agent简介

Active Agent gem为Rails带来了新的抽象:代理(毫不意外!)。代理旨在封装AI支持的逻辑,并通过使用熟悉的Rails模式将其与框架的其余部分粘合在一起:动作驱动对象(如控制器、通道和邮件程序)、回调(当然!)以及由Action View支持的提示渲染。虽然控制器负责将HTTP请求转换为响应,邮件程序负责编写和发送电子邮件,但代理的主要目的是触发各种AI生成。

让我们通过尝试实现AI的"hello world"——“给我讲个故事"代理来看看Active Agent的实际应用。不过在我们的案例中,将是"给我讲个笑话”。

以下是JokerAgent类的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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

对于Rails开发人员来说,这段代码看起来非常熟悉,几乎不需要解释它的作用。接下来,我们可以这样使用它:

1
2
3
4
5
6
7
8
result = JokerAgent.nerd_joke("Ruby on Rails").generate_now
puts result.message.content

#=> 为什么Ruby on Rails开发人员总是带着铅笔?
#=> 因为他们想绘制自己的路由!

user = User.find_by(email: "palkan@evl.ms")
JokerAgent.with(user:).dad_joke("Ruby on Rails").generate_later

如您所见,有两种模式可用:您可以立即获取生成结果(同步)或将其移动到后台作业(尽管在这种情况下,您必须使用回调来处理结果)。添加使用提示对象作为LLM请求表示,您将看到它如何与Action Mailer非常相似,只是将"deliver_“替换为"generate_"。

实际应用案例

案例1:类似Twitter的按需翻译

我首次测试Active Agent的机会是在它宣布后不久。我正在开发一个内部Martian团队建设项目,并参与典型的帖子-评论-回复对话层次结构。

我们的团队是多语言的,为了让这个项目"温暖而舒适”,我想让任何人都能用他们的母语表达自己的想法并消费他人的想法。因此,我们需要一个翻译功能,我决定采用按需翻译的方式。

以下是翻译代理代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

    # 翻译结果的形式为:<from>-><to>: <content>
    #(当时我们没有结构化输出支持)
    _, original_locale, locale, content = result.split(/^(\w{2})->(\w{2}):\s*/)

    return unless original_locale.present? && locale.present? && content.present?

    record.translations << Translation.new(locale:, content:) unless locale == original_locale
    record.original_locale = original_locale
    record.save!
  end
end

这个例子引发了一个有趣的架构问题:代理是否应该封装整个操作,包括数据库更新,还是只生成结构化输出供其他抽象处理?换句话说,这个#update_record方法是否是架构异味?它是否遵循诸如关注点分离之类的常见良好设计原则?

案例2:Redprints CFP的AI审稿人

我们的第二个示例来自使用AI驱动的提案审稿人扩展我们的Redprints CFP应用程序。该代理评估会议提案,提供分数,并提供建设性反馈,以帮助组织者做出明智决策。

我们实验的ReviewAgent是针对SF Ruby会议的需求量身定制的。我们采用了三个标准来获取每个提案的初始分数:新颖性、相关性和质量(提案本身)。因此,我们需要教导我们的代理如何评估每个标准。

工具集成以查找演讲新颖性

评估标准之一是新颖性:我们希望对新演讲和主题给予更高评分。LLM是否能够知道哪些演讲和主题已经被过度使用?也许。然而,“也许"不是我们可以接受的非确定性水平。因此,我们决定为我们的代理提供搜索Ruby演讲数据库的能力,并根据新颖性评分决定正在审查的提案之一。

首先,我们准备了来自最近几年选定会议的Ruby和Rails演讲的搜索索引。我们获取了RubyEvents"数据库”(YAML文件)并将其转换为Trieve数据集。

然后,我们为ReviewAgent定义了#search_talks工具:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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:} }
    end
  end
end

工具只是一个方法,类似于其他操作。工具参数通过#params Hash访问。您可以使用模板来渲染结果。

测试策略

可维护代码的关键属性之一是可测试性。Active Agent没有提供任何现成的测试工具。当然,您可以使用Webmock或VCR来存根对LLM API的HTTP请求,但这对我来说似乎不太合适。

幸运的是,Active Agent以类似于Active Storage适配化存储服务或Action Mailer适配化传递机制的方式适配化生成提供者。唯一缺失的部分(目前)是适用于测试环境的生成提供者。不用担心,我们可以自己添加!

这是我们用于测试的fake_llm提供者版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
require "active_agent/generation_provider/response"

module ActiveAgent
  module GenerationProvider
    class FakeLLMProvider < Base
      attr_reader :response

      class << self
        def generate_response_content
          raise NotImplementedError, "必须通过以下方式存根:allow(ActiveAgent::FakeLLM).to receive(:generate_response_content).and_return('...')"
        end

        def generations
          Thread.current[:generations] ||= []
        end
      end

      def initialize(*)
      end

      def generate(prompt)
        @prompt = prompt

        raw_response = prompt_parameters

        message = ActiveAgent::ActionPrompt::Message.new(
          content: self.class.generate_response_content,
          role: "assistant"
        )

        # 跟踪执行的提示以验证其内容
        self.class.generations << prompt

        @response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:, raw_response:)
      end

      private

      def prompt_parameters
        {
          messages: @prompt.messages.map(&:to_h),
          tools: @prompt.actions
        }
      end
    end
  end
end

未来挑战:Rails AI还需要什么

我们使用Active Agent的经验表明,虽然它为AI交互提供了类似Rails的约定,但AI应用程序领域需要更复杂的抽象。更准确地说,鉴于现代AI不断发展的性质,我们需要一个灵活且可扩展的框架,能够更快地适应变化。

以下是在为Rails应用程序构建AI驱动功能时可能需要额外工作的一些领域:

使用跟踪和AI积分

每个AI应用程序都需要使用监控和限制。用户应该有AI积分,租户应该有预算控制。AI引擎必须提供钩子来:a:跟踪使用情况,b:在没有积分可用时防止使用AI。

动态凭证和私有LLM

用户应该能够使用自己的API密钥或私有模型部署。

动态提示

硬编码的提示可能适用于较小的应用程序或基本用例。但随着使用量的增加,AI驱动的逻辑变得更加复杂,需要更多的优化。

防护和评估

您听说过提示注入吗?您确定您的提示生成的是您期望的内容,而不是一些幻觉吗?

安全始终是Rails的首要任务。AI框架必须鼓励用户构建安全的AI功能。必须验证用户输入,提示必须包括防护栏,输出也必须进行评估。

代理工作流

尽管我不会接触代理工作流(即任务编排由AI控制的工作流,因此是非确定性的),但对于某些功能来说,它可能是一个不错的选择。代理工作流可以建模为入口点编排器代理,在需要时将工作委托给其他代理。

内存和上下文持久化

对于对话式AI功能,请考虑RubyLLM。它开箱即用地提供消息持久化(由Active Record支持),非常适合此类用例。

代理通常需要记住先前的交互或在整个对话中维护上下文。内存可以是静态的(完全包含在上下文中)或动态的(可通过工具由LLM请求)、短期或长期、压缩或无损失。这些都值得拥有一个可集成到其他库中的专用库。

前进的道路

开放问题的数量是巨大的,大多数Rails AI库——包括Active Agent——仍然太年轻,无法回答所有问题。然而,我们应该期望这些库展示对日常需求的理解,并以可扩展的方式设计自己,以便其他人可以通过插件解决特定问题。

Active Agent显示出作为Rails式AI功能基础的前景。然而,真正的考验将是它是否能够演变为支持生产AI应用程序所需的复杂模式。该框架的成功最终将取决于其提供扩展点的能力,用于使用跟踪、高级检测、复杂工作流以及当AI从原型转向生产时出现的无数其他需求。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计