02、Langchain4j tools原理与核心实践(含自定义http插件)

文章目录

前言

博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

CSDN搜索:长路

视频平台:b站-Coder长路

文章配套源码

gitee:https://gitee.com/changluJava/demo-exer/tree/master/ai/langchain4j

Github:https://github.com/changluya/Java-Demos/tree/master/ai

介绍

本章节将会介绍下langchain4j中如何实现与ai的function tools调用实现,在langchain4j中给我们提供了多种方式去进行实现function tools。

初始环境搭建

技术栈选用

后端:JDK17、Langchain4j

前端:vite+vue2

pom依赖导入

在当前的demo中我们导入了相应的langchain4j starter以及对接了阿里百炼与基于Xorbits Inference部署的大模型,补充了前后端接口文档工具包:

xml 复制代码
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.2.6</spring-boot.version>
    <knife4j.version>4.3.0</knife4j.version>
    <langchain4j.version>1.3.0</langchain4j.version>
    <mybatis-plus.version>3.5.11</mybatis-plus.version>
    <langchain4j.community.version>1.3.0-beta9</langchain4j.community.version>
</properties>

<dependencies>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-core</artifactId>
    </dependency>

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
    </dependency>

    <!-- web应用程序核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 编写和运行测试用例 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- 前后端分离中的后端接口测试工具 -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

    <!-- 接入阿里云百炼平台 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-dashscope</artifactId>
    </dependency>

    <!-- Xorbits Inference -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-xinference</artifactId>
    </dependency>

    <!--langchain4j高级功能-->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
    </dependency>

    <!-- 前后端分离中的后端接口测试工具 -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

    <!--流式输出-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-reactor</artifactId>
    </dependency>

</dependencies>

<dependencyManagement>
    <dependencies>
        <!--引入SpringBoot依赖管理清单-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--引入langchain4j依赖管理清单-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>${langchain4j.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--引入百炼依赖管理清单-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-bom</artifactId>
            <version>${langchain4j.community.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

前置基本代码配置

application.yaml配置项

配置项目主要就是服务的端口,其他的就是百炼平台的model配置与Xorbits Inference的model配置:

yaml 复制代码
server:
  port: 8999

langchain4j:
  community:
    dashscope:
      chat-model:
        api-key: ${DASH_SCOPE_API_KEY}
        #        model-name: qwen-plus
        #        model-name: qwen-plus-latest
#        model-name: qwen-plus
        model-name: qwen3-30b-a3b-instruct-2507
  # 内部自部署模型
  xInference:
    chat-model:
      # ==Xorbits Inference==
      # 非思考模型
      base-url: http://178.86.99.68:9297
      model-name: dt_gptq_qwen_int8

大家可以选择百炼,比较简单,获取下key即可配置。


基础模块代码

SpringUtil.java:Spring容器类

java 复制代码
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(String name) {
        return (T)getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

EnvironmentContext.java:环境配置项类

java 复制代码
@Component
@Data
public class EnvironmentContext {

    @Autowired
    private Environment environment;

    // dashscope
    public String getDashScopeApiKey() {
        return environment.getRequiredProperty("langchain4j.community.dashscope.chat-model.api-key");
    }

    public String getDashScopeModelName() {
        return environment.getRequiredProperty("langchain4j.community.dashscope.chat-model.model-name");
    }

    // Xorbits Inference
    public String getXInferenceBaseUrl() {
        return environment.getRequiredProperty("langchain4j.xInference.chat-model.base-url");
    }

    public String getXInferenceModelName() {
        return environment.getRequiredProperty("langchain4j.xInference.chat-model.model-name");
    }
}

ChatForm.java:表单类

java 复制代码
@Data
public class ChatForm {
    private String message;   // 用户问题
}

MyWebMvcConfig:解决跨域web问题

java 复制代码
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

    /**
     * 解决跨域问题
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  //指定的映射地址
                .allowedHeaders("*") //允许携带的请求头
                .allowedMethods("*") //允许的请求方法
                .allowedOrigins("*");  //添加跨域请求头 Access-Control-Allow-Origin,值如:"https://domain1.com"或"*"
    }

}

方式一:AiService注入方式快速集成tools工具

介绍

对于langchain4j的springboot starter包中,给我们提供了快速构建ai智能体的实现,你可以理解它将一个大框架包含有rag检索+memeory+function tools的逻辑统一在DefaultAiServices中都给我们实现了,我们只需要去通过补充一个注解即可完成ai智能体的构建与注入。

对于在AiService中快速注入tools工具也十分简单。

快速集成步骤

对于一个ai bot,我们首先一定需要的是chat模型,这里我们手动创建对应的chat bean来完成注入对应的是ModelConfig,AiAssistant则是配置了对应的注解,来完成注入,同时,我们提供了一个控制器来来快速实现ai问答效果:

ModelConfig.java:Ai模型配置类

java 复制代码
@Configuration
public class ModelConfig {

    @Autowired
    private EnvironmentContext env;

    @Bean
    public StreamingChatModel qwenChatModel() {
//        return QwenStreamingChatModel.builder()
//                .apiKey(env.getDashScopeApiKey())
//                .modelName(env.getDashScopeModelName())
//                .build();
        return XinferenceStreamingChatModel.builder()
                .baseUrl(env.getXInferenceBaseUrl())
                .modelName(env.getXInferenceModelName())
                .logRequests(true)
                .logResponses(true)
                .build();
    }

}

CalculatorTools.java:自定义tools工具

注意:这里使用了@Component,直接会向spring容器注入一个bean实例。

java 复制代码
/**
 * @description  方式一:bean模式去创建tools,去注入到aiservice
 * @author changlu
 * @date 2025/8/16 17:36
 */
@Component
public class CalculatorTools {

    @Tool(name = "sum", value = "返回两个参数相加之和")
    double sum(
            @P(value="加数1", required = true) Double a,
            @P(value="加数2", required = true) Double b) {
        System.out.println("调用加法运算 ");
        return a + b;
    }

    @Tool(name = "squareRoot", value = "返回给定参数的平方根")
    double squareRoot(Double x) {
        System.out.println("调用平方根运算 ");
        return Math.sqrt(x);
    }

}

AiAssistant.java:ai bot代理类

在这里我们直接去完成tools的注入,来完成tools工具的快速集成

java 复制代码
@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        streamingChatModel = "qwenChatModel",
        tools = "calculatorTools" //配置tools
)
public interface AiAssistant {

    Flux<String> chat(String userMessage);

}

AiBotController.java:AI助手控制器层,提供接口

java 复制代码
@Tag(name = "ai控制器")
@RestController
@RequestMapping("/ai")
public class AiBotController {

    @Autowired
    private AiAssistant aiAssistant;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        return aiAssistant.chat(chatForm.getMessage());
    }

}

测试一下

我们将项目启动:

访问网址:http://127.0.0.1:8999/doc.html#/home

测试一下问题:一加三等于多少,此时正确实现回调


方式二:手动注册tools到AiService中

介绍

在方式二中,我们将尝试自己手动去构建一个ai bot,在构建的过程中,我们手动将对应的计算器类进行实例化注册到tools中。

快速集成步骤

AiBotFactory.java:Ai Bot工厂类

java 复制代码
import com.changlu.langchain4jtools.assistant.AiAssistant;
import com.changlu.langchain4jtools.env.EnvironmentContext;
import com.changlu.langchain4jtools.tools.CalculatorTools;
import com.changlu.langchain4jtools.util.SpringUtil;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.service.AiServices;
/**
 * @description  Ai Bot工厂类
 * @author changlu
 * @date 2025/8/17 00:21
 */
public class AiBotFactory {

    // 快速构建一个Aiservice
    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);
        // 创建model
        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        // 使用langchain4j提供的代理类快速实现一个ai bot
        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                .tools(new CalculatorTools())
                .build();
        return diyAiAssistant;
    }

}

