LangChain4j学习笔记(二):响应流式传输(Streaming Response)和提示词(Prompt)

LangChain4j学习笔记(二):响应流式传输(Streaming Response)和提示词(Prompt)

知识分子没文化
2026-01-10 / 0 评论 / 751 阅读 / 4,234 字数 / 正在检测是否收录...

目录

LangChain官网:LangChain4j,官方英文文档:Get Started | LangChain4j,API 文档:Overview (LangChain4j)

非官方中文文档:快速开始 | LangChain4j 中文文档

官方示例代码仓库:langchain4j/langchain4j-examples

LangChain4j版本:1.14.0-beta24

一、响应流式传输(Streaming Response)

1.1、介绍

响应流式传输(Streaming Response)是指服务器在处理请求时,将生成的数据逐步、连续地推送给客户端,而不是等全部结果生成完毕后再一次性返回。

在与大语言模型(LLM)对话时,流式传输允许模型以逐个 Token 的形式输出文本,客户端接收数据并实时渲染,从而实现将回答文本一个字一个字“打出来”的效果,好处是当模型的推理回答所消耗时间的太长时,用户实时就能看到大模型的回答,不会让用户一直处于等待中。

目前大模型流式传输的主流实现是 SSE,以 “text/event-stream” 的格式持续推送数据,其整个过程的大致步骤是:

  • 客户端发送一个普通的 HTTP 请求,并在 Header 中声明接收数据的 MIME 类型为:

    Accept: text/event-stream
  • 服务器保持连接打开,以 “text/event-stream” 格式持续推送数据:

    data: {"choices":[{"delta":{"content":"你"}}]}
    data: {"choices":[{"delta":{"content":"好"}}]}
    data: [DONE]

    每一条 data: 行代表一个事件(一个 token 或状态标记),客户端通过 EventSource API 逐条接收并处理。

  • 当模型生成完毕之后,服务器发送 [DONE] 信号并关闭连接。

1.2、实现

添加 Maven 依赖坐标:

<!-- WebFlux 流式输出,提供响应式 Web 支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.4.13</version>
</dependency>

<!-- 提供 TokenStream 到 Flux 的直接转换支持 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-reactor</artifactId>
    <version>1.14.0-beta24</version>
</dependency>

配置类 ChatModelConfig,返回的是 OpenAiStreamingChatModel 对象:

@Configuration
public class ChatModelConfig {
    @Bean("deepseekFluxChatModel")
    public OpenAiStreamingChatModel deepseekFluxChatModel() {
        return OpenAiStreamingChatModel.builder()
                .baseUrl("https://api.deepseek.com")
                .apiKey(System.getenv("Key_Deepseek"))
                .modelName("deepseek-v4-flash")
                .build();
    }
}
方法一:AI Service 接口直接返回 Flux<String>

在代理接口 Assistant 中使用 AiService.streamingChatModel 指定使用的模型

/**
 * 代理接口 Assistant
 */
@AiService(wiringMode = EXPLICIT, streamingChatModel = "deepseekFluxChatModel")
public interface Assistant {
    Flux<String> chatFlux(String userMessage);
}

/**
 * Controller 接口
 */
@RestController
public class ChatController {

    @Autowired
    private Assistant assistant;

    /**
     * 最简洁的方式:直接返回 Flux<String> 类型数据
     */
        // 请求接口预览流式传输效果时可以用 text/stream
//    @GetMapping(value = "/chatFlux", produces = "text/stream;charset=UTF-8")
    // 正式返回给前端的数据用 text/event-stream
    @GetMapping(value = "/chatFlux", produces = "text/event-stream;charset=UTF-8")
    public Flux<String> chatFlux(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
        return assistant.chatFlux(question);
    }

}
方法二:使用 TokenStream 与 Flux.create 桥接

这种方法不用导入 langchain4j-reactor 依赖包,而是手动处理 Token 与 Flux 的转换

/**
 * 代理接口 Assistant
 */
@AiService(wiringMode = EXPLICIT, streamingChatModel = "deepseekFluxChatModel")
public interface Assistant {
    TokenStream chatStreaming(String userMessage);
}

