今天,我们来看看AiService服务类的整个调度过程,看看它做了些什么?
那么快点开始吧
Assistant assistant1 = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemoryProvider(memoryId -> chatMemory)
.contentRetriever(xxxx)
...
...
.build();
通常AiService服务类需要AiServices来进行builder
,那么首先到builder里面去打个断点
1、构建AIServiceContext上下文
进来以后看见这是个创建AI上下文的方法,如果你需要自己实现上下文管理就需要实现AiServiceContextFactory接口,没有的话就使用默认的AiServiceContext。那么这个上下文有哪些东西呢?其实就是
.chatModel(chatModel) .chatMemoryProvider(memoryId -> chatMemory) .contentRetriever(xxxx) ... ...
builder后面的这些所有的属性
chatmodel对象(必须)
自定义的Assistant接口(存入aiServiceClass ,必须)
returnType返回类型
toolService,工具服务类
retrievalAugmentor检索服务类
chatMemoryService store存储类
......
创建完AiServiceContext上下文之后开始构建AiService的实现类
2.构建AiService实现类
可以看到这里跟上面说的实现AiServiceContextFactory机制一样的需要你去实现AiServicesFactory接口。
但是需要注意他这里面两个接口都使用了spi(Service Provider Interface)机制去加载一个实现类的
SPI机制链接:Java SPI 机制详解 | JavaGuide
可以看到需要在META-INF/services去定义需要实现类的接口路径的文件,然后需要在这个文件里面写入实现类的包路径
总的来说,想要实现Spi机制需要干两件事:
服务实现:第三方实现接口
配置文件:META-INF/services/[接口全类名],声明实现类
我们接着说回正文,如果没有自定的实现类就去实例化一个DefaultAiServices来作为AiService并把刚刚初始化的Context传给他。
3.代理实现类具体流程
接着去build里面使用java的动态代理去实现一个实现类,这里也是我认为,langchain4j里面最巧妙核心的地方了吧。通过自定义的Assistant接口,可以自行的去安排返回数据类型,以及方法名称,还有参数注解等等。langchain通过代理实现符合规定的接口,实现类对话接口定义与服务组装的解耦。为Assistant接口的方法都创建一个实现类
3.1实现默认Object方法
接着我们来看这个实现里面主要做了那些事情。
如果是默认的方法走默认处理,每个实现类都需要实现Object的三个基础方法
3.2验证method参数和组装上下文
如果这里的方法是来自ChatMemoryAccess的那么就走handleChatMemoryAccess方法,这个里面大概是实现了删除和获取聊天记忆的实现。
然后会去验证整个方法里面的参数是否合理
组装了invocationContext上下文来管理和存放所有需要使用到的工具,rag,store,chatmodel等等内容
这里进入了核心实现invoke(method, args, invocationContext);
3.3获取ChatMemory对象
首先拿到chatMemory为下文拿去历史消息做准备
3.4组装消息
组装系统消息和用户消息
3.5向订阅推送消息和上下文
向所有实现了AiServiceListener接口的对象推送DefaultAiServiceStartedEvent对象。
3.6RAG检索增强消息
下一步,如果存在RAG(检索模型)的话,就会调用模型检索向量数据,然后生成一个新的userMessage(包含了检索到的消息)
3.7经过安全护栏机制
RAG重新检索完消息它使用了“安全护栏“,护栏机制对输入的内容进行安全检索。那么这个guardrail是从哪里来的呢?关键就在于Context调用的guardrailService()这个方法
进入这个方法的核心里面,它会去找所有带有标注@InputGuardrails注解的对话方法,然后获取注解里面实现了InputGuardrail接口的实现类。也就是说这个安全护栏机制是需要自己去实现InputGuardrail接口来实现的,同样还有OutPutGuardrails;
3.8判断模型输出格式
这里根据 Assistant(用户自定义接口)判断返回类型是否是流式消息,以及模型是否支持Json格式输出
3.9内容审核
如果有在Assistant(用户自定义接口)的对话方法上使用Moderate注解以及添加了moderationModel的话就会进入到这里面,去请求审核模型并把它返回的规范内容作为输入内容。
3.10组装工具上下文
这里面进行了工具组装,包括了
请求时发送给大模型的ToolSpecification对象还有执行工具时的ToolExecutor,这样一般情况下langchain就会在一次对话后,如果有工具返回,就从toolExecutors这个map里面根据工具的名称拿到执行对象。
3.11流式消息
如果刚刚判断返回类型为TokenStream或者是Flux就会走这个路径
3.12构建对话请求体
如果模型支持 JSON 格式 + 已经生成了 JSON Schema → 就给 AI 开启 “强制 JSON 返回模式”,然后把这个格式设置塞进请求参数里。
接着构建chatRequest对象
3.13发送请求到对话
这里构造了chatExecutor,然后执行execute()方法时最终将整理的请求发送给了大模型,前面一大堆的铺垫总算是为了这一刻。
然后把请求体和请求结果推送给注册事件
3.14工具执行
AI 回答-------->判断是否需要调用工具(查天气、查数据库、查接口)-------->如果需要 → 自动调用工具------->把结果再丢给 AI-------->AI 整理后再回答-------->直到不需要调用工具为止
最后如果如果ai调用了立即返回工具且对话方法使用Result来包装的话就会立即返回,不再进行后续逻辑,这种一般是同步执行某个接口要么只需要返回执行状态。
3.15再进安全护栏
跟输入护栏一样方法里面如果有OutputGuardrails注解和自己实现了OutputGuardrail接口就可以对Ai输出的文本进行处理,整合成最终的输出结果。
3.16组装返回类型并返回
最后根据对话方法的returnTyoe并将response解析为对应的对象,然后判断是否需要包装为Result,否则直接返回。
4.AiService 调用完整流程
用户调用
↓
参数验证
↓
组装上下文
↓
获取 ChatMemory
↓
组装消息
↓
RAG 检索增强
↓
输入护栏检查
↓
发送请求到 LLM
↓
工具执行循环
↓
输出护栏检查
↓
解析返回结果
↓
返回给用户
总结
本文详细分析了 LangChain4j AiService 的完整调度流程,主要包括:
- 上下文构建:通过 Builder 模式初始化 AiServiceContext
- 动态代理:使用 Java 代理机制实现接口解耦
- 消息处理:组装系统消息、用户消息和历史记忆
- RAG 增强:检索向量数据增强用户输入
- 护栏机制:输入/输出双向安全检查
- 工具调用:自动识别并执行工具
- 结果解析:根据返回类型自动转换结果
下篇预告
在下一篇文章中,我们将继续探讨:
- 流式消息的实现原理
- ChatExecutor 的包装机制
- 不同返回类型的解析策略
- 自定义 TypeFactory 的实现
敬请期待!
## 参考资料
忆 4. RAG 增强:检索向量数据增强用户输入
- 护栏机制:输入/输出双向安全检查
- 工具调用:自动识别并执行工具
- 结果解析:根据返回类型自动转换结果
下篇预告
在下一篇文章中,我们将继续探讨:
- 流式消息的实现原理
- ChatExecutor 的包装机制
- 不同返回类型的解析策略
- 自定义 TypeFactory 的实现
敬请期待!
## 参考资料