别再只会调用API了!LangChainJS让大模型应用开发效率提升10倍的秘密

别再只会调用API了!LangChainJS让大模型应用开发效率提升10倍的秘密

别再只会调用API了!LangChainJS让大模型应用开发效率提升10倍的秘密


为什么每个前端工程师都应该了解LangChainJS?

当你看到这句话时,可能会有疑问:市面上已经有那么多教程教我们如何调用ChatGPT API,为什么还要专门学习一个JavaScript框架?这篇文章的标题是不是又在”标题党”?

不,这篇文章要告诉你的,是一套截然不同的开发范式。

想象一下这个场景:你的产品经理兴奋地说,”我们需要一个能理解PDF文档并回答用户问题的智能助手”。如果用传统方式,你需要:

  1. 手动解析PDF文本
  2. 实现语义搜索匹配
  3. 编写Prompt模板
  4. 处理上下文管理
  5. 拼接API调用逻辑
  6. 写一堆样板代码

用LangChainJS?这些都有现成的抽象。这不是在简化代码,而是在重新定义”快速开发”的标准。


一、项目概述与核心价值

1.1 LangChainJS是什么

LangChainJS是LangChain框架的JavaScript/TypeScript实现。LangChain最初诞生于Python社区,旨在简化大语言模型应用的开发流程。当开发者们意识到Node.js生态在前端工程化、API服务、实时应用等方面的优势后,LangChainJS应运而生。

这个项目的核心理念是:将LLM应用开发中的常见模式抽象成可组合的组件,让开发者能够像搭积木一样构建复杂的AI应用。

关键数据一览:

  • GitHub Stars:超过25,000+
  • 周下载量:100万+次
  • 支持的LLM提供商:30+
  • 活跃维护团队:LangChain官方
  • TypeScript覆盖率:95%+

1.2 为什么选择LangChainJS

技术栈统一的优势

现代Web开发中,Node.js已经无处不在。如果你正在构建一个Next.js应用、一个Express后端服务,或者一个Electron桌面应用,使用LangChainJS意味着你可以:

前端/后端代码 → TypeScript/JavaScript → LangChainJS → LLM API

不需要切换语言环境,不需要维护两套代码库,不需要处理Python和JavaScript之间的序列化问题。

TypeScript带来的开发体验

TypeScript的静态类型检查让复杂的AI流水线调试变得更加可控。当你构建一个涉及多个LLM调用、向量检索、工具调用的复杂Agent时,类型提示就是你的地图。

生态系统的丰富性

LangChainJS继承了LangChain生态系统的丰富组件库:

  • 文档加载器:PDF、Markdown、网页、Notion、Discord…
  • 文本分割器:按字符数、token数、语义边界…
  • 向量存储:Pinecone、Chroma、FAISS、MongoDB Atlas…
  • LLM集成:OpenAI、Anthropic、Google、Azure、Cohere…
  • 工具与Agent:搜索引擎、代码执行、数学计算…

企业级应用的支持

Vercel、AWS、Google Cloud等主流云平台都在积极拥抱LangChainJS。这意味着当你需要将AI应用部署到生产环境时,可以获得良好的基础设施支持。


二、环境搭建:从零开始的完整指南

2.1 前置要求

在开始之前,确保你的开发环境满足以下条件:

Node.js版本要求

Node.js ≥ 18.0.0
npm ≥ 9.0.0 或 yarn ≥ 1.22.0 或 pnpm ≥ 8.0.0

建议使用Node.js 20.x或更高版本,以获得更好的性能和最新的语言特性。

包管理器的选择

虽然npm是Node.js的默认包管理器,但在大型项目中,我推荐使用pnpm:

  • 更快的安装速度
  • 更节省磁盘空间
  • 更好的依赖管理

2.2 项目初始化

让我们创建一个完整的LangChainJS项目:

步骤一:创建项目目录

mkdir my-langchain-app
cd my-langchain-app

步骤二:初始化npm项目

npm init -y

这会创建一个基础的package.json文件。

步骤三:安装LangChainJS核心包

npm install langchain

这是一个”All-in-One”包,包含了LangChainJS的大部分核心功能。对于生产环境,这是最推荐的方式。

步骤四:安装LLM提供商包

以OpenAI为例:

npm install @langchain/openai

LangChainJS将各个LLM提供商的SDK封装成独立的包,这样的设计让项目更轻量,同时便于按需安装。

步骤五:安装额外的依赖(可选但推荐)

# 向量数据库支持
npm install @langchain/community

# 环境变量管理
npm install dotenv

# 文档解析
npm install pdf-parse cheerio

步骤六:配置TypeScript(可选但强烈推荐)

npm install -D typescript @types/node ts-node
npx tsc --init

创建一个基础的tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

步骤七:创建项目结构

my-langchain-app/
├── src/
   ├── index.ts          # 入口文件
   ├── chains/           # Chain相关代码
   ├── agents/           # Agent相关代码
   ├── prompts/          # Prompt模板
   └── utils/            # 工具函数
├── tests/                # 测试文件
├── .env                  # 环境变量
├── package.json
├── tsconfig.json
└── README.md

步骤八:配置环境变量

创建一个.env文件:

# OpenAI配置
OPENAI_API_KEY=sk-your-api-key-here

# 其他提供商(如需要)
ANTHROPIC_API_KEY=sk-ant-your-key-here

重要提醒: 永远不要将.env文件提交到版本控制系统!在.gitignore中添加:

.env
node_modules/
dist/

2.3 验证安装

创建一个简单的测试文件来验证安装是否成功:

// src/test-install.ts
import { OpenAI } from "@langchain/openai";

async function testInstallation() {
  const model = new OpenAI({
    modelName: "gpt-3.5-turbo",
    openAIApiKey: process.env.OPENAI_API_KEY,
    temperature: 0.7,
  });

  const response = await model.invoke("用一句话介绍LangChainJS");
  console.log("测试结果:", response);
}

testInstallation().catch(console.error);

运行测试:

npx ts-node src/test-install.ts

如果一切正常,你应该能看到模型返回的文本。


