使用 LangChain、Neo4j 和 TypeScript 构建全栈生成式 AI 应用
在本教程中,我们将构建一个极简的全栈 TypeScript 应用程序,在同一个 Node.js 服务器上同时提供前端页面和基于 LangChain 的生成式 AI(GenAI)后端。该应用将通过对 Neo4j 图数据库中的书籍和作者数据执行语义搜索,并利用 OpenAI 的 Chat API 生成简洁的推荐语,从而根据用户查询(例如他们最喜欢的书籍或流派)向用户推荐书籍。我们将使用 Neo4j 的新向量索引功能进行语义相似度搜索,并利用 LangChain 与 OpenAI 和 Neo4j 的集成将各个环节串联起来。

我们将实现的功能
- 后端 – 一个带有单个 API 路由(
/recommend)的 Node/TS 服务器,该路由接收查询字符串并返回书籍推荐。后端将:- 对用户查询进行向量化,并在 Neo4j 中执行向量相似度搜索以查找相关的书籍/作者数据(通过 LangChain 的 Neo4j 向量存储)。
- 将结果输入 OpenAI 的 Chat 模型,生成一段简短的推荐信息。
- 前端 – 一个静态 HTML+JS 页面(由同一个 Node 服务器提供),包含一个文本输入框和一个按钮。用户输入查询内容(例如:“我喜欢《霍比特人》,接下来我应该读什么?”),应用程序调用我们的
/recommendAPI 并显示响应。我们将使用 Tailwind CSS(通过 CDN)进行快速样式设计——无需任何框架或构建步骤。
我们假设你已经拥有一个加载了 Goodreads 图书数据集(1 万本书、5 千位作者等)的 Neo4j 实例,并且 Neo4j 的向量索引功能可用。在本教程中,我们将连接到 Neo4j 的 Aura 演示数据库(neo4j+s://demo.neo4jlabs.com,用户名/密码“goodreads”,数据库“goodreads”)。你还需要准备好自己的 OpenAI API 密钥。
1. 项目设置
首先,创建一个新的 Node.js 项目并安装所需的包。我们将使用 LangChain for JS/TS(社区版和 OpenAI 模块)、Neo4j 驱动程序以及用于配置的 dotenv。
npm init -y
npm install langchain @langchain/core @langchain/openai @langchain/community neo4j-driver dotenvCode language: Shell Session (shell)
这将安装 LangChain 的核心库以及我们所需的集成模块。接下来,设置 package.json 以配置构建和运行项目的脚本,并列出我们的依赖项。
{
"name": "genai-book-recommender",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"langchain": "^0.3.30",
"@langchain/core": "^0.6.2",
"@langchain/openai": "^0.6.2",
"@langchain/community": "^0.6.2",
"neo4j-driver": "^5.8.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"typescript": "^5.1.3"
}
}Code language: JSON / JSON with Comments (json)
我们将模块类型设置为 "module",以便在 Node 中使用 ES 模块导入语法(或者,如果更喜欢,可以使用 CommonJS 的 require 并调整配置)。我们还包含了一个基础的 tsconfig.json 来编译我们的 TypeScript 代码。
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"rootDir": "src",
"outDir": "dist",
"esModuleInterop": true,
"strict": true
}
}Code language: JSON / JSON with Comments (json)
通过此配置,我们的 TS 源码将位于 src/ 目录并编译到 dist/ 目录。现在,在项目根目录下创建一个 .env 文件(并参考创建一个 .env.example)来存储敏感凭据。
# .env (fill in your actual keys and endpoints)
OPENAI_API_KEY=<your OpenAI API key>
NEO4J_URI=neo4j+s://demo.neo4jlabs.com
NEO4J_USERNAME=goodreads
NEO4J_PASSWORD=goodreads
NEO4J_DATABASE=goodreadsCode language: PHP (php)
这将允许我们的代码连接到 Neo4j Aura Goodreads 演示数据库并对 OpenAI 进行身份验证。现在,让我们构建后端服务器。
2. 后端:基于 LangChain 的推荐 API
创建 src/index.ts(或 server.ts)作为我们的 Node 服务器。为了保持简洁,我们将使用 Node 内置的 HTTP 模块(不使用 Express)。计划如下:
- 加载环境变量并初始化连接(Neo4j 和 OpenAI)。
- 准备一个用于 Neo4j 的 LangChain 向量存储,以便我们可以在图数据上进行相似度搜索。
- 创建一个 HTTP 服务器,用于提供前端文件并处理
/recommendAPI 路由。
让我们从 index.ts 开头的导入和初始化开始。
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import 'dotenv/config';
// LangChain + Neo4j imports
import { OpenAIEmbeddings, ChatOpenAI } from '@langchain/openai';
import { Neo4jVectorStore } from '@langchain/community/vectorstores/neo4j_vector';
import neo4j from 'neo4j-driver';
// Load config from .env
const PORT = process.env.PORT || 3000;
const neo4jUrl = process.env.NEO4J_URI!;
const neo4jUser = process.env.NEO4J_USERNAME!;
const neo4jPass = process.env.NEO4J_PASSWORD!;
const neo4jDb = process.env.NEO4J_DATABASE || 'neo4j';
const openAiKey = process.env.OPENAI_API_KEY!;
// Initialize Neo4j driver (for any direct Cypher queries)
const driver = neo4j.driver(neo4jUrl, neo4j.auth.basic(neo4jUser, neo4jPass));
// Initialize LangChain components
const embeddings = new OpenAIEmbeddings(); //to embed queries
const chatModel = new ChatOpenAI({ openAIApiKey: openAiKey, temperature: 0.7 });Code language: TypeScript (typescript)
在这里,我们配置 Neo4j 驱动程序和 OpenAI。我们使用了 LangChain 中的 ChatOpenAI,并设置了一个中等的 temperature 值,以在响应中增加一点创造性。
接下来,我们设置 Neo4j 向量存储。这将使我们能够使用向量相似度搜索我们的 Neo4j 图。我们假设 Neo4j 图中已经包含了文本(如书评或描述)的嵌入向量以及相应的向量索引。LangChain 的 Neo4jVectorStore 可以与 Neo4j 的向量索引进行交互(如果需要,还可以创建一个)以启用相似度搜索。
对于 Goodreads 演示,数据包括“书”(Book)和“作者”(Author)节点,以及一组带有文本内容的“书评”(Review)节点。我们将对 Review 节点的文本执行向量搜索,以找到与用户查询相关的书籍(因为像“我喜欢《霍比特人》”这样的用户查询通过评论或描述能更好地匹配)。每个 Review 节点都连接到一本书(该书拥有一位或多位作者)。以下是该图(用户、书评、书籍、作者)的简化架构,以供参考。
Goodreads 数据集的图数据模型:用户为书籍撰写评论,作者撰写书籍(一本书可以有多条评论和多位作者)。
我们可以按如下方式初始化向量存储:
// Initialize vector index interface for Neo4j (will use existing embeddings or add if missing)
const vectorStore = await Neo4jVectorStore.fromExistingGraph(
embeddings,
{
url: neo4jUrl,
username: neo4jUser,
password: neo4jPass,
database: neo4jDb,
indexName: 'review-embedding-index', // name of the Neo4j vector index (assumed to exist)
nodeLabel: 'Review', // label of nodes to search
textNodeProperty: 'text', // property name where review text is stored
embeddingNodeProperty: 'embedding', // property name of the stored vector embeddings
searchType: 'vector' // pure vector search (cosine similarity by default)
}
);
Code language: JavaScript (javascript)
我们传递连接配置,并指定我们想要使用 Review 节点的 text 和 embedding 属性。我们还指定了索引名称(在此演示中,正如数据集中预配置的那样,它是 "review-embedding-index")。调用 fromExistingGraph 将连接到 Neo4j,并确保任何没有嵌入的 Review 节点都能计算并存储嵌入。如果指定名称的 Neo4j 向量索引不存在,它将在后台创建一个。一旦该 Promise 解析,我们的 vectorStore 就可以对图执行相似度搜索了。
现在让我们实现带有 /recommend API 路由的 HTTP 服务器。我们还需要提供静态前端文件(index.html)。为了简单起见,我们可以使用 Node 的 http 和 fs 模块。
// Create HTTP server
const server = http.createServer(async (req, res) => {
try {
const url = req.url ? new URL(req.url, `https://:${PORT}`) : null;
if (!url) {
res.writeHead(400).end('Bad Request');
return;
}
// Serve the frontend HTML (and any static files)
if (url.pathname === '/' || url.pathname === '/index.html') {
const filePath = path.join(__dirname, '../public/index.html');
const data = fs.readFileSync(filePath);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
return;
}
// API endpoint: /recommend?query=<user query>
if (url.pathname === '/recommend') {
const queryParam = url.searchParams.get('query') || '';
const userQuery = queryParam.trim();
if (!userQuery) {
res.writeHead(400).end(JSON.stringify({ error: 'Missing query' }));
return;
}
// 1. Vector search in Neo4j for similar content
const results = await vectorStore.similaritySearch(userQuery, 3);
// results are Documents, likely containing review text in pageContent and metadata including the Neo4j node ID
if (results.length === 0) {
res.writeHead(200).end(JSON.stringify({ recommendation: "Sorry, I couldn't find any related books." }));
return;
}
// 2. Take the top result and find its Book + Author in the graph via a Cypher query
const topReviewDoc = results[0];
const reviewId = topReviewDoc.metadata.id; // assuming the Review node has an 'id' property
const session = driver.session({ database: neo4jDb });
const cypher = `
MATCH (r:Review {id: $rid})-[:WRITTEN_FOR]->(b:Book)<-[:AUTHORED]-(a:Author)
RETURN b.title AS title, a.name AS author LIMIT 1
`;
const record = await session.executeRead(tx => tx.run(cypher, { rid: reviewId }))
.then(res => res.records[0]);
await session.close();
let title = record.get('title'), author = record.get('author');
if (!title || !author) {
// Fallback: try next result if any
if (results[1]) {
// (similar logic to get title/author for next doc)
}
}
// 3. Use OpenAI Chat model to format a concise recommendation message
const messages = [
{ role: 'system', content: 'You are a helpful book recommendation assistant.' },
{ role: 'user', content:
`A user said: "${userQuery}".\n` +
`You found a book suggestion: "${title}" by ${author}.\n` +
'Write a single-paragraph recommendation for this book, mentioning the title and author and why the user might like it. Be concise and upbeat.'
}
];
const response = await chatModel.call(messages);
const answer = response.text; // the generated recommendation text
// 4. Return the answer as JSON
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ recommendation: answer }));
return;
}
// If no route matched, return 404 for API or static requests
res.writeHead(404).end('Not Found');
} catch (err) {
console.error('Error handling request', err);
res.writeHead(500).end(JSON.stringify({ error: 'Internal Server Error' }));
}
});
//Start the server
server.listen(PORT, () => {
console.log(`Server running at https://:${PORT}`);
});Code language: TypeScript (typescript)
这里涉及很多逻辑,让我们拆解一下 /recommend 处理程序的核心逻辑:
- 我们从 URL 中读取
query参数。如果为空,则返回 400 错误。 - 我们调用
vectorStore.similaritySearch(userQuery, 3)来从 Neo4j 图中检索最相似的 3 条文本。在底层,这会使用 Neo4j 中的向量索引来查找距离查询向量最近的邻居。结果是一个 LangChainDocument对象数组。每个Document代表来自节点(在本例中为 Review)的一段文本,并包含所有节点属性作为元数据。 - 如果我们没有得到任何结果,我们会返回一条友好的提示消息,表明没有匹配项。
- 否则,我们获取排名第一的结果的元数据(我们预期其中包含 Review 节点的 ID)。然后,我们运行一个 Cypher 查询(使用 Neo4j 驱动程序)来查找该评论对应的书名和作者姓名。Cypher 模式将评论匹配到书籍
((:Review)-[:WRITTEN_FOR]->(:Book)),然后找到连接到该书的作者((:Author)-[:AUTHORED]->(:Book))。我们限制只返回一位作者/书籍,以防存在多个关联。 - 接下来,我们为 OpenAI 的 Chat API 构建提示词(Prompt)。我们给出简短的 系统角色(system role) 指令,然后作为“用户”消息输入:
- 原始用户查询(他们的兴趣)。
- 我们找到的书籍建议(书名和作者)。
- 一条指令:“写一段推荐语……提到书名和作者,并说明用户为什么可能会喜欢它。”
- 我们调用
chatModel.call(messages)来获取助手的回复。这会使用 OpenAI API(默认使用 ChatGPT 模型)根据我们提供的信息生成简洁的推荐。 - 最后,我们返回包含
recommendation文本的 JSON。
至此,我们的后端逻辑已完成。请注意,我们尽可能保持了最小化和同步处理。对于生产级应用程序,你可能需要批处理查询、处理多作者书籍、缓存结果等,但这些超出了我们目前的范围。
在继续之前,确保存在 public/index.html 文件(我们将在静态路由中提供该文件)——我们接下来就创建它。
3. 前端:HTML + Tailwind CSS 用户界面
我们的前端是一个不使用构建工具的单 HTML 页面。我们将使用 Tailwind 的 Play CDN 快速设置界面样式。创建 public/index.html 如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GenAI Book Recommender</title>
<!-- Tailwind CSS via CDN (Play CDN for development) -->
<script src="https://cdn.tailwindcss.com"></script> <!--:contentReference[oaicite:10]{index=10}-->
</head>
<body class="bg-gray-100 text-gray-800 flex items-center justify-center min-h-screen">
<div class="p-6 bg-white rounded shadow-md max-w-md w-full">
<h1 class="text-2xl font-bold mb-4 text-center">Book Recommendation</h1>
<p class="mb-2 text-sm text-gray-600">Ask for a book recommendation based on your favorite books or genres:</p>
<input id="queryInput" type="text"
class="w-full px-3 py-2 border rounded mb-4"
placeholder="e.g. I love epic fantasy like Lord of the Rings" />
<button id="askButton"
class="w-full bg-blue-600 text-white font-semibold py-2 rounded hover:bg-blue-700">
Recommend a Book
</button>
<div id="result" class="mt-4 text-gray-900 font-medium"></div>
</div>
<script>
const queryInput = document.getElementById('queryInput');
const resultDiv = document.getElementById('result');
document.getElementById('askButton').addEventListener('click', async () => {
const query = queryInput.value;
if (!query) return;
resultDiv.textContent = "Finding recommendations...";
try {
const resp = await fetch('/recommend?query=' + encodeURIComponent(query));
const data = await resp.json();
resultDiv.textContent = data.recommendation || "No recommendation found.";
} catch (err) {
console.error(err);
resultDiv.textContent = "Error getting recommendation.";
}
});
</script>
</body>
</html>Code language: HTML, XML (xml)
让我们拆解一下前端代码:
- 我们在
<head>中引入了 Tailwind CDN 脚本,这使我们能够在 HTML 中使用 Tailwind 工具类,而无需任何构建步骤。(对于演示或开发来说这没问题,但在生产环境中,你通常会生成一个静态 CSS 文件。) - UI 由一个居中容器组成,包含标题、简短说明、用于查询的
<input>和一个按钮。我们还有一个<div id="result">,用于显示推荐文本。 - 使用基础的 Tailwind 类来设置元素样式(例如蓝色按钮、内边距和外边距等)。
- 底部的脚本为按钮添加了点击处理程序。点击时:
- 从输入框中读取查询文本。
- 更新结果 div 显示为“正在查找推荐……”(等待时的反馈)。
- 使用
fetch调用/recommendAPI。我们将查询进行 URL 编码,并预期返回 JSON 响应。 - 一旦收到 JSON,我们将
data.recommendation显示在结果 div 中。如果出现错误(网络问题或服务器返回错误),我们会捕获错误并显示通用的错误消息。
前端就是这样!它设计得很精简——仅足以捕获输入和显示输出。你可以通过添加加载旋转动画、更好的错误显示等功能来增强它,但我们目前保持简单。
4. 运行和测试应用
现在我们已经有了后端和前端,让我们运行应用程序:
npm run build # compile TypeScript to dist/
npm start # start the Node serverCode language: Shell Session (shell)
确保 .env 中的 Neo4j 数据库凭据正确且数据库正在运行(对于 Aura 演示,它是云托管的,因此应该是可用的)。还要确保你的 OpenAI API 密钥已设置且有效。
在浏览器中打开 https://:3000(或你设置的端口)。你应该能看到 图书推荐(Book Recommendation) 界面。尝试输入类似以下内容:
输入: “我很喜欢读《霍比特人》和其他奇幻小说”
点击 推荐一本书(Recommend a Book),稍等片刻,你应该会收到响应。例如,你可能会看到:
输出: “你可能会喜欢 Patrick Rothfuss 的《风之名》(The Name of the Wind) —— 这是一部史诗般的奇幻冒险,有着丰富世界观和引人入胜的叙事,正如你喜欢之前阅读的那样。”
每次输入查询时,应用程序都会在图中查找语义相似的内容并据此推荐一本书,并以友好的推荐方式呈现。在底层,Neo4j 的向量索引是根据含义(而非仅仅是关键词)找到相关的书籍/作者,而 OpenAI 模型则为我们起草了推荐语。
5. 结论
在本教程中,我们构建了一个全栈生成式 AI 应用程序,它结合了基于图的搜索和基于 LLM 的生成。我们使用了 Neo4j 的集成向量搜索来寻找与用户兴趣相关的书籍,并使用 LangChain + OpenAI 生成了人性化的推荐。所有组件(前端、后端、AI 逻辑)都在一个简单的 Node.js 项目中运行。
这种架构可以通过多种方式扩展。例如,你可以索引书籍描述或标签以获得更精确的向量搜索,处理多个推荐,或者添加用户登录以个性化结果。随着代码库的增长,你还可以将前端和后端拆分为不同的服务(我们的模块化设置使后续操作更容易)。但即使在目前这种最小化形式下,我们也拥有了一个由知识图谱和生成式 AI 驱动的功能性推荐应用。祝你阅读愉快!
资源
- Neo4j 向量索引简介及 LangChain 集成文档
- Neo4j Aura 演示中使用的 Goodreads 数据集(1 万本图书、作者、标签)
- 通过 CDN 设置 Tailwind CSS
- SpringAI Goodreads 示例(数据模型)


