在构建智能应用时,很多开发者的第一反应是:如何优雅地调用大模型?但在真实场景下,我们往往需要的不仅仅是“问答”,而是更复杂的 对话管理、工具调用、历史记忆、流式输出 等能力。LangChain.js 就是在这种需求下的一个解决方案。本文将结合实战代码,总结一下 LangChain.js 的使用方式、最佳实践以及与 OpenAI 原生 API 的差异。


一、LangChain.js 概述

LangChain 是一个 AI 应用开发框架,主要目标是帮助开发者快速构建 与大模型交互的复杂应用。它提供了以下几个核心能力:

  1. 统一接口:支持多种 LLM 提供商(OpenAI、Anthropic、Cohere、本地模型等),调用方式统一。

  2. 消息与上下文管理:通过 SystemMessageHumanMessageAIMessageToolMessage 等类,组织对话内容。

  3. 可组合链式调用:通过 RunnableSequence 等抽象,把 prompt、模型调用、输出解析组合在一起。

  4. 工具调用(Tool invocation):允许模型请求外部工具(例如搜索引擎、数据库、API)。

  5. 流式输出:支持边生成边返回,提升用户体验。

在 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(结果解析)


四、总结与经验

  1. LangChain.js 用来“编排”,原生 OpenAI 用来“精细化”。在复杂场景下,两者结合效果最佳。

  2. 搜索工具是 AI 的“外挂记忆”。尤其在技术、时事相关的问答中,搜索让模型的回答更可靠。

  3. 流式输出是必须的。无论是 reasoning mode 的推理过程,还是普通回答,流式能显著改善用户体验。

  4. 对话历史要适度裁剪。我在代码里只保留最近 10 条消息,这样既能保持上下文,又能避免 token 爆炸。

  5. 标题自动生成是一个小细节,却能显著提升用户体验,尤其是在多会话应用中。


五、未来展望

  • 如果要支持更多工具,可以把 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;

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