Skip to main content

概述

Plugin系统是LangChat Pro的扩展机制,允许开发者通过插件方式为AI助手添加各种工具能力,如时间查询、网络搜索、新闻获取等。

设计模式

策略模式(Strategy Pattern)

Plugin系统使用策略模式来:
  1. 统一的工具接口
  2. 灵活的工具注册机制
  3. 动态工具注入
  4. 热插拔支持

核心组件

DynamicToolFactory

路径: langchat-common/langchat-common-ai/src/main/java/cn/langchat/common/ai/component/DynamicToolFactory.java 接口定义:
/**
 * 动态工具工厂接口
 */
public interface DynamicToolFactory {
    
    /**
     * 动态构建工具并添加到AiServices Builder
     *
     * @param req         聊天请求
     * @param aiServices  AiServices Builder
     */
    void dynamicTools(LcChatReq req, AiServices<LangChatAgent> aiServices);
}
实现类:
  • PluginToolFactory - 插件工具工厂
  • McpToolFactory - MCP工具工厂
  • 自定义工具工厂

PluginToolFactory

职责

根据用户配置动态加载和注册插件工具

实现流程

@Component
public class PluginToolFactory implements DynamicToolFactory {
    
    @Resource
    private AigcPluginService pluginService;
    
    @Override
    public void dynamicTools(LcChatReq req, AiServices<LangChatAgent> aiServices) {
        log.debug("开始构建插件工具: userId={}, appId={}", 
                req.getUserId(), req.getAppId());
        
        // 1. 获取启用的插件
        var plugins = pluginService.getEnabledPlugins(
            req.getUserId(), 
            req.getAppId()
        );
        
        if (CollUtil.isEmpty(plugins)) {
            log.debug("未启用插件: userId={}, appId={}", 
                    req.getUserId(), req.getAppId());
            return;
        }
        
        // 2. 构建工具列表
        var tools = plugins.stream()
            .map(this::convertToTool)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
        
        if (CollUtil.isEmpty(tools)) {
            log.debug("插件工具构建为空: userId={}, appId={}", 
                    req.getUserId(), req.getAppId());
            return;
        }
        
        // 3. 添加到AiServices
        aiServices.tools(tools);
        
        log.info("插件工具构建完成: userId={}, appId={}, toolCount={}", 
                req.getUserId(), req.getAppId(), tools.size());
    }
    
    /**
     * 将插件转换为LangChain4j Tool
     */
    private Tool convertToTool(AigcPlugin plugin) {
        return Tool.builder()
            .name(plugin.getName())
            .description(plugin.getDescription())
            .addParameter("input", 
                JsonSchemaProperty.string()
                    .description("插件输入参数")
                    .build())
            .execute((toolExecutionRequest) -> {
                try {
                    return executePlugin(plugin, toolExecutionRequest);
                } catch (Exception e) {
                    log.error("插件执行失败: pluginName={}", plugin.getName(), e);
                    return "插件执行失败: " + e.getMessage();
                }
            })
            .build();
    }
    
    /**
     * 执行插件
     */
    private String executePlugin(
        AigcPlugin plugin, 
        ToolExecutionRequest request
    ) {
        String input = request.argumentsAsString();
        log.info("执行插件: pluginName={}, input={}", plugin.getName(), input);
        
        // 根据插件类型执行不同的逻辑
        return switch (plugin.getType()) {
            case "time" -> executeTimePlugin(input);
            case "search" -> executeSearchPlugin(input);
            case "news" -> executeNewsPlugin(input);
            default -> executeCustomPlugin(plugin, input);
        };
    }
}

内置插件

1. 时间工具插件

路径: langchat-plugin/langchat-plugin-tool-time/ 功能: 提供时间、日期相关工具 实现示例:
@Component
public class TimePlugin {
    
    /**
     * 获取当前时间
     */
    public String getCurrentTime() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
    
    /**
     * 获取当前日期
     */
    public String getCurrentDate() {
        return LocalDate.now().toString();
    }
    
    /**
     * 计算日期差
     */
    public String calculateDateDiff(String startDate, String endDate) {
        LocalDate start = LocalDate.parse(startDate);
        LocalDate end = LocalDate.parse(endDate);
        long days = ChronoUnit.DAYS.between(start, end);
        return String.format("日期差: %d天", Math.abs(days));
    }
}
工具定义:
Tool.builder()
    .name("get_current_time")
    .description("获取当前时间")
    .addParameter("timezone", 
        JsonSchemaProperty.string()
            .description("时区,如:Asia/Shanghai")
            .required(false)
            .build())
    .execute(request -> getCurrentTime(request))
    .build();

2. 百度搜索插件

路径: langchat-plugin/langchat-plugin-tool-baidusearch/ 功能: 提供网络搜索能力 实现示例:
@Component
public class BaiduSearchPlugin {
    
    @Value("${plugin.baidu.search.api-key}")
    private String apiKey;
    