Demo01Controller.java:控制器类

java 复制代码
@Tag(name = "demo01 ai控制器")
@RestController
@RequestMapping("/demo01")
public class Demo01Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (instance == null) {
            instance = AiBotFactory.buildAiAssistant();
        }
        return instance.chat(chatForm.getMessage());
    }

}

你可以看到此时我们的ai智能助手就是通过工厂类来完成创建ai boot,最终进行chat问答。


测试一下

我们现在用demo01 ai控制器这部分接口来测试一下,回答通常正确:

原理初探

初步认识Langchain4j中@Tool & @P注解相关参数

标注到某个方法上

@Tool 注解用于标注在某个方法上有两个可选字段:

  • name(工具名称):工具的名称。如果未提供该字段,方法名会作为工具的名称。
  • value(工具描述):工具的描述信息。

根据工具的不同,即使没有任何描述,大语言模型可能也能很好地理解它(例如, add(a, b) 就很直观),但通常最好提供清晰且有意义的名称和描述。这样,大语言模型就能获得更多信息,以决定是否调用给定的工具以及如何调用。

标注到方法入参中

@P 注解有两个字段:

  • value:参数的描述信息,这是必填字段。
  • required:表示该参数是否为必需项,默认值为 true ,此为可选字段。

源码剖析

@Tool & @P注解

两个注解源码分别如下所示,通过查看源码可以看到一个是设置在方法中,一个是设置在参数上:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Tool {
    String name() default "";

    String[] value() default {""};
}
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface P {
    String value();

    boolean required() default true;
}

Function calling函数调用原理探究

原理探究:

提出问题:

1、ai大模型是怎么知道有哪些function calls?

  • 第一次发起提问的时候,就将系统信息、用户信息、function tools描述信息都发过去了。

2、中间的过程是ai主动发请求回来的吗?

  • 第一次提问之后,ai会返回一条需要执行function tools消息,本地执行function tools之后,会将历史消息及本地执行结果统一再发送给ai,如果中间涉及到多次function tools过程,则会重复发起ai请求调用,最终结束后ai得到结果会将结果值作为本次消息的最终结果返回过来。

整体提问过程如下:

核心源码位置:DefaultAiServices#build中代理类的invoke逻辑


核心构建function call request细节

入口为:

java 复制代码
AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
        .streamingChatModel(qwenStreamingChatModel)
        // 可以看到这里直接将工具类注入到tools中
        .tools(new CalculatorTools())
        .build();

可以直接点进去tools方法中去看下底层源码。

涉及到的源码方法有:ToolService、ToolSpecification

  • ToolSpecification:描述工具的元数据。名称、描述、参数schema等

  • ToolExecutionRequest:工具调用请求。工具名称、调用ID、参数字典

  • ToolExecutionResultMessage:工具执行结果。原始请求引用、执行结果文本

  • ToolServiceContext:工具执行上下文

  • 可用工具规范列表

  • 工具执行器映射表

  • **DefaultToolExecutor:**本地方法执行器

对于你自己写的一个方法,最终会将这个方法的名称、描述、参数名、参数描述去封装为一个ToolSpecification,同时会将这个ToolSpecification & 你的问题一起发给ai,ai如果识别到需要调用你的工具就会发起function tools调用,此时在langchain4j中就会将这个调用封装为一个ToolExecutionRequest。

通常每一个方法都会去这样子绑定:Map<ToolSpecification, ToolExecutor>

对应一个方法会绑定一个tool执行器,一旦来了ToolExecutionRequest定位到ToolSpecification,也就可以拿到ToolExecutor,默认如果你是本地方法调用的话,那么就会匹配一个DefaultToolExecutor,实际上本质是通过反射来执行对应方法的。

