LangChain4j
LangChain4j是一个流行的Java AI开发框架,提供开箱即用的API支持大模型调用和常见AI功能开发。它支持与SpringBoot集成,提供ChatModel基础交互、多模态处理、系统提示词、AIService高级抽象等功能。框架还支持会话记忆管理、结构化输出、RAG检索增强生成、工具调用和MCP协议扩展。开发模式上支持流式SSE接口响应,并包含输入输出护轨、日志观测等辅助功能。相比Sp
目录
什么是 LangChain4j?
目前主流的 Java Al 开发框架有 Spring Al 和 LangChain4j,它们都提供了很多 开箱即用的 API 来帮你调用大模型、实现AI开发常用的功能。
这两个框架的很多概念和用法都是类似的,也都提供了很多插件扩展,都支持和SpringBoot项目集成。
AI对话-ChatModel
ChatModel是最基础的概念,负责和Al大模型交互。
1.引l入至少一个Al大模型依赖
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
    <version>1.1.0-beta7</version>
</dependency>
2.获取大模型调用key
3.回到项目,在配置文件中添加大模型配置,指定模型名称和API Key:
langchain4j:
  community:
    dashscope:
      chat-model:
        model-name: qwen-max
        api-key: <You API Key here>
除了编写配置让Spring Boot自动构建ChatModel外,也可以通过构造器自己创建ChatModel对象。这种方法更灵活。
ChatModel qwenModel = QwenChatModel.builder()
                    .apiKey("You API key here")
                    .modelName("qwen-max")
                    .enableSearch(true)
                    .temperature(0.7)
                    .maxTokens(4096)
                    .stops(List.of("Hello"))
                    .build();
4.创建一个AiCodeHelper类,引入自动注入的qwenChatModel,编写简单的对话代码,并利用Lombok注解打印输出结果日志:
@Service
@Slf4j
public class AiCodeHelper {
    @Resource
    private ChatModel qwenChatModel;
    public String chat(String message) {
        UserMessage userMessage = UserMessage.from(message);
        ChatResponse chatResponse = qwenChatModel.chat(userMessage);
        AiMessage aiMessage = chatResponse.aiMessage();
        log.info("AI 输出:" + aiMessage.toString());
        return aiMessage.text();
    }
}
编写单元测试
@SpringBootTest
class AiCodeHelperTest {
    @Resource
    private AiCodeHelper aiCodeHelper;
    @Test
    void chat() {
        aiCodeHelper.chat("你好,我是程序员鱼皮");
    }
}
多模态-Multimodality
多模态是指能够同时处理,理解和生成多种不同类型数据的能力,比如文本,图像,音频,视频,pdf等等。
LangChain4j中使用多模态的方法很简单,用户消息中是可以添加图片,音视频,pdf等媒体资源的

编写一个支持传入自定义UserMessage的方法:
public String chatWithMessage(UserMessage userMessage) {
    ChatResponse chatResponse = qwenChatModel.chat(userMessage);
    AiMessage aiMessage = chatResponse.aiMessage();
    log.info("AI 输出:" + aiMessage.toString());
    return aiMessage.text();
}
编写单元测试,传入一张图片:
@Test
void chatWithMessage() {
    UserMessage userMessage = UserMessage.from(
            TextContent.from("描述图片"),
            ImageContent.from("https://www.codefather.cn/logo.png")
    );
    aiCodeHelper.chatWithMessage(userMessage);
}
目前这个模型对多模态的支持不太好,容易报错!!!
系统提示词-SystemMessage
系统提示词是设置ai模型行为规则和角色定位的隐藏指令,用户通常不能直接看到。系统prompt相当于给ai设定人格和能力边界,也就是告诉ai"你是谁?你能做什么?"
使用系统提示词,创建一个系统消息,把他和用户消息一起发送给AI
修改chat方法,代码如下:
private static final String SYSTEM_MESSAGE = """
        你是编程领域的小助手,帮助用户解答编程学习和求职面试相关的问题,并给出建议。重点关注 4 个方向:
        1. 规划清晰的编程学习路线
        2. 提供项目学习建议
        3. 给出程序员求职全流程指南(比如简历优化、投递技巧)
        4. 分享高频面试题和面试技巧
        请用简洁易懂的语言回答,助力用户高效学习与求职。
        """;