    /**
     * 执行搜索
     */
    public String search(String query, int limit) {
        // 调用百度搜索API
        String url = String.format(
            "https://api.baidu.com/api/v1/search?q=%s&limit=%d",
            URLEncoder.encode(query, StandardCharsets.UTF_8),
            limit
        );
        
        // 发送HTTP请求
        // 返回搜索结果
    }
    
    /**
     * 获取搜索摘要
     */
    public String getSearchSummary(String query) {
        List<SearchResult> results = search(query, 5);
        StringBuilder summary = new StringBuilder();
        summary.append("搜索结果:\n");
        for (SearchResult result : results) {
            summary.append(String.format("- %s\n", result.getTitle()));
            summary.append(String.format("  %s\n\n", result.getSnippet()));
        }
        return summary.toString();
    }
}
工具定义:
Tool.builder()
    .name("web_search")
    .description("网络搜索,获取最新信息")
    .addParameter("query", 
        JsonSchemaProperty.string()
            .description("搜索关键词")
            .required(true)
            .build())
    .addParameter("limit", 
        JsonSchemaProperty.integer()
            .description("返回结果数量,默认5")
            .required(false)
            .build())
    .execute(request -> executeSearch(request))
    .build();

3. 头条新闻插件

路径: langchat-plugin/langchat-plugin-tool-toutiaonews/ 功能: 获取最新新闻资讯 实现示例:
@Component
public class ToutiaoNewsPlugin {
    
    /**
     * 获取最新新闻
     */
    public String getLatestNews(String category, int limit) {
        // 调用头条新闻API
        // 返回新闻列表
    }
    
    /**
     * 获取新闻详情
     */
    public String getNewsDetail(String newsId) {
        // 获取新闻详细内容
    }
}
工具定义:
Tool.builder()
    .name("get_latest_news")
    .description("获取最新新闻")
    .addParameter("category", 
        JsonSchemaProperty.string()
            .description("新闻分类:科技、财经、娱乐等")
            .required(false)
            .build())
    .addParameter("limit", 
        JsonSchemaProperty.integer()
            .description("返回新闻数量,默认5")
            .required(false)
            .build())
    .execute(request -> executeGetNews(request))
    .build();

插件配置

AigcPlugin

public class AigcPlugin {
    private String id;
    private String name;           // 插件名称
    private String type;           // 插件类型
    private String description;    // 插件描述
    private String config;        // 插件配置(JSON)
    private Boolean enabled;       // 是否启用
    private Integer priority;      // 优先级
}

插件配置示例

{
  "id": "plugin-001",
  "name": "time_plugin",
  "type": "time",
  "description": "时间工具插件",
  "config": {
    "timezone": "Asia/Shanghai",
    "dateFormat": "yyyy-MM-dd HH:mm:ss"
  },
  "enabled": true,
  "priority": 10
}

插件开发指南

步骤1: 创建插件模块

<project>
    <parent>
        <groupId>cn.langchat</groupId>
        <artifactId>langchat-plugin</artifactId>
        <version>2.3.1</version>
    </parent>
    <artifactId>langchat-plugin-tool-yourplugin</artifactId>
    <dependencies>
        <dependency>
            <groupId>cn.langchat</groupId>
            <artifactId>langchat-common-ai</artifactId>
        </dependency>
    </dependencies>
</project>

步骤2: 实现插件逻辑

@Component
public class YourPlugin {
    
    /**
     * 插件方法1
     */
    public String method1(String input) {
        // 实现逻辑
    }
    
    /**
     * 插件方法2
     */
    public String method2(String param1, String param2) {
        // 实现逻辑
    }
}

步骤3: 创建工具定义

@Component
public class YourPluginToolFactory implements DynamicToolFactory {
    
    @Resource
    private YourPlugin yourPlugin;
    
    @Override
    public void dynamicTools(LcChatReq req, AiServices<LangChatAgent> aiServices) {
        // 判断是否启用该插件
        if (!isPluginEnabled(req, "your_plugin")) {
            return;
        }
        
        // 构建工具列表
        List<Tool> tools = List.of(
            buildTool1(),
            buildTool2()
        );
        
        // 添加到AiServices
        aiServices.tools(tools);
    }
    
    private Tool buildTool1() {
        return Tool.builder()
            .name("method1")
            .description("方法1描述")
            .addParameter("input", 
                JsonSchemaProperty.string()
                    .description("输入参数")
                    .required(true)
                    .build())
            .execute(request -> {
                String input = request.argumentsAsString();
                return yourPlugin.method1(input);
            })
            .build();
    }
    
