LLM(大语言模型)「Agent」开发教程-LangChain(三)

v1.0官方文档|最新文档

一、LangChain入门开发教程:Model I/O

二、基于LangChain的RAG开发教程

LangChain是一个能够利用大语言模型(LLM,Large Language Model)能力进行快速应用开发的框架:

  • 高度抽象的组件,可以像搭积木一样,使用LangChain的组件来实现我们的应用
  • 集成外部数据到LLM中,比如API接口数据、文件、外部应用等;
  • 提供了许多可自定义的LLM高级能力,比如Agent、RAG等等;

LangChain框架主要由以下六个部分组成:

  1. Model IO:格式化和管理LLM的输入和输出
  2. Retrieval:检索,与特定应用数据交互,比如RAG,与向量数据库密切相关,能够实现从向量数据库中搜索与问题相关的文档来作为增强LLM的上下文
  3. Agents:决定使用哪个工具(高层指令)的结构体,而tools则是允许LLM与外部系统交互的接口
  4. Chains:构建运行程序的block-style组合,即能将多个模块连接起来,实现复杂的功能应用
  5. Memory:在运行一个链路(chain)时能够存储程序状态的信息,比如存储历史对话记录,随时能够对这些历史对话记录重新加载,保证长对话的准确性
  6. Callbacks:回调机制,可以追踪任何链路的步骤,记录日志

  • 该系列的第一篇文章介绍了LangChain Model I/O涉及的Prompts、LLMs、Chat model、Output Parser等概念及其使用,
  • 第二篇文章则介绍了RAG(Retrieval Augmented Generation,检索增强生成):在生成过程中,外部的数据会通过检索然后传递给LLM,让LLM能够利用这些新知识作为上下文。

今天这篇文章则要介绍另外一个核心且当前非常热门的组件:Agent

  1. 在前面两个章节,已经出现过了许多关于Chains的概念和实践例子了,Chains是一系列的动作(actions)序列,这些动作可以是请求一个LLM、执行检索召回或者调用一个本地函数,但这些动作序列是硬编码(hardcoded in code)的,即动作的执行顺序是固定的,编写在代码逻辑中。
  2. 而Agents则是使用一个LLM(大型语言模型,Large Language Model)从动作集合中进行选择,也就是基于LLM的推理引擎(reasoning engine)来决定选择哪些actions和执行顺序, 并且在agents中,这些actions一般都是tools

初识Tools

LangChain官方文档:https://python.langchain.com/v0.1/docs/modules/tools/

实例代码:tools_agents.ipynb

Tools是Agents与外部世界交互的一种接口,它由以下几种东西构成:

  1. 工具的名称
  2. 工具的描述,即描述工具是用来做什么的
  3. 工具的输入,需要描述工具需要哪些输入,JSON schema
  4. 工具对应的函数
  5. 工具执行的结果是否需要直接返回给用户(这个是LangChain独有的设计,对于Openai等是不需要的)

这些信息是十分有用且重要的,它们会作为Prompt的一部分去指示LLM选择哪些actions也即tools,并且从用户输入中提取tools的输入内容,然后调用tools对应的function,LangChain称之为action-taking systems,从LLM选择了一个工具并调用其函数,便是take that action。

因此工具的名称、描述和输入参数的描述需要十分清晰,能够被LLM理解。

下面,让我们来看看一个工具实例的这些上述内容:

from langchain_community.utilities import WikipediaAPIWrapper
from langchain_community.tools import WikipediaQueryRunapi_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=100)
tool = WikipediaQueryRun(api_wrapper=api_wrapper)
{'name': tool.name,'description': tool.description,'input': tool.args,'return_direct': tool.return_direct,}
"""
{'name': 'wikipedia','description': 'A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.','input': {'query': {'title': 'Query', 'type': 'string'}},'return_direct': False}
"""tool.run({"query": "langchain"})
"""
'Page: LangChain\nSummary: LangChain is a framework designed to simplify the creation of applications '
"""

可以看到一个工具定义的name、description、input json schema(输入参数)和return_direct(结果是否直接返回用户),以及调用工具对应的函数 tool.run

function call

OpenAI文档:https://platform.openai.com/docs/guides/function-calling

实例代码:tools_agents.ipynb

Agents是依托ChatGPT这类LLM的能力,而LangChain只是对其实现过程进行封装,便于开发者直接使用。因此,我们其实还是需要了解Agents脱离LangChain是如何运行的,或者说Agents原本是如何运行,这对我们使用Agents会有很大帮助。

更具体一点,Agents依托的是LLM的function call能力,在该系列的开篇 LangChain入门开发教程:Model I/O中提到了LLM的role包括system、assistant(AI)、user(human),但其实还有另外一种role:tool

下面我们仍然使用我们的老朋友通义千问来演示Agents的演示,因为它是完全兼容Openai SDK的,当然你也可以使用Openai,仅需要替换api_key和base_url即可。

