Agent构建总结(LangChain)
跑通Agent的过程其实主要是在让LLM跑通以下链条:
有什么工具 → 当前需要用什么工具 → 结构化返回工具和参数 → 本地调用对应工具 → 得到最终结果
上述链条所有的实现均离不开prompt,因此我们可以将链条转换为以下两个问题:
- 问题一:如何在prompt中告知LLM可用工具及工具用途?
- 问题二:如何在prompt中约束LLM结构化返回工具和参数?
完成上述两个问题后Agent其实就可以完成单一工具的问题了,此时将会有一个新的问题:
- 问题三:如何在prompt中约束LLM有次序的选择工具完成复杂任务?
本篇文章将针对上述问题进行简单总结
注意:出于简洁便于理解的意图,本篇文章中大面积结合 LangChain 实现。
一. 如何在prompt中告知LLM可用工具及工具用途?
利用LangChain我们可以先将工具及其信息生成一个工具对象,这及便于管理也便于后续对工具的调用,如下:
from langchain.tools import StructuredTool
tool = StructuredTool.from_function(
func=func,
name="ToolName",
description="xxx",
)
参数解析:
func
传入我们已经实现的工具函数name=
对该工具的命名description
对该工具用途的描述
之后我们在写prompt的同时便可将工具及其信息一同写入,该步骤同样可用LangChain的接口实现。
from langchain_core.tools import render_text_description
with open(path, "r", encoding="utf-8") as f:
main_prompt = ChatPromptTemplate.from_messages(
[
HumanMessagePromptTemplate.from_template(f.read()),
]
).partial(
tools=render_text_description([tool_a, tool_b]),
)
render_text_description
的作用是解析我们提供的工具对象的列表,将其整理为文本的形式。
二. 如何在prompt中约束LLM结构化返回工具和参数?
与问题一类似,我们需要先利用pydantic创建一个类,该类用于描述对选择工具时输出的结构:
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
class Action(BaseModel):
name: str = Field(description="Tool name")
args: Optional[Dict[str, Any]] = Field(description="Tool input arguments, containing arguments names and values")
self.output_parser = PydanticOutputParser(pydantic_object=Action)
self.robust_parser = OutputFixingParser.from_llm(parser=self.output_parser, llm=self.llm)
action = self.robust_parser.parse(action if action else response)
tool.run(action.args)
参数解析:
name
为期望调用的工具名args
为调用该工具时期望传入的参数
同样的我们在写prompt时便利用LangChain轻松将该结构化信息写入
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
output_parser = PydanticOutputParser(pydantic_object=Action)
with open(path, "r", encoding="utf-8") as f:
self.main_prompt = ChatPromptTemplate.from_messages(
[
HumanMessagePromptTemplate.from_template(f.read()),
]
).partial(
format_instructions=self.output_parser.get_format_instructions(),
)
后续当LLM有返回结果时我们要进行以下几步操作:
- 修复返回结果(由于LLM的推理机制,因此输出不一定靠谱)
- 找到执行工具
from langchain.output_parsers import OutputFixingParser
robust_parser = OutputFixingParser.from_llm(parser=self.output_parser, llm=self.llm)
action = robust_parser.parse(action)
for tool in self.tools:
if tool.name == action.name:
observation = tool.run(action.args)
print(observation)
其中 OutputFixingParser
是LangChain提供的利用LLM本身修复结构化输出的类, robust_parser.parse()
之后将返回一个可调用的对象。
由于我们工具在问题一中已经生成工具对象,因此这里可以直接 tool.run(action.args)
执行工具。
三. 如何在prompt中约束LLM有次序的选择工具完成复杂任务?
想要完成多轮的工具选择我们就需要要求LLM在每次输出中包含除结构化信息外的内容:
- 对于问题的拆解和计划等
- 工具选择理由等
并将所有输出以短时记忆的形式写入prompt,再次给到LLM。
重复循环直到LLM认为当前任务已经彻底结束或超过我们预设的最大循环次数。
当然由于输出中包含了其他信息,因此我们需要对输出内容中的结构化信息进行解析等操作,就不在此赘述。
这里提供一个极其简易的prompt,仅供格式参考并无实际价值。
你是一个强大的智能体,你可以使用工具与指令独立自动化的解决问题
你的任务是:
{task}
可参考的资料文件均在以下目录:
{work_dir}
请始终保持该目录路径的完整。
你可以使用的工具如下:
{tools}
当前的任务执行记录:
<history>
{short_memory}
</history>
输出形式:
(1)首先,根据以下格式说明,输出你的思考过程:
**关键概念**:任务中涉及的组合型概念或实体。已经明确获得取值的关键概念,将其取值完整备注在概念后。
**概念拆解**:将任务中的关键概念拆解为一系列待查询的子要素。每个关键概念一行,后接这个概念的子要素,每个子要素一行,行前以 '-' 开始。已经明确获得取值的子概念,将其取值完整备注在子概念后。
**反思**:自我反思,观察以前的执行记录,一步步思考以下问题:
1. 当前已经获得了那些概念?
2. 还需要获取那些概念?
**计划**:详细列出当前动作的执行计划。只计划一步的动作。PLAN ONE STEP ONLY!
(2)最后,以JSON形式输出你选择执行的动作/工具
{format_instructions}
请确保每次选择工具前你都先以文字输出了你的思考分析过程。
请在每次**计划**后输出JSON格式的工具选择。
请确保你的工具选择(JSON)出现在输出的最后一部分。
请确保你输出的JSON代码块以```json\n\n```包裹。