🧠 استخراج هوشمند محتوا از فایل با مدل Qwen3.5 GGUF

راهنمای گام به گام یکپارچه‌سازی مدل‌های محلی GGUF در معماری xAiApi
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
🏢 شرکت: فن آوران ساحر علم
📅 تاریخ تهیه مستند: چهارشنبه ۸ مهر ۱۴۰۵
📦 پروژه: xAiApi

🎯 گام ۱: تحلیل رویکرد و استراتژی

مدل Qwen3.5-9B-The-Defiant-Fable-Uncensored-Heretic-NEO-IMATRIX-MAX-MTP-GGUF یک مدل زبانی بزرگ (LLM) در فرمت GGUF است. برای اجرای این مدل و استفاده از آن جهت استخراج محتوا، بهترین و سازگارترین راه با معماری فعلی پروژه شما، استفاده از Ollama به عنوان موتور اجرای محلی (Local Inference Engine) است.

💡 استراتژی دو مرحله‌ای استخراج محتوا:
۱. استخراج خام (Raw Extraction): استفاده از IFileContentExtractor (که قبلاً طراحی کردیم) برای خواندن متن خام از PDF/DOCX/TXT.
۲. پردازش هوشمند (LLM Processing): ارسال متن خام به مدل Qwen3.5 با یک System Prompt دقیق برای خلاصه‌سازی، استخراج موجودیت‌ها (NER)، یا تبدیل به JSON ساختاریافته.

⚙️ گام ۲: آماده‌سازی مدل GGUF در Ollama

از آنجا که Ollama به صورت بومی از فرمت GGUF پشتیبانی می‌کند، باید مدل را دانلود و در Ollama ایمپورت کنیم.

۱ دانلود فایل GGUF:

فایل .gguf را از لینک Hugging Face ارائه شده دانلود کرده و در مسیری مانند C:\Models\qwen3.5-defiant.gguf ذخیره کنید.

۲ ایجاد فایل Modelfile:

یک فایل متنی بدون پسوند به نام Modelfile در کنار فایل GGUF ایجاد کنید و محتوای زیر را در آن قرار دهید:

FROM C:/Models/qwen3.5-defiant.gguf

# تنظیم پارامترهای بهینه برای استخراج محتوا
PARAMETER temperature 0.2
PARAMETER top_p 0.9
PARAMETER num_ctx 8192

# تنظیم پرامپت سیستمی پیش‌فرض برای استخراج ساختاریافته
SYSTEM """
تو یک دستیار هوشمند و دقیق برای استخراج و تحلیل محتوای اسناد هستی. 
وظیفه تو خواندن متن ورودی، درک عمیق آن، و استخراج اطلاعات کلیدی به صورت ساختاریافته و دقیق است.
همیشه به زبان فارسی روان و حرفه‌ای پاسخ بده، مگر اینکه خلاف آن درخواست شود.
"""
۳ ایجاد مدل در Ollama:

ترمینال یا CMD را باز کرده و دستور زیر را اجرا کنید تا مدل با یک نام مستعار (Alias) کوتاه ثبت شود:

ollama create qwen3.5-defiant:9b -f Modelfile

سپس برای اطمینان از صحت نصب، دستور ollama list را اجرا کنید.

📝 گام ۳: پیکربندی مدل در appsettings.json

اکنون باید این مدل جدید را به لیست مدل‌های مجاز در پیکربندی پروژه اضافه کنیم تا سرویس‌ها بتوانند از آن استفاده کنند.

MODIFY xAiApi/appsettings.json
"AiApiConfiguration": {
  "Models": [
    {
      "Name": "Gemma",
      "Url": "http://localhost:11434",
      "LLM": "gemma:2b",
      "Provider": "Ollama"
    },
    {
      "Name": "QwenDefiant", 
      "Url": "http://localhost:11434",
      "LLM": "qwen3.5-defiant:9b", 
      "Provider": "Ollama"
    }
  ],
  "Prompts": [
    {
      "Name": "Introduction",
      "Template": "تو یک دستیار هوشمند مفید هستی."
    },
    {
      "Name": "DocumentExtraction",
      "Template": "متن زیر از یک فایل استخراج شده است. لطفاً آن را تحلیل کن و خروجی را دقیقاً در قالب JSON با کلیدهای زیر برگردان: {\"summary\": \"خلاصه ۳ خطی\", \"key_points\": [\"نکته ۱\", \"نکته ۲\"], \"entities\": {\"نام_اشخاص\": [], \"تاریخ_ها\": []}}. متن: {0}"
    }
  ]
}
✅ نکته: استفاده از نام مستعار qwen3.5-defiant:9b در فیلد LLM باعث می‌شود کد شما تمیز و خوانا بماند.

