共用方式為


將記憶體新增至代理程式

本教學課程示範如何透過實作AIContextProvider並將其附加至代理程式,以新增記憶體至代理程式。

這很重要

並非所有代理程式類型都支援 AIContextProvider。 此步驟使用 ChatClientAgent,它確實支援 AIContextProvider

先決條件

如需必要條件和安裝 NuGet 套件,請參閱本教學課程中的 建立並執行簡單代理程式 步驟。

建立 AIContextProvider

AIContextProvider 是一個可供繼承的抽象類別,可以將其與 AgentThread 相關聯,以用於 ChatClientAgent。 它允許您:

  1. 在代理程式叫用基礎推論服務之前和之後執行自訂邏輯。
  2. 在呼叫基礎推論服務之前,向代理程式提供其他內容。
  3. 檢查提供給代理程式和由代理程式產生的所有訊息。

呼叫前和呼叫後事件

類別有 AIContextProvider 兩個方法,您可以覆寫這些方法,以在代理程式叫用基礎推論服務之前和之後執行自訂邏輯:

  • InvokingAsync - 在代理程式叫用基礎推論服務之前呼叫。 您可以傳回 AIContext 物件,為代理程式提供其他內容。 在叫用基礎服務之前,此內容將與代理程式的現有內容合併。 可以提供指示、工具和訊息以新增至請求。
  • InvokedAsync - 在代理程式收到基礎推論服務的回應之後呼叫。 您可以檢查要求和回應訊息,並更新環境定義提供者的狀態。

Serialization

AIContextProvider 實例會在建立執行緒時以及執行緒從序列化狀態繼續時建立,並附加至 AgentThread

AIContextProvider實例可能有自己的狀態,需要在呼叫代理程式之間保存。 例如,記住使用者相關資訊的記憶體元件可能會將記憶體作為其狀態的一部分。

若要允許持續執行緒,您需要實作 SerializeAsync 類別的 AIContextProvider 方法。 您還需要提供一個接受 JsonElement 參數的建構函式,用來在恢復執行緒時反序列化狀態。

範例 AIContextProvider 實作

下列自訂記憶體元件範例會記住使用者的名稱和年齡,並在每次呼叫之前將其提供給代理程式。

首先,建立一個模型類別來保存記憶體。

internal sealed class UserInfo
{
    public string? UserName { get; set; }
    public int? UserAge { get; set; }
}

然後您可以實現 AIContextProvider 來管理記憶體。 UserInfoMemory下列類別包含下列行為:

  1. 當每次執行結束時將新訊息新增至執行緒時,它會使用 在 IChatClient 使用者訊息中尋找使用者的名稱和年齡。
  2. 它會在每次呼叫之前向代理程式提供任何現行記憶體。
  3. 如果沒有可用的記憶體,它會指示代理程式向使用者詢問遺失的資訊,並且在提供資訊之前不要回答任何問題。
  4. 它也會實作序列化,以允許將記憶體保存為執行緒狀態的一部分。
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

internal sealed class UserInfoMemory : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = userInfo ?? new UserInfo();
    }

    public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
            new UserInfo();
    }

    public UserInfo UserInfo { get; set; }

    public override async ValueTask InvokedAsync(
        InvokedContext context,
        CancellationToken cancellationToken = default)
    {
        if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
        {
            var result = await this._chatClient.GetResponseAsync<UserInfo>(
                context.RequestMessages,
                new ChatOptions()
                {
                    Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
                },
                cancellationToken: cancellationToken);
            this.UserInfo.UserName ??= result.Result.UserName;
            this.UserInfo.UserAge ??= result.Result.UserAge;
        }
    }

    public override ValueTask<AIContext> InvokingAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        StringBuilder instructions = new();
        instructions
            .AppendLine(
                this.UserInfo.UserName is null ?
                    "Ask the user for their name and politely decline to answer any questions until they provide it." :
                    $"The user's name is {this.UserInfo.UserName}.")
            .AppendLine(
                this.UserInfo.UserAge is null ?
                    "Ask the user for their age and politely decline to answer any questions until they provide it." :
                    $"The user's age is {this.UserInfo.UserAge}.");
        return new ValueTask<AIContext>(new AIContext
        {
            Instructions = instructions.ToString()
        });
    }

    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions);
    }
}