在下面方式三中,我们会带你通过手动去创建Map<ToolSpecification, ToolExecutor>来实现tools工具的集成,同时无需对写的tool类去加上注解,我们都通过采用对应手动去构建的方式去实现集成tools。


方式三:手动注册ToolSpecification集成tools

介绍

当前章节,我们将会手动去注册一个ToolSpecification以及toolExecutor,带你看下底层源码是如何去构建的,其实封装完ToolSpecification后,langchain4j底层就会拿这个ToolSpecification来发给ai,最终实现让ai进行function tools调用。

这部分我们主要去理解下底层源码如何实现tool方法的构建,这对我们后续方式四中去构建http插件打下一个基础。

快速集成步骤

CalculatorTools2.java:自定义工具类

你可以看到在这个类中我们没有@Tool也没有相应的其他参数注解,后续我们将在factory中去手动构建ToolSpecification。

java 复制代码
public class CalculatorTools2 {

    double sum(Double a, Double b) {
        System.out.println("调用加法运算 ");
        return a + b;
    }

    double squareRoot(Double x) {
        System.out.println("调用平方根运算 ");
        return Math.sqrt(x);
    }

}

AiBotFactory2.java:ai bot 工厂类

我们这里封装了两个工具方法,一个加和,一个平方根,在这章节中我们注入tools方式是手动去构建AiServices tools(Map<ToolSpecification, ToolExecutor> tools) ,自行指定一个本地工具执行器类来实现本地工具方法调用:

java 复制代码
/**
 * @description  Ai Bot工厂类
 * @author changlu
 * @date 2025/8/17 00:21
 */
public class AiBotFactory2 {

    // 快速构建一个Aiservice
    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);
        // 创建model
        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        // 使用langchain4j提供的代理类快速实现一个ai bot
        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                // 方式一:注入带@Tool注解形式
//                .tools(new CalculatorTools())
                // 方式二:手动去构建ToolSpecification
                .tools(buildDiyTools())
                .tools()
                .build();
        return diyAiAssistant;
    }

    /**
     * 模拟langchain4j底层核心封装逻辑 方法描述 & 方法执行器
     * @param
     * @return Map<ToolSpecification,ToolExecutor>
     * @author changlu
     * @createDate 2025/8/17 21:04
     */
    public static Map<ToolSpecification, ToolExecutor> buildDiyTools() {
        Map<ToolSpecification, ToolExecutor> tools = new HashMap<>();

        // 构建一个CalculatorTools2
        CalculatorTools2 calculatorTools = new CalculatorTools2();
        try {
            // 第一个方法封装 sum 两数之和
            ToolSpecification sumSpec = buildSumToolSpecification();
            Method sumMethod = CalculatorTools2.class.getDeclaredMethod("sum", Double.class, Double.class);
            tools.put(sumSpec, new DefaultToolExecutor(calculatorTools, sumMethod));

            // 第二个方法封装 平方根
            ToolSpecification sqrtSpec = buildSqrtToolSpecification();
            Method sqrtMethod = CalculatorTools2.class.getDeclaredMethod("squareRoot", Double.class);
            tools.put(sqrtSpec, new DefaultToolExecutor(calculatorTools, sqrtMethod));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return tools;
    }

    private static ToolSpecification buildSqrtToolSpecification() {
        // 定义参数属性
        JsonObjectSchema sqrtJsonObjectSchema = buildSqrtJsonObjectSchema();
        ToolSpecification sqrtSpec = ToolSpecification.builder()
                .name("squareRoot")
                .description("返回给定参数的平方根")
                .parameters(sqrtJsonObjectSchema)
                .build();
        return sqrtSpec;
    }

    private static JsonObjectSchema buildSqrtJsonObjectSchema() {
        // 定义的参数 & 类型
        String param1 = "arg0";
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        properties.put(param1, JsonNumberSchema.builder().description("需要计算平方根的数字").build());

        // 必要的参数
        List<String> required = new ArrayList<>();
        required.add(param1);

        JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
                .addProperties(properties)
                .required(required)
                .definitions(null)
                .build();
        return jsonObjectSchema;
    }

    private static ToolSpecification buildSumToolSpecification() {
        // 定义参数属性
        JsonObjectSchema sumJsonObjectSchema = buildSumJsonObjectSchema();
        ToolSpecification sumSpec = ToolSpecification.builder()
                .name("sum")
                .description("返回两个参数相加之和")
                .parameters(sumJsonObjectSchema)
                .build();
        return sumSpec;
    }

    /**
     * 构建方法元数据信息
     * @param
     * @return JsonObjectSchema
     * @author changlu
     * @createDate 2025/8/17 21:06
     */
    private static JsonObjectSchema buildSumJsonObjectSchema() {
        // 定义的参数 & 类型
        // 底层是根据method.getParameters()获取到的名字,所以这里直接使用arg0、arg1
        String param1 = "arg0";
        String param2 = "arg1";
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        properties.put(param1, JsonNumberSchema.builder().description("加数1").build());
        properties.put(param2, JsonNumberSchema.builder().description("加数2").build());

        // 必要的参数
        List<String> required = new ArrayList<>();
        required.add(param1);
        required.add(param2);

        JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
                .addProperties(properties)
                .required(required)
                .definitions(null)
                .build();
        return jsonObjectSchema;
    }



}

Demo02Controller.java:控制器类

java 复制代码
import com.changlu.langchain4jtools.assistant.AiAssistant;
import com.changlu.langchain4jtools.demo01.AiBotFactory;
import com.changlu.langchain4jtools.domain.ChatForm;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@Tag(name = "demo02 ai控制器")
@RestController
@RequestMapping("/demo02")
public class Demo02Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (instance == null) {
            instance = AiBotFactory2.buildAiAssistant();
        }
        return instance.chat(chatForm.getMessage());
    }

}

