知识目录 / Agent 开发

自定义工具开发:扩展 Agent 的能力边界

学习如何开发自定义工具,让 Agent 能做任何你想做的事情。

自定义工具开发:扩展 Agent 的能力边界

前言

内置工具能做很多事情,但总有它们覆盖不到的地方。

也许你想让 Agent 能调用公司的内部 API,也许你想让它能操作数据库,也许你想让它能发送邮件……这些都需要自定义工具。

好消息是,Agent4J 的自定义工具系统非常简单。你只需要:

  1. 定义一个参数类
  2. 实现一个工具类
  3. 注册到 Agent

就这么简单。

第一个自定义工具:天气查询

让我们从一个简单的例子开始:一个查询天气的工具。

第一步:定义参数

java
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

第二步:实现工具

java
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 会根据这个描述来决定是否使用工具

第三步:注册工具

java
AgentClient agent = new AgentClient();
agent.setModel(llm);
agent.getTools().add(new WeatherTool());

就这样,你的自定义工具就完成了!

完整示例

java
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 会:

  1. 理解你的问题:"今天北京天气怎么样?"
  2. 决定调用 get_weather 工具
  3. 解析参数:city = "北京"
  4. 调用工具,获取天气信息
  5. 用自然语言回答你

进阶:数据库查询工具

让我们做一个更实用的例子:一个查询数据库的工具。

参数定义

java
public class QueryParam extends ToolParam {
    
    @Param(description = "SQL 查询语句")
    private String sql;
    
    @Param(required = false, description = "最大返回行数,默认 100")
    private String limit;
    
    // getter 和 setter
}

工具实现

java
@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();
        }
    }
}

使用示例

java
// 创建数据源
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 属性:

java
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. 复杂参数

参数可以是复杂的对象:

java
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

java
public class AnalyzeImageParam extends ToolParam {
    
    @Param(description = "图片文件路径")
    private String imagePath;
    
    @Param(description = "分析提示")
    private String prompt;
}

工具设计最佳实践

1. 描述要详细

LLM 是根据描述来决定是否使用工具的。描述越详细,LLM 越能正确使用:

java
@ToolInfo(
    name = "search_users", 
    description = "搜索用户。支持按姓名、邮箱、手机号搜索。返回用户 ID、姓名、邮箱、注册时间。最多返回 100 条结果。"
)

2. 参数描述要清晰

java
@Param(description = "搜索关键词。可以是姓名(如 '张三')、邮箱(如 'zhangsan@example.com')或手机号(如 '13800138000')")
private String keyword;

3. 返回值要结构化

返回给 LLM 的结果最好是结构化的文本,比如 JSON 或表格:

java
// 不好的返回
return "找到用户张三,邮箱是 zhangsan@example.com";

// 好的返回
return """
        {
            "users": [
                {
                    "id": "12345",
                    "name": "张三",
                    "email": "zhangsan@example.com",
                    "registeredAt": "2024-01-15"
                }
            ],
            "total": 1
        }
        """;

4. 错误处理要友好

java
@Override
public String execute(MyParam param) {
    try {
        // 业务逻辑
        return result;
    } catch (Exception e) {
        // 返回友好的错误信息,而不是抛出异常
        return "操作失败:" + e.getMessage();
    }
}

5. 考虑安全性

如果工具涉及敏感操作,要添加权限检查:

java
@Override
public String execute(DeleteUserParam param) {
    // 检查权限
    if (!hasPermission(param.getUserId())) {
        return "没有权限执行此操作";
    }
    
    // 执行删除
    deleteUser(param.getUserId());
    return "用户已删除";
}

自定义技能

如果你有多个相关的工具,可以把它们组合成一个技能:

java
// 创建工具
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 更好地使用这些工具。

实际案例:邮件发送工具

让我们做一个完整的例子:一个发送邮件的工具。

参数定义

java
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
}

工具实现

java
@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();
        }
    }
}

使用示例

java
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 做任何你想做的事情。

开发自定义工具的步骤:

  1. 定义参数类,继承 ToolParam
  2. 实现工具类,实现 Tool<T> 接口
  3. 注册到 Agent

记住:

  • 描述要详细
  • 参数要清晰
  • 返回值要结构化
  • 错误处理要友好
  • 考虑安全性

在下一篇文章中,我们将通过一个完整的实战案例,看看如何用 Agent4J 构建一个真实的项目。


项目地址https://github.com/onlyGuo/agent4j

上一篇内置工具与技能:Agent 开箱即用的能力

下一篇实战案例:用 Agent4J 构建全栈项目