# دليل Spring AI Advisors: شرح المستشارين المدمجين والمخصصين

> تعرّف على كيفية استخدام Spring AI Advisors لإدارة طلبات ChatClient. يغطي هذا الدليل ترتيب التنفيذ، والخيارات المدمجة، وإنشاء مستشارين مخصصين.

- Canonical URL: https://coreiten.com/article/دليل-spring-ai-advisors-إعداد-السلاسل-المدمجة-والمخصصة
- Language: ar
- Section: معلومات تقنية
- Author: Sami
- Published: 2026-10-21T16:04:16+03:00
- Modified: 2026-10-21T16:04:16+03:00
- Publisher: CoreITen (https://coreiten.com)
- Keywords: Spring AI Advisors, ChatClient, SafeGuardAdvisor, MessageChatMemoryAdvisor, QuestionAnswerAdvisor, TokenUsageAdvisor, ذكاء اصطناعي

## ملخص

يشرح دليل Spring AI Advisors كيفية إضافة سلوكيات مخصصة لطلبات ChatClient، وإدارة سلسلة المستشارين وترتيب تنفيذها، واستخدام المكونات والمستشارين المدمجين عبر تطبيق OrderDesk التجريبي.

- يمر طلب ChatClient عبر المستشارين المسجلين بترتيب تصاعدي لقيمة getOrder()، بينما تعود الاستجابة بترتيب عكسي لتنفيذ المعالجة اللازمة.
- يشارك CallAdvisor في الاستدعاءات المتزامنة بينما يشارك StreamAdvisor في استدعاءات البث المباشر، ويمكن لتطبيق واحد دعم النوعين.
- تشمل قائمة المستشارين المدمجين SafeGuardAdvisor لحظر الكلمات، وMessageChatMemoryAdvisor للذاكرة، وQuestionAnswerAdvisor لقاعدة البيانات المتجهية.
- تعتمد تبعيات تطبيق OrderDesk التجريبي على spring-ai-starter-model-chat-memory-repository-jdbc وقاعدة hsqldb لحفظ رسائل الدردشة في جدول قاعدة بيانات.
- يقوم MessageChatMemoryAdvisor بالاحتفاظ بآخر 10 رسائل من كل محادثة باستخدام مكون ChatMemory وتطبيق OrderDesk التجريبي.

**لماذا يهم:** يساعد هذا الدليل المطورين على فهم آلية عمل وتكوين المستشارين في إطار عمل Spring AI لبناء مساعدين أذكياء ومخصصين للمتاجر الإلكترونية.

---

تضيف أدوات المستشارين سلوكيات مخصصة حول طلبات `ChatClient`، مثل استرداد المستندات، أو إضافة سياق المحادثة، أو تسجيل الاستخدام، أو إرجاع استجابة دون استدعاء النموذج. يستعرض دليل Spring AI Advisors هذا كيفية عمل سلسلة المستشارين، وكيفية إعداد الخيارات المدمجة، وطريقة كتابة مستشار استدعاء مخصص باستخدام تطبيق OrderDesk التجريبي.

### فهم سلسلة المستشارين وترتيب التنفيذ

يمر الطلب عبر المستشارين المسجلين قبل الوصول إلى تنفيذ النموذج. يمكن للمستشار فحص الطلب أو تغييره، ومتابعة السلسلة، ومعالجة الاستجابة المرتجعة. كما يمكنه إيقاف السلسلة عبر تقديم استجابته الخاصة:

```java
ChatClientResponse response = chain.nextCall(request);   // the rest of the chain
```

في الاستدعاءات المتزامنة، يعمل المنطق البرمجي الموجود قبل `nextCall()` قبل المعالجة اللاحقة، بينما يعمل المنطق الموجود بعده عند عودة الاستجابة. يرى المستشار الأول الطلب أولاً والاستجابة النهائية أخيراً، ما لم توقف خطوة سابقة التنفيذ. يمر استدعاء ChatClient عبر المستشارين من قيمة الترتيب الأقل إلى الأعلى، وتعود الاستجابة عبرهم بترتيب عكسي.

يحدد إطار العمل الترتيب باستخدام طريقة `getOrder()`. ينفذ المستشارون واجهة `Ordered` الخاصة بإطار العمل، ويعمل المستشار ذو القيمة الأقل أولاً. يمثل `Ordered.HIGHEST_PRECEDENCE` أصغر عدد صحيح، لذا يعمل المستشار الذي يحمله أولاً. يشارك CallAdvisor في الاستدعاءات المتزامنة، بينما يشارك StreamAdvisor في استدعاءات البث المباشر، ويمكن لتطبيق واحد دعم كليهما. تشمل قائمة المستشارين المدمجين ما يلي:

| المستشار | الوحدة | الوظيفة | الترتيب الافتراضي |
| --- | --- | --- | --- |
| SafeGuardAdvisor | spring-ai-client-chat | يحظر الطلب الذي يحتوي على كلمة مدرجة | 0 |
| MessageChatMemoryAdvisor | spring-ai-client-chat | يضيف الرسائل السابقة للمحادثة، ويحفظ الرسائل الجديدة | HIGHEST_PRECEDENCE + 200 |
| ToolCallingAdvisor | spring-ai-client-chat | يشغّل حلقة استدعاء الأدوات؛ يُضاف تلقائياً | HIGHEST_PRECEDENCE + 300 |
| QuestionAnswerAdvisor | spring-ai-vector-store-advisor | يضيف المستندات المطابقة من قاعدة البيانات المتجهية (RAG) | 0 |
| RetrievalAugmentationAdvisor | spring-ai-rag | نظام RAG تركيبي: إعادة كتابة الاستعلام، الاسترداد، التعزيز | 0 |
| SimpleLoggerAdvisor | spring-ai-client-chat | يسجل الطلب والاستجابة بمستوى DEBUG | 0 |

### إعداد التطبيق التجريبي

يتطلب تطبيق OrderDesk التجريبي تبعيات وخصائص محددة. **أضف** هذه التبعيات إلى ملف `pom.xml` لتفعيل ذاكرة الدردشة وقاعدة البيانات المتجهية:

```xml
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>org.hsqldb</groupId>
  <artifactId>hsqldb</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
```

تحفظ تبعية `spring-ai-starter-model-chat-memory-repository-jdbc` رسائل الدردشة في جدول قاعدة بيانات، بينما توفر `hsqldb` قاعدة البيانات المحفوظة في ملف. تتضمن `spring-ai-vector-store-advisor` مستشار `QuestionAnswerAdvisor` وتجلب `SimpleVectorStore`، وهي قاعدة بيانات متجهية تحتفظ بالمتجهات في الذاكرة. وأخيراً، تقرأ `spring-ai-markdown-document-reader` ملفات السياسات وتدرجها في قاعدة البيانات المتجهية.

**أضف** هذه الأسطر إلى ملف `application.properties`:

```properties
# Chat memory is saved in a file database, so it survives a restart
spring.datasource.url=jdbc:hsqldb:file:./data/orderdesk
spring.ai.chat.memory.repository.jdbc.initialize-schema=always

# Embedding model for RAG (ollama pull nomic-embed-text)
spring.ai.ollama.embedding.options.model=nomic-embed-text

# Which vector store is active: simple (default, in memory), chroma, pgvector, redis or qdrant.
# The profiles application-chroma.properties and so on switch it.
spring.ai.vectorstore.type=simple
```

يشير `spring.datasource.url` إلى ملف HSQLDB، وينشئ `initialize-schema=always` جدول `SPRING_AI_CHAT_MEMORY` عند بدء التشغيل. يحدد `spring.ai.ollama.embedding.options.model` النموذج الذي يحول النص إلى متجهات، ويختار `spring.ai.vectorstore.type=simple` قاعدة `SimpleVectorStore`. لرؤية مخرجات `SimpleLoggerAdvisor`، أضف `logging.level.org.springframework.ai.chat.client.advisor=DEBUG`.

#### المكونات التي يستخدمها المستشارون

يغلف المستشارون مكون `supportClient`، وهو مكون ChatClient. يحتوي الأمر النصي للنظام الخاص به على قواعد المتجر:

```java
package com.howtodoinjava.orderdesk.chat;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

    static final String SUPPORT_RULES = """
        You are OrderDesk, the support assistant of an online store.
        Answer in at most three sentences.
        Delivery: Pune, Mumbai and Delhi in 2 days; every other city in India in 5 days.
        Delivery is free above INR 499; below that it costs INR 49.
        We do not deliver outside India.
        """;

    @Bean
    ChatClient supportClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem(SUPPORT_RULES)
            .build();
    }

    @Bean
    ChatClient summaryClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("Summarize the customer's message in one line of at most 12 words.")
            .build();
    }
}
```

يحتاج `MessageChatMemoryAdvisor` إلى مكون ChatMemory. يحتفظ تطبيق OrderDesk بآخر 10 رسائل من كل محادثة في جدول JDBC:

```java
package com.howtodoinjava.orderdesk.memory;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatMemoryConfig {

    // Keep the last 10 messages of each conversation (the default is 20)
    @Bean
    ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
            .chatMemoryRepository(repository)
            .maxMessages(10)
            .build();
    }
}
```

يحتاج `QuestionAnswerAdvisor` إلى مكون VectorStore يحتوي على سياسات المتجر. بعد إضافة الشيفرة البرمجية، حمّل السياسات مرة واحدة باستخدام الأمر `curl -X POST localhost:8080/ingest`.

### بناء وفحص سلسلة المستشارين

#### الخطوة 1: كتابة مستشار مخصص

يسجل `TokenUsageAdvisor` المستشارين في السلسلة، وعدد الرموز، ووقت كل استدعاء:

```java
package com.howtodoinjava.orderdesk.advisors;

import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;

import reactor.core.publisher.Flux;

// Logs the time and the tokens of every ChatClient call that it advises
public class TokenUsageAdvisor implements CallAdvisor, StreamAdvisor {

    private static final Logger log = LoggerFactory.getLogger(TokenUsageAdvisor.class);

    private final int order;

    public TokenUsageAdvisor(int order) {
        this.order = order;
    }

    @Override
    public String getName() {
        return "TokenUsageAdvisor";
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        log.info("Chain: {}", chain.getCallAdvisors().stream()
            .map(a -> a.getName() + "(" + a.getOrder() + ")")
            .collect(Collectors.joining(" -> ")));
        long start = System.currentTimeMillis();

        ChatClientResponse response = chain.nextCall(request);   // the rest of the chain

        logUsage(response.chatResponse(), System.currentTimeMillis() - start);
        return response;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request,
                                                 StreamAdvisorChain chain) {
        long start = System.currentTimeMillis();
        return chain.nextStream(request)
            .doOnNext(response -> {
                ChatResponse chatResponse = response.chatResponse();
                // only the last piece of a stream carries the token counts
                if (chatResponse != null && chatResponse.getMetadata().getUsage() != null
                        && chatResponse.getMetadata().getUsage().getTotalTokens() != null
                        && chatResponse.getMetadata().getUsage().getTotalTokens() > 0) {
                    logUsage(chatResponse, System.currentTimeMillis() - start);
                }
            });
    }

    private void logUsage(ChatResponse chatResponse, long millis) {
        Usage usage = chatResponse.getMetadata().getUsage();
        log.info("Tokens: prompt={}, completion={}, total={}, time={} ms",
            usage.getPromptTokens(), usage.getCompletionTokens(), usage.getTotalTokens(), millis);
    }
}
```

ينفذ المستشار واجهة `CallAdvisor` للطلبات المتزامنة أو `StreamAdvisor` لطلبات `stream()`. تتطلب كلتا الواجهتين `getName()` و`getOrder()`. تُرجع طريقة `chain.getCallAdvisors()` السلسلة بأكملها مرتبة. لتغيير الطلب، أنشئ طلباً جديداً باستخدام `request.mutate()` ومرره إلى `chain.nextCall()`.

#### الخطوة 2: ربط المستشارين المدمجين

تُضاف أربعة مستشارين مدمجين بجوار المستشار المخصص في `AdvisorChatController`. تركت النسخة الأولية كل مستشار مدمج بترتيبه الافتراضي:

```java
supportClient.prompt()
    .user(question)
    .advisors(safeGuardAdvisor, tokenUsageAdvisor, memoryAdvisor,
        questionAnswerAdvisor, new SimpleLoggerAdvisor())
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, customerId))
    .call()
    .content();
```

تحدد طريقة `getOrder()` أولوية التنفيذ بدلاً من ترتيب الوسائط المقدمة إلى `advisors()`. لا تؤسس القيم المتساوية عقداً موثوقاً للترتيب.

#### الخطوة 3: اكتشاف المشكلتين

ظهرت مشكلتان مع الإعداد الافتراضي. أولاً، عندما شارك عميل رقم بطاقته، حظر `SafeGuardAdvisor` الطلب، لكن رقم البطاقة كان قد حُفظ بالفعل لأن مستشار الذاكرة عمل قبله. ثانياً، سألت عميلة سابقة عن اسمها وسياسة الإرجاع، لكن النموذج تجاهل السجل. يوجه القالب الافتراضي لمستشار `QuestionAnswerAdvisor` النموذج لاستخدام السياق المقدم فقط وليس المعرفة السابقة.

#### الخطوة 4: ضبط الترتيب والقالب

يحدد التكوين المُعدّل أولويات صريحة ويغير قالب RAG للسماح بكل من سياق المستند وسجل المحادثة:

```java
package com.howtodoinjava.orderdesk.advisors;

import java.util.List;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.Ordered;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

@RestController
public class AdvisorChatController {

    // The default QuestionAnswerAdvisor template says "not prior knowledge",
    // so the model ignored the chat history; this one allows both
    static final PromptTemplate POLICY_AND_HISTORY = new PromptTemplate("""
        {query}

        Answer from the conversation so far and from the policy text below.
        ---------------------
        {question_answer_context}
        ---------------------
        If neither of them has the answer, say that you don't know.
        """);

    private final ChatClient supportClient;
    private final List<Advisor> advisors;
    private final TokenUsageAdvisor tokenUsageAdvisor =
        new TokenUsageAdvisor(Ordered.HIGHEST_PRECEDENCE + 100);

    AdvisorChatController(@Qualifier("supportClient") ChatClient supportClient,
                          ChatMemory chatMemory, VectorStore vectorStore) {
        this.supportClient = supportClient;
        this.advisors = List.of(
            // 1. block sensitive input before anything else sees or stores it
            new SafeGuardAdvisor(List.of("password", "card number", "CVV"),
                "Please don't share payment details or passwords in chat.",
                Ordered.HIGHEST_PRECEDENCE),
            // 2. measure the whole call, including memory and RAG
            tokenUsageAdvisor,
            // 3. add the conversation history (default order HIGHEST_PRECEDENCE + 200)
            MessageChatMemoryAdvisor.builder(chatMemory).build(),
            // 4. add the policy sections that match the question
            QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().topK(3).similarityThreshold(0.5).build())
                .promptTemplate(POLICY_AND_HISTORY)
                .order(0)
                .build(),
            // 5. log the final prompt, just before the model call
            new SimpleLoggerAdvisor(Ordered.LOWEST_PRECEDENCE - 1));
    }

    @GetMapping("/advisors/ask")
    String ask(@RequestParam String customerId, @RequestParam String question) {
        return supportClient.prompt()
            .user(question)
            .advisors(advisors)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, customerId))
            .call()
            .content();
    }

    @GetMapping("/advisors/stream")
    Flux<String> stream(@RequestParam String question) {
        return supportClient.prompt()
            .user(question)
            .advisors(tokenUsageAdvisor)
            .stream()
            .content();
    }
}
```

يأخذ `SafeGuardAdvisor` الآن الكلمات المحظورة، ورسالة مخصصة، وترتيب `Ordered.HIGHEST_PRECEDENCE`. يحتفظ القالب بعناصر `{query}` و`{question_answer_context}`. يُرجع `SearchRequest` ما يصل إلى 3 أقسام من السياسة بدرجة تشابه لا تقل عن 0.5. يمنع نقل الحماية إلى الموضع الخارجي وصول الطلبات المحظورة إلى الذاكرة.

يؤكد الاختبار مع عميل جديد يُدعى Ravi هذه الإصلاحات:

```bash
curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=Hi, I am Ravi. How many days do I have to return a jacket?"
curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=What is my name, and how long did you say I have?"
curl -G localhost:8080/advisors/ask --data-urlencode "customerId=ravi" --data-urlencode "question=My card number is 4111 1111 1111 1111, can you save it?"
```

تأتي الإجابة الأولى من قسم السياسة الذي أضافه `QuestionAnswerAdvisor`. وتأتي الإجابة الثانية من السجل، حيث لم يسجل أي قسم سياسة درجة 0.5، وسمح القالب الجديد للنموذج باستخدام المحادثة. يعود الطلب الثالث في 13 مللي ثانية لأن `SafeGuardAdvisor` أرجع الاستجابة قبل عمل مستشار الذاكرة، تاركاً رقم البطاقة دون حفظ.

#### الخطوة 5: تقديم المشورة لاستدعاء البث المباشر

تمرر طريقة `stream()` نسخة `tokenUsageAdvisor` إلى استدعاء `stream()`. ينتج البث المباشر أجزاء استجابة متعددة، وعادةً ما يبلغ المزودون عن الاستخدام الإجمالي قرب الانتهاء. يتحقق المستشار المخصص من المجاميع غير الصفرية قبل التسجيل، حيث لا يوفر كل مزود حقول الاستخدام ذاتها.

### أخطاء شائعة

- **الترتيب المتساوي:** لا يعمل المستشارون الذين يحملون الترتيب ذاته بترتيب القائمة. حدد ترتيباً لكل مستشار يهم موقعه.
- **الحماية المتأخرة:** ضع الفحوصات قبل المعالجة التي تحميها. تحقق من عدم التقاط البيانات الحساسة بواسطة التسجيل المبكر.
- **الشيفرات البرمجية المعيقة في البث المباشر:** تجنب العمل المعيق في خيوط حلقة أحداث Reactor. إذا استدعى مستشار بث مباشر مخصص قاعدة بيانات معيقة، فاستخدم مجدولاً مناسباً.
- **المستشارون الافتراضيون يعملون في كل مكان:** تضيف طريقة `defaultAdvisors()` المستشارين إلى كل طلب يُجرى عبر ذلك العميل. أضف المستشارين لكل طلب ما لم تكن كل الطلبات بحاجة إليهم.
- **القوالب المتعارضة:** يضيف كل مستشار يضيف نصاً إلى الأمر النصي تعليمات أيضاً. اقرأ الأمر النصي النهائي في سجل `SimpleLoggerAdvisor` عند دمج المستشارين.

### مفاهيم متقدمة حول المستشارين

بينما يمكن لكل من المستشارين وجوانب Spring AOP تغليف التنفيذ، يعمل مستشارو ChatClient تحديداً مع كائنات الأمر النصي المهيكل، والاستجابة، وسياق المستشار. يمرر المستشارون البيانات لبعضهم البعض عبر سياق المستشار، وهو خريطة (Map) على الطلب والاستجابة. على سبيل المثال، يضع `QuestionAnswerAdvisor` المستندات المعثور عليها تحت `QuestionAnswerAdvisor.RETRIEVED_DOCUMENTS`، والتي يمكن لمستشار لاحق قراءتها من `response.context()`.

يمكن للمستشار تخطي استدعاء النموذج بالكامل. إذا أرجعت `adviseCall()` استجابة دون استدعاء `chain.nextCall()`، فلن يُستدعى النموذج أبداً، وهي الطريقة التي يعمل بها `SafeGuardAdvisor`. لرؤية الأمر النصي النهائي، ضع `SimpleLoggerAdvisor` عند `Ordered.LOWEST_PRECEDENCE - 1` وفعّل تسجيل DEBUG. يُذكر أن الإصدار 1.0 من إطار العمل أعاد تسمية `CallAroundAdvisor` و`StreamAroundAdvisor` إلى `CallAdvisor` و`StreamAdvisor`، و`AdvisedRequest` إلى `ChatClientRequest`.

### إدارة سلسلة المستشارين

تشكل أدوات المستشارين المسجلة سلسلة معالجة مرتبة لأوضاع التنفيذ التي تنفذها. استخدم أولويات صريحة عندما يكون الترتيب مهماً، وتحقق من الطلب النهائي عند دمج الذاكرة، والاسترداد، والأدوات، والحماية، والتسجيل. ونظراً لأن المستشار يمكنه إيقاف السلسلة، يجب ألا تفترض الشيفرات البرمجية اللاحقة أن كل طلب يصل إلى النموذج.

## المصادر

- [howtodoinjava.com](https://howtodoinjava.com/spring-ai/spring-ai-advisors/)

## مواضيع ذات صلة

- [ذكاء اصطناعي](https://coreiten.com/topic/%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A)
