ابنِ مساعداً طرفياً بلغة Python مع موفرَي نماذج صريحين، وجلسات محادثة في SQLite، وسجل محدود، وإعادات محاولة مضبوطة، واختبارات.
ما الذي ستبنيه
يبني هذا الدرس التقني تطبيق محادثة محلياً بلغة Python يتيح للمستخدم الاختيار بين نموذج بأسلوب OpenAI ChatGPT وAnthropic Claude Sonnet ٥. يستخدم التطبيق تنسيق رسائل داخلياً واحداً، ويخزن الجلسات في SQLite، ولا يحتفظ إلا بعدد محدود من الرسائل الحديثة في كل طلب، ويجعل اختيار الموفر ظاهراً في الطرفية.
التوقيت مهم. فقد قدمت Anthropic نموذج Claude Sonnet ٥ في ٣٠ يونيو ٢٠٢٦ بوصفه أكثر نماذج Sonnet لديها قدرةً على العمل كوكيل. وتقول Anthropic إن النموذج يستطيع وضع الخطط، واستخدام أدوات مثل المتصفحات والطرفيات، والعمل باستقلالية بمستوى قدرات كان يتطلب حديثاً نماذج أكبر وأكثر كلفة. كما تضع Sonnet ٥ قريباً من أداء Opus ٤.٨ بأسعار أقل، مع تحسينات مقارنة بـ Sonnet ٤.٦ في الاستدلال، واستخدام الأدوات، والبرمجة، والعمل المعرفي.
لكن ذلك لا يعني أن على عميل محادثة محلي منح النموذج تلقائياً صلاحية الوصول إلى متصفح أو طرفية أو نظام عملاء أو قاعدة بيانات داخلية. ينفذ هذا الدرس التقني محادثة نصية فقط عن قصد. فهو ينشئ أولاً حدوداً موثوقة لمقارنة النماذج وسير عمل المحادثة. وإذا أضفت أدوات لاحقاً، فيجب أن يتحقق كود التطبيق الحتمي من الصلاحيات والوسائط والمهل الزمنية ومتطلبات الموافقة قبل تنفيذ أي إجراء خارجي.
يفيد هذا النهج فرق الهندسة في دول الخليج والشرق الأوسط التي تحتاج إلى تقييم أكثر من موفر واحد للذكاء الاصطناعي مع الاحتفاظ بالتحكم في معمارية تطبيقاتها. لا يرسل التطبيق بصمت طلب Claude فاشلاً إلى OpenAI أو العكس. يختار المستخدم الموفر، ما يجعل سلوك التوجيه واضحاً أثناء التقييم التقني ومراجعة الحوكمة.
المعمارية
config.pyيقرأ متغيرات البيئة المطلوبة ويتحقق من الحدود المحلية الآمنة.providers.pyيحول تنسيق محادثة داخلياً واحداً إلى تنسيق طلب كل موفر.storage.pyينشئ جلسات SQLite دائمة ويسترجع السجل الحديث بترتيب زمني.chat.pyيوفر حلقة الطرفية والأوامر وإعادات المحاولة المضبوطة وتوجيه الموفر.
مهايئ الموفر هو قرار التصميم المهم. تعتمد بقية أجزاء البرنامج على عقد داخلي صغير بدلاً من الاعتماد مباشرة على SDK خاص بمورد معين. وهذا يجعل اختبار التطبيق أسهل، ويتيح لك إضافة بوابة داخلية معتمدة لاحقاً دون إعادة كتابة طبقة التخزين أو معالجة الأوامر.
المتطلبات الأساسية والإعداد
- Python ٣.١٠ أو أحدث.
- مفتاح OpenAI API ومعرف نموذج متاح لحسابك.
- مفتاح Anthropic API وصلاحية الوصول إلى Claude Sonnet ٥.
- معرفة أساسية بالبيئات الافتراضية ومتغيرات البيئة والطرفية.
- SQLite، وهي مضمنة مع تثبيتات CPython القياسية.
mkdir multi-model-chat
cd multi-model-chat
python -m venv .venv
# macOS and Linux
source .venv/bin/activate
# Windows PowerShell
# ..venvScriptsActivate.ps1
python -m pip install --upgrade pip
python -m pip install openai anthropic rich pytest
mkdir testsلا تضع بيانات الاعتماد في ملفات مصدر Python. اضبطها في بيئة الصدفة لديك، أو مخزن أسرار CI، أو بيئة تشغيل الحاوية، أو مدير الأسرار المعتمد للنشر. يتطلب التطبيق اسم نموذج مُعداً لكل موفر لأن توافر النماذج يعتمد على الحساب.
# macOS and Linux
export OPENAI_API_KEY="your-openai-key"
export OPENAI_MODEL="your-openai-model"
export ANTHROPIC_API_KEY="your-anthropic-key"
export ANTHROPIC_MODEL="claude-sonnet-5"
# Optional local limits
export MAX_HISTORY_MESSAGES="20"
export MAX_OUTPUT_TOKENS="1200"
export REQUEST_TIMEOUT_SECONDS="60"في Windows PowerShell، استخدم صيغة $env:OPENAI_API_KEY="..." بدلاً من ذلك. وفي بيئة إنتاج، مرر أسماء المتغيرات نفسها عبر آلية الأسرار المُدارة في المنصة. لا تطبع المفاتيح في السجلات مطلقاً، ولا تدرجها في Git، ولا تشحنها إلى كود المتصفح.
الخطوة ١: أضف التحقق من الإعدادات
أنشئ config.py. تحافظ هذه الوحدة الصغيرة على فصل الإعدادات عن منطق الأعمال، وتفشل مبكراً عندما يكون أي حد غير صالح. ولا تتطلب اعتماد dotenv؛ إذ إن متغيرات البيئة هي مدخلاتها الوحيدة.
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Settings:
openai_api_key: str | None
openai_model: str | None
anthropic_api_key: str | None
anthropic_model: str | None
max_history_messages: int
max_output_tokens: int
request_timeout_seconds: float
database_path: Path
def require_openai(self) -> tuple[str, str]:
if not self.openai_api_key or not self.openai_model:
raise RuntimeError(
"OPENAI_API_KEY and OPENAI_MODEL are required for OpenAI."
)
return self.openai_api_key, self.openai_model
def require_anthropic(self) -> tuple[str, str]:
if not self.anthropic_api_key or not self.anthropic_model:
raise RuntimeError(
"ANTHROPIC_API_KEY and ANTHROPIC_MODEL are required for Anthropic."
)
return self.anthropic_api_key, self.anthropic_model
def read_positive_int(name: str, default: int, minimum: int) -> int:
value = int(os.getenv(name, str(default)))
if value < minimum:
raise ValueError(f"{name} must be at least {minimum}.")
return value
def get_settings() -> Settings:
timeout = float(os.getenv("REQUEST_TIMEOUT_SECONDS", "60"))
if timeout <= 0:
raise ValueError("REQUEST_TIMEOUT_SECONDS must be greater than zero.")
return Settings(
openai_api_key=os.getenv("OPENAI_API_KEY"),
openai_model=os.getenv("OPENAI_MODEL"),
anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
anthropic_model=os.getenv("ANTHROPIC_MODEL"),
max_history_messages=read_positive_int(
"MAX_HISTORY_MESSAGES", default=20, minimum=2
),
max_output_tokens=read_positive_int(
"MAX_OUTPUT_TOKENS", default=1200, minimum=1
),
request_timeout_seconds=timeout,
database_path=Path(os.getenv("SQLITE_DATABASE_PATH", "chat_history.sqlite3")),
)حد عدد الرسائل هو إجراء وقائي بسيط، وليس عداداً للرموز. إذ يمكن للنماذج المختلفة ترميز النص نفسه بطرق مختلفة، ولا يمثل عدد أحرف قصير بديلاً موثوقاً لحجم الطلب. إن إبقاء قاعدة الاقتطاع معزولة يعني أنه يمكنك استبدالها لاحقاً بميزانية رموز تراعي الموفر أو بالتلخيص.
الخطوة ٢: أنشئ طبقة مهايئ الموفر
أنشئ providers.py. تستخدم OpenAI وAnthropic أشكالاً مختلفة للطلبات والاستجابات. يحول المهايئ الاستجابتين إلى CompletionResult، ولذلك لا تحتاج CLI إلى كود تحليل خاص بكل موفر.
from __future__ import annotationsfrom dataclasses import dataclass
from typing import Literal, Protocol, Sequencefrom anthropic import Anthropic
from openai import OpenAIfrom config import SettingsRole = Literal["user", "assistant"]
ProviderName = Literal["openai", "anthropic"]@dataclass(frozen=True)
class ChatMessage:
role: Role
content: str@dataclass(frozen=True)
class CompletionResult:
provider: ProviderName
model: str
text: str
input_tokens: int | None
output_tokens: int | Noneclass ChatProvider(Protocol):
name: ProviderNamedef complete(
self,
system_prompt: str,
messages: Sequence[ChatMessage],
max_output_tokens: int,
) -> CompletionResult:
...class OpenAIChatProvider:
name: ProviderName = "openai"def __init__(self, settings: Settings) -> None:
api_key, model = settings.require_openai()
self._model = model
self._client = OpenAI(
api_key=api_key,
timeout=settings.request_timeout_seconds,
max_retries=0,
)def complete(
self,
system_prompt: str,
messages: Sequence[ChatMessage],
max_output_tokens: int,
) -> CompletionResult:
response = self._client.chat.completions.create(
model=self._model,
messages=[
{"role": "system", "content": system_prompt},
*[{"role": message.role, "content": message.content} for message in messages],
],
max_tokens=max_output_tokens,
)
text = (response.choices[0].message.content or "").strip()
if not text:
raise RuntimeError("OpenAI returned an empty assistant response.")
usage = response.usage
return CompletionResult(
provider=self.name,
model=response.model,
text=text,
input_tokens=usage.prompt_tokens if usage else None,
output_tokens=usage.completion_tokens if usage else None,
)class AnthropicChatProvider:
name: ProviderName = "anthropic"def __init__(self, settings: Settings) -> None:
api_key, model = settings.require_anthropic()
...تابع القراءة
سجل دخولك مجاناً لقراءة المقال كاملاً والوصول إلى أدوات الذكاء الاصطناعي.
تسجيل الدخول / إنشاء حساب