使用 AIContextProvider 搭配代理人

若要使用自訂 AIContextProvider,您需要在建立代理程式時提供 AIContextProviderFactory。 此工廠允許代理程式為每個執行緒建立所需 AIContextProvider 的新實例。

建立ChatClientAgent時,可以提供一個ChatClientAgentOptions物件,這個物件除了可提供所有其他代理程式選項之外,還能提供AIContextProviderFactory

using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
using OpenAI;

ChatClient chatClient = new AzureOpenAIClient(
    new Uri("https://<myresource>.openai.azure.com"),
    new AzureCliCredential())
    .GetChatClient("gpt-4o-mini");

AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
    Instructions = "You are a friendly assistant. Always address the user by their name.",
    AIContextProviderFactory = ctx => new UserInfoMemory(
        chatClient.AsIChatClient(),
        ctx.SerializedState,
        ctx.JsonSerializerOptions)
});

建立新執行緒時,AIContextProvider 將建立 GetNewThread,並將其附加到該執行緒。 因此,一旦提取了記憶體,就可以透過執行緒的方法 GetService 存取記憶體元件並檢查記憶體。

// Create a new thread for the conversation.
AgentThread thread = agent.GetNewThread();

Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));

// Access the memory component via the thread's GetService method.
var userInfo = thread.GetService<UserInfoMemory>()?.UserInfo;
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");

本教學課程說明如何實作 a ContextProvider 並將其附加至代理程式,以將記憶體新增至代理程式。

這很重要

並非所有代理程式類型都支援 ContextProvider。 此步驟使用 ChatAgent,它確實支援 ContextProvider

先決條件

如需必要條件和安裝套件,請參閱本教學課程中的 建立並執行簡式代理程式 步驟。

建立 ContextProvider

ContextProvider 是可以繼承的抽象類,並且可以與 AgentThread for a ChatAgent相關聯。 它允許您:

  1. 在代理程式叫用基礎推論服務之前和之後執行自訂邏輯。
  2. 在呼叫基礎推論服務之前,向代理程式提供其他內容。
  3. 檢查提供給代理程式和由代理程式產生的所有訊息。

呼叫前和呼叫後事件

類別有 ContextProvider 兩個方法,您可以覆寫這些方法,以在代理程式叫用基礎推論服務之前和之後執行自訂邏輯:

  • invoking - 在代理程式叫用基礎推論服務之前呼叫。 您可以傳回 Context 物件,為代理程式提供其他內容。 在叫用基礎服務之前,此內容將與代理程式的現有內容合併。 可以提供指示、工具和訊息以新增至請求。
  • invoked - 在代理程式收到基礎推論服務的回應之後呼叫。 您可以檢查要求和回應訊息,並更新環境定義提供者的狀態。

Serialization

ContextProvider 實例會在建立執行緒時以及執行緒從序列化狀態繼續時建立,並附加至 AgentThread

ContextProvider實例可能有自己的狀態,需要在呼叫代理程式之間保存。 例如,記住使用者相關資訊的記憶體元件可能會將記憶體作為其狀態的一部分。

若要允許持續執行緒,您必須實作類別的 ContextProvider 序列化。 您也必須提供建構函式,以在繼續執行緒時,從序列化資料還原狀態。

範例 ContextProvider 實作

下列自訂記憶體元件範例會記住使用者的名稱和年齡,並在每次呼叫之前將其提供給代理程式。

首先,建立一個模型類別來保存記憶體。

from pydantic import BaseModel

class UserInfo(BaseModel):
    name: str | None = None
    age: int | None = None

