本文将简单介绍如何评价LLM的tool-using 能力。

引言

在工具使用评估方面,过去的研究主要有以下几种思路:

对比工具使用和纯LLM在基准测试上的分数:例如Toolformer和LATM。

  • 在Toolformer研究中,通过下游任务如语言模型评估基准测试、数学推理任务和问答任务来验证工具使用的有效性。

  • LATM则采用了来自BigBench的六个数据集进行评估。

测试工具使用的准确率和响应质量:例如API-Bank。

  • 在评估过程中,首先初始化评估系统,确保每个API的数据库包含默认值。然后,将预测的API调用与手动标注的API调用进行比较,以确定它们的一致性。响应评估则使用ROUGE-L指标。

利用LLM对工具使用的效果进行评价:例如Tool-bench。

two evaluation metrics:

  • Pass Rate: Calculates the proportion of successfully completing an instruction within limited OpenAI API calls.

  • Preference: Measured by comparing two answers (action sequences) for a given instruction.We pre-define a set of criteria for a better answer, which are organized as prompts for ChatGPT.

构造虚拟运行环境,测试代理与环境的交互结果:例如ToolAlpaca。

  • 利用LLM模拟环境(用户代理和助手代理),并使用GPT-4对ToolAlpaca模型进行机器评估,评估其使用各种未见工具的能力。

对于绝大多数企业和垂直场景下, 其中思路1需要构建额外的测试集成本比较高(但还是有必要的), 而思路4构造虚拟运行环境实际上并不现实; 所以还是选择思路2,外加通过思路3辅助判断;换句话说, 根据场景,构造工具列表和工具调用的ground-truth(包括函数名, 和传入参数) ,在存在歧义时,采用ROUGE评价响应质量, 或者使用LLM判断响应结果并评价。

顺带提一下Langchain 项目中有关Agent的tool-using能力测试的内容, 不过由于Langchain项目又臭又长,且有很大的局限性, 这里只讨论其思路。

在 Benchmarking Agent Tool Use 一文中,提出了4种指标用于评价Agent 的tool-using 能力:

  1. Correctness 即通过LLM判断工具调用结果是否同ground truth 相同;

  2. Correct final state 即检查agent 所交互环境的最终状态是否同预期相同;

  3. Intermediate step correctness 除了最终状态, 也可以检查中间状态;

  4. Ratio of steps taken to the expected steps 即实际步骤与预期步骤的比例。

在LangChain Benchmarks 项目提供了4个(3个)任务用于测试tool-using, 本质上也是一种构造虚拟环境并与之交互。

Multiverse Math 任务 : 要求模型使用提供的工具来解决数学问题。为了确保模型不依赖于内在知识,数学运算被修改以产生不同于预期的结果。例如,乘法和加法的结果会有所不同,但仍保留某些数学性质。 任务示例包括基本的加减乘除运算、幂运算、对数运算等。

code
"""Solve basic math question using the provided tools.

Must use the provided tools to solve the math question.

To make sure that innate knowledge is not used, the math operations
have been altered to yield different results than expected.

The modified operations should yield different results, but still retain
appropriate properties. For example, the modified multiplication operation
should still be commutative.

Please note that the modified operations are not guaranteed to even make sense
in the real world since not all properties will be retained (e.g.,
distributive property).

For example,

I ate 1 apple and 2 oranges every day for 7 days. How many fruits did I eat?

One would expect the answer to be 21, but in this universe, the answer is 32.34.

In addition, it depends on how the operations are grouped:

(1 + 2) * 7 = 32.34

But:

1 * 7 + 2 * 7 = 24.3

Due to these changes certain questions are not allowed as inputs as they
would yield different results if evaluated in different ways.

For example, "convert 15 degrees to radians" is not allowed as an input
as it could be interpreted as either:

divide(multiply(15, pi()), 180)
or
multiply(divide(15, 180), pi())
"""

Relational Data任务 :要求模型使用提供的工具来回答关于关系数据的问题。环境中包含关于用户及其位置和喜好食物的虚假数据,模型需要使用工具查询这些数据以回答问题。 任务示例包括查询用户的位置信息、喜好食物等

