Blog

So erstellen Sie mit LangGraph.js und Elasticsearch einen KI-Suchworkflow für das Finanzwesen

Erfahren Sie, wie Sie LangGraph.js mit Elasticsearch verwenden können, um einen KI-gestützten Finanz-Suchworkflow zu erstellen, der Abfragen in natürlicher Sprache in dynamische, bedingte Filter für Investitions- und Marktanalysen umwandelt.

Beim Erstellen von KI-Suchanwendungen müssen häufig mehrere Aufgaben, der Datenabruf und die Datenextraktion zu einem nahtlosen Workflow koordiniert werden. LangGraph vereinfacht diesen Prozess, indem es Entwicklern ermöglicht, KI-Agenten mithilfe einer node-basierten Struktur zu orchestrieren. In diesem Artikel werden wir eine Finanzlösung mit LangGraph.js erstellen.

Was ist LangGraph?

LangGraph ist ein Framework zum Erstellen von KI-Agenten und deren Orchestrierung in einem Workflow, um KI-unterstützte Anwendungen zu entwickeln. LangGraph verfügt über eine Knotenarchitektur, in der wir Funktionen deklarieren können, die Aufgaben darstellen, und diese als Knoten des Workflows zuweisen können. Das Ergebnis der Interaktion mehrerer Knoten ist ein Graph. LangGraph ist Teil des umfassenderen LangChain-Ökosystems, das Tools für die Erstellung modularer und zusammensetzbarer KI-Systeme bereitstellt.

Zur Veranschaulichung dessen, warum LangGraph nützlich ist, werden wir eine problematische Situation damit lösen.

Überblick über die Lösung

In einem Risikokapitalunternehmen haben Investoren Zugriff auf eine umfangreiche Datenbank mit zahlreichen Filteroptionen, aber wenn man Kriterien kombinieren möchte, wird es schwierig und langsam. Dies kann dazu führen, dass einige relevante Start-ups für Investitionen nicht entdeckt werden. Das Ergebnis: Man verbringt viele Stunden damit, die besten Kandidaten zu identifizieren, oder verpasst sogar Chancen.

Mit LangGraph und Elasticsearch können wir gefilterte Suchen in natürlicher Sprache durchführen, sodass Nutzer komplexe Anfragen mit Dutzenden von Filtern nicht manuell erstellen müssen. Um die Flexibilität zu erhöhen, entscheidet der Workflow anhand der Nutzereingaben automatisch zwischen zwei Abfragetypen.

  • Investitionsorientierte Anfragen: Diese zielen auf finanzielle und finanzierungsbezogene Aspekte von Start-ups ab, wie Finanzierungsrunden, Bewertung oder Umsatz. Beispiel: „Suche Start-ups mit einer Series-A- oder Series-B-Finanzierung zwischen 8 und 25 Mio. Dollar und einem monatlichen Umsatz von über 500.000 Dollar.“

  • Marktorientierte Anfragen: Diese konzentrieren sich auf Branchensegmente, geografische Märkte oder Geschäftsmodelle und helfen dabei, Chancen in bestimmten Sektoren oder Regionen zu identifizieren. Beispiel: „Suche Fintech- und Healthcare-Start-ups in San Francisco, New York oder Boston.“

Um die Abfragen robust zu halten, werden wir das LLM dazu bringen, Suchvorlagen anstelle vollständiger DSL-Abfragen zu erstellen. Auf diese Weise erhalten Sie immer die gewünschte Abfrage, und der LLM muss lediglich die Lücken ausfüllen und trägt nicht die Verantwortung, die benötigte Abfrage jedes Mal neu zu erstellen.

Was Sie brauchen, um loszulegen

  • Elasticsearch API-Schlüssel

  • OpenAPI-API-Schlüssel

  • Node 18 oder neuer

Schritt-für-Schritt-Anweisungen

In diesem Abschnitt schauen wir uns an, wie die App aussehen wird. Dafür verwenden wir TypeScript, ein Superset von JavaScript, das statische Typen hinzufügt, um den Code zuverlässiger, leichter zu pflegen und sicherer zu machen, indem Fehler frühzeitig erkannt werden, während er vollständig kompatibel mit bestehendem JavaScript bleibt.

Der Knotenfluss sieht wie folgt aus:

