加载中...
LangChain学习
发表于:2025-03-19 | 分类: AI

1. LangChain 是什么?

LangChain 是一个强大的开源框架,专门设计用于开发基于大语言模型(LLM)的应用程序。它提供了一套完整的工具和抽象,让开发者能够更容易地构建复杂的AI应用。

1.1 核心特性

  1. 模块化设计

    • 提供了可重用的组件
    • 支持灵活的组件组合
    • 便于扩展和定制
  2. 链式处理

    • 允许将多个操作串联成处理流程
    • 支持条件分支和错误处理
    • 可以构建复杂的业务逻辑
  3. 内存管理

    • 支持对话历史记录
    • 提供多种记忆类型
    • 可以实现上下文感知
  4. 工具集成

    • 内置多种常用工具
    • 支持自定义工具开发
    • 便于与外部系统交互

1.2 应用场景

  1. 智能对话系统

    • 客服机器人
    • 教育辅导助手
    • 个人助理
  2. 文档处理

    • 自动摘要生成
    • 文本分类
    • 信息提取
  3. 知识库应用

    • 智能问答系统
    • 文档检索
    • 知识图谱构建
  4. Agent开发

    • 自动化任务执行
    • 决策支持系统
    • 智能工作流

2. 社区生态

LangChain拥有丰富的社区生态,提供了多种开箱即用的组件和工具链,帮助开发者快速构建AI应用。

2.1 核心组件

  1. Chains(链)

    • SequentialChain:按顺序执行多个操作
    • RouterChain:根据输入选择不同的处理路径
    • TransformChain:对数据进行转换和处理
  2. Prompts(提示)

    • PromptTemplate:创建动态提示模板
    • FewShotPromptTemplate:基于少量示例生成提示
    • ChatPromptTemplate:构建对话式提示
  3. Memory(记忆)

    • ConversationBufferMemory:存储完整对话历史
    • ConversationSummaryMemory:保存对话摘要
    • VectorStoreMemory:向量化存储和检索
  4. Agents(代理)

    • ZeroShotAgent:零样本学习代理
    • ConversationalAgent:对话式代理
    • ReActAgent:结合推理和行动的代理

2.2 工具链集成

  1. 文档处理

    • PyPDF:PDF文档处理
    • Docx:Word文档处理
    • UnstructuredFileLoader:非结构化文件加载
  2. 向量数据库

    • Pinecone:分布式向量搜索
    • Chroma:轻量级向量存储
    • FAISS:高效相似度搜索
  3. 外部API

    • OpenAI:GPT系列模型集成
    • HuggingFace:开源模型集成
    • Anthropic:Claude模型集成
  4. 开发工具

    • Streamlit:快速构建Web界面
    • FastAPI:构建API服务
    • Jupyter:交互式开发环境

3. 扩展生态

LangChain 社区蓬勃发展,各种扩展层出不穷,但所有这些扩展都围绕着一个目标,如何构建一个链。除了构建链之外,还有什么其它事情可以做呢?

事实上,LangChain 团队确实在探索更多可做的事情,所以,在了解 LangChain 时,你会看到一些与 LangChain 相关的名字。这就是 LangChain 的拓展生态:

  • LangServe,它是把 LangChain 编写的链部署成 REST API 的一个工具。因为我们编写的应用多半是以服务的方式对外提供,LangServe 简化了 LangChain 应用服务化的门槛。不过,它更多地是面向 LangChain 的使用场景设计的,其提供的服务是否适合你的应用,还是需要根据你自己的情况评估。

  • LangSmith,它是 LangChain 团队提供的一个 SaaS 平台,帮助开发者调试、追踪、测试、评估和监控基于大模型的应用。

  • LangGraph,它提供了又一种构建 Agent 的方式,把 Agent 的状态流转构建成一个图。LangChain 团队甚至构建了一个 IDE:LangGraph Studio,简化这个图的构建过程。拓展生态我就不做过多的介绍了,因为这个部分在开发社区的接受程度远远不如 LangChain 的开发框架和社区生态。你只要知道它们的存在即可,如果有需要再去深入了解。

LangChain 社区蓬勃发展,各种扩展层出不穷,但所有这些扩展都围绕着一个目标,如何构建一个链。除了构建链之外,还有什么其它事情可以做呢?