三、核心概念详解

3.1 LLMs与Chat Models

LangChainJS区分两种类型的模型抽象:

LLMs(大型语言模型)

这是对纯文本补全模型的抽象,典型代表是GPT-3的text-davinci系列。输入是文本,输出也是文本。

import { OpenAI } from "@langchain/openai";

const llm = new OpenAI({
  modelName: "gpt-3.5-turbo-instruct",
  temperature: 0.9,
  maxTokens: 100,
});

// 同步调用风格
const result = await llm.invoke("给我讲一个关于程序员的笑话");

Chat Models(聊天模型)

这是对对话模型的抽象,典型代表是GPT-3.5-turbo和GPT-4。输入是消息列表,输出是单条消息。

import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";

const chatModel = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.8,
  maxTokens: 500,
});

// 构建对话上下文
const messages = [
  new SystemMessage("你是一个幽默的AI助手"),
  new HumanMessage("你好,请介绍一下你自己"),
];

const response = await chatModel.invoke(messages);
console.log(response.content); // AI的回复

两者的核心区别

特性 LLMs Chat Models
输入格式 字符串 消息数组
输出格式 字符串 消息对象
上下文处理 需手动管理 原生支持
适用场景 文本生成任务 对话系统

3.2 Prompts与Prompt Templates

Prompt工程是LLM应用的核心。LangChainJS提供了强大的Prompt模板功能。

基础Prompt模板

import { PromptTemplate } from "@langchain/core/prompts";

const template = "请将以下文本翻译成{targetLanguage}:{text}";

const promptTemplate = PromptTemplate.fromTemplate(template);

// 使用模板
const formattedPrompt = await promptTemplate.format({
  targetLanguage: "日语",
  text: "今天天气真好",
});

console.log(formattedPrompt);
// 输出: 请将以下文本翻译成日语:今天天气真好

带示例的Prompt模板

Few-shot learning能显著提升模型表现:

import { FewShotPromptTemplate } from "@langchain/core/prompts";

// 定义示例
const examples = [
  { input: "开心", output: "happy" },
  { input: "悲伤", output: "sad" },
  { input: "愤怒", output: "angry" },
];

const exampleTemplate = "中文: {input} → 英文: {output}";
const examplePrompt = PromptTemplate.fromTemplate(exampleTemplate);

// 构建few-shot提示
const fewShotPrompt = new FewShotPromptTemplate({
  examples: examples,
  examplePrompt: examplePrompt,
  prefix: "将以下中文情感词翻译成英文:",
  suffix: "中文: {word} → 英文:",
  inputVariables: ["word"],
  exampleSeparator: "\n",
  templateFormat: "f-string",
});

const result = await fewShotPrompt.format({ word: "惊讶" });
console.log(result);

3.3 Chains(链)

Chain是LangChain的核心概念,它将多个组件串联起来形成完整的处理流程。

LLMChain:最简单的链

import { OpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { LLMChain } from "langchain/chains";

// 定义Prompt模板
const template = "请为{product}写一句吸引人的广告词,最多15个字";
const prompt = PromptTemplate.fromTemplate(template);

// 创建LLM实例
const llm = new OpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.9,
});

// 创建链
const chain = new LLMChain({ llm, prompt });

// 调用链
const result = await chain.invoke({ product: "智能手表" });
console.log(result.text);

SequentialChain:顺序执行的链

当需要多个LLM依次处理数据时使用:

import { SequentialChain, LLMChain } from "langchain/chains";
import { OpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

// Chain 1: 生成故事大纲
const outlineTemplate = "为{genre}类型的小说写一个三段式大纲";
const outlinePrompt = PromptTemplate.fromTemplate(outlineTemplate);
const outlineChain = new LLMChain({
  llm: new OpenAI({ temperature: 0.8 }),
  prompt: outlinePrompt,
  outputKey: "outline",  // 输出结果的键名
});

// Chain 2: 扩展故事
const storyTemplate = "基于以下大纲,写一个500字的故事:\n{outline}";
const storyPrompt = PromptTemplate.fromTemplate(storyTemplate);
const storyChain = new LLMChain({
  llm: new OpenAI({ temperature: 0.7 }),
  prompt: storyPrompt,
  outputKey: "story",
});

// 组合成顺序链
const sequentialChain = new SequentialChain({
  chains: [outlineChain, storyChain],
  inputVariables: ["genre"],
  outputVariables: ["outline", "story"],
  verbose: true,
});

// 执行
const result = await sequentialChain.invoke({ genre: "科幻" });
console.log("故事大纲:", result.outline);
console.log("\n完整故事:\n", result.story);

RouterChain:智能路由

根据输入内容动态选择不同的处理路径:

import { RouterChain } from "langchain/chains";
import { OpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

// 定义路由逻辑
const routerTemplate = `根据用户的问题,选择最合适的处理方式。
问题: {input}

选项:
1. math - 数学计算相关
2. code - 编程代码相关
3. general - 日常问答

只输出选项名称,不要其他内容。`;

const routerPrompt = PromptTemplate.fromTemplate(routerTemplate);
const routerChain = new LLMChain({
  llm: new OpenAI({ temperature: 0 }),
  prompt: routerPrompt,
});

// 根据路由选择对应的处理链
async function routeRequest(input: string): Promise<string> {
  const route = await routerChain.invoke({ input });

  if (route.text.includes("math")) {
    return "将问题分解为数学步骤并计算";
  } else if (route.text.includes("code")) {
    return "用代码解释器处理这个问题";
  } else {
    return "用日常语言回答这个问题";
  }
}

3.4 Memory(记忆)

Memory组件让Chain能够”记住”之前的对话内容,这对于构建聊天机器人至关重要。

对话缓冲记忆

最简单的记忆形式,只保留最近的几轮对话:

import { BufferMemory } from "langchain/memory";
import { ChatOpenAI } from "@langchain/openai";
import { ConversationChain } from "langchain/chains";

const memory = new BufferMemory({
  memoryKey: "history",  // 存储历史记录的键名
  returnMessages: true,   // 返回消息对象而非字符串
});

const chat = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.7,
});