public String chat(String message) {
    SystemMessage systemMessage = SystemMessage.from(SYSTEM_MESSAGE);
    UserMessage userMessage = UserMessage.from(message);
    ChatResponse chatResponse = qwenChatModel.chat(systemMessage, userMessage);
    AiMessage aiMessage = chatResponse.aiMessage();
    log.info("AI 输出:" + aiMessage.toString());
    return aiMessage.text();
}
AI服务-AI Service
在学习更多特性前,了解LangChain4j最重要的开发模式--AI Service,提供了很多高层抽象的,用起来更方便的API,把AI应用当做服务来开发。
1.使用AI Service
首先引入langchain4j依赖
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.1.0</version>
</dependency>
然后创建一个编程助手AI Service服务,采用声明式开发方法,编写一个对话方法,然后可以直接通过@SystemMessage注解定义系统提示词。
public interface AiCodeHelperService {
    @SystemMessage("你是一位编程小助手")
    String chat(String userMessage);
}
不过由于我们提示词较长,写到注解里很不优雅,所以单独在resources目录下新建文件system-prompt.txt来存储系统提示词。
@SystemMessage注解支持从文件中读取系统提示词:
public interface AiCodeHelperService {
    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);
}
然后我们需要编写工厂类,用于创建AIService:
@Configuration
public class AiCodeHelperServiceFactory {
    @Resource
    private ChatModel qwenChatModel;
    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        return AiServices.create(AiCodeHelperService.class, qwenChatModel);
    }
}
调用Aiservices.create 方法就可以创建出 Al Service 的实现类了,背后的原理是利用Java 反射机制创建了一个实现接口的代理对象,代理对象负责输入和输出的转换,比如把String类型的用户消息参数转为UserMessage类型并调用ChatModel,再将Al返回的AiMessage类型转换为String类型作为返回值。
编写单元测试,调用我们开发的AI Service:
@SpringBootTest
class AiCodeHelperServiceTest {
    @Resource
    private AiCodeHelperService aiCodeHelperService;
    @Test
    void chat() {
        String result = aiCodeHelperService.chat("你好,我是程序员鱼皮");
        System.out.println(result);
    }
}
2.Sping Boot项目中使用
如果觉得手动调用create方法来创建Service比较麻烦,在SpringBoot项目中可以引l入依赖:
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.1.0-beta7</version>
</dependency>
然后给Al Service加上@AiService注解,就能自动创建出服务实例了:
@AiService
public interface AiCodeHelperService {
    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);
}
记得注释掉之前工厂类的@Configuration注解,否则会出现Bean冲突
这种方式虽然更方便了,但是缺少了自主构建的灵活性(可以自由设置很多参数)。
会话记忆-ChatMemory
会话记忆是指让AI能够记住用户之前的对话内容,并保持上下文连贯性,这是实现AI应用的核心特性。
// 自己实现会话记忆
Map<String, List<Message>> conversationHistory = new HashMap<>();
public String chat(String message, String userId) {
    // 获取用户历史记录
    List<Message> history = conversationHistory.getOrDefault(userId, new ArrayList<>());
    
    // 添加用户新消息
    Message userMessage = new Message("user", message);
    history.add(userMessage);
    
    // 构建完整历史上下文
    StringBuilder contextBuilder = new StringBuilder();
    for (Message msg : history) {
        contextBuilder.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n");
    }
    
    // 调用 AI API
    String response = callAiApi(contextBuilder.toString());
    
    // 保存 AI 回复到历史
    Message aiMessage = new Message("assistant", response);
    history.add(aiMessage);
    conversationHistory.put(userId, history);
    
    return response;
}
1.使用会话记忆
LangChain4j为我们提供了开箱即用的MessageWindowchatMemory会话记忆,最多保存N条消息,多余的会自动淘汰。
创建会话记忆后,在构造Al Service设置chatMemory:
@Configuration
public class AiCodeHelperServiceFactory {
    @Resource
    private ChatModel qwenChatModel;
    @Bean
    public AiCodeHelperService aiCodeHelperService() {
        // 会话记忆
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
        AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
                .chatModel(qwenChatModel)
                .chatMemory(chatMemory)
                .build();
        return aiCodeHelperService;
    }
}
2.进阶用法
会话记忆默认是存储在内存的,重启后会丢失,可以通过自定义ChatMemoryStore接口的实现类,将消息保存到MySQL等其他数据源中。