/**
 * Controller 接口
 */
@RestController
public class ChatController {

    @Autowired
    private Assistant assistant;

    /**
     * 精细控制:使用 TokenStream 与 Flux.create 桥接
     */
    // 请求接口预览流式传输效果时可以用 text/stream
//    @GetMapping(value = "/chatStreaming", produces = "text/stream;charset=UTF-8")
    // 正式返回给前端的话用 text/event-stream
    @GetMapping(value = "/chatStreaming", produces = "text/event-stream;charset=UTF-8")
    public Flux<String> chatStreaming(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
        return Flux.create((FluxSink<String> sink) -> {
            // 从模型的回答中获取 TokenStream
            TokenStream tokenStream = assistant.chatStreaming(question);
            // 注册流式回调
            tokenStream.onPartialResponse(token -> sink.next(token)) // 收到 token,推送到 Flux
                    .onToolExecuted(toolExecution -> {           // 工具被执行时的回调
                        System.out.println("工具执行完成: " + toolExecution.request().name());
                    })
                    .onCompleteResponse(response -> {
                        System.out.println("\n完成");
                        sink.complete();      // 流结束
                    })                                               // 推送完成
                    .onError(error -> {
                        System.err.println("Stream error: " + error.getMessage());
                        sink.error(error);    // 发生错误,通知Flux
                    })
                    .start();                                        // 调用 start() 触发请求
        });
    }

}

二、提示词

提示词(Prompt) 是给大模型下达的指令或输入的文本,提示词的质量,直接决定了输出的上限。在大模型后端开发中,提示词不可能是固定的,因此 LangChain4j 将提示词做成了一种可拼装、可复用的组件,支持自定义模板、动态参数绑定、Java对象对象映射成提示词等功能。

LangChain4j 中主要由四种消息类型:

类型 说明 使用场景
SystemMessage 系统消息,定义角色、规则、输出格式等全局指令 每次对话的“剧本”
UserMessage 用户消息,承载用户输入或具体问题 对话中的问句或指令
AiMessage 模型回复的消息 多轮对话的历史记录
ToolExecutionResultMessage 工具调用的结果 在 function calling 流程中反馈工具执行信息

每一次大模型调用时 LangChain4j 都会将这些消息对象按顺序放入一个列表并发送给模型,过程类似于:

List<ChatMessage> messageList = new ArrayList<>();
// 系统提示词 SystemMessage
messageList.add(SystemMessage.from("你是一个经验丰富的英语翻译"));
// 用户提示词 UserMessage
messageList.add(UserMessage.from("翻译:Hello World"));
// AI 之前的回复消息 AiMessage
messageList.add(AiMessage.from("上次AI的回复内容"));
// 如果本次调用了工具,加上调用工具的返回结果 ToolExecutionResultMessage
messageList.add(ToolExecutionResultMessage.from(toolExecutionRequest, "工具执行结果文本"));


// 将合并的所有消息类型发送给模型
openAiChatModel.chat(messageList);

2.1、提示词注解

系统提示词 @SystemMessage

@SystemMessage 为当前 AI 服务方法设置系统提示词。系统提示词是发给大模型的最高优先级指令,用来定义 AI 的全局人设、行为边界、回答风格等。

生效范围:当标注在方法上时,表示仅对该方法生效,标注在接口上时,则对该接口中的所有方法生效。如果同时存在,方法上的提示词会覆盖接口上的提示词。

支持模板变量占位符 “{{变量名}}”,配合方法参数上的 @V 注解进行变量的动态填充。例如:

@SystemMessage("你是一个经验丰富的{{role}}")
String chat(@V("role") String role, String question);

@V("变量名") 用于将方法参数绑定到模板中的 {{变量名}} 占位符,@V 标注的参数不会参与 {} 的顺序填充,未标注 @V 的 String 参数默认对应 {} 占位符。

从 resources 资源路径下读取文本文件:

@SystemMessage(fromResource = "system-prompt.txt")
用户提示词 @UserMessage

@UserMessage 定义用户消息模板,只能标注在方法上,框架会将方法的参数填充到这个模板中。

