Partilhar via


Tutorial: Criar um agente de LangChain.js com o Azure AI Search

Constrói um assistente de RH inteligente usando serviços LangChain.js e Azure. Este agente ajuda os funcionários da fictícia empresa NorthWind a encontrar respostas a questões de recursos humanos, pesquisando na documentação da empresa.

Vais usar o Azure AI Search para encontrar documentos relevantes e o Azure OpenAI para gerar respostas precisas. A estrutura LangChain.js trata da complexidade da orquestração de agentes, permitindo-lhe focar-se nas necessidades específicas do seu negócio.

O que irá aprender:

  • Implementar recursos do Azure usando a Azure Developer CLI
  • Crie um agente LangChain.js que se integre com os serviços Azure
  • Implementar geração aumentada por recuperação (RAG) para pesquisa de documentos
  • Teste e depure o seu agente localmente e no Azure

No final deste tutorial, tens uma API REST funcional que responde a perguntas de RH usando a documentação da tua empresa.

Visão geral da arquitetura

Captura de ecrã do diagrama que ilustra o fluxo de trabalho do agente LangChain.js e o seu caminho de decisão para utilizar a documentação de RH para responder a perguntas.

A NorthWind baseia-se em duas fontes de dados:

  • Documentação de RH acessível a todos os colaboradores
  • Base de dados confidencial de RH contendo dados sensíveis dos colaboradores.

Este tutorial foca-se na construção de um agente LangChain.js que determina se a pergunta de um colaborador pode ser respondida usando os documentos públicos de RH. Em caso afirmativo, o agente LangChain.js fornece a resposta diretamente.

Pré-requisitos

Para usar este exemplo no Codespace ou num contentor de desenvolvimento local, incluindo a construção e execução do agente LangChain.js, precisa do seguinte:

Se executares o código de exemplo localmente sem um contentor de desenvolvimento, também precisas de:

Recursos do Azure

São necessários os seguintes recursos do Azure. Eles são criados para si neste artigo usando os templates Azure Developer CLI e Bicep usando Azure Verified Modules (AVM). Os recursos são criados tanto com acesso sem palavra-passe como com chave para fins de aprendizagem. Este tutorial utiliza a sua conta local de programador para autenticação sem palavra-passe:

Diagrama da arquitetura Azure mostrando a implementação do RAG com componentes Container Apps, OpenAI, AI Search, Container Registry e Managed Identity com as suas ligações.

Arquitetura do agente

A estrutura LangChain.js fornece um fluxo de decisão para a construção de agentes inteligentes como um LangGraph. Neste tutorial, você cria um agente de LangChain.js que se integra ao Azure AI Search e ao Azure OpenAI para responder a perguntas relacionadas a RH. A arquitetura do agente foi projetada para:

  • Determine se uma pergunta é relevante para a documentação geral de RH disponível para todos os colaboradores.
  • Recupere documentos relevantes do Azure AI Search com base na consulta do utilizador.
  • Use o Azure OpenAI para gerar uma resposta com base nos documentos recuperados e no modelo LLM.

Componentes-chave:

  • Estrutura do gráfico: O agente LangChain.js é representado como um gráfico, onde:

    • Os nós executam tarefas específicas, como tomada de decisões ou recuperação de dados.
    • As arestas definem o fluxo entre nós, determinando a sequência de operações.
  • Integração do Azure AI Search:

    • Utiliza um modelo de embeddings para criar vetores.
    • Insere documentos de RH (*.md, *.pdf) na loja vetorial. Os documentos incluem:
      • Informação da empresa
      • Manual do funcionário
      • Manual de benefícios
      • Biblioteca de funções dos colaboradores
    • Recupera documentos relevantes com base no pedido do utilizador.
  • Integração Azure OpenAI:
    • Utiliza um grande modelo de linguagem para:
      • Determina se uma pergunta pode ser respondida a partir de documentos impessoais de RH.
      • Gera uma resposta com instrução usando o contexto de documentos e a pergunta do utilizador.

A tabela a seguir tem exemplos de perguntas de usuários que são e não são relevantes e respondíveis a partir de documentos gerais de Recursos Humanos:

Pergunta Relevante Explanation
Does the NorthWind Health Plus plan cover eye exams? Yes Os documentos de RH, como o manual do funcionário, devem fornecer uma resposta.
How much of my perks + benefits have I spent? Não Esta pergunta requer acesso a dados confidenciais dos funcionários, que estão fora do escopo deste agente.

Ao usar o framework LangChain.js, evita grande parte do código padrão agentico normalmente necessário para agentes e integração de serviços Azure, permitindo-lhe focar-se nas necessidades do seu negócio.

Clonar o repositório de código de exemplo

Num novo diretório, clone o repositório de código de exemplo e altere para o novo diretório:

git clone https://github.com/Azure-Samples/azure-typescript-langchainjs.git
cd azure-typescript-langchainjs

Este exemplo fornece o código necessário para criar recursos Azure seguros, construir o agente LangChain.js com Azure AI Search e Azure OpenAI, e usar o agente a partir de um servidor API Node.js Fastify.

Autentique-se no Azure CLI e no Azure Developer CLI

Inicie sessão no Azure com a CLI do Azure Developer, crie os recursos do Azure e implemente o código-fonte. Como o processo de implementação utiliza tanto o Azure CLI como o Azure Developer CLI, inicie sessão no Azure CLI e depois configure o Azure Developer CLI para usar a sua autenticação a partir do Azure CLI:

az login
azd config set auth.useAzCliAuth true

Crie recursos e implemente código com o Azure Developer CLI

Inicie o processo de implementação executando o azd up comando:

azd up

Durante o azd up comando, responda às perguntas:

  • Novo nome de ambiente: introduza um nome único de ambiente, como langchain-agent. Este nome de ambiente é usado como parte do grupo de recursos Azure.
  • Selecione uma subscrição Azure: selecione a subscrição onde os recursos são criados.
  • Selecione uma região: como eastus2.

O destacamento demora aproximadamente 10-15 minutos. A CLI Azure Developer orquestra o processo usando fases e ganchos definidos no azure.yaml ficheiro:

Fase de provisão (equivalente a azd provision):

  • Cria recursos Azure definidos em infra/main.bicep:
  • Gancho pós-provisionamento: Verifica se o índice do Azure AI Search já existe
    • Se o índice não existir: executa npm install e npm run load_data para carregar documentos de RH usando o carregador de PDF do LangChain.js e o cliente de embedding
    • Se o índice existir: salta o carregamento de dados para evitar duplicados (pode recarregar manualmente eliminando o índice ou executando npm run load_data) Fase de implementação (equivalente a azd deploy):
  • Gancho pré-implementação: Constrói a imagem Docker para o servidor API Fastify e envia-a para o Azure Container Registry
  • Implementa o servidor API containerizado para o Azure Container Apps

Quando a implementação termina, as variáveis do ambiente e a informação de recursos são guardadas no .env ficheiro na raiz do repositório. Pode consultar os recursos no portal Azure.

Os recursos são criados tanto com acesso sem palavra-passe como com chave para fins de aprendizagem. Este tutorial introdutório utiliza a sua conta local de programador para autenticação sem palavra-passe. Para aplicações de produção, utilize apenas autenticação sem palavra-passe com identidades geridas. Saiba mais sobre autenticação sem palavra-passe.

Use o código de exemplo localmente

Agora que os recursos Azure estão criados, podes executar o agente LangChain.js localmente.

Instalar dependências

  1. Instala os pacotes de Node.js para este projeto.

    npm install 
    

    Este comando instala as dependências definidas nos dois package.json ficheiros do packages-v1 diretório, incluindo:

  2. Constrói os dois pacotes: o servidor API e o agente de IA.

    npm run build
    

    Este comando cria uma ligação entre os dois pacotes para que o servidor API possa chamar o agente de IA.

Executar o servidor API localmente

O Azure Developer CLI criou os recursos Azure necessários e configurou as variáveis de ambiente no ficheiro raiz .env . Esta configuração incluía um gancho de pós-provisionamento para carregar os dados no armazenamento vetorial. Agora, podes correr o servidor API do Fastify que aloja o agente LangChain.js. Inicia o servidor API do Fastify.

npm run dev