测试一下

测试一下,成功调用:

ok了如果你能看懂,后续的自定义http插件应该也可以看懂滴


方式四:自定义支持http插件完成tools工具集成

介绍

通过调研,发现coze平台的插件本质上就是基于http来实现的,所以我们这里直接去模拟写一个http tools工具来实现快速集成http请求到我们的ai助手中。

下面是调研coze的一部分文档:

插件文档:https://www.coze.cn/open/docs/guides/plugin

操作 描述
创建自定义插件 扣子提供了多种创建自定义插件的方式供你选择,相关操作步骤可参见以下文档。 基于 API 创建插件使用 IDE 创建插件通过 JSON 或 YAML 文件导入插件使用代码注册插件
使用插件 插件可以直接在智能体内使用,拓展智能体的能力边界。插件也可以作为节点添加到工作流,实现工作流的任务处理能力。详情参见使用插件

基于api创建插件:我感觉就是你可以自定义http请求在某个插件中,一个方法就是一个api,同时可自行设置传参数。【这块和aiflowy及其类似,估计也是参考实现】

基于IDE创建插件:通过代码形式构建插件。

基于JSON或YAML文件导入插件:其实本质也就是上面基于api创建插件(内部封装http请求)。

快速集成步骤

demo说明

我们单独继承了ToolExecutor接口,实现了HttpToolExecutor,该实现类能够去完成http工具的调用。

你可以理解,我们如何集成http接口作为tools工具的,就是还是本质将你接口所需要的参数去封装成ToolSpecification,然后发给ai,ai会发起function tool调用,此时就能够拿到一个ToolExecutionRequest,接着我们直接使用对应的HttpToolExecutor来完成http请求接口调用即可实现集成http接口到ai助手中。

这个也是coze的核心原理实现,在langchain4j强大的抽象封装下,我们可以很好的快速扩展并实现这样子的http插件。

下面的实现基本就是参照coze的插件模式去复刻实现的。


HttpPlugin、HttpPluginMethod、HttpToolParameter(插件三件套,插件+插件方法+插件参数类)

HttpPlugin插件类如下:

java 复制代码
@Data
@Builder
public class HttpPlugin {

    // 基础服务名称
    private String baseUrl;

    // 公共请求头
    private Map<String, String> staticHeaders;

    // 包含多个插件方法
    List<HttpPluginMethod> pluginMethods;

}

HttpPluginMethod插件方法如下:

java 复制代码
/**
 * @description  插件方法
 * @author changlu
 * @date 2025/8/19 01:06
 */
@Data
@Builder
public class HttpPluginMethod {

    // 方法名称
    private String methodName;
    // 方法描述
    private String methodDescription;

    // 请求方法类型Code
    private Integer httpMethodType;
    // 请求资源点 例如:https://baidu.com/news,其中uri就是/news
    private String uri;

    // 请求参数集合
    private List<HttpToolParameter> parameters;

}

HttpToolParameter:插件方法参数类

java 复制代码
/**
 * HTTP工具参数统一配置类
 */
public class HttpToolParameter {
    private final String methodParamName; // 参数名
    private final String methodParamDescription;     // 参数描述
    private final String mappedName;      // 映射后的真实参数名
    private final int useTypeValue;       // 参数使用类型值(QUERY/BODY/PATH/HEADER)
    private final int dataTypeValue;      // 参数数据类型值(STRING/INTEGER/NUMBER/BOOLEAN)
    private final Object defaultValue;    // 默认值
    private final boolean required;       // 是否必填

    public HttpToolParameter(String methodParamName, String mappedName,
                             int useTypeValue, int dataTypeValue,
                             Object defaultValue, boolean required,
                             String methodParamDescription) {
        this.methodParamName = methodParamName;
        this.mappedName = mappedName;
        this.useTypeValue = useTypeValue;
        this.dataTypeValue = dataTypeValue;
        this.defaultValue = defaultValue;
        this.required = required;
        this.methodParamDescription = methodParamDescription;
    }

    // Getters
    public String getMethodParamName() {
        return methodParamName;
    }

    public String getMappedName() {
        return mappedName;
    }

    public int getUseTypeValue() {
        return useTypeValue;
    }

    public int getDataTypeValue() {
        return dataTypeValue;
    }

    public Object getDefaultValue() {
        return defaultValue;
    }

    public boolean isRequired() {
        return required;
    }

    public String getMethodParamDescription() {
        return methodParamDescription;
    }

    /**
     * 转换为ToolSpecification所需的JsonSchemaElement
     */
    public JsonSchemaElement toJsonSchemaElement() {
        HttpPluginEnums.ParameterType dataType = HttpPluginEnums.ParameterType.fromValue(dataTypeValue);

        switch (dataType) {
            case INTEGER:
                return JsonIntegerSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case NUMBER:
                return JsonNumberSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case BOOLEAN:
                return JsonBooleanSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case STRING:
            default:
                return JsonStringSchema.builder()
                        .description(methodParamDescription)
                        .build();
        }
    }

    /**
     * 转换为HttpToolExecutor所需的ParameterConfig
     */
    public HttpToolExecutor.ParameterConfig toParameterConfig() {
        return new HttpToolExecutor.ParameterConfig(
                mappedName,
                HttpPluginEnums.ParameterUseType.fromValue(useTypeValue),
                defaultValue,
                required
        );
    }
}

HttpPluginEnums:插件枚举类

java 复制代码
public class HttpPluginEnums {

    /**
     * HTTP 方法枚举(支持值和名称)
     */
    public enum HttpMethod {
        GET(1, "GET"),
        POST(2, "POST"),
        PUT(3, "PUT"),
        DELETE(4, "DELETE"),
        PATCH(5, "PATCH");

        private final int value;
        private final String methodName;