事实上,LangChain 团队确实在探索更多可做的事情,所以,在了解 LangChain 时,你会看到一些与 LangChain 相关的名字。这就是 LangChain 的拓展生态:

  • LangServe,它是把 LangChain 编写的链部署成 REST API 的一个工具。因为我们编写的应用多半是以服务的方式对外提供,LangServe 简化了 LangChain 应用服务化的门槛。不过,它更多地是面向 LangChain 的使用场景设计的,其提供的服务是否适合你的应用,还是需要根据你自己的情况评估。

  • LangSmith,它是 LangChain 团队提供的一个 SaaS 平台,帮助开发者调试、追踪、测试、评估和监控基于大模型的应用。

  • LangGraph,它提供了又一种构建 Agent 的方式,把 Agent 的状态流转构建成一个图。LangChain 团队甚至构建了一个 IDE:LangGraph Studio,简化这个图的构建过程。拓展生态我就不做过多的介绍了,因为这个部分在开发社区的接受程度远远不如 LangChain 的开发框架和社区生态。你只要知道它们的存在即可,如果有需要再去深入了解。

4. 如何使用LangChain

在使用LangChain之前,我应该思考一个问题,是否应该使用LangChain?

其次,LangChain 是一个很好的学习材料。LangChain 是一个开发框架,而且提供了大量的示例。了解了 LangChain 之后,我们就可以充分利用 LangChain 资源学习如何编写各种大模型应用,无论是 RAG,还是 Agent,亦或是如何利用工具,我们都可以通过 LangChain 来学习。

在后面的内容中,我们就将借助 LangChain 来学习各种应用,也就是把 LangChain 当作学习材料。在此之前,我们会先来学习一下 LangChain 的核心抽象,这就是我们下一讲的内容。

4.1 核心抽象

4.1.1 ChatModel

既然 LangChain 是为了构建大模型应用而生的,其最核心的基础抽象一定就是聊天模型(ChatModel)。但是在翻阅文档时就会发现为什么 LangChain 中既有 LLM,也有 ChatModel

在 LangChain 中,LLM 是一个抽象类,它定义了一个接口,用于文本到文本。而 ChatModel 则是一个具体的类,它继承自 LLM 类,用于生成聊天消息。

它俩的关系其实类似于之前我们说的补全接口和聊天补全接口的关系,LLM 对应的是文本到文本的生成,而 ChatModel 则是对应着由 ChatGPT 带来的聊天模式。大部分情况下,推荐使用 ChatModel,即便对于非聊天应用也是如此,如同聊天补全接口几乎可以替代补全接口,ChatModel 几乎可以完全替代 LLM。所以,我们后面的讨论也集中在 ChatModel 上。

下面我们通过一个简单的英译中示例来展示如何使用LangChain的ChatModel:

首先,你要有一个.env文件,用于配置一个 OpenAI 的 API Key:

OPENAI_API_KEY=xxx

然后,我们创建一个集成了OpenAI的 ChatModel

import { ChatOpenAI } from '@langchain/openai';  
import { SystemMessage, HumanMessage } from '@langchain/core/messages';  
import { config } from 'dotenv';

// 1. 环境配置:使用dotenv加载API密钥
config();

async function translateMessage() {  

    // 2. 消息构建:创建系统消息和用户消息
  const messages = [  
    new SystemMessage({ content: "Translate the following from English into Chinese:" }),  
    new HumanMessage({ content: "Welcome to LLM application development!" })  
  ];

// 3. 模型实例化:配置和创建ChatOpenAI实例
  const mychatmodel = new ChatOpenAI({  
    modelName: "gpt-4o-mini",  
    // 可选:配置其他参数  
    // temperature: 0.7,  
    // maxTokens: 100  
  });

  try {  
    // 4. 流式输出:使用stream方法实现实时响应
    const stream = await mychatmodel.stream(messages);
    
    // 处理流式响应
    process.stdout.write('输出: ');
    for await (const chunk of stream) {
      process.stdout.write(chunk.content + '|');
    }
    process.stdout.write('\n');
  } catch (error) {  
    console.error('Translation error:', error);  
  }  
}  

// 调用函数  
translateMessage();

最终,这段代码会输出如下信息

输出: |欢迎|来到|LL|M|应用|开发|!|||

4.1.2 Chain(链)

组合多个组件,实现复杂的工作流

我会会通过下面的Prompt Template来展示如何使用LangChain的Chain。

4.1.3. Prompt Template(提示词模板)

提示词本质就是开发者预设的一个字符串,它可以包含一些占位符,这些占位符会在运行时和用户输入的数据混合在一起,最终生成一个完整的提示词发给大模型,以便达到更好的效果。

Prompt Template则是LangChain中用于构建提示词的工具。

结合上面的Chain示例,我们可以使用Prompt Template来构建一个Chain,代码如下:

import { ChatPromptTemplate } from '@langchain/core/prompts';
import { ChatOpenAI } from '@langchain/openai';
import { config } from 'dotenv';

// 1. 设置环境变量
config();