除了与 @SystemMessage 一样支持模板变量占位符 “{{变量名}}”@UserMessage 也支持一种特有的简化占位符 “{}”,存在多个参数时,多个 “{}” 按照实际参数顺序对应:

@UserMessage("请用{{language}}语言回答:{}")
String chat(@V("language") String language, String question);

不过,“{}” 是按顺序填充的,如果参数较多,可读性会比较差。因此,推荐全部使用模板变量占位符 “{{变量名}}”

@UserMessage 也支持读取 resources 资源路径下的文本文件:

@UserMessage(fromResource = "user-prompt.txt")

需要说明的是,当 AI Service 方法没有标注 @UserMessage 时,框架的默认行为是:将方法的第一个 String 类型参数作为用户消息。例如以下两种写法等价:

// 以下两种写法等价
String chat(String question);

@UserMessage("{}")
String chat(String question);

@SystemMessage@UserMessage 这两个注解组成完整的提示词结构:

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {

    @SystemMessage("你是一个经验丰富的{{role}}")
    @UserMessage("请用{{language}}回答问题:{{question}}")
    String chat(@MemoryId String memoryId, @V("role") String role, @V("language") String language, @V("question") String question);

}

多轮对话 中需要 ChatMemory 来管理历史消息,@MemoryId 注解可以自动注入当前会话的 ID。

2.2、提示词模板 PromptTemplate

PromptTemplate 是依赖原始的字符串和键值对(如 Map)来传递动态数据的文本模板工具。

通过调用静态方法 PromptTemplate.from(..) 初始化提示词模板,模板中使用双花括号 {{变量名}} 作为变量占位符,然后将存放变量映射的 Map 传入到 apply(Map map) 方法中,即可根据变量灵活构造出提示词。

PromptTemplate template = PromptTemplate.from("请将以下文本翻译成{{language}}:\n{{text}}");

Prompt prompt = template.apply(Map.of(
    "language", "中文",
    "text", "Hello world"
));

/**
 * 构造好的提示词为:
 * 请将以下文本翻译成中文:
 * Hello world
 */
String promptStr = prompt.text();

构造出来的 Prompt 对象可以转换成多种消息类型:

SystemMessage systemMessage = prompt.toSystemMessage();

UserMessage userMessage = prompt.toUserMessage();

AiMessage aiMessage = prompt.toAiMessage();

2.3、结构化提示词注解 @StructuredPrompt

@StructuredPromptMap 传参升级为Java 对象传参,把这个类的实例传给 StructuredPromptProcessor.toPrompt() 方法时,框架会自动利用反射机制,将这个结构化的数据对象转换成自然语言形式的提示词。

使用 @StructuredPrompt 注解将一个类标记为结构化提示词模板,每一个字段可以用 @Description() 注解来解释字段的含义

@StructuredPrompt("你是一个经验丰富的{{role}},请将以下内容翻译为{{targetLanguage}}:\n{{translateText}}")
public class TranslationPrompt {

    private String role;

    private String targetLanguage;

    private String translateText;

    public TranslationPrompt(String role, String targetLanguage, String translateText) {
        this.role = role;
        this.targetLanguage = targetLanguage;
        this.translateText = translateText;
    }

}

根据提示词模板构造出提示词文本:

TranslationPrompt translationPrompt = new TranslationPrompt("翻译家", "汉语", "Good health is over wealth");
Prompt prompt = StructuredPromptProcessor.toPrompt(translationPrompt);
String promptText = prompt.text();
System.out.println(promptText);
/**
 * 输出:
 * 你是一个经验丰富的翻译家,请将以下内容翻译为汉语:
 *  Good health is over wealth
 */

@StructuredPrompt("你是一个经验丰富的{{role}},请将以下内容翻译为{{targetLanguage}}:\n{{translateText}}") 中的占位符最终会调用 TranslationPrompt 类中字段值的 toString() 方法,如果类中包含集合、数组、对象,那么需要重写该类的 toString() 方法。

除了手动调用 StructuredPromptProcessor.toPrompt() 方法,还可以直接作为 AI Service 方法参数类型使用 @StructuredPrompt 标记的类:

@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
    String chat(TranslationPrompt prompt);  // 框架自动将TranslationPrompt对象转为提示词
}

三、多模态输入

模态(Modality)指信息存在或被感知的一种形式,例如常见的有:文本(Text)、图像(Image)、音频(Audio)、视频(Video)、传感器数据(Sensor Data)、3D点云(Point Cloud)、脑电信号(EEG)、深度图(Depth Map)等。

多模态输入(Multimodal Input)特指在大模型(如多模态大语言模型,MLLM)的推理过程中,能够同时接收、编码并理解多种模态的数据形式,并且作为前置指令或上下文素材。

多模态支持的广度取决于使用的模型提供商和具体模型,对于不支持的模型,传入 ImageContent 会导致 API 报错,因此务必确认所用模型的文档。

而在 LangChain4j 中,将多模态数据构造成提示词的核心是 UserMessage + Content,其中 Content 常见的子类有:TextContentImageContentAudioContentVideoContent

3.1、同时包含文本和图片

最简单的情况:一张网络图片 + 一个文本提问。

UserMessage userMessage = UserMessage.from(
    TextContent.from("请描述这张图片的内容。"),
    ImageContent.from("https://img.peapix.com/55da8d0d85b4456d902449f0cfaf2c2b_UHD.jpg")
);

// 多张图片
UserMessage userMessage = UserMessage.from(
    TextContent.from("这两张图分别是什么动物?请用 JSON 格式回答。"),
    ImageContent.from("https://img.peapix.com/d1fcb066dfcc43cdba3bbdde61b9944c_UHD.jpg"),
    ImageContent.from("https://img.peapix.com/d073970bf88a437faa59b2d0c5b7c0cd_UHD.jpg")
);

也可以使用 UserMessage.userMessage(Content... contents) 方法,效果相同。

3.2、使用 Base64 编码图片

当图片以 Base64 字符串形式存在时,需要指定 MIME 类型:

String base64Image = "iVBORw0KGgoAAAANSUhEUgAA...";
ImageContent image = ImageContent.from(base64Image, "image/png");
UserMessage userMessage = UserMessage.from(
    TextContent.from("这张截图中有什么错误?"),
    image
);

3.3、从本地文件或输入流构造图片

LangChain4j 没有直接提供从 FileInputStream 创建 ImageContent 的工厂方法,但你可以很轻松地自行转换:

public class ImageUtils {
    public static ImageContent fromFile(Path filePath) throws IOException {
        byte[] bytes = Files.readAllBytes(filePath);
        String mimeType = Files.probeContentType(filePath); // 如 image/jpeg
        String base64 = Base64.getEncoder().encodeToString(bytes);
        return ImageContent.from(base64, mimeType);
    }
}

// 使用
UserMessage userMessage = UserMessage.from(
    TextContent.from("提取这张图片中的文字"),
    ImageUtils.fromFile("path/image.png")
);

注意:确保 MIME 类型与图片实际格式一致,否则模型可能拒绝处理。

3.4、与SystemMessage组合

完整的请求通常包含一个设定全局规则的 SystemMessage,再拼接上多模态的 UserMessage。下面以 Qwen 模型为例:

OpenAiChatModel qwenChatModel = OpenAiChatModel.builder()
        .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
        .apiKey(System.getenv("ApiKey_AliBaiLian"))
        .modelName("qwen3-vl-235b-a22b-thinking")
        .build();

SystemMessage systemMsg = SystemMessage.from(
    "你是一个经验丰富的图片分析助手。对于每张图片,请用中文给出简要描述。"
);

UserMessage userMsg = UserMessage.from(
    TextContent.from("请描述下面这张图片的内容。"), ImageContent.from("https://img.peapix.com/d073970bf88a437faa59b2d0c5b7c0cd_UHD.jpg")
);

ChatResponse chatResponse = qwenChatModel.chat(systemMsg, userMsg);
System.out.println(chatResponse.aiMessage().text());

参考资料:

官方英文文档:Get Started | LangChain4j,API 文档:Overview (LangChain4j)

非官方中文文档:快速开始 | LangChain4j 中文文档

0

评论 (0)

取消