        HttpMethod(int value, String methodName) {
            this.value = value;
            this.methodName = methodName;
        }

        public int getValue() {
            return value;
        }

        public String getMethodName() {
            return methodName;
        }

        /**
         * 根据值获取枚举
         */
        public static HttpMethod fromValue(int value) {
            for (HttpMethod method : values()) {
                if (method.value == value) {
                    return method;
                }
            }
            throw new IllegalArgumentException("无效的HttpMethod值: " + value);
        }

        /**
         * 根据名称获取枚举(不区分大小写)
         */
        public static HttpMethod fromName(String name) {
            for (HttpMethod method : values()) {
                if (method.methodName.equalsIgnoreCase(name)) {
                    return method;
                }
            }
            throw new IllegalArgumentException("无效的HttpMethod名称: " + name);
        }
    }

    /**
     * 参数数据类型枚举
     */
    public enum ParameterType {
        STRING(1, "string"),
        INTEGER(2, "integer"),
        NUMBER(3, "number"),
        BOOLEAN(4, "boolean");

        private final int value;
        private final String name;

        ParameterType(int value, String name) {
            this.value = value;
            this.name = name;
        }

        public int getValue() {
            return value;
        }

        public String getName() {
            return name;
        }

        public static ParameterType fromValue(int value) {
            for (ParameterType type : values()) {
                if (type.value == value) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterType value: " + value);
        }

        public static ParameterType fromName(String name) {
            for (ParameterType type : values()) {
                if (type.name.equalsIgnoreCase(name)) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterType name: " + name);
        }
    }

    /**
     * 参数使用类型
     */
    public enum ParameterUseType {
        QUERY(1, "query"),    // URL查询参数
        BODY(2, "body"),     // 请求体参数
        PATH(3, "path"),     // URL路径参数
        HEADER(4, "header"); // 请求头参数

        private final int value;
        private final String name;

        ParameterUseType(int value, String name) {
            this.value = value;
            this.name = name;
        }

        public int getValue() {
            return value;
        }

        public String getName() {
            return name;
        }

        // 根据值获取枚举
        public static ParameterUseType fromValue(int value) {
            for (ParameterUseType type : values()) {
                if (type.value == value) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterUseType value: " + value);
        }

        // 根据名称获取枚举
        public static ParameterUseType fromName(String name) {
            for (ParameterUseType type : values()) {
                if (type.name.equalsIgnoreCase(name)) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterUseType name: " + name);
        }
    }


}

ToolExecutionRequestUtil:需要使用到langchian4j源码包的类

java 复制代码
import dev.langchain4j.internal.Json;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description  copy from langchain4j【dev.langchain4j.service.tool.ToolExecutionRequestUtil】
 * @author changlu
 * @date 2025/8/17 22:57
 */
public class ToolExecutionRequestUtil {

    private static final Pattern TRAILING_COMMA_PATTERN = Pattern.compile(",(\\s*[}\\]])");
    private static final Pattern LEADING_TRAILING_QUOTE_PATTERN = Pattern.compile("^\"|\"$");
    private static final Pattern ESCAPED_QUOTE_PATTERN = Pattern.compile("\\\\\"");

    private ToolExecutionRequestUtil() {}

    private static final Type MAP_TYPE = new ParameterizedType() {

        @Override
        public Type[] getActualTypeArguments() {
            return new Type[] {String.class, Object.class};
        }

        @Override
        public Type getRawType() {
            return Map.class;
        }

        @Override
        public Type getOwnerType() {
            return null;
        }
    };

    /**
     * Convert arguments to map.
     *
     * @param arguments json string
     * @return map
     */
    static Map<String, Object> argumentsAsMap(String arguments) {
        if (isNullOrBlank(arguments)) {
            return Map.of();
        }

        try {
            return Json.fromJson(arguments, MAP_TYPE);
        } catch (Exception ignored) {
            String normalizedArguments = removeTrailingComma(normalizeJsonString(arguments));
            return Json.fromJson(normalizedArguments, MAP_TYPE);
        }
    }

    /**
     * Removes trailing commas before closing braces or brackets in JSON strings.
     *
     * @param json the JSON string
     * @return the corrected JSON string
     */
    static String removeTrailingComma(String json) {
        if (isNullOrEmpty(json)) {
            return json;
        }
        Matcher matcher = TRAILING_COMMA_PATTERN.matcher(json);
        return matcher.replaceAll("$1");
    }

    /**
     * Normalizes a JSON string by removing leading and trailing quotes and unescaping internal double quotes.
     *
     * @param arguments the raw JSON string
     * @return the normalized JSON string
     */
    static String normalizeJsonString(String arguments) {
        if (isNullOrEmpty(arguments)) {
            return arguments;
        }

        Matcher leadingTrailingMatcher = LEADING_TRAILING_QUOTE_PATTERN.matcher(arguments);
        String normalizedJson = leadingTrailingMatcher.replaceAll("");

        Matcher escapedQuoteMatcher = ESCAPED_QUOTE_PATTERN.matcher(normalizedJson);
        return escapedQuoteMatcher.replaceAll("\"");
    }

    /**
     * Is the given string {@code null} or empty ("")?
     *
     * @param string The string to check.
     * @return true if the string is {@code null} or empty.
     */
    public static boolean isNullOrEmpty(String string) {
        return string == null || string.isEmpty();
    }

    /**
     * Is the given string {@code null} or blank?
     *
     * @param string The string to check.
     * @return true if the string is {@code null} or blank.
     */
    public static boolean isNullOrBlank(String string) {
        return string == null || string.trim().isEmpty();
    }

    /**
     * Convert map to JSON string.
     *
     * @param map the map to convert
     * @return JSON string
     */
    public static String toJson(Map<String, Object> map) {
        return Json.toJson(map);
    }
}

HttpToolExecutor:http工具执行器(核心)

主要去实现execute的方法,你可以理解当ai发起调用的时候,会调用execute方法,将本次请求方法作为ToolExecutionRequest类来传递过来,此时我们可以根据初始的参数配置信息来完成http请求调用。

java 复制代码
/**
 * HTTP 工具执行器,支持多种参数类型:
 * - Query 参数(URL查询参数)
 * - Body 参数(JSON格式请求体)
 * - Path 参数(URL路径参数)
 * - Header 参数(请求头参数)
 * 支持参数映射和默认值配置
 */
@Slf4j
public class HttpToolExecutor implements ToolExecutor {

    private final OkHttpClient httpClient;
    private final String baseUrl;
    private final Map<String, ParameterConfig> parameterConfigs;
    private final Map<String, String> staticHeaders;
    private final HttpPluginEnums.HttpMethod method;

    /**
     * 参数配置类
     */
    public static class ParameterConfig {
        public final String mappedName;  // 映射后的参数名
        public final HttpPluginEnums.ParameterUseType type;  // 参数类型
        public final Object defaultValue; // 默认值
        public final boolean required;    // 是否必填

        public ParameterConfig(String mappedName, HttpPluginEnums.ParameterUseType type, Object defaultValue, boolean required) {
            this.mappedName = mappedName;
            this.type = type;
            this.defaultValue = defaultValue;
            this.required = required;
        }
    }


    /**
     * 构造函数
     * @param baseUrl 基础URL
     * @param method HTTP方法
     * @param staticHeaders 静态请求头(固定值)
     * @param parameterConfigs 参数配置映射(参数名 -> 参数配置)
     */
    public HttpToolExecutor(String baseUrl,
                            int methodValue, // 使用数值表示方法
                            Map<String, String> staticHeaders,
                            Map<String, ParameterConfig> parameterConfigs) {
        this.httpClient = new OkHttpClient();
        this.baseUrl = baseUrl;
        this.method = HttpPluginEnums.HttpMethod.fromValue(methodValue); // 转换为枚举
        this.staticHeaders = staticHeaders != null ? staticHeaders : new HashMap<>();
        this.parameterConfigs = parameterConfigs != null ? parameterConfigs : new HashMap<>();
    }

    @Override
    public String execute(ToolExecutionRequest toolExecutionRequest, Object memoryId) {
        long startTime = System.currentTimeMillis();

        // 将工具执行请求的参数转换为Map
        Map<String, Object> arguments = ToolExecutionRequestUtil.argumentsAsMap(toolExecutionRequest.arguments());

        // 处理参数:应用默认值并验证必填参数
        Map<String, Object> finalArguments = processArguments(arguments);

        // 构建包含路径参数的URL
        String url = buildUrlWithPathParams(finalArguments);

        Request.Builder requestBuilder = new Request.Builder().url(url);

        // 添加静态请求头【处理初始插件配置参数】
        staticHeaders.forEach(requestBuilder::addHeader);

        // 处理动态请求头参数【类型为HEADER会封装到请求头中】
        addHeaderParameters(requestBuilder, finalArguments);

        // 智能判断请求体类型并处理参数
        RequestBody requestBody = processParameters(requestBuilder, finalArguments);

        // 记录请求详情
        logRequestDetails(url, requestBuilder, requestBody, finalArguments);

        try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
            ResponseBody body = response.body();
            String responseContent = body != null ? body.string() : "Empty response";

            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;

            // 记录响应详情
            logResponseDetails(response.code(), responseContent, duration);

            if (!response.isSuccessful()) {
                return "HTTP请求失败,状态码: " + response.code();
            }
            return responseContent;
        } catch (IOException e) {
            long endTime = System.currentTimeMillis();
            log.error("HTTP请求执行失败,耗时 {} 毫秒", (endTime - startTime), e);
            return "HTTP请求失败: " + e.getMessage();
        }
    }

    /**
     * 智能处理参数并返回请求体(可能为null)
     */
    private RequestBody processParameters(Request.Builder requestBuilder, Map<String, Object> arguments) {
        // 判断是否需要使用JSON请求体
        boolean useJsonBody = shouldUseJsonBody(arguments);

        if (useJsonBody) {
            // 处理JSON请求体参数
            Map<String, Object> bodyParams = new HashMap<>();
            for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
                if (entry.getValue().type == HttpPluginEnums.ParameterUseType.BODY && arguments.containsKey(entry.getKey())) {
                    bodyParams.put(entry.getValue().mappedName, arguments.get(entry.getKey()));
                }
            }
            String json = ToolExecutionRequestUtil.toJson(bodyParams);
            RequestBody requestBody = RequestBody.create(json, MediaType.parse("application/json"));
            requestBuilder.method(method.name(), requestBody);
            return requestBody;
        } else {
            // 处理查询参数
            HttpUrl.Builder urlBuilder = HttpUrl.parse(requestBuilder.build().url().toString()).newBuilder();
            for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
                if (entry.getValue().type == HttpPluginEnums.ParameterUseType.QUERY && arguments.containsKey(entry.getKey())) {
                    urlBuilder.addQueryParameter(
                            entry.getValue().mappedName,
                            arguments.get(entry.getKey()).toString()
                    );
                }
            }
            requestBuilder.url(urlBuilder.build());
            requestBuilder.method(method.name(), null);
            return null;
        }
    }

