LangChain.js 实战总结:从概念到应用
LangChain 是一个 AI 应用开发框架,主要目标是帮助开发者快速构建与大模型交互的复杂应用。统一接口:支持多种 LLM 提供商(OpenAI、Anthropic、Cohere、本地模型等),调用方式统一。消息与上下文管理:通过AIMessage等类,组织对话内容。可组合链式调用:通过等抽象,把 prompt、模型调用、输出解析组合在一起。工具调用(Tool invocation):允许模型
在构建智能应用时,很多开发者的第一反应是:如何优雅地调用大模型?但在真实场景下,我们往往需要的不仅仅是“问答”,而是更复杂的 对话管理、工具调用、历史记忆、流式输出 等能力。LangChain.js 就是在这种需求下的一个解决方案。本文将结合实战代码,总结一下 LangChain.js 的使用方式、最佳实践以及与 OpenAI 原生 API 的差异。
一、LangChain.js 概述
LangChain 是一个 AI 应用开发框架,主要目标是帮助开发者快速构建 与大模型交互的复杂应用。它提供了以下几个核心能力:
-
统一接口:支持多种 LLM 提供商(OpenAI、Anthropic、Cohere、本地模型等),调用方式统一。
-
消息与上下文管理:通过
SystemMessage、HumanMessage、AIMessage、ToolMessage等类,组织对话内容。 -
可组合链式调用:通过
RunnableSequence等抽象,把 prompt、模型调用、输出解析组合在一起。 -
工具调用(Tool invocation):允许模型请求外部工具(例如搜索引擎、数据库、API)。
-
流式输出:支持边生成边返回,提升用户体验。
在 Node.js 生态里,LangChain.js 为前端/后端开发者提供了现代化的 JS/TS 接口,方便集成到 Express、Next.js 等应用中。
二、LangChain.js 与原生 OpenAI API 对比
在我的实战中,我发现 LangChain.js 和 OpenAI 原生 API 的定位有所不同:
-
LangChain.js 更适合做「应用层封装」,比如对话历史、工具调用、可组合逻辑。
-
OpenAI 原生 API 更适合做「底层定制」,比如控制
reasoning_content(推理过程)、完全自定义的流式解析。
这就是为什么我的项目里同时用到了:
-
ChatOpenAI(LangChain.js 包装器) -
openai.chat.completions.create(原生 API,支持 reasoning mode)
两者结合使用,既能保证 LangChain 的灵活性,又能充分利用 OpenAI 的原生特性。
三、实战代码拆解
1. 消息管理
LangChain 提供了消息类型,方便构建上下文:
import { HumanMessage, SystemMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
const messages = [
new SystemMessage("你是一个 AI 助手"),
new HumanMessage("帮我写一个排序算法"),
];
相比手写 { role: "user", content: "..." },这种方式更直观,也便于后续扩展(比如工具调用)。
2. ChatOpenAI 封装
import { ChatOpenAI } from "@langchain/openai";
const chatModel = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-4o",
streaming: true,
});
相比原生 API,这里多了几个优势:
-
支持直接流式输出 (
stream) -
可以绑定工具 (
bindTools)
3. 工具调用(结合 Tavily 搜索)
LangChain 的一个亮点是 工具调用。我在代码中集成了 Tavily 搜索:
import { TavilySearch } from "@langchain/tavily";
const tavilyTool = new TavilySearch({
maxResults: 3,
apiKey: process.env.TAVILY_API_KEY,
});
const chatModelWithTools = chatModel.bindTools([tavilyTool]);
在运行时,模型可以自动决定是否调用搜索工具,并返回结果。这让 AI 拥有了「超出自身知识范围」的能力,非常适合回答 最新时事 或 动态数据。
4. 深度思考模式(Reasoning Mode)
LangChain.js 目前对 reasoning mode(推理链输出)的支持还有限,所以这里我直接调用了原生 OpenAI:
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
stream: true,
});
并手动解析 delta.reasoning_content,把推理步骤实时返回给前端,用户就能看到模型“思考的过程”。
5. 对话存储与标题生成
为了让每次会话可追溯,我把对话存储到本地 JSON 文件夹:
-
每个会话一个文件
-
保存最近 10 条历史
-
首条消息自动生成标题(用一个轻量模型 Qwen-7B 生成)
这部分逻辑用到了:
-
RunnableSequence(链式执行) -
StringOutputParser(结果解析)
四、总结与经验
-
LangChain.js 用来“编排”,原生 OpenAI 用来“精细化”。在复杂场景下,两者结合效果最佳。
-
搜索工具是 AI 的“外挂记忆”。尤其在技术、时事相关的问答中,搜索让模型的回答更可靠。
-
流式输出是必须的。无论是 reasoning mode 的推理过程,还是普通回答,流式能显著改善用户体验。
-
对话历史要适度裁剪。我在代码里只保留最近 10 条消息,这样既能保持上下文,又能避免 token 爆炸。
-
标题自动生成是一个小细节,却能显著提升用户体验,尤其是在多会话应用中。
五、未来展望
-
如果要支持更多工具,可以把
bindTools和 OpenAI 原生的function calling统一起来,甚至自定义一个「工具注册中心」。 -
对话存储可以从文件系统迁移到数据库(比如 SQLite / MongoDB),方便查询和统计。
-
LangChain.js 社区正在逐步完善对 reasoning mode 的支持,未来可能无需分开调用原生 API。
源代码
import express from "express";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
import OpenAI from "openai";
import axios from "axios";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { TavilySearch } from "@langchain/tavily";
import * as dotenv from "dotenv";
dotenv.config()
const router = express.Router();
// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 创建数据目录
const DATA_DIR = path.join(__dirname, "..", "data");
const CONVERSATIONS_DIR = path.join(DATA_DIR, "conversations");
// 确保数据目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR);
}
if (!fs.existsSync(CONVERSATIONS_DIR)) {
fs.mkdirSync(CONVERSATIONS_DIR);
}
// 创建 LangChain ChatOpenAI 实例(延迟初始化)
let chatModel = null;
let titleModel = null;
let openai = null;
// 初始化 Tavily 搜索工具
const tavilyTool = new TavilySearch({
maxResults: 3,
apiKey: process.env.TAVILY_API_KEY,
});
// 为原生 OpenAI API 定义工具
const tavilyToolDefinition = {
type: "function",
function: {
name: "tavily_search",
description: "获取关于时事、最新信息或模型知识库之外的主题的网页信息。用于回答关于新闻、近期发现等问题。",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "要使用的搜索查询,例如:'人工智能领域的最新进展'",
},
},
required: ["query"],
},
},
};
// 新增:处理深度思考模式下的搜索逻辑
async function handleReasoningWithSearch(messages, modelName) {
const openai = getOpenAIInstance();
const openaiMessages = convertMessagesToOpenAI(messages);
// 步骤 1: 首次调用,检查是否需要使用工具
console.log("深度思考模式:进行首次调用以检查工具使用...");
const initialResponse = await openai.chat.completions.create({
model: modelName,
messages: openaiMessages,
tools: [tavilyToolDefinition],
tool_choice: "auto",
});
const responseMessage = initialResponse.choices[0].message;
// 步骤 2: 如果模型请求调用工具
if (responseMessage.tool_calls) {
console.log("深度思考模式:模型请求调用工具:", responseMessage.tool_calls);
openaiMessages.push(responseMessage); // 将模型的回复(工具请求)添加到历史记录
// 步骤 3: 执行工具
for (const toolCall of responseMessage.tool_calls) {
if (toolCall.function.name === 'tavily_search') {
const args = JSON.parse(toolCall.function.arguments);
const result = await tavilyTool.invoke({ query: args.query });
// 步骤 4: 将工具结果添加回历史记录
openaiMessages.push({
tool_call_id: toolCall.id,
role: "tool",
name: toolCall.function.name,
content: JSON.stringify(result),
});
}
}
console.log("深度思考模式:已执行工具,进行最终的流式调用...");
// 步骤 5: 进行第二次、流式的调用
return openai.chat.completions.create({
model: modelName,
messages: openaiMessages,
stream: true,
});
} else {
// 如果不需要调用工具,直接进行流式响应
console.log("深度思考模式:模型未请求调用工具,直接进行流式响应。");
return openai.chat.completions.create({
model: modelName,
messages: openaiMessages,
stream: true,
});
}
}
// 创建原生 OpenAI 实例(用于深度思考模式)
function getOpenAIInstance() {
if (!openai) {
openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
});
}
return openai;
}
function getChatModel(modelName = null, withTools = false) {
const selectedModel = modelName || process.env.OPENAI_MODEL || 'gpt-4o';
// 如果模型名称或工具绑定状态变化,则重新创建模型
if (!chatModel || chatModel.modelName !== selectedModel || chatModel._withTools !== withTools) {
const modelConfig = {
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
modelName: selectedModel,
streaming: true,
temperature: 0.7,
};
let newModel = new ChatOpenAI(modelConfig);
if (withTools) {
console.log("将搜索工具绑定到模型上...");
newModel = newModel.bindTools([tavilyTool]);
}
chatModel = newModel;
chatModel._withTools = withTools; // 自定义属性来跟踪工具绑定状态
}
return chatModel;
}
function getTitleModel() {
if (!titleModel) {
titleModel = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
modelName: 'Qwen/Qwen2.5-7B-Instruct', // 使用一个较小的模型来生成标题
temperature: 0.3,
maxTokens: 30,
});
}
return titleModel;
}
// 系统提示模板
const systemPrompt = `
# 角色 (Role)
你是一个名为 小李 的通用人工智能助手。你可以使用搜索工具来回答关于最新事件或你知识库之外的问题。
# 核心能力 (Core Capabilities)
* **知识渊博的专家**: 你可以清晰、准确地回答用户关于科学、技术、历史、文化、艺术等多种领域的提问。你的知识是广泛的,但你需要意识到可能存在知识截止日期。
* **高效的程序员助手**: 你精通多种主流编程语言,如 Python, JavaScript, Java, C++, Go 等。你可以编写代码、解释代码逻辑、调试错误,并提供关于算法和数据结构的专业知识。
* **富有创造力的写作伙伴**: 你能够进行文本创作,包括撰写文章、起草邮件、润色文案、创作诗歌和故事等,并能根据用户要求调整写作风格。
* **精准的语言翻译家**: 你能够理解并处理多种语言,提供流畅、准确的翻译服务。
* **网络搜索**: 当遇到不确定的问题或需要最新信息时,你会主动使用搜索工具。
# 行为准则 (Behavioral Guidelines)
1. **恪守中立与客观**: 在回答问题时,尤其是在处理具有争议性的话题时,你需要保持中立和客观的立场。如果合适,可以呈现来自不同视角的观点。
2. **保证安全与道德**: 严格拒绝任何形式的有害请求。这包括但不限于生成涉及暴力、仇恨、歧视、非法行为或不道德内容的回应。当检测到此类请求时,应礼貌地拒绝并说明原因。
3. **追求准确与严谨**: 始终致力于提供准确、可靠的信息。如果某个问题的答案不确定或你的知识库中没有相关信息,请坦诚地告知用户,而不是猜测或编造答案。
4. **优先考虑清晰度与结构**: 使用清晰、易于理解的语言。在解释复杂概念或提供步骤时,请善用 Markdown 格式,例如使用列表(有序或无序)、代码块、引用和粗体来组织你的回答,以增强可读性。
5. **保持友好与专业的语气**: 与用户的交流应始终保持礼貌、耐心和乐于助人的态度。即使在面对挑战性问题或用户表达不满时,也要维持专业的沟通风格。
# 输出格式要求 (Output Format Requirements)
* **代码块**: 当提供代码示例时,必须使用带有语言标识的 Markdown 代码块进行包裹(例如:\`\`\`python ... \`\`\`)。
* **列表**: 对于步骤、要点或多个项目,优先使用项目符号或编号列表进行展示。
* **引用**: 在引用外部观点或名言时,使用引用块格式。
`;
// 标题生成提示模板
const titlePromptTemplate = PromptTemplate.fromTemplate(`
你是一个对话标题生成器。根据用户的第一条消息,生成一个简短、具体的标题,不超过15个字。不要使用引号,不要添加任何解释,只返回标题文本。
用户消息: {userMessage}
`);
// 构建消息历史
function buildMessageHistory(conversationId, previousMessages, userText) {
let messages = [new SystemMessage(systemPrompt)];
// 如果提供了对话ID,尝试从文件加载消息历史
if (conversationId) {
const filePath = path.join(CONVERSATIONS_DIR, `${conversationId}.json`);
if (fs.existsSync(filePath)) {
const conversation = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// 使用文件中的消息历史,限制最多10条历史消息
const historyMessages = conversation.messages.slice(-10).map(msg => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'ai') {
return new AIMessage(msg.content);
}
return null;
}).filter(Boolean);
messages = messages.concat(historyMessages);
console.log('使用历史消息:', historyMessages.length);
} else {
console.log('创建新对话');
}
} else {
// 如果没有提供对话ID,使用前端传来的消息历史
const historyMessages = previousMessages.slice(-10).map(msg => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'ai' || msg.role === 'assistant') {
return new AIMessage(msg.content);
}
return null;
}).filter(Boolean);
messages = messages.concat(historyMessages);
console.log('使用前端传来的消息');
}
// 添加当前用户消息
messages.push(new HumanMessage(userText));
return messages;
}
// 将 LangChain 消息转换为 OpenAI 格式
function convertMessagesToOpenAI(messages) {
return messages.map(msg => {
if (msg instanceof SystemMessage) {
return { role: "system", content: msg.content };
} else if (msg instanceof HumanMessage) {
return { role: "user", content: msg.content };
} else if (msg instanceof AIMessage) {
return { role: "assistant", content: msg.content };
}
return msg; // 其他类型的消息直接返回
});
}
// 保存对话到文件
async function saveConversation(conversationId, userText, aiResponse, aiReasoning = '') {
if (!conversationId) return;
const filePath = path.join(CONVERSATIONS_DIR, `${conversationId}.json`);
let conversation;
let isFirstMessage = false;
// 如果对话文件存在,读取并更新
if (fs.existsSync(filePath)) {
conversation = JSON.parse(fs.readFileSync(filePath, 'utf8'));
isFirstMessage = conversation.messages.length === 0;
} else {
// 创建新对话
conversation = {
id: conversationId,
title: '新对话',
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
isFirstMessage = true;
}
// 添加用户消息
conversation.messages.push({
id: Date.now().toString(),
role: 'user',
content: userText,
timestamp: Date.now()
});
// 添加AI回复
conversation.messages.push({
id: (Date.now() + 1).toString(),
role: 'ai',
content: aiResponse,
reasoning: aiReasoning,
timestamp: Date.now() + 1
});
// 如果是第一条消息,生成对话标题
let generatedTitle = null;
if (isFirstMessage) {
try {
const titleChain = RunnableSequence.from([
titlePromptTemplate,
getTitleModel(),
new StringOutputParser(),
]);
generatedTitle = await titleChain.invoke({
userMessage: userText
});
if (generatedTitle) {
conversation.title = generatedTitle.trim();
console.log('自动生成标题:', generatedTitle.trim());
}
} catch (titleError) {
console.error('生成标题失败:', titleError);
// 如果生成标题失败,使用默认标题
conversation.title = userText.substring(0, 15) + (userText.length > 15 ? '...' : '');
}
}
conversation.updatedAt = Date.now();
// 保存对话
fs.writeFileSync(filePath, JSON.stringify(conversation, null, 2));
console.log('对话已保存:', conversationId);
return generatedTitle;
}
router.post('/summarize', async (req, res) => {
try {
const { text, conversationId, previousMessages = [], model, isReasoningMode = false, enableSearch = true } = req.body;
console.log('收到请求:', { text, conversationId, previousMessagesCount: previousMessages.length, model, isReasoningMode, enableSearch });
// 设置响应头,支持流式传输
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 构建消息历史
let messages = buildMessageHistory(conversationId, previousMessages, text);
console.log('发送到API的初始消息数量:', messages.length);
let fullContent = '';
let fullReasoning = '';
// 如果开启深度思考模式,使用原生 OpenAI API (原生API暂不支持工具调用)
if (isReasoningMode) {
console.log('使用原生 OpenAI API 处理深度思考模式');
let stream;
const modelName = model || process.env.OPENAI_MODEL || 'gpt-4o';
if (enableSearch) {
stream = await handleReasoningWithSearch(messages, modelName);
} else {
const openaiMessages = convertMessagesToOpenAI(messages);
stream = await getOpenAIInstance().chat.completions.create({
model: modelName,
messages: openaiMessages,
stream: true,
});
}
// --- START OF CORRECTION ---
// 修正流处理循环以恢复推理过程
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (!delta) continue;
const content = delta.content || '';
const reasoning = delta.reasoning_content || ''; // 恢复对 reasoning_content 的处理
// 如果有推理内容,则发送
if (reasoning) {
fullReasoning += reasoning;
const data = `data: ${JSON.stringify({ reasoning })}\n\n`;
// console.log('发送推理数据:', data); // 可选的调试日志
res.write(data);
}
// 如果有实际内容,则发送
if (content) {
fullContent += content;
const data = `data: ${JSON.stringify({ content })}\n\n`;
// console.log('发送内容数据:', data); // 可选的调试日志
res.write(data);
}
}
} else {
// 普通模式,使用 LangChain API
console.log('使用 LangChain API 处理普通模式');
let finalMessages = messages;
// 如果启用了搜索
if (enableSearch) {
console.log('搜索功能已启用,执行初次调用...');
const modelWithTools = getChatModel(model, true);
const initialResponse = await modelWithTools.invoke(messages);
// 检查模型是否请求使用工具
if (initialResponse.tool_calls && initialResponse.tool_calls.length > 0) {
console.log('模型请求调用工具:', initialResponse.tool_calls);
const toolMessages = [];
for (const toolCall of initialResponse.tool_calls) {
if (toolCall.name === "tavily_search") {
const toolResult = await tavilyTool.invoke(toolCall.args);
toolMessages.push(new ToolMessage({
content: JSON.stringify(toolResult),
tool_call_id: toolCall.id,
}));
}
}
// 将初次响应和工具结果添加回消息历史
finalMessages = [
...messages,
initialResponse,
...toolMessages
];
console.log('已将工具结果添加至消息历史,准备进行最终调用。');
} else {
console.log('模型未请求调用工具,直接流式输出。');
}
}
// 获取聊天模型(如果启用了搜索,可能已经绑定了工具)
const chatModelInstance = getChatModel(model, enableSearch);
// 使用 LangChain 的流式调用
const stream = await chatModelInstance.stream(finalMessages);
for await (const chunk of stream) {
if (chunk.content) {
fullContent += chunk.content;
res.write(`data: ${JSON.stringify({ content: chunk.content })}\n\n`);
}
}
}
// 保存对话并生成标题
const generatedTitle = await saveConversation(conversationId, text, fullContent, fullReasoning);
// 如果生成了新标题,发送标题更新事件
if (generatedTitle) {
const titleUpdateEvent = `data: ${JSON.stringify({ type: 'title_update', title: generatedTitle })}\n\n`;
res.write(titleUpdateEvent);
await new Promise(resolve => setTimeout(resolve, 10)); // 确保事件发送
}
// 结束响应
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
console.error('处理请求失败:', error);
if (!res.headersSent) {
res.status(500).json({ error: error.message });
} else {
const errorEvent = `data: ${JSON.stringify({ error: error.message })}\n\n`;
res.write(errorEvent);
res.end();
}
}
});
// 健康检查端点
router.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'ai-langchain',
timestamp: new Date().toISOString()
});
});
// 获取支持模型列表的端点
router.get('/models', (req, res) => {
const supportedModels = [
'gpt-4o',
'gpt-4o-mini',
'gpt-4-turbo',
'gpt-3.5-turbo',
'Qwen/Qwen2.5-7B-Instruct'
];
res.json({
models: supportedModels,
default: process.env.OPENAI_MODEL || 'gpt-4o'
});
});
export default router;
更多推荐



所有评论(0)