const conversationChain = new ConversationChain({
  llm: chat,
  memory: memory,
  verbose: true,
});

// 多轮对话
await conversationChain.invoke({ input: "我叫小明,18岁" });
await conversationChain.invoke({ input: "我叫什么名字?" }); // 应该记住"小明"
await conversationChain.invoke({ input: "我今年多大了?" }); // 应该记住"18岁"

缓冲窗口记忆

只保留最近N轮对话,节省token:

import { BufferWindowMemory } from "langchain/memory";

const windowMemory = new BufferWindowMemory({
  k: 5,  // 只保留最近5轮对话
  memoryKey: "chat_history",
});

实体记忆

专注于记住对话中的实体信息:

import { EntityMemory } from "langchain/memory";

const entityMemory = new EntityMemory({
  llm: chat,
  sessionId: "user-123",
  maxEntities: 10,  // 最多记住10个实体
});

3.5 Agents(智能体)

Agent是LangChainJS中最强大的功能之一。它不仅仅是执行预设的流程,而是让LLM能够”思考”并决定下一步行动。

Agent的核心工作机制

用户输入 → LLM思考 → 选择工具 → 执行工具 → 获取结果 → LLM再思考 → ...

创建一个基础Agent

import { OpenAI } from "@langchain/openai";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { SerpAPI } from "langchain/tools";
import { Calculator } from "langchain/tools/calculator";

// 初始化LLM
const model = new OpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0,
  verbose: true,
});

// 定义工具
const tools = [
  new SerpAPI(process.env.SERPAPI_API_KEY, {
    location: "Beijing,China",
    hl: "zh-cn",
  }),
  new Calculator(),
];

// 创建Agent
const executor = await initializeAgentExecutorWithOptions(tools, model, {
  agentType: "zero-shot-react-description",
  verbose: true,
});

// 使用Agent
const result = await executor.invoke({
  input: "查找2024年诺贝尔物理学奖得主,并计算他们的平均年龄",
});

console.log(result.output);

AgentType详解

LangChainJS提供了多种Agent类型:

AgentType 特点 适用场景
zero-shot-react-description 基于工具描述自动选择 通用问题
react-docstore 结合搜索和推理 需要查资料的复杂问题
self-ask-with-search 追问式思考 需要逐步推理的问题
conversational-conversational 对话式交互 聊天机器人

3.6 Document Loaders与Text Splitters

处理外部文档是LLM应用的常见需求。

文档加载器

import { PDFLoader } from "langchain/document_loaders/fs/pdf";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { CSVLoader } from "langchain/document_loaders/fs/csv";

// 加载PDF
const pdfLoader = new PDFLoader("./document.pdf");
const pdfDocs = await pdfLoader.load();

// 加载文本文件
const textLoader = new TextLoader("./article.txt");
const textDocs = await textLoader.load();

// 加载CSV
const csvLoader = new CSVLoader("./data.csv");
const csvDocs = await csvLoader.load();

console.log(`加载了 ${pdfDocs.length} 页PDF`);
console.log(`第一页内容预览: ${pdfDocs[0].pageContent.substring(0, 200)}`);

文本分割器

import { RecursiveCharacterTextSplitter } from "langchain/text splitter";

const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,       // 每块的最大字符数
  chunkOverlap: 200,      // 块之间的重叠字符数
  separators: ["\n\n", "\n", " ", ""],  // 分割优先级
});

// 分割文档
const chunks = await textSplitter.splitDocuments(pdfDocs);

console.log(`分割成 ${chunks.length} 个文本块`);
chunks.forEach((chunk, index) => {
  console.log(`块 ${index + 1}: ${chunk.pageContent.length} 字符`);
});

3.7 Vector Stores与嵌入

向量存储是实现语义搜索的基础。

创建向量存储

import { OpenAIEmbeddings } from "@langchain/openai";
import { HNSWLib } from "langchain/vectorstores/hnswlib";

// 初始化嵌入模型
const embeddings = new OpenAIEmbeddings({
  openAIApiKey: process.env.OPENAI_API_KEY,
});

// 从文档创建向量存储
const vectorStore = await HNSWLib.fromDocuments(
  chunks,      // 之前分割的文本块
  embeddings   // 嵌入模型
);

// 保存到本地(可选)
await vectorStore.save("./vector_store");

语义搜索

// 相似性搜索
const results = await vectorStore.similaritySearch(
  "关于人工智能的最新发展",  // 查询文本
  3                          // 返回结果数量
);

results.forEach((result, index) => {
  console.log(`\n结果 ${index + 1}:`);
  console.log(`内容: ${result.pageContent.substring(0, 150)}...`);
  console.log(`元数据: ${JSON.stringify(result.metadata)}`);
});

// 带相似度分数的搜索
const resultsWithScore = await vectorStore.similaritySearchWithScore(
  "什么是机器学习",
  5
);

resultsWithScore.forEach(([doc, score]) => {
  console.log(`相似度分数: ${score.toFixed(4)}`);
  console.log(`内容: ${doc.pageContent.substring(0, 100)}...`);
});

从本地加载向量存储

const loadedVectorStore = await HNSWLib.load(
  "./vector_store",
  embeddings
);

四、实战教程:构建一个PDF问答助手

现在让我们综合运用前面学到的知识,构建一个完整的PDF文档问答助手。

4.1 项目架构

pdf-qa-assistant/
├── src/
   ├── index.ts              # 主入口
   ├── loaders/
      └── pdfLoader.ts      # PDF加载逻辑
   ├── vectorstore/
      └── store.ts          # 向量存储管理
   ├── chains/
      └── qaChain.ts        # 问答链
   ├── prompts/
      └── qaPrompt.ts       # Prompt模板
   └── types/
       └── index.ts          # 类型定义
├── data/
   └── sample.pdf            # 示例PDF
├── .env
├── package.json
└── tsconfig.json

4.2 类型定义

// src/types/index.ts

export interface DocumentMetadata {
  source: string;
  page?: number;
}