如果有多个用户,希望每个用户之间的消息隔离,可以通过给对话方法增加memoryld参数和注解,在调用对话时传入memoryld即可(类似聊天室的房间号):
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
构造Al Service时,可以通过chatMemoryProvider来指定每个memoryld单独创建会话记忆:
// 构造 AI Service
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
        .chatModel(qwenChatModel)
        .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
        .build();
结构化输出
结构化输出是指将大模型返回的文本输出转换为结构化的数据格式,比如一段JSON、一个对象、或者是复杂的对象列表。

结构化输出有3种实现方式:
利用大模型的JSON schema
利用 Prompt + JSON Mode
利用 Prompt
默认是Prompt 模式,也就是在原本的用户提示词下 拼接一段内容来指定大模型强制输出包含特定字段的JSON文本。
你是一个专业的信息提取助手。请从给定文本中提取人员信息,
并严格按照以下 JSON 格式返回结果:
{
    "name": "人员姓名",
    "age": 年龄数字,
    "height": 身高(米),
    "married": true/false,
    "occupation": "职业"
}
重要规则:
1. 只返回 JSON 格式,不要添加任何解释
2. 如果信息不明确,使用 null
3. age 必须是数字,不是字符串
4. married 必须是布尔值
开发,只要修改对话方法的返回值,框架就会自动帮我们实现结构化输出。

增加一个让AI生成学习报告的方法,AI需要输出学习报告对象,包含名称和建议列表:
@SystemMessage(fromResource = "system-prompt.txt")
Report chatForReport(String userMessage);
// 学习报告
record Report(String name, List<String> suggestionList){}
编写单元测试:
@Test
void chatForReport() {
    String userMessage = "你好,我是xxx,学编程两年半,请帮我制定学习报告";
    AiCodeHelperService.Report report = aiCodeHelperService.chatForReport(userMessage);
    System.out.println(report);
}
如果你发现AI有时无法生成准确的JSON,那么可以采用JSONSchema模式,直接在请求中约束LLM的输出格式。这是目前最可靠、精确度最高的结构化输出实现
ResponseFormat responseFormat = ResponseFormat.builder()
        .type(JSON)
        .jsonSchema(JsonSchema.builder()
                .name("Person")
                .rootElement(JsonObjectSchema.builder()
                        .addStringProperty("name")
                        .addIntegerProperty("age")
                        .addNumberProperty("height")
                        .addBooleanProperty("married")
                        .required("name", "age", "height", "married") 
                        .build())
                .build())
        .build();
ChatRequest chatRequest = ChatRequest.builder()
        .responseFormat(responseFormat)
        .messages(userMessage)
        .build();
检索增强生成-RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索技术和 Al内容生成的混合架构,可以解决大模型的知识时效性限制和幻觉问题。
简单来说,RAG就像给AI配了一个"小抄本",让AI回答问题前先查一查特定的知识库来获取知识,确保回答是基于真实资料而不是凭空想象。很多企业也基于RAG搭建了自己的智能客服,可以用自己积累的领域知识回复用户。
RAG的完整工作流程如下:

1.极简版RAG
极简版适合快速查看效果,首先需要引l入额外的依赖,里面包含了内置的离线Embedding模型,开箱即用:
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-easy-rag</artifactId>
    <version>1.1.0-beta7</version>
</dependency>
示例代码如下,使用内置的文档加载器读取文档,然后利用内置的Embedding模型将文档转换成向量,并存储在内置的Embedding内存存储中,最后给Al Service绑定默认的内容检索器。
// RAG
// 1. 加载文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
// 2. 使用内置的 EmbeddingModel 转换文本为向量,然后存储到自动注入的内存 embeddingStore 中
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
// 构造 AI Service
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
        .chatModel(qwenChatModel)
        .chatMemory(chatMemory)
        // RAG:从内存 embeddingStore 中检索匹配的文本片段
        .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
        .build();
