作者:Kevin Hu. 编译:Cointime.com QDD
1. 概述
最近,我研究了 LangChain 项目,我对它如何在如此短的时间内成为一个功能强大且成熟的项目感到惊讶。它提供了许多创建自己的基于 LLM 的项目所需的基本工具,只需几行代码即可抽象出繁琐的步骤。
我喜欢该项目的发展方向,开发团队一直积极主动地将最新的 LLM 功能的新想法纳入项目中。
了解这个新项目的过程并不是一帆风顺的。它对于代码组织有自己的观点,对于如何为自己的项目进行黑客攻击,超出了教程的范围,可能不太直观。许多教程都解释了如何使用 LangChain 创建一个小应用程序,但并没有涵盖如何直观地理解抽象和设计选择。
因此,我主动记录了我在这个过程中的个人认知过程。通过这样做,我既可以澄清自己的理解,同时也可以为那些对通过黑客攻击 LangChain 获得乐趣和利益感兴趣的人提供帮助。
这篇博客文章将致力于对所有概念的整体理解。我发现首先理解与 LLM 直接交互的概念,尤其是核心 API 接口,非常有帮助。一旦你了解了所有 LangChain 的抽象概念,黑客攻击和扩展自己的实现就更加直观了。
我将介绍关于 Chain 和 Agents 的基本概念:
l Chain(链)
l Tool(工具)
l Template(模板)
l Agent(代理)
l AgentExecutor(代理执行器)
在这篇博客中我不会提及的内容,将在另一篇博客或讨论中涉及:
l 嵌入
l 内存
l 文档加载器
l 向量存储
选择正确的概念,编程将自然而然地从设计中流淌;
选择错误的概念,编程将成为一系列令人讨厌的意外。
——麻省理工学院教授丹尼尔·杰克逊(Daniel Jackson)在他的书《软件抽象》中关于软件抽象的观点
2. 概念
2.1 链(Chains)
链是组织操作、扩展 LLM 功能和集成不同链操作的基本方式。你可以将它想象为一组"链"在一起的操作。
基本 Chain 类的接口如下所示:
class Chain:
@property
def input_keys(self) -> List[str]:
...
@property
def output_keys(self) -> List[str]:
...
@abstractmethod
def _call(self, input: Dict[str, Any], ...) -> Dict[str, str]:
...
def __call__(self, input: Union[Dict[str, Any], Any]) -> Dict[str, Any]:
# which is a wrapper around _call() and preprocesses input args
...
def run(self, *kargs, **kwargs) -> str:
# which is a wrapper around __call__()
...
一旦你理解了这个接口,扩展 Chain 就非常清晰了。你需要定义:
l 输入参数
l 输出参数
l 在调用 Chain 时要执行的操作,通过定义抽象的 _call() 方法(或者 _acall() 用于异步调用,但我暂时不涉及这些)。
call() 和 run() 方法实际上只是对处理输入参数的核心方法的包装。
有时可能会混淆同一个 Chain 有很多不同的调用方式。但是请思考:
l _call() 是你作为开发者需要定义的基本功能。它有好的、经过预处理的输入参数。
l call() 或 run() 是项目用户的接口,接受更灵活的输入。
通过这个接口,你可以通过在一个链中将它们链接在一起来扩展功能。前一个链的输出将成为下一个链的输入键。
在这里的示例中查看更多内容
2.1.1 LLMChain
LLMChain 是一种特殊类型的链,它包装了底层的 LLM 生成引擎。它是最常用的链,用于直接使用和可扩展性。你可以扩展它以实现任何特殊功能,甚至可以将它们"链"起来以执行与 LLM 相关的一系列操作。
LLMChain 的接口非常简单。详见源代码。
class LLMChain:
prompt: BasePromptTemplate
llm: BaseLanguageModel
output_key: str
LLMChain 通过定义以下内容来扩展原始 Chain:
l 输入:与文本模板所需的相同输入。
l 输出:"text" 字段,即 LLM 生成的输出结果。
你可以直接调用它,或者用它来构建更专业的 Chains。
chain = LLMChain(llm=llm, template=template)
chain.run('LLM prompt')
详见"模板"部分了解 prompt 模板。
2.1.2 扩展和连接 Chains
你可以扩展 Chain 来完成任何需要输入并产生输出的任务。可以将其视为一个任务,你可以将其用于例如文本预处理,甚至解析等任何你认为在实现整个任务流程时有用的任务。
一个 Chain 并不一定需要涉及与 LLM 的交互。当你实现整个任务流水线时,它可以是任何你认为有用的任务。
在"通用链"部分中查看示例:
在示例中,TransformChain 只是执行正则表达式转换以删除空格。你可以与其他 Chains 结合使用,使用 SequentialChain 将它们链接在一起,创建一个转换 -> 重写的转换流水线。
sequential_chain = SequentialChain(
chains=[clean_extra_spaces_chain, style_paraphrase_chain],
input_variables=['text', 'style'],
output_variables=['final_output'])
一旦你理解了 Chains,你就可以在 LangChain 中构建强大的链流水线(因此得名)。有一些链可以:
l 计算和运行数学运算。
l 对文本进行摘要。
l 将文本翻译成其他语言。
l 提供产品名称和口号。
l ...
在 Github 上查看更多链的示例
2.2 模板(Template)
当我第一次接触 LangChain 时,我对 prompt 和模板的概念感到困惑。但实际上,这个想法非常简单。它与任何模板的想法相同:你定义一个模板文本,并用文本变量插值。
prompt 模板最常见的用例是创建 LLM 的输入大纲,你可以通过变量自定义输入。就是这样。就是这么简单。
Template 的一个常见用例是,如上所述,格式化最终的 LLM prompt。在 Agents 中非常有用,因为你可以对 LLM 进行多个查询,并且你可以在每个迭代中使用不同的中间步骤来定义 prompt。
让我们来看看代理的概念。
2.3 代理(Agent)
LLM 最强大的应用之一是工具使用。代理提供了一种选择工具箱以解决更开放和复杂的问题的抽象方法。
根据 LangChain 的官方文档:
有些应用程序需要基于用户输入灵活地调用 LLM 和其他工具的链式调用。代理接口为这样的应用程序提供了灵活性。代理可以使用一套工具,并根据用户输入决定使用哪些工具。代理可以使用多个工具,并将一个工具的输出作为下一个工具的输入。
详见:
l https://python.langchain.com/docs/modules/agents/
l https://archive.pinecone.io/learn/langchain-agents/
2.3.1 代理接口
class Agent:
def plan(self):
...
def aplan(self):
...
这就是代理的接口。
理解代理的第一步是摒弃复杂的工具使用等特性,仅关注接口。
代理是一种自动执行者,可以根据 LLM 输出的每个步骤制定"计划"。你可以添加更多功能,创建一个完整的、功能齐全的代理,可以为你执行操作,例如使用工具、构建提示模板、解析输出。
要创建自己的 LangChain 代理,你只需要关注制定计划(例如处理输入、创建提示、解析输出并返回输出)。
为了说明代理的接口,我创建了一个非常简单的虚拟代理的实现,它根据定义的工具执行动作恰好 3 次。
在这个示例中,计划是:使用给定的工具返回 3 次的 AgentAction,然后返回 AgentFinish。
class DummyAgent(BaseSingleActionAgent):
# initiate other part of the code like input, output, etc. # ...
tool: str = ''
count: int = 3
def plan(self,
intermediate_steps: List[Tuple[AgentAction, str]],
**kwargs: Any):
if self.count <= 0:
return AgentFinish({'output': 'Finished execution'},
log='Action Finished: ')
self.count -= 1
return AgentAction(tool=self.tool,
ool_input=kwargs['tool_input'],
log='Agent Action: ')
(在 Gist 上查看代码片段。此外,我刚刚开始了一个小的副项目,专门研究代理。在 Github 上了解更多详情。)
2.3.2 工具(Tools)
工具是与其他环境交互的接口。该接口同样非常简单,具有 run 或异步的 arun。
工具可以是与 LLM 相关的任何外部操作,例如计算器、搜索引擎、SQL 执行、文档或数据加载器,或具有 API 的任何其他操作。它还可以是任何其他链!
它的接口同样简单。同样地,你只需要定义输入、输出和要运行的内容。
from langchain.tools.base import BaseTool
class ExampleTool(BaseTool):
name = 'example'
description = 'An example tool'
def _run(self, query):
return 'some run results'
def _arun(self, query):
return 'async run'
有时候这可能会让人困惑,因为它可以以不同的初始化方式使用:
# initializing by setting the name, description, and a callable functionmath_tool = Tool(
name='Calculator',
func=llm_math.run,
description='Useful for when you need to answer questions about math.',
)
# or, initializing with a function call
Tool.from_function(
name='Calculator',
func=llm_math.run,
description='Useful for when you need to answer questions about math',
)
但思想是相同的。记住,这只是通过设置名称、描述和 _run 步骤来调用函数创建工具的语法糖。
工具的函数可以是 API 调用(例如计算器、搜索、加载文本等),也可以是调用其他链。它非常灵活,你可以重用链甚至将代理作为工具函数。因此,在这种方式下,一个代理可以调用其他代理。
2.3.3 代理执行器(AgentExecutor)
为了了解代理是如何产生以及 LLM 任务执行的一些基本思想,可以参阅我之前在了解 LLM 推理方面发现有用的论文列表的另一篇博客。
AgentExecutor 也是一个 Chain:它具有与 Chain 完全相同的简单接口:输入、输出和动作,即将与代理相关的一切内容包装在一起。
LangChain 库提供了许多语法糖来"初始化代理"。但记住,它不是返回一个代理,而是返回一个 AgentExecutor,它具有"Chain"的接口。
from langchain.agents import initialize_agent
zero_shot_agent = initialize_agent(
agent="zero-shot-react-description",
tools=tools,
llm=llm,
verbose=True,
max_iterations=3
)
Agent 类抽象了代理行为的最关键部分:它如何根据输入和中间结果"计划"每个步骤,以及它如何决定采取什么行动,或者是否完成代理执行。
AgentExecutor 的 _call() 实现将所有这些内容封装在一起:
l 通过将参数传递给它来初始化代理。
l 读取代理的输出。
l 运行工具箱中的操作。
l 通过提供输出来完成执行。
l 其他基础设施代码,如超时、迭代限制、输出流等。
这些繁琐的工作都在 AgentExecutor 中实现,这样我们就可以专注于有趣的部分,即实际的规划。
通常我们忽略这些繁琐的工作,只关注有趣的部分,例如创建一个根据给定工具执行任务的 ReAct 代理。
是的,AgentExecutor 是一个 Chain,因此它可以与其他 Chains 或作为其他代理的工具一起使用。
在上面提到的 Pinecone 教程中查看另一个示例:
# initializing by setting the name, description, and a callable function
math_tool = Tool(
name='Calculator',
func=llm_math.run,
description='Useful for when you need to answer questions about math.',
)
llm_math 是一个 AgentExecutor 类,它包装了 "llm_math" 代理,它是一个 Chain,其 run() 接口是调用代理的函数。
清楚了吗?
LangChain 已经提供了丰富的代理库,可以执行一些有趣的工作,例如读取 CSV 数据、管理文件、调用 API 等。
请参阅这里
3. 将所有内容结合起来:ZeroshotAgent
一旦你理解了所有这些组件,你可以将它们组合在一起,创建自己的代理。
实现背后有两篇论文。我在之前的博客中也提到了它们:
l MRKL Systems:一种将大型语言模型、外部知识源和离散推理相结合的模块化神经符号架构,描述了将 LLM 与外部工具结合起来的思想。
l ReAct:在语言模型中协同推理和行动,描述了如何通过格式化提示使 LLM 与外部工具进行推理和思维链推理。
LangChain 实现了 ZeroShotAgent
1) 创建一个提示以指导工作流程,创建几个样本遵循以下模式:
l 问题:
l 思考:
l 动作:
l 动作输入:
l 观察结果:(使用工具,Thought + Action + Observation 循环可能发生 N 次)
l 最终答案:
2) 为代理创建工具
3) 解析来自 Thought 的 LLM 输出,例如要使用的工具,是否为最终答案。
4) 调用工具并创建"观察结果"。
5) 基于输出创建另一个提示,然后再次将其提供给 LLM。
6) 重复此过程直到获得最终答案。输出。
如果你认为这对你有帮助,我会继续探索并写下我对 LangChain 和 NLP + LLM 的其他发现。希望这对你的理解有所帮助!
所有评论