LangChain4j知识总结
LangChanin4j知识总结汇总
文章目录
-
- 1、语言模型
- 2、ChatMessage 的类型
- 3、ChatMemory聊天记忆
- 4、AI服务
- 5、Agents 与 Agentic AI
- 6、工具
- 7、RAG
-
- 7.1、核心 RAG API
-
- 7.1.1、Document
- 7.1.2、Metadata
- 7.1.3、Document Loader文档加载器
- 7.1.4、Document Parser文档解析器
- 7.1.5、Document Transformer文档转换器
- 7.1.6、Graph Transformer知识图谱转换器
- 7.1.7、Text Segment文本块
- 7.1.8、Document Splitter文档分割器
- 7.1.9、Text Segment Transformer文本块转换器
- 7.1.10、Embedding向量数值
- 7.1.11、Embedding Model嵌入模型
- 7.1.12、Embedding Store嵌入存储
- 7.1.13、EmbeddingSearchRequest嵌入搜索请求
- 7.1.14、Filter过滤器
- 7.1.15、EmbeddingSearchResult嵌入搜索结果
- 7.1.16、Embedding Match嵌入匹配
- 7.1.17、嵌入存储摄取器(Embedding Store Ingestor)
- 7.2、Easy RAG 简单RAG
- 7.3、Naive RAG 初级RAG
- 7.4、Advanced RAG 高级RAG
-
- 7.4.1、Retrieval Augmentor检索增强器
- 7.4.2、Default Retrieval Augmentor默认检索增强器
- 7.4.3、Query查询
- 7.4.4、Query Metadata查询元数据
- 7.4.5、Query Transformer查询转换器
- 7.4.6、Content内容
- 7.4.7、Content Retriever内容检索器
- 7.4.8、Query Router 查询路由器
- 7.4.9、Content Aggregator 内容聚合器
- 7.4.10、Content Injector 内容注入器
- 7.4.11、Parallelization 并行化
- 7.4.12、Accessing Sources 访问来源
- 7.4.13、控制聊天内存中存储的内容
- 8、结构化输出
- 9、Guardrails(护栏机制)
LangChain4j很容易上手,根据官方教程和例子学习就行了。
官方教程: https://docs.langchain4j.dev/category/tutorials
官方API: https://docs.langchain4j.dev/apidocs/index.html
官方代码示例: https://github.com/langchain4j/langchain4j-examples
总结内容来自LangChain4j版本 v1.10.0。
文中代码参考自官方:https://github.com/langchain4j/langchain4j-examples
1、语言模型
5种模型类型
,支持llm列表:https://docs.langchain4j.dev/integrations/language-models/
- ChatModel — 文本模型,
- EmbeddingModel — 该模型可以将文本转换为 Embedding
- ImageModel — 该模型可以生成和编辑Image。
- ModerationModel — 该模型可以检查文本是否包含有害内容。
- ScoringModel — 该模型可以针对查询对多段文本进行打分(或排序)
1.1、ChatModel
ChatModel分为传统请求(同步阻塞)和流式请求(异步非阻塞)
1.1.1、传统请求
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.temperature(0.3)
.timeout(ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
String prompt = "介绍一下你自己";
String response = model.chat(prompt);
System.out.println(response);
1.1.2、流式请求
OpenAiStreamingChatModel model = OpenAiStreamingChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
String userMessage = "介绍一下你自己";
model.chat(userMessage, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
System.out.println("onPartialResponse: " + partialResponse);
}
@Override
public void onPartialThinking(PartialThinking partialThinking) {
System.out.println("onPartialThinking: " + partialThinking);
}
@Override
public void onPartialToolCall(PartialToolCall partialToolCall) {
System.out.println("onPartialToolCall: " + partialToolCall);
}
@Override
public void onCompleteToolCall(CompleteToolCall completeToolCall) {
System.out.println("onCompleteToolCall: " + completeToolCall);
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
System.out.println("onCompleteResponse: " + completeResponse);
}
@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});
2、ChatMessage 的类型
目前有四种聊天消息类型,每种对应不同的消息来源:
- UserMessage:来自用户的消息。根据 LLM 所支持的模态,UserMessage 可以只包含文本(String),
也可以包含其他模态。 - AiMessage:AI 生成的消息,用于响应输入的消息。
它可以包含:
text():文本内容
thinking():推理/思考内容
toolExecutionRequests():执行工具的请求。
attributes():额外属性,通常是提供商特定的
ToolExecutionResultMessage:这是 ToolExecutionRequest 的结果。 - SystemMessage:系统发出的消息,通常由开发者定义其内容,写明LLM在对话中的角色、它应如何表现、回答的风格等。不要让终端用户直接输入或注入SystemMessage,通常它位于对话的开头。
- CustomMessage:自定义消息,可以包含任意属性。仅当 ChatModel支持时才能使用该消息类型(目前仅 Ollama 支持)。
2.1、UserMessage
UserMessage 不仅可以包含文本,还可以包含其他类型的内容。UserMessage 包含一个 List contents。
Content 是一个接口,具有以下实现:
- TextContent
- ImageContent
- AudioContent
- VideoContent
- PdfFileContent
同时发送文本和图像给 LLM 的示例:
UserMessage userMessage = UserMessage.from(
TextContent.from("Describe the following image"),
ImageContent.from("https://example.com/cat.jpg")
);
ChatResponse response = model.chat(userMessage);
3、ChatMemory聊天记忆
LangChain4j 提供了两种开箱即用的实现:
- MessageWindowChatMemory,它像一个滑动窗口一样,保留最近的N条消息并淘汰不再适用的旧消息。由于每条消息可能包含不同数量的令牌,MessageWindowChatMemory主要用于快速原型开发。
- TokenWindowChatMemory,它也像一个滑动窗口一样运行,但它侧重于保留最近的 N 个令牌,并根据需要淘汰旧消息。消息是不可分割的,如果一条消息不适合,它将被完全淘汰。TokenWindowChatMemory 需要一个 TokenCountEstimator 来计算每条 ChatMessage 中的令牌数。
3.1、MessageWindowChatMemory使用例子
public class ServiceWithMemoryExample {
interface Assistant {
String chat(String message);
}
public static void main(String[] args) {
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemory(chatMemory)
.build();
String answer = assistant.chat("你好,我叫张明");
System.out.println(answer);
String answerWithName = assistant.chat("你还记得我的名字吗?");
System.out.println(answerWithName);
}
}
为每个用户提供独立聊天记忆
public class ServiceWithMemoryForEachUserExample {
interface Assistant {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build();
System.out.println("用户1:" + assistant.chat(1, "你好,我是张明!"));
System.out.println("用户2:" + assistant.chat(2, "你好,我是李军"));
System.out.println("用户1:" + assistant.chat(1, "我的名字是什么?"));
System.out.println("用户2:" + assistant.chat(2, "我的名字是什么?"));
}
}
为每个用户提供持久化聊天记忆
public class ServiceWithPersistentMemoryForEachUserExample {
interface Assistant {
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}
public static void main(String[] args) {
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(store)
.build();
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemoryProvider(chatMemoryProvider)
.build();
System.out.println("用户1:" + assistant.chat(1, "你好,我是张明!"));
System.out.println("用户2:" + assistant.chat(2, "你好,我是李军"));
System.out.println("用户1:" + assistant.chat(1, "我的名字是什么?"));
System.out.println("用户2:" + assistant.chat(2, "我的名字是什么?"));
}
static class PersistentChatMemoryStore implements ChatMemoryStore {
private final DB db = DBMaker.fileDB("multi-user-chat-memory.db").transactionEnable().make();
private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();
public List<ChatMessage> getMessages(Object memoryId) {
String json = map.get((int) memoryId);
return messagesFromJson(json);
}
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = messagesToJson(messages);
map.put((int) memoryId, json);
db.commit();
}
public void deleteMessages(Object memoryId) {
map.remove((int) memoryId);
db.commit();
}
}
}
3.2、TokenWindowChatMemory使用例子
public class ChatMemoryExamples {
public static void main(String[] args) {
// ChatMemory chatMemory = TokenWindowChatMemory.withMaxTokens(300, new OpenAiTokenCountEstimator(ApiKeys.MODEL_NAME));
ChatMemory chatMemory = TokenWindowChatMemory.withMaxTokens(300, new QwenTokenCountEstimator(ApiKeys.MODEL_NAME));
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
chatMemory.add(userMessage("Hello, my name is Klaus"));
AiMessage answer = model.chat(chatMemory.messages()).aiMessage();
System.out.println(answer.text()); // Hello Klaus! How can I assist you today?
chatMemory.add(answer);
chatMemory.add(userMessage("What is my name?"));
AiMessage answerWithName = model.chat(chatMemory.messages()).aiMessage();
System.out.println(answerWithName.text()); // Your name is Klaus.
chatMemory.add(answerWithName);
}
}
4、AI服务
LangChain4j提供了AI 服务来简化LLM 的应用程序开发,让开发者专注于业务逻辑,而不是底层实现的细节。AI服务目前不支持多模态,请使用低级API(ChatModel)来实现。
普通的请求例子
public class Example {
interface Assistant {
String chat(String message);
}
public static void main(String[] args) {
Assistant assistant = AiServices.create(Assistant.class, model);
String userMessage = "转让有价证券、股权及其他类似收益的增值部分";
String answer = assistant.chat(userMessage);
System.out.println(answer);
}
}
带系统提示词的请求
public class Example {
interface Chef {
@SystemMessage("您是一位专业厨师。您亲切、有礼且言简意赅。")
String answer(String question);
}
public static void main(String[] args) {
Chef chef = AiServices.create(Chef.class, model);
String answer = chef.answer("我应该烤鸡多久?");
System.out.println(answer);
}
}
带系统提示词和变量的请求
public class Example {
interface TextUtils {
@SystemMessage("您是一名{{language}}语的专业翻译人员。")
@UserMessage("翻译以下文本内容: {{text}}")
String translate(@V("text") String text, @V("language") String language);
@SystemMessage("将用户的所有信息总结为{{n}}个要点。仅提供要点形式的表述。")
List<String> summarize(@UserMessage String text, @V("n") int n);
}
public static void main(String[] args) {
TextUtils utils = AiServices.create(TextUtils.class, model);
String translation = utils.translate("你好,你怎么样?", "italian");
System.out.println(translation);
String text = "人工智能,即人工智能,是计算机科学的一个分支,旨在创造能够模拟人类智能的机器。其应用范围涵盖从简单的任务(如识别模式或语音)到更复杂的任务(如做出决策或进行预测)等各个方面。";
List<String> bulletPoints = utils.summarize(text, 3);
bulletPoints.forEach(System.out::println);
}
}
结合枚举类型进行分析
public class Example {
public enum IssueCategory {
MAINTENANCE_ISSUE,
SERVICE_ISSUE,
COMFORT_ISSUE,
FACILITY_ISSUE,
CLEANLINESS_ISSUE,
CONNECTIVITY_ISSUE,
CHECK_IN_ISSUE,
OVERALL_EXPERIENCE_ISSUE
}
interface HotelReviewIssueAnalyzer {
@UserMessage("请对以下评论进行分析: |||{{it}}|||")
List<IssueCategory> analyzeReview(String review);
}
public static void main(String[] args) {
HotelReviewIssueAnalyzer hotelReviewIssueAnalyzer = AiServices.create(HotelReviewIssueAnalyzer.class, model);
String review = "我们在酒店的住宿经历可谓喜忧参半。我们房间的空调无法正常工作,导致夜晚十分不适。";
List<IssueCategory> issueCategories = hotelReviewIssueAnalyzer.analyzeReview(review);
// Should output [MAINTENANCE_ISSUE, SERVICE_ISSUE, COMFORT_ISSUE, OVERALL_EXPERIENCE_ISSUE]
System.out.println(issueCategories);
}
}
提取数据
public class Example {
interface DateTimeExtractor {
@UserMessage("提取日期信息 {{it}}")
LocalDate extractDateFrom(String text);
@UserMessage("提取日期信息 {{it}}")
LocalTime extractTimeFrom(String text);
@UserMessage("提取日期信息 {{it}}")
LocalDateTime extractDateTimeFrom(String text);
}
public static void main(String[] args) {
DateTimeExtractor extractor = AiServices.create(DateTimeExtractor.class, model);
String text = "1968 年的这个夜晚充满了宁静,距离午夜还有 15 分钟。这是在独立日庆祝活动之后的夜晚。";
LocalDate date = extractor.extractDateFrom(text);
System.out.println(date); // 1968-07-04
LocalTime time = extractor.extractTimeFrom(text);
System.out.println(time); // 23:45
LocalDateTime dateTime = extractor.extractDateTimeFrom(text);
System.out.println(dateTime); // 1968-07-04T23:45
}
}
结构化输出
// 例子1
public class POJO_Example1 {
static class Person {
@Description("first name of a person")
private String firstName;
private String lastName;
private LocalDate birthDate;
@Override
public String toString() {
return "Person {" +
" firstName = \"" + firstName + "\"" +
", lastName = \"" + lastName + "\"" +
", birthDate = " + birthDate +
" }";
}
}
interface PersonExtractor {
@UserMessage("从以下文本中提取出一个人名: {{it}}")
Person extractPersonFrom(String text);
}
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL_QWEN)
.apiKey(ApiKeys.OPENAI_API_KEY_QWEN)
.modelName(ApiKeys.MODEL_NAME_QWEN)
// .responseFormat("json_schema")
.strictJsonSchema(true)
.timeout(ofSeconds(60))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String text = "1968年,在独立日余音渐消的时刻,一个名叫约翰的孩子在宁静的夜幕下降临。这个姓多伊的新生儿,开启了新的旅程。";
Person person = extractor.extractPersonFrom(text);
System.out.println(person); // Person { firstName = "约翰", lastName = "多伊", birthDate = 1968-07-04 }
}
}
// 例子2
public class POJO_Example2 {
static class Recipe {
@Description("简短标题,最多 8 个词")
private String title;
@Description("简短描述,最多两句话即可")
private String description;
@Description("每一步都应用少量词来描述,且各步骤之间应相互关联。")
private List<String> steps;
private Integer preparationTimeMinutes;
@Override
public String toString() {
return "Recipe {" +
" title = \"" + title + "\"" +
", description = \"" + description + "\"" +
", steps = " + steps +
", preparationTimeMinutes = " + preparationTimeMinutes +
" }";
}
}
@StructuredPrompt("制作一份仅需使用{{ingredients}}即可烹制而成的{{dish}}的食谱")
static class CreateRecipePrompt {
private String dish;
private List<String> ingredients;
}
interface Chef {
Recipe createRecipeFrom(String... ingredients);
Recipe createRecipe(CreateRecipePrompt prompt);
}
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL_QWEN)
.apiKey(ApiKeys.OPENAI_API_KEY_QWEN)
.modelName(ApiKeys.MODEL_NAME_QWEN)
// .responseFormat("json_schema")
.strictJsonSchema(true)
.timeout(ofSeconds(60))
.build();
Chef chef = AiServices.create(Chef.class, model);
Recipe recipe = chef.createRecipeFrom("黄瓜", "西红柿", "羊乳酪", "洋葱", "橄榄", "柠檬");
System.out.println(recipe);
//Recipe { title = "地中海沙拉", description = "清新爽口的蔬菜与羊乳酪搭配,淋上柠檬橄榄油酱汁。", steps = [切黄瓜和西红柿成小块, 切洋葱薄片并浸泡去辛辣, 混合橄榄和羊乳酪, 挤柠檬汁拌入橄榄油, 将所有材料拌匀], preparationTimeMinutes = 15 }
CreateRecipePrompt prompt = new CreateRecipePrompt();
prompt.dish = "烤盘";
prompt.ingredients = asList("黄瓜", "西红柿", "羊乳酪", "洋葱", "橄榄", "土豆");
Recipe anotherRecipe = chef.createRecipe(prompt);
System.out.println(anotherRecipe);
// Recipe { title = "地中海烤蔬菜配羊乳酪", description = "简单健康的烤蔬菜拼盘,融合黄瓜、西红柿与羊乳酪的清新风味,搭配烤土豆与橄榄的醇香。", steps = [将土豆切块,黄瓜、西红柿、洋葱切厚片, 将所有蔬菜与橄榄放入烤盘,淋上橄榄油, 撒上盐和黑胡椒,均匀拌匀, 顶部放上羊乳酪块,不需融化, 200°C烤30分钟,至土豆金黄、蔬菜软嫩], preparationTimeMinutes = 15 }
}
}
5、Agents 与 Agentic AI
5.1、Agent
在LangChain4j中,Agent是使用LLM执行某个特定任务(或一组任务)的组件。
Agent可以通过在接口方法上添加@Agent注解来定义,注解中提供简短的描述,方便其他Agent了解该Agent的能力,以便决定何时调用。写法类似普通AI服务。
Agent与普通AI服务功能相同,只是它们可以被组合到更复杂的工作流和Agentic系统中。
主要区别在于Agent需要指定outputName —— 即结果会存储在共享变量中,以便其他Agent使用,也可以直接在@Agent注解中声明outputName。
public interface CreativeWriter {
@UserMessage("""
You are a creative writer.
Generate a draft of a story no more than
3 sentences long around the given topic.
Return only the story and nothing else.
The topic is {{topic}}.
""")
//@Agent(outputName = "story", description = "Generates a story based on the given topic")
@Agent("Generates a story based on the given topic")
String generateStory(@V("topic") String topic);
}
使用 AgenticServices.agentBuilder() 方法来创建该Agent的实例,并指定接口与Chat模型:
CreativeWriter creativeWriter = AgenticServices
.agentBuilder(CreativeWriter.class)
.chatModel(myChatModel)
.outputName("story")
.build();
5.2、Agentic系统或Agentic AI
通过协调和组合多个AI服务的能力来创建能够完成更复杂任务的AI应用。
使用大语言模型(LLMs)来完成以下内容:
- 协调任务执行
- 管理工具调用
- 在交互中保持上下文
Agentic 系统架构 可以分为两大类:
- 工作流(workflows)
- 纯代理(pure agents)
5.3、工作流模式
5.3.1、顺序工作流
多个 Agent 依次执行,每个 Agent 的输出作为下一个 Agent 的输入。适用于需要按照固定顺序完成一系列任务的场景。
public interface CvGenerator {
@UserMessage("""
这是关于我的生活和职业历程的信息,
您应当将其整理成一份整洁完整的简历。
切勿编造事实,也不要遗漏技能或经历。
这份简历稍后会进行完善,目前请确保其内容完整。
仅提交简历,不要附带其他文字。
我的人生故事:{{lifeStory}}
""")
@Agent("根据用户提供的信息生成一份整洁的简历")
String generateCv(@V("lifeStory") String userInfo);
}
public interface CvTailor {
@Agent("根据具体要求定制简历")
@SystemMessage("""
你可以通过优化简历来满足要求,但切勿编造事实。
如果这样做能让简历更符合指示要求,你可以删除一些不相关的内容。
目标是让求职者获得面试机会,并且之后能够按照简历所述的表现出色。
不要让简历过长。主简历:{{masterCv}}
""")
@UserMessage("""
以下是调整简历的步骤说明:{{instructions}}
""")
String tailorCv(@V("masterCv") String masterCv, @V("instructions") String instructions);
}
public class Example {
/**
* 该示例展示了如何实现两个代理:
* - CvGenerator(接收个人经历故事并生成完整的简历)
* - CvTailor(接收主简历并根据特定指令(工作描述、反馈等)对其进行调整)
* 然后,我们将按照固定的流程依次调用它们
* 使用序列构建器,并演示如何在它们之间传递参数。
* 在组合多个代理时,所有输入、中间和输出参数以及调用链都会
* 存储在 AgenticScope 中,可供高级用例使用。
*/
private static final ChatModel CHAT_MODEL = ChatModelProvider.createChatModel();
public static void main(String[] args) throws IOException {
CvGenerator cvGenerator = AgenticServices
.agentBuilder(CvGenerator.class)
.chatModel(CHAT_MODEL)
.outputKey("masterCv")
.build();
CvTailor cvTailor = AgenticServices
.agentBuilder(CvTailor.class)
.chatModel(CHAT_MODEL)
.outputKey("tailoredCv")
.build();
UntypedAgent tailoredCvGenerator = AgenticServices
.sequenceBuilder()
.subAgents(cvGenerator, cvTailor)
.outputKey("tailoredCv")
.build();
String lifeStory = StringLoader.loadFromResource("/documents/user_life_story.txt");
String instructions = "根据以下职位描述调整简历内容." + StringLoader.loadFromResource("/documents/job_description_backend.txt");
Map<String, Object> arguments = Map.of(
"lifeStory", lifeStory,
"instructions", instructions
);
String tailoredCv = (String) tailoredCvGenerator.invoke(arguments);
System.out.println("=== TAILORED CV UNTYPED ===");
System.out.println((String) tailoredCv);
}
}
使用强类型接口接收参数
public interface SequenceCvGenerator {
@Agent("根据用户提供的信息生成简历,并根据指示进行定制,不要让简历过长,避免出现空行。")
ResultWithAgenticScope<Map<String, String>> generateTailoredCv(@V("lifeStory") String lifeStory, @V("instructions") String instructions);
}
public static void main(String[] args) throws IOException {
//部分代码省略
SequenceCvGenerator sequenceCvGenerator = AgenticServices
.sequenceBuilder(SequenceCvGenerator.class)
.subAgents(cvGenerator, cvTailor)
.outputKey("bothCvsAndLifeStory")
.output(agenticScope -> {
Map<String, String> bothCvsAndLifeStory = Map.of(
"lifeStory", agenticScope.readState("lifeStory", ""),
"masterCv", agenticScope.readState("masterCv", ""),
"tailoredCv", agenticScope.readState("tailoredCv", "")
);
return bothCvsAndLifeStory;
})
.build();
ResultWithAgenticScope<Map<String,String>> bothCvsAndScope = sequenceCvGenerator.generateTailoredCv(lifeStory, instructions);
AgenticScope agenticScope = bothCvsAndScope.agenticScope();
Map<String,String> bothCvsAndLifeStory = bothCvsAndScope.result();
System.out.println("=== USER INFO (input) ===");
System.out.println(bothCvsAndLifeStory.get("lifeStory"));
System.out.println("=== MASTER CV TYPED (intermediary variable) ===");
System.out.println(bothCvsAndLifeStory.get("masterCv"));
System.out.println("=== TAILORED CV TYPED (output) ===");
System.out.println(bothCvsAndLifeStory.get("tailoredCv"));
}
5.3.2、循环工作流
一个 Agent 多次执行,直到满足退出条件。
public interface CvReviewer {
@Agent("根据具体指示审阅一份简历,给出反馈意见和评分。要考虑到简历与职位的匹配程度。")
@SystemMessage("""
您是此次招聘活动的招聘经理:
{{jobDescription}}
您需要审查申请人的简历,并决定邀请多少名候选人参加现场面试。
您会给每份简历打分并给出反馈(包括优点和缺点)。
您可以忽略诸如缺少地址和占位符之类的事项。
""")
@UserMessage("""
请审阅这份简历:{{cv}}
""")
CvReview reviewCv(@V("cv") String cv, @V("jobDescription") String jobDescription);
}
public interface ScoredCvTailor {
@Agent("根据具体要求定制简历")
@SystemMessage("""
这里有一份简历,需要根据具体的职位描述、反馈或其他指示进行调整。
你可以通过优化简历来满足要求,但切勿编造事实。
如果这样做能让简历更符合指示要求,你可以删除一些不相关的内容。
目的是让求职者获得面试机会,并在面试中展现出简历所描述的优秀表现。
当前的简历:{{cv}}
""")
@UserMessage("""
以下是调整简历的说明及反馈内容:
(再次强调,切勿编造不属于原简历的任何事实。
如果申请人不适用,应突出其与原简历最为匹配的现有特点,
但切勿编造事实)
评审结果:{{cvReview}}
""")
String tailorCv(@V("cv") String cv, @V("cvReview") CvReview cvReview);
}
public class CvReview {
@Description("请从0到1打分,表示您邀请这位候选人参加面试的可能性有多大。")
public double score;
@Description("关于简历的反馈,哪些地方做得好,哪些需要改进,缺少哪些技能,有哪些潜在问题,等等。")
public String feedback;
public CvReview() {}
public CvReview(double score, String feedback) {
this.score = score;
this.feedback = feedback;
}
@Override
public String toString() {
return "\nCvReview: " +
" - score = " + score +
"\n- feedback = \"" + feedback + "\"\n";
}
}
/**
* 该示例展示了如何实现一个 CvReviewer 代理,我们可以将其添加到一个循环中,与我们的 CvTailor 代理一起使用。我们将实现两个代理:
* - ScoredCvTailor(接收一份简历并对其进行定制,使其符合 CvReview(反馈/指导 + 评分)的要求)
* - CvReviewer(接收定制后的简历和职位描述,并返回一个 CvReview 对象(反馈 + 评分)
* 此外,当评分高于某个阈值(例如 0.7)时,循环将结束(退出条件)
*/
public static void main(String[] args) throws IOException {
CvReviewer cvReviewer = AgenticServices.agentBuilder(CvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("cvReview")
.build();
ScoredCvTailor scoredCvTailor = AgenticServices.agentBuilder(ScoredCvTailor.class)
.chatModel(CHAT_MODEL)
.outputKey("cv")
.build();
// 4. Build the sequence
UntypedAgent reviewedCvGenerator = AgenticServices
.loopBuilder().subAgents(cvReviewer, scoredCvTailor)
.outputKey("cv")
.exitCondition(agenticScope -> {
CvReview review = (CvReview) agenticScope.readState("cvReview");
System.out.println("根据分数检查退出条件=" + review.score);
return review.score > 0.8;
})
.maxIterations(3)
.build();
String masterCv = StringLoader.loadFromResource("/documents/master_cv.txt");
String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
Map<String, Object> arguments = Map.of(
"cv", masterCv,
"jobDescription", jobDescription
);
String tailoredCv = (String) reviewedCvGenerator.invoke(arguments);
System.out.println("=== REVIEWED CV UNTYPED ===");
System.out.println(tailoredCv);
}
5.3.3、并行工作流
多个 Agent 可以 独立处理相同输入,这时可用 并行工作流模式。
public interface HrCvReviewer {
@Agent(name = "hrReviewer", description = "审查简历以检查候选人是否符合人力资源部门的要求,给出反馈并打分")
@SystemMessage("""
您是人力资源部门的一员,负责审查简历以填补具有以下要求的职位:
{{hrRequirements}}
您会对每份简历进行评分并给出反馈(包括优点和缺点)。
您可以忽略诸如缺少地址和占位符之类的事项。
重要提示:请仅以有效的 JSON 格式返回您的回复,换行符使用 \\n,不要使用任何 Markdown 格式或代码块。
""")
@UserMessage("""
请审阅这份简历:{{candidateCv}} 以及附带的电话面试记录:{{phoneInterviewNotes}}
""")
CvReview reviewCv(@V("candidateCv") String cv, @V("phoneInterviewNotes") String phoneInterviewNotes, @V("hrRequirements") String hrRequirements);
}
public interface ManagerCvReviewer {
@Agent(name = "managerReviewer", description = "根据职位描述对简历进行评估,给出反馈并打分")
@SystemMessage("""
您是此次招聘活动的招聘经理:
{{jobDescription}}
您需要审查申请人的简历,并决定邀请多少名候选人参加现场面试。
您会给每份简历打分并给出反馈(包括优点和缺点)。
您可以忽略诸如缺少地址和占位符之类的事项。
重要提示:请仅以有效的 JSON 格式返回您的回复,换行符使用 \\n,不要使用任何 Markdown 格式或代码块。
""")
@UserMessage("""
查看这份简历:{{candidateCv}}
""")
CvReview reviewCv(@V("candidateCv") String cv, @V("jobDescription") String jobDescription);
}
public interface TeamMemberCvReviewer {
@Agent(name = "teamMemberReviewer", description = "审查简历以判断候选人是否适合该团队,并给出反馈和评分。")
@SystemMessage("""
你与一群积极主动、自我驱动力强的同事一起工作,而且拥有很大的自由度。
你的团队重视协作、责任和务实精神。
你们会审查申请人的简历,并需决定此人是否适合加入你们的团队。
你们会给每份简历打分并给出反馈(包括优点和缺点)。
你可以忽略诸如缺少地址和占位符之类的事项。
重要提示:请仅以有效的 JSON 格式返回您的回复,换行符使用 \\n,不要使用任何 Markdown 格式或代码块。
""")
@UserMessage("""
查看这份简历:{{candidateCv}}
""")
CvReview reviewCv(@V("candidateCv") String cv);
}
/**
此示例展示了如何实现 3 个并行的 CvReviewer 代理,它们将同时评估简历。我们将实现三个代理:
- 管理员简历评估器(评估候选人完成工作的能力)
输入:简历和职位描述
- 团队成员简历评估器(评估候选人融入团队的能力)
输入:简历
- 人力资源简历评估器(从人力资源角度检查候选人是否符合要求)
输入:简历、人力资源要求
*/
public static void main(String[] args) throws IOException {
HrCvReviewer hrCvReviewer = AgenticServices.agentBuilder(HrCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("hrReview")
.build();
ManagerCvReviewer managerCvReviewer = AgenticServices.agentBuilder(ManagerCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("managerReview")
.build();
TeamMemberCvReviewer teamMemberCvReviewer = AgenticServices.agentBuilder(TeamMemberCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("teamMemberReview")
.build();
var executor = Executors.newFixedThreadPool(3);
UntypedAgent cvReviewGenerator = AgenticServices
.parallelBuilder()
.subAgents(hrCvReviewer, managerCvReviewer, teamMemberCvReviewer)
.executor(executor)
.outputKey("fullCvReview")
.output(agenticScope -> {
CvReview hrReview = (CvReview) agenticScope.readState("hrReview");
CvReview managerReview = (CvReview) agenticScope.readState("managerReview");
CvReview teamMemberReview = (CvReview) agenticScope.readState("teamMemberReview");
String feedback = String.join("\n",
"HR Review: " + hrReview.feedback,
"Manager Review: " + managerReview.feedback,
"Team Member Review: " + teamMemberReview.feedback
);
double avgScore = (hrReview.score + managerReview.score + teamMemberReview.score) / 3.0;
return new CvReview(avgScore, feedback);
})
.build();
String candidateCv = StringLoader.loadFromResource("/documents/tailored_cv.txt");
String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
String hrRequirements = StringLoader.loadFromResource("/documents/hr_requirements.txt");
String phoneInterviewNotes = StringLoader.loadFromResource("/documents/phone_interview_notes.txt");
Map<String, Object> arguments = Map.of(
"candidateCv", candidateCv,
"jobDescription", jobDescription
, "hrRequirements", hrRequirements
, "phoneInterviewNotes", phoneInterviewNotes
);
var review = cvReviewGenerator.invoke(arguments);
System.out.println("=== REVIEWED CV ===");
System.out.println(review);
executor.shutdown();
}
5.3.4、条件工作流
根据条件选择 Agent
public interface EmailAssistant {
@Agent("向未通过筛选的候选人发送拒绝邮件,若无法发送邮件则返回“0”,否则返回已发送邮件的 ID 。")
@SystemMessage("""
您向未通过首轮审核的申请者发送了一封亲切的电子邮件。
您还将申请状态更新为“拒绝”。
您收回已发送的电子邮件的收件编号。
""")
@UserMessage("""
被拒绝的候选人:{{candidateContact}}
关于此职位: {{jobDescription}}
""")
int send(@V("candidateContact") String candidateContact, @V("jobDescription") String jobDescription);
}
public interface InterviewOrganizer {
@Agent("对申请人进行现场面试")
@SystemMessage("""
您会通过向所有相关人员发送日程邀请来组织现场会议,邀请他们在一周内(从当前日期开始)于上午进行 3 小时的面试。
这就是所提及的职位空缺信息:{{职位描述}}
您还会通过一封祝贺邮件邀请候选人,并附上面试详情以及他在前来现场之前应了解的任何事项。
最后,您将申请状态更新为“已邀请到现场”。
""")
@UserMessage("""
安排与这位候选人(适用外部访客政策)的现场面试会面:{{candidateContact}}
""")
String organize(@V("candidateContact") String candidateContact, @V("jobDescription") String jobDescription);
}
/**
* 该示例展示了条件型代理工作流程。
* 根据得分和候选人资料,我们将选择其中一种行动方案:
* - 调用一个代理来为与候选人的现场面试做好一切准备
* - 调用另一个代理发送一封友好的电子邮件,表示我们将不再推进此事
**/
private static final ChatModel CHAT_MODEL = ChatModelProvider.createChatModel();
public static void main(String[] args) throws IOException {
EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
.chatModel(CHAT_MODEL)
.tools(new OrganizingTools())
.build();
InterviewOrganizer interviewOrganizer = AgenticServices.agentBuilder(InterviewOrganizer.class)
.chatModel(CHAT_MODEL)
.tools(new OrganizingTools())
.contentRetriever(RagProvider.loadHouseRulesRetriever())
.build();
UntypedAgent candidateResponder = AgenticServices
.conditionalBuilder()
.subAgents(agenticScope -> ((CvReview) agenticScope.readState("cvReview")).score >= 0.8, interviewOrganizer)
.subAgents(agenticScope -> ((CvReview) agenticScope.readState("cvReview")).score < 0.8, emailAssistant)
.build();
String candidateCv = StringLoader.loadFromResource("/documents/tailored_cv.txt");
String candidateContact = StringLoader.loadFromResource("/documents/candidate_contact.txt");
String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
CvReview cvReviewFail = new CvReview(0.6, "这份简历很不错,但缺少一些与后端职位相关的技术细节。");
CvReview cvReviewPass = new CvReview(0.9, "这份简历非常出色,完全符合后端岗位的所有要求。");
Map<String, Object> arguments = Map.of(
"candidateCv", candidateCv,
"candidateContact", candidateContact,
"jobDescription", jobDescription,
"cvReview", cvReviewPass // 切换至“cvReviewFail”状态,查看其他分支
);
candidateResponder.invoke(arguments);
}
5.4、纯代理式AI
有些情况下代理系统需要更灵活和自适应,允许代理根据上下文和之前的结果自主决定下一步如何执行。这通常被称为 纯代理式 AI。
langchain4j-agentic模块提供了一个开箱即用的监督代理(supervisor agent),它可以被赋予一组子代理,并能够自主生成一个计划,决定下一个要调用的代理,或者判断任务是否已完成。
/**
* 到目前为止,我们构建的是确定性的工作流程:
* - 顺序的、并行的、条件的、循环的,以及这些流程的组合。
* 您还可以构建一个监督者代理系统,在这个系统中,一个代理将动态地决定按何种顺序调用其子代理。
* 在这个例子中,监督者协调招聘工作流程:
* 他应该执行人力资源/经理/团队审查,并要么安排面试,要么发送拒绝邮件。
* 就像组合工作流程示例的第二部分一样,但现在是“自我组织”的。
* 注意,监督者超级代理可以在组合工作流程中像其他超级代理类型一样使用。
* 重要提示:使用 GPT-4o-mini 运行此示例大约需要 50 秒。您可以在漂亮的日志中持续看到正在发生的事情。
* 有一些方法可以加快执行速度,请参阅此文件末尾的注释。
*/
public static void main(String[] args) throws IOException {
// 1. 定义所有子代理
HrCvReviewer hrReviewer = AgenticServices.agentBuilder(HrCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("hrReview")
.build();
ManagerCvReviewer managerReviewer = AgenticServices.agentBuilder(ManagerCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("managerReview")
.build();
TeamMemberCvReviewer teamReviewer = AgenticServices.agentBuilder(TeamMemberCvReviewer.class)
.chatModel(CHAT_MODEL)
.outputKey("teamMemberReview")
.build();
InterviewOrganizer interviewOrganizer = AgenticServices.agentBuilder(InterviewOrganizer.class)
.chatModel(CHAT_MODEL)
.tools(new OrganizingTools())
.build();
EmailAssistant emailAssistant = AgenticServices.agentBuilder(EmailAssistant.class)
.chatModel(CHAT_MODEL)
.tools(new OrganizingTools())
.build();
// 2. 构建“主管”代理程序
SupervisorAgent hiringSupervisor = AgenticServices.supervisorBuilder()
.chatModel(CHAT_MODEL)
.subAgents(hrReviewer, managerReviewer, teamReviewer, interviewOrganizer, emailAssistant)
.contextGenerationStrategy(SupervisorContextStrategy.CHAT_MEMORY_AND_SUMMARIZATION)
.responseStrategy(SupervisorResponseStrategy.SUMMARY) // 我们想要的是关于所发生事情的概要描述,而不是获取一个回复。
.supervisorContext("始终使用所有可用的评审人员。请始终用英语回复。在调用代理时,请使用纯 JSON 格式(不要使用反引号,换行符用“\\n”表示)。") // optional context for the supervisor on how to behave
.build();
// 3.加载求职者简历及职位描述
String jobDescription = StringLoader.loadFromResource("/documents/job_description_backend.txt");
String candidateCv = StringLoader.loadFromResource("/documents/tailored_cv.txt");
String candidateContact = StringLoader.loadFromResource("/documents/candidate_contact.txt");
String hrRequirements = StringLoader.loadFromResource("/documents/hr_requirements.txt");
String phoneInterviewNotes = StringLoader.loadFromResource("/documents/phone_interview_notes.txt");
long start = System.nanoTime();
// 4. 以自然的语气向主管发出请求
String result = (String) hiringSupervisor.invoke(
"Evaluate the following candidate:\n" +
"Candidate CV:\n" + candidateCv + "\n\n" +
"Candidate Contacts:\n" + candidateContact + "\n\n" +
"Job Description:\n" + jobDescription + "\n\n" +
"HR Requirements:\n" + hrRequirements + "\n\n" +
"Phone Interview Notes:\n" + phoneInterviewNotes
);
// 在日志中,您会注意到最终调用了“agent 'done'”这一操作,这就是监控程序完成调用序列的方式。
System.out.println(result);
}
6、工具
LangChain4j 提供了两种使用工具的抽象级别:
- 低级:使用 ChatModel 和 ToolSpecification API
- 高级:使用 AI Services 及 @Tool 注解的 Java 方法
6.1、低级工具 API
可以使用 ChatModel 的 chat(ChatRequest) 方法。StreamingChatModel 中也有类似的方法。
在创建 ChatRequest 时,可以指定一个或多个 ToolSpecification。
ToolSpecification 是一个包含工具全部信息的对象,建议尽可能提供详尽信息:清晰的名称、完整的描述、每个参数的说明等。
- 工具的 name
- 工具的 description
- 工具的 parameters 及其描述
两种方法创建ToolSpecification
- 手动创建
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.description("Returns the weather forecast for a given city")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city") // 必填字段需要显式指定
.build())
.build();
- 使用辅助方法
- ToolSpecifications.toolSpecificationsFrom(Class)
- ToolSpecifications.toolSpecificationsFrom(Object)
- ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools {
@Tool("Returns the weather forecast for a given city")
String getWeather(
@P("The city for which the weather forecast should be returned") String city,
TemperatureUnit temperatureUnit
) {
...
}
}
List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
调用工具
/**
如果 LLM 决定调用工具,返回的 AiMessage 会包含toolExecutionRequests 字段。
此时 AiMessage.hasToolExecutionRequests() 会返回 true。
根据模型不同,它可能包含一个或多个 ToolExecutionRequest 对象 (部分 LLM 支持并行调用多个工具)。
每个 ToolExecutionRequest 应包含:
工具调用的 id(注意:某些 LLM 提供商,如 Google、Ollama,可能省略该 ID)
要调用的工具名称,例如:getWeather
调用参数,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }
*/
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();
你需要根据 ToolExecutionRequest 手动执行工具。
如果你想把执行结果返回给 LLM,需要为每个 ToolExecutionRequest 创建一个 ToolExecutionResultMessage,并与之前的所有消息一起发送:
String result = "It is expected to rain in London tomorrow.";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
.messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response2 = model.chat(request2);
6.2、高级工具 API
给任意 Java 方法添加 @Tool 注解,并在创建 AI Service 时指定它们。
AI Service 会自动将这些方法转换为 ToolSpecification,并在每次与 LLM 交互时包含在请求中。
当 LLM 决定调用工具时,AI Service 会自动执行相应方法,方法的返回值(如果有)会自动发送回 LLM。
// 示例
@Tool("Searches Google for relevant URLs, given the query")
public List<String> searchGoogle(@P("search query") String query) {
return googleSearchService.search(query);
}
@Tool("Returns the content of a web page, given the URL")
public String getWebPageContent(@P("URL of the page") String url) {
Document jsoupDocument = Jsoup.connect(url).get();
return jsoupDocument.body().text();
}
- 工具方法的限制,使用 @Tool 注解的方法:可以是静态的或非静态的,可以是任意可见性(public、private 等)
- 工具方法参数,带有 @Tool 注解的方法可以接受任意数量、类型的参数:
基本类型:int、double 等
对象类型:String、Integer、Double 等
自定义 POJO(可包含嵌套 POJO)
enum 枚举
List<T> / Set<T>,其中 T 是上述类型之一
Map<K,V>(需通过 @P 明确指定 K 和 V 的类型)
不带参数的方法也支持。
- 必填与可选,默认情况下,所有工具方法参数都被视为 必填。LLM 必须为这些参数生成值。
通过在参数上使用 @P(required = false),可以将其设为可选:
@Tool
void getTemperature(String location, @P(value = "Unit of temperature", required = false) Unit unit) {
...
}
record User(String name, @JsonProperty(required = false) String email) {}
// 复杂参数的字段和子字段默认也为 必填。 @JsonProperty(required = false) 将其设为可选:
@Tool
void add(User user) {
...
}
- 工具方法返回类型,带有 @Tool 注解的方法可以返回任意类型,包括 void。
如果方法返回类型为 void,则执行成功后会向 LLM 返回 "Success" 字符串。
如果返回类型为 String,则原样返回字符串给 LLM。
其他返回类型会在返回前自动转换为 JSON 字符串。
注解说明
- @Tool
@Tool 注解有两个可选字段:
- name: 工具的名称。如果未提供,则方法名会作为工具名。
- value: 工具的描述。
任何带有 @Tool 注解并在构建 AI Service 时显式指定的方法,都可以被 LLM 执行:
interface MathGenius {
String ask(String question);
}
class Calculator {
@Tool
double add(int a, int b) {
return a + b;
}
@Tool
double squareRoot(double x) {
return Math.sqrt(x);
}
}
MathGenius mathGenius = AiServices.builder(MathGenius.class)
.chatModel(model)
.tools(new Calculator())
.build();
String answer = mathGenius.ask("What is the square root of 475695037565?");
System.out.println(answer); // The square root of 475695037565 is 689706.486532.
- @P
@P 注解有两个字段:
- value: 参数的描述。必填。
- required: 参数是否必填,默认为 true。可选。
- @Description
类和字段的描述可以使用 @Description 注解来指定:
@Description("要执行的查询")
class Query {
@Description("要选择的字段")
private List<String> select;
@Description("过滤条件")
private List<Condition> where;
}
@Tool
Result executeQuery(Query query) {
...
}
- @ToolMemoryId
如果 AI Service 方法中有使用 @MemoryId 注解的参数,那么你也可以在 @Tool 方法的参数上使用 @ToolMemoryId。 这样,AI Service 方法提供的值会自动传递给 @Tool 方法。 这在多用户/多会话记忆的场景中非常有用,可以区分不同用户或对话。
6.3、工具幻觉处理策略
LLM 有时可能会“幻觉”出不存在的工具(即调用了未定义的工具)。 默认情况下,LangChain4j 会抛出异常。
可以配置一个策略,决定在遇到不存在的工具时返回什么结果。 例如,可以返回一条错误信息,引导 LLM 重试其他工具调用:
AssistantHallucinatedTool assistant = AiServices.builder(AssistantHallucinatedTool.class)
.chatModel(chatModel)
.tools(new HelloWorld())
.hallucinatedToolNameStrategy(toolExecutionRequest -> ToolExecutionResultMessage.from(
toolExecutionRequest, "Error: there is no tool called " + toolExecutionRequest.name()))
.build();
7、RAG
LangChain4j 提供三种风格的 RAG:
- Easy RAG:开始使用 RAG 的最简单方法
- Naive RAG:使用向量搜索的 RAG 基本实现
- Advanced RAG:一个模块化的 RAG 框架,允许进行额外的步骤,例如查询转换、从多个来源检索和重新排名
7.1、核心 RAG API
LangChain4j 提供了一套丰富的 API,让您可以轻松构建自定义 RAG 管道,从简单的到高级的。
7.1.1、Document
Document 类表示一个完整的文档,例如单个 PDF 文件或网页。目前,Document 只能表示文本信息,但未来的更新将使其也支持图像和表格。
有用的方法
- Document.text() 返回 Document 的文本
- Document.metadata() 返回 Document 的 Metadata(参见下面的 “Metadata” 部分)
- Document.toTextSegment() 将 Document 转换为 TextSegment(参见下面的 “TextSegment” 部分)
- Document.from(String, Metadata) 从文本和 Metadata 创建一个 Document
- Document.from(String) 从文本创建一个带有空 Metadata 的 Document
7.1.2、Metadata
每个Document都包含Metadata。它存储关于Document的元信息,例如其名称、来源、上次更新日期、所有者或任何其他相关细节。
Metadata以键值对映射的形式存储,其中键的类型为 String,值的类型可以是以下之一:String、Integer、Long、Float、Double、UUID。
Metadata 有几个用途:
- 在将 Document 的内容包含在发送给 LLM 的提示中时,也可以包含元数据条目,为 LLM 提供额外的参考信息。例如,提供Document名称和来源可以帮助 LLM 更好地理解内容。
- 在搜索要包含在提示中的相关内容时,可以按 Metadata 条目进行过滤。
- 当Document的来源更新时(例如,文档的某个特定页面),可以通过其元数据条目(例如 “id”、“source” 等)轻松找到相应的 Document,并在 EmbeddingStore 中进行更新以保持同步。
有用的方法 - Metadata.from(Map) 从 Map 创建 Metadata
- Metadata.put(String key, String value) / put(String, int) / 等,向 Metadata 添加条目
- Metadata.putAll(Map) 向 Metadata 添加多个条目
- Metadata.getString(String key) / getInteger(String key) / 等,返回 Metadata 条目的值,并将其转换为所需的类型
- Metadata.containsKey(String key) 检查 Metadata 是否包含具有指定键的条目
- Metadata.remove(String key) 按键从 Metadata 中删除条目
- Metadata.copy() 返回 Metadata 的副本
- Metadata.toMap() 将 Metadata 转换为 Map
- Metadata.merge(Metadata) 将当前 Metadata 与另一个 Metadata 合并
7.1.3、Document Loader文档加载器
库中包含的文档加载器之一:
- FileSystemDocumentLoader 来自 langchain4j 模块
- ClassPathDocumentLoader 来自 langchain4j 模块
- UrlDocumentLoader 来自 langchain4j 模块
- AmazonS3DocumentLoader 来自 langchain4j-document-loader-amazon-s3 模块
- AzureBlobStorageDocumentLoader 来自 langchain4j-document-loader-azure-storage-blob 模块
- GitHubDocumentLoader 来自 langchain4j-document-loader-github 模块
- GoogleCloudStorageDocumentLoader 来自 langchain4j-document-loader-google-cloud-storage 模块
- SeleniumDocumentLoader 来自 langchain4j-document-loader-selenium 模块
- PlaywrightDocumentLoader 来自 langchain4j-document-loader-playwright 模块
- TencentCosDocumentLoader 来自 langchain4j-document-loader-tencent-cos 模块
7.1.4、Document Parser文档解析器
Document 可以表示各种格式的文件,例如 PDF、DOC、TXT 等。为了解析这些格式,有一个 DocumentParser 接口,库中包含了一些实现:
- TextDocumentParser来自 langchain4j 模块,能够解析纯文本格式的文件(例如 TXT、HTML、MD 等)
- ApachePdfBoxDocumentParser 来自 langchain4j-document-parser-apache-pdfbox 模块 ,能够解析 PDF 文件
- ApachePoiDocumentParser来自 langchain4j-document-parser-apache-poi 模块 ,能够解析微软 Office 文件格式(例如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等)
- ApacheTikaDocumentParser来自 langchain4j-document-parser-apache-tika 模块,能够自动检测并解析几乎所有现有的文件格式
- MarkdownDocumentParser来自 langchain4j-document-parser-markdown 模块,能够解析 Markdown 格式的文件
- YamlDocumentParser来自 langchain4j-document-parser-yaml 模块,能够解析 YAML 格式的文件
以下是如何从文件系统加载一个或多个 Document 的示例:
// 加载单个文档
Document document = FileSystemDocumentLoader.loadDocument("/home/langchain4j/file.txt", new TextDocumentParser());
// 加载目录中的所有文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j", new TextDocumentParser());
// 加载目录中所有 *.txt 文档
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.txt");
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j", pathMatcher, new TextDocumentParser());
// 加载目录及其子目录中的所有文档
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("/home/langchain4j", new TextDocumentParser());
可以不显式指定 DocumentParser 来加载文档。在这种情况下,将使用默认的 DocumentParser。默认的解析器通过 SPI 加载(例如从 langchain4j-document-parser-apache-tika 或 langchain4j-easy-rag,如果其中一个被导入)。如果没有通过 SPI 找到 DocumentParser,则使用 TextDocumentParser 作为回退。
7.1.5、Document Transformer文档转换器
DocumentTransformer 的实现可以执行各种文档转换,例如:
- 清理:这涉及从 Document 的文本中删除不必要的噪音,可以节省 token 并减少干扰。
- 过滤:完全排除某些 Document 不进行搜索。
- 丰富:可以向 Document 添加额外信息,以潜在地增强搜索结果。
- 摘要:可以对 Document 进行摘要,并将其简短摘要存储在 Metadata 中,以便稍后包含在每个 TextSegment 中(我们将在下面介绍),以潜在地改善搜索。
等等。
此阶段还可以添加、修改或删除 Metadata 条目。
目前,开箱即用提供的唯一实现:
- HtmlToTextDocumentTransformer来自langchain4j-document-transformer-jsoup 模块,它可以从原始 HTML 中提取所需的文本内容和元数据条目。
由于没有一刀切的解决方案,我们建议根据您的独特数据实施您自己的 DocumentTransformer。
7.1.6、Graph Transformer知识图谱转换器
GraphTransformer 是一个接口,通过提取语义图元素(例如节点和关系),将非结构化的 Document 对象转换为结构化的 GraphDocument。它非常适合将原始文本转换为结构化语义图。
GraphTransformer 将原始文档转换为 GraphDocument。这些包括:
- 一组代表文本中实体或概念的节点 (GraphNode)。
- 一组代表这些实体如何连接的关系 (GraphEdge)。
- 作为 source 的原始 Document。
默认实现是 LLMGraphTransformer,它使用语言模型(例如 OpenAI)通过提示工程从自然语言中提取图信息。
主要优点
- 实体和关系提取:识别关键概念及其语义连接。
- 图表示:输出已准备好集成到知识图或图数据库中。
- 模型驱动解析:使用大型语言模型从非结构化文本中推断结构。
Maven 依赖项
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-llm-graph-transformer</artifactId>
<version>${latest version here}</version>
</dependency>
示例用法
import dev.langchain4j.data.document.Document;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.community.data.document.graph.GraphDocument;
import dev.langchain4j.community.data.document.graph.GraphNode;
import dev.langchain4j.community.data.document.graph.GraphEdge;
import dev.langchain4j.community.data.document.transformer.graph.GraphTransformer;
import dev.langchain4j.community.data.document.transformer.graph.llm.LLMGraphTransformer;
import java.time.Duration;
import java.util.Set;
public class GraphTransformerExample {
public static void main(String[] args) {
// 创建一个由 LLM 支持的 GraphTransformer
GraphTransformer transformer = new LLMGraphTransformer(
OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.timeout(Duration.ofSeconds(60))
.build()
);
// 输入文档
Document document = Document.from("Barack Obama was born in Hawaii and served as the 44th President of the United States.");
// 转换文档
GraphDocument graphDocument = transformer.transform(document);
// 访问节点和关系
Set<GraphNode> nodes = graphDocument.nodes();
Set<GraphEdge> relationships = graphDocument.relationships();
nodes.forEach(System.out::println);
relationships.forEach(System.out::println);
}
}
输出示例
GraphNode(name=Barack Obama, type=Person)
GraphNode(name=Hawaii, type=Location)
GraphEdge(from=Barack Obama, predicate=was born in, to=Hawaii)
GraphEdge(from=Barack Obama, predicate=served as, to=President of the United States)
7.1.7、Text Segment文本块
Document被加载,要将它们分割(分块)成更小的片段了。LangChain4j 的领域模型包含一个 TextSegment 类,它表示 Document 的一个片段。TextSegment 只能表示文本信息。
Document分割文档目前有两种广泛使用的方法:
- 每个文档(例如 PDF 文件、网页等)都是原子且不可分割的。在 RAG 管道的检索过程中,检索到最相关的 N 个文档并注入到提示中。在这种情况下,您很可能需要使用长上下文 LLM,因为文档可能相当长。如果检索完整的文档很重要,例如您不能错过某些细节时,此方法是合适的。
- 优点:不会丢失上下文。
- 缺点:
- 消耗更多的 token。
- 有时,文档可能包含多个部分/主题,并非所有部分都与查询相关。
- 向量搜索质量会受到影响,因为各种大小的完整文档被压缩成单个固定长度的向量。
- 文档被分割成更小的片段,例如章节、段落,有时甚至是句子。在 RAG 管道的检索过程中,检索到最相关的 N 个片段并注入到提示中。挑战在于确保每个片段为 LLM 提供了足够的上下文/信息以理解它。缺少上下文可能导致 LLM 误解给定的片段并产生幻觉。一个常见的策略是将文档分割成带有重叠的片段,但这并不能完全解决问题。几种高级技术可以提供帮助,例如“句子窗口检索”、“自动合并检索”和“父文档检索”。我们在此不详细介绍,但本质上,这些方法有助于获取检索到的片段周围的更多上下文,为 LLM 提供检索到的片段之前和之后的额外信息。
- 优点:
- 更好的向量搜索质量。
- 减少 token 消耗。
- 缺点:可能仍会丢失部分上下文。
有用的方法 - TextSegment.text() 返回 TextSegment 的文本
- TextSegment.metadata() 返回 TextSegment 的 Metadata
- TextSegment.from(String, Metadata) 从文本和 Metadata 创建一个 TextSegment
- TextSegment.from(String) 从文本创建一个带有空 Metadata 的 TextSegment
7.1.8、Document Splitter文档分割器
LangChain4j 有一个 DocumentSplitter 接口,并提供了一些开箱即用的实现:
- DocumentByParagraphSplitter
- DocumentByLineSplitter
- DocumentBySentenceSplitter
- DocumentByWordSplitter
- DocumentByCharacterSplitter
- DocumentByRegexSplitter
- 递归:DocumentSplitters.recursive(…)
它们的工作方式如下:
- 您实例化一个 DocumentSplitter,指定所需的 TextSegment 大小,以及可选的字符或 token 重叠。
- 您调用 DocumentSplitter 的 split(Document) 或 splitAll(List) 方法。
- DocumentSplitter 将给定的 Document 分割成更小的单元,其性质因分割器而异。例如,DocumentByParagraphSplitter 将文档分割成段落(由两个或多个连续的换行符定义),而 DocumentBySentenceSplitter 使用 OpenNLP 库的句子检测器将文档分割成句子,依此类推。
- DocumentSplitter 然后将这些更小的单元(段落、句子、单词等)组合成 TextSegment,尝试在不超过步骤 1 中设置的限制的情况下,将尽可能多的单元包含在一个 TextSegment 中。如果某些单元仍然太大而无法放入 TextSegment 中,它会调用一个子分割器。这是另一个 DocumentSplitter,能够将不适合的单元分割成更细粒度的单元。所有 Metadata 条目都从 Document 复制到每个 TextSegment。一个唯一的元数据条目 “index” 被添加到每个文本片段。第一个 TextSegment 将包含 index=0,第二个 index=1,依此类推。
7.1.9、Text Segment Transformer文本块转换器
TextSegmentTransformer 与 DocumentTransformer(如上所述)类似,但它转换的是 TextSegment。
与 DocumentTransformer 一样,没有一刀切的解决方案,因此我们建议根据您的独特数据实施您自己的 TextSegmentTransformer。
一种在改进检索方面效果很好的技术是将 Document 标题或简短摘要包含在每个 TextSegment 中。
7.1.10、Embedding向量数值
Embedding 类封装了一个数值向量,它代表内容(通常是文本,例如 TextSegment)进行向量化后的数据。
有用的方法
- Embedding.dimension() 返回嵌入向量的维度(其长度)
- CosineSimilarity.between(Embedding, Embedding) 计算两个 Embedding 之间的余弦相似度
- Embedding.normalize() 归一化嵌入向量(原地)
7.1.11、Embedding Model嵌入模型
EmbeddingModel 嵌入模型,可将文本转换为 Embedding。
目前支持的嵌入模型https://docs.langchain4j.dev/category/embedding-models/。
有用的方法
- EmbeddingModel.embed(String) 嵌入给定的文本
- EmbeddingModel.embed(TextSegment) 嵌入给定的 TextSegment
- EmbeddingModel.embedAll(List) 嵌入所有给定的 TextSegment
- EmbeddingModel.dimension() 返回此模型生成的 Embedding 的维度
7.1.12、Embedding Store嵌入存储
EmbeddingStore 接口代表一个用于存储 Embedding(也称为向量数据库)的存储。它能够存储 Embedding 并高效地搜索相似(在嵌入空间中距离相近)的 Embedding。
目前支持的向量数据库https://docs.langchain4j.dev/integrations/embedding-stores/。
EmbeddingStore 可以单独存储 Embedding,也可以将 Embedding 和相应的 TextSegment 一起存储:
它可以仅通过 ID 存储 Embedding。原始嵌入数据可以存储在其他地方,并使用 ID 进行关联。
它可以同时存储 Embedding 和原始嵌入数据(通常是 TextSegment)。
常用方法
- EmbeddingStore.add(Embedding):向存储中添加一个给定的 Embedding 并返回一个随机 ID。
- EmbeddingStore.add(String id, Embedding):向存储中添加一个具有指定 ID 的给定 Embedding。
- EmbeddingStore.add(Embedding, TextSegment):向存储中添加一个给定 Embedding 及其关联的 TextSegment,并返回一个随机 ID。
- EmbeddingStore.addAll(List):向存储中添加一个给定的 Embedding 列表,并返回一个随机 ID 列表。
- EmbeddingStore.addAll(List, List):向存储中添加一个给定的 Embedding 列表及其关联的 TextSegment 列表,并返回一个随机 ID 列表。
- EmbeddingStore.addAll(List ids, List, List):向存储中添加一个给定的 Embedding 列表及其关联的 ID 和 TextSegment 列表。
- EmbeddingStore.search(EmbeddingSearchRequest):搜索最相似的 Embedding。
- EmbeddingStore.remove(String id):通过 ID 从存储中删除单个 Embedding。
- EmbeddingStore.removeAll(Collection ids):从存储中删除所有 ID 在给定集合中的 Embedding。
- EmbeddingStore.removeAll(Filter):从存储中删除所有与指定 Filter 匹配的 Embedding。
- EmbeddingStore.removeAll():从存储中删除所有 Embedding。
7.1.13、EmbeddingSearchRequest嵌入搜索请求
EmbeddingSearchRequest 代表一个在 EmbeddingStore 中进行搜索的请求。它有以下属性:
- Embedding queryEmbedding:用作参考的嵌入。
- int maxResults:返回的最大结果数。这是一个可选参数。默认值:3。
- double minScore:最小分数,范围为 0 到 1(包含)。只有分数 >= minScore 的嵌入才会被返回。这是一个可选参数。默认值:0。
- Filter filter:在搜索过程中应用于 Metadata 的过滤器。只有 Metadata 与 Filter 匹配的 TextSegment 才会被返回。
7.1.14、Filter过滤器
Filter 允许在执行向量搜索时按 Metadata 条目进行过滤。
目前支持以下 Filter 类型/操作:
- IsEqualTo (等于)
- IsNotEqualTo (不等于)
- IsGreaterThan (大于)
- IsGreaterThanOrEqualTo (大于或等于)
- IsLessThan (小于)
- IsLessThanOrEqualTo (小于或等于)
- IsIn (在…中)
- IsNotIn (不在…中)
- ContainsString (包含字符串)
- And (且)
- Not (非)
- Or (或)
注
并非所有嵌入存储都支持按 Metadata 过滤,请参见此处的“按 Metadata 过滤”列。
一些支持按 Metadata 过滤的存储并不支持所有可能的 Filter 类型/操作。例如,ContainsString 目前仅由 Milvus、PgVector 和 Qdrant 支持。
有关 Filter 的更多详细信息查询这里:https://github.com/langchain4j/langchain4j/pull/610
7.1.15、EmbeddingSearchResult嵌入搜索结果
EmbeddingSearchResult 代表在 EmbeddingStore 中搜索的结果。它包含一个 EmbeddingMatch 列表。
7.1.16、Embedding Match嵌入匹配
EmbeddingMatch 代表一个匹配的 Embedding,以及其相关性分数、ID 和原始嵌入数据(通常是 TextSegment)。
7.1.17、嵌入存储摄取器(Embedding Store Ingestor)
EmbeddingStoreIngestor 代表一个摄取管道,负责将 Document 摄取到 EmbeddingStore 中。
在最简单的配置中,EmbeddingStoreIngestor 使用指定的 EmbeddingModel 嵌入提供的 Document,并将它们连同其 Embedding 一起存储在指定的 EmbeddingStore 中:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(document1);
ingestor.ingest(document2, document3);
IngestionResult ingestionResult = ingestor.ingest(List.of(document4, document5, document6));
EmbeddingStoreIngestor 中的所有 ingest() 方法都返回一个 IngestionResult。IngestionResult 包含有用的信息,包括 TokenUsage,它显示了用于嵌入的 token 数量。
EmbeddingStoreIngestor 还可以选择使用指定的 DocumentTransformer 转换 Document。如果您想在嵌入之前清理、丰富或格式化 Document,这会很有用。
EmbeddingStoreIngestor 还可以选择使用指定的 DocumentSplitter 将 Document 拆分为 TextSegment。如果 Document 很大,并且您想将它们拆分为更小的 TextSegment 以提高相似性搜索的质量并减少发送给 LLM 的提示大小和成本,这会很有用。
EmbeddingStoreIngestor 还可以选择使用指定的 TextSegmentTransformer 转换 TextSegment。如果您想在嵌入之前清理、丰富或格式化 TextSegment,这会很有用。
一个例子:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// 为每个 Document 添加 userId 元数据条目,以便稍后可以按其过滤
.documentTransformer(document -> {
document.metadata().put("userId", "12345");
return document;
})
// 将每个 Document 拆分为 1000 个 token 的 TextSegment,重叠 200 个 token
.documentSplitter(DocumentSplitters.recursive(1000, 200, new OpenAiTokenCountEstimator("gpt-4o-mini")))
// 将 Document 的名称添加到每个 TextSegment 中以提高搜索质量
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
7.2、Easy RAG 简单RAG
Easy RAG功能使入门RAG尽可能简单,无需学习嵌入、选择向量存储、选择嵌入模型、如何解析和分割文档等。
使用例子
public class Easy_RAG_Example {
private static final ChatModel CHAT_MODEL = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
/**
* 该示例展示了如何实现一个“简易 RAG”(检索增强生成)应用程序。
*/
public static void main(String[] args) {
// 1、加载用于RAG的文档。
List<Document> documents = loadDocuments(toPath("documents/"), glob("*.txt"));
System.out.println("Loaded documents:" + documents.size());
// 2、在这里,我们为文档及其嵌入创建一个空的内存存储。
// 支持的存储库:https://docs.langchain4j.dev/integrations/embedding-stores/
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
// 3、将文档导入到存储中。
IngestionResult result = EmbeddingStoreIngestor.ingest(documents, embeddingStore);
System.out.println("=====result: " + result.tokenUsage().totalTokenCount());
// 4、从嵌入存储中创建一个内容检索器。
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.from(embeddingStore);
// 5、创建一个能够访问文档的助手。
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(CHAT_MODEL)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10)) // 它应该能记住最近的 10 条消息
.contentRetriever(contentRetriever) // 它应当能够访问我们的文件。
.build();
// 6、与助手进行交流。提出问题:
Logger log = LoggerFactory.getLogger(Assistant.class);
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
log.info("==================================================");
log.info("User: ");
String userQuery = scanner.nextLine();
log.info("==================================================");
if ("exit".equalsIgnoreCase(userQuery)) {
break;
}
String agentAnswer = assistant.answer(userQuery);
log.info("==================================================");
log.info("Assistant: " + agentAnswer);
}
}
}
}
7.3、Naive RAG 初级RAG
public class Example {
/**
* 该示例展示了如何实现一个简单的检索增强生成(RAG)应用程序。
* “简单”一词意味着我们不会使用任何高级的 RAG 技术。
* 在与大型语言模型(LLM)的每次交互中,我们将:
* 1. 直接采用用户的查询内容。
* 2. 使用嵌入模型对其进行嵌入处理。
* 3. 用查询的嵌入值在嵌入存储库(包含您文档的小片段)中搜索 X 个最相关的片段。
* 4. 将找到的片段附加到用户的查询内容后。
* 5. 将组合后的输入(用户查询 + 片段)发送给 LLM。
* 6. 希望:
* - 用户的查询表述清晰,并包含进行检索所需的全部细节。
* - 找到的片段与用户的查询相关。
*/
public static void main(String[] args) {
// 让我们创建一个能够了解我们这份文件的助手吧
Assistant assistant = createAssistant("documents/miles-of-smiles-terms-of-use.txt");
// 现在,让我们与助手开始对话吧。我们可以提出这样的问题:
// - 我可以取消预订吗?
// - 我出了意外,需要额外付费吗?
startConversationWith(assistant);
}
private static Assistant createAssistant(String documentPath) {
// 首先,让我们创建一个聊天模型,也就是所谓的大型语言模型(LLM),它将回答我们的问题。
// 在这个示例中,我们将使用 OpenAI 的 gpt-4o-mini,但您可以选择任何支持的模型。
// Langchain4j 目前支持超过 10 家流行的 LLM 提供商。
ChatModel chatModel = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
// 现在,让我们加载一个我们将用于 RAG 的文档。
// 我们将使用一家虚构的汽车租赁公司“微笑里程”的使用条款。
// 对于这个示例,我们将仅导入一个文档,但您可以加载所需的任意数量的文档。
// LangChain4j 提供了从各种来源加载文档的内置支持:
// 文件系统、URL、亚马逊 S3、微软 Azure 存储、GitHub、腾讯 COS。
// 此外,LangChain4j 支持解析多种文档类型:
// 文本、PDF、DOC、XLS、PPT。
// 但是,您也可以从其他来源手动导入您的数据。
DocumentParser documentParser = new TextDocumentParser();
Document document = loadDocument(toPath(documentPath), documentParser);
// 现在,我们需要将这份文档分割成更小的部分,也就是所谓的“片段”。
// 这种方法能让我们在用户提出查询时仅向语言模型发送相关的片段,
// 而不是整个文档。例如,如果用户询问有关取消政策的问题,
// 我们将识别并仅发送与取消相关的那些片段。
// 一个不错的起点是使用递归文档分割器,它最初会尝试按段落进行分割。
// 如果一个段落太大以至于无法放入一个片段中,
// 分割器会递归地按换行符分割它,然后按句子分割,最后(如果必要的话)按单词分割,
// 以确保每段文本都能放入一个片段中。
DocumentSplitter splitter = DocumentSplitters.recursive(300, 0);
List<TextSegment> segments = splitter.split(document);
System.out.println("====Segments: " + segments.size());
// 现在,我们需要对这些片段进行嵌入(也称为“向量化”)处理。
// 嵌入是进行相似性搜索所必需的步骤。
// 在本示例中,我们将使用本地进程内的嵌入模型,但您可以选择任何支持的模型。
// Langchain4j 目前支持超过 10 种流行的嵌入模型提供商。
EmbeddingModel embeddingModel = new BgeSmallEnV15QuantizedEmbeddingModel();
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
System.out.println("====Embeddings: " + embeddings.size() + "," + embeddings.get(2).vectorAsList());
// 接下来,我们将把这些嵌入信息存储在一个嵌入存储库中(也被称为“向量数据库”)。
// 这个存储库将在与语言模型每次交互的过程中用于搜索相关的片段。
// 为了简便起见,此示例使用的是内存中的嵌入存储库,但您可以从任何支持的存储库中进行选择。
// Langchain4j 目前支持超过 15 种流行的嵌入存储库。
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings, segments);
// 内容检索器的任务是根据用户查询来检索相关的内容。
// 目前,它能够检索文本片段,但未来还会进行改进,以支持更多形式的内容,例如图片、音频等。
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2) // 在每次交互过程中,我们将提取出两个最相关的片段。
.minScore(0.5) // 我们希望获取与用户查询内容至少有一定相似度的片段。
.build();
// 最后一步是构建我们的人工智能服务,
// 并将其配置为使用我们之前创建的组件。
return AiServices.builder(Assistant.class)
.chatModel(chatModel)
.contentRetriever(contentRetriever)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
}
}
7.4、Advanced RAG 高级RAG
高级 RAG 可以使用以下核心组件在 LangChain4j 中实现:
- QueryTransformer (查询转换器)
- QueryRouter (查询路由器)
- ContentRetriever (内容检索器)
- ContentAggregator (内容聚合器)
- ContentInjector (内容注入器)
整个流程如下:
- 用户生成一个 UserMessage,它被转换为一个 Query。
- QueryTransformer 将 Query 转换为一个或多个 Query。
- 每个 Query 由 QueryRouter 路由到一个或多个 ContentRetriever。
- 每个 ContentRetriever 为每个 Query 检索相关 Content。
- ContentAggregator 将所有检索到的 Content 组合成一个最终的排名列表。
- 这个 Content 列表被注入到原始的 UserMessage 中。
- 最后,包含原始查询和注入的相关内容的 UserMessage 被发送给 LLM。
7.4.1、Retrieval Augmentor检索增强器
RetrievalAugmentor 是 RAG 管道的入口点。它负责使用从各种来源检索到的相关 Content 增强 ChatMessage。
可以在创建 AI 服务时指定 RetrievalAugmentor 的实例:
Assistant assistant = AiServices.builder(Assistant.class)
...
.retrievalAugmentor(retrievalAugmentor)
.build();
每次调用 AI 服务时,都会调用指定的 RetrievalAugmentor 来增强当前的 UserMessage。
您可以使用 RetrievalAugmentor 的默认实现(如下所述)或实现一个自定义的。
7.4.2、Default Retrieval Augmentor默认检索增强器
LangChain4j 提供了 RetrievalAugmentor 接口的开箱即用实现:DefaultRetrievalAugmentor,它应该适用于大多数 RAG 用例。
7.4.3、Query查询
Query 代表 RAG 管道中的用户查询。它包含查询的文本和查询元数据。
7.4.4、Query Metadata查询元数据
Query 中的 Metadata 包含在 RAG 管道的各个组件中可能很有用的信息,例如:
- Metadata.userMessage():应该被增强的原始 UserMessage。
- Metadata.chatMemoryId():@MemoryId 注解的方法参数的值。更多详细信息在此处。这可以用于识别用户并在检索期间应用访问限制或过滤器。
- Metadata.chatMemory():所有以前的 ChatMessage。这有助于理解提出 Query 的上下文。
7.4.5、Query Transformer查询转换器
QueryTransformer 将给定的 Query 转换为一个或多个 Query。其目标是通过修改或扩展原始 Query 来提高检索质量。
一些已知的提高检索质量的方法包括:
- 查询压缩
- 查询扩展
- 查询重写
- Step-back prompting(回退提示)
- Hypothetical document embeddings (HyDE)(假设文档嵌入)
更多详细信息可以在这里https://blog.langchain.com/query-transformations/。
7.4.5.1、Default Query Transformer默认查询转换器
DefaultQueryTransformer 是 DefaultRetrievalAugmentor 中使用的默认实现。它不对 Query 做任何修改,只是简单地传递它。
7.4.5.2、Compressing Query Transformer压缩查询转换器
CompressingQueryTransformer 使用 LLM 将给定的 Query 和之前的对话压缩为一个独立的 Query。当用户可能提出后续问题,而这些问题引用了之前问题或答案中的信息时,这会很有用。
7.4.5.3、Expanding Query Transformer扩展查询转换器
ExpandingQueryTransformer 使用 LLM 将给定的 Query 扩展为多个 Query。这很有用,因为 LLM 可以用各种方式重述和重新组织 Query,这有助于检索更多相关内容。
7.4.6、Content内容
Content 代表与用户 Query 相关的内容。目前,它仅限于文本内容(即 TextSegment),但将来可能支持其他模式(例如图像、音频、视频等)。
7.4.7、Content Retriever内容检索器
ContentRetriever 使用给定的 Query 从底层数据源检索 Content。底层数据源几乎可以是任何东西:
- 嵌入存储
- 全文搜索引擎
- 向量和全文搜索的混合体
- 网络搜索引擎
- 知识图谱
- SQL 数据库
- 等等
ContentRetriever 返回的 Content 列表按相关性从高到低排序。
7.4.7.1、Embedding Store Content Retriever嵌入存储内容检索器
EmbeddingStoreContentRetriever 使用 EmbeddingModel 嵌入 Query,然后从 EmbeddingStore 中检索相关 Content。
EmbeddingStore embeddingStore = ...
EmbeddingModel embeddingModel = ...
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
// maxResults 也可以根据查询动态指定
.dynamicMaxResults(query -> 3)
.minScore(0.75)
// minScore 也可以根据查询动态指定
.dynamicMinScore(query -> 0.75)
.filter(metadataKey("userId").isEqualTo("12345"))
// filter 也可以根据查询动态指定
.dynamicFilter(query -> {
String userId = getUserId(query.metadata().chatMemoryId());
return metadataKey("userId").isEqualTo(userId);
})
.build();
7.4.7.2、Web Search Content Retriever网络搜索内容检索器
WebSearchContentRetriever 使用 WebSearchEngine 从网络检索相关 Content。
所有支持的WebSearchEngine集成在此处https://docs.langchain4j.dev/category/web-search-engines/。
WebSearchEngine googleSearchEngine = GoogleCustomWebSearchEngine.builder()
.apiKey(System.getenv("GOOGLE_API_KEY"))
.csi(System.getenv("GOOGLE_SEARCH_ENGINE_ID"))
.build();
ContentRetriever contentRetriever = WebSearchContentRetriever.builder()
.webSearchEngine(googleSearchEngine)
.maxResults(3)
.build();
完整例子
public class Example {
/**
* 本示例展示了如何将网络搜索引擎用作额外的内容检索器。
* 本示例需要“langchain4j-web-search-engine-tavily”依赖项。
* */
public static void main(String[] args) {
Assistant assistant = createAssistant();
startConversationWith(assistant);
}
private static Assistant createAssistant() {
// 让我们创建我们的嵌入存储内容检索器。
EmbeddingModel embeddingModel = new BgeSmallEnV15QuantizedEmbeddingModel();
EmbeddingStore<TextSegment> embeddingStore =
embed(toPath("documents/miles-of-smiles-terms-of-use.txt"), embeddingModel);
ContentRetriever embeddingStoreContentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.6)
.build();
// 让我们来创建我们的网络搜索内容提取器。
WebSearchEngine webSearchEngine = TavilyWebSearchEngine.builder()
.apiKey(System.getenv("TAVILY_API_KEY")) // get a free key: https://app.tavily.com/sign-in
.build();
ContentRetriever webSearchContentRetriever = WebSearchContentRetriever.builder()
.webSearchEngine(webSearchEngine)
.maxResults(3)
.build();
// 让我们创建一个查询路由器,它会将每个查询分别传递给两个检索器。
QueryRouter queryRouter = new DefaultQueryRouter(embeddingStoreContentRetriever, webSearchContentRetriever);
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.queryRouter(queryRouter)
.build();
ChatModel model = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
return AiServices.builder(Assistant.class)
.chatModel(model)
.retrievalAugmentor(retrievalAugmentor)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
}
private static EmbeddingStore<TextSegment> embed(Path documentPath, EmbeddingModel embeddingModel) {
DocumentParser documentParser = new TextDocumentParser();
Document document = loadDocument(documentPath, documentParser);
DocumentSplitter splitter = DocumentSplitters.recursive(300, 0);
List<TextSegment> segments = splitter.split(document);
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings, segments);
return embeddingStore;
}
}
7.4.7.3、SQL Database Content Retriever 数据库SQL内容检索器
SqlDatabaseContentRetriever 是 ContentRetriever 的一个实验性实现,可以在 langchain4j-experimental-sql 模块中找到。它使用 DataSource 和 LLM 来为给定的自然语言 Query 生成并执行 SQL 查询。
public class Example {
/**
* 本示例展示了如何使用 SQL 数据库内容检索器。
* 注意!尽管有趣且令人兴奋,但 SqlDatabaseContentRetriever使用起来是危险的!
* 请永远不要在生产环境中使用它!数据库用户必须具有非常有限的只读权限!
* 尽管生成的 SQL 有一定的验证(以确保 SQL 是一个 SELECT 语句),
* 但无法保证它是无害的。自行承担使用风险!
* 在此示例中,我们将使用一个内存中的 H2 数据库,包含 3 个表:客户、产品和订单。
* 本示例需要“langchain4j-experimental-sql”依赖项。
*/
public static void main(String[] args) {
Assistant assistant = createAssistant();
// 您可以提出诸如“我们有多少客户?”以及“我们的畅销产品是什么?”这类问题。
startConversationWith(assistant);
}
private static Assistant createAssistant() {
DataSource dataSource = createDataSource();
ChatModel chatModel = OpenAiChatModel.builder()
.baseUrl(ApiKeys.API_URL)
.apiKey(ApiKeys.OPENAI_API_KEY)
.modelName(ApiKeys.MODEL_NAME)
.build();
ContentRetriever contentRetriever = SqlDatabaseContentRetriever.builder()
.dataSource(dataSource)
.chatModel(chatModel)
.build();
return AiServices.builder(Assistant.class)
.chatModel(chatModel)
.contentRetriever(contentRetriever)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
}
private static DataSource createDataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
dataSource.setUser("sa");
dataSource.setPassword("sa");
String createTablesScript = read("sql/create_tables.sql");
execute(createTablesScript, dataSource);
String prefillTablesScript = read("sql/prefill_tables.sql");
execute(prefillTablesScript, dataSource);
return dataSource;
}
private static String read(String path) {
try {
return new String(Files.readAllBytes(toPath(path)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void execute(String sql, DataSource dataSource) {
try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
for (String sqlStatement : sql.split(";")) {
statement.execute(sqlStatement.trim());
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
7.4.7.4、Elasticsearch Content Retriever Elasticsearch内容检索器
ElasticsearchContentRetriever 是与 Elasticsearch 的集成。它支持全文搜索、向量搜索和混合搜索。可在 langchain4j-elasticsearch 模块中找到它。
public class Example {
private final String indexName;
private final EmbeddingModel embeddingModel;
private final ElasticsearchContentRetriever contentRetrieverWithVector;
private final ElasticsearchContentRetriever contentRetrieverWithFullText;
private final ElasticsearchContentRetriever contentRetrieverWithHybrid;
public Example() {
embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel();
indexName = randomUUID();
boolean includeVector = elasticsearchClientHelper.isGTENineTwo();
// 全文搜索
contentRetrieverWithFullText = ElasticsearchContentRetriever.builder()
.configuration(ElasticsearchConfigurationFullText.builder().build())
.restClient(elasticsearchClientHelper.restClient)
.indexName(indexName)
.maxResults(3)
.minScore(0.0)
.build();
// 向量搜索
contentRetrieverWithVector = ElasticsearchContentRetriever.builder()
.configuration(ElasticsearchConfigurationKnn.builder()
.includeVectorResponse(includeVector)
.build())
.restClient(elasticsearchClientHelper.restClient)
.indexName(indexName)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.0)
.build();
// 混合搜索
contentRetrieverWithHybrid = ElasticsearchContentRetriever.builder()
.configuration(ElasticsearchConfigurationHybrid.builder()
.includeVectorResponse(includeVector)
.build())
.restClient(elasticsearchClientHelper.restClient)
.indexName(indexName)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.0)
.build();
}
// 省略code
}
7.4.7.5、Neo4j 内容检索器
Neo4jContentRetriever 是与 Neo4j 图数据库的集成。它将自然语言查询转换为 Neo4j Cypher 查询,并通过在 Neo4j 中运行这些查询来检索相关信息。它可在 langchain4j-community-neo4j-retriever 模块中找到。
7.4.8、Query Router 查询路由器
QueryRouter 负责将 Query 路由到适当的 ContentRetriever。
7.4.8.1、默认查询路由器(Default Query Router)
DefaultQueryRouter 是 DefaultRetrievalAugmentor 中使用的默认实现。它将每个 Query 路由到所有配置的 ContentRetriever。
7.4.8.2、Language Model Query Router 语言模型查询路由器
LanguageModelQueryRouter 使用 LLM 来决定将给定的 Query 路由到哪里。
7.4.9、Content Aggregator 内容聚合器
ContentAggregator 负责聚合来自以下来源的多个排名列表的 Content:
- 多个 Query
- 多个 ContentRetriever
- 两者兼有
7.4.9.1、Default Content Aggregator 默认内容聚合器
DefaultContentAggregator 是 ContentAggregator 的默认实现,它执行两阶段的倒数排名融合(RRF)。请参阅 DefaultContentAggregator Javadoc 以获取更多详细信息。
7.4.9.2、Re-Ranking Content Aggregator 重新排名内容聚合器
ReRankingContentAggregator 使用 ScoringModel(如 Cohere)来执行重新排名。支持的评分(重新排名)模型的完整列表可以在这里找到。请参阅 ReRankingContentAggregator Javadoc 以获取更多详细信息。
7.4.10、Content Injector 内容注入器
ContentInjector 负责将 ContentAggregator 返回的 Content 注入到 UserMessage 中。
7.4.10.1、Default Content Injector 默认内容注入器
DefaultContentInjector 是 ContentInjector 的默认实现,它只是简单地在 UserMessage 的末尾附加 Content,并带有前缀 Answer using the following information: (使用以下信息回答:)。
您可以通过 3 种方式自定义如何将 Content 注入 UserMessage:
- 重写默认的 PromptTemplate:
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("{{userMessage}}\n{{contents}}"))
.build())
.build();
请注意,PromptTemplate 必须包含 {{userMessage}} 和 {{contents}} 变量。
- 扩展 DefaultContentInjector 并重写其中一个 format 方法。
- 实现一个自定义的 ContentInjector。
DefaultContentInjector 还支持从检索到的 Content.textSegment() 中注入 Metadata 条目:
DefaultContentInjector.builder()
.metadataKeysToInclude(List.of("source"))
.build()
在这种情况下,TextSegment.text() 将以“content: ”前缀开头,并且来自 Metadata 的每个值都将以其键作为前缀。最终的 UserMessage 将如下所示:
我如何取消预订?
Answer using the following information:
content: To cancel a reservation, go to ...
source: ./cancellation_procedure.html
content: Cancellation is allowed for ...
source: ./cancellation_policy.html
7.4.11、Parallelization 并行化
当只有一个 Query 和一个 ContentRetriever 时,DefaultRetrievalAugmentor 在同一线程中执行查询路由和内容检索。否则,将使用 Executor 来并行处理。默认情况下,使用修改过的 (keepAliveTime 为 1 秒而不是 60 秒) Executors.newCachedThreadPool(),但您可以在创建 DefaultRetrievalAugmentor 时提供一个自定义的 Executor 实例:
DefaultRetrievalAugmentor.builder()
...
.executor(executor)
.build;
7.4.12、Accessing Sources 访问来源
如果您在使用 AI 服务时希望访问来源(用于增强消息的检索到的 Content),您可以轻松地通过将返回类型包装在 Result 类中来实现:
interface Assistant {
Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("How to do Easy RAG with LangChain4j?");
String answer = result.content();
List<Content> sources = result.sources();
当进行流式传输时,可以使用 onRetrieved() 方法指定一个 Consumer<List>:
interface Assistant {
TokenStream chat(String userMessage);
}
assistant.chat("How to do Easy RAG with LangChain4j?")
.onRetrieved((List<Content> sources) -> ...)
.onPartialResponse(...)
.onCompleteResponse(...)
.onError(...)
.start();
7.4.13、控制聊天内存中存储的内容
在使用与 AI 服务结合的检索增强器时,您可以控制是将增强后的用户消息(其中包含检索到的内容)还是原始用户消息存储在聊天内存中。
这种行为的设置是通过在“AiServices”构建器中使用“storeRetrievedContentInChatMemory”选项来实现的。
配置
- true(默认值)
将增强后的用户消息(原始查询加上检索到的内容)存储在聊天内存中。
同时,该增强后的消息也会发送给语言模型。 - false
仅在聊天内存中保存原始的用户消息(不包含检索到的内容)。
增强后的消息仍会在推理过程中发送给语言模型。
仅存储原始用户消息在某些情况下会很有用,比如您希望保持聊天记录简洁,并与用户的实际输入保持一致,同时还能为语言模型提供获取到的相关背景信息,以便生成回答。
interface Assistant {
String chat(String userMessage);
}
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
MessageWindowChatMemory chatMemory =
MessageWindowChatMemory.withMaxMessages(10);
RetrievalAugmentor retrievalAugmentor =
DefaultRetrievalAugmentor.builder()
.contentRetriever(
EmbeddingStoreContentRetriever.from(embeddingStore, embeddingModel))
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemory(chatMemory)
.retrievalAugmentor(retrievalAugmentor)
// Store only the original user message in chat memory
.storeRetrievedContentInChatMemory(false)
.build();
8、结构化输出
许多大型语言模型(LLMs)和 LLM 提供商都支持生成结构化格式的输出,通常是 JSON。这些输出可以轻松地映射到 Java 对象,并在应用程序的其他部分中使用。
根据 LLM 和 LLM 提供商的不同,有三种方法可以实现这一目标(从最可靠到最不可靠):
- JSON Schema
- 提示 + JSON 模式
- 提示
8.1、JSON Schema
LangChain4j 在低级 ChatModel API 和高级 AI Service API 中都支持 JSON Schema 功能。
8.1.1、在 ChatModel 中使用 JSON Schema
在低级 ChatModel API 中,可以在创建 ChatRequest 时使用与 LLM 提供商无关的 ResponseFormat 和 JsonSchema 来指定 JSON Schema:
ResponseFormat responseFormat = ResponseFormat.builder()
.type(JSON) // type 可以是 TEXT (默认) 或 JSON
.jsonSchema(JsonSchema.builder()
.name("Person") // OpenAI 需要为 schema 指定名称
.rootElement(JsonObjectSchema.builder() // 见 [1]
.addStringProperty("name")
.addIntegerProperty("age")
.addNumberProperty("height")
.addBooleanProperty("married")
.required("name", "age", "height", "married") // 见 [2]
.build())
.build())
.build();
UserMessage userMessage = UserMessage.from("""
John is 42 years old and lives an independent life.
He stands 1.75 meters tall and carries himself with confidence.
Currently unmarried, he enjoys the freedom to focus on his personal goals and interests.
""");
ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.logRequests(true)
.logResponses(true)
.build();
ChatResponse chatResponse = chatModel.chat(chatRequest);
String output = chatResponse.aiMessage().text();
System.out.println(output); // {"name":"John","age":42,"height":1.75,"married":false}
Person person = new ObjectMapper().readValue(output, Person.class);
System.out.println(person); // Person[name=John, age=42, height=1.75, married=false]
JSON Schema 的结构是使用 JsonSchemaElement 接口定义的,其子类型如下:
- JsonObjectSchema - 用于对象类型。
- JsonStringSchema - 用于 String、char/Character 类型。
- JsonIntegerSchema - 用于 int/Integer、long/Long、BigInteger 类型。
- JsonNumberSchema - 用于 float/Float、double/Double、BigDecimal 类型。
- JsonBooleanSchema - 用于 boolean/Boolean 类型。
- JsonEnumSchema - 用于 enum 类型。
- JsonArraySchema - 用于数组和集合(例如,List、Set)。
- JsonReferenceSchema - 用于支持递归(例如,Person 有一个 Set children 字段)。
- JsonAnyOfSchema - 用于支持多态性(例如,Shape 可以是 Circle 或 Rectangle)。
- JsonNullSchema - 用于支持可空类型。
- JsonRawSchema - 用于使用自定义的完整定义的 JSON Schema。
8.1.1.1、JsonObjectSchema
JsonObjectSchema 表示一个带有嵌套属性的对象。它通常是 JsonSchema 的根元素。
有几种方法可以向 JsonObjectSchema 添加属性:
- 您可以使用 properties(Map<String, JsonSchemaElement> properties) 方法一次性添加所有属性:
JsonSchemaElement citySchema = JsonStringSchema.builder()
.description("The city for which the weather forecast should be returned")
.build();
JsonSchemaElement temperatureUnitSchema = JsonEnumSchema.builder()
.enumValues("CELSIUS", "FAHRENHEIT")
.build();
Map<String, JsonSchemaElement> properties = Map.of(
"city", citySchema,
"temperatureUnit", temperatureUnitSchema
);
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addProperties(properties)
.required("city") // 必需的属性应显式指定
.build();
- 您可以使用 addProperty(String name, JsonSchemaElement jsonSchemaElement) 方法单独添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addProperty("city", citySchema)
.addProperty("temperatureUnit", temperatureUnitSchema)
.required("city")
.build();
- 您可以使用 add{Type}Property(String name) 或 add{Type}Property(String name, String description) 方法单独添加属性:
JsonSchemaElement rootElement = JsonObjectSchema.builder()
.addStringProperty("city", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city")
.build();
8.1.1.2、JsonEnumSchema
创建一个 JsonEnumSchema 的示例:
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.description("Marital status of the person")
.enumValues(List.of("SINGLE", "MARRIED", "DIVORCED"))
.build();
8.1.1.3、JsonArraySchema
创建一个 JsonArraySchema 以定义一个字符串数组的示例:
JsonSchemaElement itemSchema = JsonStringSchema.builder()
.description("The name of the person")
.build();
JsonSchemaElement arraySchema = JsonArraySchema.builder()
.description("All names of the people found in the text")
.items(itemSchema)
.build();
8.1.1.4、JsonReferenceSchema
JsonReferenceSchema 可用于支持递归:
String reference = "person"; // 引用在 schema 中应该是唯一的
JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
.addStringProperty("name")
.addProperty("children", JsonArraySchema.builder()
.items(JsonReferenceSchema.builder()
.reference(reference)
.build())
.build())
.required("name", "children")
.definitions(Map.of(reference, JsonObjectSchema.builder()
.addStringProperty("name")
.addProperty("children", JsonArraySchema.builder()
.items(JsonReferenceSchema.builder()
.reference(reference)
.build())
.build())
.required("name", "children")
.build()))
.build();
// 注
// JsonReferenceSchema 目前仅受 Azure OpenAI、Mistral 和 OpenAI 支持。
8.1.1.5、添加描述
所有 JsonSchemaElement 子类型,除了 JsonReferenceSchema,都有一个 description 属性。如果 LLM 没有提供所需的输出,可以提供描述以向 LLM 提供更多指令和正确输出的示例,例如:
JsonSchemaElement stringSchema = JsonStringSchema.builder()
.description("The name of the person, for example: John Doe")
.build();
8.1.1.6、限制
在 ChatModel 中使用 JSON Schema 时,存在一些限制:
- 它仅适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- 对于 OpenAI,它尚不适用于流式模式。对于 Google AI Gemini、Mistral 和 Ollama,可以在创建/构建模型时通过 responseSchema(…) 指定 JSON Schema。
- JsonReferenceSchema 和 JsonAnyOfSchema 目前仅受 Azure OpenAI、Mistral 和 OpenAI 支持。
8.1.2、在 AI Services 中使用 JSON Schema
在使用 AI Services 时,可以更轻松地以更少的代码实现相同的目标:
interface PersonExtractor {
Person extractPersonFrom(String text);
}
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.strictJsonSchema(true)
.logRequests(true)
.logResponses(true)
.build();
// 或者
ChatModel chatModel = OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("llama3.1")
.supportedCapabilities(RESPONSE_FORMAT_JSON_SCHEMA)
.logRequests(true)
.logResponses(true)
.build();
PersonExtractor personExtractor = AiServices.create(PersonExtractor.class, chatModel); // 见 [1]
String text = """
John is 42 years old and lives an independent life.
He stands 1.75 meters tall and carries himself with confidence.
Currently unmarried, he enjoys the freedom to focus on his personal goals and interests.
""";
Person person = personExtractor.extractPersonFrom(text);
System.out.println(person); // Person[name=John, age=42, height=1.75, married=false]
当满足所有以下条件时:
- AI Service 方法返回一个 POJO
- 所使用的 ChatModel 支持 JSON Schema 功能
- 在所使用的 ChatModel 上启用了 JSON Schema 功能
ResponseFormat 和 JsonSchema 将根据指定的返回类型自动生成。
注
请确保在配置 ChatModel 时显式启用 JSON Schema 功能,因为它默认是禁用的。
生成的 JsonSchema 的 name 是返回类型的简单名称(getClass().getSimpleName()),在本例中为:“Person”。
一旦 LLM 响应,输出将被解析为对象并从 AI Service 方法返回。
必需和可选
默认情况下,生成的 JsonSchema 中的所有字段和子字段都被视为可选。这是因为当 LLM 缺乏足够的信息时,它倾向于虚构并用合成数据填充字段(例如,当名称缺失时使用“John Doe”)。
注
请注意,如果 LLM 未为可选的原始类型字段(例如 int、boolean 等)提供值,它们将被初始化为默认值(例如 int 为 0,boolean 为 false)。
注
请注意,当严格模式开启时 (strictJsonSchema(true)),可选的 enum 字段仍可能被虚构的值填充。
要使字段成为必需的,可以使用 @JsonProperty(required = true) 进行注解:
record Person(@JsonProperty(required = true) String name, String surname) {
}
interface PersonExtractor {
Person extractPersonFrom(String text);
}
注
请注意,当与工具一起使用时,所有字段和子字段默认都被视为必需。
添加描述
如果 LLM 没有提供所需的输出,可以对类和字段使用 @Description 进行注解,以向 LLM 提供更多指令和正确输出的示例,例如
@Description("a person")
record Person(@Description("person's first and last name, for example: John Doe") String name,
@Description("person's age, for example: 42") int age,
@Description("person's height in meters, for example: 1.78") double height,
@Description("is person married or not, for example: false") boolean married) {
}
// 注
// 请注意,放置在 enum 值上的 @Description 没有效果 且**不会**包含在生成的 JSON Schema 中:
enum Priority {
@Description("Critical issues such as payment gateway failures or security breaches.") // 这将被忽略
CRITICAL,
@Description("High-priority issues like major feature malfunctions or widespread outages.") // 这将被忽略
HIGH,
@Description("Low-priority issues such as minor bugs or cosmetic problems.") // 这将被忽略
LOW
}
限制
在 AI Services 中使用 JSON Schema 时,存在一些限制:
- 它仅适用于支持的 Azure OpenAI、Google AI Gemini、Mistral、Ollama 和 OpenAI 模型。
- JSON Schema 的支持需要在配置 ChatModel 时显式启用。
- 它不适用于流式模式。
- 并非所有类型都受支持。请参阅这里支持的类型列表。
- POJO 可以包含:
- 标量/简单类型(例如,String、int/Integer、double/Double、boolean/Boolean 等)
- enum
- 嵌套的 POJO
- List、Set 和 T[],其中 T 是一个标量、一个 enum 或一个 POJO
- 递归目前仅受 Azure OpenAI、Mistral 和 OpenAI 支持。
- 尚不支持多态性。返回的 POJO 及其嵌套的 POJO 必须是具体类;不支持接口或抽象类。
- 当 LLM 不支持 JSON Schema 功能,或者它未启用,或者类型不受支持时,AI Service 将退回到提示。
9、Guardrails(护栏机制)
注意
护栏是一项实验性功能。其接口规范和运行方式在后续版本中可能会有所变化。
防护栏是一种用于验证语言模型输入和输出的有效机制,以确保其符合您的预期。通过防护栏,您可以执行以下一些操作:
- 验证用户输入是否在规定范围内
- 在调用语言模型之前,确保输入符合某些标准(例如,防止提示注入攻击)
- 确保输出格式正确(即,是一个具有正确模式的 JSON 文档)
- 确保语言模型的输出与业务规则和约束条件相一致(例如,如果这是 X 公司的聊天机器人,那么回复中不应包含任何关于竞争对手 Y 的内容)
- 检测幻觉现象
注意
只有在使用 AI 服务时才会有防护栏这一功能。它是一种更高级别的设置,不能应用于 ChatModel 或 StreamingChatModel 中。
9.1、实现 Guardrails
理想情况下,Guardrail 的实现应遵循单一职责原则,即每个 Guardrail 类只验证一件事情。然后将多个 Guardrail 串联起来,以防护多个方面。
Guardrail 链中的顺序很重要。第一个失败的 Guardrail 会触发整体失败。应确保最容易捕获错误的 Guardrail 排在链的前面,而那些仅在极少情况下才会失败的 Guardrail 放在链的后面。
另外请记住,Guardrail 本身可以调用其他服务,甚至触发其他 LLM 交互。如果这些 Guardrail 执行有延迟或会带来额外的成本,请务必考虑这个因素。对于更“昂贵”的 Guardrail,可以将其放在链的末尾。
9.2、输入 Guardrails
输入 Guardrails 是在调用 LLM 之前执行的函数。如果输入 Guardrail 失败,将阻止 LLM 被调用。输入 Guardrails 是调用 LLM 之前的最后一步,且在任何 RAG 操作完成后执行。
实现输入 Guardrails
实现输入 Guardrails 需要实现 InputGuardrail 接口。该接口提供两种 validate 方法的变体,必须至少实现其中一种:
InputGuardrailResult validate(UserMessage userMessage);
InputGuardrailResult validate(InputGuardrailRequest params);
第一种适用于简单的 Guardrail,或只需要访问 UserMessage 的场景。
第二种适用于需要更多信息的复杂 Guardrail,例如聊天记录、用户消息模板、增强结果或传递给模板的变量。详细信息请参见 InputGuardrailRequest。
一些可以做的示例:
- 检查增强结果中是否有足够的文档
- 确保用户不是重复提问
- 缓解潜在的提示注入攻击
输入 Guardrail 的结果
输入 Guardrail 的结果类型如下,并且 InputGuardrail 接口中提供了对应的辅助方法:
| 结果 | InputGuardrail接口的辅助方法 | 描述 |
|---|---|---|
| success | success() | - 输入有效 - 执行链中的下一个 Guardrail - 如果最后一个 Guardrail 通过,则调用 LLM |
| success with alternate result | successWith(String) | 类似 success,但用户消息会在继续下一步(下一个 Guardrail 或 LLM 调用)前被修改 |
| failure | failure(String) 或 failure(String, Throwable) |
- 输入无效,但仍继续执行链中的其他 Guardrails 以收集所有验证问题 - LLM 不会被调用 |
| fatal | fatal(String) 或 fatal(String, Throwable) | - 输入无效,立即停止执行并抛出 InputGuardrailException - LLM 不会被调用 |
声明输入 Guardrails
声明输入 Guardrails 的方式有以下几种,按优先级排序:
- 在 AiServices 构建器上直接设置 InputGuardrail 实现类或实例
- 在单个 AI Service 方法上使用 @InputGuardrails 注解
- 在 AI Service 类上使用 @InputGuardrails 注解
无论如何声明,输入 Guardrails 都会按列表顺序依次执行。
使用 AiServices 构建器
在 AiServices 构建器中直接设置的 Guardrails 拥有最高优先级。如果在其他位置也声明了 Guardrails,将以构建器上的配置为准。
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.inputGuardrailClasses(FirstInputGuardrail.class, SecondInputGuardrail.class)
.build();
第一种方式是传递实现 InputGuardrail 的类,框架会通过反射动态创建这些类的实例。
在单个 AI Service 方法上使用注解
@InputGuardrails 注解可以放在单个 AI Service 方法上,优先级次于构建器配置。
public interface Assistant {
@InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.create(Assistant.class, chatModel);
在此示例中,只有 chat 方法有 Guardrails:
- FirstInputGuardrail 会先执行
- 只有它成功,LLM 才会被调用
- 如果 FirstInputGuardrail 没有返回 fatal,SecondInputGuardrail 才会执行
- 两个 Guardrail 都可以重写用户消息
- 如果第一个 Guardrail 重写了用户消息,第二个 Guardrail 将收到修改后的消息
doSomethingElse 方法没有 Guardrails。
在 AI Service 类上使用注解
如果在 AI Service 类上添加 @InputGuardrails 注解,优先级最低。
@InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.create(Assistant.class, chatModel);
在此示例中,chat 和 doSomethingElse 方法都会应用 Guardrails:
- FirstInputGuardrail 先执行
- 只有它成功,才会调用 LLM
- SecondInputGuardrail 仅在第一个没有返回 fatal 时执行
- 两个 Guardrail 都可能重写用户消息
- 如果第一个 Guardrail 修改了消息,第二个会接收修改后的消息
9.3、输出 Guardrails
输出 Guardrails 是在 LLM 生成结果后执行的函数。如果输出 Guardrail 失败,可以支持更高级的操作,例如 重试 或 重新提示,以改善响应。它们在所有其他操作(包括函数/工具调用)之后执行。
实现输出 Guardrails
实现输出 Guardrails 需要实现 OutputGuardrail 接口。该接口提供两种 validate 方法,必须至少实现其中一种:
OutputGuardrailResult validate(AiMessage responseFromLLM);
OutputGuardrailResult validate(OutputGuardrailRequest params);
第一种适用于简单的 Guardrail,或只需访问生成的 AiMessage。
第二种适用于需要更多上下文信息的复杂 Guardrail,例如完整的聊天响应、聊天记录、用户消息模板或传递给模板的变量。详细信息请参见 OutputGuardrailRequest。
可以做的示例:
- 验证输出格式是否正确(如符合指定 JSON 模式)
- 检测 LLM 幻觉
- 验证 LLM 响应中包含必要信息
声明输出 Guardrails
声明方式与输入 Guardrails 类似,优先级顺序如下:
- 在 AiServices 构建器上直接设置 OutputGuardrail 实现类或实例
- 在单个 AI Service 方法上使用 @OutputGuardrails 注解
- 在 AI Service 类上使用 @OutputGuardrails 注解
无论如何声明,输出 Guardrails 都会按列表顺序依次执行
使用 AiServices 构建器
在 AiServices 构建器中直接设置的 Guardrails 优先级最高。
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.outputGuardrailClasses(FirstOutputGuardrail.class, SecondOutputGuardrail.class)
//.outputGuardrails(new FirstOutputGuardrail(), new SecondOutputGuardrail())
.build();
对单个 AI Service 方法的注解
在单个 AI Service 方法上使用的 @OutputGuardrails 注解 具有次高优先级。
public interface Assistant {
@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.create(Assistant.class, chatModel);
在这个示例中,只有 chat 方法应用了输出护栏(guardrails)。
- 对于 chat 方法,FirstOutputGuardrail 首先执行。
- 仅当其验证成功时,结果才会返回给调用方。如果 FirstOutputGuardrail 的结果不是 fatal、fatal with retry 或 fatal with reprompt,才会执行 SecondOutputGuardrail。
- SecondOutputGuardrail 将接收 FirstOutputGuardrail 的输出。
- 如果 SecondOutputGuardrail 在重试或重新提示后成功,则 FirstOutputGuardrail 和 SecondOutputGuardrail 都会重新执行。
注解在 AI Service 类上
在 AI Service 类上使用 @OutputGuardrails 注解 具有最低优先级。
@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}
var assistant = AiServices.create(Assistant.class, chatModel);
在这个示例中,chat 和 doSomethingElse 方法都应用了输出护栏。
- 上一个示例类似,FirstOutputGuardrail 首先执行。
- 只有在验证成功后,结果才返回给调用方。如果 FirstOutputGuardrail 结果不是 fatal、fatal with retry 或 fatal with reprompt,才会执行 SecondOutputGuardrail。
- SecondOutputGuardrail 将接收 FirstOutputGuardrail 的输出。
- 如果 SecondOutputGuardrail 在重试或重新提示后成功,则两个护栏都会重新执行。
配置
输出护栏可以提供以下附加配置
public interface MethodLevelAssistant {
@OutputGuardrails(
value = { FirstOutputGuardrail.class, SecondOutputGuardrail.class },
maxRetries = 10
)
String chat(String question);
}
var assistant = AiServices.create(MethodLevelAssistant.class, chatModel);
// 或
public interface Assistant {
String chat(String message);
}
var outputGuardrailsConfig = OutputGuardrailsConfig.builder()
.maxRetries(10)
.build();
var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.outputGuardrailsConfig(outputGuardrailsConfig)
.outputGuardrailClasss(FirstOutputGuardrail.class, SecondOutputGuardrail.class)
.build();
流式响应中的输出护栏
输出护栏也可用于具有流式响应的操作:
public interface StreamingAssistant {
@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
TokenStream streamingChat(String message);
}
在这种情况下,输出护栏会在整个流完成后执行,更准确地说是在调用 TokenStream.onCompleteResponse 时。onPartialResponse 会被缓冲,并在护栏验证成功后重放。
如果在链中的 retry 或 reprompt 最终成功,则整个链会 同步 重新执行。每个护栏会按原始顺序逐一重新执行。完成后,结果会传递给 TokenStream.onCompleteResponse
内置的输出护栏
LangChain4j 提供了一些常见用例的输出护栏实现:
| 护栏类 | 描述 |
|---|---|
| JsonExtractorOutputGuardrail | 一个输出护栏,用于检查响应是否能成功从 JSON 反序列化为特定类型对象。 - 使用 Jackson ObjectMapper 尝试反序列化对象。 - 如果响应无法反序列化为预期对象类型,则会重新提示 LLM。 - 可直接使用,也可通过扩展和自定义(有多个 protected 方法可重写以定制行为)。 |
更多推荐



所有评论(0)