    /**
     * 智能判断是否使用JSON请求体
     */
    private boolean shouldUseJsonBody(Map<String, Object> arguments) {
        // GET/DELETE方法强制使用查询参数
        if (method == HttpPluginEnums.HttpMethod.GET || method == HttpPluginEnums.HttpMethod.DELETE) {
            return false;
        }

        // 检查是否存在BODY类型参数
        boolean hasBodyParams = parameterConfigs.values().stream()
                .anyMatch(config -> config.type == HttpPluginEnums.ParameterUseType.BODY &&
                        (arguments.containsKey(config.mappedName) || config.defaultValue != null));

        // POST/PUT/PATCH方法且存在BODY参数时使用JSON请求体
        return hasBodyParams;
    }

    // 以下方法保持不变(与之前版本相同)
    private Map<String, Object> processArguments(Map<String, Object> providedArguments) {
        Map<String, Object> processed = new HashMap<>();
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            String paramName = entry.getKey();
            ParameterConfig config = entry.getValue();
            if (providedArguments.containsKey(paramName)) {
                processed.put(paramName, providedArguments.get(paramName));
            } else if (config.defaultValue != null) {
                processed.put(paramName, config.defaultValue);
            } else if (config.required) {
                throw new IllegalArgumentException("缺少必填参数 '" + paramName + "'");
            }
        }
        return processed;
    }