export interface QAResponse {
  question: string;
  answer: string;
  sources: Array<{
    content: string;
    metadata: DocumentMetadata;
    score: number;
  }>;
}

export interface VectorStoreConfig {
  persistDirectory: string;
  embeddingsModel: string;
}

4.3 PDF加载模块

// src/loaders/pdfLoader.ts

import { PDFLoader } from "langchain/document_loaders/fs/pdf";
import { RecursiveCharacterTextSplitter } from "langchain/text splitter";
import { Document } from "langchain/document";

export interface LoadPdfResult {
  docs: Document[];
  metadata: {
    source: string;
    totalPages: number;
    totalChunks: number;
  };
}

export async function loadPdf(
  filePath: string,
  chunkSize: number = 1000,
  chunkOverlap: number = 200
): Promise<LoadPdfResult> {
  console.log(`\n# ===== 开始加载PDF =====`);
  console.log(`文件路径: ${filePath}`);

  // 加载PDF
  const loader = new PDFLoader(filePath);
  const rawDocs = await loader.load();

  console.log(`原始文档数: ${rawDocs.length}`);

  // 分割文本
  const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize,
    chunkOverlap,
    separators: ["\n\n", "\n", "。", "!", "?", " ", ""],
  });

  const docs = await textSplitter.splitDocuments(rawDocs);

  console.log(`分割后文本块数: ${docs.length}`);
  console.log(`# ===== PDF加载完成 =====\n`);

  return {
    docs,
    metadata: {
      source: filePath,
      totalPages: rawDocs.length,
      totalChunks: docs.length,
    },
  };
}

4.4 向量存储管理

// src/vectorstore/store.ts

import { OpenAIEmbeddings } from "@langchain/openai";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { Document } from "langchain/document";
import * as fs from "fs";
import * as path from "path";

export class VectorStoreManager {
  private embeddings: OpenAIEmbeddings;
  private vectorStore: HNSWLib | null = null;
  private persistDirectory: string;

  constructor(persistDirectory: string = "./data/vectorstore") {
    this.embeddings = new OpenAIEmbeddings({
      openAIApiKey: process.env.OPENAI_API_KEY,
    });
    this.persistDirectory = persistDirectory;
  }

  async createFromDocuments(docs: Document[]): Promise<void> {
    console.log(`\n# ===== 创建向量存储 =====`);
    console.log(`文档数量: ${docs.length}`);

    this.vectorStore = await HNSWLib.fromDocuments(docs, this.embeddings);

    // 确保目录存在
    if (!fs.existsSync(this.persistDirectory)) {
      fs.mkdirSync(this.persistDirectory, { recursive: true });
    }

    await this.vectorStore.save(this.persistDirectory);
    console.log(`已保存到: ${this.persistDirectory}`);
    console.log(`# ===== 向量存储创建完成 =====\n`);
  }

  async load(): Promise<void> {
    console.log(`\n# ===== 加载向量存储 =====`);
    console.log(`从: ${this.persistDirectory}`);

    if (!fs.existsSync(this.persistDirectory)) {
      throw new Error(`向量存储不存在: ${this.persistDirectory}`);
    }

    this.vectorStore = await HNSWLib.load(this.persistDirectory, this.embeddings);
    console.log(`# ===== 向量存储加载完成 =====\n`);
  }

  async similaritySearch(
    query: string,
    k: number = 4
  ): Promise<Array<{ doc: Document; score: number }>> {
    if (!this.vectorStore) {
      throw new Error("向量存储未初始化");
    }

    const results = await this.vectorStore.similaritySearchWithScore(query, k);

    return results.map(([doc, score]) => ({
      doc,
      score,
    }));
  }

  isLoaded(): boolean {
    return this.vectorStore !== null;
  }
}

4.5 Prompt模板

// src/prompts/qaPrompt.ts

import { PromptTemplate } from "@langchain/core/prompts";

export const QA_TEMPLATE = `你是一个专业的文档问答助手。你的任务是根据提供的文档片段回答用户的问题。

## 重要规则:
1. 只使用提供的文档内容回答问题,不要编造信息
2. 如果文档中没有相关信息,请明确告知用户
3. 回答要准确、简洁、有条理
4. 如果有多处相关内容,综合整理后回答

## 文档内容:
{context}

## 用户问题:
{question}

## 回答要求:
1. 先给出直接回答
2. 如果适用,引用相关文档片段
3. 指出信息来源

请开始回答:`;

export const CONDENSE_QUESTION_TEMPLATE = `给定以下对话历史和用户后续问题,将后续问题改写为一个独立、完整的问句,使其能够直接用于向量搜索。

## 对话历史:
{chat_history}

## 用户后续问题:
{question}

## 改写后的独立问题:`;

export function createQAPrompt(): PromptTemplate {
  return PromptTemplate.fromTemplate(QA_TEMPLATE);
}

export function createCondenseQuestionPrompt(): PromptTemplate {
  return PromptTemplate.fromTemplate(CONDENSE_QUESTION_TEMPLATE);
}

4.6 问答链

// src/chains/qaChain.ts

import { OpenAI } from "@langchain/openai";
import { VectorStoreManager } from "../vectorstore/store";
import { LLMChain } from "langchain/chains";
import { createQAPrompt, createCondenseQuestionPrompt } from "../prompts/qaPrompt";
import { QAResponse } from "../types";

export class PDFQAChain {
  private vectorStore: VectorStoreManager;
  private qaChain: LLMChain;
  private history: Array<{ role: string; content: string }> = [];

  constructor(vectorStore: VectorStoreManager) {
    this.vectorStore = vectorStore;

    // 初始化LLM
    const llm = new OpenAI({
      modelName: "gpt-3.5-turbo",
      temperature: 0.3,
      maxTokens: 1000,
    });

    // 创建问答链
    this.qaChain = new LLMChain({
      llm,
      prompt: createQAPrompt(),
    });
  }

