从零构建大模型智能体:实现可扩展工具系统

引言

在智能体系统(Agent)中,工具(Tool)是让大模型从"纯文本回答"升级为"可执行任务"的关键能力。本章将从最简单、最直观的方式出发,演示如何仅通过提示词让 LLM 理解你的函数并主动调用它们。随后,我们会进一步封装工具类与装饰器,使工具体系更加规范、可扩展,为构建更复杂的智能体打下基础。

新增本文内容后的完整代码: Vero

用提示词教会模型调用你的函数

在智能体(Agent)体系里,"工具(Tool)"本质上就是你提供给模型的一段可执行逻辑,通常表现为:

  • 一个普通的 Python 函数
  • 或一个带方法的类
  • 或一个 API 调用包装

对模型来说,它不关心你的工具由什么构成,只关心两件事:

  1. 名字 (如 add
  2. 可用参数 (如 a: int, b: int

只要模型知道"有哪些动作可以选",它就能按照提示词主动调用这些工具。

在真正实现可扩展的工具系统之前,我们从最朴素、也最容易理解的一步开始:使用提示词,让模型按照我们约定的格式主动调用工具。这种方法不依赖任何框架或协议,只需要自然语言告诉模型:

  • 有哪些函数
  • 每个函数的名字与参数
  • 什么时候需要调用它

只要提示词设计得足够清晰,模型就能根据你的要求自动输出一段"函数调用结构",你的程序只需要解析这段结构并执行相应函数即可。

假设我们有两个函数:

python 复制代码
def multiply(a: int, b: int):
    """返回 a 与 b 的乘积"""
    return a * b

def get_weather(location: str):
    """返回location的天气"""
    return f"{location}的天气:多云转阴. 23/19℃"

当我们需要计算乘法的时候,只要模型能输出函数名和参数,我们就可以真正去调用它,把结果返回给大模型。

下面是一个最小可行的提示词模板:

复制代码
你可以调用工具。以下是可用的工具:

```
1. multiply(a: int, b: int)
   - 返回 a 与 b 的乘积。
2. get_weather(location: str):
   - 返回location的天气
```

当需要使用工具时:
- 请使用格式: TOOL_CALL:tool_name:{"param1": 1, "param2": "abc"}
- parameters 必须是标准 JSON 对象,包含工具所需的所有参数
- 如果不需要使用工具,直接回答文本
"""

这里把这两个函数的签名以及docstr写到提示词中,然后指定调用函数的格式。以关键词TOOL_CALL触发,并且通过JSON格式明确指出每个参数的取值,方便后续解析。

我们来真正调用一下:

py 复制代码
from vero.core import ChatOpenAI, Message
# 确保.env配置好所需的环境变量,可以参考上一篇文章
llm = ChatOpenAI()

def multiply(a: int, b: int):
    """返回 a 与 b 的乘积"""
    return a * b

def get_weather(location: str):
    """返回location的天气"""
    return f"{location}的天气:多云转阴. 23/19℃"

system_prompt = """你可以调用工具。以下是可用的工具:
```
1. multiply(a: int, b: int)
   - 返回 a 与 b 的乘积。
2. get_weather(location: str):
   - 返回location的天气
```

当需要使用工具时:
- 请使用格式: TOOL_CALL:tool_name:{"param1": 1, "param2": "abc"}
- parameters 必须是标准 JSON 对象,包含工具所需的所有参数
- 如果不需要使用工具,直接回答文本
"""

messages = [Message.system(system_prompt)]
messages.append(Message.user("12346乘以98765等于多少"))

output = llm.generate(messages)
print(output)
复制代码
'TOOL_CALL:multiply:{"a": 12346, "b": 98765}'

模型不会尝试自己去瞎答,而是输出了我们指定的调用工具的格式。那这里怎么去调用工具呢?

当然首先需要解析出工具名称和参数列表,我们可以编写一个函数完成:

py 复制代码
from typing import Optional, Tuple, Dict
import re
import ast

def parse_tool_call(text: str) -> Tuple[bool, Optional[str], Optional[Dict]]:
    """
    解析 TOOL_CALL:{tool_name}:{parameters} 格式的字符串。

    参数:
        text (str): 模型输出文本

    返回:
        Tuple:
            - has_tool_call (bool): 是否包含工具调用
            - tool_name (str | None): 工具名称
            - params (dict | None): 参数字典
    """
    pattern = r"TOOL_CALL:(\w+):(.+)"
    match = re.search(pattern, text)
    if not match:
        return False, None, None

    tool_name = match.group(1)
    params_str = match.group(2)
    try:
        # ast.literal_eval 更安全,能解析 dict 风格参数
        params = ast.literal_eval(params_str)
    except Exception:
        params = None

    return True, tool_name, params

针对模型的输出,尝试解析出工具名称和参数:

py 复制代码
has_tool, tool_name, params = parse_tool_call(output)
print(has_tool, tool_name, params)
复制代码
True multiply {'a': 12346, 'b': 98765}

那我们怎么调用呢,我们基于python中函数也是一等公民,可以当成字典的value,然后基于抽取到的名称作为key去获取:

py 复制代码
# 简单工具注册
TOOLS = {
    "multiply": multiply,
    "get_weather": get_weather
}

if has_tool and tool_name in TOOLS:
  	# result = 1219352690
    result = TOOLS[tool_name](**params) # 把参数传递进去,得到函数调用结果
    messages.append(Message.assistant(f"TOOL_RESULT:{result}"))
    messages.append(Message.user("根据结果回答用户的问题"))
    # 再次调用模型生成最终回答
    final_output = llm.generate(messages)
    print("模型回答:", final_output)    
else:
    print("直接回答:", output)
复制代码
模型回答: 12346 乘以 98765 等于 1219352690。

这样就完成了一次工具的调用。我们可以打印出整个消息列表:

py 复制代码
for msg in messages:
    print(msg)
复制代码
[system] 你可以调用工具。以下是可用的工具:

```
1. multiply(a: int, b: int)
   - 返回 a 与 b 的乘积。
2. get_weather(location: str):
   - 返回location的天气
```

当需要使用工具时:
- 请使用格式: TOOL_CALL:tool_name:{"param1": 1, "param2": "abc"}
- parameters 必须是标准 JSON 对象,包含工具所需的所有参数
- 如果不需要使用工具,直接回答文本

[user] 12346乘以98765等于多少
[assistant] TOOL_RESULT:1219352690
[user] 根据结果回答用户的问题

如果改成询问天气,那么它会调用我们定义的获取天气函数:

py 复制代码
messages.append(Message.user("我想知道深圳明天的天气怎么样"))
output = llm.generate(messages)
复制代码
'\n\nTOOL_CALL:get_weather:{"location": "深圳"}'

最终回答:

复制代码
模型回答: 深圳明天的天气将多云转阴,气温在19℃到23℃之间。

这就是 所有现代 Agent 框架的核心能力让模型知道"什么时候需要行动",以及"如何输出行动的结构"。

通过这样一个简单的提示词协议,你就已经拥有了一个可扩展的工具系统雏形。后续你只要不断增加工具即可,模型也会自动根据语义挑选正确的工具。

封装工具类

我们把刚才调用工具所需的函数名称、参数列表、函数描述(docstr)封装到一个叫做Tool的类中,同时增加工具名称和工具输出类型:

py 复制代码
class Tool:
    """
    表示一个可被 LLM 智能体系统调用的"工具"。

    属性:
        name (str): 工具名称(默认为被包装函数的函数名)。
        description (str): 对工具功能的中文描述。
        func (callable): 实际执行逻辑的 Python 函数。
        arguments (list): 参数列表,每项为 (参数名, 参数类型, 默认值)。
        outputs (str): 返回值类型(根据函数返回注解解析)。
    """

    def __init__(self, name: str, description: str, func: callable,
                 arguments: list, outputs: str):
        self.name = name
        self.description = description
        self.func = func
        self.arguments = arguments
        self.outputs = outputs

    def to_string(self) -> str:
        """
        返回工具的结构化字符串形式描述,便于用于提示词或调试。
        """
        args_str = ", ".join(
            [
                f"{n}: {t}" + (f" = {d}" if d is not None else "")
                for (n, t, d) in self.arguments
            ]
        )

        return (
            f"工具名: {self.name}, "
            f"描述: {self.description}, "
            f"参数: {args_str}, "
            f"返回: {self.outputs}"
        )

    def __call__(self, *args, **kwargs):
        """
        调用包装的底层 Python 函数。
        """
        return self.func(*args, **kwargs)

既然有了Tool类,那么我们要怎么初始化呢?如果还是像前面那样每次手动编写代码太复杂了,所以我们可以定义一个装饰器,让它自动为我们抽取。

定义装饰器

如果你用过Langchain就会知道它有一个@tool装饰器可以直接加到函数上,非常方便。我们下面实现一个简化版的:

py 复制代码
import inspect

def tool(func):
    """
    将一个普通 Python 函数转换为 Tool 对象的装饰器。

    装饰器负责自动解析函数的结构信息,包括:
      - 函数名
      - 参数名称、类型、默认值
      - 返回值类型注解
      - 函数的 docstring 作为工具描述

    通过这种方式,工具接口更加规范、清晰,便于让 LLM 正确调用。

    返回:
        Tool: 构建完成的 Tool 实例。
    """

    signature = inspect.signature(func)
    arguments = []

    # 解析所有参数
    for param in signature.parameters.values():
        annotation = param.annotation
        type_str = annotation.__name__ if hasattr(annotation, "__name__") else str(annotation)

        # 解析默认值,没有默认值则设为 None
        default = None if param.default is inspect._empty else param.default

        arguments.append((param.name, type_str, default))

    # 解析返回值类型
    return_annotation = signature.return_annotation
    if return_annotation is inspect._empty:
        outputs = "None"
    else:
        outputs = (
            return_annotation.__name__
            if hasattr(return_annotation, "__name__")
            else str(return_annotation)
        )

    # 使用 docstring 作为描述
    description = func.__doc__ or "无描述。"

    name = func.__name__
    
    return Tool(
        name=name,
        description=description,
        func=func,
        arguments=arguments,
        outputs=outputs,
    )

应用tool装饰器

py 复制代码
from vero.tool import tool

# ----------------------------------------------------------------------
# 示例工具 1:multiply
# ----------------------------------------------------------------------
@tool
def multiply(a: int, b: int) -> int:
    """返回两个整数的乘积。"""
    return a * b


# 示例工具 2:get_weather
# ----------------------------------------------------------------------
@tool
def get_weather(location: str) -> str:
    """根据地点返回天气(模拟版)。"""
    fake_weather = {
        "北京": "晴转多云 5℃ ~ 12℃",
        "上海": "小雨 8℃ ~ 15℃",
        "广州": "多云 16℃ ~ 25℃",
        "深圳": "大部多云 19℃ ~ 26℃",
    }
    return fake_weather.get(location, "天气未知")


tools = [multiply, get_weather]

tool_descriptions = "\n".join([f"{tool.name}: {tool.description}" for tool in tools])
tool_by_names = {tool.name: tool for tool in tools}

print(tool_descriptions) # 所有工具的描述
print(tool_by_names) # 工具名称到工具对象的映射
复制代码
multiply: 返回两个整数的乘积。
get_weather: 根据地点返回天气(模拟版)。
{'multiply': <vero.tool.tool.Tool object at 0x1220ab4d0>, 'get_weather': <vero.tool.tool.Tool object at 0x1220aac10>}

还是和上面的例子一样,下篇文章我们将会把这些逻辑封装到Agent类中:

py 复制代码
system_prompt = f"""你可以调用工具。以下是可用的工具:

```
{tool_descriptions}
```

当需要使用工具时:
- 请使用格式: TOOL_CALL:tool_name:{{"param1": 1, "param2": "abc"}}
- parameters 必须是标准 JSON 对象,包含工具所需的所有参数
- 如果不需要使用工具,直接回答文本
"""

messages = [Message.system(system_prompt)]

messages.append(Message.user("我想知道深圳明天的天气怎么样"))

output = llm.generate(messages)
has_tool, tool_name, params = parse_tool_call(output)
if has_tool and tool_name in TOOLS:
    result = tool_by_names[tool_name](**params)
    messages.append(Message.user(f"TOOL_RESULT:{result} , 根据结果回答用户的问题"))
    # 再次调用模型生成最终回答
    final_output = llm.generate(messages)
    print("模型回答:", final_output)    
else:
    print("直接回答:", output)
复制代码
模型回答: 深圳明天的天气将是大部多云,气温在19℃到26℃之间。
相关推荐
i***586736 分钟前
自动驾驶---E2E架构演进
人工智能·架构·自动驾驶
二哈喇子!36 分钟前
如何在昇腾平台上部署与优化vLLM:高效推理与性能提升指南
人工智能
CV-杨帆37 分钟前
大模型生成(题目)安全
人工智能
SmartBrain37 分钟前
思考:用信任创造共同的远方
人工智能·华为·创业创新
汽车仪器仪表相关领域38 分钟前
PSN-1:氮气加速 + 空燃比双控仪 ——NOS 系统的 “安全性能双管家”
大数据·linux·服务器·人工智能·功能测试·汽车·可用性测试
汽车仪器仪表相关领域1 小时前
PSB-1:安全增压与空燃比双监控仪表 - 高性能引擎的 “双重安全卫士“
java·人工智能·功能测试·单元测试·汽车·可用性测试·安全性测试
攻城狮杰森1 小时前
AI·重启思维:Gemini 3 带你走进智能的下一个维度
人工智能·语言模型·ai作画·aigc·googlecloud
糖葫芦君1 小时前
20-Gradient Surgery for Multi-Task Learning
人工智能
paperxie_xiexuo1 小时前
从数据观测到学术断言:面向证据链构建的智能分析工具协同机制研究
大数据·人工智能·机器学习·数据分析