参与 LangChain 开源贡献之旅

1,437 阅读9分钟

一、了解 LangChain

LangChain 基本概念

LangChain 是一个用于开发由大型语言模型(LLMs)驱动的应用程序的框架,旨在将大型语言模型(如 GPT-4)与外部计算和数据源结合起来。它的定位是帮助开发人员构建更高级的应用程序,使语言模型不仅能处理文本,还能与外部环境进行交互。

LangChain 关键特性

LangChain 具备以下关键特性:

  1. 组件(Components)

    LangChain 提供了多种组件,用于封装大型语言模型的接口、模板提示和信息检索索引。这些组件可以独立使用,也可以组合起来解决复杂任务。

  2. 链(Chains) 链是 LangChain 的核心概念之一,它允许开发者将多个组件组合在一起,形成一个工作流。例如,可以将信息检索和语言生成组件组合起来,构建一个智能问答系统。

  3. 代理(Agents) 代理使得语言模型能够与外部环境进行交互,例如通过 API 请求执行操作。这使得语言模型不仅能处理文本,还能执行具体的任务。

  4. 索引(Indexes) 索引帮助从大型语言模型中提取相关信息,提升信息检索的效率和准确性。

  5. 提示模板(Prompt Templates) 提示模板允许动态生成查询,避免硬编码文本输入。这使得开发者可以根据用户输入动态生成查询,并发送给语言模型。

  6. 嵌入与向量存储(Embedding and Vector Stores) LangChain 支持将文本转换为向量表示,并存储在向量数据库中,以便进行相似性搜索和信息检索

LangChain 的这种结构设计使得大型语言模型不仅能够处理文本,还能够在更广泛的应用环境中进行操作和响应,大大扩展了它们的应用范围和有效性。

二、LangChain 表达式语言(LCEL)和语言模型(Model I/O)

LangChain 表达式语言(LCEL)和 Model I/O 是 LangChain 框架中的两个核心概念,它们在整体架构中扮演着重要角色,并且相互关联,共同构建了一个灵活且强大的语言处理框架:

  1. LCEL 作为框架
    • LCEL 提供了一个声明式的语言,用于定义和组合不同的组件(包括 Model I/O 组件)。
    • 通过 LCEL,开发者可以轻松地将输入提示、模型调用和输出解析组合成一个完整的工作流。
  2. Model I/O 作为组件
    • Model I/O 提供了标准化的接口,用于与语言模型进行交互。
    • 这些接口可以作为 LCEL 中的组件,参与到更复杂的工作流中。

LangChain App Arch

LangChain 表达式语言(LCEL)

LangChain 表达式语言(LCEL) 是一种声明式语言,用于定义和组合不同的组件以形成复杂的工作流。它的设计目标是简化从原型到生产环境的过渡,使开发者能够轻松地构建和管理复杂的语言处理任务。LCEL 的核心概念之一是 Runnable。Runnable 提供了一组标准化的接口,使得不同的组件可以在一个统一的框架内协作,从而实现更灵活和模块化的语言处理系统。

Runnable 接口定义了一些标准方法,用于执行和管理链条中的操作。以下是主要的接口方法:

  • invoke:直接调用链条上的输入,适用于单一输入的情况。
  • batch:对一组输入进行批量处理,适用于需要同时处理多个输入的情况。
  • stream:流式处理输出,适用于需要逐步获取输出的情况。

LangChain 中的大多数组件必须实现了 Runnable 接口,包括:

LangChain Runnable Design

LangChain 语言模型(Model I/O)

Model I/O 是 LangChain 中用于与大型语言模型(LLMs)进行交互的标准化接口。它将模型的使用过程拆解为三个主要部分:输入提示、模型调用和输出解析。其中,语言模型组件是其核心部分之一,旨在与各种大型语言模型(LLMs)进行交互。LangChain 支持多种类型的语言模型组件,可分为三类:

1、大型语言模型(LLMs): 这些模型接受文本字符串作为输入,并返回文本字符串作为输出。例如:OpenAI 的 GPT-3.5、GPT-4。