🔧 گام ۴: ایجاد سرویس استخراج محتوا (Extraction Service)

ما یک سرویس اختصاصی ایجاد می‌کنیم که ترکیبی از IFileContentExtractor (برای خواندن فایل) و IChatClient (برای پردازش با Qwen) باشد.

NEW xAiApi/Interfaces/IDocumentExtractionService.cs
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace xAiApi.Interfaces
{
    public interface IDocumentExtractionService
    {
        /// <summary>
        /// استخراج و تحلیل هوشمند محتوای یک فایل
        /// </summary>
        Task<string> ExtractAndAnalyzeAsync(
            IFormFile file,
            string extractionPromptTemplate,
            CancellationToken cancellationToken = default
        );
    }
}
NEW xAiApi/Providers/XDocumentExtractionService.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using xAiApi.Configurations;
using xAiApi.Constants;
using xAiApi.Extensions;
using xAiApi.Interfaces;
using xAiModels.Constants;
using xAiModels.Extensions;
using xAiService.Interfaces;
using xExceptions.Constants;

namespace xAiApi.Providers
{
    public class XDocumentExtractionService : IDocumentExtractionService
    {
        private readonly IFileContentExtractor _fileExtractor;
        private readonly XAiApiConfiguration _configuration;
        private readonly ILogger<XDocumentExtractionService> _logger;

        public XDocumentExtractionService(
            IFileContentExtractor fileExtractor,
            XAiApiConfiguration configuration,
            ILogger<XDocumentExtractionService> logger)
        {
            _fileExtractor = fileExtractor;
            _configuration = configuration;
            _logger = logger;
        }

        public async Task<string> ExtractAndAnalyzeAsync(
            IFormFile file,
            string extractionPromptTemplate,
            CancellationToken cancellationToken = default)
        {
            // ۱. اعتبارسنجی فایل
            if (file == null || file.Length == 0)
            {
                XException.InvalidArgs.Throw("فایل نامعتبر است.");
            }

            // ۲. استخراج متن خام از فایل
            string rawText;
            using (var stream = file.OpenReadStream())
            {
                if (_fileExtractor.CanExtract(file.ContentType))
                {
                    rawText = await _fileExtractor.ExtractAsync(stream, file.ContentType, cancellationToken);
                }
                else
                {
                    XException.InvalidData.Throw($"فرمت فایل {file.ContentType} پشتیبانی نمی‌شود.");
                }
            }

            if (string.IsNullOrWhiteSpace(rawText))
            {
                XException.InvalidData.Throw("محتوای استخراج شده از فایل خالی است.");
            }

            // ۳. آماده‌سازی پرامپت نهایی
            var finalPrompt = string.Format(extractionPromptTemplate, rawText);

            // ۴. دریافت کلاینت مدل QwenDefiant از پیکربندی
            var modelDescriptor = _configuration.GetModel("QwenDefiant");
            if (!modelDescriptor.IsValid())
            {
                XException.InvalidConfiguration.Throw("مدل QwenDefiant در پیکربندی یافت نشد.");
            }

            // ۵. ساخت ChatClient (با استفاده از منطق موجود در XAIServiceBase یا مستقیم)
            using var client = CreateChatClient(modelDescriptor);

            // ۶. ارسال درخواست به مدل
            var history = new[] 
            { 
                new ChatMessage(ChatRole.System, "تو یک متخصص استخراج داده از اسناد هستی. فقط خروجی درخواست شده را تولید کن."),
                new ChatMessage(ChatRole.User, finalPrompt)
            };

            var response = await client.GetResponseAsync(history, cancellationToken: cancellationToken);

            if (!response.IsValid())
            {
                XException.ActionFailed.Throw("مدل هوش مصنوعی پاسخی معتبر تولید نکرد.");
            }

            return response.Text;
        }

        private IChatClient CreateChatClient(XAiModels.Models.XAiModelDescriptor descriptor)
        {
            // بازنویسی منطق ساخت کلاینت Ollama بر اساس معماری پروژه
            var httpClient = new HttpClient { BaseAddress = new Uri(descriptor.Url), Timeout = Timeout.InfiniteTimeSpan };
            var ollamaClient = new OllamaSharp.OllamaApiClient(httpClient, descriptor.LLM);
            
            return new ChatClientBuilder(ollamaClient)
                .UseFunctionInvocation()
                .Build();
        }
    }
}

🎮 گام ۵: ایجاد Controller اختصاصی

یک endpoint جدید برای دریافت فایل و بازگرداندن محتوای تحلیل‌شده ایجاد می‌کنیم.

NEW xAiApi/Controllers/DocumentExtractionController.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using xCommons.Configurations;
using xCommons.Providers;
using xIdentityService.Interfaces;
using xAiApi.Interfaces;
using xAiApi.Extensions;