  async ask(question: string): Promise<QAResponse> {
    console.log(`\n# ===== 处理问题 =====`);
    console.log(`问题: ${question}`);

    // 1. 从向量存储中检索相关文档
    const relevantDocs = await this.vectorStore.similaritySearch(question, 4);

    console.log(`\n# 检索到 ${relevantDocs.length} 个相关文档`);

    // 2. 构建上下文
    const context = relevantDocs
      .map((item, index) => `[文档 ${index + 1}]\n${item.doc.pageContent}`)
      .join("\n\n");

    // 3. 调用LLM生成答案
    const response = await this.qaChain.invoke({
      context,
      question,
    });

    console.log(`# ===== 回答生成完成 =====\n`);

    // 4. 更新历史记录
    this.history.push({ role: "user", content: question });
    this.history.push({ role: "assistant", content: response.text });

    // 5. 返回结果
    return {
      question,
      answer: response.text,
      sources: relevantDocs.map((item) => ({
        content: item.doc.pageContent,
        metadata: item.doc.metadata as any,
        score: item.score,
      })),
    };
  }

  getHistory(): Array<{ role: string; content: string }> {
    return [...this.history];
  }

  clearHistory(): void {
    this.history = [];
  }
}

4.7 主入口文件

// src/index.ts

import "dotenv/config";
import * as readline from "readline";
import { loadPdf } from "./loaders/pdfLoader";
import { VectorStoreManager } from "./vectorstore/store";
import { PDFQAChain } from "./chains/qaChain";

