LLM说: 给我Tools,我来安排工作流(Agentic workflows)

55 阅读9分钟

提供工具给大模型,大模型会在适当的时候,自主决定是否要使用工具,让大模型自主决定的流程,是一种agentic workflows或者reasoning-heavy workflows.

LLM能够解析自然语言并按正确顺序使用合适的工具,以完成各种不同的任务

LLM说: 给我Tools我来安排工作流(Agentic workflow)

🎉注意

选择合适的模型很重要:对于简单任务,较小的模型更快、更便宜;而对于推理密集型工作流reasoning-heavy workflows,更强的模型则表现更佳。

大模型使用工具

这里大模型不知道当前系统的时间,我们提供一个工具(这里是一个函数)给大模型。

image.png

让大模型自主决定是否使用工具中的任何一个

  1. 当大模型需要使用工具的时候,它会告诉我们去调用工具,这里要知道时间,它需要让我们去执行get_current_time,执行之后,我们再把结果给LLM
  2. 当大模型不需要使用工具,它就没有使用工具,尽管我们给他了时间的工具,说明它能够自主判断是否需要使用工具

image.png

Agentic应用开发

所以作为一个开发者,在开发一个Agentic App智能体应用时,应该考虑要实现哪些功能?然后创建相应的函数(工具),为大模型实现并开放不同的工具,大模型会自己决定是否要使用工具。

比如要创建一个日历智能体助手。给模型提供一系列操作日历的工具,比如:检查日历,预约事项,删除预约等。大模型会在可用的工具列表中判断。

大模型进行决定最先要用的可能checkCalendar,于是调用check_calendar,将执行结果反馈给大模型,接下来它决定选择一个时间段,比如下午三点,然后调用make_appointment创建预约,同样将操作的结果反馈给大模型。

🔔需要注意的时候,整个任务的拆分和调用工具的流程完全由LLM自主决定,也许他可能跳过check_calendar直接执行make_appointment也说不定。这不是一个工业级别严格执行的workflow,而是一个agentic workflow

image.png

🎉LLM使用工具的原理

  1. 大模型LLM只是被训练用来输出文本(输出token)的。它本身并不会执行工具。
  2. 工具(Tools): LLM能够请求要执行的代码

LLM并不会直接调用函数,而是以特定格式输出内容:

  1. 在提示词中告诉LLM有哪些工具,并且告诉他如果决定要使用工具时,要输出的格式(要调用的函数名称,甚至函数的参数信息)。
  2. 接着我们的代码进行拦截处理,判断LLM请求要执行工具,我们就调用函数执行代码
  3. 将执行结果再反馈给LLM

image.png

来自我另一篇文章【 Agent的ReAct(推理+行动)模式】 这里拦截LLM的输出,然后进行反射调用方法。

image.png

代码角度理解工具执行的原理

image.png

get_current_time会将自己的注释提供给LLM,告诉LLM这个工具是干什么用的,让LLM自主决定什么时候使用

from datetime import datetime

def get_current_time():
    """
    Returns the current time as a string.
    """
    return datetime.now().strftime("%H:%M:%S")

aisuite这个框架封装了提示词,并且tools=[get_current_time]将工具列表解析出来:工具名称以及描述(这里是注释)

此时框架的作用就体现出来了: 🎉Turning function into an LLM tool🎉,在抽象层面将函数转化成LLM可执行的工具。

尽管我们知道底层是通过提示词引导,输出结果拦截解析,然后执行具体的函数。AISuite 处理了所有复杂流程:提取包含工具调用的消息、在本地执行该调用。

但是在上层,这已经可以理解为给LLM赋能,提供脚和手的工具,增强了LLM对这个世界的认知。

import aisuite as ai
# Create an instance of the AISuite client
client = ai.Client()

# Message structure
prompt = "What time is it?"
messages = [
    {
        "role": "user",
        "content": prompt,
    }
]

response = client.chat.completions.create(
    model="openai:gpt-4o",
    messages=messages, # 历史消息
    tools=[get_current_time], # 🎉工具列表
    max_turns=5  # ReAct限制大模型连续请求的次数
)

内部执行深入了解

函数成工具的处理

