TL;DR

Langchain 的核心模块与 LLM 应用开发基础

⚠️ 未经许可禁止转载与再分发。引用时请注明出处 ONDA(온다)。

大家好!我是 ONDA 的 Elliot(엘리엇,이은규),负责公司整体开发工作。

最近,以 GPT-4、Bard、LLaMa 为代表的大科技公司大语言模型(LLM)竞争,成了市场最热的话题。在这场范式变革的中心,开源主导的小规模语言模型及其应用项目也展现出瞩目的增长,对生成式 AI 与语言模型应用能力重要性的共识正在快速扩散。

LLM 的应用彻底革新了我们过去处理文本数据的方式,提出了分析、生成、应用数据的全新方法。我们可以用 LLM 自动生成或摘要某个主题的长文,对问题获得主动的回答。还能实现将非结构化数据智能转换为结构化数据等功能——很多以前无法尝试或极其复杂的想法,现在都能轻松实现。

特别是最近,基于 LLM 的各种有趣实现与尝试层出不穷。比如,用 LLM 构建可相互交流的 AI 角色群体并在虚拟空间中模拟的研究项目,或是为完成给定指令而自主思考、规划、行动的 Auto-GPT 智能体项目

看到这些曾经人人都想过的点子真正落地,大家一定很好奇这些项目是如何操控和构建 LLM 的。

这些项目乍看像是建立在极其复杂的工程设计和晦涩的数学理论之上,但实际上,当你了解到构成它们的核心思路比想象中简单得多时,多数人会感到惊讶。

今天要介绍的 Langchain,就是一个帮你轻松控制 LLM 复杂连锁反应的框架,它将操作模块化,让以语言模型为引擎的应用开发变得简单

什么是 Langchain?

Langchain 是 2022 年 10 月推出的开源框架。截至 2023 年 6 月,它在 GitHub 上获得了近 4.4 万个星标,社区呈爆发式增长,正在框架之上构建一个庞大的生态系统。

Langchain 的核心概念从名字就能看出来——将 LLM 提示词(Prompt)的执行与外部操作(如计算器、谷歌搜索、Slack 消息发送或代码执行等)串联起来(Chaining)

可以把它理解为:将带有输入输出的各种组件串成一条流程(Flow),再把这些流程模块化并组装成一个完整应用的框架。这一点通过 Langchain 可视化设计工具 Langflow 的界面能更直观地理解。

[图源:Langflow]
[图源:Langflow]

Langchain 有预设的模块(Module),用户可以将这些模块适当组合成各种组件(Component),再设置组件之间的流水线。

模块种类繁多,并随着框架生态的发展持续增加。多个模块组成一个组件,多个组件串成链条,像搭乐高积木一样构建出一个完整的应用

Langchain 的模块种类很多,但最核心的几个模块如下:

  • LLMs(大语言模型)
  • Prompt Templates(提示词模板)
  • Agents(智能体)
  • Tools(工具)

本文将解释各模块的作用,并用简单示例说明它们如何运作。需要注意的是,Langchain 的模块并非都在同一层级工作(有些模块需要其他模块作为必要组成部分),同一模块在不同场景下也可能承担不同角色。就像职场上,同一职务在不同公司 R&R 不同一样。

💡 除了上述模块,还有 Memory、Vectorstore、Retriever、Text Splitter、Document Loader 等多种领域模块。此外,Agent Executor、Toolkit 等更细分或更抽象的模块也在持续支持和添加。如果想深入使用框架,建议进一步了解。

现在我们逐一来看各模块。本文示例使用 Langchain 官方的 JavaScript 版本 langchain-js。

1. LLMs(大语言模型)

LLM 模块是 Langchain 的引擎。它的作用是将不同语言模型或语言模型服务的 API,规范化为 Langchain 其他模块可使用的统一接口

模块可接入任何类型的 LLM。可以是 OpenAI 的 GPT-4、GPT-3.5 或 Davinci、Ada 模型,也可以是 Hugging Face Inference API 托管的模型,或本地运行的基于 LLaMa 的模型。社区发展迅速,支持大部分端点,如有需要,自行构建接口也不难。

而且一个应用不必只用一个 LLM。可以在需要轻量推理的简单提示词上使用 Ada 模型,在需要更深度推理的任务上使用自托管的 Vicuna 模型,在需要复杂推理和行动规划的 Agent 模块上使用 GPT-4 模型——这样的应用架构既能减少不必要的资源浪费,又能降低成本。

创建 LLM 模块实例非常简单。下面是用 GPT-4 模型创建 LLM 模块实例的示例:

import { ChatOpenAI } from 'langchain/chat_models';

export const gpt4Model = new ChatOpenAI({
  temperature: 0.6,
  modelName: 'gpt-4',
  verbose: true,
  streaming: true,
});