2、聊天模型(Chat Models): 这些模型基于语言模型,但接口更结构化,接受聊天消息列表作为输入,并返回聊天消息。例如:OpenAI 的 ChatGPT。

3、文本嵌入模型(Text Embedding Models): 这些模型将文本转换为向量表示,用于相似性搜索、聚类等任务。例如:Hugging Face 的嵌入模型。

三、开发过程实录

理解 LangChain 的继承关系

LangChain 的大部分组件都是基于 LangChain 表达式语言(LCEL)定义的,即继承自 Runnable 基类。

  1. Runnable 抽象类:提供了 batch 和 stream 方法的默认实现。invoke 方法是抽象方法,需要子类实现。
  2. BaseLangChain 抽象类:继承自 Runnable 基类,增加了 verbose 日志输出标识、metadata 元数据、callbacks 回调函数和 tags 标签等基础属性。
  3. BaseLanguageModel 抽象类:继承自 BaseLangChain 基类,增加了 Token 计算方法。
  4. BaseChatModel 抽象类:继承自 BaseLanguageModel 基类,实现了 invoke 方法,并定义了抽象方法 _generate,用于语言模型生成接口。

LangChain BaseChatModel Class

星火大模型集成实现

  1. 目标

实现星火大模型与 LangChain 的集成,通过自定义语言模型类,支持流式和非流式调用。

  1. 设计思路

2.1 自定义语言模型类

  • 继承:自定义语言模型类继承自 LangChain 的 BaseChatModel
  • 实现方法:实现 _generate 方法,用于解析星火大模型的数据。
// 处理星火大模型分片输出
let response: ChatCompletionResponse = { result: "" };
for await (const chunk of streams) {
	const data = JSON.parse(chunk) as ChatCompletionChunk;
	const { header, payload } = data;
	if (header.code === 0) {
		if (header.status === 0) {
			response.result = payload.choices?.text[0]?.content ?? "";
    } else if (header.status === 1) {
      response.result += payload.choices?.text[0]?.content ?? "";
    } else if (header.status === 2) {
      response = { ...response, usage: payload.usage?.text };
      break;
    }
  } else {
    break;
  }
}

2.2 通信协议

  • WebSocket:星火大模型使用 WebSocket 进行通信。
// 定义抽象方法 openWebSocketStream,Web 和 Node 入口分别实现 Websocket 连接方式
/**
 * Method that retrieves the auth websocketStream for making requests to the Iflytek Xinghuo API.
 * @returns The auth websocketStream for making requests to the Iflytek Xinghuo API.
 */
abstract openWebSocketStream<T extends BaseWebSocketStream<string>>(
  options: WebSocketStreamOptions
): Promise<T>;
  • 双入口设计
    • Web 入口:使用浏览器环境的 WebSocket 建立连接通信。
    • Node 入口:使用 ws 库建立 WebSocket 连接后进行通信。
import WebSocket from "ws";

class WebSocketStream extends BaseWebSocketStream {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  openWebSocket(url: string, options: WebSocketStreamOptions): WebSocket {
    return new WebSocket(url, options.protocols ?? []);
  }
}

/**
 * @example
 * ```typescript
 * const model = new ChatIflytekXinghuo();
 * const response = await model.invoke([new HumanMessage("Nice to meet you!")]);
 * console.log(response);
 * ```
 */
export class ChatIflytekXinghuo extends BaseChatIflytekXinghuo {
  async openWebSocketStream<WebSocketStream>(
    options: WebSocketStreamOptions
  ): Promise<WebSocketStream> {
    const host = "spark-api.xf-yun.com";
    const date = new Date().toUTCString();
    const url = `GET /${this.version}/chat HTTP/1.1`;
    const { createHmac } = await import("node:crypto");
    const hash = createHmac("sha256", this.iflytekApiSecret)
      .update(`host: ${host}\ndate: ${date}\n${url}`)
      .digest("base64");
    const authorization_origin = `api_key="${this.iflytekApiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${hash}"`;
    const authorization = Buffer.from(authorization_origin).toString("base64");
    let authWebSocketUrl = this.apiUrl;
    authWebSocketUrl += `?authorization=${authorization}`;
    authWebSocketUrl += `&host=${encodeURIComponent(host)}`;
    authWebSocketUrl += `&date=${encodeURIComponent(date)}`;
    return new WebSocketStream(authWebSocketUrl, options) as WebSocketStream;
  }
}
class WebSocketStream extends BaseWebSocketStream {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  openWebSocket(url: string, options: WebSocketStreamOptions): WebSocket {
    return new window.WebSocket(url, options.protocols ?? []);
  }
}

