JAVA如何手搓一个AI智能体(一)

1,493 阅读5分钟
前言

随着DeepSeek的出现,本地私有化部署AI大模型的成本急速下降,去年由于大模型对于函数调用的支持还不够普及,基于文心一言实现了自己手搓的智能体DEMO,这篇文章先介绍如何使用百度千帆的大模型实现一个自己的单智能体应用,后续再基于这个已实现的版本改造为私有化部署大模型(Ollama)DeepSeek的版本,如何私有化部署大模型会单独出一个文章

使用到的主要依赖
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>${langchain4j.version}</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-dashscope</artifactId>
    <version>${langchain4j.version}</version>
    <exclusions>
       <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-simple</artifactId>
       </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-qianfan</artifactId>
    <version>${langchain4j.version}</version>
</dependency>
配置聊天
  1. 构建模型
private ChatLanguageModel getModel(Environment environment) {
    return QianfanChatModel.builder()
            .modelName(QianfanChatModelNameEnum.ERNIE_BOT.getModelName())
            .apiKey(environment.getProperty("qianfan.apikey"))
            .secretKey(environment.getProperty("qianfan.secretKey"))
            .temperature(0.7)
            .logRequests(true)
            .logResponses(true)
            .build();
}

2. 构建记忆

private ChatMemoryProvider getChatMemory(Environment environment) {
    return memoryId -> MessageWindowChatMemory.builder()
            .id(memoryId)
            .maxMessages(99)
            .chatMemoryStore(new InMemoryChatMemoryStore())
            .build();
}

3. 简单聊天实现

public String chat(String message) {
    UserMessage userMessage = userMessage(message);
    Response<AiMessage> aiMessageResponse = chatLanguageModel.generate(userMessage);
    return aiMessageResponse.content().text();
}

简单配置聊天后就可以使用对话的方式与大模型进行沟通,可以进行简单的人设设定

创建工具
@Tool("想吃点什么得先找附近的店。查询附近的美食店、蛋糕店、奶茶店、咖啡店、特色餐厅...等?")
String someNiceStoresNearby(@ToolMemoryId String memoryId, @P("当前地点") String location) {
    log.info("当前地点:"+location);
    return ToolMock.trimIndent(ToolMock.MOCK_01);
}

其中@Tool用于定义工具的用途,用于让大模型理解什么情况下要使用这个工具,@P注解用于标注如何从用户的提问中获取关键词作为工具的入参

public String chatTools(String message) {
    UserMessage botMessage = userMessage("你是一个智能助理,能够根据用户提出的问题及function_call调用返回,逐步推理给出正确的回答");
    UserMessage userMessage = userMessage(message);
    Response<AiMessage> aiMessageResponse = chatLanguageModel
            .generate(ListUtil.toList(botMessage, userMessage), ToolSpecifications.toolSpecificationsFrom(ChatTools.class));
    return aiMessageResponse.content().text();
}

设定大模型人设,配置工具的使用,其中ChatTools就是一组工具的集合

@Component
@Slf4j
public class ChatTools {

    @Tool("想吃点什么得先找附近的店。查询附近的美食店、蛋糕店、奶茶店、咖啡店、特色餐厅...等?")
    String someNiceStoresNearby(@ToolMemoryId String memoryId, @P("当前地点") String location) {
        log.info("当前地点:"+location);
        return ToolMock.trimIndent(ToolMock.MOCK_01);
    }

    @Tool("查询附近的美食店、蛋糕店、奶茶店、冰品店、特色餐厅...等,是否有我想吃的东西?")
    String somethingDeliciousNearby(@ToolMemoryId String memoryId, @P("当前地点") String location, @P("想吃的东西") String yammy) {
        log.info("当前地点:"+location+" 想吃的东西:"+yammy);
        return ToolMock.trimIndent(ToolMock.MOCK_02);
    }

