引言
在智能体系统(Agent)中,工具(Tool)是让大模型从"纯文本回答"升级为"可执行任务"的关键能力。本章将从最简单、最直观的方式出发,演示如何仅通过提示词让 LLM 理解你的函数并主动调用它们。随后,我们会进一步封装工具类与装饰器,使工具体系更加规范、可扩展,为构建更复杂的智能体打下基础。
新增本文内容后的完整代码: Vero。
用提示词教会模型调用你的函数
在智能体(Agent)体系里,"工具(Tool)"本质上就是你提供给模型的一段可执行逻辑,通常表现为:
- 一个普通的 Python 函数
- 或一个带方法的类
- 或一个 API 调用包装
对模型来说,它不关心你的工具由什么构成,只关心两件事:
- 名字 (如
add) - 可用参数 (如
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℃之间。