namespace xAiApi.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class DocumentExtractionController : XBaseIdentityApiV1Controller
    {
        private readonly IDocumentExtractionService _extractionService;
        private readonly XAiApiConfiguration _configuration;

        public DocumentExtractionController(
            ILogger<DocumentExtractionController> logger,
            XAppConfiguration appConfiguration,
            XValidationProvider validationProvider,
            IXIdentityProvider identityProvider,
            IDocumentExtractionService extractionService,
            XAiApiConfiguration configuration) 
            : base(logger, appConfiguration, validationProvider, identityProvider)
        {
            _extractionService = extractionService;
            _configuration = configuration;
        }

        /// <summary>
        /// آپلود فایل و استخراج هوشمند محتوا با مدل Qwen3.5
        /// </summary>
        [HttpPost("Extract")]
        [Consumes("multipart/form-data")]
        public async Task<ActionResult<object>> Extract(
            [FromForm] IFormFile file,
            [FromForm] string promptName = "DocumentExtraction",
            CancellationToken cancellationToken = default)
        {
            try
            {
                ValidationProvider.NotNull(file, nameof(file));

                // دریافت الگوی پرامپت از پیکربندی
                var promptTemplate = _configuration.GetPrompt(promptName);
                if (string.IsNullOrWhiteSpace(promptTemplate))
                {
                    promptTemplate = "متن زیر را تحلیل و خلاصه کن: {0}";
                }

                // فراخوانی سرویس استخراج
                var result = await _extractionService.ExtractAndAnalyzeAsync(
                    file: file,
                    extractionPromptTemplate: promptTemplate,
                    cancellationToken: cancellationToken
                );

                return Ok(new { success = true, data = result });
            }
            catch (Exception ex)
            {
                return GetExceptionActionResult(ex);
            }
        }
    }
}

🔗 گام ۶: ثبت وابستگی‌ها (Dependency Injection)

سرویس جدید را در متد ConfigureServices فایل Startup.cs ثبت کنید.

MODIFY xAiApi/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... (ثبت‌های قبلی)

    // ثبت Extractor های فایل (اگر قبلاً ثبت نشده‌اند)
    services.AddSingleton<IFileContentExtractor, PlainTextContentExtractor>();
    services.AddSingleton<IFileContentExtractor, PdfContentExtractor>();
    services.AddSingleton<IFileContentExtractor, CompositeFileContentExtractor>();

    // ✅ ثبت سرویس جدید استخراج محتوا
    services.AddScoped<IDocumentExtractionService, XDocumentExtractionService>();

    // ... (بقیه کدها)
}

🔄 گام ۷: جریان کامل پردازش

📤 کلاینت: آپلود فایل → 🎮 DocumentExtractionController → 📄 IFileContentExtractor (استخراج متن خام) → 🧠 Qwen3.5 GGUF (تحلیل و ساختارسازی) → ✅ بازگرداندن JSON/متن تحلیل‌شده

نمونه درخواست (cURL):

curl -X POST "http://localhost:5000/api/DocumentExtraction/Extract" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@/path/to/document.pdf" \
  -F "promptName=DocumentExtraction"

💡 گام ۸: نکات کلیدی و مهندسی پرامپت برای مدل‌های GGUF

🎯 پرامپت‌نویسی دقیق

مدل‌های GGUF محلی به دستورالعمل‌های شفاف بسیار خوب پاسخ می‌دهند. در appsettings.json حتماً قالب خروجی (مثلاً JSON) را به صراحت مشخص کنید.

⚡ مدیریت Context Window

در Modelfile مقدار num_ctx را بر اساس حجم فایل‌های شما تنظیم کنید (مثلاً 8192 یا 16384). اگر فایل بزرگ است، آن را به قطعات (Chunks) تقسیم کنید.

🛡️ مدیریت خطا

همیشه احتمال خطای OOM (کمبود حافظه RAM/VRAM) در مدل‌های 9B را در نظر بگیرید. لاگ‌های Ollama را برای پایش مصرف حافظه بررسی کنید.

🖼️ پشتیبانی از تصویر (Vision)

اگر این نسخه خاص از Qwen از ورودی تصویر پشتیبانی کند، می‌توانید در XDocumentExtractionService به جای متن خام، فایل تصویر را به DataContent تبدیل و ارسال کنید.

✅ جمع‌بندی: با این طراحی، شما بدون تغییر در هسته اصلی XAIServiceBase، یک ماژول کاملاً ایزوله و قدرتمند برای استخراج محتوا با مدل‌های محلی GGUF ایجاد کرده‌اید که کاملاً با معماری ماژولار شرکت فن آوران ساحر علم همخوانی دارد.