结构化工具调用的两个实现

AI 模型在与人类交互时通常使用自然语言进行交流。但如果它们需要与需要结构化格式的外部系统交互怎么办?工具调用,也称为功能调用,在这种情况下很有帮助,使大型语言模型(LLM)能够生成结构化的响应。

我们将代码分为三个不同的 Python 文件,分别用于 LangChain 和 Groq 实现:

  • app.py -> 运行模型的主要文件。
  • models.py -> 定义结构化响应模式以确保数据一致性。
  • tools.py -> 包含 LLM 将调用以执行操作的功能。

1、models.py

这段代码对 Groq 和 LangChain 实现都是通用的。

from enum import Enum  
  
class ActionEnum(str, Enum):  
    create = 'create'  
    write = 'write'  
    read = 'read'

定义允许的操作:这防止了 LLM 选择自己的操作,如 mkdirrundelete,并强制它只能选择 createwriteread

from pydantic import BaseModel, Field, ConfigDict  
  
class ToolCall(BaseModel):  
    model_config = ConfigDict(use_enum_values=True)  
  
    path: str = Field(description='文件名')  
    action: ActionEnum = Field(description='保存在文件中的内容。')  
    content: str = Field(description='传递给命令的参数。')
  • path — LLM 想要创建的文件/文件夹名称。
  • action — 必须是 "create""write""read" 中的一个。
  • content — 要写入文件的数据(如果适用)。
class ResponseModel(BaseModel):  
    tool_calls: list[ToolCall]

确保可以处理多个工具调用。

示例输出:

{
“tool_calls”: [
{“action”: “create”, “path”: “notes.txt”, “content”: “”},
{“action”: “write”, “path”: “notes.txt”, “content”: “这是示例。”}
]
}

2、tools.py

import os  
from typing import Literal  
from langchain.tools import tool  
  
@tool  
def run_command(path: str, action: Literal["create", "write", "read"], content: str = None):  
    """   
    处理创建文件或文件夹、将内容写入文件或将内容从文件中读取的操作。  
    """  
    dirname = os.path.dirname(path)  
      
    if dirname and not os.path.exists(dirname):  
        os.makedirs(dirname, exist_ok=True)  
      
    if action == 'create':  
        if '.' in os.path.basename(path):  
            with open(path, 'w', encoding='utf-8') as file:  
                file.write(content)  
                print(f"{path} 文件已创建。")  
        else:  
            os.makedirs(path, exist_ok=True)  
            print(f"{path} 文件夹已创建。")  
  
  
    elif action == 'write':  
        if not os.path.basename(path) or '.' not in os.path.basename(path):  
            raise ValueError("无法向文件夹写入内容。")  
          
        with open(path, 'w', encoding='utf-8') as file:  
            file.write(content)  
            print(f"内容已成功写入 {path}。")  
  
      
    elif action == 'read':  
        if not os.path.exists(path):  
            raise FileNotFoundError(f"未找到 {path} 的文件。")  
          
        if os.path.isdir(path):  
            raise IsADirectoryError('无法读取文件夹路径。')  
          
        with open(path, 'r', encoding='utf-8') as file:  
            print(f"正在读取 {path} 的内容。")  
            return file.read()  
          
      
    else:  
        raise ValueError(f"不支持的操作:{action}")

@tool 装饰器将此函数注册为 LangChain 中的可调用工具,我们不用于 Groq 实现。

该函数接受 三个参数

  • path — 文件或文件夹名称。
  • action — 必须是 "create""write""read" 中的一个。
  • content — 要写入的数据(仅在 "write" 时需要)。

run_command :这赋予 LLM 创建文件、写入内容和从文件中读取的能力。

3、app.py — LangChain 实现

import os  
import json  
import argparse  
  
from langchain.tools import StructuredTool  
from langchain_groq import ChatGroq  
from tools import run_command  
from models import ResponseModel  
  
load_dotenv()  
apiKey = os.getenv('GROQ_API_KEY')  
  
client = ChatGroq(temperature=0.6, model='deepseek-r1-distill-llama-70b', api_key=apiKey)

我们使用 ChatGroq 初始化一个 Groq 客户端,参数如下:

  1. temperature=0.6 — 平衡创造力和精确度。
  2. model='deepseek-r1-distill-llama-70b' — 指定一个 LLM 模型。
  3. api_key=apiKey — 认证以访问 Groq 的 API
run_command_tool = StructuredTool.from_function(  
        func=run_command,  
        name='run_command',  
        description="创建、写入或读取文件或文件夹。",  
        args_schema=ResponseModel  
    )

为了使 run_command 成为 LLM 可执行的工具,我们使用 LangChain 的 StructuredTool

  • from_function(func=run_command) — 告诉 LangChain 这是 LLM 需要调用的函数。
  • name='run_command' — LLM 将通过名称 "run_command" 识别此工具。
  • description="创建、写入或读取文件或文件夹。" — 给 LLM 一个关于此函数功能的简短说明。
  • args_schema=ResponseModel — 确保函数遵循结构化的输入格式。
