Section 13.7.3 : Intégration IA avancée
🎯 Objectif pédagogique
Intégrer des fonctionnalités IA avancées dans le projet final : streaming de réponses, gestion de contexte conversationnel, monitoring des coûts, et gestion des erreurs IA.
Streaming des réponses IA
Au lieu d'attendre la réponse complète (5-10 secondes), le streaming affiche les tokens au fur et à mesure, comme ChatGPT :
# app/services/streaming_ai_service.rb
class StreamingAiService
def self.stream_chat(messages, &block)
OPENAI_CLIENT.chat(
parameters: {
model: 'gpt-4',
messages: messages,
stream: proc { |chunk, _bytesize|
delta = chunk.dig('choices', 0, 'delta', 'content')
block.call(delta) if delta
}
}
)
end
end
# Avec Action Cable pour le streaming temps réel
# app/channels/chat_stream_channel.rb
class ChatStreamChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_stream_#{params[:conversation_id]}"
end
def ask(data)
conversation = Conversation.find(params[:conversation_id])
messages = conversation.messages.map { |m| { role: m.role, content: m.content } }
messages << { role: 'user', content: data['content'] }
# Sauvegarder le message utilisateur
conversation.messages.create!(role: 'user', content: data['content'])
# Streamer la réponse
full_response = ''
StreamingAiService.stream_chat(messages) do |token|
full_response += token
ActionCable.server.broadcast(
"chat_stream_#{conversation.id}",
{ token: token, type: 'stream' }
)
end
# Sauvegarder la réponse complète
conversation.messages.create!(role: 'assistant', content: full_response)
ActionCable.server.broadcast(
"chat_stream_#{conversation.id}",
{ type: 'complete' }
)
end
end
Gestion du contexte conversationnel
# app/services/conversation_manager.rb
class ConversationManager
MAX_CONTEXT_TOKENS = 4000
def self.build_messages(conversation)
messages = []
# System message
messages << {
role: 'system',
content: system_prompt(conversation)
}
# Contexte RAG si pertinent
if conversation.uses_rag?
context = RagService.get_context(conversation.last_user_message)
messages << {
role: 'system',
content: "Contexte documentaire :\n#{context}"
}
end
# Historique (limité pour respecter la fenêtre de contexte)
recent_messages = conversation.messages
.order(created_at: :desc)
.limit(20)
.reverse
recent_messages.each do |msg|
messages << { role: msg.role, content: msg.content }
end
# Tronquer si nécessaire
truncate_to_token_limit(messages)
end
private
def self.truncate_to_token_limit(messages)
# Garder le system message et les derniers messages
while estimate_tokens(messages) > MAX_CONTEXT_TOKENS && messages.length > 2
messages.delete_at(1) # Supprimer les plus anciens (garder system)
end
messages
end
def self.estimate_tokens(messages)
messages.sum { |m| m[:content].length / 4 } # Approximation
end
end
Monitoring des coûts
# app/models/ai_usage.rb
class AiUsage < ApplicationRecord
belongs_to :user
# Tracker chaque appel API
def self.track(user:, model:, input_tokens:, output_tokens:)
costs = {
'gpt-4' => { input: 0.03, output: 0.06 }, # par 1K tokens
'gpt-3.5-turbo' => { input: 0.0015, output: 0.002 },
'text-embedding-3-small' => { input: 0.00002, output: 0 }
}
model_cost = costs[model]
total_cost = (input_tokens / 1000.0 * model_cost[:input]) +
(output_tokens / 1000.0 * model_cost[:output])
create!(
user: user,
model: model,
input_tokens: input_tokens,
output_tokens: output_tokens,
cost_usd: total_cost
)
end
# Limites par utilisateur
def self.monthly_cost(user)
where(user: user)
.where('created_at >= ?', Time.current.beginning_of_month)
.sum(:cost_usd)
end
end
Gestion des erreurs IA
# app/services/resilient_ai_service.rb
class ResilientAiService
MAX_RETRIES = 3
RETRY_DELAY = 1 # seconde
def self.chat(prompt, **options)
retries = 0
begin
result = AiService.chat(prompt, **options)
raise 'Réponse vide' if result.blank?
result
rescue Faraday::TooManyRequestsError => e
# Rate limiting — attendre et réessayer
retries += 1
if retries <= MAX_RETRIES
sleep(RETRY_DELAY * retries)
retry
end
fallback_response('Service temporairement surchargé.')
rescue Faraday::ServerError => e
# Erreur serveur OpenAI
Rails.logger.error("OpenAI server error: #{e.message}")
fallback_response('Le service IA est temporairement indisponible.')
rescue JSON::ParserError => e
Rails.logger.error("Invalid JSON from OpenAI: #{e.message}")
fallback_response('Erreur de format dans la réponse IA.')
end
end
private
def self.fallback_response(message)
"Désolé, #{message} Veuillez réessayer dans quelques instants."
end
end
Coûts IA en production
Surveillez vos coûts OpenAI dès le début. Un utilisateur qui abuse du chat peut facilement générer 10$/jour en tokens GPT-4. Implémentez des limites quotidiennes/mensuelles par utilisateur et utilisez GPT-3.5-turbo pour les tâches simples (classification, extraction) et GPT-4 uniquement quand la qualité est critique.
Pourquoi est-il important de streamer les réponses IA au lieu d'attendre la réponse complète ?
Learn AI, from prompts to agents