    private String buildUrlWithPathParams(Map<String, Object> arguments) {
        String url = baseUrl;
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            if (entry.getValue().type == HttpPluginEnums.ParameterUseType.PATH) {
                String paramName = entry.getKey();
                String placeholder = "{" + entry.getValue().mappedName + "}";
                if (arguments.containsKey(paramName)) {
                    url = url.replace(placeholder, arguments.get(paramName).toString());
                }
            }
        }
        return url;
    }

    private void addHeaderParameters(Request.Builder requestBuilder, Map<String, Object> arguments) {
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            if (entry.getValue().type == HttpPluginEnums.ParameterUseType.HEADER && arguments.containsKey(entry.getKey())) {
                requestBuilder.addHeader(
                        entry.getValue().mappedName,
                        arguments.get(entry.getKey()).toString()
                );
            }
        }
    }

    private void logRequestDetails(String url, Request.Builder requestBuilder,
                                   RequestBody requestBody, Map<String, Object> arguments) {
        log.info("=== HTTP 请求详情 ===");
        log.info("URL: {}", url);
        log.info("方法: {}", method);
        Request request = requestBuilder.build();
        log.info("请求头: {}", request.headers().toMultimap());
        Map<HttpPluginEnums.ParameterUseType, Map<String, Object>> paramsByType = arguments.entrySet().stream()
                .collect(Collectors.groupingBy(
                        e -> parameterConfigs.get(e.getKey()).type,
                        Collectors.toMap(
                                e -> parameterConfigs.get(e.getKey()).mappedName,
                                Map.Entry::getValue
                        )
                ));
        paramsByType.forEach((type, params) ->
                log.info("{} 参数: {}", type, params)
        );
        if (requestBody != null) {
            log.info("请求体: {}", requestBody.toString());
        }
        log.info("=====================");
    }

    private void logResponseDetails(int statusCode, String responseContent, long durationMs) {


        log.info("=== HTTP 响应详情 ===");
        log.info("状态码: {}", statusCode);
        log.info("耗时: {} 毫秒", durationMs);
        String truncatedResponse = responseContent.length() > 1000
                ? responseContent.substring(0, 1000) + "...[截断]"
                : responseContent;
        log.info("响应体: {}", truncatedResponse);
        log.info("=====================");
    }
}

AiBotFactory3:AiBot工厂类

在buildHttpTools中,我们去植入了一个web 检索http请求插件,并进行加载到AiServices中,实现加载了一个http插件:

java 复制代码
/**
 * @description Ai Bot factory class with enhanced HTTP tool support including default values
 */
