1. 概述
AI 驱动的应用已成为现实。我们广泛实现了各种 RAG 应用、提示词 API,并使用 LLMs 创建了令人印象深刻的项目。借助 Spring AI,我们能更快、更一致地完成这些任务。
本文将深入探讨 Spring AI Advisors 这一强大功能,它能帮我们处理各种常规任务。
2. 什么是 Spring AI Advisor?
我们将提示词发送给附加了 Advisor 链的聊天模型。在提示词送达前,链中的每个 Advisor 都会执行其 before 操作。类似地,在收到聊天模型响应前,每个 Advisor 也会调用其 after 操作。
3. 聊天记忆 Advisors
Chat Memory Advisors 是一组非常实用的 Advisor 实现。我们可以用这些 Advisors 为聊天提示词提供通信历史,从而提高聊天响应的准确性。
3.1. MessageChatMemoryAdvisor
使用 MessageChatMemoryAdvisor 可以通过 messages 属性为聊天客户端调用提供聊天历史。我们将所有消息保存在 ChatMemory 实现中,并能控制历史记录大小。
下面是一个简单示例:
@SpringBootTest(classes = ChatModel.class)
@EnableAutoConfiguration
@ExtendWith(SpringExtension.class)
public class SpringAILiveTest {
@Autowired
@Qualifier("openAiChatModel")
ChatModel chatModel;
ChatClient chatClient;
@BeforeEach
void setup() {
chatClient = ChatClient.builder(chatModel).build();
}
@Test
void givenMessageChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {
ChatMemory chatMemory = new InMemoryChatMemory();
MessageChatMemoryAdvisor chatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
String responseContent = chatClient.prompt()
.user("将此名称添加到列表并返回所有值:Bob")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob");
responseContent = chatClient.prompt()
.user("将此名称添加到列表并返回所有值:John")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John");
responseContent = chatClient.prompt()
.user("将此名称添加到列表并返回所有值:Anna")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John")
.contains("Anna");
}
}
3.2. PromptChatMemoryAdvisor
⚠️ 原文未提供具体实现细节,此处跳过
3.3. VectorStoreChatMemoryAdvisor
使用 VectorStoreChatMemoryAdvisor 能获得更强大的功能。我们通过向量存储中的 相似性匹配 搜索消息上下文。搜索相关文档时会考虑对话 ID。本例中我们使用稍作修改的 SimpleVectorStore,也可以替换为任何 向量数据库。
首先创建向量存储的 bean:
@Configuration
public class SimpleVectorStoreConfiguration {
@Bean
public VectorStore vectorStore(@Qualifier("openAiEmbeddingModel")EmbeddingModel embeddingModel) {
return new SimpleVectorStore(embeddingModel) {
@Override
public List<Document> doSimilaritySearch(SearchRequest request) {
float[] userQueryEmbedding = embeddingModel.embed(request.query);
return this.store.values()
.stream()
.map(entry -> Pair.of(entry.getId(),
EmbeddingMath.cosineSimilarity(userQueryEmbedding, entry.getEmbedding())))
.filter(s -> s.getSecond() >= request.getSimilarityThreshold())
.sorted(Comparator.comparing(Pair::getSecond))
.limit(request.getTopK())
.map(s -> this.store.get(s.getFirst()))
.toList();
}
};
}
}
这里我们创建了 SimpleVectorStore 的 bean 并重写了 doSimilaritySearch() 方法。默认的 SimpleVectorStore 不支持元数据过滤,我们暂时忽略这点(测试中只有一个对话,完全够用)。
现在测试历史上下文行为:
@Test
void givenVectorStoreChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {
VectorStoreChatMemoryAdvisor chatMemoryAdvisor = new VectorStoreChatMemoryAdvisor(vectorStore);
String responseContent = chatClient.prompt()
.user("从聊天历史中查找猫,添加狮子并返回列表")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("狮子");
responseContent = chatClient.prompt()
.user("从聊天历史中查找猫,添加美洲狮并返回列表")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("狮子")
.contains("美洲狮");
responseContent = chatClient.prompt()
.user("从聊天历史中查找猫,添加豹子并返回列表")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("狮子")
.contains("美洲狮")
.contains("豹子");
}
4. QuestionAnswerAdvisor
⚠️ 原文未提供具体实现细节,此处跳过
5. SafeGuardAdvisor
有时需要防止客户端提示词中出现敏感词。使用 SafeGuardAdvisor 可以轻松实现:指定禁用词列表并添加到提示词的 Advisor 实例中。如果搜索请求包含这些词,请求将被拒绝,并提示重新表述:
// 示例用法(原文未提供代码,此处补充典型用法)
SafeGuardAdvisor safeGuardAdvisor = new SafeGuardAdvisor(
List.of("密码", "敏感信息"),
"检测到敏感词,请重新表述问题"
);
String response = chatClient.prompt()
.user("我的密码是什么?")
.advisors(safeGuardAdvisor)
.call()
.content();
// 实际会返回安全提示而非真实密码
6. 实现自定义 Advisor
当然,我们可以根据需要实现自定义 Advisor。下面创建一个 CustomLoggingAdvisor 记录所有聊天请求和响应:
public class CustomLoggingAdvisor implements CallAroundAdvisor {
private final static Logger logger = LoggerFactory.getLogger(CustomLoggingAdvisor.class);
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
this.observeAfter(advisedResponse);
return advisedResponse;
}
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info(advisedResponse.response()
.getResult()
.getOutput()
.getContent());
}
private AdvisedRequest before(AdvisedRequest advisedRequest) {
logger.info(advisedRequest.userText());
return advisedRequest;
}
@Override
public String getName() {
return "CustomLoggingAdvisor";
}
@Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
这里我们实现了 CallAroundAdvisor 接口,在调用前后添加了日志逻辑。特别注意:我们在 getOrder() 方法中返回了最大整数值,确保此 Advisor 在链中最后执行。
测试新的 Advisor:
@Test
void givenCustomLoggingAdvisor_whenSendPrompt_thenPromptTextAndResponseShouldBeLogged() {
CustomLoggingAdvisor customLoggingAdvisor = new CustomLoggingAdvisor();
String responseContent = chatClient.prompt()
.user("从1数到10")
.advisors(customLoggingAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("1")
.contains("10");
}
执行后查看日志输出:
c.b.s.advisors.CustomLoggingAdvisor : 从1数到10
c.b.s.advisors.CustomLoggingAdvisor : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
✅ 成功记录了提示词和聊天响应!
7. 总结
本文深入探讨了 Spring AI 的 Advisors 功能。通过 Advisors 我们能获得:
- 聊天记忆能力
- 敏感词控制
- 向量存储无缝集成
- 自定义扩展能力
使用 Advisors 能以一致且简单的方式实现这些功能,是构建 AI 应用的利器。建议在实际项目中多尝试不同的 Advisor 组合,踩坑后就能体会到它的强大。