code
"""Answer questions about relational data using the provided tools.

The environment contains fake data about users and their locations and favorite foods.

The environment provides a set of tools that can be used to query the data.

All questions can be answered by using the provided tools. The answers
include the expected result as well as the most efficient way to answer the
question using the tools.
"""

Typewriter(single-tool, 26-tools): 要求模型使用工具逐字打印给定的字符串。模型需要调用工具来模拟打字机的行为,每次打印一个字符,直到完成整个字符串。 任务示例包括打印特定的句子或段落。

code
```python

"""A task where the agent must type a given string one letter at a time.
In this variation of the task, the agent is given a single function,
that takes a letter as an argument.
"""
```

根据综述文章《What Are Tools Anyway? A Survey from the Language Model Perspective》,在验证代理或模型的工具使用能力时,可以关注以下指标:

Task completion:任务完成度,或pass-ratio 这是一个相对宏观的指标, 工具调用的最核心目的就是为了完成任务, 任务的成功与否表征了工具调用的最终结果。

Tool selection: 如何在众多工具中挑选到最合适的工具,这考验了模型在特定场景下对任务的理解和最优化的工具选择;

Tool reusability: 指的是一个工具在不同情境下或解决不同问题时能够被多次使用的能力, 不过往往和工具生成(tool-marking)相关, 不在这次的讨论范围中。

方法论

综合以上讨论, tool-using 的测试可以简单分为:

单轮简单测试

在这种测试中,主要测试function_name 和传入参数。关注的指标是pass-rate 和 Preference。具体方法是不需要使用LLM进行判断,只需将返回的function_name 和 function_arguments 与ground-truth进行比对,确认是否一致。最终使用pass-ratio 评价测试结果。

Json-mode验证

由于并非所有模型都能严格按要求输出所需要的json格式,考虑到这种情况,可以进行Json-mode的验证,以确保输出格式的正确性。

优化版本

通过ROUGE判断function_arguments

  • 目的:避免严格一致所带来的问题。

通过LLM辅助判断function_arguments

  • 优点:准确率更高。

  • 缺点:成本也更高。

多轮调用

以上单轮tool-using测试在复杂场景下不太适用。

在一些复杂场景中,如代码生成与执行、SQL生成并执行等,需要按照一定顺序调用多个tools。这些场景需要LLM根据前几次tool-using的结果反馈,调整function_arguments 或 function_name。

这些场景无外乎都需要LLM根据前几次tool-using的结果反馈, 修改function_arguments 或者是function_name;

将tool-using的过程抽象为强化学习中agent同环境(env)的交互过程。目标是减少agent与环境的交互次数,从而降低成本,并争取一次或者几次就能做对,避免重复错误。可以借用langchain中的思想,增加对中间和最终状态的检查,并比对实际交互次数和预期交互次数。这类测试需要额外构建虚拟环境,超出此次讨论范围。

测试集

NousResearch/func-calling-eval-glaive

NousResearch/func-calling-eval-singleturn

NousResearch/func-calling-eval

上手:测试LLM的tool-using 能力

这里,选择最简单的tool-using用于演示, 即直接调用OpenAI 的tool-using API

环境

import os
# os.environ["OPENAI_API_KEY"] = "..."
from openai import OpenAI
import json

client = OpenAI()

定义工具函数

考虑了一个简单的医疗场景,共4个工具;

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_medical_definition",
            "description": "Get the definition of a medical term",
            "parameters": {
                "type": "object",
                "properties": {
                    "term": {"type": "string", "description": "The medical term to define"}
                },
                "required": ["term"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_treatment_options",
            "description": "Get treatment options for a disease",
            "parameters": {
                "type": "object",
                "properties": {
                    "disease": {"type": "string", "description": "The disease to get treatment options for"}
                },
                "required": ["disease"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_medication_side_effects",
            "description": "Get the potential side effects of a medication",
            "parameters": {
                "type": "object",
                "properties": {
                    "medication": {"type": "string", "description": "The name of the medication"}
                },
                "required": ["medication"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_disease_symptoms",
            "description": "Get the symptoms associated with a specific disease",
            "parameters": {
                "type": "object",
                "properties": {
                    "disease": {"type": "string", "description": "The disease to get symptoms for"}
                },
                "required": ["disease"]
            }
        }
    }
]

定义测试用例

创建测试用例数据集,每个测试用例包含查询、预期函数调用及其参数、以及期望的响应。

使用GPT-4构建。

test_cases = [
    {
        "query": "What are the symptoms of influenza?",
        "expected_function": "get_disease_symptoms",
        "function_arguments": {"disease": "influenza"},
        "expected_response": "Symptoms of influenza include fever, chills, muscle aches, cough, congestion, runny nose, headaches, and fatigue."
    },
    {
        "query": "What is the definition of asthma?",
        "expected_function": "get_medical_definition",
        "function_arguments": {"term": "asthma"},
        "expected_response": "Asthma is a condition in which your airways narrow and swell and may produce extra mucus, making breathing difficult and triggering coughing, wheezing, and shortness of breath."
    },
    {
        "query": "What treatments are available for rheumatoid arthritis?",
        "expected_function": "get_treatment_options",
        "function_arguments": {"disease": "rheumatoid arthritis"},
        "expected_response": "Treatment options for rheumatoid arthritis include medications such as NSAIDs, steroids, DMARDs, and biologics, as well as physical therapy and lifestyle changes."
    },
    {
        "query": "What are the side effects of metformin?",
        "expected_function": "get_medication_side_effects",
        "function_arguments": {"medication": "metformin"},
        "expected_response": "Common side effects of metformin include gastrointestinal issues such as diarrhea, nausea, and abdominal discomfort, as well as lactic acidosis in rare cases."
    },
    {
        "query": "What symptoms are associated with hypothyroidism?",
        "expected_function": "get_disease_symptoms",
        "function_arguments": {"disease": "hypothyroidism"},
        "expected_response": "Symptoms of hypothyroidism include fatigue, weight gain, cold intolerance, dry skin, constipation, and depression."
    },
    ...
  ]

定义测试metrics

自定义了pass-ratio,即function_name, function_arguments 严格一致为pass。

运行测试并验证响应

def call_openai_tool(model, prompt, tools):
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": prompt}
        ],
        tools=tools,
        tool_choice="auto"
    )
    return response


def run_tests(test_cases, model=deployment_name):
    passed_tests = 0
    failed_tests = 0

    for case in test_cases:
        response = call_openai_tool(model, case['query'], tools)

        if case['expected_function'] and case['function_arguments']:
            if response.choices[0].message.tool_calls:
                tool_calls = response.choices[0].message.tool_calls

                # 验证函数调用名称和参数
                try:
                    assert tool_calls[0].function.name == case['expected_function'], f"Expected function {case['expected_function']}, but got {tool_calls[0].function.name}"
                    assert json.loads(tool_calls[0].function.arguments) == case['function_arguments'], f"Expected arguments {case['function_arguments']}, but got {tool_calls[0].function.arguments}"

                    print(f"Test passed for query: {case['query']}")
                    passed_tests += 1
                except AssertionError as e:
                    print(e)
                    print(f"Test failed for query: {case['query']}")
                    failed_tests += 1
            else:
                print(f"Test failed for query (no tool call): {case['query']}")
                failed_tests += 1
        else:
            # # 验证响应内容
            # response_text = response.choices[0].message.content.strip()

            # 验证没有调用工具
            if response.choices[0].message.tool_calls:
                print(f"Test failed for query (unexpected tool call): {case['query']}")
                failed_tests += 1

            else:
                print(f"Test passed for query: {case['query']}")
                passed_tests += 1

    total_tests = passed_tests + failed_tests
    accuracy = (passed_tests / total_tests) * 100 if total_tests > 0 else 0
    print(f"Total tests: {total_tests}, Passed: {passed_tests}, Failed: {failed_tests}, Accuracy: {accuracy:.2f}%")


run_tests(test_cases)

结果:


Test passed for query: What are the symptoms of chronic kidney disease?
Test passed for query: What does the medical term 'bipolar disorder' mean?
Expected arguments {'disease': 'chronic obstructive pulmonary disease'}, but got {
  "disease": "COPD"
}
Test failed for query: What are the treatment options for chronic obstructive pulmonary disease (COPD)?
Test passed for query: What side effects are associated with the use of aspirin?
Test passed for query: Tell me a joke.
Test passed for query: What is the capital of France?
Total tests: 22, Passed: 21, Failed: 1, Accuracy: 95.45%

对于test failed case, 这只是个模凌两可的错误。

如果采用LLM作为裁判是可以避免的。

基于ROUGE的响应判断

环境

! pip install rouge-score --quiet 
from rouge_score import rouge_scorer

def calculate_rouge_l(predicted, expected):
    scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)
    scores = scorer.score(predicted, expected)
    return scores['rougeL'].fmeasure