Das obige Bild wird von LangGraph generiert und stellt den Workflow dar, der die Ausführungsreihenfolge und die bedingte Logik zwischen den Knoten definiert:

  • decideStrategy: Verwendet ein LLM, um die Anfrage des Nutzers zu analysieren und zwischen zwei spezialisierten Suchstrategien zu entscheiden – investitionsorientiert oder marktorientiert.

  • prepareInvestmentSearch: Extrahiert Filterwerte aus der Abfrage und erstellt eine vordefinierte Vorlage, die finanzielle und finanzierungsbezogene Parameter hervorhebt.

  • prepareMarketSearch: Extrahiert ebenfalls Filterwerte, baut jedoch dynamisch Parameter auf, die den Markt-, Branchen- und geografischen Kontext betonen.

  • executeSearch: Sendet die konstruierte Abfrage mit einer Suchvorlage an Elasticsearch und ruft die entsprechenden Start-up-Dokumente ab.

  • VisualizeResults: Formatiert die Endergebnisse in einer klaren, lesbaren Zusammenfassung, die die wichtigsten Startup-Attribute wie Finanzierung, Branche und Umsatz aufzeigt.

Dieser Fluss umfasst eine bedingte Verzweigung, die als „if“-Anweisung fungiert und basierend auf der Eingabe des Nutzers bestimmt, ob der Investitions- oder Marktsuchpfad verwendet wird. Diese vom LLM gesteuerte Entscheidungslogik macht den Workflow adaptiv und kontextsensitiv – ein Mechanismus, den wir in den nächsten Abschnitten genauer untersuchen werden.

LangGraph-Status

Bevor wir jeden Knoten einzeln betrachten, müssen wir verstehen, wie die Knoten kommunizieren und Daten austauschen. Dafür ermöglicht uns LangGraph, den Workflow-Status zu definieren. Dies definiert den gemeinsamen Status, der zwischen Knoten weitergegeben wird.

Der Zustand fungiert als gemeinsamer Container, der während des gesamten Workflows Zwischendaten speichert: Er beginnt mit der natürlichsprachlichen Anfrage des Nutzers, speichert dann die ausgewählte Suchstrategie, die vorbereiteten Parameter für Elasticsearch, die abgerufenen Suchergebnisse und schließlich den formatierten Ausgang.

Diese Struktur ermöglicht es jedem Knoten, den Status zu lesen und zu aktualisieren, wodurch ein konsistenter Informationsfluss vom Nutzer-Eingang bis zur endgültigen Visualisierung gewährleistet wird.

const VCState = Annotation.Root({
  input: Annotation<string>(), // User's natural language query
  searchStrategy: Annotation<string>(), // Search strategy chosen by LLM
  searchParams: Annotation<any>(), // Prepared search parameters
  results: Annotation<any[]>(), // Search results
  final: Annotation<string>(), // Final formatted response
});

Einrichten der Anwendung

Der gesamte Code in diesem Abschnitt ist im elasticsearch-labs-Repository zu finden.

Öffnen Sie ein Terminal in dem Ordner, in dem sich die App befindet, und initialisieren Sie eine Node.js-Anwendung mit folgendem Befehl:

npm init -y

Nun können wir die notwendigen Abhängigkeiten für dieses Projekt installieren:

npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod && npm install --save-dev @types/node tsx typescript
  • @elastic/elasticsearch: Hilft uns bei der Bearbeitung von Elasticsearch-Anfragen, z. B. bei der Daten-Ingestion und dem Abrufen von Daten.

  • @langchain/langgraph: JS-Abhängigkeit zur Bereitstellung aller LangGraph-Tools.

  • @langchain/openai: LLM-Client von OpenAI für LangChain.

  • @langchain/core: Bietet die grundlegenden Bausteine für LangChain-Apps, einschließlich Prompt-Vorlagen.

  • dotenv: Notwendige Abhängigkeit zur Verwendung von Umgebungsvariablen in JavaScript.

  • zod: Abhängigkeit von Typdaten.

@types/node tsx typescript ermöglicht es uns, TypeScript-Code zu schreiben und auszuführen.

Erstellen Sie nun die folgenden Dateien:

  • elasticsearchSetup.ts: Erstellt die Index-Mappings, lädt die Daten aus einer JSON-Datei und führt den Ingest der Daten in Elasticsearch durch.

  • main.ts: Wird die LangGraph-Anwendung enthalten.

  • .envDatei zum Speichern der Umgebungsvariablen

In der .env-Datei fügen wir die folgenden Umgebungsvariablen hinzu:

ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"

Der OpenAPI-APIKey wird nicht direkt im Code verwendet; stattdessen wird er intern von der Bibliothek @langchain/openai verwendet.