可以看到,极简版的特点是“一切皆默认",实际开发中,为了更好的效果,建议采用标准版或进阶版。
2.标准版RAG
下面来试试标准版RAG实现,为了更好地效果,我们需要:
加载 Markdown 文档并按需切割
Markdown文档补充文件名信息
自定义 Embedding 模型
自定义内容检索器
在 Spring Boot 配置文件中添加 Embedding 模型配置,使用阿里云提供的 text-embedding-v4 模型:
langchain4j:
  community:
    dashscope:
      chat-model:
        model-name: qwen-max
        api-key: <You API Key here>
      embedding-model:
        model-name: text-embedding-v4
        api-key: <You API Key here>
新建rag.RagConfig,编写 RAG 相关的代码,执行 RAG 的初始流程并返回了一个定制的内容检索器 Bean:
/**
 * 加载 RAG
 */
@Configuration
public class RagConfig {
    @Resource
    private EmbeddingModel qwenEmbeddingModel;
    @Resource
    private EmbeddingStore<TextSegment> embeddingStore;
    @Bean
    public ContentRetriever contentRetriever() {
        // ------ RAG ------
        // 1. 加载文档
        List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
        // 2. 文档切割:将每个文档按每段进行分割,最大 1000 字符,每次重叠最多 200 个字符
        DocumentByParagraphSplitter paragraphSplitter = new DocumentByParagraphSplitter(1000, 200);
        // 3. 自定义文档加载器
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(paragraphSplitter)
                // 为了提高搜索质量,为每个 TextSegment 添加文档名称
                .textSegmentTransformer(textSegment -> TextSegment.from(
                        textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),
                        textSegment.metadata()
                ))
                // 使用指定的向量模型
                .embeddingModel(qwenEmbeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        // 加载文档
        ingestor.ingest(documents);
        // 4. 自定义内容查询器
        ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(qwenEmbeddingModel)
                .maxResults(5) // 最多 5 个检索结果
                .minScore(0.75) // 过滤掉分数小于 0.75 的结果
                .build();
        return contentRetriever;
    }
}
然后再构建AI Service时绑定内容检索器:
@Resource
private ContentRetriever contentRetriever;
@Bean
public AiCodeHelperService aiCodeHelperService() {
    // 会话记忆
    ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
    // 构造 AI Service
    AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
            .chatModel(qwenChatModel)
            .chatMemory(chatMemory)
            .contentRetriever(contentRetriever) // RAG 检索增强生成
            .build();
    return aiCodeHelperService;
}
获取引用源文档
如果能够给AI的回答下面展示回答的来源,更容易增加内容的可信度。

在 LangChain4j中,实现这个功能很简单。在 Al Service 中新增方法,在原本的返回类型外封装一层 Result 类,就可以获得封装后的结果,从中能够获取到RAG引I用的源文档、以及Token的消耗情况等等。
@SystemMessage(fromResource = "system-prompt.txt")
Result<String> chatWithRag(String userMessage);
修改单元测试,输出更多信息:
@Test
void chatWithRag() {
    Result<String> result = aiCodeHelperService.chatWithRag("怎么学习 Java?有哪些常见面试题?");
    String content = result.content();
    List<Content> sources = result.sources();
    System.out.println(content);
    System.out.println(sources);
}
工具调用-Tools
工具调用(ToolCalling)可以理解为让Al大模型借用外部工具来完成它自己做不到的事情。
跟人类一样,如果只凭手脚完成不了工作,那么就可以利用工具箱来完成。
工具可以是任何东西,比如网页搜索、对外部API的调用、访问外部数据、或执行特定的代码等。比如用户提问“帮我查询上海最新的天气",AI本身并没有这些知识,它就可以调用“查询天气工具",来完成任务。
需要注意的是,工具调用的本质并不是AI服务器自己调用这些工具、也不是把工具的代码发送给AI服务器让它执行,它只能提出要求,表示“我需要执行XX工具完成任务"。而真正执行工具的是我们自己的应用程序,执行后再把结果告诉AI,让它继续工作。

