Multiplexar múltiplos utilizadores numa sessão de agente hospedada

Por predefinição, cada autor da chamada recebe a sua própria sessão alojada do agente, conforme descrito em Isolar sessões alojadas do agente para cada utilizador. Aplicações que servem muitos utilizadores – um bot do Teams, um gateway ISV ou uma plataforma de apoio ao cliente – não precisam de uma sessão por utilizador. Em vez disso, um serviço de nível intermédio mapeia muitos utilizadores para um conjunto limitado de sessões partilhadas e identifica cada utilizador em cada chamada.

Este artigo mostra-lhe como agrupar sessões entre utilizadores do seu escalão intermédio, mantendo os dados de cada utilizador isolados dentro de uma sessão partilhada.

A plataforma isola o estado da conversa para o utilizador, mesmo quando os utilizadores partilham uma sessão: uma cadeia de respostas criada por um utilizador não pode ser continuada por outro utilizador através de previous_response_id, e context.get_history() devolve apenas o histórico que o utilizador do pedido atual está autorizado a ver. És responsável por duas coisas: o mapeamento entre utilizador e sessão na tua camada intermédia e o particionamento de quaisquer dados que o teu contentor armazena ele próprio (ficheiros, linhas ou cache), para além desse estado de conversação gerido pela plataforma.

Um exemplo completo e executável de multiplexação de sessões demonstra ambos os lados – o pool de sessões de nível intermédio e o gestor de contentores – e este artigo liga os seus ficheiros à medida que avança.

Pré-requisitos

  • Um agente hospedado que utiliza o protocolo container versão 2.0.0. Para atualizar, veja Migrar agentes hospedados.
  • A Microsoft.CognitiveServices/accounts/AIServices/agents/endpoints/UserIdentityImpersonation/action permissão atribuída à identidade do seu serviço de nível intermédio. Esta permissão não está incluída nas funções incorporadas; conceda-a através de uma função personalizada — veja Delegar a identidade do utilizador final. Sem ele, o x-ms-user-identity cabeçalho é rejeitado com um 403.
  • A biblioteca cliente Azure AI Projects para o nível intermédio, e o Azure AI AgentServer SDK para o contentor (azure-ai-agentserver-core2.0.0b7+ para Python, ou Azure.AI.AgentServer.Core 1.0.0-beta.26+ para .NET).
  • Um agente destacado para testar. O isolamento não é aplicado para as viagens locais.

Isolar dois utilizadores numa sessão partilhada

Comece pelo comportamento principal: dois utilizadores – chamem-lhes Alice e Bob, os utilizadores representados na amostra – podem partilhar um, agent_session_ide a plataforma mantém a conversa de cada utilizador privada. A sua camada intermédia identifica, em cada chamada, o utilizador representado através do cabeçalho x-ms-user-identity (delegação). Para continuar a conversa do utilizador, transmite a resposta anterior desse utilizador como previous_response_id.

O cliente mínimo invoke_previous_response_isolation.py no exemplo envia exatamente isso, usando o cliente Responses associado a agentes do SDK:

# Agent-bound Responses client from the Foundry SDK.
responses_client = project_client.get_openai_client(agent_name=agent_name).responses

# Target the shared session with agent_session_id, and identify the acted-for
# user with x-ms-user-identity (delegation). Pass previous_response_id to
# continue this user's own chain. Don't send x-agent-user-id; Foundry sets the
# container-side request context after it resolves the user.
kwargs = {
    "input": user_message,
    "stream": False,
    "store": True,
    "extra_body": {"agent_session_id": session_id},
    "extra_headers": {"x-ms-user-identity": user_id},
}
if previous_response_id:
    kwargs["previous_response_id"] = previous_response_id

response = responses_client.create(**kwargs)

A plataforma liga cada cadeia de respostas ao utilizador que a criou. Se o Bob enviar a previous_response_id chamada da Alice enquanto está na mesma sessão, a chamada falha – o Bob não pode continuar a conversa da Alice. Essa garantia mantém-se sem qualquer código extra de isolamento no seu contentor.

Escalar para vários utilizadores com um pool de sessões

Isolar dois utilizadores numa sessão é o bloco de construção. Para servir muitos utilizadores, agrupe-os num conjunto limitado de sessões em vez de abrir uma sessão por utilizador.

Cada sessão conta contra os limites regionais de sessões concorrentes enquanto processa ativamente um turno, por isso uma sessão por utilizador não escala. Como os utilizadores leem, pensam e digitam entre uma interação e outra, o pico de pedidos simultâneos é normalmente uma pequena fração do número total de utilizadores. Dimensione um pool até esse pico, depois mapeie cada utilizador para uma sessão nela e passe a identidade desse utilizador em cada chamada, exatamente como na secção anterior.

Decida como mapear os utilizadores para as sessões. Estratégias comuns incluem:

  • Persistente, menos sobrecarregado. Um utilizador que regressa reutiliza a sua sessão; Novos utilizadores vão para a sessão menos carregada. Esta estratégia distribui a carga de forma uniforme e mantém os turnos do utilizador juntos. Aumenta o grupo quando as sessões atingirem um limite por utilizador.
  • Baseado em hash. Atribuir uma sessão com hash(user_id) % pool_size. Esta estratégia é simples e sem estado, mas a carga pode ser desigual e redimensionar o pool reorganiza os utilizadores.
  • Round-robin. Distribua os pedidos de forma equilibrada pelo pool. Esta estratégia é simples, mas os turnos do utilizador podem depender de sessões diferentes.
  • Baseado em grupos. Encaminhe por inquilino, equipa ou região para que os utilizadores relacionados partilhem sessões. Esta estratégia é útil quando os utilizadores estão num contexto de partilha de grupo.