from openai import OpenAI
from datetime import datetime
import json
import osclient = OpenAI(api_key=os.getenv("DASHSCOPE_API_KEY"), # 如果您没有配置环境变量,请在此处用您的API Key进行替换base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

定义工具列表

tools所需的基本要素仍然是一样的(除了LangChain自定义的return_direct):工具名称-name、工具描述-description、工具需要的参数描述-parameters、工具对应的函数-如get_current_time

# 定义工具列表,模型在选择使用哪个工具时会参考工具的name和description
tools = [# 工具1 获取当前时刻的时间{"type": "function","function": {"name": "get_current_time","description": "当你想知道现在的时间时非常有用。","parameters": {}  # 因为获取当前时间无需输入参数,因此parameters为空字典}},  # 工具2 获取指定城市的天气{"type": "function","function": {"name": "get_current_weather","description": "当你想查询指定城市的天气时非常有用。","parameters": {  # 查询天气时需要提供位置,因此参数设置为location"type": "object","properties": {"location": {"type": "string","description": "城市或县区,比如北京市、杭州市、余杭区等。"}}},"required": ["location"]}}
]# 模拟天气查询工具。返回结果示例:“北京今天是晴天。”
def get_current_weather(location):return f"{location}今天是雨天。 "# 查询当前时间的工具。返回结果示例:“当前时间:2024-04-15 17:15:18。“
def get_current_time():# 获取当前日期和时间current_datetime = datetime.now()# 格式化当前日期和时间formatted_time = current_datetime.strftime('%Y-%m-%d %H:%M:%S')# 返回格式化后的当前时间return f"当前时间:{formatted_time}。"

Agents迭代

# 封装模型响应函数
def get_response(messages):completion = client.chat.completions.create(model="qwen-plus",messages=messages,tools=tools)return completion.model_dump()def call_with_messages():print('\n')messages = [{"content": "杭州和北京天气怎么样?现在几点了?","role": "user"}]print("-"*60)# 模型的第一轮调用i = 1first_response = get_response(messages)assistant_output = first_response['choices'][0]['message']print(f"\n第{i}轮大模型输出信息:{first_response}\n")if  assistant_output['content'] is None:assistant_output['content'] = ""messages.append(assistant_output)# 如果不需要调用工具,则直接返回最终答案if assistant_output['tool_calls'] == None:  # 如果模型判断无需调用工具,则将assistant的回复直接打印出来,无需进行模型的第二轮调用print(f"无需调用工具,我可以直接回复:{assistant_output['content']}")return# 如果需要调用工具,则进行模型的多轮调用,直到模型判断无需调用工具while assistant_output['tool_calls'] != None:# 如果判断需要调用查询天气工具,则运行查询天气工具if assistant_output['tool_calls'][0]['function']['name'] == 'get_current_weather':tool_info = {"name": "get_current_weather", "role":"tool"}# 提取位置参数信息arguments = assistant_output['tool_calls'][0]['function']['arguments']if 'properties' in arguments:location = arguments['properties']['location']else:location = arguments['location']tool_info['content'] = get_current_weather(location)# 如果判断需要调用查询时间工具,则运行查询时间工具elif assistant_output['tool_calls'][0]['function']['name'] == 'get_current_time':tool_info = {"name": "get_current_time", "role":"tool"}tool_info['content'] = get_current_time()print(f"工具输出信息:{tool_info['content']}\n")print("-"*60)messages.append(tool_info)assistant_output = get_response(messages)['choices'][0]['message']if  assistant_output['content'] is None:assistant_output['content'] = ""messages.append(assistant_output)i += 1print(f"第{i}轮大模型输出信息:{assistant_output}\n")print(f"最终答案:{assistant_output['content']}")call_with_messages()
"""
------------------------------------------------------------第1轮大模型输出信息:{'id': 'chatcmpl-aaf6e713-622a-9a7c-9024-b3d5056830f9', 'choices': [{'finish_reason': 'tool_calls', 'index': 0, 'logprobs': None, 'message': {'content': '', 'role': 'assistant', 'function_call': None, 'tool_calls': [{'id': 'call_88713c110c0b4442af9039', 'function': {'arguments': '{"properties": {"location": "杭州市"}}', 'name': 'get_current_weather'}, 'type': 'function', 'index': 0}]}}], 'created': 1721742531, 'model': 'qwen-plus', 'object': 'chat.completion', 'system_fingerprint': None, 'usage': {'completion_tokens': 21, 'prompt_tokens': 230, 'total_tokens': 251}}工具输出信息:杭州市今天是雨天。 ------------------------------------------------------------
第2轮大模型输出信息:{'content': '', 'role': 'assistant', 'function_call': None, 'tool_calls': [{'id': 'call_e302c8e9f6f64cdebe2720', 'function': {'arguments': '{"properties": {"location": "北京市"}}', 'name': 'get_current_weather'}, 'type': 'function', 'index': 0}]}工具输出信息:北京市今天是雨天。 ------------------------------------------------------------
第3轮大模型输出信息:{'content': '', 'role': 'assistant', 'function_call': None, 'tool_calls': [{'id': 'call_a91a2b8fe5624c728bca82', 'function': {'arguments': '{}', 'name': 'get_current_time'}, 'type': 'function', 'index': 0}]}工具输出信息:当前时间:2024-07-23 21:48:53。------------------------------------------------------------
第4轮大模型输出信息:{'content': '目前,杭州市和北京市的天气都是雨天。现在的具体时间是2024年07月23日21点48分53秒。请记得带伞出行!', 'role': 'assistant', 'function_call': None, 'tool_calls': None}最终答案:目前,杭州市和北京市的天气都是雨天。现在的具体时间是2024年07月23日21点48分53秒。请记得带伞出行!
"""
  1. 在普通的LLM completion调用的基础上,将要素齐全的tools传递给LLM
  2. LLM会自己决定是否调用某一个tool,其信息存储在tool_calls中,包括tool的名称、tool需要的实参。如果tool_calls=None,则不需要调用tool
  3. 在Agents的迭代过程中,每一次function call的结果需要拼接在message列表,其role为上述的tool,content为tool对应的function执行结果
  4. 然后继续传递给LLM,来LLM来决定是否结束function call迭代,即可以将内容返回给用户了,或者继续循环上述的function call过程

LangChain Tools

实例代码:tools_agents.ipynb

接下来,我们再看看如何使用LangChain的封装来实现上一个章节同样功能的Agents迭代过程。

自定义工具

首先仍然是定义工具,在LangChain中,它提供了许多种自定义tools的方式。

1. @tool decorator

# Import things that are needed generically
from langchain.pydantic_v1 import BaseModel, Field
from langchain.tools import BaseTool, StructuredTool, tool@tool
def get_current_weather(location: str) -> str:"""当你想查询指定城市的天气时非常有用。"""return f"{location}今天是雨天。"get_current_weather.name, get_current_weather.description, get_current_weather.args
"""
('get_current_weather','get_current_weather(location: str) -> str - 当你想查询指定城市的天气时非常有用。',{'location': {'title': 'Location', 'type': 'string'}})
"""

这种方法的缺点是无法添加工具参数的描述。

2. tool decorator with JSON args

tool装饰器其实是支持传参的,我们可以通过pydantic定义tool的输入,加上输入的description,传参给tool装饰器。

class InputSchema(BaseModel):location: str = Field(description="城市或县区,比如北京市、杭州市、余杭区等。")@tool("get_current_weather", args_schema=InputSchema)
def get_current_weather(location: str):"""当你想查询指定城市的天气时非常有用。"""return f"{location}今天是雨天。"get_current_weather.name, get_current_weather.description, get_current_weather.args
"""
('get_current_weather','get_current_weather(location: str) - 当你想查询指定城市的天气时非常有用。',{'location': {'title': 'Location','description': '城市或县区,比如北京市、杭州市、余杭区等。','type': 'string'}})
"""

3. BaseTool的子类

继承BaseTool,重写_run函数,_run便对应tool的function call

from typing import Optional, Typefrom langchain.callbacks.manager import (AsyncCallbackManagerForToolRun,CallbackManagerForToolRun,
)class InputSchema(BaseModel):location: str = Field(description="城市或县区,比如北京市、杭州市、余杭区等。")class GetCurrentWeatherTool(BaseTool):name = "get_current_weather"description = "当你想查询指定城市的天气时非常有用。"args_schema: Type[BaseModel] = InputSchemadef _run(self, location: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:"""Use the tool."""return f"{location}今天是雨天。"get_current_weather = GetCurrentWeatherTool()
get_current_weather.name, get_current_weather.description, get_current_weather.args
"""
('get_current_weather','当你想查询指定城市的天气时非常有用。',{'location': {'title': 'Location','description': '城市或县区,比如北京市、杭州市、余杭区等。','type': 'string'}})
"""

4. StructuredTool

通过StructuredTool的from_function方法来构建tool

class InputSchema(BaseModel):location: str = Field(description="城市或县区,比如北京市、杭州市、余杭区等。")def get_current_weather_func(location: str):"""当你想查询指定城市的天气时非常有用。"""return f"{location}今天是雨天。"get_current_weather = StructuredTool.from_function(func=get_current_weather_func,name="get_current_weather",description="当你想查询指定城市的天气时非常有用。",args_schema=InputSchema
)get_current_weather.name, get_current_weather.description, get_current_weather.args
"""
('get_current_weather','get_current_weather(location: str) - 当你想查询指定城市的天气时非常有用。',{'location': {'title': 'Location','description': '城市或县区,比如北京市、杭州市、余杭区等。','type': 'string'}})
"""

LangChain function call

第一步,仍然需要定义工具。 同样使用前面的例子来我们使用上一小节的方法tool装饰器来定义工具:

class WeatherSchema(BaseModel):location: str = Field(description="城市或县区,比如北京市、杭州市、余杭区等。")@tool("get_current_weather", args_schema=WeatherSchema)
def get_current_weather(location: str):"""当你想查询指定城市的天气时非常有用。"""return f"{location}今天是雨天。"# 查询当前时间的工具。返回结果示例:“当前时间:2024-04-15 17:15:18。“
@tool("get_current_time")
def get_current_time():"""当你想知道现在的时间时非常有用。"""# 获取当前日期和时间current_datetime = datetime.now()# 格式化当前日期和时间formatted_time = current_datetime.strftime('%Y-%m-%d %H:%M:%S')# 返回格式化后的当前时间return f"当前时间:{formatted_time}。"

第二步,将自定义的LangChain工具转化为OpenAI接口要求的格式

  • LangChain1.x许多涉及function calling及tools相关的实现要么对应OpenAI的旧实现(Legacy):新的都是使用tools参数返回tool_calls,旧的是用function参数返回function_calls,这点需要注意,许多其他的LLM都是仅支持tools的
  • 要么就根本解析没有function call的返回结果

在这里,我将这些旧实现进行转换(主要转换代码在tools&chat_model),可以更好地理解整个过程。当然,你可以直接使用LangChain2.x,这是更好的解决方法。

from tongyi.function_calling import convert_to_openai_toolfunctions = [get_current_weather, get_current_time]
tools = [convert_to_openai_tool(t) for t in functions]
tools
"""
[{'type': 'function','function': {'name': 'get_current_weather','description': 'get_current_weather(location: str) - 当你想查询指定城市的天气时非常有用。','parameters': {'type': 'object','properties': {'location': {'description': '城市或县区,比如北京市、杭州市、余杭区等。','type': 'string'}},'required': ['location']}}},{'type': 'function','function': {'name': 'get_current_time','description': 'get_current_time() - 当你想知道现在的时间时非常有用。','parameters': {'type': 'object', 'properties': {}}}}]
"""

第三步,可以直接使用LangChain的LCEL调用。我们先来看看加入tools之后,LLM的返回变成了什么:

(如上所述,这里的LLM实现比如Tongyi也需要进行改造,来兼容其function call接口)

如下代码所示,LangChain只是将原生function call的返回进行包装,将其放在了AIMessage.tool_calls,仍然包括LLM选择的工具名称和所需参数。

from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from tongyi.chat_model import CustomChatTongyimodel = CustomChatTongyi(model='qwen-plus')messages = [HumanMessage(content="杭州和北京天气怎么样?现在几点了?")]assistant_output = model.invoke(messages, tools=tools)
assistant_output
"""
AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'name': 'get_current_weather', 'arguments': '{"location": "杭州市"}'}, 'index': 0, 'id': 'call_c72afbea224b4de6bc3957', 'type': 'function'}]}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'tool_calls', 'request_id': '6ee616af-aa17-9600-bd04-8bcaf00e2f56', 'token_usage': {'input_tokens': 261, 'output_tokens': 18, 'total_tokens': 279}}, id='run-63bc17f2-8f36-41c2-b890-96d3a7ab7376-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '杭州市'}, 'id': 'call_c72afbea224b4de6bc3957'}])
"""

最后一步,迭代整个function call过程,直到不再需要调用工具,将LLM的最终结果进行返回。

functions_to_call = {'get_current_weather': get_current_weather, 'get_current_time': get_current_time}while assistant_output.tool_calls:messages.append(assistant_output)for tool in assistant_output.tool_calls:args = tool['args'] if 'properties' not in tool['args'] else tool['args']['properties']tool_content = functions_to_call[tool['name']].invoke(args)messages.append(ToolMessage(name=tool['name'], tool_call_id=tool['id'], content=tool_content))print(f"工具输出信息:{tool_content}\n")print("-"*60)assistant_output = model.invoke(messages, tools=tools)print(f"大模型输出信息:{assistant_output}\n")
"""
工具输出信息:杭州市今天是雨天。------------------------------------------------------------
大模型输出信息:content='' additional_kwargs={'tool_calls': [{'function': {'name': 'get_current_weather', 'arguments': '{"location": "北京市"}'}, 'index': 0, 'id': 'call_6dfa56e25ab84322b5cc2f', 'type': 'function'}]} response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'tool_calls', 'request_id': '88873f57-7d98-962f-af86-7e4b3e2c9a80', 'token_usage': {'input_tokens': 294, 'output_tokens': 20, 'total_tokens': 314}} id='run-6d079aaa-9469-4be9-9c22-7a0f97e9dd85-0' tool_calls=[{'name': 'get_current_weather', 'args': {'location': '北京市'}, 'id': 'call_6dfa56e25ab84322b5cc2f'}]工具输出信息:北京市今天是雨天。------------------------------------------------------------
大模型输出信息:content='' additional_kwargs={'tool_calls': [{'function': {'name': 'get_current_time', 'arguments': '{"properties": {}}'}, 'index': 0, 'id': 'call_97f6441d9b9d4503ae2dea', 'type': 'function'}]} response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'tool_calls', 'request_id': '62145e44-b81a-9bf9-aa5f-26f76f598fd3', 'token_usage': {'input_tokens': 330, 'output_tokens': 19, 'total_tokens': 349}} id='run-e8cbd101-d23e-48e5-8b20-24a4e88589d2-0' tool_calls=[{'name': 'get_current_time', 'args': {'properties': {}}, 'id': 'call_97f6441d9b9d4503ae2dea'}]工具输出信息:当前时间:2024-07-26 12:16:34。------------------------------------------------------------
大模型输出信息:content='目前杭州市和北京市的天气都是雨天。现在的时间是2024年7月26日12点16分34秒。记得带伞出门哦!' response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'e7c81621-8e39-981a-8a9f-de3f2a172106', 'token_usage': {'input_tokens': 380, 'output_tokens': 42, 'total_tokens': 422}} id='run-d5d14038-4454-4ad5-9375-c4f1384e0b70-0'
"""

bing tools

在上一个小节中,我们是通过将工具列表传递给invoke函数的**kwargs,最终会传递到LLM的tools参数,去调用Tongyi的原生接口,来实现function call的功能。

但这似乎不够"LangChain",其实可以将LLM和tools进行绑定(bind),这样我们也无需显式地将LangChain形式的tools转化为OpenAI形式

model_with_tools = model.bind_tools(functions)messages = [HumanMessage(content="杭州和北京天气怎么样?现在几点了?")]assistant_output = model_with_tools.invoke(messages)
assistant_output
"""
AIMessage(content='', additional_kwargs={'tool_calls': [{'function': {'name': 'get_current_weather', 'arguments': '{"location": "杭州市"}'}, 'index': 0, 'id': 'call_72d4e08d6ddc4fe5974f72', 'type': 'function'}]}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'tool_calls', 'request_id': '86640a08-c7fd-9c0a-a350-0deb481bf5d1', 'token_usage': {'input_tokens': 261, 'output_tokens': 18, 'total_tokens': 279}}, id='run-c663249c-2967-41e6-a695-88269dcc4446-0', tool_calls=[{'name': 'get_current_weather', 'args': {'location': '杭州市'}, 'id': 'call_72d4e08d6ddc4fe5974f72'}])
"""

Agents

LangChain官网文档:https://python.langchain.com/v0.1/docs/modules/agents/

实例代码:tools_agents.ipynb

其实上一个章节对于tools的介绍,基本上是已经涵盖了agents的整个过程了,包括:

  • 对LLM对tools的选择(如下图-[function call])
  • tools执行,然后把执行结果拼接到Prompt中,继续调用LLM的迭代过程(如下图-[agents])。

function call

agents

但是,这个过程LangChain也进行了封装了,我们下面来看看。

第一步,设计提示词模板。

这里加入了chat_history来存放对轮对话历史。还有,可以根据需要加入SystemMessage来保持LLM的人设,指导LLM更精准的回复。

from langchain.prompts import (ChatPromptTemplate,PromptTemplate,HumanMessagePromptTemplate,MessagesPlaceholder,
)prompt = ChatPromptTemplate.from_messages([MessagesPlaceholder(variable_name='chat_history', optional=True),HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], template='{input}')),MessagesPlaceholder(variable_name='agent_scratchpad')])

**第二步,跟前面一样,自定义工具列表。**这里便不再重复。

tools = [get_current_weather, get_current_time]

**第三步,创建自己的Agent。**主要是通过create_tool_calling_agent将工具与LLM进行绑定,并构建LLM调用和tools call的解析执行的chain。

from langchain.agents import create_tool_calling_agent
from langchain.agents import AgentExecutorfrom tongyi.chat_model import CustomChatTongyimodel = CustomChatTongyi(model='qwen-plus')agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

最后,便可以直接使用LangChain的LCEL调用

AgentExecutor的invoke仅支持stream模式,因此需要LLM支持流式模式的function call,不然便需要在_stream方法中进行改造,比如通义千问就不支持流式的function call,改造详见本仓库代码。

agent_executor.invoke({'input': '杭州和北京天气怎么样?现在几点了?'})
"""
> Entering new AgentExecutor chain...Invoking: `get_current_weather` with `{'location': '杭州市'}`杭州市今天是雨天。
Invoking: `get_current_weather` with `{'location': '北京市'}`北京市今天是雨天。
Invoking: `get_current_time` with `{}`当前时间:2024-07-30 15:35:17。目前杭州市和北京市的天气都是雨天。现在的时间是2024年7月30日15点35分17秒。记得带伞出门哦!> Finished chain.{'input': '杭州和北京天气怎么样?现在几点了?','output': '目前杭州市和北京市的天气都是雨天。现在的时间是2024年7月30日15点35分17秒。记得带伞出门哦!'}
"""