在aisuite这个包中,底层会生成一个详细描述函数(工具)的JSON结构,每个工具都带有其预期的固定模式(schema)

tools = [{
    "type": "function",
    "function": {
        "name": "get_current_time", # <--- Your functions name
        "description": "Returns the current time as a string.", # <--- a description for the LLM
        "parameters": {}
    }
}]

如果是带有参数,JSON的Schema长这样。

image.png


模型返回需要调用工具

这里只让Agent与LLM循环一次,方便查看LLM的输出结果

import json

# Message structure
prompt = "What time is it?"
messages = [
    {
        "role": "user",
        "content": prompt,
    }
]

response = client.chat.completions.create(
    model="openai:gpt-4o",
    messages=messages,
    tools=tools, # <-- Your list of tools with get_current_time
    # max_turns=5 # <-- When defining tools manually, you must handle calls yourself and cannot use max_turns
)

print(json.dumps(response.model_dump(), indent=2, default=str))

注意,在响应中,你可以在 message 下看到 tool_calls。LLM 的此响应表示它现在想要调用一个工具,具体来说是 get_current_time。你可以添加一些逻辑来处理这种情况,然后将结果传回模型,并获取最终响应。

{
  "id": "chatcmpl-DUbu4GSDZ3XPDdOP1ei6ufJj7X6Nu",
  "choices": [
    {
      "finish_reason": "tool_calls",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": null,
        "refusal": null,
        "role": "assistant",
        "annotations": [],
        "audio": null,
        "function_call": null,
        "tool_calls": [
          {
            "id": "call_hEZFo8JLKq1p9bcoVJykkhWu",
            "function": {
              "arguments": "{}",
              "name": "get_current_time"
            },
            "type": "function"
          }
        ]
      }
    }
  ],
  "created": 1776187912,
  "model": "gpt-4o-2024-08-06",
  "object": "chat.completion",
  "service_tier": "default",
  "system_fingerprint": "fp_071222c984",
  "usage": {
    "completion_tokens": 11,
    "prompt_tokens": 45,
    "total_tokens": 56,
    "completion_tokens_details": {
      "accepted_prediction_tokens": 0,
      "audio_tokens": 0,
      "reasoning_tokens": 0,
      "rejected_prediction_tokens": 0
    },
    "prompt_tokens_details": {
      "audio_tokens": 0,
      "cached_tokens": 0
    }
  }
}

手动处理函数执行

现在手动执行函数,然后将执行结果添加到messages中,再反馈给LLM

response2 = None

# Create a condition in case tool_calls is in response object
if response.choices[0].message.tool_calls:
    # Pull out the specific tool metadata from the response
    tool_call = response.choices[0].message.tool_calls[0]
    args = json.loads(tool_call.function.arguments)

    # Run the tool locally
    tool_result = get_current_time()

    # Append the result to the messages list
    messages.append(response.choices[0].message)
    messages.append({
        "role": "tool", "tool_call_id": tool_call.id, "content": str(tool_result)
    })

    # Send the list of messages with the newly appended results back to the LLM
    response2 = client.chat.completions.create(
        model="openai:gpt-4o",
        messages=messages,
        tools=tools,
    )

    print(response2.choices[0].message.content) # The current time is 06:20:57.

查看完整的messages流程

[
  {
    "role": "user",
    "content": "What time is it?"
  },
  "ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_hEZFo8JLKq1p9bcoVJykkhWu', function=Function(arguments='{}', name='get_current_time'), type='function')])",
  {
    "role": "tool",
    "tool_call_id": "call_hEZFo8JLKq1p9bcoVJykkhWu",
    "content": "06:33:02"
  }
]

有意思的是我将此时的messages粘贴给deepseek,它真的能够根据上下文,推断出来的了上下文的时间,尽管当时我执行的时间是北京14:25

image.png

小结

整个过程的消息角色:"user", "assistant", "system",还有一个"tool"

LLM自主决策选择工具

定义工具

提供了三个工具给LLM

工具名参数描述
get_weather_from_ip无(自动检测IP)检测用户IP地址,通过外部API调用获取当前位置的当前温度、最高温和最低温。
write_txt_filefile_path(文件路径), content(内容)在本地环境中创建指定内容的文本文件。
generate_qr_codedata(数据), filename(文件名), img_path(图片路径,可选)根据数据生成二维码图像,并可选择嵌入图片。
import requests
import qrcode
from qrcode.image.styledpil import StyledPilImage