/**
 * @example
 * ```typescript
 * const model = new ChatIflytekXinghuo();
 * const response = await model.invoke([new HumanMessage("Nice to meet you!")]);
 * console.log(response);
 * ```
 */
async openWebSocketStream<WebSocketStream>(options: WebSocketStreamOptions): Promise<WebSocketStream> {
	const host = "spark-api.xf-yun.com";
	const date = new Date().toUTCString();
	const url = `GET /${this.version}/chat HTTP/1.1`;
	const keyBuffer = new TextEncoder().encode(this.iflytekApiSecret);
	const dataBuffer = new TextEncoder().encode(`host: ${host}\ndate: ${date}\n${url}`);
	const cryptoKey = await crypto.subtle.importKey(
		"raw",
	  keyBuffer,
	  { name: "HMAC", hash: "SHA-256" },
	  false,
	  ["sign"]
	);
	const signature = await crypto.subtle.sign("HMAC", cryptoKey, dataBuffer);
	const hash = window.btoa(String.fromCharCode(...new Uint8Array(signature)));
	const authorization_origin = `api_key="${this.iflytekApiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${hash}"`;
	const authorization = window.btoa(authorization_origin);
	let authWebSocketUrl = this.apiUrl;
	authWebSocketUrl += `?authorization=${authorization}`;
	authWebSocketUrl += `&host=${encodeURIComponent(host)}`;
	authWebSocketUrl += `&date=${encodeURIComponent(date)}`;
	return new WebSocketStream(authWebSocketUrl, options) as WebSocketStream;
}

2.3 调用方式

  • 流式调用:星火大模型仅支持流式调用。
  • 封装接口:封装语言模型生成接口,支持流式和非流式输出,并根据流式输出标识指定调用方式。
/**
 * Calls the Xinghuo API completion.
 * @param request The request to send to the Xinghuo API.
 * @param signal The signal for the API call.
 * @returns The response from the Xinghuo API.
 */
async completion(
  request: ChatCompletionRequest,
  stream: true,
  signal?: AbortSignal
): Promise<IterableReadableStream<string>>;

async completion(
  request: ChatCompletionRequest,
  stream: false,
  signal?: AbortSignal
): Promise<ChatCompletionResponse>;

开源贡献 PR 链接:github.com/langchain-a…

LangChain Xinghuo PR

四、使用方式

基础用法

在这个示例中,我们展示了如何使用 LangChain 集成星火大模型进行基本的聊天操作:

  1. 导入模块:首先,我们从 @langchain/community/chat_models/iflytek_xinghuo 导入 ChatIflytekXinghuo 模型,并从 @langchain/core/messages 导入 HumanMessage
  2. 初始化模型:创建一个 ChatIflytekXinghuo 模型实例。
  3. 发送消息:创建两个不同的消息数组,每个数组包含一个 HumanMessage 实例。
  4. 调用模型:使用 invoke 方法调用模型,并传递消息数组。
  5. 输出结果:将模型的响应输出到控制台。
import { ChatIflytekXinghuo } from "@langchain/community/chat_models/iflytek_xinghuo";
import { HumanMessage } from "@langchain/core/messages";

const model = new ChatIflytekXinghuo();

const messages1 = [new HumanMessage("Nice to meet you!")];

const res1 = await model.invoke(messages1);

console.log(res1);

const messages2 = [new HumanMessage("Hello")];

const res2 = await model.invoke(messages2);

console.log(res2);

ReAct Agent 实现