结构化Agent

前面提到,Agents的整个迭代过程如下:

agents

其中关键的一步便是LLM来决定是否调用工具,一般来说,包括前面出现的全部使用方法,都是基于使用的LLM支持function call,即将工具列表信息传递给LLM的tools参数。

但是,LangChain还另外支持了结构化的Agents实现方式,则不依赖于LLM的function call,可以通过对Prompt的设计来达到同样的效果,这能够让不支持function call的LLM实现Agents的效果

不过目前主流大模型都支持function call,因此下面便简单介绍一下即可。

XML Agent

我们以XML Agent为例来介绍结构化Agent(其他还包括:JSON Chat Agent、Structured chat、Self-ask with search)

这些结构化Agent与上面使用的Agent的区别在于下图红框部分:如何从LLM回复中提取tool call信息,再将tool执行结果进行整理,拼接到原来的Prompt中,继续传到LLM。

在LangChain代码中,Agent仍然被设计成Chain的形式(开头已经提及Agent和Chain的区别),包括四部分,如下图所示:

  • prompt:提示词模板
  • llm_with_tools:与tools绑定后的LLM
  • ToolsAgentOutputParser:对LLM的回复内容进行解析
  • RunnablePassthrough:将ToolsAgentOutputParser解析之后的内容进行整理拼接到Prompt中