Die gesamte Logik bezüglich der Erstellung von Mappings, der Erstellung von Suchvorlagen und der Datensatz-Ingestion kann in der Datei elasticsearchSetup.ts gefunden werden. In den nächsten Schritten konzentrieren wir uns auf die main.ts-Datei. Sie können den Datensatz auch überprüfen, um besser zu verstehen, wie die Daten in dataset.json aussehen.

LangGraph-App

In der main.ts-Datei importieren wir einige notwendige Abhängigkeiten, um die LangGraph-Anwendung zu konsolidieren. In dieser Datei müssen Sie auch die Knotenfunktionen und die Zustandsdeklaration angeben. Die Graphdeklaration erfolgt in den nächsten Schritten anhand einermain-Methode. Die elasticsearchSetup.ts-Datei wird Elasticsearch-Helfer enthalten, die wir in den nächsten Schritten in den Knoten verwenden werden.

import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });

Wie bereits erwähnt, wird der LLM-Client verwendet, um die Elasticsearch-Suchvorlagenparameter basierend auf der Frage des Nutzers zu generieren.

async function saveGraphImage(app: any): Promise<void> {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}

Die oben dargelegte Methode erzeugt das Graphbild im PNG-Format und verwendet hinter den Kulissen die Mermaid.INK-API. Dies ist nützlich, wenn Sie sehen möchten, wie die App-Knoten mit einer gestylten Visualisierung zusammenwirken.

LangGraph-Knoten

Sehen wir uns nun die einzelnen Knoten im Detail an:

decideSearchStrategy-Knoten

Der decideSearchStrategy-Knoten analysiert die Eingabe und entscheidet, ob eine investitions- oder marktorientierte Suche durchgeführt werden soll. Er verwendet ein LLM mit einem strukturierten Ausgang (definiert mit Zod), um den Abfragetyp zu klassifizieren. Bevor die Entscheidung getroffen wird, werden mithilfe einer Aggregation die verfügbaren Filter aus dem Index abgerufen, um sicherzustellen, dass das Modell über einen aktuellen Kontext zu Branchen, Standorten und Finanzierungsdaten verfügt.

Um die möglichen Filterwerte zu extrahieren und an das LLM zu senden, verwenden wir eine Aggregation, um sie direkt aus dem Elasticsearch-Index abzurufen. Diese Logik wird in einer Methode namens getAvailableFilterszugeordnet:

async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}

Mit der obigen Aggregationsanfrage erhalten wir die folgenden Ergebnisse:

{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}

Alle Ergebnisse finden Sie hier.

Für beide Strategien verwenden wir eine hybride Suche, um sowohl den strukturierten Teil der Frage (Filter) als auch die subjektiveren Teile (Semantik) zu erkennen. Hier ist ein Beispiel für beide Abfragen unter Verwendung von Suchvorlagen:

await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });

Sehen Sie sich die Abfragen an, die in derelasticsearchSetup.ts-Datei detailliert beschrieben sind. Im folgenden Knoten wird entschieden, welche der beiden Abfragen verwendet wird:

// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

  // Get dynamic filters from Elasticsearch
  const availableFilters = await getAvailableFilters();

  const prompt = `Query: "${state.input}"
    Available filters: ${JSON.stringify(availableFilters, null, 2)}

    Choose between two specialized search strategies:
    
    - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
    
    - market_focused: For queries about industries, locations, business models, market segments, geographic markets
    
    Analyze the query intent and choose the most appropriate strategy.
  `;

  try {
    const result = await decisionLLM.invoke(prompt);
    console.log(
      `🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
    );

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}

prepareInvestmentSearch- und prepareMarketSearch-Knoten

Beide Knoten verwenden eine gemeinsame Hilfsfunktion, extractFilterValues, die das LLM nutzt, um relevante Filter zu identifizieren, die in den Nutzereingaben erwähnt werden, wie z. B. Branche, Standort, Finanzierungsphase, Geschäftsmodell usw. Wir verwenden dieses Schema, um unsere Suchvorlage zu erstellen.

// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

  const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
  const availableFilters = await getAvailableFilters();

  const extractPrompt = `Extract ALL relevant filter values from: "${input}"
    Available options: ${JSON.stringify(availableFilters, null, 2)}
    Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;

  return await extractorLLM.invoke(extractPrompt);
}

Je nach erkannter Absicht wählt der Workflow einen von zwei Pfaden:

prepareInvestmentSearch: Erstellt finanzorientierte Suchparameter, einschließlich Finanzierungsphase, Finanzierungsbetrag, Investor und Erneuerungsinformationen. Die gesamte Abfragevorlage finden Sie in der elasticsearchSetup.ts-Datei:

// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: INVESTMENT_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}

prepareMarketSearch: erstellt marktorientierte Parameter, die sich auf Branchen, Geografien und Geschäftsmodelle konzentrieren. Die vollständige Abfrage finden Sie in der Datei elasticsearchSetup.ts:

// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: MARKET_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}

executeSearch-Knoten

Dieser Knoten nimmt die gesuchten Parameter aus dem Zustand und sendet sie zuerst an Elasticsearch, wobei er die _render-API verwendet, um die Abfrage für Debugging-Zwecke zu visualisieren, und sendet dann eine Anfrage zur Abrufung der Ergebnisse.

// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

  try {
    // getting formed query from template for debugging
    const renderedTemplate = await esClient.renderSearchTemplate({
      id: searchParams.template_id,
      params: searchParams,
    });

    console.log(
      "📋 Complete query:",
      JSON.stringify(renderedTemplate.template_output, null, 2)
    );

    const results = await esClient.searchTemplate({
      index: INDEX_NAME,
      id: searchParams.template_id,
      params: searchParams,
    });

    return {
      results: results.hits.hits.map((hit: any) => hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}

visualizeResults-Knoten

Dieser Knoten zeigt schließlich die Elasticsearch-Ergebnisse an.

// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

  let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;

  results.forEach((startup: any, index: number) => {
    formattedResults += `${index + 1}. **${startup.company_name}**\n`;
    formattedResults += `   📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
    formattedResults += `   💰 ${startup.funding_stage} - $${(
      startup.funding_amount / 1000000
    ).toFixed(1)}M\n`;
    formattedResults += `   👥 ${startup.employee_count} employees | 📈 $${(
      startup.monthly_revenue / 1000
    ).toFixed(0)}K MRR\n`;
    formattedResults += `   🏦 Lead: ${startup.lead_investor}\n`;
    formattedResults += `   📝 ${startup.description}\n\n`;
  });

  return {
    final: formattedResults,
  };
}

Programmatisch sieht der gesamte Graph so aus:

  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow

Wie Sie sehen können, haben wir eine bedingte Kante, bei der die App entscheidet, welcher „Pfad“ oder Knoten als Nächstes ausführen wird. Dieses Feature ist nützlich, wenn Workflows Verzweigungslogik benötigen, etwa die Wahl zwischen mehreren Tools oder das Einfügen eines Human-in-the-Loop-Schrittes.

Nachdem wir die Kern-Features von LangGraph verstanden haben, können wir die Anwendung einrichten, in der der Code ausgeführt werden soll:

Alles wird in einer main-Methode zusammengefasst. Hier deklarieren wir den Graphen mit allen Elementen unter der Variablen „Workflow“:

async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

  // Create the workflow graph with shared state
  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow


  const app = workflow.compile();

  await saveGraphImage(app);

  const query =
    "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}

Die Abfragevariable simuliert die in einer hypothetischen Suchleiste eingegebenen Nutzereingaben:

Aus der natürlichsprachlichen Formulierung „Suche Start-ups mit Series-A- oder Series-B-Finanzierung zwischen 8 und 25 Millionen US-Dollar und einem monatlichen Umsatz von über 500.000 US-Dollar“ werden alle Filter extrahiert.

Rufen Sie abschließend die Hauptmethode auf:

main().catch(console.error);

Ergebnisse

🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"

🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.

💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "funding_stage": [
                        "Series A",
                        "Series B"
                      ]
                    }
                  },
                  {
                    "range": {
                      "funding_amount": {
                        "gte": 8000000,
                        "lte": 25000000
                      }
                    }
                  },
                  {
                    "terms": {
                      "lead_investor": []
                    }
                  },
                  {
                    "range": {
                      "monthly_revenue": {
                        "gte": 500000,
                        "lte": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 100,
      "rank_constant": 20
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **TechFlow**
   📍 San Francisco, CA | 🏢 logistics | 💼 B2B
   💰 Series A - $8.0M
   👥 45 employees | 📈 $500K MRR
   🏦 Lead: Sequoia Capital
   📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.

2. **DataViz**
   📍 New York, NY | 🏢 enterprise software | 💼 B2B
   💰 Series A - $10.0M
   👥 42 employees | 📈 $450K MRR
   🏦 Lead: Battery Ventures
   📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.

3. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

4. **UrbanMobility**
   📍 New York, NY | 🏢 logistics | 💼 B2B2C
   💰 Series B - $15.0M
   👥 78 employees | 📈 $750K MRR
   🏦 Lead: Kleiner Perkins
   📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.

5. **HealthTech Solutions**
   📍 Boston, MA | 🏢 healthcare | 💼 B2B
   💰 Series B - $18.0M
   👥 95 employees | 📈 $900K MRR
   🏦 Lead: General Catalyst
   📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.

✨  Done in 18.80s.

Für die gesendete Eingabe wählt die Anwendung den investitionsorientierten Pfad, wodurch wir die Elasticsearch-Abfrage sehen, die vom Workflow generiert wird und die Werte und Bereiche aus dem Eingang des Nutzers extrahiert. Wir können auch die an Elasticsearch gesendete Anfrage mit den extrahierten Werten sehen, und schließlich die vom visualizeResults-Knoten formatierten Ergebnisse.

Testen wir nun den marktorientierten Knoten mit der Abfrage „Suche Fintech- und Healthcare-Start-ups in San Francisco, New York oder Boston.“:

...

🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston

🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.

🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "industry": [
                        "fintech",
                        "healthcare"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "location": [
                        "San Francisco, CA",
                        "New York, NY",
                        "Boston, MA"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "business_model": []
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 10
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

2. **CryptoWallet**
   📍 Miami, FL | 🏢 fintech | 💼 B2C
   💰 Series B - $16.0M
   👥 73 employees | 📈 $820K MRR
   🏦 Lead: Coinbase Ventures
   📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.

...

✨  Done in 7.41s.

Erkenntnisse

Während des Schreibprozesses habe ich Folgendes gelernt:

  • Wir müssen dem LLM die exakten Werte der Filter zeigen, sonst sind wir darauf angewiesen, dass der Nutzer die präzisen Werte eingibt. Bei niedriger Kardinalität ist dieser Ansatz akzeptabel, aber wenn die Kardinalität hoch ist, benötigen wir einen Mechanismus zum Herausfiltern der Ergebnisse.

  • Die Verwendung von Suchvorlagen führt zu deutlich konsistenteren Ergebnissen als die automatische Generierung der Elasticsearch-Abfrage durch das LLM und ist zudem schneller.

  • Bedingte Kanten sind ein leistungsstarker Mechanismus, um Anwendungen mit mehreren Varianten und verzweigten Pfaden zu erstellen.

  • Strukturierte Ausgaben sind bei der Informationsgenerierung mit LLMs äußerst nützlich, da sie vorhersehbare, typsichere Antworten erzwingen. Dies verbessert die Zuverlässigkeit und reduziert Fehlinterpretationen von Prompts.

Die Kombination von semantischem und strukturiertem Suchen durch hybriden Abruf führt zu besseren und relevanteren Ergebnissen, wobei Präzision und Kontextverständnis in Einklang gebracht werden.

Fazit

In diesem Beispiel kombinieren wir LangGraph.js mit Elasticsearch, um einen dynamischen Workflow zu schaffen, der natürliche Sprachanfragen interpretieren und zwischen finanz- oder marktorientierten Suchstrategien entscheiden kann. Dieser Ansatz reduziert die Komplexität manueller Abfragen und verbessert gleichzeitig die Flexibilität und Genauigkeit für Risikokapitalanalysten.

Zugehörige Inhalte

Fortgeschrittene RAG-Techniken Teil 2: Abfragen und Testen

Han Xiang Choong

Entitätsauflösung mit Elasticsearch, Teil 4: Die ultimative Herausforderung

Jessica Moszkowicz

Automatisierung des Log-Parsing in Streams mit ML

Nastia Havriushenko

Entwicklung eines KI-Agenten für die Personalabteilung mit Elastic Agent Builder und GPT-OSS

Tomás Murúa

Erweiterte RAG-Techniken Teil 1: Datenverarbeitung

Han Xiang Choong

Sind Sie bereit, hochmoderne Sucherlebnisse zu schaffen?

Eine ausreichend fortgeschrittene Suche kann nicht durch die Bemühungen einer einzelnen Person erreicht werden. Elasticsearch wird von Datenwissenschaftlern, ML-Ops-Experten, Ingenieuren und vielen anderen unterstützt, die genauso leidenschaftlich an der Suche interessiert sind wie Sie. Lasst uns in Kontakt treten und zusammenarbeiten, um das magische Sucherlebnis zu schaffen, das Ihnen die gewünschten Ergebnisse liefert.

Probieren Sie es selbst aus