def run_tests_with_rouge(test_cases, model=deployment_name, rouge_l_threshold=0.8):
    passed_tests = 0
    failed_tests = 0

    for case in test_cases:
        response = call_openai_tool(model, case['query'], tools)

        if case['expected_function'] and case['function_arguments']:
            if response.choices[0].message.tool_calls:
                tool_calls = response.choices[0].message.tool_calls

                # 验证函数调用名称和参数
                try:
                    assert tool_calls[0].function.name == case['expected_function'], f"Expected function {case['expected_function']}, but got {tool_calls[0].function.name}"
                    predicted_arguments = json.loads(tool_calls[0].function.arguments)
                    
                    # 计算ROUGE-L分数
                    rouge_l_score = calculate_rouge_l(json.dumps(predicted_arguments), json.dumps(case['function_arguments']))
                    
                    assert rouge_l_score >= rouge_l_threshold, f"ROUGE-L score {rouge_l_score} is less than threshold {rouge_l_threshold}"
                    
                    print(f"Test passed for query: {case['query']}")
                    passed_tests += 1
                except AssertionError as e:
                    print(e)
                    print(f"Test failed for query: {case['query']}")
                    failed_tests += 1
            else:
                print(f"Test failed for query (no tool call): {case['query']}")
                failed_tests += 1
        else:
            # 验证没有调用工具
            if response.choices[0].message.tool_calls:
                print(f"Test failed for query (unexpected tool call): {case['query']}")
                failed_tests += 1
            else:
                print(f"Test passed for query: {case['query']}")
                passed_tests += 1

    total_tests = passed_tests + failed_tests
    accuracy = (passed_tests / total_tests) * 100 if total_tests > 0 else 0
    print(f"Total tests: {total_tests}, Passed: {passed_tests}, Failed: {failed_tests}, Accuracy: {accuracy:.2f}%")

使用ROUGE-L 计算function_arguments 同预期的差距,若大于一定阈值,则判断失败。

基于LLM验证和判断

def chat_completion_request(messages, tools=None, tool_choice=None, model=deployment_name, json_mode=False):
    try:
        params = {
            'model': model,
            'messages': messages,
            'tools': tools,
            'tool_choice': tool_choice
        }

        if json_mode:
            params['response_format'] = { "type": "json_object" }

        response = client.chat.completions.create(**params)
        return response

    except Exception as e:
        print("Unable to generate ChatCompletion response")
        print(f"Exception: {e}")
        return e

def evaluate_with_llm(predicted_arguments, expected_arguments, model, tools):
    prompt = f"""
    Compare the following predicted arguments and expected arguments for a function call.
    Predicted Arguments: {json.dumps(predicted_arguments, indent=2)}
    Expected Arguments: {json.dumps(expected_arguments, indent=2)}
    Evaluate if the predicted arguments sufficiently match the expected arguments, considering minor differences acceptable. Respond with a JSON object containing a single key "match" with a boolean value indicating if they match.

    Example response:
    {{
        "match": true
    }}
    """
    response = chat_completion_request(messages=[{"role": "system", "content": prompt}],model = model, json_mode=True)
    json_response = json.loads(response.choices[0].message.content)
    return json_response.get("match", False)