例子:需求->让ai能够通过刷题网站来搜索面试题
实现方案:利用Jsoup库抓取面试鸭搜索页面的题目列表
1.先引入Jsoup库:
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.20.1</version>
</dependency>
然后在tools包下编写工具,通过@Tool注解就能声明工具了
@Slf4j
public class InterviewQuestionTool {
    /**
     * 从面试鸭网站获取关键词相关的面试题列表
     *
     * @param keyword 搜索关键词(如"redis"、"java多线程")
     * @return 面试题列表,若失败则返回错误信息
     */
    @Tool(name = "interviewQuestionSearch", value = """
            Retrieves relevant interview questions from mianshiya.com based on a keyword.
            Use this tool when the user asks for interview questions about specific technologies,
            programming concepts, or job-related topics. The input should be a clear search term.
            """
    )
    public String searchInterviewQuestions(@P(value = "the keyword to search") String keyword) {
        List<String> questions = new ArrayList<>();
        // 构建搜索URL(编码关键词以支持中文)
        String encodedKeyword = URLEncoder.encode(keyword, StandardCharsets.UTF_8);
        String url = "https://www.mianshiya.com/search/all?searchText=" + encodedKeyword;
        // 发送请求并解析页面
        Document doc;
        try {
            doc = Jsoup.connect(url)
                    .userAgent("Mozilla/5.0")
                    .timeout(5000)
                    .get();
        } catch (IOException e) {
            log.error("get web error", e);
            return e.getMessage();
        }
        // 提取面试题
        Elements questionElements = doc.select(".ant-table-cell > a");
        questionElements.forEach(el -> questions.add(el.text().trim()));
        return String.join("\n", questions);
    }
}
给AlService绑定工具:
// 构造 AI Service
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
        .chatModel(qwenChatModel)
        .chatMemory(chatMemory)
        .contentRetriever(contentRetriever) // RAG 检索增强生成
        .tools(new InterviewQuestionTool()) // 工具调用
        .build();
前面是最简单的工具定义方法--声明式。
除了联网搜索外,还有一些经典的工具,比如文件读写、PDF生成、调用终端、输出图表等等。这些工具我们可以自己开发,也可以通过MCP直接使用别人开发好的工具。
模型上下文协议-MCP
MCP(Model ContextProtocol,模型上下文协议)是一种开放标准,目的是增强Al与外部系统的交互能力。MCP 为AI提供了与外部工具、资源和服务交互的标准化方式,让AI能够访问最新数据、执行复杂操作,并与现有系统集成。
可以将MCP想象成AI应用的USB接口。就像USB为设备连接各种外设和配件提供了标准化方式一样,MCP为AI模型连接不同的数据源和工具提供了标准化的方法。
简单来说,通过MCP协议,AI应用可以轻松接入别人提供的服务来实现更多功能,比如查询地理位置、操作数据库、部署网站、甚至是支付等等。刚刚我们通过工具调用实现了面试题的搜索,下面我们利用MCP实现全网搜索内容,这也是一个典型的MCP应用场景了。
SSE方式
1.首先从MCP服务市场搜索web Search服务,他提供了SSE在线调用服务,不用自己在本地安装启动,很方便。
用别人的服务可能是需要API Key的,一般是按量付费。
2.去平台官方获取API Key
3.在程序中使用这个MCP服务
1>引入依赖
<!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-mcp -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-mcp</artifactId>
    <version>1.1.0-beta7</version>
</dependency>
2>在配置文件中新增API Key的配置
bigmodel:
  api-key: <Your Api Key>
3>新建mcp.McpConfig,按照官方的开发方式,初始化和MCP服务的通讯,并创建McpToolProvider的Bean:
@Configuration
public class McpConfig {
    @Value("${bigmodel.api-key}")
    private String apiKey;
    @Bean
    public McpToolProvider mcpToolProvider() {
        // 和 MCP 服务通讯
        McpTransport transport = new HttpMcpTransport.Builder()
                .sseUrl("https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization=" + apiKey)
                .logRequests(true) // 开启日志,查看更多信息
                .logResponses(true)
                .build();
        // 创建 MCP 客户端
        McpClient mcpClient = new DefaultMcpClient.Builder()
                .key("yupiMcpClient")
                .transport(transport)
                .build();
        // 从 MCP 客户端获取工具
        McpToolProvider toolProvider = McpToolProvider.builder()
                .mcpClients(mcpClient)
                .build();
        return toolProvider;
    }
}
注意:上面是通过SSE方式调用MCP.
通过npx或者uvx本地启动MCP服务
1.先安装对应的工具,并且利用下面的配置建立通讯:
McpTransport transport = new StdioMcpTransport.Builder()
    .command(List.of("/usr/bin/npm", "exec", "@modelcontextprotocol/server-everything@0.6.2"))
    .logEvents(true) // only if you want to see the traffic in the log
    .build();
