使用智普清言的Tools功能实现ToolAgent

大家好,我是雨飞。最近在尝试智普的API实现Agent功能,由于langchain本身的接口只支持OpenAI的function call和tools,因此做了一些修改。

langchain原生的实现,可以看官方的文档,更新太频繁了,就感觉昨天还能用的东西,今天就不行了。

OpenAI tools | ️ Langchain

下面这个是核心的代码,实现了简单的Agent功能,通过调用不同的工具去回答问题。agent_output_parser 函数,自己做了重构和实现。

代码中,实现了两个简单的工具,获取天气、计算两个数的和。

python 复制代码
import requests
import datetime
from langchain.agents import tool
from zhipu_llm import ChatZhipuAI
from langchain_core.utils.function_calling import convert_to_openai_tool
from langchain.schema.agent import  AgentFinish
from langchain.prompts import MessagesPlaceholder
from langchain.prompts import ChatPromptTemplate
from agent_output_parser import OpenAIToolsAgentOutputParser,format_to_openai_tool_messages

zhipuai_api_key=""
glm3= "glm-3-turbo"
glm4="glm-4"

chat_zhipu = ChatZhipuAI(
    temperature=0.8,
    api_key=zhipuai_api_key,
    model=glm3
)

@tool
def get_current_temperature(latitude: float, longitude: float):
    """Fetch current temperature for given coordinates."""

    BASE_URL = "https://api.open-meteo.com/v1/forecast"

    # Parameters for the request
    params = {
        'latitude': latitude,
        'longitude': longitude,
        'hourly': 'temperature_2m',
        'forecast_days': 1,
    }

    # Make the request
    response = requests.get(BASE_URL, params=params)

    if response.status_code == 200:
        results = response.json()
    else:
        raise Exception(f"API Request failed with status code: {response.status_code}")

    current_utc_time = datetime.datetime.utcnow()
    time_list = [datetime.datetime.fromisoformat(time_str.replace('Z', '+00:00')) for time_str in
                 results['hourly']['time']]
    temperature_list = results['hourly']['temperature_2m']

    closest_time_index = min(range(len(time_list)), key=lambda i: abs(time_list[i] - current_utc_time))
    current_temperature = temperature_list[closest_time_index]

    return f'The current temperature is {current_temperature}°C'

@tool
def sum_nums(s1: float, s2: float):
    """Sum the result for given inputs."""
    return "The result is {a}".format(a = s1+s2)


