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

news2025/1/10 2:21:12

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 WikipediaQueryRun

api_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 os

client = 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 = 1
    first_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 += 1
        print(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, Type

from 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] = InputSchema

    def _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_tool

functions = [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 CustomChatTongyi

model = 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 AgentExecutor

from tongyi.chat_model import CustomChatTongyi

model = 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, Dict

from 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, Field


class Response(BaseModel):
    """最终的回答如果包含天气、时间等信息,则需要调用该工具"""

    weather: Dict[str, str] = Field(
        description='最终的回答中不同城市对应的天气,比如北京的天气为阴天,则返回`{"北京": "阴天"}`。如果不存在天气信息则忽略', default={})
    current_time: str = Field(
        description="最终的回答中的当前时间,如果不存在则忽略", default="")


def parse(output):
    name = None
    # 判断调用的工具是否为 Response
    if 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 CustomChatTongyi

model = 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.coloradmin.cn/o/1986741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享一个基于微信小程序的流浪动物救助领养平台springboot(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

C++基础编程100题-035 OpenJudge-1.4-16 三角形判断

更多资源请关注纽扣编程微信公众号 http://noi.openjudge.cn/ch0104/16/ 描述 给定三个正整数&#xff0c;分别表示三条线段的长度&#xff0c;判断这三条线段能否构成一个三角形。 输入 输入共一行&#xff0c;包含三个正整数&#xff0c;分别表示三条线段的长度&#x…

数据资产:发展现状与未来展望

数据资产&#xff1a;发展现状与未来展望 数据资产作为当今数字经济发展的关键要素&#xff0c;正发挥着日益重要的作用。数据资产是被合法拥有或控制的&#xff0c;能进行计量的&#xff0c;为组织带来经济和社会价值的数据资源。它经历了从数据到数据资产的市场化过程&#x…

一文带你玩转全新采集配置 CRD:AliyunPipelineConfig

作者&#xff1a;玄飏 既然是一文玩转&#xff0c;自然要讲些背景 1.1. 什么是 iLogtail 采集配置 长话短说&#xff1a; SLS&#xff1a;阿里云日志服务&#xff0c;一站式提供数据采集、加工、查询与分析、可视化、告警、消费与投递等功能&#xff0c;全面提升您在研发、…

atsec增加Swift CSP评估资质

atsec信息安全评估员现已被Swift列为Swift客户安全计划&#xff08;CSP&#xff1a;Customer Security Programme&#xff09;认证评估员目录中的评估提供商&#xff0c;可以帮助全球金融机构评估其针对CSP强制性和咨询性控制的合规级别。在金融行业&#xff0c;Swift要求使用其…

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

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

基础岛-任务1:书生大模型全链路开源开放体系

视频地址: https://www.bilibili.com/video/BV18142187g5/ 书生大模型全链路开源开放体系–笔记 技术发展 inerLM2.5 支持100wtoken核心技术思路 数据合成 1.基于规则的构造代码 公式&#xff0c;函数&#xff0c;数学题解2.基于模型的数据扩充代码加注释3.基于人类反馈的数…

从ssm框架 到 spring框架 再到 springmvc框架 设计思想

一、在使用ssm框架的时候&#xff0c;从前端发送get请求到后端处理数据的完整流程是&#xff1a; WebServlet(name "getAll", value "/getAll") public class AddServelet extends HttpServlet {Overrideprotected void doGet(HttpServletRequest reque…

FPGA知识基础之RAM--基于IP核的单端口RAM设计

目录 一、RAM简介1.1 基本特性1.2 RAM对电脑性能的影响1.3 RAM的应用场景 二、存储器的分类1.存储器分类2.RAM分类 三、实验任务四、程序设计1.系统模块2.波形绘制3.IP核设置4.代码编写RTL代码Testbench代码 五、仿真 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案…

WebRTC 初探

前言 项目中有局域网投屏与文件传输的需求&#xff0c;所以研究了一下 webRTC&#xff0c;这里记录一下学习过程。 WebRTC 基本流程以及概念 下面以 1 对 1 音视频实时通话案例介绍 WebRTC 的基本流程以及概念 WebRTC 中的角色 WebRTC 终端,负责音视频采集、编解码、NAT 穿…

【书生大模型实战营第三期】基础岛 第2关 8G 显存玩转书生大模型 Demo

欢迎大家参与第三期书生大模型实战营&#xff01;&#xff01;&#xff01; 1. 基础任务 任务描述&#xff1a;使用 Cli Demo 完成 InternLM2-Chat-1.8B 模型的部署&#xff0c;并生成 300 字小故事&#xff0c;记录复现过程并截图。 1. 首先创建开发机&#xff0c;并建立远程链…

操作系统03:调度算法和文件系统

文章目录 调度算法进程调度算法先来先服务调度算法最短作业优先调度算法高响应比优先调度算法时间片轮转调度算法最高优先级调度算法 内存页面置换算法最佳页面置换算法&#xff08;OPT&#xff09;先进先出置换算法&#xff08;FIFO&#xff09;最近最久未使用的置换算法&…

电赛必备-TI开发板MSPM0G3507开发应用教程-环境配置

这篇文章将手把手详细介绍如何安装及环境配置对MSPM0G3507进行开发 这个网址是TI德州仪器的官网&#xff1a; 模拟 | 嵌入式处理 | 半导体公司 | 德州仪器 TI.com.cn 这个网址直接复制&#xff0c;进入即可跳转我们需要的内容页面https://www.ti.com.cn/tool/cn/LP-MSPM0G350…

LLaMA- Adapter V2: Parameter-Efficient Visual Instruction Model

发表时间&#xff1a;28 Apr 2023 论文链接&#xff1a;https://arxiv.org/pdf/2304.15010 作者单位&#xff1a; Shanghai Artificial Intelligence Laboratory Motivation&#xff1a;如何有效地将大型语言模型 (LLM) 转换为指令追随者最近是一个流行的研究方向&#xff0…

MyBatisPlus简介及入门案例

MyBatisPlus简介 MyBatis-Plus&#xff08;简称 MP&#xff09;是一个 MyBatis的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入&#xff1a;只做增强不做改变&#xff0c;引入它不会对现有工程产生影响&…

【知识跨境电商API接口丨python数分实战】国际电商平台用户成交转化分析

今天这篇文章将给大家介绍国际电商平台用户成交转化分析案例。 01 初步思路 问题已经明确 GMV的下降&#xff0c;可能出在用户下单成交环节。 这里需要明确GMV下降是今年与去年相比吗&#xff1f;明确GMV下降到底如何判断出来的&#xff1f;对比的对象是什么&#xff1f;如…

2024免费文案生成器,一键生成原创文案

在自媒体时代的今天&#xff0c;文案创作需求日益增长&#xff0c;无论是营销推广、社交媒体运营&#xff0c;还是品牌宣传、内容创作&#xff0c;优质的文案都起着至关重要的作用。对于文案创作者来说&#xff0c;如何在有限的时间内创作出吸引人、有价值且独特的文案&#xf…

【Linux】成功解决“appropriate BINARY mode and that you have compiled UnZip properly”

【Linux】成功解决“appropriate BINARY mode and that you have compiled UnZip properly” 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393;…

xss漏洞(五,xss-labs靶场搭建及简单讲解)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 前言&#xff1a; 本文基于github上的xss-labs靶场以及PHP study进行操作。 一&#xff0c;靶场环境搭建。 1, 下载并解压到phpstudy的www目录下。 同前文一致&#xff0c;将文件…

分数化小数(Fractions As Decimals)

题目描述 给定一个以十进制表示的分数 a/b,保证 a<b,请将它化成一个二进制的小数。 若存在循环节,用圆括号表示循环节。例如: ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ …