Qwen模型集成ReAct框架:AI Agent构建实战与外部工具调用详解

41

在人工智能领域,大型语言模型(LLM)正以前所未有的速度发展。其中,Qwen模型以其卓越的性能和开源特性,受到了广泛关注。为了进一步提升Qwen模型的能力,使其能够更好地服务于实际应用,本文将深入探讨如何利用ReAct框架,为Qwen模型构建一个能够自动调用外部工具的AI Agent。

ReAct框架:赋予LLM思考与行动的能力

ReAct(Reasoning and Acting)框架是一种结合了推理(Reasoning)和行动(Acting)的AI架构。它允许大型语言模型像人类一样,在解决问题的过程中进行思考、规划,并采取相应的行动。通过ReAct框架,LLM可以与外部环境进行交互,获取所需的信息,并根据实际情况调整策略,从而更加高效地完成任务。

ReAct的核心思想在于,将复杂的任务分解为一系列小的步骤,并在每个步骤中进行推理和行动。模型首先根据当前的任务状态进行思考,然后选择合适的行动,执行该行动后,模型会观察到新的状态,并再次进行思考和行动,直到完成整个任务。这种迭代式的过程使得模型能够不断地学习和改进,从而提高解决问题的能力。

ReAct原理

搭建Qwen模型的ReAct环境

为了实践ReAct框架在Qwen模型上的应用,我们需要进行以下步骤:

1. 环境准备

首先,确保你已经安装了以下Python库:

  • transformers:用于加载和使用Qwen模型。
  • tiktoken:用于处理文本分词。

你可以使用pip命令安装这些库:

pip install transformers tiktoken

2. 加载Qwen模型

接下来,我们需要加载Qwen模型。你可以从Hugging Face Model Hub下载Qwen模型的预训练权重。这里以Qwen-7B-Chat模型为例:

from transformers import AutoModelForCausalLM, AutoTokenizer

model_path = './model/qwen/Qwen-7B-Chat'
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto", trust_remote_code=True)

3. 构建提示词模板

为了让Qwen模型能够理解我们的意图,并按照ReAct框架进行推理和行动,我们需要构建一个合适的提示词模板。该模板应该包含以下几个部分:

  • Question:用户提出的问题。
  • Thought:模型应该思考的内容。
  • Action:模型应该采取的行动。
  • Action Input:行动所需的输入。
  • Observation:行动的结果。

一个简单的提示词模板如下所示:

PROMPT_REACT = """
Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do

Begin!

Question:{query}"""

4. 定义外部工具

ReAct框架的核心在于与外部环境的交互。为了让Qwen模型能够获取所需的信息,我们需要定义一些外部工具。在本例中,我们模拟一个课程信息数据库,用于存储课程的详细信息:

class CourseDatabase:
    def __init__(self):
        self.database = {
            "大模型技术实战":{
                "课时": 200,
                "每周更新次数": 3,
                "每次更新小时": 2
            },
             "机器学习实战":{
                "课时": 230,
                "每周更新次数": 2,
                "每次更新小时": 1.5
            },
            "深度学习实战":{
                "课时": 150,
                "每周更新次数": 1,
                "每次更新小时": 3
            },
            "AI数据分析":{
                "课时": 10,
                "每周更新次数": 1,
                "每次更新小时": 1
            },
        }
    def course_query(self, course_name):
        return self.database.get(course_name, "目前没有该课程信息")

course_db = CourseDatabase()

为了让Qwen模型能够调用该工具,我们需要定义工具的描述信息:

TOOLS = [
    {
        'name_for_human': '课程信息数据库',
        'name_for_model': 'CourseDatabase',
        'description_for_model': '课程信息数据库存储有各课程的详细信息,包括目前的上线课时,每周更新次数以及每次更新的小时数。通过输入课程名称,可以返回该课程的详细信息。',
        'parameters': [{
            'name': 'course_query',
            'description': '课程名称,所需查询信息的课程名称',
            'required': True,
            'schema': {
                'type': 'string'
            },
        }],
    },
    # 其他工具的定义可以在这里继续添加
]

5. 实现ReAct循环

有了提示词模板和外部工具,我们就可以实现ReAct循环了。ReAct循环包括以下几个步骤:

  1. 接收用户提出的问题。
  2. 将问题格式化为提示词,输入到Qwen模型中。
  3. Qwen模型根据提示词进行思考,并选择合适的行动。
  4. 如果行动是调用外部工具,则执行该工具,并获取结果。
  5. 将行动的结果输入到Qwen模型中。
  6. Qwen模型根据结果再次进行思考,并选择下一步的行动。
  7. 重复步骤4-6,直到完成任务。

以下是一个简单的ReAct循环的实现:

import json

def generate_action_prompt(query):
    """
    根据用户查询生成最终的动作提示字符串。
    函数内部直接引用全局变量 TOOLS, TOOL_DESC, 和 PROMPT_REACT.

    参数:
    - query: 用户的查询字符串。

    返回:
    - action_prompt: 格式化后的动作提示字符串。

    """

    tool_descs = []
    tool_names = []

    for info in TOOLS:
        tool_descs.append(
            TOOL_DESC.format(
                name_for_model = info['name_for_model'],
                name_for_human = info['name_for_human'],
                description_for_model = info['description_for_model'],
                parameters = json.dumps(info['parameters'], ensure_ascii=False),
            )
        )
        tool_names.append(info['name_for_model'])

    tool_descs_str = '\n\n'.join(tool_descs)
    tool_names_str = ','.join(tool_names)

    action_prompt = PROMPT_REACT.format(tool_descs=tool_descs_str, tool_names=tool_names_str, query=query)
    return action_prompt


def parse_plugin_action(text: str):
    """
    解析模型的ReAct输出文本提取名称及其参数。

    参数:
    - text: 模型ReAct提示的输出文本

    返回值:
    - action_name: 要调用的动作(方法)名称。
    - action_arguments: 动作(方法)的参数。
    """
    # 查找“Action:”和“Action Input:”的最后出现位置
    action_index = text.rfind('\nAction:')
    action_input_index = text.rfind('\nAction Input:')
    observation_index = text.rfind('\nObservation:')

    # 如果文本中有“Action:”和“Action Input:”
    if 0 <= action_index < action_input_index:
        if observation_index < action_input_index:
            text = text.rstrip() + '\nObservation:'
            observation_index = text.rfind('\nObservation:')

    # 确保文本中同时存在“Action:”和“Action Input:”
    if 0 <= action_index < action_input_index < observation_index:
        # 提取“Action:”和“Action Input:”之间的文本为动作名称
        action_name = text[action_index + len('\nAction:'):action_input_index].strip()
        # 提取“Action Input:”之后的文本为动作参数
        action_arguments = text[action_input_index + len('\nAction Input:'):observation_index].strip()
        return action_name, action_arguments

    # 如果没有找到符合条件的文本,返回空字符串
    return '', ''


def execute_plugin_from_react_output(response):
    """
    根据模型的ReAct输出执行相应的插件调用,并返回调用结果。

    参数:
    - response: 模型的ReAct输出字符串。

    返回:
    - result_dict: 包括状态码和插件调用结果的字典。
    """
    # 从模型的ReAct输出中提取函数名称及函数入参
    plugin_configuration = parse_plugin_action(response)
    first_config_line = plugin_configuration[1:][0].split('\n')[0]
    config_parameters = json.loads(first_config_line)
    result_dict = {"status_code": 200}

    for k, v in config_parameters.items():
        if k in TOOLS[0]["parameters"][0]['name']:
            # 通过eval函数执行存储在字符串中的python表达式,并返回表达式计算结果。其执行过程实质上是实例化类
            tool_instance = eval(TOOLS[0]["name_for_model"])
            # 然后通过getattr函数传递对象和字符串形式的属性或方法名来动态的访问该属性和方法h
            tool_func = getattr(tool_instance, k)
            # 这一步实际上执行的过程就是:course_db,course_query('大模型技术实战')
            tool_result = tool_func(v)
            result_dict["result"] = tool_result
            return result_dict

    result_dict["status_code"] = 404
    result_dict["result"] = "未找到匹配的插件配置"
    return result_dict


query = '请帮我查一下:我们的大模型技术实战课程目前一共上线了多少节?'
react_stop_words = [
    tokenizer.encode('Observation:'),
    tokenizer.encode('Observation:\n'),
]

action_prompt = generate_action_prompt(query)
response, history = model.chat(tokenizer, action_prompt, history=None,
                              stop_words_ids=react_stop_words)
tool_result = execute_plugin_from_react_output(response)
response += " " + str(tool_result)
response, history = model.chat(tokenizer, response, history=history,
                              stop_words_ids=react_stop_words)
print(response)

总结与展望

通过本文的实践,我们成功地将ReAct框架应用到了Qwen模型上,使其能够自动调用外部工具,从而增强了自身的功能。ReAct框架为大型语言模型提供了一种有效的与外部环境交互的方式,使其能够更好地解决实际问题。未来,我们可以进一步探索ReAct框架在更多场景下的应用,例如,结合知识图谱、搜索引擎等工具,构建更加智能的AI Agent。

附录:完整版的提示词模板代码

TOOL_DESC = """{name_for_model}: Call this tool to interact with the {name_for_human} API. What is the {name_for_human} API useful for? {description_for_model} Parameters:{parameters}
"""

PROMPT_REACT = """Answer the following questions as best you con. You have access to the following

{tool_descs}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can be repeated zero or more times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {query}"""