这样创建的 LLM 实例会传递给其他模块,承担执行提示词的角色。

2. Prompt Templates(提示词模板)

Prompt Template 是一个模板模块,方便将变量插入预设的提示词

假设你设计了这样一个简单的语言检测提示词并要使用它:

Detect the language of text in <input></input>. Your answer must be in english. don't use language code, return full name of the language in english.
if the language seems like one of programming language (like code blocks) return "english".
if detected language is ambigous, return most popular one.
if you can't detect language, return "english".

use the following format for your answer:
Detected: <detected language>

Example:
<input>
안녕하세요.
</input>

Detected: korean

User Input:

<input>
这里要检测这段文字的语言。
</input>

为了复用这个提示词,把 [这里要检测这段文字的语言。] 部分变成动态输入值,做成模板,可以写成这样的代码:

import { BaseOutputParser } from 'langchain/schema';
import { PromptTemplate } from 'langchain';

const LANG_DETECTION = `Detect the language of text in <input></input>. Your answer must be in english. don't use language code, return full name of the language in english.
if the language seems like one of programming language (like code blocks) return "english".
if detected language is ambigous, return most popular one.
if you can't detect language, return "english".

use the following format for your answer:
Detected: <detected language>

Example:
<input>
안녕하세요.
</input>

Detected: korean

User Input:

<input>
{input}
</input>`;

class LanguageDetectionParser extends BaseOutputParser {
  parse = async (output: string): Promise<string> => {
    const result = output.match(/Detected: (.*)/);
    if (result) {
      return result[1];
    }
    return 'english';
  };

  getFormatInstructions(): string {
    return `Your response should be in following format.
Detected: <detected language>`;
  }
}

export const languageDetectionPrompt = new PromptTemplate({
  template: LANG_DETECTION,
  inputVariables: ['input'],
  outputParser: new LanguageDetectionParser(),
});

LANG_DETECTION 字符串就是把提示词中要替换的部分改成 {变量名}。如果要输入大括号字符本身,需要用双大括号 {{}}

LanguageDetectionParser 是一个 OutputParser,用来解析提示词执行结果,只提取需要的内容。示例中只解析 Detected: 后面的字符串。如果 LLM 模型的执行结果是 Detected: korean,解析后的输出就是 korean。

languageDetectionPrompt 是完成后的 PromptTemplate。除了上述组件,还将 inputVariables 传入了模板中使用的变量名 input

这样声明的 Prompt Template 可以通过 LLMChain 模块直接连接到 LLM 使用,或传递给 Agent 由 Agent 执行,作为各种需要提示词输入的模块的输入

3. Agents(智能体)

Agent 是 Langchain 中最核心的模块,也是负责执行最复杂、最精细思考任务的模块

Agent 的概念源于几篇关键的生成式 AI 论文,并随着新方法的提出持续改进。要更完整地理解 Agent 的概念,可以参考以下重要文献:

不必全部读完,但如果读了,对理解 Agent 的概念和工作原理会很有帮助。

简单来说,Agent 会:

  1. 为了完成分配的任务(Task),根据可用的工具(Tools)和当前情况进行思考(Thought),设计接下来需要的行动(Action planning/Reasoning)。

  2. 设计完成后,以适当的输入执行当前需要的 Action。

  3. Action 执行完后,分析执行结果(Observation),并根据分析结果和此前执行的 Action 结果,重复步骤 1~3(Chain of thought)。

  4. 分析结果后,如果任务已完成或可以完成,就结束工作。

这样,Agent 主动利用给定资源执行某项工作

Agent 的角色复杂,实现方式也不是千篇一律。即使使用同一个 Langchain,构建 Agent 的方法也有很多种,根据用途不同,所需模块的构成或提示词形式可能完全不同。因此,本文将看一下 Langchain 官方文档给出的基础示例,并解释其中使用的各个要素。

💡 即使同样的 Agent,根据 Agent Executor 的实现不同,执行方式也会不同。上面描述的步骤针对的是最基础的 Action Agent,还有 Plan-and-Execute Agent 等其他类型——它不只推理当前步骤的 Action,而是从头规划整个执行阶段并分步执行。

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

const model = new OpenAI({ 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: "zero-shot-react-description",
  verbose: true,
});

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

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

这是 LangchainJs 官方文档中关于 MRKL(Modular Reasoning, Knowledge and Language;读作"miracle")Agent 的示例。

示例代码很简洁,因为 agentType 定义为 zero-shot-react-description,这是一种预设配置,包含了 Agent 构建所需的许多要素。

I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.
Action: Search
Action Input: "Olivia Wilde boyfriend"
Observation: Olivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.
Thought: I need to find out Harry Styles' age.
Action: Search
Action Input: "Harry Styles age"
Observation: 29 years
Thought: I need to calculate 29 raised to the 0.23 power.
Action: Calculator
Action Input: 29^0.23
Observation: Answer: 2.169459462491557