public class AiBotFactory3 {

    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);

        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                .tools(buildHttpTools())
                .build();
        return diyAiAssistant;
    }

    private static Map<ToolSpecification, ToolExecutor> buildHttpTools() {
        // 定义参数配置
        List<HttpToolParameter> parameters = new ArrayList<>();
        parameters.add(new HttpToolParameter(
                "query", "q",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                null, true, "搜索关键词"));
        parameters.add(new HttpToolParameter(
                "country", "gl",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                "cn", false, "国家代码(如:'cn')"));
        parameters.add(new HttpToolParameter(
                "language", "hl",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                "zh-CN", false, "语言代码(如:'zh-CN')"));
        // 构建插件方法
        HttpPluginMethod httpPluginMethod = HttpPluginMethod.builder()
                .uri("/search")
                .methodName("searchWeb")
                .methodDescription("使用 Serper API 搜索网络")
                .httpMethodType(HttpPluginEnums.HttpMethod.POST.getValue())
                .parameters(parameters).build();


        // 定义静态请求头
        Map<String, String> staticHeaders = new HashMap<>();
        staticHeaders.put("X-API-KEY", "d420dbfcefdd0cf0261ba09f5a91dc4a35933c59");
        staticHeaders.put("Content-Type", "application/json");

        // 封装http插件,目前这里就一个插件
        HttpPlugin httpPlugin = HttpPlugin.builder()
                .baseUrl("https://google.serper.dev")
                .staticHeaders(staticHeaders)
                .pluginMethods(Arrays.asList(httpPluginMethod))
                .build();


        // -----------------
//        // 封装构建插件
//        HttpPluginMethod searchPlugin = httpPlugin.getPluginMethods().get(0);
//
//        // 转换为HttpToolExecutor需要的配置
//        Map<String, HttpToolExecutor.ParameterConfig> parameterConfigs = new HashMap<>();
//        parameters.forEach(param ->
//                parameterConfigs.put(param.getMethodParamName(), param.toParameterConfig()));
//
//        httpTools.put(
//                buildToolSpecification(searchPlugin.getMethodName(), searchPlugin.getMethodDescription(), parameters),
//                new HttpToolExecutor(
//                        httpPlugin.getBaseUrl() + searchPlugin.getUri(),
//                        searchPlugin.getHttpMethodType(),
//                        httpPlugin.getStaticHeaders(),
//                        parameterConfigs
//                )
//        );
        Map<ToolSpecification, ToolExecutor> httpPluginTools = buildHttpPluginTools(httpPlugin);
        return httpPluginTools;
    }

    public static Map<ToolSpecification, ToolExecutor> buildHttpPluginTools(HttpPlugin httpPlugin) {
        Map<ToolSpecification, ToolExecutor> res = new HashMap<>();

        String baseUrl = httpPlugin.getBaseUrl();
        Map<String, String> staticHeaders = httpPlugin.getStaticHeaders();
        List<HttpPluginMethod> pluginMethods = httpPlugin.getPluginMethods();

        for (HttpPluginMethod httpPluginMethod : pluginMethods) {
            Pair<ToolSpecification, ToolExecutor> pair = buildHttpPluginTool(baseUrl, staticHeaders, httpPluginMethod);
            res.put(pair.getFirst(), pair.getSecond());
        }
        return res;
    }

    private static Pair<ToolSpecification, ToolExecutor> buildHttpPluginTool(String baseUrl, Map<String, String> staticHeaders, HttpPluginMethod httpPluginMethod) {
        String uri = httpPluginMethod.getUri();
        Integer httpMethodType = httpPluginMethod.getHttpMethodType();
        String methodName = httpPluginMethod.getMethodName();
        String methodDescription = httpPluginMethod.getMethodDescription();
        List<HttpToolParameter> parameters = httpPluginMethod.getParameters();

        // 构建toolSpecification
        ToolSpecification toolSpecification = buildToolSpecification(methodName, methodDescription, parameters);
        // 构建HttpToolExecutor
        Map<String, HttpToolExecutor.ParameterConfig> parameterConfigs = new HashMap<>();
        parameters.forEach(param ->
                parameterConfigs.put(param.getMethodParamName(), param.toParameterConfig()));
        HttpToolExecutor httpToolExecutor = new HttpToolExecutor(
                baseUrl + uri,
                httpMethodType,
                staticHeaders,
                parameterConfigs
        );

        return new Pair<>(toolSpecification, httpToolExecutor);
    }


    private static ToolSpecification buildToolSpecification(String methodName, String methodDescription, List<HttpToolParameter> parameters) {
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        List<String> required = new ArrayList<>();

        for (HttpToolParameter param : parameters) {
            properties.put(param.getMethodParamName(), param.toJsonSchemaElement());
            if (param.isRequired()) {
                required.add(param.getMethodParamName());
            }
        }

        return ToolSpecification.builder()
                .name(methodName)
                .description(methodDescription)
                .parameters(JsonObjectSchema.builder()
                        .addProperties(properties)
                        .required(required)
                        .build())
                .build();
    }
}

Demo03Controller:Ai Bot控制器类

java 复制代码
@Tag(name = "demo03 ai控制器")
@RestController
@RequestMapping("/demo03")
@Slf4j
public class Demo03Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/event-stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        String message = chatForm.getMessage();
        if (instance == null) {
            instance = AiBotFactory3.buildAiAssistant();
        }
        return Flux.<String>create(emitter -> {  // Explicit type parameter
                    try {
                        if (StringUtils.isEmpty(message)) {
                            emitter.complete();
                            return;
                        }
                        // 特定ai调用
                        instance.chat(message)
                                .subscribe(
                                        response -> {
                                            log.debug("[LLM-Response] 收到大模型响应片段 | length: {}, content: {}", response.length(), response);
                                            emitter.next("final|CONTENT|" + response);
                                        },
                                        error -> {
                                            log.error("[LLM-Error] 大模型调用异常 |  error: {}", error.getMessage(), error);
                                            emitter.error(error);
                                        },
                                        () -> {
                                            log.info("[LLM-Complete] 大模型调用完成 ");
                                            if (!emitter.isCancelled()) {
                                                emitter.complete();
                                            }
                                        }
                                );
                    }catch (Exception e) {
                        log.error("[Chat-Error] 流式对话处理异常 | error: {}", e.getMessage(), e);
                        emitter.error(e);
                    }
                })
                .onBackpressureBuffer(128) // 设置缓冲区大小
                .doOnCancel(() -> log.warn("[Chat-Cancel] 流式对话被取消"))
                .doOnTerminate(() -> log.info("[Chat-End] 流式对话终止)"))
                .subscribeOn(Schedulers.boundedElastic());
    }

}

对接插件(web搜索):serper.dev

网址:https://serper.dev/playground

免费提供检索的额度大概是2500样子。需要去注册下获取key

mac配置环境变量如下:

shell 复制代码
vim ~/.zshrc

# 配置内容
export SERPER_KEY="xxxxxxxxx"

# 生效配置文件
source ~/.zshrc

记得重启下IDEA


测试一下

我们现在的controller已经改为了流式返回的,对应前端ui可以使用这个仓库的前端工程:

https://gitee.com/changluJava/demo-exer/tree/master/ai/langchain4j

需要将请求地址修改下:

指向本地的服务端口为:8999

启动前端服务:

shell 复制代码
npm install

npm run dev

接着启动后端服务:

我们访问网址:http://localhost:81/#/

效果如下,本地ai就会去选择使用web插件进行http请求检索相关信息,最终来实现ai回答内容:

袋鼠云一站式数据中台

本地的接口调用如下:


整理者:长路 时间:2025.8.17

资料获取

大家点赞、收藏、关注、评论啦~

精彩专栏推荐订阅:在下方专栏👇🏻

更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅

相关推荐
悟空码字6 小时前
Spring Boot 整合 MongoDB 最佳实践:CRUD、分页、事务、索引全覆盖
java·spring boot·后端
皮皮林5512 天前
拒绝写重复代码,试试这套开源的 SpringBoot 组件,效率翻倍~
java·spring boot
用户908324602734 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840825 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解5 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解5 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记5 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者6 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840826 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解6 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端