다음을 통해 공유


에이전트에 메모리 추가

이 자습서에서는 AIContextProvider를 구현하여 에이전트에 메모리를 추가하고, 이를 에이전트에 연결하는 방법을 보여줍니다.

중요합니다

모든 에이전트 유형이 AIContextProvider를 지원하는 것은 아닙니다. 이 단계에서는 ChatClientAgent를 사용하며, 이는 AIContextProvider을 지원합니다.

필수 조건

필수 구성 요소 및 NuGet 패키지 설치는 이 자습서의 간단한 에이전트 만들기 및 실행 단계를 참조하세요.

AIContextProvider 만들기

AIContextProvider는 상속할 수 있는 추상 클래스이며, AgentThread와 연결하여 ChatClientAgent에 사용할 수 있습니다. 다음을 수행할 수 있습니다.

  1. 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행합니다.
  2. 기본 유추 서비스를 호출하기 전에 에이전트에 추가 컨텍스트를 제공합니다.
  3. 에이전트에서 제공되고 생성된 모든 메시지를 검사합니다.

호출 사전 및 사후 이벤트

AIContextProvider 클래스에는 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행하도록 재정의할 수 있는 두 가지 메서드가 있습니다.

  • InvokingAsync - 에이전트가 기본 유추 서비스를 호출하기 전에 호출됩니다. 에이전트에게 추가 컨텍스트를 제공하려면 AIContext 개체를 반환할 수 있습니다. 이 컨텍스트는 기본 서비스를 호출하기 전에 에이전트의 기존 컨텍스트와 병합됩니다. 요청에 추가할 지침, 도구 및 메시지를 제공할 수 있습니다.
  • InvokedAsync - 에이전트가 기본 유추 서비스에서 응답을 받은 후 호출됩니다. 요청 및 응답 메시지를 검사하고 컨텍스트 공급자의 상태를 업데이트할 수 있습니다.

직렬화

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. 또한 메모리를 스레드 상태의 일부로 유지할 수 있도록 serialization을 구현합니다.
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}");

이 자습서는 ContextProvider을 에이전트에 연결하여 메모리를 추가하는 방법을 보여줍니다.

중요합니다

모든 에이전트 유형이 ContextProvider를 지원하는 것은 아닙니다. 이 단계에서는 ChatAgent를 사용하며, 이는 ContextProvider을 지원합니다.

필수 조건

필수 구성 요소 및 패키지 설치는 이 자습서의 간단한 에이전트 만들기 및 실행 단계를 참조하세요.

ContextProvider 만들기

ContextProvider는 상속할 수 있으며, AgentThread에 연결할 수 있는 추상 클래스입니다 ChatAgent. 다음을 수행할 수 있습니다.

  1. 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행합니다.
  2. 기본 유추 서비스를 호출하기 전에 에이전트에 추가 컨텍스트를 제공합니다.
  3. 에이전트에서 제공되고 생성된 모든 메시지를 검사합니다.

호출 사전 및 사후 이벤트

ContextProvider 클래스에는 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행하도록 재정의할 수 있는 두 가지 메서드가 있습니다.

  • invoking - 에이전트가 기본 유추 서비스를 호출하기 전에 호출됩니다. 개체를 반환하여 에이전트에 Context 추가 컨텍스트를 제공할 수 있습니다. 이 컨텍스트는 기본 서비스를 호출하기 전에 에이전트의 기존 컨텍스트와 병합됩니다. 요청에 추가할 지침, 도구 및 메시지를 제공할 수 있습니다.
  • invoked - 에이전트가 기본 유추 서비스에서 응답을 받은 후 호출됩니다. 요청 및 응답 메시지를 검사하고 컨텍스트 공급자의 상태를 업데이트할 수 있습니다.

직렬화

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. 또한 메모리를 스레드 상태의 일부로 유지할 수 있도록 serialization을 구현합니다.

from collections.abc import MutableSequence, Sequence
from typing import Any

from agent_framework import ContextProvider, Context, ChatAgent, ChatClientProtocol, ChatMessage, ChatOptions


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."""
        # Ensure request_messages is a list
        messages_list = [request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages)

        # Check if we need to extract user info from user messages
        user_messages = [msg for msg in messages_list if 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=messages_list,
                    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 and isinstance(result.value, UserInfo):
                    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(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
            if thread.context_provider:
                user_info_memory = thread.context_provider.providers[0]
                if isinstance(user_info_memory, UserInfoMemory):
                    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())

다음 단계