    private Tool buildTool2() {
        return Tool.builder()
            .name("method2")
            .description("方法2描述")
            .addParameter("param1", 
                JsonSchemaProperty.string()
                    .description("参数1")
                    .required(true)
                    .build())
            .addParameter("param2", 
                JsonSchemaProperty.string()
                    .description("参数2")
                    .required(false)
                    .build())
            .execute(request -> {
                Map<String, Object> args = request.argumentsAsMap();
                return yourPlugin.method2(
                    (String) args.get("param1"),
                    (String) args.get("param2")
                );
            })
            .build();
    }
    
    private boolean isPluginEnabled(LcChatReq req, String pluginName) {
        // 检查插件是否启用
        // 可以从数据库或配置中获取
        return true;
    }
}

步骤4: 注册插件

使用@Component注解,Spring会自动扫描并注册

步骤5: 配置插件

在数据库中添加插件配置:
INSERT INTO aigc_plugin (id, name, type, description, enabled)
VALUES (
    'plugin-your-001',
    'your_plugin',
    'custom',
    '你的自定义插件',
    1
);

工具调用流程

1. AI决定调用工具

用户问题:"今天是什么天气?"

LLM分析问题

识别需要调用工具: "get_weather"

准备工具调用请求

2. 工具执行

AiServices执行工具

调用对应的Tool

执行插件方法

获取结果

3. 结果返回

工具执行结果

返回给LLM

LLM根据工具结果生成最终回答

返回给用户

配置说明

application.yml

langchat:
  plugin:
    enabled: true                    # 是否启用插件系统
    timeout: 30                      # 插件执行超时时间(秒)
    max-retries: 3                    # 最大重试次数
    cache-enabled: true                # 是否启用结果缓存
    cache-ttl: 3600                    # 缓存时间(秒)

plugin:
  baidu:
    search:
      api-key: your-api-key
  time:
    timezone: Asia/Shanghai
  news:
    toutiao:
      api-key: your-api-key

最佳实践

1. 工具命名

  • 使用小写字母和下划线
  • 名称要清晰描述功能
  • 示例: get_weather, web_search, calculate_distance

2. 描述编写

  • 简洁明了
  • 说明工具的功能
  • 包含使用场景
  • 示例: “获取指定城市的当前天气信息”

3. 参数定义

  • 明确参数类型
  • 标注必需参数
  • 提供参数说明
  • 合理设置默认值

4. 错误处理

  • 捕获所有异常
  • 返回友好的错误信息
  • 记录详细日志
  • 考虑重试机制

5. 性能优化

  • 缓存插件结果
  • 使用异步执行
  • 设置超时时间
  • 避免阻塞操作

6. 安全考虑

  • 验证输入参数
  • 限制调用频率
  • 过滤敏感操作
  • 记录调用日志

扩展示例

HTTP请求插件

@Component
public class HttpPlugin {
    
    @Resource
    private RestTemplate restTemplate;
    
    public String httpRequest(String url, String method, String body) {
        // 执行HTTP请求
        HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
        
        HttpEntity<String> entity = new HttpEntity<>(body);
        ResponseEntity<String> response = restTemplate.exchange(
            url, httpMethod, entity, String.class
        );
        
        return response.getBody();
    }
}
工具定义:
Tool.builder()
    .name("http_request")
    .description("执行HTTP请求")
    .addParameter("url", 
        JsonSchemaProperty.string()
            .description("请求URL")
            .required(true)
            .build())
    .addParameter("method", 
        JsonSchemaProperty.string()
            .description("请求方法:GET, POST, PUT, DELETE")
            .required(true)
            .build())
    .addParameter("body", 
        JsonSchemaProperty.string()
            .description("请求体(POST/PUT使用)")
            .required(false)
            .build())
    .execute(request -> executeHttpRequest(request))
    .build();

数据库查询插件

@Component
public class DatabasePlugin {
    
    @Resource
    private JdbcTemplate jdbcTemplate;
    
    public String executeQuery(String sql) {
        List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
        return JsonUtil.toJson(results);
    }
}
工具定义:
Tool.builder()
    .name("execute_query")
    .description("执行SQL查询")
    .addParameter("sql", 
        JsonSchemaProperty.string()
            .description("SQL查询语句")
            .required(true)
            .build())
    .execute(request -> {
        String sql = request.argumentsAsString();
        // 安全检查,限制为SELECT语句
        if (!sql.trim().toUpperCase().startsWith("SELECT")) {
            throw new SecurityException("只允许SELECT语句");
        }
        return databasePlugin.executeQuery(sql);
    })
    .build();

性能优化

1. 插件缓存

@Component
public class CachedPlugin {
    
    @Cacheable(value = "plugin", key = "#input", unless = "#result == null")
    public String executeWithCache(String input) {
        // 插件逻辑
    }
}

2. 异步执行

@Component
public class AsyncPlugin {
    
    @Async
    public CompletableFuture<String> executeAsync(String input) {
        // 异步执行
    }
}

3. 批量处理

@Component
public class BatchPlugin {
    
    public List<String> executeBatch(List<String> inputs) {
        // 批量处理
    }
}

参考文档