基于 LangGraph.js + Elasticsearch 9.x 搭建金融 AI 搜索工作流
最终效果:你只用说一句大白话,系统自动帮你筛选符合条件的初创公司,不用写一行复杂的ES查询语句。
示例:输入「找A/B轮融资800-2500万美元、月营收超50万美元的上海SaaS公司」,直接返回整理好的企业清单。
一、先搞懂:这个项目到底是怎么跑的?
1.1 核心痛点解决
风投/金融从业者找项目时,要手动在数据库里组合几十个筛选条件,又慢又容易漏机会。现在要做的这个工具,就是把「自然语言→智能筛选→结果整理」全流程自动化。
1.2 全流程
整个系统就像一条自动化流水线,每个工位只干一件事,全程用一个「共享记事本」传递信息,不会丢数据:
用户说一句大白话 → 【工位1:判断意图】AI看你是查「财务/融资」还是「行业/地域」→ 【流水线分叉口】走对应分支
→ 【工位2:提取参数】AI从你的话里抠出所有筛选条件,填到提前写好的「填空题模板」里
→ 【工位3:执行查询】把填好的模板发给Elasticsearch(以下简称ES),从数据库里找符合条件的公司
→ 【工位4:整理结果】把查出来的数据排版成清晰可读的格式,返回给你
1.3 核心概念
| 专业术语 | 大白话类比 | 核心作用 |
|---|---|---|
| LangGraph 节点 | 流水线的单个工位 | 每个节点只干一件固定的事(比如判断意图、提取参数),干完就把结果写到共享记事本里 |
| LangGraph 状态(State) | 流水线的「共享记事本+传料盒」 | 全程只有这一个本子,所有节点都能读、能写,从用户输入到最终结果,所有信息都存在这里,不会丢 |
| 条件边 | 流水线的分叉口 | 根据「意图判断」的结果,自动选对应的加工路线,查财务就走财务分支,查行业就走行业分支 |
| ES 搜索模板 | 标准化的「填空题试卷」 | 我们提前把ES查询的固定框架写好,AI只需要往空里填关键词/数字,不用自己写查询语句,从根源上避免AI写的DSL语法错误 |
| 混合检索(RRF) | 找对象的双重标准 | 既要符合「硬性条件」(比如融资轮次、地域=结构化过滤),又要「聊得来」(比如业务描述匹配=语义向量搜索),两个结果融合,找出来的内容最准 |
| 结构化输出(Zod) | 给AI定的「答题规矩」 | 强制AI必须返回我们规定的格式,不能乱说话、不能答非所问,保证代码能正常读取AI的结果 |
二、前置准备:
每一步都有「操作命令+验证成功的方法」,跟着做不会卡壳
2.1 步骤1:安装Node.js(代码运行环境)
-
下载地址:Node.js官网,选18.x及以上的LTS版本(稳定版),一路默认安装即可。
-
验证是否安装成功:打开终端(Windows用CMD/PowerShell,Mac用终端),输入以下命令,能输出版本号就是成功:
node -v # 输出v18.0.0以上即可 npm -v # 对应输出版本号即可
2.2 步骤2:本地安装Elasticsearch 9.x
推荐用Docker安装,一键启动,不会踩环境坑;没有Docker的话,下面也给了二进制安装方法
方式一:Docker安装(如遇到问题可以参照前面博文)
-
先安装Docker:Docker官网下载,一路默认安装,启动Docker。
-
打开终端,输入以下命令,一键拉取ES 9.x镜像并启动:
# 拉取ES 9.x官方镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:9.0.0 # 启动单节点ES容器,配置端口、安全、内存 docker run -d \ --name es-local \ -p 9200:9200 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=true" \ -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ docker.elastic.co/elasticsearch/elasticsearch:9.0.0 -
验证是否启动成功:
# 等待10秒,输入这个命令,能返回一串JSON,就是启动成功了 curl http://localhost:9200 -
重置elastic用户密码(后面要用):
# 容器内执行密码重置,会自动生成一个随机密码,一定要记下来! docker exec -it es-local bin/elasticsearch-reset-password -u elastic -a示例输出:
Password for the [elastic] user successfully reset. New value: abc123xyz,把这个密码记好,后面一直要用。
方式二:二进制包安装(不用Docker)
-
下载地址:ES 9.x官网下载,选对应系统的版本。
-
解压后,打开
config/elasticsearch.yml,把下面的内容复制进去,覆盖默认配置:cluster.name: my-local-cluster node.name: node-1 network.host: 0.0.0.0 discovery.type: single-node xpack.security.enabled: true -
启动ES:
-
Windows:双击
bin/elasticsearch.bat -
Mac/Linux:终端进入解压目录,输入
./bin/elasticsearch
-
-
重置密码:新开一个终端,进入ES的bin目录,输入
./elasticsearch-reset-password -u elastic -a,记下生成的密码。
2.3 步骤3:获取2个必备的API Key
(1)生成ES的API Key(代码连接ES用)
新手推荐用Kibana可视化操作,不会写错命令;也可以用curl命令
-
先安装Kibana(和ES版本必须一致,都是9.0.0):
# Docker安装Kibana docker pull docker.elastic.co/kibana/kibana:9.0.0 docker run -d --name kibana-local -p 5601:5601 -e "ELASTICSEARCH_HOSTS=http://host.docker.internal:9200" docker.elastic.co/kibana/kibana:9.0.0 -
打开浏览器,访问
http://localhost:5601,用户名是elastic,密码是刚才重置的ES密码。 -
左侧菜单找到「Management → Dev Tools」,在控制台输入以下命令,点击运行:
POST /_security/api_key { "name": "financial-search-app", "role_descriptors": { "search-role": { "cluster": ["all"], "index": [ { "names": ["vc_startups*"], "privileges": ["read", "write", "manage"] } ] } } } -
运行后会返回结果,把
id和api_key拼起来,格式是id:api_key,记下来,后面要用。
示例:
id是abc123,api_key是xyz789,最终的Key就是abc123:xyz789
(2)获取OpenAI API Key(AI大模型用)
-
打开OpenAI平台,注册登录,在「API Keys」里生成一个新的Key,记下来。
-
国内用户替代方案:可以换成阿里云通义千问、智谱AI等国内大模型,后面会给替换代码。
三、项目搭建:从零开始,每一步都讲透
3.1 步骤1:初始化项目,安装依赖
-
新建一个项目文件夹,比如叫
es-financial-search,终端进入这个文件夹:mkdir es-financial-search && cd es-financial-search -
初始化Node.js项目,一路回车即可:
npm init -y -
安装所有依赖(每个依赖的作用都标清楚了,不是黑盒):
# 安装核心依赖 npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod openai # 安装TypeScript开发依赖(直接运行TS代码,不用编译) npm install --save-dev @types/node tsx typescript
| 依赖包 | 大白话作用 |
|---|---|
| @elastic/elasticsearch | ES官方客户端,代码和ES数据库之间的「翻译官」 |
| @langchain/langgraph | LangGraph核心库,用来搭我们的自动化流水线 |
| @langchain/openai | 让LangGraph能调用OpenAI的大模型 |
| dotenv | 把密钥存在.env文件里,不会硬写在代码里,安全 |
| zod | 给AI定「答题规矩」,强制AI返回固定格式,不会乱输出 |
| tsx | 直接运行TypeScript代码,不用先编译成JS,新手省事 |
- 配置TypeScript(新手直接复制,不会踩坑):
终端输入npx tsc --init生成配置文件,然后打开tsconfig.json,把里面的内容全替换成下面的:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["*.ts"],
"exclude": ["node_modules"]
}
3.2 步骤2:创建4个核心文件
在项目文件夹里,新建以下4个文件,后面会逐个写内容:
-
.env:存所有密钥和配置 -
dataset.json:初创公司的测试数据 -
elasticsearchSetup.ts:ES数据库的所有配置(建表、模板、导入数据) -
main.ts:LangGraph流水线的核心代码
3.3 文件1:.env(配置文件,新手直接复制)
把里面的内容替换成你自己的Key,不要加多余的引号、空格
# Elasticsearch 配置
ELASTICSEARCH_ENDPOINT="http://localhost:9200"
ELASTICSEARCH_API_KEY="你刚才生成的ES API Key(格式:id:api_key)"
# OpenAI 配置
OPENAI_API_KEY="你的OpenAI API Key"
# 索引和模板名称(不用改,直接用)
INDEX_NAME="vc_startups"
INVESTMENT_TEMPLATE="investment_focused_template"
MARKET_TEMPLATE="market_focused_template"
3.4 文件2:dataset.json(测试数据集,直接复制)
这里给10条测试数据,你后面可以自己加更多,格式完全一致即可
[
{
"company_name": "TechFlow",
"location": "San Francisco, CA",
"industry": "logistics",
"business_model": "B2B",
"funding_stage": "Series A",
"funding_amount": 8000000,
"monthly_revenue": 500000,
"employee_count": 45,
"lead_investor": "Sequoia Capital",
"description": "TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue."
},
{
"company_name": "FinanceAI",
"location": "San Francisco, CA",
"industry": "fintech",
"business_model": "B2C",
"funding_stage": "Series B",
"funding_amount": 25000000,
"monthly_revenue": 1200000,
"employee_count": 120,
"lead_investor": "Tiger Global Management",
"description": "FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users."
},
{
"company_name": "UrbanMobility",
"location": "New York, NY",
"industry": "logistics",
"business_model": "B2B2C",
"funding_stage": "Series B",
"funding_amount": 15000000,
"monthly_revenue": 750000,
"employee_count": 78,
"lead_investor": "Kleiner Perkins",
"description": "UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn."
},
{
"company_name": "HealthTech Solutions",
"location": "Boston, MA",
"industry": "healthcare",
"business_model": "B2B",
"funding_stage": "Series B",
"funding_amount": 18000000,
"monthly_revenue": 900000,
"employee_count": 95,
"lead_investor": "General Catalyst",
"description": "HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%."
},
{
"company_name": "DataViz",
"location": "New York, NY",
"industry": "enterprise software",
"business_model": "B2B",
"funding_stage": "Series A",
"funding_amount": 10000000,
"monthly_revenue": 450000,
"employee_count": 42,
"lead_investor": "Battery Ventures",
"description": "DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise."
},
{
"company_name": "CryptoWallet",
"location": "Miami, FL",
"industry": "fintech",
"business_model": "B2C",
"funding_stage": "Series B",
"funding_amount": 16000000,
"monthly_revenue": 820000,
"employee_count": 73,
"lead_investor": "Coinbase Ventures",
"description": "CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features."
},
{
"company_name": "GreenEnergy",
"location": "Austin, TX",
"industry": "clean energy",
"business_model": "B2B",
"funding_stage": "Series A",
"funding_amount": 12000000,
"monthly_revenue": 600000,
"employee_count": 55,
"lead_investor": "Breakthrough Energy Ventures",
"description": "GreenEnergy develops commercial solar energy solutions for small and medium businesses. Helps companies reduce energy costs by 40% on average."
},
{
"company_name": "EdTech Labs",
"location": "Boston, MA",
"industry": "education",
"business_model": "B2C",
"funding_stage": "Series A",
"funding_amount": 7500000,
"monthly_revenue": 350000,
"employee_count": 38,
"lead_investor": "Owl Ventures",
"description": "EdTech Labs creates AI-powered learning platforms for K12 students. Personalized curriculum that adapts to each student's learning pace and style."
},
{
"company_name": "SaaSFlow",
"location": "Shanghai, China",
"industry": "SaaS",
"business_model": "B2B",
"funding_stage": "Series A",
"funding_amount": 10000000,
"monthly_revenue": 550000,
"employee_count": 60,
"lead_investor": "Sequoia Capital China",
"description": "SaaSFlow provides one-stop enterprise management SaaS for small and medium businesses. Covers CRM, HR, finance and inventory management in a single platform."
},
{
"company_name": "FoodTech",
"location": "Los Angeles, CA",
"industry": "food & beverage",
"business_model": "B2B",
"funding_stage": "Series B",
"funding_amount": 22000000,
"monthly_revenue": 880000,
"employee_count": 85,
"lead_investor": "a16z",
"description": "FoodTech develops plant-based food ingredients for restaurants and food manufacturers. Helps brands create healthier, more sustainable menu items."
}
]
3.5 文件3:elasticsearchSetup.ts(ES核心配置)
这个文件干3件事:① 给ES建表(含向量字段);② 创建「填空题模板」;③ 把测试数据导入ES(含向量生成)
import { Client } from "@elastic/elasticsearch";
import * as fs from "fs";
import * as path from "path";
import OpenAI from "openai";
import dotenv from "dotenv";
// 加载.env里的配置
dotenv.config();
// ========== 1. 初始化ES客户端(和ES数据库建立连接) ==========
export const esClient = new Client({
node: process.env.ELASTICSEARCH_ENDPOINT, // ES的地址
auth: { apiKey: process.env.ELASTICSEARCH_API_KEY! }, // 刚才生成的API Key
tls: { rejectUnauthorized: false } // 本地开发关闭证书校验,生产环境要打开
});
// ========== 2. 初始化OpenAI客户端(生成向量用) ==========
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ========== 3. 常量定义(从.env里读取,不用改) ==========
export const INDEX_NAME = process.env.INDEX_NAME!;
export const INVESTMENT_TEMPLATE = process.env.INVESTMENT_TEMPLATE!;
export const MARKET_TEMPLATE = process.env.MARKET_TEMPLATE!;
// ========== 功能1:创建ES索引(相当于数据库里的「表」) ==========
export async function createIndex() {
// 先检查表是否存在,如果存在就删掉重建(开发环境方便测试)
const indexExists = await esClient.indices.exists({ index: INDEX_NAME });
if (indexExists) {
console.log(`🗑️ 索引${INDEX_NAME}已存在,删除重建...`);
await esClient.indices.delete({ index: INDEX_NAME });
}
// 定义表结构(每个字段是什么类型,干什么用的)
const indexMapping = {
properties: {
company_name: { type: "text" }, // 公司名称,文本类型
location: { type: "keyword" }, // 地域,keyword类型(用于精确匹配筛选)
industry: { type: "keyword" }, // 行业,keyword类型
business_model: { type: "keyword" }, // 商业模式,keyword类型
funding_stage: { type: "keyword" }, // 融资轮次,keyword类型
funding_amount: { type: "long" }, // 融资金额,数字类型(单位:美元)
monthly_revenue: { type: "long" }, // 月营收,数字类型(单位:美元)
employee_count: { type: "integer" }, // 员工数,整数类型
lead_investor: { type: "keyword" }, // 领投方,keyword类型
description: { type: "text" }, // 公司描述,文本类型(用于语义搜索)
// 重点:向量字段,用于语义搜索,匹配公司描述的含义
semantic_field: {
type: "dense_vector", // 向量类型
dims: 1536, // 向量维度,和OpenAI embedding模型的维度一致
index: true, // 开启索引,用于快速搜索
similarity: "cosine" // 相似度计算方式,用余弦相似度(最常用)
}
}
};
// 执行创建索引
await esClient.indices.create({
index: INDEX_NAME,
body: { mappings: indexMapping }
});
console.log(`✅ 索引${INDEX_NAME}创建成功!`);
}
// ========== 功能2:创建搜索模板(核心!填空题模板) ==========
// 原理:我们提前写好ES查询的固定框架,AI只需要往空里填值,不会写语法错误的查询
export async function createSearchTemplates() {
// 模板1:投资型查询模板(聚焦融资、营收、财务指标)
const investmentTemplate = {
script: {
lang: "mustache", // ES模板的固定语法,类似填空题
source: `{
"size": 5, // 最多返回5条结果
"retriever": {
"rrf": { // RRF混合检索:同时用「语义搜索」和「结构化过滤」,结果融合
"retrievers": [
// 第一个检索器:语义搜索(匹配公司描述的含义)
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field", // 用哪个向量字段
"query": "{{query_text}}" // 填空:用户的原始查询
}
}
}
},
// 第二个检索器:结构化过滤(精确匹配财务条件)
{
"standard": {
"query": {
"bool": {
"filter": [
// 只有用户提到了融资轮次,才会生成这个过滤条件
{{#funding_stage}}
{"terms": {"funding_stage": {{#toJson}}funding_stage{{/toJson}}}},
{{/funding_stage}}
// 只有用户提到了融资金额下限,才会生成这个过滤条件
{{#funding_amount_gte}}
{"range": {"funding_amount": {
"gte": {{funding_amount_gte}}
{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}
}}},
{{/funding_amount_gte}}
// 只有用户提到了月营收下限,才会生成这个过滤条件
{{#monthly_revenue_gte}}
{"range": {"monthly_revenue": {
"gte": {{monthly_revenue_gte}}
{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}
}}},
{{/monthly_revenue_gte}}
// 只有用户提到了领投方,才会生成这个过滤条件
{{#lead_investor}}
{"terms": {"lead_investor": {{#toJson}}lead_investor{{/toJson}}}}
{{/lead_investor}}
]
}
}
}
}
],
"rank_window_size": 100, // 参与融合的结果数量
"rank_constant": 20 // RRF调优参数,固定20即可
}
}
}`
}
};
// 模板2:市场型查询模板(聚焦行业、地域、商业模式)
const marketTemplate = {
script: {
lang: "mustache",
source: `{
"size": 5,
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field",
"query": "{{query_text}}"
}
}
}
},
{
"standard": {
"query": {
"bool": {
"filter": [
{{#industry}}
{"terms": {"industry": {{#toJson}}industry{{/toJson}}}},
{{/industry}}
{{#location}}
{"terms": {"location": {{#toJson}}location{{/toJson}}}},
{{/location}}
{{#business_model}}
{"terms": {"business_model": {{#toJson}}business_model{{/toJson}}}}
{{/business_model}}
]
}
}
}
}
],
"rank_window_size": 50,
"rank_constant": 10
}
}
}`
}
};
// 把两个模板注册到ES里
await esClient.putScript({ id: INVESTMENT_TEMPLATE, body: investmentTemplate });
await esClient.putScript({ id: MARKET_TEMPLATE, body: marketTemplate });
console.log("✅ 投资型/市场型搜索模板创建成功!");
}
// ========== 功能3:导入测试数据到ES(含向量生成) ==========
export async function ingestDocuments() {
// 读取dataset.json里的测试数据
const datasetPath = path.join(__dirname, "dataset.json");
const rawData = fs.readFileSync(datasetPath, "utf-8");
const documents = JSON.parse(rawData);
// 批量处理数据:给每条数据生成向量,然后导入ES
const bulkOperations: any[] = [];
for (const doc of documents) {
// 调用OpenAI的embedding接口,给公司描述生成向量
const embeddingResult = await openai.embeddings.create({
model: "text-embedding-3-small",
input: doc.description // 用公司描述生成向量,用于语义搜索
});
// 把生成的向量加到数据里
doc.semantic_field = embeddingResult.data[0].embedding;
// 构建批量导入的操作(ES的bulk语法固定格式)
bulkOperations.push({ index: { _index: INDEX_NAME } });
bulkOperations.push(doc);
}
// 执行批量导入
await esClient.bulk({ body: bulkOperations, refresh: true });
console.log(`✅ 成功导入${documents.length}条初创公司数据!`);
}
3.6 文件4:main.ts(LangGraph流水线核心代码)
这个文件就是搭我们的自动化流水线,定义每个工位(节点)干什么,流水线怎么流转,逐行注释无黑盒
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import { writeFileSync } from "fs";
import dotenv from "dotenv";
// 导入ES里的配置和方法
import {
esClient,
createIndex,
createSearchTemplates,
ingestDocuments,
INDEX_NAME,
INVESTMENT_TEMPLATE,
MARKET_TEMPLATE
} from "./elasticsearchSetup.js";
// 加载.env配置
dotenv.config();
// ========== 1. 初始化大模型 ==========
const llm = new ChatOpenAI({
model: "gpt-4o-mini", // 用这个模型性价比最高,速度快、效果好
temperature: 0 // 温度设为0,让AI输出更稳定,不会乱发挥
});
// ========== 2. 定义「共享记事本」(LangGraph State) ==========
// 整个流水线所有节点都能读、能写这个记事本,全程信息不丢失
const VCState = Annotation.Root({
input: Annotation<string>(), // 【初始值】用户输入的自然语言查询
searchStrategy: Annotation<string>(), // 节点1输出:AI判断的搜索策略(investment_focused/market_focused)
searchParams: Annotation<any>(), // 节点2输出:填到模板里的筛选参数
results: Annotation<any[]>(), // 节点3输出:ES查询返回的结果
final: Annotation<string>() // 节点4输出:最终整理好的可读结果
});
// ========== 3. 通用辅助函数 ==========
// 辅助1:保存流水线的可视化图(png格式,能看到整个流程的流转)
async function saveWorkflowGraph(app: any) {
try {
const drawableGraph = app.getGraph();
const imageBuffer = await drawableGraph.drawMermaidPng();
writeFileSync("./workflow.png", Buffer.from(await imageBuffer.arrayBuffer()));
console.log("📊 流水线流程图已保存为 workflow.png");
} catch (error) {
console.log("⚠️ 无法保存流程图:", error);
}
}
// 辅助2:从ES里动态获取所有可用的筛选值(比如有哪些行业、哪些地域)
// 作用:把这些值给AI,AI就不会瞎编不存在的行业/地域,减少幻觉
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_stats: { stats: { field: "funding_amount" } } // 融资金额的范围(最大/最小)
}
});
return response.aggregations;
} catch (error) {
console.error("❌ 获取筛选值失败:", error);
return {};
}
}
// ========== 4. 定义流水线的每个工位(节点) ==========
// ------------------------------
// 节点1:判断搜索策略(意图识别)
// 作用:看用户的查询是查「财务/融资」还是「行业/地域」,决定走哪个分支
// ------------------------------
async function decideSearchStrategy(state: typeof VCState.State) {
// 用Zod给AI定规矩:必须返回这个格式的结果,不能乱说话
const SearchDecisionSchema = z.object({
search_type: z.enum(["investment_focused", "market_focused"]), // 二选一
reasoning: z.string() // 解释为什么选这个策略,方便我们调试
});
// 让AI必须按上面的Schema返回结果
const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);
// 获取ES里所有可用的筛选值,给AI参考
const availableFilters = await getAvailableFilters();
// 给AI的提示词,说清楚规则
const prompt = `
用户的查询是:"${state.input}"
当前数据库里可用的筛选值:${JSON.stringify(availableFilters, null, 2)}
请从两个搜索策略里选一个最合适的:
1. investment_focused:用户的查询涉及融资轮次、融资金额、月营收、领投方等财务/投资相关的指标
2. market_focused:用户的查询涉及行业、地域、商业模式、市场细分等市场相关的维度
只返回规定的JSON格式,不要加其他内容。
`;
try {
const result = await decisionLLM.invoke(prompt);
console.log(`🤔 策略判断结果:${result.search_type} - ${result.reasoning}`);
// 把结果写到共享记事本里
return { searchStrategy: result.search_type };
} catch (error) {
console.error("❌ 策略判断失败,默认使用投资型策略:", error);
return { searchStrategy: "investment_focused" };
}
}
// ------------------------------
// 通用函数:从用户查询里提取筛选参数
// 作用:AI从用户的话里抠出所有筛选条件,填到模板里
// ------------------------------
async function extractFilterParams(input: string) {
// 用Zod定规矩:AI必须返回这个格式的参数
const FilterSchema = z.object({
// 投资型参数
funding_stage: z.array(z.string()).default([]), // 融资轮次
funding_amount_gte: z.number().optional(), // 融资金额下限
funding_amount_lte: z.number().optional(), // 融资金额上限
monthly_revenue_gte: z.number().optional(), // 月营收下限
monthly_revenue_lte: z.number().optional(), // 月营收上限
lead_investor: z.array(z.string()).default([]), // 领投方
// 市场型参数
industry: z.array(z.string()).default([]), // 行业
location: z.array(z.string()).default([]), // 地域
business_model: z.array(z.string()).default([]) // 商业模式
});
const extractorLLM = llm.withStructuredOutput(FilterSchema);
const availableFilters = await getAvailableFilters();
const prompt = `
从用户的查询里提取所有筛选参数:"${input}"
数据库里可用的选项:${JSON.stringify(availableFilters, null, 2)}
规则:
1. 只提取用户明确提到的参数,没提到的不要填,留空
2. 必须从上面的可用选项里提取,不要瞎编不存在的值
3. 金额单位统一为美元,比如用户说800万美元,就填8000000
只返回规定的JSON格式,不要加其他内容。
`;
return await extractorLLM.invoke(prompt);
}
// ------------------------------
// 节点2A:准备投资型搜索的参数
// 作用:用户查财务相关的,就走这个节点,提取参数,填到投资型模板里
// ------------------------------
async function prepareInvestmentSearch(state: typeof VCState.State) {
console.log("💰 正在准备投资型搜索参数...");
try {
// 提取用户查询里的参数
const filterParams = await extractFilterParams(state.input);
// 组装模板需要的参数
const searchParams = {
template_id: INVESTMENT_TEMPLATE, // 用哪个模板
query_text: state.input, // 用户的原始查询(用于语义搜索)
...filterParams // 提取的筛选参数
};
// 写到共享记事本里
return { searchParams };
} catch (error) {
console.error("❌ 投资型参数准备失败:", error);
return { searchParams: {} };
}
}
// ------------------------------
// 节点2B:准备市场型搜索的参数
// 作用:用户查行业/地域相关的,就走这个节点,提取参数,填到市场型模板里
// ------------------------------
async function prepareMarketSearch(state: typeof VCState.State) {
console.log("🔍 正在准备市场型搜索参数...");
try {
const filterParams = await extractFilterParams(state.input);
const searchParams = {
template_id: MARKET_TEMPLATE,
query_text: state.input,
...filterParams
};
return { searchParams };
} catch (error) {
console.error("❌ 市场型参数准备失败:", error);
return { searchParams: {} };
}
}
// ------------------------------
// 节点3:执行ES搜索
// 作用:把填好的模板发给ES,执行查询,拿到结果
// ------------------------------
async function executeSearch(state: typeof VCState.State) {
const { searchParams } = state;
try {
// 【调试用】先渲染模板,看看最终生成的ES查询语句是什么样的
const renderedTemplate = await esClient.renderSearchTemplate({
id: searchParams.template_id,
params: searchParams
});
console.log("📋 最终生成的ES查询语句:\n", JSON.stringify(renderedTemplate.template_output, null, 2));
// 执行搜索,拿到结果
const searchResult = await esClient.searchTemplate({
index: INDEX_NAME,
id: searchParams.template_id,
params: searchParams
});
// 把结果里的公司数据提取出来,写到共享记事本里
return { results: searchResult.hits.hits.map((hit: any) => hit._source) };
} catch (error) {
console.error("❌ ES搜索失败:", error);
return { results: [] };
}
}
// ------------------------------
// 节点4:整理结果,生成可读的输出
// 作用:把ES返回的原始数据,排版成清晰好看的格式
// ------------------------------
async function visualizeResults(state: typeof VCState.State) {
const results = state.results || [];
let formattedOutput = `🎯 找到${results.length}家符合条件的初创公司:\n\n`;
// 遍历结果,逐条排版
results.forEach((startup, index) => {
formattedOutput += `${index + 1}. **${startup.company_name}**\n`;
formattedOutput += ` 📍 地域:${startup.location} | 🏢 行业:${startup.industry} | 💼 商业模式:${startup.business_model}\n`;
formattedOutput += ` 💰 融资轮次:${startup.funding_stage} | 融资金额:$${(startup.funding_amount / 1000000).toFixed(1)}M\n`;
formattedOutput += ` 👥 员工数:${startup.employee_count}人 | 📈 月营收:$${(startup.monthly_revenue / 1000).toFixed(0)}K\n`;
formattedOutput += ` 🏦 领投方:${startup.lead_investor}\n`;
formattedOutput += ` 📝 公司描述:${startup.description}\n\n`;
});
// 把最终结果写到共享记事本里
return { final: formattedOutput };
}
// ========== 5. 组装流水线,定义流转规则 ==========
async function main() {
// 第一步:初始化ES环境(建表、建模板、导入数据)
console.log("===== 开始初始化ES环境 =====");
await createIndex();
await createSearchTemplates();
await ingestDocuments();
console.log("===== ES环境初始化完成 =====\n");
// 第二步:搭建LangGraph流水线
const workflow = new StateGraph(VCState)
// 1. 注册所有工位(节点)
.addNode("decideStrategy", decideSearchStrategy)
.addNode("prepareInvestment", prepareInvestmentSearch)
.addNode("prepareMarket", prepareMarketSearch)
.addNode("executeSearch", executeSearch)
.addNode("visualizeResults", visualizeResults)
// 2. 定义流水线的流转顺序
.addEdge(START, "decideStrategy") // 起点:先执行意图判断
// 3. 定义分叉口:根据意图判断的结果,走对应的分支
.addConditionalEdges(
"decideStrategy", // 上一个节点:意图判断
(state) => state.searchStrategy, // 用共享记事本里的策略判断结果
{
investment_focused: "prepareInvestment", // 投资型,走这个分支
market_focused: "prepareMarket" // 市场型,走这个分支
}
)
// 4. 两个分支都要走到「执行搜索」节点
.addEdge("prepareInvestment", "executeSearch")
.addEdge("prepareMarket", "executeSearch")
// 5. 搜索完了整理结果,然后结束
.addEdge("executeSearch", "visualizeResults")
.addEdge("visualizeResults", END);
// 第三步:编译流水线,生成可执行的app
const app = workflow.compile();
// 保存流水线的流程图
await saveWorkflowGraph(app);
// 第四步:测试查询1:投资型查询
console.log("===== 测试投资型查询 =====");
const investmentQuery = "找A轮或B轮融资800-2500万美元,月营收超过50万美元的初创公司";
const investmentResult = await app.invoke({ input: investmentQuery });
console.log(investmentResult.final);
// 第五步:测试查询2:市场型查询
console.log("===== 测试市场型查询 =====");
const marketQuery = "找旧金山、纽约、波士顿的金融科技和医疗健康行业的初创公司";
const marketResult = await app.invoke({ input: marketQuery });
console.log(marketResult.final);
}
// 启动整个程序
main().catch(console.error);
四、运行测试:一键启动,看效果
-
打开终端,进入项目文件夹,输入以下命令,直接运行:
npx tsx main.ts -
预期输出(全程有日志,不会黑盒):
-
先初始化ES环境,建表、建模板、导入数据
-
保存流水线流程图到
workflow.png,打开就能看到整个流程的流转 -
执行投资型测试查询,输出符合条件的公司列表
-
执行市场型测试查询,输出符合条件的公司列表
-
五、新手必看:常见踩坑指南(会遇到的问题)
1. ES连接失败
-
报错:
connect ECONNREFUSED 127.0.0.1:9200 -
解决办法:
-
检查ES是否启动成功:
curl http://localhost:9200能不能返回JSON -
检查
.env里的ELASTICSEARCH_ENDPOINT地址是否正确 -
检查API Key是否正确,格式是不是
id:api_key
-
2. 搜索不到结果
-
解决办法:
-
看终端输出的「最终生成的ES查询语句」,检查过滤条件是不是写反了(比如月营收下限50万,上限0,就会查不到)
-
检查金额单位是不是正确,代码里的单位是美元,用户输入的时候要对应
-
检查数据集里有没有符合条件的数据
-
3. LLM返回格式错误
-
报错:
Zod validation error -
解决办法:
-
把
temperature设为0,降低AI的随机性 -
检查提示词里有没有明确要求返回JSON格式
-
换用更稳定的模型(比如gpt-4o)
-
4. 向量维度不匹配
-
报错:
vector dimension mismatch -
解决办法:
-
检查ES映射里的
dims和embedding模型的维度是否一致(text-embedding-3-small是1536维) -
重建索引,重新导入数据
-
5. TypeScript运行报错
-
报错:
Cannot find module -
解决办法:
-
检查
tsconfig.json里的module和moduleResolution是不是设为NodeNext -
检查import的文件后缀是不是加了
.js(比如./elasticsearchSetup.js)
-
六、国内用户优化:替换为国内大模型
如果OpenAI访问不了,可以把代码里的OpenAI换成阿里云通义千问、智谱AI等国内大模型,示例如下:
替换为通义千问
-
安装依赖:
npm install @langchain/community -
替换
main.ts里的LLM初始化代码:import { ChatTongyi } from "@langchain/community/chat_models/tongyi"; const llm = new ChatTongyi({ model: "qwen-turbo", temperature: 0, apiKey: "你的阿里云通义千问API Key" }); -
向量生成也换成通义千问的embedding模型即可。
七、进阶扩展:自己动手优化
-
加新的搜索分支:比如「技术栈聚焦」「团队背景」等分支
-
增加用户反馈节点:用户可以对结果打分,AI自动优化查询
-
导出结果:把查询结果导出成Excel/CSV文件
-
加权限控制:给不同的用户分配不同的查询权限
-
接入实时数据:对接Crunchbase、IT桔子等平台,实时更新初创公司数据
更多推荐



所有评论(0)