Thought: I now know the final answer.
Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 29 years old and his age raised to the 0.23 power is 2.169459462491557.

运行这段代码,可以看到 Agent 收到用户的第一个问题 [Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?] 后:

  1. 用 SerpAPI 在谷歌上搜索 Olivia Wilde 的男友是谁

  2. 发现 Harry Styles 是 Olivia Wilde 的男友,于是搜索他的年龄

  3. 知道他 29 岁后,用 Calculator Tool 计算 29 的 0.23 次方

  4. 得到计算结果后,生成对用户问题的最终回答

可以观察到它自主判断并依次执行上述过程。

💡 如果想知道上述过程如何运作,可以提供自定义 CallBackManager 来监控 LLM 模型的输入输出。还可以阅读官方文档的 Custom Agents 部分,从代码层面分析运行过程。Langchain 框架提供的功能虽多,但文档化范围相对有限。如果直接参考代码仓库使用,能更容易地使用各种高级功能。

用过 Auto GPT 的朋友对这个过程应该很熟悉。Auto GPT 也是在精心编排的 Agent 链上提供多种可用 Tool 的大型 Agent,如果你好奇 Auto GPT 是怎么运作的,看到这里应该能解开部分疑惑。

4. Tools(工具)

Tool 是 Agent 执行各项 Action 时使用的抽象化的"函数"

Agent 会将自己可用的 Tool 列表连同以下信息一起作为提示词的一部分接收:

  • Tool 的名称
  • 该 Tool 的描述(执行什么功能、输入如何传递等)
interface Tool {
  call(arg: string): Promise<string>;

  name: string;

  description: string;
}

Langchain 定义的 Tool 接口如上所示。简洁但灵活,只要以字符串形式返回结果,call 内部执行什么操作都可以。

由多个不同模块构成的复杂 Agent,也能以 Tool 的形式提供给其他 Agent,这种简洁但强大的接口,是 Langchain 应用可能性无穷大的要素之一

下面是一个使用 LLMChain 的简单示例,它是一个 Calculator 工具,在把算式传给 Calculator 之前,先用提示词清洗输入,确保输入的算式 Calculator 能理解。

import { LLMChain, PromptTemplate } from 'langchain';
import { BaseOutputParser } from 'langchain/schema';
import { Calculator, Tool } from 'langchain/tools';

const CALC_EQUATION_GENERATE = `
Generate a calculation equation from contents of <input> block. generated equation must be valid numeric calculation equation.
You need to convert words to numbers, if feasible.
Do not try to calculate the equation, just generate it.
If the input cannot be converted to a valid equation, return <EOF/>.
If the input is already a valid equation, return itself.
Provide your response in <output> block.


Example
\`\`\`
<input>
(1 billion plus 1 million) * 12
</input>

<output>
(1000000000 + 1000000) * 12
</output>
\`\`\`


Now, Here's your input.

<input>
{input}
</input>
`;

class CalcEquationParser extends BaseOutputParser {
  parse = async (output: string): Promise<string> => {
    try {
      if (/^<EOF\/>$/.test(output)) {
        return '';
      }

      return /<output>([\s\S]*)<\/output>/.exec(output)[1].trim();
    } catch (err) {
      return '';
    }
  };

  getFormatInstructions(): string {
    return `Provide your response in <output> block.`;
  }
}

const calcEquationPrompt = new PromptTemplate({
  template: CALC_EQUATION_GENERATE,
  inputVariables: ['input'],
  outputParser: new CalcEquationParser(),
});

const generateCalcEquationChain = new LLMChain({
  llm: gpt3Model,
  prompt: calcEquationPrompt,
  outputKey: 'data',
});

class GenericCalculator extends Tool {
  name = 'calculator';

  async _call(input: string) {
    const { data: generatedEquation } = await generateCalcEquationChain.call({
      input,
    });

    if (!generatedEquation) {
      return `Invalid equation. Please provide a valid equation. Try not to use word for numbers. For example, use 2 instead of two.`;
    }

    return new Calculator().call(generatedEquation);
  }

  description =
    'Useful for getting the result of a math expression. The input to this tool should be a valid mathmatical expression that could be executed by a simple calculator.';
}

这个示例用到了除 Agent 外本文介绍的所有模块,以及 LLMChain 这个链模块。大家可以看看 LLMChain 如何运作、PromptTemplate 和 LLM 模型如何协同执行。


本文介绍了 Langchain 及其几个核心模块,以及使用该框架轻松开发 LLM 应用的基础思路

Langchain 现在仍在以很快的速度开发。还有文中没讲到的应用示例和更多模块,其中包括基于 VectorStore 实现长期记忆等非常有趣的用例。如果你想用生成式 AI 开发应用,一定要参考一下。谢谢!