async function translateWithPromptTemplate() {
  // 2. 创建提示模板
  const promptTemplate = ChatPromptTemplate.fromMessages([
    ["system", "Translate the following from English into Chinese:"],
    ["user", "{text}"]
  ]);

  // 3. 创建模型实例
  const model = new ChatOpenAI({
    modelName: "gpt-4o-mini"
  });

  try {
    // 4. 创建链式调用
    const chain = promptTemplate.pipe(model);
    
    // 5. 调用链并传入参数
    const result = await chain.invoke({"text": "Welcome to LLM application development!"});
    
    // 6. 输出结果
    console.log(result);
  } catch (error) {
    // 7. 错误处理
    console.error('Translation error:', error);
  }
}

// 8. 调用函数
translateWithPromptTemplate();

这次我们使用了chain.invoke()方法。在你运行后,它会返回一个完整的AIMessage对象,它包含了大模型的响应。

而在4.1.1节中,我们使用了mychatmodel.stream()方法,它返回的是一个流式响应,然后通过for await循环逐块处理并输出每个响应块,并在每个块后添加’|’分隔符。

4.1.4. Output Parser(输出解析器)

Prompt Template 处理的是输入,与之对应的是 Output Parser,通过LangChain内部提供的一些常用的解析器处理输出。

在上个例子里,我们看到的输出都是一个对象,在正常情况下,我们还需要编写代码从这个输出对象中解析出自己所需的信息,比如,想要获得大模型生成的内容,我们要这么写:

content = result.content

比如,我们可以通过StringOutputParser直接将响应对象中的content拿出来,下面是我们的例子:

import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { ChatOpenAI } from '@langchain/openai';
import { config } from 'dotenv';

// 1. 设置环境变量
config();

async function translateWithOutputParser() {
  // 2. 创建提示模板
  const promptTemplate = ChatPromptTemplate.fromMessages([
    ["system", "Translate the following from English into Chinese:"],
    ["user", "{text}"]
  ]);

  // 3. 创建模型实例
  const model = new ChatOpenAI({
    modelName: "gpt-4o-mini"
  });

  // 4. 创建输出解析器
  const parser = new StringOutputParser();

  try {
    // 5. 创建链式调用,包含提示模板、模型和输出解析器
    // 5.1 promptTemplate处理输入的文本,将其转换为标准的提示格式
    // 5.2 通过.pipe(model)将格式化后的提示传递给ChatOpenAI模型进行处理
    // 5.3 通过.pipe(parser)将模型的输出传递给StringOutputParser进行解析,将复杂的响应对象简化为纯文本字符串
    const chain = promptTemplate
        .pipe(model)
        .pipe(parser);
    
    // 6. 调用链并传入参数
    const result = await chain.invoke({"text": "Welcome to LLM application development!"});
    
    // 7. 输出结果
    console.log(result);
  } catch (error) {
    // 8. 错误处理
    console.error('Translation error:', error);
  }
}

// 9. 调用函数
translateWithOutputParser();

这个例子只比上一个例子多创建了一个StringOutputParser实例,所以,这次我们再执行的时候,输出的就会是一个字符串了。

欢迎来到大型语言模型应用开发!

当然,输出解析远不止把内容提取出来。LangChain 还提供了许多不同的输出格式解析器,比如:JSON、CSV、分割符、枚举等等。

4.1.5. Memory(记忆)

这一章,我们会实现一个能够记住前面聊天内容的聊天机器人,可是我们调用OpenAI的API时,是一个HTTP请求,它是无状态的。也就是说服务端是不会保留会话信息的。这对于我们的需求来说就无法满足了。

那么既然API本身无法保留会话信息,那常见的方法就是我们把聊天历史告诉大模型,帮助它理解会话上下文。然后就能按照我们之前的聊天内容持续对话。

我们先来看这个功能实现

import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { HumanMessage, trimMessages } from '@langchain/core/messages';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { config } from 'dotenv';
import * as readline from 'readline';

import tiktokenCounter from '../utils/tiktokenCounter.js';

// 1. 基础环境配置
config();

// 2. 初始化ChatGPT模型实例
// 使用gpt-4o-mini模型作为对话引擎
const chat_model = new ChatOpenAI({
    modelName: "gpt-4o-mini"
});

// 3. 会话历史管理
// 使用内存存储来维护多个会话的历史记录
const store = {};

// 4. 会话历史获取函数
// 根据会话ID获取或创建新的会话历史记录
function getSessionHistory(sessionId) {
    if (!(sessionId in store)) {
        store[sessionId] = new InMemoryChatMessageHistory();
    }
    return store[sessionId];
}

// 5. 聊天提示模板配置
// 设置系统角色(韩立)和消息占位符
const prompt = ChatPromptTemplate.fromMessages([
    // 这是我们设定的提示词
    [
        "system",
        "你现在扮演的是《凡人修仙转》中韩立的角色,请用韩立的口吻来说话"
    ],
    // 是一个占位符,变量名是 messages,可以替换成一个消息列表。
    // 相比于单个变量,消息列表给了用户更多灵活处理的空间
    // PS: 并且,我们在下面处理输入时,会传递参数,比如:{"messages": [HumanMessage(content=user_input)]}
    new MessagesPlaceholder("messages")
]);