async function main() {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║                                                           ║
║              PDF 文档智能问答助手                          ║
║                                                           ║
║     使用 LangChainJS 构建的本地文档问答系统                 ║
║                                                           ║
╚═══════════════════════════════════════════════════════════╝
  `);

  // 检查API密钥
  if (!process.env.OPENAI_API_KEY) {
    console.error("错误: 请设置 OPENAI_API_KEY 环境变量");
    console.log("创建 .env 文件并添加: OPENAI_API_KEY=your-api-key");
    process.exit(1);
  }

  // ========== 步骤1: 加载PDF文档 ==========
  console.log("# 正在初始化系统...\n");

  const pdfPath = "./data/sample.pdf";
  const { docs, metadata } = await loadPdf(pdfPath, 1000, 200);

  // ========== 步骤2: 创建向量存储 ==========
  const vectorStore = new VectorStoreManager("./data/vectorstore");

  // 检查是否已有向量存储
  try {
    await vectorStore.load();
    console.log("已加载已有的向量存储");
  } catch {
    console.log("创建新的向量存储...");
    await vectorStore.createFromDocuments(docs);
  }

  // ========== 步骤3: 初始化问答链 ==========
  const qaChain = new PDFQAChain(vectorStore);

  // ========== 步骤4: 交互式问答 ==========
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  const askQuestion = (): void => {
    rl.question("\n请输入您的问题(输入 'quit' 退出): ", async (question) => {
      if (question.toLowerCase() === "quit") {
        console.log("\n感谢使用!再见!\n");
        rl.close();
        return;
      }

      if (!question.trim()) {
        askQuestion();
        return;
      }

      try {
        const result = await qaChain.ask(question);

        console.log("\n" + "=".repeat(50));
        console.log("回答:");
        console.log("=".repeat(50));
        console.log(result.answer);

        console.log("\n" + "-".repeat(50));
        console.log("参考来源 (共3条):");
        console.log("-".repeat(50));

        result.sources.slice(0, 3).forEach((source, index) => {
          console.log(`\n[来源 ${index + 1}] 相似度: ${(1 - source.score).toFixed(2)}`);
          console.log(source.content.substring(0, 200) + "...");
        });

      } catch (error) {
        console.error("\n处理问题时出错:", error);
      }

      askQuestion();
    });
  };

  console.log("\n# 系统准备就绪,请开始提问!\n");
  askQuestion();
}

// 运行
main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

4.8 运行项目

首先,准备一个PDF文件放在./data/sample.pdf目录下,然后运行:

npx ts-node src/index.ts

交互式界面将启动:

╔═══════════════════════════════════════════════════════════╗
║                                                           ║
║              PDF 文档智能问答助手                          ║
║                                                           ║
╚═══════════════════════════════════════════════════════════╝

# 正在初始化系统...

# ===== 开始加载PDF =====
文件路径: ./data/sample.pdf
原始文档数: 5
分割后文本块数: 23
# ===== PDF加载完成 =====

# ===== 创建向量存储 =====
文档数量: 23
已保存到: ./data/vectorstore
# ===== 向量存储创建完成 =====

# 系统准备就绪,请开始提问!

请输入您的问题(输入 'quit' 退出): 

五、更多实用案例

5.1 案例一:智能客服Agent

构建一个能够访问商品信息和订单状态的客服Agent:

import { OpenAI } from "@langchain/openai";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { Toolkit } from "langchain/agents/toolkits";
import { ChainTool } from "langchain/tools";

// 模拟数据库查询
const mockDatabase = {
  products: [
    { id: "P001", name: "无线耳机", price: 299, stock: 50 },
    { id: "P002", name: "机械键盘", price: 599, stock: 0 },
    { id: "P003", name: "人体工学鼠标", price: 199, stock: 120 },
  ],
  orders: [
    { orderId: "O2024001", userId: "U001", status: "已发货", items: ["P001"] },
    { orderId: "O2024002", userId: "U001", status: "处理中", items: ["P002", "P003"] },
  ],
};

// 创建工具函数
const productSearchTool = new ChainTool({
  name: "searchProduct",
  description: "搜索商品信息。输入商品名称或ID。返回商品详情。",
  chain: new LLMChain({
    llm: new OpenAI({ temperature: 0 }),
    prompt: PromptTemplate.fromTemplate(`
      在商品列表中搜索匹配的商品:{query}

      商品列表:
      ${JSON.stringify(mockDatabase.products, null, 2)}

      返回JSON格式结果。
    `),
  }),
});

const orderQueryTool = new ChainTool({
  name: "queryOrder",
  description: "查询订单状态。输入订单号。返回订单详情。",
  chain: new LLMChain({
    llm: new OpenAI({ temperature: 0 }),
    prompt: PromptTemplate.fromTemplate(`
      查询订单状态。订单号:{orderId}

      订单列表:
      ${JSON.stringify(mockDatabase.orders, null, 2)}

      返回JSON格式结果。
    `),
  }),
});

// 初始化Agent
const model = new OpenAI({ temperature: 0, modelName: "gpt-3.5-turbo" });

const agentExecutor = await initializeAgentExecutorWithOptions(
  [productSearchTool, orderQueryTool],
  model,
  {
    agentType: "openai-functions",
    verbose: true,
  }
);

// 对话循环
async function customerService() {
  const responses = [
    "我想买一个无线耳机,有货吗?",
    "那机械键盘呢?",
    "帮我查一下订单O2024001的状态",
  ];

  for (const message of responses) {
    console.log(`\n用户: ${message}`);
    const result = await agentExecutor.invoke({ input: message });
    console.log(`客服: ${result.output}`);
  }
}

customerService();

5.2 案例二:多语言翻译流水线

构建一个支持多种语言、能够保持上下文一致性的翻译系统:

import { ChatOpenAI } from "@langchain/openai";
import { LLMChain } from "langchain/chains";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

interface TranslationResult {
  original: string;
  translated: string;
  language: string;
  tone?: string;
}

class MultiLanguageTranslator {
  private chatModel: ChatOpenAI;
  private toneOptions = ["正式", "口语化", "文学", "技术"];
  private languageMap: Record<string, string> = {
    en: "English",
    zh: "中文",
    ja: "日本語",
    ko: "한국어",
    fr: "Français",
    de: "Deutsch",
    es: "Español",
  };

  constructor() {
    this.chatModel = new ChatOpenAI({
      modelName: "gpt-3.5-turbo",
      temperature: 0.3,
    });
  }

  async translate(
    text: string,
    targetLanguage: string,
    tone: string = "通用",
    sourceLanguage?: string
  ): Promise<TranslationResult> {
    const template = `你是一个专业的翻译专家。

## 翻译任务
- 源语言:${sourceLanguage ? this.languageMap[sourceLanguage] : "自动检测"}
- 目标语言:${this.languageMap[targetLanguage]}
- 语气风格:${tone}

## 翻译要求
1. 准确传达原文含义
2. 符合目标语言的语言习惯
3. 保持原文的语气和风格
4. 专业术语保持一致

## 待翻译文本
${text}

## 输出格式
只输出翻译结果,不要其他内容。`;

    const chain = new LLMChain({
      llm: this.chatModel,
      prompt: PromptTemplate.fromTemplate(template),
      outputParser: new StringOutputParser(),
    });

    const result = await chain.invoke({});

    return {
      original: text,
      translated: result.text,
      language: targetLanguage,
      tone,
    };
  }

  async batchTranslate(
    texts: string[],
    targetLanguage: string,
    tone: string = "通用"
  ): Promise<TranslationResult[]> {
    const results: TranslationResult[] = [];

    for (const text of texts) {
      const result = await this.translate(text, targetLanguage, tone);
      results.push(result);

      // 添加小延迟避免API限流
      await new Promise((resolve) => setTimeout(resolve, 500));
    }

    return results;
  }

  async translateWithGlossary(
    text: string,
    targetLanguage: string,
    glossary: Record<string, string>
  ): Promise<string> {
    const template = `你是一个专业的翻译专家,使用以下术语表确保翻译一致性:

## 术语表
${Object.entries(glossary)
  .map(([zh, en]) => `- ${zh}${en}`)
  .join("\n")}

## 翻译任务
将以下中文文本翻译成${this.languageMap[targetLanguage]}

${text}

## 要求
1. 严格按照术语表翻译
2. 保持专业性和准确性
3. 只输出翻译结果`;

    const chain = new LLMChain({
      llm: this.chatModel,
      prompt: PromptTemplate.fromTemplate(template),
    });

    const result = await chain.invoke({});
    return result.text;
  }
}

// 使用示例
async function main() {
  const translator = new MultiLanguageTranslator();

  // 单条翻译
  const singleResult = await translator.translate(
    "人工智能正在改变我们的生活方式",
    "en",
    "正式"
  );
  console.log("单条翻译:", singleResult);

  // 带术语表的翻译
  const glossary = {
    人工智能: "Artificial Intelligence",
    机器学习: "Machine Learning",
    深度学习: "Deep Learning",
    神经网络: "Neural Network",
  };

  const techResult = await translator.translateWithGlossary(
    "深度学习是机器学习的一个分支,神经网络是其核心组件。",
    "en",
    glossary
  );
  console.log("技术文档翻译:", techResult);
}

main();

5.3 案例三:代码审查助手

import { ChatOpenAI } from "@langchain/openai";
import { LLMChain } from "langchain/chains";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

interface CodeReviewResult {
  issues: Array<{
    severity: "critical" | "warning" | "info";
    line?: number;
    message: string;
    suggestion: string;
  }>;
  summary: string;
  score: number;
}

class CodeReviewAssistant {
  private chatModel: ChatOpenAI;

  constructor() {
    this.chatModel = new ChatOpenAI({
      modelName: "gpt-3.5-turbo",
      temperature: 0.2,
    });
  }

  async review(code: string, language: string = "typescript"): Promise<CodeReviewResult> {
    const template = `你是一个经验丰富的代码审查专家。审查以下${language}代码,找出潜在问题。

## 审查重点
1. 安全性漏洞(SQL注入、XSS、敏感信息泄露等)
2. 性能问题(内存泄漏、算法复杂度、数据库查询等)
3. 代码质量(可读性、可维护性、最佳实践等)
4. 错误处理(异常捕获、日志记录等)
5. 最佳实践(设计模式、命名规范等)

## 代码
\`\`\`${language}
${code}
\`\`\`

## 输出要求
以JSON格式输出,结构如下:
{
  "issues": [
    {
      "severity": "critical|warning|info",
      "line": 行号(如适用),
      "message": "问题描述",
      "suggestion": "修改建议"
    }
  ],
  "summary": "总体评价(100字以内)",
  "score": 1-10的质量评分
}

只输出JSON,不要其他内容。`;

    const chain = new LLMChain({
      llm: this.chatModel,
      prompt: PromptTemplate.fromTemplate(template),
      outputParser: new StringOutputParser(),
    });

    const result = await chain.invoke({});

    try {
      return JSON.parse(result.text);
    } catch {
      return {
        issues: [],
        summary: "无法解析审查结果",
        score: 0,
      };
    }
  }

  async reviewPullRequest(
    diff: string,
    context?: string
  ): Promise<CodeReviewResult> {
    const template = `你是一个专业的代码审查专家。审查以下Pull Request变更。

## PR背景
${context || "无额外上下文"}

## 变更内容(diff格式)
\`\`\`diff
${diff}
\`\`\`

## 审查要点
1. 变更是否符合代码库的编码规范
2. 是否引入了安全隐患
3. 是否有性能影响
4. 是否进行了充分的测试
5. 变更的必要性

## 输出格式(JSON)
{
  "issues": [...],
  "summary": "...",
  "score": 数字
}`;

    const chain = new LLMChain({
      llm: this.chatModel,
      prompt: PromptTemplate.fromTemplate(template),
      outputParser: new StringOutputParser(),
    });

    const result = await chain.invoke({});

    try {
      return JSON.parse(result.text);
    } catch {
      return {
        issues: [],
        summary: "无法解析审查结果",
        score: 0,
      };
    }
  }
}

// 使用示例
async function main() {
  const reviewer = new CodeReviewAssistant();

  const codeToReview = `
async function getUserData(userId: string) {
  const query = "SELECT * FROM users WHERE id = " + userId;
  const result = await db.execute(query);
  return result;
}

function processPassword(password: string) {
  const hashed = password + "salt";
  return hashed;
}
`;

  const result = await reviewer.review(codeToReview, "typescript");

  console.log("代码审查结果");
  console.log("=".repeat(50));
  console.log(`质量评分: ${result.score}/10`);
  console.log(`\n总体评价: ${result.summary}`);
  console.log(`\n发现问题: ${result.issues.length}个`);

  result.issues.forEach((issue, index) => {
    console.log(`\n[${index + 1}] [${issue.severity.toUpperCase()}] ${issue.message}`);
    if (issue.line) console.log(`    行号: ${issue.line}`);
    console.log(`    建议: ${issue.suggestion}`);
  });
}

main();

六、最佳实践与性能优化

6.1 Prompt工程最佳实践

使用结构化的输出格式

让模型输出JSON比自由文本更可靠:

const structuredPrompt = PromptTemplate.fromTemplate(`
回答以下问题,并以JSON格式输出结果。

问题:{question}

输出格式:
{
  "answer": "直接回答",
  "confidence": 0-1之间的置信度,
  "reasoning": "推理过程"
}
`);

// 模型更容易返回有效的JSON

分离指令和上下文

// 不推荐:混杂在一起
const badPrompt = `用户问:${question},请根据以下文档回答:${document}`;

// 推荐:清晰的分区
const goodPrompt = PromptTemplate.fromTemplate(`
## 指令
回答用户的问题,只使用提供的文档内容。

## 文档
{document}

## 问题
{question}

## 输出格式
按以下格式回答:...
`);

使用示例引导输出

Few-shot prompting能显著提升效果:

const fewShotPrompt = new FewShotPromptTemplate({
  examples: [
    {
      input: "北京的人口是多少?",
      output: '{"city": "北京", "metric": "人口", "value": "约2100万"}',
    },
    {
      input: "东京的GDP是多少?",
      output: '{"city": "东京", "metric": "GDP", "value": "约2万亿美元"}',
    },
  ],
  // ... 其他配置
});

6.2 Token使用优化

选择合适的模型

场景 推荐模型 理由
简单问答 gpt-3.5-turbo 成本低、速度快
复杂推理 gpt-4 更强的推理能力
批量处理 gpt-3.5-turbo-instruct 专为补全任务优化
嵌入生成 text-embedding-ada-002 性价比最高

使用流式响应

对于长文本,使用流式处理改善用户体验:

import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  streaming: true,
});

const stream = await model.stream("写一篇关于AI的文章");

for await (const chunk of stream) {
  process.stdout.write(chunk.content);
}

缓存频繁使用的嵌入

import { CacheBackedEmbeddings } from "langchain/embeddings";
import { Redis } from "ioredis";

// 创建带缓存的嵌入模型
const underlyingEmbeddings = new OpenAIEmbeddings();

const redisClient = new Redis(process.env.REDIS_URL);

const cachedEmbeddings = new CacheBackedEmbeddings({
  underlyingEmbeddings,
  documentEmbeddingCache: new RedisByteStore(redisClient),
});

6.3 错误处理与重试机制

import { RetryStrategy, ExponentialBackoffRetryStrategy } from "langchain/core/util";

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      console.log(`尝试 ${attempt + 1}/${maxRetries} 失败,${(baseDelay * Math.pow(2, attempt)) / 1000}秒后重试...`);
      await new Promise(resolve => 
        setTimeout(resolve, baseDelay * Math.pow(2, attempt))
      );
    }
  }

  throw lastError!;
}

// 使用示例
const result = await withRetry(async () => {
  return await chain.invoke({ input: "test" });
});

6.4 生产环境配置

import { ChatOpenAI } from "@langchain/openai";

// 生产环境配置
const productionModel = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.3,
  maxTokens: 2000,
  timeout: 60000,           // 60秒超时
  maxRetries: 3,            // 自动重试
  streaming: false,         // 生产环境可关闭流式
  callbacks: [
    {
      handleLLMEnd: (output) => {
        console.log("Token使用情况:", output.llmOutput?.tokenUsage);
      },
      handleLLMError: (error) => {
        console.error("LLM调用错误:", error);
        // 可以在这里发送告警
      },
    },
  ],
});

6.5 监控与日志

import { CallbackManager } from "langchain/callbacks";
import { LangChainTracer } from "langchain/callbacks/tracers";

