文章目录
- 前言
- 文章配套源码
- 介绍
- 初始环境搭建
- 方式一:AiService注入方式快速集成tools工具
-
- 介绍
- 快速集成步骤
-
- ModelConfig.java:Ai模型配置类
- CalculatorTools.java:自定义tools工具
- [AiAssistant.java:ai bot代理类](#AiAssistant.java:ai bot代理类)
- AiBotController.java:AI助手控制器层,提供接口
- 测试一下
- 方式二:手动注册tools到AiService中
-
- 介绍
- 快速集成步骤
-
- [AiBotFactory.java:Ai Bot工厂类](#AiBotFactory.java:Ai Bot工厂类)
- Demo01Controller.java:控制器类
- 测试一下
- 原理初探
-
- [初步认识Langchain4j中@Tool & @P注解相关参数](#初步认识Langchain4j中@Tool & @P注解相关参数)
- 源码剖析
-
- [@Tool & @P注解](#@Tool & @P注解)
- [Function calling函数调用原理探究](#Function calling函数调用原理探究)
- [核心构建function call request细节](#核心构建function call request细节)
- 方式三:手动注册ToolSpecification集成tools
-
- 介绍
- 快速集成步骤
-
- CalculatorTools2.java:自定义工具类
- [AiBotFactory2.java:ai bot 工厂类](#AiBotFactory2.java:ai bot 工厂类)
- Demo02Controller.java:控制器类
- 测试一下
- 方式四:自定义支持http插件完成tools工具集成
-
- 介绍
- 快速集成步骤
-
- demo说明
- HttpPlugin、HttpPluginMethod、HttpToolParameter(插件三件套,插件+插件方法+插件参数类)
- HttpPluginEnums:插件枚举类
- ToolExecutionRequestUtil:需要使用到langchian4j源码包的类
- HttpToolExecutor:http工具执行器(核心)
- AiBotFactory3:AiBot工厂类
- [Demo03Controller:Ai Bot控制器类](#Demo03Controller:Ai Bot控制器类)
- 对接插件(web搜索):serper.dev
- 测试一下
- 资料获取

前言
博主介绍:✌目前全网粉丝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
资料获取
大家点赞、收藏、关注、评论啦~
精彩专栏推荐订阅:在下方专栏👇🏻
- 长路-文章目录汇总(算法、后端Java、前端、运维技术导航):博主所有博客导航索引汇总
- 开源项目Studio-Vue---校园工作室管理系统(含前后台,SpringBoot+Vue):博主个人独立项目,包含详细部署上线视频,已开源
- 学习与生活-专栏:可以了解博主的学习历程
- 算法专栏:算法收录
更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