1. 项目概述:当Ruby遇见大语言模型
如果你是一位Ruby开发者,最近肯定没少被AI和LLM(大语言模型)刷屏。看着Python社区里各种LangChain、LlamaIndex玩得风生水起,是不是偶尔也会想:咱们Ruby生态里,有没有什么趁手的工具,能让我们优雅地集成这些强大的AI能力,而不是每次都去调用外部API或者写一堆胶水代码?今天要聊的crmne/ruby_llm,就是这样一个试图回答这个问题的项目。它不是一个简单的API封装器,而是一个旨在为Ruby应用提供一套统一、可扩展接口的LLM集成框架。
简单来说,ruby_llm想做的,是成为Ruby世界里的“LLM抽象层”。它把不同的大语言模型提供商(比如OpenAI的GPT系列、Anthropic的Claude,甚至是本地部署的模型)的差异封装起来,让你可以用一套几乎相同的Ruby代码去调用它们。这意味着,你今天用OpenAI的GPT-4写了个智能客服原型,明天想换成更便宜的Claude 3 Sonnet,或者出于数据隐私考虑换成本地部署的Llama 3,可能只需要改几行配置,核心的业务逻辑代码完全不用动。这对于需要快速迭代、进行供应商成本对比或者有特定部署要求的团队来说,价值巨大。
这个项目适合谁呢?首先是所有正在或计划在Ruby on Rails、Sinatra、Hanami等Ruby Web框架中集成AI功能的开发者。其次,是那些构建需要AI能力的CLI工具、后台任务或数据管道的工程师。即使你只是个Ruby爱好者,想在自己的小项目里试试AI的魔力,ruby_llm降低的集成门槛也值得你关注。它解决的核心痛点,正是“碎片化”——让你不必为每个AI服务商都学习一套不同的SDK用法,而是专注于用Ruby思维解决业务问题。
2. 核心设计理念与架构拆解
2.1 统一接口与适配器模式
ruby_llm最核心的设计思想源于经典的适配器模式。想象一下,你的应用程序是一个标准的欧标插头,而不同的LLM提供商(OpenAI、Anthropic等)则是美标、英标等各式各样的插座。直接连接要么不通,要么需要转接头。ruby_llm就是这个“万能转接头”的制造工厂。
它定义了一套标准的Ruby接口(或者说是一组约定),这套接口描述了“向AI模型发起一次对话”需要哪些基本元素:messages(对话历史)、model(模型名称)、temperature(创造性参数)等。然后,它为每个支持的LLM提供商(如OpenAI、Anthropic)编写一个具体的适配器类。这个适配器类的唯一职责,就是将其内部的标准接口调用,翻译成对应提供商API所要求的特定HTTP请求格式、头部信息(如认证)和参数结构。
这样做的好处是显而易见的。首先,它极大地降低了代码耦合度。你的业务代码只依赖ruby_llm定义的那套稳定接口,而不需要关心底层的API是POST /v1/chat/completions还是POST /v1/messages。当某个提供商的API发生变动时,你只需要更新对应的适配器,业务代码可以安然无恙。其次,它提升了可测试性。你可以轻松地为这个标准接口创建模拟对象,在不需要真实调用API、不产生费用的情况下,完成业务逻辑的单元测试。
2.2 配置中心化与依赖注入
另一个关键设计是中心化配置。通常,不同LLM API的认证方式(API Key、Base URL)和默认参数(如默认模型、超时时间)各不相同。ruby_llm鼓励(或要求)你在一个统一的地方进行这些配置。
在Rails项目中,你可能会在config/initializers/ruby_llm.rb这样的文件里进行全局配置:
RubyLlm.configure do |config| config.providers[:openai] = { api_key: ENV['OPENAI_API_KEY'], default_model: 'gpt-4o', request_timeout: 120 } config.providers[:anthropic] = { api_key: ENV['ANTHROPIC_API_KEY'], default_model: 'claude-3-5-sonnet-20241022' } config.default_provider = :openai # 设置默认使用的提供商 end这种模式的好处是管理清晰和环境隔离。所有敏感信息和可变设置都集中在一处,方便根据不同的环境(开发、测试、生产)切换不同的API Key或模型。通过default_provider的设置,你可以在代码中不显式指定提供商的情况下,使用一个全局默认的,这简化了大多数场景下的调用。
同时,框架通常支持在每次调用时覆盖全局配置,这为更精细的控制提供了可能。这种设计体现了“约定优于配置”和“依赖注入”的思想,让整个应用对LLM服务的依赖变得明确且可管理。
2.3 面向消息的对话管理
与早期直接拼接字符串提示词的方式不同,现代LLM应用普遍采用消息数组来管理对话上下文。ruby_llm紧跟这一最佳实践。它将一次对话抽象为一系列具有角色的消息对象。
最常见的三种角色是:
system: 系统指令,用于设定AI助手的背景、行为规范或目标任务。这条消息通常在最开始,且对整个对话有全局性影响。user: 用户输入,代表人类用户的问题或指令。assistant: AI助手的回复,在构建多轮对话时,你需要将AI的历史回复也放入上下文。
ruby_llm的接口会让你以类似下面的方式来构建一次调用:
messages = [ { role: 'system', content: '你是一个专业的Ruby代码审查助手。' }, { role: 'user', content: '请帮我优化这段代码:def find_user; User.where(active: true).first; end' } ] response = RubyLlm.chat(messages: messages, model: 'gpt-4', temperature: 0.7)这种结构化的消息管理,使得实现多轮对话、保持对话历史、以及实现复杂的对话流程(比如让AI先思考再回答的“Chain of Thought”)变得更加自然和模块化。框架内部会负责将这些消息对象序列化成各个API所需的具体格式。
3. 核心功能深度解析与实操
3.1 基础聊天补全功能实现
让我们从最核心的chat(聊天补全)功能开始,看看如何用ruby_llm完成一次完整的AI交互。假设我们正在构建一个代码解释器功能。
首先,你需要确保已经通过bundle安装了ruby_llmgem,并完成了上述的初始化配置。一个完整的调用示例如下:
require 'ruby_llm' # 方式1:使用全局默认配置的提供商 response = RubyLlm.chat( messages: [ { role: 'system', content: '你是一个资深软件工程师,擅长用简洁清晰的语言解释代码。' }, { role: 'user', content: '解释一下Ruby中`&:symbol`这种写法的含义和作用。' } ], model: 'gpt-4o', # 可覆盖全局配置的默认模型 temperature: 0.3, # 控制创造性,越低越确定,越高越随机。解释代码适合较低值。 max_tokens: 500 # 限制回复的最大长度 ) puts response.dig('choices', 0, 'message', 'content') # 预期输出一个关于Symbol#to_proc的详细解释 # 方式2:显式指定使用某个提供商(例如Anthropic) response = RubyLlm.with_provider(:anthropic).chat( messages: [...], model: 'claude-3-haiku-20240307' )关键参数解析:
temperature(0.0 ~ 2.0):这是控制输出随机性的核心参数。0意味着模型每次都会给出最确定、概率最高的下一个词,结果可重复性强,适合事实问答、代码生成。0.7~1.0是常见的创造性写作范围。2.0则会让输出变得非常天马行空。对于需要稳定、准确输出的生产环境任务,建议从0.1到0.3开始尝试。max_tokens:限制AI单次回复的令牌数(约等于单词数)。必须设置,否则可能产生超长回复,消耗大量token。需要根据任务预估,例如简短回答设200,长文分析设1000。model:指定使用的具体模型。不同提供商、不同模型的性能、价格、上下文长度差异巨大。务必查阅官方文档,选择适合你任务和预算的模型。
注意:API调用是异步且耗时的网络操作。在生产环境的Web请求中,绝对不要同步等待LLM回复,这会导致请求超时。正确的做法是将LLM调用放入后台任务队列(如Sidekiq、GoodJob),通过WebSocket或轮询向客户端推送结果。
3.2 流式响应处理
对于需要长时间生成内容(如写长邮件、生成报告)或希望实现类似ChatGPT那样逐字打印效果的场景,流式响应是必备功能。它允许服务器在AI生成内容的同时,就分块(chunk)地将数据推送给客户端,极大地提升了用户体验。
ruby_llm通常也会提供流式接口。其核心是处理一个Server-Sent Events (SSE)或类似分块数据的流。下面是一个在Rails控制器或后台任务中处理流式响应的简化示例:
# 假设在Rails的一个Action中 def stream_explanation response.headers['Content-Type'] = 'text/event-stream' response.headers['Cache-Control'] = 'no-cache' messages = [{ role: 'user', content: '用500字介绍Ruby的元编程' }] # 调用流式接口,传入一个处理每个数据块的block RubyLlm.chat_stream(messages: messages) do |chunk| # chunk通常是一个Hash,包含增量内容或完成状态 content = chunk.dig('choices', 0, 'delta', 'content') if content # 将内容推送到前端 response.stream.write("data: #{JSON.dump({text: content})}\n\n") end end ensure response.stream.close end在前端,你需要使用EventSourceAPI来连接这个端点并监听消息事件。流式处理的关键在于连接管理和错误处理。网络可能中断,流可能意外结束。你的代码需要确保在发生错误时能优雅地关闭连接,并通知前端。同时,流式响应会保持一个HTTP连接长时间开放,这对服务器的并发连接数有要求,需要评估你的应用服务器(如Puma)配置。
3.3 函数调用与工具集成
这是让LLM从“聊天机器人”升级为“智能体”的关键功能。函数调用允许你定义一系列工具(函数),描述它们的名称、作用和参数格式,然后LLM可以根据用户的问题,智能地决定是否需要调用某个工具,并生成符合你定义的参数格式的JSON。
例如,你有一个查询数据库用户信息的功能:
# 1. 定义工具(函数)列表 tools = [ { type: 'function', function: { name: 'get_user_profile', description: '根据用户ID获取用户的姓名和邮箱', parameters: { type: 'object', properties: { user_id: { type: 'integer', description: '用户的唯一ID' } }, required: ['user_id'] } } } ] # 2. 在聊天请求中传入工具定义 response = RubyLlm.chat( messages: [{ role: 'user', content: '请帮我查一下用户12345的邮箱地址。' }], tools: tools, tool_choice: 'auto' # 让模型自行决定是否调用工具 ) # 3. 解析响应,检查模型是否决定调用工具 message = response.dig('choices', 0, 'message') if message['tool_calls'] tool_call = message['tool_calls'].first if tool_call['function']['name'] == 'get_user_profile' arguments = JSON.parse(tool_call['function']['arguments']) user_id = arguments['user_id'] # 4. 执行实际的函数(查询数据库) user = User.find_by(id: user_id) result = user ? { name: user.name, email: user.email } : { error: '用户未找到' } # 5. 将函数执行结果作为新的消息,再次发送给模型,让它生成面向用户的最终回答 follow_up_response = RubyLlm.chat( messages: [ { role: 'user', content: '请帮我查一下用户12345的邮箱地址。' }, message, # 包含工具调用的消息 { role: 'tool', tool_call_id: tool_call['id'], content: result.to_json } ], tools: tools ) final_answer = follow_up_response.dig('choices', 0, 'message', 'content') puts final_answer # “用户张三的邮箱是 zhangsan@example.com” end end这个过程实现了LLM与现实世界系统(数据库、API、内部服务)的联结。设计工具描述是关键:description要清晰准确,parameters的JSON Schema定义要严谨,这直接影响到模型调用的准确率。通常需要反复调试提示词和工具描述。
4. 高级应用场景与模式
4.1 构建链式调用与智能体
单一的问-答模式往往不够。复杂的任务需要将多个LLM调用、工具调用和条件判断串联起来,形成一个工作流,这就是链。ruby_llm作为底层引擎,可以很方便地用来构建这样的链。
一个经典的链式应用是检索增强生成。假设我们要回答一个关于公司内部文档的问题:
# 伪代码,展示逻辑链 class DocumentQaAgent def answer(question) # 第一步:将用户问题转换为搜索查询词 search_query = generate_search_query(question) # 第二步:使用查询词从向量数据库检索相关文档片段 relevant_chunks = vector_db.search(search_query, limit: 5) # 第三步:将问题和检索到的上下文一起发给LLM,生成最终答案 context = relevant_chunks.join("\n---\n") final_prompt = <<~PROMPT 基于以下上下文信息,回答用户的问题。如果上下文不包含答案,请直接说“根据现有信息无法回答”。 上下文: #{context} 问题:#{question} 答案: PROMPT response = RubyLlm.chat(messages: [{ role: 'user', content: final_prompt }]) response.dig('choices', 0, 'message', 'content') end private def generate_search_query(question) # 使用LLM优化问题,使其更适合检索 messages = [ { role: 'system', content: '你是一个搜索查询优化器。将用户的问题提炼成2-3个最相关的关键词或短语,用空格分隔。' }, { role: 'user', content: question } ] resp = RubyLlm.chat(messages: messages, temperature: 0.1) resp.dig('choices', 0, 'message', 'content').strip end end在这个链中,我们两次调用了RubyLlm.chat,第一次用于优化查询,第二次用于生成最终答案,中间穿插了数据库检索操作。你可以将这个模式扩展,加入验证步骤、多路检索、投票表决等,构建出非常强大的智能体。
4.2 实现异步处理与任务队列集成
如前所述,LLM调用耗时且可能失败,必须异步化。在Rails生态中,Sidekiq是最常见的选择。下面是一个将LLM摘要生成任务放入后台的完整示例:
# app/jobs/summarize_article_job.rb class SummarizeArticleJob < ApplicationJob queue_as :default retry_on StandardError, wait: :exponentially_longer, attempts: 3 # 配置重试 def perform(article_id) article = Article.find(article_id) # 构建提示词 prompt = "请用三段话总结以下文章的核心观点:\n\n#{article.content[0..5000]}" # 限制长度 begin response = RubyLlm.chat( messages: [{ role: 'user', content: prompt }], model: 'gpt-3.5-turbo', # 摘要任务可用成本更低的模型 temperature: 0.2 ) summary = response.dig('choices', 0, 'message', 'content') # 更新文章记录 article.update(summary: summary) # 可选:通知用户(如通过Action Cable) ActionCable.server.broadcast("article_#{article_id}", { type: 'summary_ready', summary: summary }) rescue RubyLlm::ApiError => e # 处理API错误,如额度不足、模型不可用 Rails.logger.error "LLM API Error for Article #{article_id}: #{e.message}" raise e # 触发Sidekiq重试 rescue JSON::ParserError, Net::ReadTimeout => e # 处理网络或解析错误 Rails.logger.error "Network/Parse Error for Article #{article_id}: #{e.message}" raise e end end end # 在控制器或服务中触发任务 SummarizeArticleJob.perform_later(@article.id)关键点:
- 错误处理与重试:LLM API可能因网络、限速、服务不可用而失败。必须用
rescue捕获特定异常,并利用Sidekiq的重试机制。对于暂时性错误(如限速429),重试是有效的。 - 超时设置:在
RubyLlm的全局配置或调用参数中设置合理的request_timeout。对于长文本,可能需要超过30秒。 - 队列隔离:建议为LLM任务创建独立的Sidekiq队列(如
queue_as :llm),并配置单独的worker进程来处理,避免慢任务阻塞其他关键业务作业。 - 成本与限流:在Job中记录每次调用的token使用量(如果API返回),便于监控成本。考虑在应用层实现限流,防止意外循环触发大量API调用导致巨额账单。
4.3 提示词工程与模板管理
随着应用复杂化,提示词会变得又长又复杂,且经常需要动态插入变量。将提示词硬编码在代码中是维护的噩梦。一个好的实践是引入提示词模板管理。
你可以创建一个简单的模板系统:
# app/services/prompt_templates.rb module PromptTemplates TEMPLATES = { code_review: <<~PROMPT.freeze, 你是一位严格的Ruby高级工程师。请审查以下代码: ``` {code} ``` 请从以下方面提供反馈: 1. 代码风格和约定(遵循Ruby社区最佳实践,如RuboCop)。 2. 潜在的性能问题(如N+1查询、低效循环)。 3. 可能的边界情况或错误处理缺失。 4. 给出具体的改进建议和修改后的代码示例。 请用中文回复,结构清晰。 PROMPT customer_support: <<~PROMPT.freeze, 你是{company_name}的客服AI助手,专业、友好、耐心。 根据以下知识库回答问题: {knowledge_base} 用户问题:{user_question} 如果知识库中有明确答案,请直接回答。如果没有,请礼貌地表示无法解决,并建议用户通过{contact_channel}联系人工客服。 PROMPT } def self.render(template_name, variables = {}) template = TEMPLATES[template_name] or raise "Template #{template_name} not found" template.gsub(/\{(\w+)\}/) { variables[$1.to_sym] || "{" + $1 + "}" } end end # 使用示例 code = File.read('app/models/user.rb') prompt_text = PromptTemplates.render(:code_review, { code: code }) response = RubyLlm.chat(messages: [{ role: 'user', content: prompt_text }])更进一步,可以将模板存储在数据库或YAML文件中,并开发一个简单的管理界面,让非技术人员也能更新提示词。将系统指令、少样本示例、输出格式要求都封装在模板里,能让你的核心业务代码保持干净,并实现提示词的版本控制和A/B测试。
5. 生产环境部署与优化实战
5.1 性能优化与缓存策略
LLM API调用是应用的主要延迟来源和成本中心。实施有效的缓存策略可以大幅提升响应速度并降低成本。
1. 语义缓存:对于内容生成类任务(如文章摘要、产品描述生成),如果输入提示词完全相同,输出理应相同。我们可以对提示词进行哈希(如MD5或SHA256),将哈希值作为缓存键。
# 使用Rails.cache的简化示例 def cached_chat(prompt, options = {}) cache_key = "llm:chat:#{Digest::SHA256.hexdigest(prompt.to_s)}" Rails.cache.fetch(cache_key, expires_in: 1.week) do RubyLlm.chat(messages: [{ role: 'user', content: prompt }], **options) end end # 使用 summary = cached_chat("总结文章:#{article_content}", model: 'gpt-3.5-turbo')但要注意:对于temperature > 0的请求,输出具有随机性,缓存可能不合适。通常只对temperature=0或极低值的确定性任务使用缓存。
2. 向量相似度缓存(高级):更智能的缓存是存储提示词的嵌入向量。当新的用户问题到来时,先计算其嵌入向量,在缓存中查找相似度(余弦相似度)超过某个阈值(如0.95)的历史提示词及其回答,直接返回缓存结果。这可以处理“意思相同但表述不同”的情况。这需要集成向量数据库(如Qdrant, Pinecone),实现更复杂,但命中率更高。
3. 令牌使用优化:
- 精简提示词:移除不必要的礼貌用语、冗余解释。使用更精确的指令。
- 设置
max_tokens:根据历史回复数据,设定一个合理的上限,避免为无用长文付费。 - 上下文窗口管理:对于长对话,旧消息会消耗token。可以设计策略,在上下文达到一定长度时,选择性丢弃最早或最不重要的消息,或生成一个“对话摘要”作为新的系统消息。
5.2 监控、日志与可观测性
在生产环境中,必须对LLM调用进行全面的监控。
1. 结构化日志记录:不要只记录“调用了API”,要记录关键指标,便于后续分析和审计。
# 在初始化器或中间件中封装日志 RubyLlm.configure do |config| config.before_request do |request_params| Rails.logger.info("[LLM Request Start] Provider: #{request_params[:provider]}, Model: #{request_params[:model]}") request_params[:start_time] = Time.now end config.after_request do |response, request_params| duration = Time.now - request_params[:start_time] token_usage = response.dig('usage') Rails.logger.info( "[LLM Request End] Duration: #{duration.round(2)}s, " \ "Tokens: #{token_usage&.dig('total_tokens') || 'N/A'}, " \ "Model: #{request_params[:model]}" ) end end2. 关键指标监控:
- 延迟:P95/P99响应时间。如果延迟飙升,可能是提供商问题或网络问题。
- 错误率:4xx/5xx错误比例。错误率升高需要立即告警。
- 令牌消耗:记录每次调用的输入、输出和总令牌数。可以按模型、按功能聚合,用于成本分析和预算控制。
- 速率限制:监控429(Too Many Requests)错误,调整你的请求频率或实现退避重试。
3. 链路追踪:在微服务架构中,将LLM调用纳入你的分布式追踪系统(如OpenTelemetry)。为每次调用生成一个唯一的Trace ID,这样当用户报告一个AI回答有问题时,你可以回溯到完整的请求参数、响应和当时的系统状态。
5.3 安全与合规考量
集成第三方AI服务,安全是重中之重。
1. 数据隐私与脱敏:绝对不要将用户个人身份信息、密码、密钥、内部IP等敏感数据直接发送给外部LLM API。在构建提示词前,必须进行数据脱敏。
# 简单的脱敏示例 class SensitiveDataScrubber def self.scrub(text) text.gsub!(/\b\d{3}[-.]?\d{2}[-.]?\d{4}\b/, '[SSN_REDACTED]') # 模拟社会安全号 text.gsub!(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, '[EMAIL_REDACTED]') # 邮箱 text.gsub!(/\b(?:\d{1,3}\.){3}\d{1,3}\b/, '[IP_REDACTED]') # IP地址 text end end safe_prompt = SensitiveDataScrubber.scrub(user_generated_content)对于企业级应用,考虑使用本地部署的模型或提供数据保密协议的云服务。
2. 内容安全与审核:LLM可能生成有害、偏见或不符合政策的内容。即使有系统指令约束,也不能完全信任。
- 输入审核:在将用户输入发送给LLM前,用关键词过滤或轻量级分类模型进行初步筛查。
- 输出审核:对AI生成的内容进行二次审核。可以调用另一个专门的内容安全API,或使用一套规则引擎进行检查,再将内容展示给用户或存入数据库。
3. 成本控制与预算:
- 预算告警:设置每日/每月的令牌消耗或金额预算,达到阈值时触发告警(邮件、Slack)。
- 用户级限流:对免费用户或不同套餐等级的用户,限制其每天/每月的LLM调用次数或总令牌数。
- 熔断机制:当某个提供商的API错误率持续过高时,自动切换到备用提供商。
6. 常见问题排查与调试技巧
即使有了完善的框架,在实际开发中你依然会遇到各种问题。以下是一些常见坑点及其解决方案。
6.1 网络与API调用问题
问题:超时或连接错误。
- 排查:首先确认网络连通性。使用
curl或Postman直接测试目标API端点。检查是否位于需要代理的网络环境(注意:此处仅讨论企业内网代理,绝对不涉及任何违规翻墙行为)。 - 解决:
- 增加
request_timeout配置,对于长上下文或慢模型,可能需要60秒以上。 - 在
RubyLlm的适配器层或通过Net::HTTP库配置代理(如需)。例如,在初始化时设置http_proxy环境变量。 - 实现重试逻辑,使用指数退避算法。许多HTTP客户端库(如
Faraday)内置了此功能。
- 增加
问题:API返回429(速率限制)错误。
- 排查:检查提供商文档的速率限制(RPM-每分钟请求数,TPM-每分钟令牌数)。使用监控日志统计你应用的调用频率。
- 解决:
- 应用层限流:使用
Ratelimiter等gem,在向API发送请求前进行限流。 - 队列与延迟:将非实时请求全部放入队列,由后台作业以平稳速率消费。
- 升级计划:如果业务量确实大,考虑联系服务商升级API套餐。
- 应用层限流:使用
6.2 模型行为与输出问题
问题:模型不遵循指令或“胡言乱语”。
- 排查:首先检查
system消息是否设置正确且位置在最前。检查temperature参数是否设置过高(如>1.0),导致随机性太强。 - 解决:
- 强化系统指令:在
system消息中更明确、更强势地规定角色和格式。例如:“你必须以JSON格式输出,且只包含以下两个字段:summary和confidence。” - 使用更低temperature:对于需要确定性的任务,尝试将
temperature设为0或0.1。 - 少样本提示:在
messages中提供1-2个输入输出的示例,让模型模仿。 - 尝试不同模型:某些任务在GPT-4上表现好,在Claude上可能一般,反之亦然。
- 强化系统指令:在
问题:回复被意外截断。
- 排查:检查
max_tokens参数设置是否过小。模型输出达到这个限制后会停止。 - 解决:合理估算回复长度。如果不确定,可以设置一个较大的值(如2000),但同时监控成本。更精细的做法是:先让模型用一句话概括长度,再动态调整
max_tokens进行第二次调用。
6.3 框架集成与配置问题
问题:初始化RubyLlm时提示缺少配置或适配器未找到。
- 排查:检查初始化文件是否被正确加载(Rails中确保在
config/initializers目录下)。检查providers配置的键名是否与调用时指定的:provider符号一致(注意大小写,Ruby符号通常使用小写和蛇形命名,如:openai)。 - 解决:
- 在调用
RubyLlm.chat之前,先打印RubyLlm.configuration.providers.keys查看已注册的提供商。 - 确保环境变量(如
ENV['OPENAI_API_KEY'])已正确设置,可以通过rails console进行验证。 - 如果使用了自定义适配器,确保其加载路径正确,并继承了正确的基类。
- 在调用
问题:在测试环境中,不想真实调用API。
- 解决:利用依赖注入和模拟对象。你可以为
RubyLlm模块创建一个测试替身。
这样,你的业务逻辑测试就可以在不依赖网络和真实API的情况下运行,速度快且稳定。# spec/support/llm_helper.rb module FakeLlm def self.chat(messages:, **) # 返回一个结构固定的模拟响应 { 'id' => 'chatcmpl-fake', 'choices' => [ { 'message' => { 'role' => 'assistant', 'content' => "这是对消息的模拟回复。输入消息是:#{messages.last[:content][0..50]}..." } } ] } end end # 在测试中替换 RSpec.configure do |config| config.before(:each, type: :feature) do allow(RubyLlm).to receive(:chat).and_wrap_original do |original_method, *args| if Rails.env.test? FakeLlm.chat(*args) else original_method.call(*args) end end end end