model = client.bind_tools([run_command_tool])

然后我们将 run_command 工具绑定到模型,以便当任务涉及文件处理时,它知道可以使用 run_command,而不是仅仅回复文本。

def run_model(query):  
  
    system_prompt = "你是一个有用的助手,帮助用户而不产生错误。"  
  
    messages = [  
        {"role": "system", "content": system_prompt},  
        {"role": "user", "content": query},  
    ]

messages 列表包含**,**系统提示,告诉 LLM 如何行为,以及用户查询,这是由用户发送给模型的内容。

例如:

“创建一个 Python 文件来预测给定年份是否为闰年。”
    llm_call = model.invoke(messages)  
    tool_calls = llm_call.additional_kwargs.get("tool_calls", [])

如果 AI 决定使用工具(因为它可能并不总是需要工具),我们提取工具调用从响应中。

    if tool_calls:  
        for tool_call in tool_calls:  
            selected_tool = {"run_command": run_command}[tool_call.get("function")["name"].lower()]  
            arguments = tool_call["function"]["arguments"]  
            for args in json.loads(arguments)["tool_calls"]:  
                selected_tool.invoke(args)  
      
    return "过程完成。"
  • 由于我们只有一个工具 (run_command),我们将 "run_command" 映射到实际的 Python 函数。
  • 参数仍然是 JSON 字符串格式,所以我们使用 json.loads(arguments) 将其转换为字典。
  • LLM 可能会一次请求多个操作(例如,先创建一个文件夹,然后在其中写入文件),所以我们循环遍历 "tool_calls" 并逐个执行它们。
  • 我们不需要返回任何花哨的东西,因为 LLM 已经执行了操作。
if __name__ == '__main__':  
    parser = argparse.ArgumentParser()  
    parser.add_argument("query", help="你想传递给模型的查询。例如:创建一个 Python 文件来预测给定年份是否为闰年。")  
    args = parser.parse_args()  
    run_model(args.query)

这段代码设置了一个命令行界面 (CLI),允许用户在运行脚本时作为参数传递查询。它接收用户输入,将其发送给模型 (run_model(args.query)),并根据结构化工具调用框架执行任何必要的操作。

4、app.py — Groq 实现

import os  
import argparse  
from dotenv import load_dotenv  
  
import instructor  
from groq import Groq  
  
from models import ResponseModel  
from tools import run_command  
  
load_dotenv()  
apiKey = os.getenv('GROQ_API_KEY')  
  
client = instructor.from_groq(Groq(), model=instructor.Mode.JSON)

像我们之前做的那样,我们使用 instructor.from_groq() 初始化 Groq 客户端以获取结构化输出。

tool_schema = {  
    "name": "run_command",  
    "description": "创建、写入或读取文件或文件夹。",  
    "parameters": {  
        "type": "object",  
        "properties": {  
            "path": {  
                "type": "string",  
                "description": "以 / 结尾的文件夹名称或文件名。例如:app.py, env/"   
            },  
            "action": {  
                "type": "string",  
                "description": "需要执行的操作,只能是 create、read、write 中的一个。",  
                "enum" : ["create", "write", "read"]  
            },  
            "content": {  
                "type": "string",  
                "description": "需要写入文件的内容。"  
            }  
        },  
        "required": ["path", "action"]  
    }  
}
  • 使用 enum"action" 字段限制为 "create""write""read",防止无效操作。
  • 此模式确保 LLM 生成所需的字段。
def run_model(query):  
  
    system_prompt = "你是一个有用的助手,帮助用户而不产生错误。"  
  
    messages = [  
        {"role": "system", "content": system_prompt},  
        {"role": "user", "content": query},  
    ]

定义 messages 并设置系统提示和查询变量,如我们之前所做的。

llm_call = client.chat.completions.create(  
        model='deepseek-r1-distill-llama-70b',  
        response_model=ResponseModel,  
        messages=messages,  
        tools=tool_schema,  
        tool_choice="auto"  
    )
  • 调用 Groq 的 LLM,传递结构化的消息。
  • response_model=ResponseModel 确保响应遵循预期格式。
  • tools=tool_schema 告诉模型它可以使用 run_command 工具。
  • tool_choice="auto" 允许模型决定是否调用工具或仅用文本回复。
    for tool_call in llm_call.tool_calls:  
        path = tool_call.path  
        action = tool_call.action  
        content = tool_call.content  
  
        run_command(path, action, content)  
  
    return None
  • 检查 LLM 是否请求了任何工具执行。
  • 如果是这样,它提取 pathactioncontent 字段并执行 run_command()

5、执行结果


原文链接:Structured Tool Calling with LangChain and Groq

汇智网翻译整理,转载请标明出处