    @Tool("在一家指定的商店里下单,指定商品名称与数量。")
    String pressAnOrder(@ToolMemoryId String memoryId, @P("店名") String store,
                        @P("想吃的东西") String yammy, @P("数量") Integer quantity) {
        log.info("店名:"+store+" 想吃的东西:"+ yammy+" 数量:"+ quantity);
        return ToolMock.trimIndent(ToolMock.MOCK_03);
    }
}

在ChatTools的工具中,我们可以调用第三方接口或者自己写好的接口把数据返回给大模型做进一步的处理甚至是直接让大模型帮我们做日常业务,如买一杯咖啡,执行一些任务等等

配置灵活的动态自定义工具

可能有的人已经发现了这里的问题,工具需要硬编码的方式在代码中写好,如果我要动态配置工具,动态配置执行的工具业务逻辑怎么办

由于我是在24年6月研究的智能体实现,当时的版本还是0.31.0(截止编写时间,最新版本为1.0.0-bate1),不支持动态的创建工具,我简单改了下相关代码,由于后续版本已迭代完善,这里不做代码展示,在后续的使用私有化部署的Deepseek作为大模型中会做相关代码展示,最终实现的效果可以通过在线的方式创建工具并配置在聊天中

动态代码编译

在创建动态工具中有一个问题就是如何插入动态的代码逻辑,因为只是demo性质的,这里使用的是URLClassLoader动态加载类实现业务逻辑的动态处理,这里只为了验证动态创建工具的可行性

public static Dict compile(String className, String sourceCode, String methodName, Map<String,String> params) {
    boolean success = compileSource(className, sourceCode);

    if (success) {
        log.info("动态代码编译成功!");
        try {
            URL classUrl = Paths.get("./compiled_classes").toUri().toURL();
            URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classUrl});

            Class<?> cls = Class.forName(className, true, classLoader);
            Object instance = cls.getDeclaredConstructor().newInstance();
            Object obj = cls.getDeclaredMethod(methodName, Map.class).invoke(instance, params);
            if(obj instanceof Map){
                return Dict.parse(obj);
            }
            log.info("代码片段返回的数据类型不是Map:"+obj);
            return Dict.of();
        } catch (Exception e) {
            log.error("动态执行代码片段失败", e);
        }
    } else {
        log.error("动态代码编译失败!");
    }
    return null;
}
private static boolean compileSource(String className, String sourceCode) {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    JavaFileObject file = new JavaSourceFromString(className, sourceCode);

    Iterable<? extends JavaFileObject> compilationUnits = List.of(file);
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

    Iterable<String> options = Arrays.asList("-d", "./compiled_classes");
    JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, null, compilationUnits);

    boolean success = task.call();

    try {
        fileManager.close();
    } catch (Exception e) {
        log.error("动态编译代码失败!", e);
    }

    return success;
}
public static JavaCodeVO buildJavaCode(String className, String sourceCode) {
    boolean success = compileSource(className, sourceCode);
    if(success) {
        try {
            URL classUrl = Paths.get("./compiled_classes").toUri().toURL();
            URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classUrl});

            Class<?> cls = Class.forName(className, true, classLoader);
            Object instance = cls.getDeclaredConstructor().newInstance();
            List<String> methodNames = Arrays.stream(instance.getClass().getDeclaredMethods()).map(Method::getName).toList();
            return new JavaCodeVO().setClassName(instance.getClass().getName())
                    .setCode(sourceCode).setMethodList(methodNames);
        } catch (Exception e) {
          log.info("获取类信息失败!", e);
          throw new BusinessException("获取类信息失败!");
        }
    }else {
        throw new BusinessException("类编译失败,请检查代码是否正确!");
    }
}

查询某地天气插件的动态代码片段

image.png

实现效果

image.png

26a2780a42b816b575da08812b43455.png

image.png

总结

使用大模型API加langchain4j实现的一套智能体Demo,后续可以通过一次聊天的工具调用顺序实现类似工作流编排的效果,在现在Dify之类的开源工具越来越多之后自己实现显得没有必要,不过也能够在调试的过程中更多得理解大模型是如何调用我们定义的工具的