然後您可以實現 ContextProvider 來管理記憶體。 UserInfoMemory下列類別包含下列行為:

  1. 它會使用聊天用戶端,在每次執行結束時將新訊息新增至執行緒時,在使用者訊息中尋找使用者的名稱和年齡。
  2. 它會在每次呼叫之前向代理程式提供任何現行記憶體。
  3. 如果沒有可用的記憶體,它會指示代理程式向使用者詢問遺失的資訊,並且在提供資訊之前不要回答任何問題。
  4. 它也會實作序列化,以允許將記憶體保存為執行緒狀態的一部分。

from agent_framework import ContextProvider, Context, InvokedContext, InvokingContext, ChatAgent, ChatClientProtocol


class UserInfoMemory(ContextProvider):
    def __init__(self, chat_client: ChatClientProtocol, user_info: UserInfo | None = None, **kwargs: Any):
        """Create the memory.

        If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
        """

        self._chat_client = chat_client
        if user_info:
            self.user_info = user_info
        elif kwargs:
            self.user_info = UserInfo.model_validate(kwargs)
        else:
            self.user_info = UserInfo()

    async def invoked(
        self,
        request_messages: ChatMessage | Sequence[ChatMessage],
        response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
        invoke_exception: Exception | None = None,
        **kwargs: Any,
    ) -> None:
        """Extract user information from messages after each agent call."""
        # Check if we need to extract user info from user messages
        user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role.value == "user"]

        if (self.user_info.name is None or self.user_info.age is None) and user_messages:
            try:
                # Use the chat client to extract structured information
                result = await self._chat_client.get_response(
                    messages=request_messages,
                    chat_options=ChatOptions(
                        instructions="Extract the user's name and age from the message if present. If not present return nulls.",
                        response_format=UserInfo,
                    ),
                )

                # Update user info with extracted data
                if result.value:
                    if self.user_info.name is None and result.value.name:
                        self.user_info.name = result.value.name
                    if self.user_info.age is None and result.value.age:
                        self.user_info.age = result.value.age

            except Exception:
                pass  # Failed to extract, continue without updating

    async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
        """Provide user information context before each agent call."""
        instructions: list[str] = []

        if self.user_info.name is None:
            instructions.append(
                "Ask the user for their name and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's name is {self.user_info.name}.")

        if self.user_info.age is None:
            instructions.append(
                "Ask the user for their age and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's age is {self.user_info.age}.")

        # Return context with additional instructions
        return Context(instructions=" ".join(instructions))

    def serialize(self) -> str:
        """Serialize the user info for thread persistence."""
        return self.user_info.model_dump_json()

將 ContextProvider 與代理程式搭配使用

若要使用自訂 ContextProvider,您必須在建立代理程式時提供具現化的 ContextProvider

在建立ChatAgent時,您可以提供context_providers參數來將記憶體元件連接至代理程式。

import asyncio
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential

async def main():
    async with AzureCliCredential() as credential:
        chat_client = AzureAIAgentClient(async_credential=credential)

        # Create the memory provider
        memory_provider = UserInfoMemory(chat_client)

        # Create the agent with memory
        async with ChatAgent(
            chat_client=chat_client,
            instructions="You are a friendly assistant. Always address the user by their name.",
            context_providers=memory_provider,
        ) as agent:
            # Create a new thread for the conversation
            thread = agent.get_new_thread()

            print(await agent.run("Hello, what is the square root of 9?", thread=thread))
            print(await agent.run("My name is Ruaidhrí", thread=thread))
            print(await agent.run("I am 20 years old", thread=thread))

            # Access the memory component via the thread's context_providers attribute and inspect the memories
            user_info_memory = thread.context_provider.providers[0]
            if user_info_memory:
                print()
                print(f"MEMORY - User Name: {user_info_memory.user_info.name}")
                print(f"MEMORY - User Age: {user_info_memory.user_info.age}")


if __name__ == "__main__":
    asyncio.run(main())

後續步驟