prompt = ChatPromptTemplate.from_messages([
    ("system", "You are helpful but sassy assistant"),
    ("user", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])
tools =[get_current_temperature,sum_nums]
zhipu_tools = [convert_to_openai_tool(f) for f in tools]
model = chat_zhipu.bind(tools=zhipu_tools)
chain = prompt | model | OpenAIToolsAgentOutputParser()

def run_agent(user_input):
    intermediate_steps = []
    tool_mappings = {
        "get_current_temperature": get_current_temperature,
        "sum_nums":sum_nums
    }
    while True:
        mes = format_to_openai_tool_messages(intermediate_steps)
        result = chain.invoke({
            "input": user_input,
            "agent_scratchpad": mes
        })
        if isinstance(result, AgentFinish):
            return result
        tool = tool_mappings[result.tool]
        observation = tool.run(result.tool_input)
        intermediate_steps.append((result, observation))
        print((result,observation))

result = run_agent("what is the weather is NewYork?")
print(result)
print(result.messages[0].content)

result2 = run_agent("what is the result of 19+23?")
print(result2)
print(result2.messages[0].content)

agent_output_parser.py 这个类的代码,这个类是参考了 OpenAI 的 functions 的 Agent 类的实现,修改了最后返回的对象。因为是工具调用,最终要返回的是 ToolMessage,而 functions 最终返回的是FunctionMessage。

python 复制代码
# !/usr/bin env python3
import json
from json import JSONDecodeError
from typing import List, Union,Sequence, Tuple

from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import (
    AIMessage,
    BaseMessage,
    ToolMessage
)
from langchain_core.outputs import ChatGeneration, Generation

from langchain.agents.agent import AgentOutputParser


class OpenAIToolsAgentOutputParser(AgentOutputParser):
    """Parses a message into agent action/finish.

    Is meant to be used with OpenAI models, as it relies on the specific
    function_call parameter from OpenAI to convey what tools to use.

    If a function_call parameter is passed, then that is used to get
    the tool and tool input.

    If one is not passed, then the AIMessage is assumed to be the final output.
    """

    @property
    def _type(self) -> str:
        return "openai-functions-agent"

    @staticmethod
    def _parse_ai_message(message: BaseMessage) -> Union[AgentAction, AgentFinish]:
        """Parse an AI message."""
        if not isinstance(message, AIMessage):
            raise TypeError(f"Expected an AI message got {type(message)}")

        function_call = message.additional_kwargs.get("tool_calls", [{}])
        function_call = function_call[0].get("function",{})
        if function_call:
            function_name = function_call["name"]
            try:
                if len(function_call["arguments"].strip()) == 0:
                    # OpenAI returns an empty string for functions containing no args
                    _tool_input = {}
                else:
                    # otherwise it returns a json object
                    _tool_input = json.loads(function_call["arguments"], strict=False)
            except JSONDecodeError:
                raise OutputParserException(
                    f"Could not parse tool input: {function_call} because "
                    f"the `arguments` is not valid JSON."
                )

            # HACK HACK HACK:
            # The code that encodes tool input into Open AI uses a special variable
            # name called `__arg1` to handle old style tools that do not expose a
            # schema and expect a single string argument as an input.
            # We unpack the argument here if it exists.
            # Open AI does not support passing in a JSON array as an argument.
            if "__arg1" in _tool_input:
                tool_input = _tool_input["__arg1"]
            else:
                tool_input = _tool_input

            content_msg = f"responded: {message.content}\n" if message.content else "\n"
            log = f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n"
            return AgentActionMessageLog(
                tool=function_name,
                tool_input=tool_input,
                log=log,
                message_log=[message],
            )

        return AgentFinish(
            return_values={"output": message.content}, log=str(message.content)
        )

    def parse_result(
        self, result: List[Generation], *, partial: bool = False
    ) -> Union[AgentAction, AgentFinish]:
        if not isinstance(result[0], ChatGeneration):
            raise ValueError("This output parser only works on ChatGeneration output")
        message = result[0].message
        return self._parse_ai_message(message)

    def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
        raise ValueError("Can only parse messages")

def format_to_openai_tool_messages(
    intermediate_steps: Sequence[Tuple[AgentAction, str]],
) -> List[BaseMessage]:
    """Convert (AgentAction, tool output) tuples into FunctionMessages.

    Args:
        intermediate_steps: Steps the LLM has taken to date, along with observations

    Returns:
        list of messages to send to the LLM for the next prediction

    """
    messages = []

    for agent_action, observation in intermediate_steps:
        messages.extend(_convert_agent_action_to_messages(agent_action, observation))

    return messages

def _convert_agent_action_to_messages(
    agent_action: AgentAction, observation: str
) -> List[BaseMessage]:
    """Convert an agent action to a message.

    This code is used to reconstruct the original AI message from the agent action.

    Args:
        agent_action: Agent action to convert.

    Returns:
        AIMessage that corresponds to the original tool invocation.
    """
    if isinstance(agent_action, AgentActionMessageLog):
        return list(agent_action.message_log) + [
            _create_function_message(agent_action, observation)
        ]
    else:
        return [AIMessage(content=agent_action.log)]


def _create_function_message(
    agent_action: AgentAction, observation: str
) -> ToolMessage:
    """Convert agent action and observation into a function message.
    Args:
        agent_action: the tool invocation request from the agent
        observation: the result of the tool invocation
    Returns:
        FunctionMessage that corresponds to the original tool invocation
    """
    if not isinstance(observation, str):
        try:
            content = json.dumps(observation, ensure_ascii=False)
        except Exception:
            content = str(observation)
    else:
        content = observation
    return ToolMessage(
        tool_call_id=agent_action.tool,
        content=content,
    )

智普大模型调用部分,也就是 zhipu_llm 这个模块的文件,

可以参考这篇文章里的代码,代码比较长,就不再贴了。

雨飞:利用智普大模型进行表格数据问答

另外,也可以使用 langchain 的 AgentExecutor 封装部分代码去实现 Agent 的功能,示例如下:

python 复制代码
from langchain.agents import AgentExecutor
def agent_builder():
    agent = (
        {"input":lambda x:x["input"],
         "agent_scratchpad":lambda  x:format_to_openai_tool_messages(x["intermediate_steps"])
         }
    )| prompt | model | OpenAIToolsAgentOutputParser()

    agent_executor = AgentExecutor(agent=agent,tools=tools,verbose=True)
    return agent_executor

query = "how many letters in the word educa?"
agent = agent_builder()
result = agent.invoke({"input":query})
print(result)
相关推荐
地平线开发者11 分钟前
点云算法整理与常用计算模块:从数据到模型,再到工程与量化
算法·自动驾驶
武子康2 小时前
H3-World 为何在加载 LoRA 前,先检查导入的代码?
人工智能·深度学习·算法
秋天的怀念_David3 小时前
算法一千零一夜 · 第二十八夜 注册
java·程序人生·算法
liferecords4 小时前
不训练一个参数,把图像编辑器变成视频编辑器:EditVid 的局部-全局拆解术
人工智能·llm·aigc·多模态
NEFU_Jyj5 小时前
贝尔数(组合数学)
算法
希和20276 小时前
C语言程序设计题|有序数组插入数字冒泡排序
c语言·数据结构·算法
Pniubi6 小时前
力扣73 矩阵置零
算法·leetcode·矩阵
布莱克60516 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
泯泷18 小时前
为什么你的 AI Agent“越改越差”?Anthropic 总结了 8 步评测方法
llm·agent·claude