create_tool_calling_agent

而整个迭代过程是在AgentExecutor._call中。

因此,XML Agent需要调整的部分是在创建Chain形式的Agent,即create_xml_agent,而不是AgentExecutor,如下图所示:

# Construct the XML agent
agent = create_xml_agent(llm, tools, prompt)

create_xml_agent

第一部分,Prompt的设计,下面是LangChain官方提供的一个提示词:

You are a helpful assistant. Help the user answer any questions.You have access to the following tools:{tools}In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags. You will then get back a response in the form <observation></observation>
For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond:<tool>search</tool><tool_input>weather in SF</tool_input>
<observation>64 degrees</observation>When you are done, respond with a final answer between <final_answer></final_answer>. For example:<final_answer>The weather in SF is 64 degrees</final_answer>Begin!Previous Conversation:
{chat_history}Question: {input}
{agent_scratchpad}

第二部分,LLM绑定的则不再是tools,而是上图-[create_xml_agent]中的bing方式,会以</tool_input>作为结果进行截断,因为根据Prompt可以看出LLM会以</tool_input>标签来存储工具的参数;

第三部分,则是LLM的回复内容解析 XMLAgentOutputParser 其实主要就是对选择工具</tool>、工具参数</tool_input>,还是最终回复</final_answer>这几个标签的提取;

第四部分,便是LLM选择的工具的函数调用,这个中间结果(包括工具执行结果)会存放在intermediate_steps中;

最后,则是RunnablePassthrough中使用format_xml函数来对工具调用的中间结果即intermediate_steps进行解析和提取,拼接到Prompt中(写入到agent_scratchpad占位变量),接着去继续请求LLM,进行Agent的迭代。

model = CustomChatTongyi(model='qwen-plus')agent = create_xml_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True)agent_executor.invoke({'input': '杭州和北京天气怎么样?现在几点了?'})
"""
{'input': '杭州和北京天气怎么样?现在几点了?','output': '杭州和北京今天的天气都是雨天。现在的时间是2024年7月31日16点59分52秒。','intermediate_steps': [(AgentAction(tool='get_current_weather', tool_input='杭州', log='<tool>get_current_weather</tool><tool_input>杭州'),'杭州今天是雨天。'),(AgentAction(tool='get_current_weather', tool_input='北京', log='<tool>get_current_weather</tool><tool_input>北京'),'北京今天是雨天。'),(AgentAction(tool='get_current_time', tool_input='', log='<tool>get_current_time</tool><tool_input>'),'当前时间:2024-07-31 16:59:52。')]}
"""

PS:LangChain对无参数的函数工具支持并不好,会抛出异常,并且在LangChain2.x仍未修复,我已经提了PR,看官方要不要merge了。或者你可以直接使用本仓库的代码,对tools的基类StructuredTool进行改正。

Agent迭代器

上面的例子,Agent的调用都是直接输出最终结果(虽然包含了中间调用的日志)。但其实是可以使用Agent迭代器来循环整个过程的,这样的话,可控性更高,可以对一些中间步骤进行校验或者校正,减少出现重复调用或者调用链路错误等异常情况。

如下代码,上面也提到了LLM选择tools和其执行结果都是存放在intermediate_step中,我们可以根据实际情况对其进行校验。而最终结果则存放在output

for i, step in enumerate(agent_executor.iter({'input': '杭州和北京天气怎么样?现在几点了?'})):print(f'Agent的第 {i+1} 步输出:')if output := step.get("intermediate_step"):action, value = output[0]print(f'\taction: {action}')print(f'\tvalue: {value}')else:print(step['output'])"""
Agent的第 1 步输出:action: tool='get_current_weather' tool_input={'location': '杭州市'} log="\nInvoking: `get_current_weather` with `{'location': '杭州市'}`\n\n\n"......'value: 杭州市今天是雨天。
Agent的第 2 步输出:action: tool='get_current_weather' tool_input={'location': '北京市'} log="\nInvoking: `get_current_weather` with `{'location': '北京市'}`\n\n\n"......'value: 北京市今天是雨天。
Agent的第 3 步输出:action: tool='get_current_time' tool_input={'properties': {}} log="\nInvoking: `get_current_time` with `{'properties': {}}`\n\n\n"......'value: 当前时间:2024-08-01 08:04:33。
Agent的第 4 步输出:
目前杭州市和北京市的天气都是雨天。现在的时间是2024年8月1日8点04分33秒。请记得带伞出门哦!
"""

结构化输出

截止到这里,Agent的最终输出都是一个字符串,那么我们是否也可以像前面的文章提到的Output Parsers那样,对Agent的输出也进行结构化转化,比如存储在一个数据对象中?

答案,当然是肯定的,这无疑对下流任务的开发对接提供了极大的便捷。

下面,我们继续以询问天气和时间为例子,前面没有进行特殊处理,Agent最终返回了目前杭州市和北京市的天气都是雨天。现在的时间是2024年8月1日8点04分33秒。请记得带伞出门哦!,接下来,我们便让它以json的格式返回。

(这里可能有人会有疑问,为什么不直接在Prompt加入让LLM返回json格式的提示词呢?其实如果每次都是询问天气和时间,那么是可以这么做的,当我们是使用Agent,即并不是每一次都是询问天气和时间,可能是其他的问题或者聊天)

第一步,仍然是定义天气和时间的tools,如上面的例子无异,不再重复。

第二步,定义触发结构化返回天气和时间的工具。这一步是核心思想,可以看出,我们仍然是以工具的形式来实现这个做法:

  • 即如果LLM调用了Response这个工具,那么意味着用户的输入是关于天气和时间的,我们便退出Agent的迭代过程,并且天气和时间的字段提取是完全交给LLM,而不需要我们自己去处理。
  • 而如果是调用其他工具或者无需调用工具,便交由正常的逻辑parse_ai_message_to_tool_action去处理,这个是我们最普通的Agent创建,即create_tool_calling_agentToolsAgentOutputParser的处理方法。