2.在 Al Service中应用MCP工具:
@Resource
private McpToolProvider mcpToolProvider;
// 构造 AI Service
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
        .chatModel(qwenChatModel)
        .chatMemory(chatMemory)
        .contentRetriever(contentRetriever) // RAG 检索增强生成
        .tools(new InterviewQuestionTool()) // 工具调用
        .toolProvider(mcpToolProvider) // MCP 工具调用
        .build();
护轨-Guardrail
把它理解为拦截器就好了。分为输入护轨(input guardrails)和输出护轨(output guardrails),可以在请求 Al前和接收到 Al 的响应后执行一些额外操作,比如调用 Al 前鉴权、调用AI后记录日志。
例子:在调用AI前进行敏感词检测,如果用户提示词包含敏感词,则直接拒绝。
新建guardrail.SafeInputGuardrail,实现InputGuardrail接口:
/**
 * 安全检测输入护轨
 */
public class SafeInputGuardrail implements InputGuardrail {
    private static final Set<String> sensitiveWords = Set.of("kill", "evil");
    /**
     * 检测用户输入是否安全
     */
    @Override
    public InputGuardrailResult validate(UserMessage userMessage) {
        // 获取用户输入并转换为小写以确保大小写不敏感
        String inputText = userMessage.singleText().toLowerCase();
        // 使用正则表达式分割输入文本为单词
        String[] words = inputText.split("\\W+");
        // 遍历所有单词,检查是否存在敏感词
        for (String word : words) {
            if (sensitiveWords.contains(word)) {
                return fatal("Sensitive word detected: " + word);
            }
        }
        return success();
    }
}
LangChain4j提供了几种快速返回的方法,简单来说,想继续调用Al就返回success、否则就返回fatal。

修改AlService,使用输入护轨:
@InputGuardrails({SafeInputGuardrail.class})
public interface AiCodeHelperService {
    @SystemMessage(fromResource = "system-prompt.txt")
    String chat(String userMessage);
    @SystemMessage(fromResource = "system-prompt.txt")
    Report chatForReport(String userMessage);
    // 学习报告
    record Report(String name, List<String> suggestionList) {
    }
}
日志和可观测性
之前我们都是通过Debug查看运行信息,不仅不便于调试,而且生产环境肯定不能这么做。
官方提供了日志和可观测性,来帮我们更好地调试程序、发现问题。
1. 日志
开启日志的方法很简单,直接构造模型时指定开启、或者直接编写 Spring Boot 配置,支持打印 Al 请求和响应日志。
OpenAiChatModel.builder()
    ...
    .logRequests(true)
    .logResponses(true)
    .build();
langchain4j.open-ai.chat-model.log-requests = true
langchain4j.open-ai.chat-model.log-responses = true
logging.level.dev.langchain4j = DEBUG
但并不是所有的ChatModel都支持,比如我测试下来QwenChatModel就不支持。这时只能把希望交给可观测性了。
2.可观测性
可以通过自定义Listener获取ChatModel的调用信息,比较灵活。新建
listener.ChatModelListenerConfig,输出请求、响应、错误信息:
@Configuration
@Slf4j
public class ChatModelListenerConfig {
    