O autor da chamada invoke_session_pool.py no exemplo implementa a atribuição detida pelo autor da chamada com duas estratégias, sticky-fill e round-robin. Um utilizador que regressa mantém sempre a sua sessão; Um novo utilizador é colocado pela estratégia selecionada. O caminho de preenchimento fixo preenche a sessão menos carregada e só abre uma nova quando cada sessão estiver cheia:

def get_session_for_user(self, user_id: str) -> str:
    if user_id in self.user_to_session:
        return self.user_to_session[user_id]      # returning user is sticky
    session_id = self._next_fill_session()        # new user: place by strategy
    self.user_to_session[user_id] = session_id
    self.session_user_counts[session_id] += 1
    return session_id

def _next_fill_session(self) -> str:
    # Reuse a session with capacity; open a new one only when all are full.
    session_id = next(
        (s for s, count in self.session_user_counts.items()
         if count < self.max_users_per_session),
        None,
    )
    if session_id is None:
        session_id = self._session_name(len(self.session_user_counts))
        self.session_user_counts[session_id] = 0
    return session_id

Introduza o ID da sessão devolvido na mesma chamada delegada mostrada anteriormente: passa a ser agent_session_id em extra_body, e x-ms-user-identity mantém-se como o identificador por utilizador.

Processe a solicitação no seu contentor

No protocolo 2.0.0, a plataforma resolve o utilizador em nome do qual a ação é executada e disponibiliza-o ao seu processador através de get_request_context(). Valida esse contexto (falha no encerramento quando está em falta, como em execuções locais), depois deixa a plataforma devolver o histórico por utilizador com context.get_history(). O main.py handler na amostra não mantém nenhum estado de conversa próprio:

from azure.ai.agentserver.core import get_request_context

@app.response_handler
async def handler(request, context, _cancellation_signal):
    ctx = get_request_context()
    if not (ctx.user_id and ctx.call_id):
        # Hosted protocol 2.0.0 populates this context; off-platform it's absent.
        raise ValueError("A user context is required on protocol 2.0.0.")

    user_input = await context.get_input_text() or "Hello!"
    history = await context.get_history()       # platform-authorized for this user
    input_items = _build_input(user_input, history)

    response = _responses_client.create(model=_model, input=input_items, store=False)
    return TextResponse(context, request, text=response.output_text)

Como a plataforma autoriza context.get_history() por pedido, um utilizador numa sessão partilhada nunca recebe o histórico de conversas de outro utilizador.

Particione os dados por utilizador que o seu contentor armazena

A plataforma isola o histórico das conversas para si. Se o teu contentor também armazena os seus próprios dados – ficheiros, linhas de base de dados ou cache – esses dados não são particionados automaticamente. Escreve-o tanto pelo ID da sessão como pelo ID do utilizador, para que dois utilizadores na mesma sessão não possam ver os dados um do outro:

partition = (agent_session_id, user_id)

Warning

Quando os utilizadores partilham uma sessão, a plataforma não particiona os dados que o seu contentor armazena. Se o teu contentor indexar esses dados apenas pelo ID da sessão, todos os utilizadores no pool veem os mesmos dados. Inclua sempre o ID do utilizador na chave de partição.

Leia o ID do utilizador a partir do contexto da plataforma de cada pedido:

from azure.ai.agentserver.core import get_request_context

def partition_key() -> tuple[str, str]:
    ctx = get_request_context()
    if not ctx or not ctx.user_id:
        raise PermissionError("A user context is required on protocol 2.0.0.")
    return (ctx.session_id, ctx.user_id)   # key all user-owned data by this

A plataforma também injeta o utilizador no cabeçalho do pedido x-agent-user-id. Se o seu ambiente de execução não usar o contexto do SDK, leia este cabeçalho diretamente.

A plataforma preenche get_request_context().user_id o protocolo 2.0.0. Nunca use apenas o ID da sessão para dados pertencentes ao utilizador quando mais do que um utilizador puder entrar na sessão.

Para um exemplo prático de armazenamento por sessão para construir, veja o exemplo de agente de tomada de notas. Coloca um ficheiro por sessão em $HOME. Para uma sessão partilhada, estende essa chave com o ID de utilizador do contexto do pedido para que cada utilizador tenha a sua própria partição.

Verifique o isolamento

Confirme a garantia com o teste A-A-B da amostra, invoke_previous_response_isolation.py. Execute-o no teu agente implementado com dois utilizadores distintos (o exemplo utiliza, por predefinição, Alice e Bob):

  1. Como Alice, cria uma resposta numa sessão partilhada e captura o seu id.
  2. Como Alice, crie uma segunda resposta na mesma sessão com previous_response_id definido para o id da primeira resposta e capture o respetivo id.
  3. Enquanto o Bob, na mesma sessão, envia um pedido com previous_response_id definido para a segunda resposta da Alice. A chamada falha – o Bob não consegue continuar a cadeia da Alice.

Use dois utilizadores ou IDs de objetos diferentes do Entra. Dois rótulos que correspondem à mesma identidade não constituem um teste válido entre utilizadores diferentes.

Enviar um cabeçalho de isolamento legado num caminho do protocolo 2.0.0 devolve um erro, porque esse modelo é substituído pelo contexto do utilizador da plataforma.