
掌握 Llm 代理:构建多功能通用代理的分步指南
LLM代理的高级概述。 (作者提供的图片)
为什么要构建一个通用代理? 因为它是原型设计用例的优秀工具,并为设计您自己的自定义代理架构奠定基础。
在我们深入之前,让我们快速介绍一下LLM代理。 随意跳过。
什么是LLM代理?
LLM代理是一个程序,其执行逻辑由其底层模型控制。
从独立LLM到代理系统。 (作者提供的图片)
LLM代理与少量提示或固定工作流等方法的区别在于它能够定义和调整执行用户查询所需的步骤。在访问一组工具(如代码执行或网络搜索)的情况下,代理可以决定使用哪个工具、如何使用它,并根据输出对结果进行迭代。这种适应性使系统能够以最小的配置处理多样化的用例。
代理架构的光谱。 (作者提供的图片)
代理架构存在于一个光谱上,从固定工作流的可靠性到自主代理的灵活性。例如,像检索增强生成(RAG)这样的固定流程可以通过自我反思循环进行增强,使程序在初始响应不足时能够进行迭代。或者,一个ReAct代理可以配备固定流程作为工具,提供灵活而结构化的方法。架构的选择最终取决于用例以及可靠性和灵活性之间的期望权衡。
有关更深入的概述,请查看这个视频。
让我们从零开始构建一个通用的LLM代理!
第一步:选择合适的LLM
选择合适的模型对于实现您期望的性能至关重要。有几个因素需要考虑,比如许可、成本和语言支持。构建LLM代理时最重要的考虑因素是模型在编码、工具调用和推理等关键任务上的表现。评估的基准包括:
- 大规模多任务语言理解 (MMLU) (推理)
- 伯克利函数调用排行榜 (工具选择和工具调用)
- 人类评估 和 大代码基准 (编码)
另一个关键因素是模型的上下文窗口。代理工作流可能会消耗大量的tokens——有时超过100K——更大的上下文窗口确实很有帮助。
考虑的模型(写作时)
- 前沿模型: GPT4-o, Claude 3.5
- 开源模型: Llama3.2, Qwen2.5。
一般来说,较大的模型往往提供更好的性能,但仍然可以选择在本地运行的小模型。使用较小的模型时,您将受到更简单用例的限制,可能只能将代理连接到一两个基本工具。
第2步. 定义代理的控制逻辑(即通信结构)
单代理架构。(作者提供的图片)
简单的LLM与代理之间的主要区别在于系统提示。
在LLM的上下文中,系统提示是一组在模型与用户查询互动之前提供给模型的指令和上下文信息。
LLM预期的代理行为可以在系统提示中进行编码。
以下是一些常见的代理模式,可以根据您的需求进行定制:
- 工具使用: 代理决定何时将查询路由到适当的工具或依赖于自己的知识。
- 反思: 代理在响应用户之前审查和修正其答案。大多数LLM系统也可以添加反思步骤。
- 先推理后行动(ReAct): 代理通过推理逐步解决查询,执行一个动作,观察结果,并决定是否采取另一个动作或提供响应。
- 先计划后执行: 代理提前规划,将任务分解为子步骤(如果需要),然后执行每个步骤。
最后两个模式——ReAct和先计划后执行——通常是构建通用单代理的最佳起点。
常见代理模式概述。(作者提供的图片)
为了有效地实现这些行为,您需要进行一些提示工程。您可能还希望使用结构化生成技术。这基本上意味着将LLM的输出调整为匹配特定格式或模式,以便代理的响应与您所期望的沟通风格保持一致。
示例: 以下是来自蜜蜂代理框架的ReAct风格代理的系统提示摘录。
通信结构
您仅使用指令行进行沟通。格式为:“指令:预期输出”。您必须仅使用这些指令行,并且在指令行之间不得输入空行或其他内容。如果不需要调用函数,则必须跳过指令行的函数名称、函数输入和函数输出。
消息:用户的消息。您从不使用此指令行。
思考:回答用户消息的单行计划。必须立即跟随最终答案。
思考:回答用户消息的单行逐步计划。您可以使用上面定义的可用函数。此指令行必须立即跟随函数名称,如果需要调用上面定义的可用函数,或者跟随最终答案。这里不要提供答案。
函数名称:函数的名称。此指令行必须立即跟随函数输入。
函数输入:函数参数。空对象是有效参数。
函数输出:以JSON格式输出函数的结果。
思考:继续您的思考过程。
最终答案:回答用户或请求更多信息或澄清。它必须始终在思考之前。
示例
消息:你能把“How are you”翻译成法语吗?
思考:用户想把文本翻译成法语。我可以做到。
最终答案:Comment vas-tu?
第3步. 定义代理的核心指令
我们往往理所当然地认为LLMs自带一系列功能。有些功能很棒,但其他功能可能并不是您所需要的。为了获得您想要的性能,明确您希望在系统提示中包含的所有功能——以及不希望包含的功能——是很重要的。
这可能包括如下指令:
- 代理名称和角色: 代理的名称以及其预期的功能。
- 语气和简洁性: 应该多正式或随意,以及应该多简短。
- 何时使用工具: 决定何时依赖外部工具与模型自身知识之间的选择。
- 处理错误: 当工具或过程出现问题时,代理应该怎么做。
示例: 以下是蜜蜂代理框架中指令部分的一个片段。
## Instructions
User can only see the Final Answer, all answers must be provided there.
You must always use the communication structure and instructions defined above. Do not forget that Thought must be a single-line immediately followed by Final Answer.
You must always use the communication structure and instructions defined above. Do not forget that Thought must be a single-line immediately followed by either Function Name or Final Answer.
Functions must be used to retrieve factual or historical information to answer the message.
If the user suggests using a function that is not available, answer that the function is not available. You can suggest alternatives if appropriate.
When the message is unclear or you need more information from the user, ask in Final Answer.
你的能力
更倾向于使用这些能力而不是函数。
- 你理解这些语言:英语、西班牙语、法语。
- 你可以翻译和总结,甚至是长文档。
注意事项
- 如果你不知道答案,就说你不知道。
- 当前的时间和日期可以在最后一条消息中找到,格式为ISO。
- 回答用户时,使用友好的时间和日期格式。
- 使用markdown语法格式化代码片段、链接、JSON、表格、图像、文件。
- 有时候,事情并不会按计划进行。函数在最初几次尝试中可能无法提供有用的信息。在宣布问题无法解决之前,你应该始终尝试几种不同的方法。
- 当函数没有给你想要的答案时,你必须使用另一个函数或不同的函数输入。
- 使用搜索引擎时,你可以尝试查询的不同表述,甚至可能使用不同的语言。
- 在不使用函数的情况下,你无法进行复杂的计算、运算或数据处理。
第4步。定义和优化您的核心工具
工具赋予您的代理超级能力。通过一组狭窄且定义明确的工具,您可以实现广泛的功能。需要包含的关键工具有代码执行、网络搜索、文件读取和数据分析。
对于每个工具,您需要定义以下内容并将其作为系统提示的一部分:
- 工具名称: 该功能的唯一描述性名称。
- 工具描述: 对工具的功能及使用时机的清晰解释。这有助于代理确定何时选择正确的工具。
- 工具输入模式: 描述所需和可选参数、其类型及任何约束的模式。代理根据用户的查询使用此模式填写所需的输入。
- 指向运行工具的位置/方式的指针。
示例: 以下是来自 Langchain社区 的 Arxiv 工具实现的摘录。该实现需要一个 ArxivAPIWrapper 实现。
class ArxivInput(BaseModel):
"""Arxiv工具的输入。"""
query: str = Field(description="用于查找的搜索查询")
class ArxivQueryRun(BaseTool):
"""搜索Arxiv API的工具。"""
name: str = "arxiv"
description: str = (
"Arxiv.org的包装器 "
"当您需要回答有关物理、数学、 "
"计算机科学、定量生物学、定量金融、统计学、 "
"电气工程和经济学 "
"的科学文章时非常有用。 "
"输入应为搜索查询。"
)
api_wrapper: ArxivAPIWrapper = Field(default_factory=ArxivAPIWrapper)
args_schema: Type[BaseModel] = ArxivInput
def _run(
self,
query: str,
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""使用Arxiv工具。"""
return self.api_wrapper.run(query)
在某些情况下,您需要优化工具以获得所需的性能。这可能涉及通过一些提示工程调整工具名称或描述、设置高级配置以处理常见错误,或过滤工具的输出。
第5步. 决定内存处理策略
LLMs的上下文窗口有限——它们一次可以“记住”的标记数量。随着多轮对话中的过去互动、冗长的工具输出或代理所依赖的额外上下文等内容的增加,这个内存可能会很快填满。这就是为什么拥有一个可靠的内存处理策略至关重要。
内存, 在代理的上下文中,指的是系统存储、回忆和利用过去互动信息的能力。这使得代理能够随着时间的推移保持上下文,基于之前的交流改进其响应,并提供更个性化的体验。
常见的内存处理策略:
- 滑动内存: 保留最近的 k 次对话并丢弃较早的内容。
- 标记内存: 保留最近的 n 个标记并忘记其余的内容。
- 摘要内存: 使用LLM在每轮对话中总结内容并丢弃单独的消息。
此外,您还可以让LLM检测关键时刻以存储在长期内存中。这使得代理能够“记住”用户的重要事实,从而使体验更加个性化。
到目前为止,我们所讨论的五个步骤为设置代理奠定了基础。但是,如果我们在这个阶段通过LLM运行用户查询,会发生什么呢?
答案:您将得到原始文本输出。(图片由作者提供)
以下是可能的输出示例:
用户消息:从这个数据集中提取关键见解 文件:bill-of-materials.csv 思考:首先,我需要检查数据集的列并提供基本的数据统计信息。 函数名称:Python 函数输入:{“language”:“python”,“code”:“import pandas as pd\n\ndataset = pd.read_csv(‘bill-of-materials.csv’)\n\nprint(dataset.columns)\nprint(dataset.describe())”,“inputFiles”:[“bill-of-materials.csv”]} 函数输出:
此时,代理生成原始文本输出。那么我们如何让它实际执行下一步呢?这就是解析和调度发挥作用的地方。
第6步. 解析代理的原始输出
解析器是一个将原始数据转换为您的应用程序可以理解和处理的格式(例如具有属性的对象)的函数。
对于我们正在构建的代理,解析器需要识别我们在第2步中定义的通信结构,并返回结构化输出,例如JSON。这使得应用程序更容易处理和执行代理的下一步。
注意:一些模型提供者如OpenAI可以默认返回可解析的输出。对于其他模型,尤其是开源模型,这需要进行配置.
第7步. 协调代理的下一步
最后一步是设置协调逻辑。这决定了在LLM输出结果后会发生什么。根据输出,您将要么:
- 执行工具调用,或者
- 返回答案 — 要么是对用户查询的最终响应,要么是请求更多信息的后续请求。
扩展的单代理架构。(作者提供的图像)
如果触发了工具调用,工具的输出将被发送回LLM(作为其工作记忆的一部分)。然后,LLM将决定如何处理这些新信息:要么执行另一个工具调用,要么返回答案给用户。
以下是该协调逻辑在代码中可能的样子:
def orchestrator(llm_agent, llm_output, tools, user_query):
"""
根据LLM输出协调响应,并在必要时进行迭代。
参数:
- llm_agent (可调用): 处理工具输出的LLM代理函数。
- llm_output (dict): LLM的初始输出,指定下一个动作。
- tools (dict): 可用工具的字典及其执行方法。
- user_query (str): 原始用户查询。
返回:
- str: 对用户的最终响应。
"""
while True:
action = llm_output.get("action")
if action == "tool_call":
tool_name = llm_output.get("tool_name")
tool_params = llm_output.get("tool_params", {})
if tool_name in tools:
try:
tool_result = tools[tool_name](**tool_params)
llm_output = llm_agent({"tool_output": tool_result})
except Exception as e:
return f"执行工具'{tool_name}'时出错:{str(e)}"
else:
return f"错误:未找到工具'{tool_name}'。"
elif action == "return_answer":
return llm_output.get("answer", "未提供答案。")
else:
return "错误:无法识别的LLM输出动作类型。"
瞧! 您现在拥有一个能够处理各种用例的系统 — 从竞争分析和高级研究到自动化复杂工作流程。
多代理系统的作用是什么?
虽然这一代的LLMs功能强大,但它们有一个关键限制:它们在信息过载方面表现不佳。过多的上下文或过多的工具可能会让模型不堪重负,导致性能问题。通用的单一代理最终会遇到这个瓶颈,特别是因为代理通常对令牌的需求很高。
对于某些用例,多代理设置可能更为合理。通过在多个代理之间分配职责,可以避免单个LLM代理的上下文过载,并提高整体效率。
也就是说,通用的单代理设置是原型设计的绝佳起点。它可以帮助您快速测试用例,并识别问题开始出现的地方。通过这个过程,您可以:
- 理解任务的哪些部分真正受益于代理方法。
- 确定可以作为独立流程在更大工作流中分离的组件。
从单一代理开始,您可以获得宝贵的见解,以便在扩展到更复杂的系统时优化您的方法。
什么是开始的最佳方式?
准备好深入并开始构建了吗?使用框架可以是快速测试和迭代您的 LLM代理 配置的好方法。
您在构建通用代理方面有什么经验?在评论中分享您的想法!