    @Bean
    ChatModelListener chatModelListener() {
        return new ChatModelListener() {
            @Override
            public void onRequest(ChatModelRequestContext requestContext) {
                log.info("onRequest(): {}", requestContext.chatRequest());
            }
            @Override
            public void onResponse(ChatModelResponseContext responseContext) {
                log.info("onResponse(): {}", responseContext.chatResponse());
            }
            @Override
            public void onError(ChatModelErrorContext errorContext) {
                log.info("onError(): {}", errorContext.error().getMessage());
            }
        };
    }
}
但是只定义Listener好像对QwenChatModel不起作用,所以我们需要手动构造自定义的QwenChatModel。
新建model.QwenchatModelconfig,构造ChatModel对象并绑定Listener:
@Configuration
@ConfigurationProperties(prefix = "langchain4j.community.dashscope.chat-model")
@Data
public class QwenChatModelConfig {
    private String modelName;
    private String apiKey;
    @Resource
    private ChatModelListener chatModelListener;
    @Bean
    public ChatModel myQwenChatModel() {
        return QwenChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .listeners(List.of(chatModelListener))
                .build();
    }
}
然后,可以将原本引用ChatModel 的名称改为myQwenchatModel,防止和 Spring Boot 自动注入的ChatModel冲突。
再次调用AI,就能看到很多信息了:
AI服务化
至此,AI的能力基本开发完成,但是目前只支持本地运行,需要编写一个接口提供给前端调用,让AI能够成为个服务。
我们平时开发的大多数接口都是同步接口,也就是等后端处理完再返回。但是对于AI应用,特别是响应时间较长的对话类应用,可能会让用户失去耐心等待,因此推荐使用SSE(Server-SentEvents)技术实现实时流式输出,类似打字机效果,大幅提升用户体验。
1.SSE流式接口开发
LangChain提供了2种方式来支持流式响应(注意,流式响应不支持结构化输出)。一种方法是TokenStream,先让Al对话方法返回TokenStream,然后创建Al Service时指定流式对话模型StreamingChatModel:
interface Assistant {
    TokenStream chat(String message);
}
StreamingChatModel model = OpenAiStreamingChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();
Assistant assistant = AiServices.create(Assistant.class, model);
TokenStream tokenStream = assistant.chat("Tell me a joke");
tokenStream.onPartialResponse((String partialResponse) -> System.out.println(partialResponse))
    .onRetrieved((List<Content> contents) -> System.out.println(contents))
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
    .onCompleteResponse((ChatResponse response) -> System.out.println(response))
    .onError((Throwable error) -> error.printStackTrace())
    .start();
另一种方法,使用Flux代替TokenStream,让Al对话方法返回Flux响应式对象即可。示例代码:
interface Assistant {
  Flux<String> chat(String message);
}
1.首先引入响应式包依赖
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.1.0-beta7</version>
</dependency>
2.然后给Al Service增加流式对话方法,这里顺便支持下多用户的会话记忆:
// 流式对话
Flux<String> chatStream(@MemoryId int memoryId, @UserMessage String userMessage);
3.由于要用到流式模型,需要增加流式模型配置
langchain4j:
  community:
    dashscope:
      streaming-chat-model:
        model-name: qwen-max
        api-key: <Your Api Key>
4.构造AI Service时指定流式对话模型(自动注入即可),并且补充会话记忆提供者
@Resource
private StreamingChatModel qwenStreamingChatModel;
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
        .chatModel(myQwenChatModel)
        .streamingChatModel(qwenStreamingChatModel)
        .chatMemory(chatMemory)
        .chatMemoryProvider(memoryId ->
                MessageWindowChatMemory.withMaxMessages(10)) // 每个会话独立存储
        .contentRetriever(contentRetriever) // RAG 检索增强生成
        .tools(new InterviewQuestionTool()) // 工具调用
        .toolProvider(mcpToolProvider) // MCP 工具调用
        .build();
最后,编写Controller接口。为了方便测试,这里使用Get请求:
@RestController
@RequestMapping("/ai")
public class AiController {
    @Resource
    private AiCodeHelperService aiCodeHelperService;
    @GetMapping("/chat")
    public Flux<ServerSentEvent<String>> chat(int memoryId, String message) {
        return aiCodeHelperService.chatStream(memoryId, message)
                .map(chunk -> ServerSentEvent.<String>builder()
                        .data(chunk)
                        .build());
    }
}
增加服务器配置,指定后端端口和接口路径前缀:
server:
  port: 8081
  servlet:
    context-path: /api
启动服务器,用CURL工具测试调用:
curl -G 'http://localhost:8081/api/ai/chat' \
  --data-urlencode 'message=我是xxx' \
  --data-urlencode 'memoryId=1'
后端支持跨域
为了让前端项目能够顺利调用后在config 包下创建跨域配置类,代码如下:
/**
 * 全局跨域配置
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 覆盖所有请求
        registry.addMapping("/**")
                // 允许发送 Cookie
                .allowCredentials(true)
                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}
LangChain4j与SpringAI
Spring Al目前支持的能力更多,还有国内Spring AlAlibaba的巨头加持,生态更好,遇到问题更容易解决;LangChain4j的优势在于可以独立于Spring项目使用,更自由灵活一些。

更多推荐
 
 


所有评论(0)