from typing import List, Dictfrom langchain_core.agents import AgentActionMessageLog, AgentFinish, AgentAction
from langchain.agents.output_parsers.tools import parse_ai_message_to_tool_action
from langchain_core.pydantic_v1 import BaseModel, Fieldclass Response(BaseModel):"""最终的回答如果包含天气、时间等信息,则需要调用该工具"""weather: Dict[str, str] = Field(description='最终的回答中不同城市对应的天气,比如北京的天气为阴天,则返回`{"北京": "阴天"}`。如果不存在天气信息则忽略', default={})current_time: str = Field(description="最终的回答中的当前时间,如果不存在则忽略", default="")def parse(output):name = None# 判断调用的工具是否为 Responseif output.tool_calls:tool_calls = output.tool_calls[0]name = tool_calls["name"]inputs = tool_calls["args"]elif output.additional_kwargs.get("tool_calls"):tool_calls = output.additional_kwargs["tool_calls"][0]name = tool_calls["name"]inputs = tool_calls["args"]if name == "Response":return AgentFinish(return_values=inputs, log=str(tool_calls))return parse_ai_message_to_tool_action(output)

第三步,定义Agent的Chain,前面也提到了,在LangChain代码中,Agent是以Chain的形式来实现的。这在上面[结构化Agent]小节中也重点介绍了这个Chain。

重要的一步,是要把Response工具也绑定到LLM,即model.bind_tools(tools + [Response])