def get_weather_from_ip():
    """
    Gets the current, high, and low temperature in Fahrenheit for the user's
    location and returns it to the user.
    """
    # Get location coordinates from the IP address
    lat, lon = requests.get('https://ipinfo.io/json').json()['loc'].split(',')

    # Set parameters for the weather API call
    params = {
        "latitude": lat,
        "longitude": lon,
        "current": "temperature_2m",
        "daily": "temperature_2m_max,temperature_2m_min",
        "temperature_unit": "fahrenheit",
        "timezone": "auto"
    }

    # Get weather data
    weather_data = requests.get("https://api.open-meteo.com/v1/forecast", params=params).json()

    # Format and return the simplified string
    return (
        f"Current: {weather_data['current']['temperature_2m']}°F, "
        f"High: {weather_data['daily']['temperature_2m_max'][0]}°F, "
        f"Low: {weather_data['daily']['temperature_2m_min'][0]}°F"
    )

# Write a text file
def write_txt_file(file_path: str, content: str):
    """
    Write a string into a .txt file (overwrites if exists).
    Args:
        file_path (str): Destination path.
        content (str): Text to write.
    Returns:
        str: Path to the written file.
    """
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(content)
    return file_path


# Create a QR code
def generate_qr_code(data: str, filename: str, image_path: str):
    """Generate a QR code image given data and an image path.

    Args:
        data: Text or URL to encode
        filename: Name for the output PNG file (without extension)
        image_path: Path to the image to be used in the QR code
    """
    qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
    qr.add_data(data)

    img = qr.make_image(image_factory=StyledPilImage, embedded_image_path=image_path)
    output_file = f"{filename}.png"
    img.save(output_file)

    return f"QR code saved as {output_file} containing: {data[:50]}..."

执行任务

给提示词Can you get the weather for my location?

prompt = "Can you get the weather for my location?"

response = client.chat.completions.create(
    model="openai:o4-mini",
    messages=[{"role": "user", "content": (
        prompt
    )}],
    tools=[
        get_current_time,
        get_weather_from_ip,
        write_txt_file,
        generate_qr_code
    ],
    max_turns=5
)

display_functions.pretty_print_chat_completion(response)

可以看到LLM能够在众多的工具集中能够选择,正确的工具进行执行。

image.png

🎉Agentic Workflow的体现

将LLM的推理能力与外部工具相结合,以完成更复杂的任务,不仅能自主安排了流程,而且自己选择了相应的工具。

提示词

Can you help me create a qr code that goes to www.deeplearning.com from the image dl_logo.jpg? Also write me a txt note with the current weather please.

这个提示需要相当多的逻辑才能理解何时调用哪个工具。例如,虽然你要求它先写文本笔记再描述内容,但LLM需要先获取信息才能传给文本笔记。如果LLM先调用文件写入工具,它就会缺少从天气工具获取的信息。但是LLM能够自己拆分任务顺序,它知道先查询天气,然后再写入文本。这很好地体现了LLM的能力:解析自然语言并按正确顺序使用合适的工具,以完成各种不同的任务

LLM说: 给我Tools我来安排工作流(Agentic workflow)

import display_functions

prompt = "Can you help me create a qr code that goes to www.deeplearning.com from the image dl_logo.jpg? Also write me a txt note with the current weather please."

response = client.chat.completions.create(
    model="openai:o4-mini",
    messages=[{"role": "user", "content": (
        prompt
    )}],
    tools=[   # 🎉给你工具,让LLM你自己捣鼓去吧
        get_weather_from_ip,
        get_current_time,
        write_txt_file,
        generate_qr_code
    ],
    max_turns=10
)

display_functions.pretty_print_chat_completion(response)

执行结果:

get_weather_from_ip → generate_qr_code → write_txt_file

LLM自己安排顺序,选择相应的执行工具。(我们知道get_weather_from_ip → write_txt_file是有严格先后顺序的,generate_qr_code则是一个独立的步骤)。

image.png

模拟邮件助手工作流

案例实践【Agentic workflow实践:模拟邮件助手工作流】