// 集成LangSmith监控
const callbackManager = new CallbackManager([
  new LangChainTracer({
    projectName: "production-assistant",
    // 可选:配置LangSmith API
    // ... 其他配置
  }),
]);

const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  callbackManager,
});

七、常见问题与解决方案

7.1 API相关问题

问题:Rate LimitExceededError

// 解决方案1:添加请求间隔
import { AsyncQueue } from "./utils/asyncQueue";

const queue = new AsyncQueue({ concurrency: 1, interval: 1000 });

async function rateLimitedCall(fn: () => Promise<any>) {
  return queue.add(fn);
}

// 解决方案2:使用批量API
const batchModel = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  batchSize: 10,  // LangChain会自动批量处理
});

问题:Invalid API Key

// 确保环境变量正确加载
import "dotenv/config";

// 验证API Key
if (!process.env.OPENAI_API_KEY?.startsWith("sk-")) {
  throw new Error("Invalid OPENAI_API_KEY format");
}

7.2 向量存储问题

问题:向量搜索返回空结果

// 可能原因1:嵌入模型未正确初始化
const embeddings = new OpenAIEmbeddings({
  openAIApiKey: process.env.OPENAI_API_KEY,
  // 确保模型名称正确
  modelName: "text-embedding-ada-002",
});

// 可能原因2:向量维度不匹配
// 确保向量存储和检索使用相同的嵌入模型

// 可能原因3:文本分割方式不适合
// 尝试调整chunkSize和overlap
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,      // 减小
  chunkOverlap: 100,
  separators: ["\n", "。", "!", "?"],  // 使用中文分隔符
});

7.3 内存问题

问题:对话历史过长导致Token溢出

import { BufferMemory } from "langchain/memory";

const memory = new BufferMemory({
  memoryKey: "chat_history",
  maxTokenLimit: 2000,  // 限制历史长度
  returnMessages: true,
});

7.4 Agent执行问题

问题:Agent陷入死循环

const executor = await initializeAgentExecutorWithOptions(tools, model, {
  agentType: "zero-shot-react-description",
  maxIterations: 10,  // 限制最大迭代次数
  earlyStoppingMethod: "force",
});

八、项目扩展与生态

8.1 LangChain生态全景图

LangChain生态系统
├── 核心框架
   ├── langchain (Python)
   ├── langchainjs (JavaScript/TypeScript)
   └── langchain-go (Go)

├── LangServe
   └── 将Chain部署为REST API

├── LangSmith
   ├── 调试与测试
   ├── 性能监控
   └── 数据集管理

└── 社区生态
    ├── 第三方集成
    ├── 预构建模板
    └── 工具市场

8.2 相关的优秀开源项目

LangChain Python版

  • 仓库: github.com/langchain-ai/langchain
  • 用途: Python生态的LLM应用开发

LangServe

  • 仓库: github.com/langchain-ai/langserve
  • 用途: 快速将LangChain应用部署为REST API

LangSmith

  • 官网: smith.langchain.com
  • 用途: LLMOps平台,用于调试、测试和监控

LlamaIndex

  • 仓库: github.com/jerryjliu/llama_index
  • 用途: 专注于知识检索增强(RAG)

AutoGPT

  • 仓库: significantgravitas/AutoGPT
  • 用途: 自主Agent实验项目

8.3 学习资源推荐

官方文档

  • LangChainJS文档: js.langchain.com
  • LangChain文档: python.langchain.com

社区资源

  • LangChain Discord服务器
  • GitHub Discussions
  • 官方博客

视频教程

  • LangChain官方YouTube频道
  • 各技术平台的AI应用开发系列

九、总结与展望

9.1 核心要点回顾

在这篇文章中,我们深入探索了LangChainJS的各个方面:

基础概念

  • LLMs和Chat Models的区别与使用场景
  • Prompt Templates的创建与使用
  • Chains的组合与执行流程
  • Memory组件实现上下文保持

核心功能

  • Agent的构建与工具集成
  • 文档加载与处理
  • 向量存储与语义搜索

实战技能

  • PDF问答助手的完整实现
  • 智能客服Agent的构建
  • 多语言翻译系统
  • 代码审查助手

最佳实践

  • Prompt工程技巧
  • Token使用优化
  • 错误处理机制
  • 生产环境配置

9.2 LangChainJS的未来

LangChainJS作为一个活跃发展的项目,正在快速演进:

近期发展方向

  • 更强大的Agent能力
  • 更丰富的工具集成
  • 更好的性能优化
  • 更完善的TypeScript支持

生态系统成熟度

  • 更多的生产级应用采用
  • 企业级支持增强
  • 与主流云平台的深度集成

9.3 行动建议

立即开始

  1. 按照文章中的环境搭建步骤,创建你的第一个LangChainJS项目
  2. 从简单的LLMChain开始,体验基础功能
  3. 逐步尝试更复杂的Agent和向量检索功能

深入学习

  1. 阅读LangChainJS官方文档和示例代码
  2. 参与LangChain社区讨论
  3. 尝试将LangChainJS集成到你现有的项目中

生产准备

  1. 了解LangSmith监控工具
  2. 使用LangServe部署你的应用
  3. 建立完善的错误处理和监控机制

9.4 最后的思考

LangChainJS代表的不仅是另一个框架,更是一种新的应用开发范式。它将复杂的LLM调用封装成可组合的组件,让开发者能够专注于业务逻辑而非底层细节。

正如React改变了前端开发、Next.js改变了React应用开发一样,LangChainJS正在改变AI应用开发的方式。无论你是前端工程师、后端开发者,还是AI爱好者,掌握LangChainJS都将为你的技术栈增添一块重要的拼图。

现在,是时候开始了。用LangChainJS构建你的第一个AI应用吧!


相关链接

  • GitHub仓库: github.com/langchain-ai/langchainjs
  • 官方文档: js.langchain.com
  • NPM包: npmjs.com/package/langchain
  • Discord社区: discord.gg/langchain
  • 官方博客: blog.langchain.dev

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注