ReAct(Reasoning and Acting)是一种 AI Agent 的设计思想,强调在执行任务时结合推理和行动两个方面,使 Agent 能够在复杂和动态的环境中更有效地工作。其通常涉及以下关键步骤:

  1. 理解上下文:Agent 首先理解所处的环境和任务的上下文,包括理解自然语言指令、感知环境状态或识别问题本质。
  2. 推理:基于理解的上下文进行逻辑推理,确定最佳行动方案,包括规划、决策制定、问题解决或预测可能的结果。
  3. 规划:在推理基础上制定行动计划,确定一系列有序步骤以实现既定目标或响应特定指令。
  4. 执行:根据规划的步骤执行行动,可能与环境进行交互,如使用 API 调用、操作用户界面或执行其他形式的 I/O 操作。
  5. 反馈和迭代:执行行动后收集反馈,以评估行动效果,并据此调整推理和规划策略,改进未来性能。
import { ChatIflytekXinghuo } from "@langchain/community/chat_models/iflytek_xinghuo";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import type { PromptTemplate } from "@langchain/core/prompts";

import { pull } from "langchain/hub";
import { AgentExecutor, createReactAgent } from "langchain/agents";

// Define the tools the agent will have access to.
const tools = [new TavilySearchResults({ maxResults: 1 })];

const llm = new ChatIflytekXinghuo ({
  temperature: 0,
});

// Get the prompt to use - you can modify this!
// If you want to see the prompt in full, you can at:
// https://smith.langchain.com/hub/hwchase17/react
const prompt = await pull<PromptTemplate>("hwchase17/react");

const agent = await createReactAgent({
  llm,
  tools,
  prompt,
});

const agentExecutor = new AgentExecutor({
  agent,
  tools,
});

// See public LangSmith trace here: https://smith.langchain.com/public/d72cc476-e88f-46fa-b768-76b058586cc1/r
const result = await agentExecutor.invoke({
  input: "what is LangChain?",
});

console.log(result);

// Get the prompt to use - you can modify this!
// If you want to see the prompt in full, you can at:
// https://smith.langchain.com/hub/hwchase17/react-chat
const promptWithChat = await pull<PromptTemplate>("hwchase17/react-chat");

const agentWithChat = await createReactAgent({
  llm,
  tools,
  prompt: promptWithChat,
});

const agentExecutorWithChat = new AgentExecutor({
  agent: agentWithChat,
  tools,
});

const result2 = await agentExecutorWithChat.invoke({
  input: "what's my name?",
  // Notice that chat_history is a string, since this prompt is aimed at LLMs, not chat models
  chat_history: "Human: Hi! My name is Cob\nAI: Hello Cob! Nice to meet you",
});

console.log(result2);

MRKL Agent 实现

MRKL 是 Modular Reasoning, Knowledge and Language 的缩写,中文意思是模块化推理、知识和语言。MRKL 代理是一种具有特定结构和功能的代理系统。

一个 MRKL 代理通常由以下几个部分组成:

  1. 工具(Tools):代理可以使用的工具,例如进行搜索、计算等功能的工具。
  2. LLM 链(LLMChain):用于生成文本,该文本会以某种方式解析以确定要采取的操作。
  3. 代理类本身:负责解析 LLM 链的输出,从而确定要执行的具体操作。
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatIflytekXinghuo } from "@langchain/community/chat_models/iflytek_xinghuo";
import { Calculator } from "@langchain/community/tools/calculator";
import { SerpAPI } from "@langchain/community/tools/serpapi";

export const run = async () => {
  const model = new ChatIflytekXinghuo({ temperature: 0 });
  const tools = [
    new SerpAPI(process.env.SERPAPI_API_KEY, {
      location: "Austin,Texas,United States",
      hl: "en",
      gl: "us",
    }),
    new Calculator(),
  ];

  const executor = await initializeAgentExecutorWithOptions(tools, model, {
    agentType: "chat-zero-shot-react-description",
    returnIntermediateSteps: true,
  });
  console.log("Loaded agent.");

  const input = `Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?`;

  console.log(`Executing with input "${input}"...`);

  const result = await executor.invoke({ input });

  console.log(`Got output ${result.output}`);

  console.log(
    `Got intermediate steps ${JSON.stringify(
      result.intermediateSteps,
      null,
      2
    )}`
  );
};