O servidor arranca e ouve na porta 3000. Pode testar o servidor navegando até [http://localhost:3000] no seu navegador web. Deverias ver uma mensagem de boas-vindas a indicar que o servidor está a funcionar.

Use a API para colocar questões

Pode usar uma ferramenta como o REST Client ou curl enviar um pedido POST ao /ask endpoint com um corpo JSON contendo a sua pergunta.

As consultas do cliente REST estão disponíveis no packages-v1/server-api/http diretório.

Exemplo usando curl:

curl -X POST http://localhost:3000/answer -H "Content-Type: application/json" -d "{\"question\": \"Does the NorthWind Health Plus plan cover eye exams?\"}"

Deverias receber uma resposta em JSON com a resposta do agente LangChain.js.

{
  "answer": "Yes, the NorthWind Health Plus plan covers eye exams. According to the Employee Handbook, employees enrolled in the Health Plus plan are eligible for annual eye exams as part of their vision benefits."
}

Várias perguntas de exemplo estão disponíveis no packages-v1/server-api/http diretório. Abra os ficheiros no Visual Studio Code com o cliente REST para os testar rapidamente.

Compreender o código da aplicação

Esta secção explica como o agente LangChain.js se integra com os serviços Azure. A aplicação do repositório está organizada como um espaço de trabalho npm com dois pacotes principais:

Project Root
│
├── packages-v1/
│   │
│   ├── langgraph-agent/                    # Core LangGraph agent implementation
│   │   ├── src/
│   │   │   ├── azure/                      # Azure service integrations
│   │   │   │   ├── azure-credential.ts     # Centralized auth with DefaultAzureCredential
│   │   │   │   ├── embeddings.ts           # Azure OpenAI embeddings + PDF loading + rate limiting
│   │   │   │   ├── llm.ts                  # Azure OpenAI chat completion (key-based & passwordless)
│   │   │   │   └── vector_store.ts         # Azure AI Search vector store + indexing + similarity search
│   │   │   │
│   │   │   ├── langchain/                  # LangChain agent logic
│   │   │   │   ├── node_get_answer.ts      # RAG: retrieves docs + generates answers
│   │   │   │   ├── node_requires_hr_documents.ts  # Determines if HR docs needed
│   │   │   │   ├── nodes.ts                # LangGraph node definitions + state management
│   │   │   │   └── prompt.ts               # System prompts + conversation templates
│   │   │   │
│   │   │   └── scripts/                    # Utility scripts
│   │   │       └── load_vector_store.ts    # Uploads PDFs to Azure AI Search
│   │   │
│   │   └── data/                           # Source documents (PDFs) for vector store
│   │
│   └── server-api/                         # Fastify REST API server
│       └── src/
│           └── server.ts                   # HTTP server with /answer endpoint
│
├── infra/                                  # Infrastructure as Code
│   └── main.bicep                          # Azure resources: Container Apps, OpenAI, AI Search, ACR, managed identity
│
├── azure.yaml                              # Azure Developer CLI config + deployment hooks
├── Dockerfile                              # Multi-stage Docker build for containerized deployment
└── package.json                            # Workspace configuration + build scripts

Decisões arquitetónicas chave:

  • Estrutura monorepo : os espaços de trabalho npm permitem dependências partilhadas e pacotes ligados
  • Separação de preocupações: A lógica do agente (langgraph-agent) é independente do servidor API (server-api)
  • Autenticação centralizada: Os ficheiros em ./langgraph-agent/src/azure lidam com autenticação baseada em chaves e sem palavra-passe e integração com serviços Azure

Autenticação para Azure Services

A aplicação suporta métodos de autenticação baseados em chaves e sem palavra-passe, controlados pela SET_PASSWORDLESS variável de ambiente. A API DefaultAzureCredential da biblioteca Azure Identity é usada para autenticação sem palavra-passe, permitindo que a aplicação corra de forma fluida em ambientes de desenvolvimento local e Azure. Pode ver esta autenticação no seguinte excerto de código:

import { DefaultAzureCredential } from "@azure/identity";

export const CREDENTIAL = new DefaultAzureCredential();

export const SCOPE_OPENAI = "https://cognitiveservices.azure.com/.default";

export async function azureADTokenProvider_OpenAI() {
  const tokenResponse = await CREDENTIAL.getToken(SCOPE_OPENAI);
  return tokenResponse.token;
}

Ao usar bibliotecas de terceiros como a LangChain.js ou a OpenAI para aceder ao Azure OpenAI, precisa de uma função de fornecedor de token em vez de passar diretamente um objeto de credencial. A getBearerTokenProvider função da biblioteca Azure Identity resolve este problema criando um fornecedor de tokens que obtém e atualiza automaticamente os tokens portadores OAuth 2.0 para um âmbito específico de recurso Azure (por exemplo, "https://cognitiveservices.azure.com/.default"). Configuras o âmbito uma vez durante a configuração, e o fornecedor de tokens trata de toda a gestão automaticamente. Esta abordagem funciona com qualquer credencial de biblioteca Azure Identity, incluindo credenciais de identidade gerida e credenciais de CLI Azure. Enquanto as bibliotecas do Azure SDK aceitam DefaultAzureCredential diretamente, bibliotecas de terceiros como LangChain.js requerem este padrão de fornecedor de tokens para ultrapassar a lacuna de autenticação.

A integração do Azure AI Search

O recurso Azure AI Search armazena embeddings de documentos e permite a pesquisa semântica por conteúdos relevantes. A aplicação usa LangChains AzureAISearchVectorStore para gerir a loja vetorial sem que tenhas de definir o esquema do índice.

O armazenamento vetorial é criado com configuração para operações de administração (escrita) e de consulta (leitura), permitindo que o carregamento e a consulta de documentos possam usar configurações diferentes. Isto é importante quer esteja a usar chaves ou autenticação sem palavra-passe com identidades geridas.

A implementação do Azure Developer CLI inclui um hook pós-implementação que carrega os documentos para a loja vetorial usando o carregador de PDF e o cliente de incorporação do LangChain.js. Este gancho pós-implementação é o último passo do azd up comando após a criação do recurso Azure AI Search. O script de carregamento de documentos utiliza lógica de lote e de retentativa para lidar com os limites de taxa de serviço.

postdeploy:
  posix:
    sh: bash
    run: |
      echo "Checking if vector store data needs to be loaded..."
      
      # Check if already loaded
      INDEX_CREATED=$(azd env get-values | grep INDEX_CREATED | cut -d'=' -f2 || echo "false")
      
      if [ "$INDEX_CREATED" = "true" ]; then
        echo "Index already created. Skipping data load."
        echo "Current document count: $(azd env get-values | grep INDEX_DOCUMENT_COUNT | cut -d'=' -f2)"
      else
        echo "Loading vector store data..."
        npm install
        npm run build
        npm run load_data
        
        # Get document count from the index
        SEARCH_SERVICE=$(azd env get-values | grep AZURE_AISEARCH_ENDPOINT | cut -d'/' -f3 | cut -d'.' -f1)
        DOC_COUNT=$(az search index show --service-name $SEARCH_SERVICE --name northwind --query "documentCount" -o tsv 2>/dev/null || echo "0")
        
        # Mark as loaded
        azd env set INDEX_CREATED true
        azd env set INDEX_DOCUMENT_COUNT $DOC_COUNT
        
        echo "Data loading complete! Indexed $DOC_COUNT documents."
      fi

Use o ficheiro raiz .env criado pela CLI do Azure Developer, pode autenticar-se no recurso Azure AI Search e criar o cliente AzureAISearchVectorStore :

const endpoint = process.env.AZURE_AISEARCH_ENDPOINT;
const indexName = process.env.AZURE_AISEARCH_INDEX_NAME;

const adminKey = process.env.AZURE_AISEARCH_ADMIN_KEY;
const queryKey = process.env.AZURE_AISEARCH_QUERY_KEY;

export const QUERY_DOC_COUNT = 3;
const MAX_INSERT_RETRIES = 3;

const shared_admin = {
  endpoint,
  indexName,
};

export const VECTOR_STORE_ADMIN_KEY: AzureAISearchConfig = {
  ...shared_admin,
  key: adminKey,
};

export const VECTOR_STORE_ADMIN_PASSWORDLESS: AzureAISearchConfig = {
  ...shared_admin,
  credentials: CREDENTIAL,
};

export const VECTOR_STORE_ADMIN_CONFIG: AzureAISearchConfig =
  process.env.SET_PASSWORDLESS == "true"
    ? VECTOR_STORE_ADMIN_PASSWORDLESS
    : VECTOR_STORE_ADMIN_KEY;

const shared_query = {
  endpoint,
  indexName,
  search: {
    type: AzureAISearchQueryType.Similarity,
  },
};

// Key-based config
export const VECTOR_STORE_QUERY_KEY: AzureAISearchConfig = {
  key: queryKey,
  ...shared_query,
};

export const VECTOR_STORE_QUERY_PASSWORDLESS: AzureAISearchConfig = {
  credentials: CREDENTIAL,
  ...shared_query,
};

export const VECTOR_STORE_QUERY_CONFIG =
  process.env.SET_PASSWORDLESS == "true"
    ? VECTOR_STORE_QUERY_PASSWORDLESS
    : VECTOR_STORE_QUERY_KEY;

Quando realizas uma consulta, a loja vetorial converte a consulta do utilizador numa embedding, pesquisa documentos com representações vetoriais semelhantes e devolve as partes mais relevantes.

export function getReadOnlyVectorStore(): AzureAISearchVectorStore {
  const embeddings = getEmbeddingClient();
  return new AzureAISearchVectorStore(embeddings, VECTOR_STORE_QUERY_CONFIG);
}

export async function getDocsFromVectorStore(
  query: string,
): Promise<Document[]> {
  const store = getReadOnlyVectorStore();

  // @ts-ignore
  //return store.similaritySearchWithScore(query, QUERY_DOC_COUNT);
  return store.similaritySearch(query, QUERY_DOC_COUNT);
}

Como o armazenamento vetorial é construído sobre LangChain.js, ele simplifica a complexidade de interagir diretamente com o armazenamento vetorial. Depois de aprenderes a interface LangChain.js vector store, podes facilmente mudar para outras implementações de vector store no futuro.

Integração Azure OpenAI

A aplicação utiliza Azure OpenAI tanto para embeddings como para capacidades de grandes modelos de linguagem (LLM). A AzureOpenAIEmbeddings classe do LangChain.js é usada para gerar embeddings para documentos e consultas. Depois de criar o cliente de embeddings, LangChain.js usa-o para criar os embeddings.

Integração Azure OpenAI para embeddings

Use o ficheiro raiz .env criado pela CLI Azure Developer para autenticar no recurso Azure OpenAI e crie o cliente AzureOpenAIEmbeddings :

const shared = {
  azureOpenAIApiInstanceName: instance,
  azureOpenAIApiEmbeddingsDeploymentName: model,
  azureOpenAIApiVersion: apiVersion,
  azureOpenAIBasePath,
  dimensions: 1536, // for text-embedding-3-small
  batchSize: EMBEDDING_BATCH_SIZE,
  maxRetries: 7,
  timeout: 60000,
};

export const EMBEDDINGS_KEY_CONFIG = {
  azureOpenAIApiKey: key,
  ...shared,
};

export const EMBEDDINGS_CONFIG_PASSWORDLESS = {
  azureADTokenProvider: azureADTokenProvider_OpenAI,
  ...shared,
};

export const EMBEDDINGS_CONFIG =
  process.env.SET_PASSWORDLESS == "true"
    ? EMBEDDINGS_CONFIG_PASSWORDLESS
    : EMBEDDINGS_KEY_CONFIG;
export function getEmbeddingClient(): AzureOpenAIEmbeddings {
  return new AzureOpenAIEmbeddings({ ...EMBEDDINGS_CONFIG });
}

Integração Azure OpenAI para LLM

Use o ficheiro raiz .env criado pelo Azure Developer CLI para autenticar no recurso Azure OpenAI e crie o cliente AzureChatOpenAI :

const shared = {
  azureOpenAIApiInstanceName: instance,
  azureOpenAIApiDeploymentName: model,
  azureOpenAIApiVersion: apiVersion,
  azureOpenAIBasePath,
  maxTokens: maxTokens ? parseInt(maxTokens, 10) : 100,
  maxRetries: 7,
  timeout: 60000,
  temperature: 0,
};

export const LLM_KEY_CONFIG = {
  azureOpenAIApiKey: key,
  ...shared,
};

export const LLM_CONFIG_PASSWORDLESS = {
  azureADTokenProvider: azureADTokenProvider_OpenAI,
  ...shared,
};

export const LLM_CONFIG =
  process.env.SET_PASSWORDLESS == "true"
    ? LLM_CONFIG_PASSWORDLESS
    : LLM_KEY_CONFIG;

A aplicação utiliza a AzureChatOpenAI classe da LangChain.js @langchain/openai para interagir com modelos Azure OpenAI.

export const callChatCompletionModel = async (
  state: typeof StateAnnotation.State,
  _config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
  const llm = new AzureChatOpenAI({
    ...LLM_CONFIG,
  });

  const completion = await llm.invoke(state.messages);
  completion;

  return {
    messages: [
      ...state.messages,
      {
        role: "assistant",
        content: completion.content,
      },
    ],
  };
};

Fluxo de trabalho do agente LangGraph

O agente utiliza o LangGraph para definir um fluxo de decisão que determina se uma pergunta pode ser respondida usando documentos de RH.

Estrutura do grafo:

import { StateGraph } from "@langchain/langgraph";
import {
  START,
  ANSWER_NODE,
  DECISION_NODE,
  route as endRoute,
  StateAnnotation,
} from "./langchain/nodes.js";
import { getAnswer } from "./langchain/node_get_answer.js";
import {
  requiresHrResources,
  routeRequiresHrResources,
} from "./langchain/node_requires_hr_documents.js";

const builder = new StateGraph(StateAnnotation)
  .addNode(DECISION_NODE, requiresHrResources)
  .addNode(ANSWER_NODE, getAnswer)
  .addEdge(START, DECISION_NODE)
  .addConditionalEdges(DECISION_NODE, routeRequiresHrResources)
  .addConditionalEdges(ANSWER_NODE, endRoute);

export const hr_documents_answer_graph = builder.compile();
hr_documents_answer_graph.name = "Azure AI Search + Azure OpenAI";

O fluxo de trabalho consiste nos seguintes passos:

  • Começar: O utilizador submete uma pergunta.
  • requires_hr_documents nó: O LLM determina se a pergunta pode ser respondida com base em documentos gerais de RH.
  • Encaminhamento condicional:
    • Se sim, então prossegue para o nó get_answer.
    • Se não, responde que a pergunta requer dados pessoais do RH.
  • get_answer nó: Recupera documentos e gera respostas.
  • Fim: Devolve a resposta ao utilizador.

Esta verificação de relevância é importante porque nem todas as perguntas de RH podem ser respondidas a partir de documentos gerais. Perguntas pessoais como "Quanto PTO tenho?" exigem acesso a bases de dados de colaboradores que contenham dados individuais. Ao verificar primeiro a relevância, o agente evita alucinar respostas para perguntas que necessitam de informações pessoais a que não tem acesso.

Decida se a pergunta requer documentos de RH

O requires_hr_documents nó utiliza um LLM para determinar se a pergunta do utilizador pode ser respondida usando documentos gerais de RH. Utiliza um modelo de prompt que instrui o modelo a responder com YES ou NO com base na relevância da pergunta. Devolve a resposta numa mensagem estruturada, que pode ser passada ao longo do fluxo de trabalho. O nó seguinte usa esta resposta para encaminhar o fluxo de trabalho para END ou ANSWER_NODE.

// @ts-nocheck
import { getLlmChatClient } from "../azure/llm.js";
import { StateAnnotation } from "../langchain/state.js";
import { RunnableConfig } from "@langchain/core/runnables";
import { BaseMessage } from "@langchain/core/messages";
import { ANSWER_NODE, END } from "./nodes.js";

const PDF_DOCS_REQUIRED = "Answer requires HR PDF docs.";

export async function requiresHrResources(
  state: typeof StateAnnotation.State,
  _config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> {
  const lastUserMessage: BaseMessage = [...state.messages].reverse()[0];

  let pdfDocsRequired = false;

  if (lastUserMessage && typeof lastUserMessage.content === "string") {
    const question = `Does the following question require general company policy information that could be found in HR documents like employee handbooks, benefits overviews, or company-wide policies, then answer yes. Answer no if this requires personal employee-specific information that would require access to an individual's private data, employment records, or personalized benefits details: '${lastUserMessage.content}'. Answer with only "yes" or "no".`;

    const llm = getLlmChatClient();
    const response = await llm.invoke(question);
    const answer = response.content.toLocaleLowerCase().trim();
    console.log(`LLM question (is HR PDF documents required): ${question}`);
    console.log(`LLM answer (is HR PDF documents required): ${answer}`);
    pdfDocsRequired = answer === "yes";
  }

  // If HR documents (aka vector store) are required, append an assistant message to signal this.
  if (!pdfDocsRequired) {
    const updatedState = {
      messages: [
        ...state.messages,
        {
          role: "assistant",
          content:
            "Not a question for our HR PDF resources. This requires data specific to the asker.",
        },
      ],
    };

    return updatedState;
  } else {
    const updatedState = {
      messages: [
        ...state.messages,
        {
          role: "assistant",
          content: `${PDF_DOCS_REQUIRED} You asked: ${lastUserMessage.content}. Let me check.`,
        },
      ],
    };

    return updatedState;
  }
}

export const routeRequiresHrResources = (
  state: typeof StateAnnotation.State,
): typeof END | typeof ANSWER_NODE => {
  const lastMessage: BaseMessage = [...state.messages].reverse()[0];

  if (lastMessage && !lastMessage.content.includes(PDF_DOCS_REQUIRED)) {
    console.log("go to end");
    return END;
  }
  console.log("go to llm");
  return ANSWER_NODE;
};

Obtenha os documentos necessários de RH

Uma vez determinado que a pergunta requer documentos de RH, o fluxo de trabalho usa getAnswer para recuperar os documentos relevantes do repositório vetorial, adicioná-los ao contexto do prompt e passar todo o prompt para o LLM.

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { getLlmChatClient } from "../azure/llm.js";
import { StateAnnotation } from "./nodes.js";
import { AIMessage } from "@langchain/core/messages";
import { getReadOnlyVectorStore } from "../azure/vector_store.js";

const EMPTY_STATE = { messages: [] };

export async function getAnswer(
  state: typeof StateAnnotation.State = EMPTY_STATE,
): Promise<typeof StateAnnotation.Update> {
  const vectorStore = getReadOnlyVectorStore();
  const llm = getLlmChatClient();

  // Extract the last user message's content from the state as input
  const lastMessage = state.messages[state.messages.length - 1];

  const userInput =
    lastMessage && typeof lastMessage.content === "string"
      ? lastMessage.content
      : "";

  const docs = await vectorStore.similaritySearch(userInput, 3);

  if (docs.length === 0) {
    const noDocMessage = new AIMessage(
      "I'm sorry, I couldn't find any relevant information to answer your question.",
    );
    return {
      messages: [...state.messages, noDocMessage],
    };
  }

  const formattedDocs = docs.map((doc) => doc.pageContent).join("\n\n");

  const prompt = ChatPromptTemplate.fromTemplate(`
    Use the following context to answer the question:

    {context}

    Question: {question}
    `);

  const ragChain = prompt.pipe(llm);

  const result = await ragChain.invoke({
    context: formattedDocs,
    question: userInput,
  });

  const assistantMessage = new AIMessage(result.text);

  return {
    messages: [...state.messages, assistantMessage],
  };
}

Se não forem encontrados documentos relevantes, o agente devolve uma mensagem a indicar que não conseguiu encontrar resposta nos documentos de RH.

Solução de problemas

Para quaisquer problemas com o procedimento, crie um problema no repositório de código de exemplo

Limpeza de recursos

Pode eliminar o grupo de recursos, que contém o recurso Azure AI Search e o recurso Azure OpenAI, ou usar o Azure Developer CLI para eliminar imediatamente todos os recursos criados por este tutorial.

azd down --purge