# prompt & tools的定义在上面[Agents-Quickstart]小节
from langchain.agents.format_scratchpad import format_to_tool_messages
from langchain.agents import AgentExecutor
from tongyi.chat_model import CustomChatTongyimodel = CustomChatTongyi(model='qwen-plus')
llm_with_tools = model.bind_tools(tools + [Response])agent = ({"input": lambda x: x["input"],# Format agent scratchpad from intermediate steps"agent_scratchpad": lambda x: format_to_tool_messages(x["intermediate_steps"]),}| prompt| llm_with_tools| parse
)

最后,便是AgentExecutor即Agent迭代过程的创建和调用了。这里可以看到,因为我们自定义了Response工具的特殊逻辑,因此我们不需要再把Response工具加入Agent的迭代过程。

结果也符合预期,以json的形式返回时间和天气。

agent_executor = AgentExecutor(tools=tools, agent=agent, verbose=True)agent_executor.invoke({"input": "杭州和北京天气怎么样?现在几点了?"},return_only_outputs=True,
)
"""
{'current_time': '2024-08-01 22:19:30', 'weather': {'杭州市': '雨天', '北京市': '雨天'}}
"""

总结

  1. 这篇文章我们介绍了Agent这个热门的概念,以及它是如何依托LLM的function call来实现的
  2. 接着,再展示了借助LangChain的封装来简化这个开发过程,
  3. 并且还介绍了另外一种不依赖function call的Agent实现。

完整代码

llms/langchain_tutorial

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/393084.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智能仪表板DevExpress Dashboard v24.1 - 新增级联参数过滤

使用DevExpress Analytics Dashboard&#xff0c;再选择合适的UI元素&#xff08;图表、数据透视表、数据卡、计量器、地图和网格&#xff09;&#xff0c;删除相应参数、值和序列的数据字段&#xff0c;就可以轻松地为执行主管和商业用户创建有洞察力、信息丰富的、跨平台和设…

揭秘LoRA:利用深度学习原理在Stable Diffusion中打造完美图像生成的秘密武器

文章目录 引言LoRA的原理LoRA在角色生成中的应用LoRA在风格生成中的应用LoRA在概念生成中的应用LoRA在服装生成中的应用LoRA在物体生成中的应用结论 引言 在生成式人工智能领域&#xff0c;图像生成模型如Stable Diffusion凭借其出色的生成效果和广泛的应用场景&#xff0c;逐…

NVIDIA Triton系列03-开发资源说明

NVIDIA Triton系列03-开发资源说明 大部分要学习 Triton 推理服务器的入门者&#xff0c;都会被搜索引擎或网上文章引导至官方的 https://developer.nvidia.com/nvidia-triton-inference-server 处&#xff08;如下截图&#xff09;&#xff0c;然后从 “Get Started” 直接安…

Google四年推迟两次,Cookie不弃了,但也不藏了

四年两次推迟&#xff0c;这段改变了数字广告生态系统发展的代码&#xff0c;还是被Google保留了下来。2020年&#xff0c;Google第一次提出&#xff0c;将在2022年初结束Cookie的使用&#xff0c;同步推出隐私沙盒计划&#xff1b;2021年6月&#xff0c;Google第一次进行了延迟…

人脸识别Arcface的Tensorrt C++

代码已经上传至github&#xff0c;欢迎使用&#xff0c;不是为了研究人脸识别&#xff0c;而是为了实现Tensorrt部署Arcface模型&#xff0c;推理耗时33ms左右~ GitHub - Broad-sky/face-recognition-arcface-tensort: This project mainly implements the transplantation of…

50etf期权行权采用什么交割方式 ?

50ETF期权是欧式期&#xff0c;要到期日当天才能行权交制&#xff0c;其交割方式是实物交割买卖双方在到期行权日时需要准备一手交钱&#xff0c;一手收货或是一手交&#xff0c;一手收钱&#xff0c;如果持有期权到达到期日之前&#xff0c;投资者认为行权并不划算&#xff0c…

Linux 照片图像编辑器

前言 照片图像编辑器是一种软件程序,它允许用户对数字照片或图像进行各种编辑和修改。以下是一些常见的功能及其解释: 裁剪与旋转 : 裁剪:移除图像的某些部分,以改善构图或符合特定尺寸要求。旋转:改变图像的方向,可以校正歪斜的照片或者为了艺术效果而旋转。调整亮度…

【画流程图工具】

画流程图工具 draw.io draw.io&#xff08;现称为 diagrams.net&#xff09;是一款在线图表绘制工具&#xff0c;可以用于创建各种类型的图表&#xff0c;如流程图、网络图、组织结构图、UML图、思维导图等。以下是关于它的一些优点、应用场景及使用方法&#xff1a; 优点&a…

密码学基础-身份认证

密码学基础-身份认证 概述 书信的亲笔签名&#xff1b;公文、证书的印章起到了核准、认证的功能。 如前文密码学基础-数据加密所述&#xff0c;信息安全少不了身份认证的话题。只有认证了信息的来源&#xff0c;我们才能知道这条信息是否是正确的&#xff0c;合法的&#xff…

如何在linux系统上安装tomcat应用程序?

1&#xff09;首先查看安装包信息 yum info tomcat yum info tomcat 2&#xff09;安装 yum -y install tomcat yum -y install tomcat 3&#xff09;查看安装是否成功 rpm -q tomcat rpm -q tomcat 4&#xff09;如果输出一下内容则代表安装成功 tomcat-7.0.76-16.el7_9.n…

力扣高频SQL 50题(基础版)第三十八题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十八题1484.按日期分组销售产品题目说明实现过程准备数据实现方式结果截图总结 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十八题 1484.按日期分组销售产品 题目说明 表 Activities&#xff1a; ---…

Python的100道经典练习题,每日一练,必成大神!!!

Python的100道经典练习题是一个广泛而深入的学习资源&#xff0c;可以帮助Python初学者和进阶者巩固和提升编程技能 完整的100多道练习题可在下面图片免沸获取哦~ 整理了100道Python的题目&#xff0c;如果你是一位初学者&#xff0c;这一百多道题可以 帮助你轻松的使用Python…

新书《计算机视觉从入门到进阶实战:基于Pytorch》

本书基于PyTorch深度学习框架&#xff0c;结合计算机视觉中的主流任务&#xff0c;介绍了深度学习相关算法的计算机视觉上的应用。 本书主要内容分为两部分。 第一部分为PyTorch框架使用的相关知识&#xff0c;以及计算机视觉和深度学习的入门知识。第二部分重点介绍深度学习在…

C++——多态经典案例(三)计算器

案例&#xff1a;使用多态实现一个简单的计算器&#xff0c;计算两个数的加减乘除结果 分析&#xff1a;定义一个抽象类AbstractCalc &#xff0c;其内部定义一个纯虚函数getResult&#xff0c;用于得到计算结果 定义加减乘除四个类&#xff0c;分别继承这个抽象类AbstractCal…

【面试题】【简历版】完整版

一、Java 基础 java 面向对象特性 封装&#xff08;Encapsulation&#xff09;&#xff1a; public class Student {// 将name和age封装起来private String name;private int age;// 提供方法设置和获取这些属性public void setName(String name){this.name name;}public Str…

建议收藏!免费素材管理软件,设计师必备工具

前言 在设计的世界里&#xff0c;素材管理无疑是一项既重要又繁琐的任务。设计师们常常面临着海量素材的整理、分类和检索问题&#xff0c;这不仅消耗了大量的时间和精力&#xff0c;也常常因为素材的杂乱无章而影响创作灵感的涌现。因此&#xff0c;寻找一款能够解决这些痛点…

python实现小游戏随机猜数

1、脚本练习 import random# 初始化剩余的猜测次数 counts 3 # 生成一个1到10之间的随机整数 numb random.randint(1, 10)# 循环直到猜测次数用完 while counts > 0:tmp input("请输入小鱼手里的数字 (你还剩下 {} 次机会): ".format(counts))guess int(tmp)…

SemanticKernel/C#:使用Ollama中的对话模型与嵌入模型用于本地离线场景

前言 上一篇文章介绍了使用SemanticKernel/C#的RAG简易实践&#xff0c;在上篇文章中我使用的是兼容OpenAI格式的在线API&#xff0c;但实际上会有很多本地离线的场景。今天跟大家介绍一下在SemanticKernel/C#中如何使用Ollama中的对话模型与嵌入模型用于本地离线场景。 开始…

LVS部署DR集群

介绍 DR&#xff08;Direct Routing&#xff09;&#xff1a;直接路由&#xff0c;是LVS默认的模式&#xff0c;应用最广泛. 通过为请求报文重新封装一个MAC首部进行转发&#xff0c;源MAC是DIP所在的接口的MAC&#xff0c;目标MAC是某挑选出的RS的RIP所在接口的MAC地址. 整个…

生物信息学入门:Linux学习指南

还没有使用过生信云服务器&#xff1f;快来体验一下吧 20核心256G内存最低699元半年。 更多访问 https://ad.tebteb.cc 介绍 大家好&#xff01;作为一名生物信息学的新人&#xff0c;您可能对Linux感到陌生&#xff0c;但别担心&#xff0c;本教程将用简单明了的方式&#xff…