def run_tests_with_llm(test_cases, model=deployment_name, rouge_l_threshold=0.8):
    passed_tests = 0
    failed_tests = 0

    for case in test_cases:
        response = call_openai_tool(model, case['query'], tools)

        if case['expected_function'] and case['function_arguments']:
            if response.choices[0].message.tool_calls:
                tool_calls = response.choices[0].message.tool_calls

                # 验证函数调用名称和参数
                try:
                    assert tool_calls[0].function.name == case['expected_function'], f"Expected function {case['expected_function']}, but got {tool_calls[0].function.name}"
                    predicted_arguments = json.loads(tool_calls[0].function.arguments)
                    
                    # 使用 LLM 来判断参数匹配
                    match = evaluate_with_llm(predicted_arguments, case['function_arguments'], model, tools)
                    
                    assert match, "LLM evaluation determined the arguments do not match"
                    
                    print(f"Test passed for query: {case['query']}")
                    passed_tests += 1
                except AssertionError as e:
                    print(e)
                    print(f"Test failed for query: {case['query']}")
                    failed_tests += 1
            else:
                print(f"Test failed for query (no tool call): {case['query']}")
                failed_tests += 1
        else:
            # 验证没有调用工具
            if response.choices[0].message.tool_calls:
                print(f"Test failed for query (unexpected tool call): {case['query']}")
                failed_tests += 1
            else:
                print(f"Test passed for query: {case['query']}")
                passed_tests += 1

    total_tests = passed_tests + failed_tests
    accuracy = (passed_tests / total_tests) * 100 if total_tests > 0 else 0
    print(f"Total tests: {total_tests}, Passed: {passed_tests}, Failed: {failed_tests}, Accuracy: {accuracy:.2f}%")
附: prompt 生成示例
def chat_completion_request(messages, tools=None, tool_choice=None, model=deployment_name, json_mode=False):
    try:
        params = {
            'model': model,
            'messages': messages,
            'tools': tools,
            'tool_choice': tool_choice
        }

        if json_mode:
            params['response_format'] = { "type": "json_object" }

        response = client.chat.completions.create(**params)
        return response

    except Exception as e:
        print("Unable to generate ChatCompletion response")
        print(f"Exception: {e}")
        return e


def generate_test_cases_from_tools(tools, examples):
    tools_descriptions = "\n".join([
        f"- {tool['function']['name']}: {tool['function']['description']} (Parameters: {', '.join(tool['function']['parameters']['properties'].keys())})"
        for tool in tools
    ])

    prompt = f"""
    Create a series of test cases for evaluating the tool-using capabilities of a language model. The test cases should be relevant to the domain and should utilize the provided tools. Each test case should include the following fields:
    - query: A question or request related to the domain.
    - expected_function: The function that should be called to answer the query.
    - function_arguments: The arguments that should be passed to the function.
    - expected_response: The expected response from the function.

    The available tools are:
    {tools_descriptions}

    Here are some examples to follow:

    {examples}

    Now, generate 10 more test cases in the same format utilizing the provided tools. Make sure the output format is JSON and enclose the result in triple backticks (```):
    """
    return prompt

# Example usage
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_medical_definition",
            "description": "Get the definition of a medical term",
            "parameters": {
                "type": "object",
                "properties": {
                    "term": {"type": "string", "description": "The medical term to define"}
                },
                "required": ["term"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_treatment_options",
            "description": "Get treatment options for a disease",
            "parameters": {
                "type": "object",
                "properties": {
                    "disease": {"type": "string", "description": "The disease to get treatment options for"}
                },
                "required": ["disease"]
            }
        }
    }
]
examples = """
[
    {
        "query": "What is diabetes?",
        "expected_function": "get_medical_definition",
        "function_arguments": {"term": "diabetes"},
        "expected_response": "Diabetes is a chronic condition that affects the way the body processes blood sugar (glucose)."
    },
    {
        "query": "How can hypertension be treated?",
        "expected_function": "get_treatment_options",
        "function_arguments": {"disease": "hypertension"},
        "expected_response": "Treatment options for hypertension include lifestyle changes, such as diet and exercise, and medications like ACE inhibitors, beta-blockers, and diuretics."
    }
]
"""
sys_prompt = generate_test_cases_from_tools(tools, examples)
messages=[
    {"role": "system", "content": sys_prompt}
]
resp = chat_completion_request(messages, json_mode=True)
test_cases = eval(resp.choices[0].message.content)

参考

Function-calling & JSON-mode Evaluation

Benchmarking Agent Tool Use

What Are Tools Anyway? A Survey from the Language Model Perspective

Berkeley Function Calling Leaderboard

Toolformer

LATM

API-Bank

Tool-bench

ToolAlpaca