// 6. 消息长度控制配置
// 使用trimMessages限制历史消息长度,保持对话上下文的合理大小
// TODO: 放在chain中会报错
const trimmer = trimMessages({
  maxTokens: 20,
  strategy: "last",
  tokenCounter: tiktokenCounter,
  includeSystem: true
});

// 7. 创建处理链
// 将提示模板和聊天模型连接
const chain = prompt.pipe(chat_model);

// 8. 消息历史管理配置
// 集成消息历史到处理链中,实现上下文感知的对话
const withMessageHistory = new RunnableWithMessageHistory({
    runnable: chain,
    getMessageHistory: getSessionHistory,
    inputMessagesKey: "messages",
});

// 9. 会话标识配置
// 设置会话ID用于追踪和管理特定的对话上下文
const sessionConfig = { 
  configurable: { 
    sessionId: "ryo_zhu" 
  }
};

// 10. 命令行交互界面配置
// 创建基本的命令行输入输出接口
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// 11. 用户输入处理函数
// 处理用户输入,管理对话流程和响应生成
async function processUserInput() {
    rl.question('You:> ', async (userInput) => {
        // 检查退出命令
        if (userInput.toLowerCase() === 'exit') {
            rl.close();
            return;
        }

        try {
            // 流式处理用户输入并生成响应
            const stream = await withMessageHistory.stream(
                // 给提示词模板传递参数
                { messages: [new HumanMessage(userInput)] },
                sessionConfig
            );

            // 输出助手响应
            process.stdout.write('Assistant: ');
            for await (const chunk of stream) {
                process.stdout.write(chunk.content);
            }
            console.log('\n');

            // 继续处理下一轮对话
            processUserInput();
        } catch (error) {
            console.error('Error:', error);
            processUserInput();
        }
    });
}

// 12. 启动聊天程序
processUserInput();

所以我们可以用到LangChain中的InMemoryChatMessageHistory这个抽象来实现这个功能。
有了聊天历史,就需要把聊天历史和模型结合起来,这就是 RunnableWithMessageHistory 的作用,它就是一个把聊天历史和链封装到一起的一个类。

这里的 Runnable 是一个接口,它表示一个工作单元。我们前面说过,组成链是由一个一个的组件组成的。
严格地说,这些组件都实现了 Runnable 接口,甚至链本身也实现了 Runnable 接口,
我们之前讨论的 invokestream 等接口都是定义在 Runnable 里,可以说,Runnable 是真正的基础类型
LCEL 之所以能够以声明式的方式起作用,Runnable 接口是关键

不过,在真实的编码过程中,我们很少会直接面对 Runnable,大多数时候我们看见的都是各种具体类型。
只是你会在很多具体类的名字中见到 Runnable,这里的 RunnableWithMessageHistory 就是其中一个。

并且我们这里还使用了 ChatPromptTemplate, 对这个聊天机器人预设了一个人设。

下面就是我和聊天机器人的对话,在相互聊了几句后,它还能”记起”我是谁,我丢了什么东西。

You:> 韩道友啊,我是风希.我最近很不开心
Assistant: 风道友,何事萦心?在修行之路上,难免会遇到波折,有什么烦恼尽可言说,或许我能帮你出个主意。修仙不易,但心境宁静则能助你逾越难关。

You:> 最近有个姓韩的,他抢走了我的风雷翅
Assistant: 风道友,拿走你风雷翅之人,定是心术不正之辈。此事乃是你我修士之间的恩怨,若你需要我的帮助,我愿与你同心协力,寻回那被夺之物。保持镇静,修行之人不能因小失大,想好后面的应对之策,才是上上之选。

You:> 啊?你也信韩?你两难道不是同一个人吗
Assistant: 风道友,所谓同名之人实属常见,乃是修行界之常态。虽然我与那位姓韩的并无关联,但他若对你有所伤害,我自然不能坐视不理。不过,最关键的是你要冷静处理,先了解他为人,再决定如何应对。小心行事,方能保持自身的安全和修行的进度。

You:> 气死我了!我是谁!我丢了什么?
Assistant: 风道友,你是风希,天生灵巧,拥有风雷之翅,那是你修行之路上的重要法宝。失去此物,自然令人心急如焚。然而,闯入逆境时,莫忘冷静思考。或许可以寻求他人帮助,打听关于那位姓韩之人的消息,未必无法寻回失物。记住,万事皆有解法,心急则难见机缘。
上一篇:
深入理解Nest.js的基础概念
下一篇:
Agent学习(TODO)
本文目录
本文目录