自定义工具开发:扩展 Agent 的能力边界
学习如何开发自定义工具,让 Agent 能做任何你想做的事情。
自定义工具开发:扩展 Agent 的能力边界
前言
内置工具能做很多事情,但总有它们覆盖不到的地方。
也许你想让 Agent 能调用公司的内部 API,也许你想让它能操作数据库,也许你想让它能发送邮件……这些都需要自定义工具。
好消息是,Agent4J 的自定义工具系统非常简单。你只需要:
- 定义一个参数类
- 实现一个工具类
- 注册到 Agent
就这么简单。
第一个自定义工具:天气查询
让我们从一个简单的例子开始:一个查询天气的工具。
第一步:定义参数
import ink.icoding.llm.core.tool.ToolParam;
import ink.icoding.llm.core.tool.annotations.Param;
public class WeatherParam extends ToolParam {
@Param(description = "城市名称,比如 '北京'、'上海'")
private String city;
@Param(required = false, description = "温度单位,celsius 或 fahrenheit,默认 celsius")
private String unit;
// getter 和 setter
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
}
参数类继承 ToolParam,每个字段用 @Param 注解标记。
@Param 注解有两个属性:
description:参数的描述,LLM 会根据这个描述来理解参数的含义required:是否必填,默认为 true
第二步:实现工具
import ink.icoding.llm.core.tool.Tool;
import ink.icoding.llm.core.tool.annotations.ToolInfo;
@ToolInfo(name = "get_weather", description = "查询指定城市的天气信息")
public class WeatherTool implements Tool<WeatherParam> {
@Override
public String execute(WeatherParam param) {
String city = param.getCity();
String unit = param.getUnit() != null ? param.getUnit() : "celsius";
// 这里调用真实的天气 API
// 为了示例,我们返回模拟数据
String weather = """
城市:%s
天气:晴朗
温度:25°C
湿度:60%
风力:3级
""".formatted(city);
return weather;
}
}
工具类实现 Tool<WeatherParam> 接口,用 @ToolInfo 注解标记。
@ToolInfo 注解有两个属性:
name:工具的名称,LLM 会用这个名称来调用工具description:工具的描述,LLM 会根据这个描述来决定是否使用工具
第三步:注册工具
AgentClient agent = new AgentClient();
agent.setModel(llm);
agent.getTools().add(new WeatherTool());
就这样,你的自定义工具就完成了!
完整示例
public class WeatherExample {
```java
public class WeatherExample {
public static void main(String[] args) {
LLMModel llm = LLMModel.create(ModelType.OpenAI, "https://token-plan-cn.xiaomimimo.com", "mimo-v-2.5-pro", "your-api-key");
AgentClient agent = new AgentClient();
agent.setName("WeatherAssistant");
agent.setDescription("一个天气查询助手");
agent.setModel(llm);
agent.getTools().add(new WeatherTool());
agent.createSession()
.command("今天北京天气怎么样?")
.then(new AgentResultHandler() {
public void onMessage(String msg) {
System.out.print(msg);
}
})
.error(e -> e.printStackTrace());
}
}
运行这段代码,Agent 会:
- 理解你的问题:"今天北京天气怎么样?"
- 决定调用
get_weather工具 - 解析参数:city = "北京"
- 调用工具,获取天气信息
- 用自然语言回答你
进阶:数据库查询工具
让我们做一个更实用的例子:一个查询数据库的工具。
参数定义
public class QueryParam extends ToolParam {
@Param(description = "SQL 查询语句")
private String sql;
@Param(required = false, description = "最大返回行数,默认 100")
private String limit;
// getter 和 setter
}
工具实现
@ToolInfo(name = "query_database", description = "执行 SQL 查询,返回查询结果")
public class DatabaseQueryTool implements Tool<QueryParam> {
private final DataSource dataSource;
public DatabaseQueryTool(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public String execute(QueryParam param) {
String sql = param.getSql();
int limit = param.getLimit() != null ? Integer.parseInt(param.getLimit()) : 100;
// 添加 limit 限制
if (!sql.toLowerCase().contains("limit")) {
sql += " LIMIT " + limit;
}
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
StringBuilder result = new StringBuilder();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
// 表头
for (int i = 1; i <= columnCount; i++) {
if (i > 1) result.append("\t");
result.append(metaData.getColumnName(i));
}
result.append("\n");
// 数据
int rowCount = 0;
while (rs.next() && rowCount < limit) {
for (int i = 1; i <= columnCount; i++) {
if (i > 1) result.append("\t");
result.append(rs.getString(i));
}
result.append("\n");
rowCount++;
}
return result.toString();
} catch (SQLException e) {
return "查询失败:" + e.getMessage();
}
}
}
使用示例
// 创建数据源
DataSource dataSource = ... // 你的数据源配置
// 创建工具
DatabaseQueryTool dbTool = new DatabaseQueryTool(dataSource);
// 创建 Agent
AgentClient agent = new AgentClient();
agent.setModel(llm);
agent.getTools().add(dbTool);
// 查询
agent.createSession()
.command("请查询用户表中最近注册的 10 个用户")
.then(handler)
.error(errorHandler);
Agent 会自动生成 SQL 语句,执行查询,返回结果。
高级特性
1. 枚举参数
如果参数只有几个固定的值,可以使用 enums 属性:
public class SearchParam extends ToolParam {
@Param(description = "搜索类型", enums = {"web", "image", "video"})
private String type;
@Param(description = "搜索关键词")
private String query;
}
LLM 会知道 type 参数只能是 "web"、"image" 或 "video" 之一。
2. 复杂参数
参数可以是复杂的对象:
public class CreateOrderParam extends ToolParam {
@Param(description = "用户 ID")
private String userId;
@Param(description = "商品列表,JSON 格式,如 [{\"productId\": \"1\", \"quantity\": 2}]")
private String items;
@Param(description = "收货地址")
private String address;
}
对于复杂参数,建议在描述中给出示例格式。
3. 文件参数
如果工具需要处理文件,可以使用 MessageAttachment:
public class AnalyzeImageParam extends ToolParam {
@Param(description = "图片文件路径")
private String imagePath;
@Param(description = "分析提示")
private String prompt;
}
工具设计最佳实践
1. 描述要详细
LLM 是根据描述来决定是否使用工具的。描述越详细,LLM 越能正确使用:
@ToolInfo(
name = "search_users",
description = "搜索用户。支持按姓名、邮箱、手机号搜索。返回用户 ID、姓名、邮箱、注册时间。最多返回 100 条结果。"
)
2. 参数描述要清晰
@Param(description = "搜索关键词。可以是姓名(如 '张三')、邮箱(如 'zhangsan@example.com')或手机号(如 '13800138000')")
private String keyword;
3. 返回值要结构化
返回给 LLM 的结果最好是结构化的文本,比如 JSON 或表格:
// 不好的返回
return "找到用户张三,邮箱是 zhangsan@example.com";
// 好的返回
return """
{
"users": [
{
"id": "12345",
"name": "张三",
"email": "zhangsan@example.com",
"registeredAt": "2024-01-15"
}
],
"total": 1
}
""";
4. 错误处理要友好
@Override
public String execute(MyParam param) {
try {
// 业务逻辑
return result;
} catch (Exception e) {
// 返回友好的错误信息,而不是抛出异常
return "操作失败:" + e.getMessage();
}
}
5. 考虑安全性
如果工具涉及敏感操作,要添加权限检查:
@Override
public String execute(DeleteUserParam param) {
// 检查权限
if (!hasPermission(param.getUserId())) {
return "没有权限执行此操作";
}
// 执行删除
deleteUser(param.getUserId());
return "用户已删除";
}
自定义技能
如果你有多个相关的工具,可以把它们组合成一个技能:
// 创建工具
WebSearchTool searchTool = new WebSearchTool();
WebFetchTool fetchTool = new WebFetchTool();
WebParseTool parseTool = new WebParseTool();
// 创建技能
Skill webSkill = new Skill(
"Web Research",
"从互联网上搜索和获取信息",
List.of(searchTool, fetchTool, parseTool),
"""
## 网页搜索指南
1. 使用 `web_search` 搜索相关网页
2. 使用 `web_fetch` 获取网页完整内容
3. 使用 `web_parse` 解析网页内容
4. 回答时要注明来源
### 注意事项
- 搜索时使用简洁的关键词
- 获取内容时注意网页是否需要登录
- 解析时提取关键信息,忽略广告和无关内容
""");
// 注册技能
agent.getSkills().add(webSkill);
技能的使用指南会帮助 LLM 更好地使用这些工具。
实际案例:邮件发送工具
让我们做一个完整的例子:一个发送邮件的工具。
参数定义
public class SendEmailParam extends ToolParam {
@Param(description = "收件人邮箱,多个用逗号分隔")
private String to;
@Param(required = false, description = "抄送邮箱,多个用逗号分隔")
private String cc;
@Param(description = "邮件主题")
private String subject;
@Param(description = "邮件正文,支持 HTML 格式")
private String body;
@Param(required = false, description = "是否是 HTML 格式,默认 true")
private String isHtml;
// getter 和 setter
}
工具实现
@ToolInfo(name = "send_email", description = "发送邮件。支持多人发送、抄送、HTML 格式。")
public class SendEmailTool implements Tool<SendEmailParam> {
private final String smtpHost;
private final int smtpPort;
private final String username;
private final String password;
public SendEmailTool(String smtpHost, int smtpPort, String username, String password) {
this.smtpHost = smtpHost;
this.smtpPort = smtpPort;
this.username = username;
this.password = password;
}
@Override
public String execute(SendEmailParam param) {
try {
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost);
props.put("mail.smtp.port", String.valueOf(smtpPort));
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(username));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(param.getTo()));
if (param.getCc() != null && !param.getCc().isEmpty()) {
message.setRecipients(Message.RecipientType.CC,
InternetAddress.parse(param.getCc()));
}
message.setSubject(param.getSubject());
boolean isHtml = param.getIsHtml() == null ||
param.getIsHtml().equalsIgnoreCase("true");
if (isHtml) {
message.setContent(param.getBody(), "text/html; charset=utf-8");
} else {
message.setText(param.getBody());
}
Transport.send(message);
return "邮件发送成功。收件人:" + param.getTo();
} catch (Exception e) {
return "邮件发送失败:" + e.getMessage();
}
}
}
使用示例
SendEmailTool emailTool = new SendEmailTool(
"smtp.gmail.com", 587,
"your-email@gmail.com", "your-password"
);
agent.getTools().add(emailTool);
agent.createSession()
.command("""
请给 team@company.com 发一封邮件:
主题:项目进度汇报
内容:本周完成了用户模块开发,下周计划完成订单模块。
""")
.then(handler)
.error(errorHandler);
总结
自定义工具是 Agent4J 最强大的特性之一。通过自定义工具,你可以让 Agent 做任何你想做的事情。
开发自定义工具的步骤:
- 定义参数类,继承
ToolParam - 实现工具类,实现
Tool<T>接口 - 注册到 Agent
记住:
- 描述要详细
- 参数要清晰
- 返回值要结构化
- 错误处理要友好
- 考虑安全性
在下一篇文章中,我们将通过一个完整的实战案例,看看如何用 Agent4J 构建一个真实的项目。