使用 Quarkus 和 Google Gemini 保护 Agentic AI
简介
Quarkus LangChain4j
项目提供了 Quarkus 和 LangChain4j 之间一流的集成,帮助开发人员创建生成式和 Agentic AI 代理。
特别是 Agentic AI,它使用复杂的推理和迭代规划来自主解决复杂的多步骤问题,目前正受到开发人员的广泛关注。我们鼓励您阅读《使用 Quarkus 构建 Agentic AI - 第一部分》和《使用 Quarkus 构建 Agentic AI - 第二部分》博客文章,了解使用Quarkus LangChain4j 创建 Agentic AI 代理的技术和模式。
在本文中,我们将介绍 Gemini 个人助理
,这是一个安全的 Agentic AI 代理,可以帮助当前登录的用户分析日程安排、更新和创建新事件,并关注用户 Google 日历中的事件冲突。
集成式 AI 安全
需要注意的是,要让能够处理用户数据的 AI 代理投入生产,它必须是安全的。
但创建安全的 AI 代理涉及到什么?
AI 安全是一个复杂的主题,将在 Quarkus LangChain4j 文档中深入介绍。 Quarkus LangChain4j 已提供重要的 Guardrails AI 安全功能,支持数据验证和提示匿名化。
对于本文而言,重要的一点是,AI 安全是您应用程序安全架构的组成部分。
只有经过身份验证的用户才能访问敏感的应用程序数据。无论是否涉及 AI,都是如此。允许处理用户数据的 Agentic AI 代理只能由经过身份验证的用户访问,以便 AI 能够处理用户身份。
设计集成的应用程序和 AI 安全架构对于创建安全、强大、生产质量的 Agentic AI 代理至关重要。
API 密钥与访问令牌
使用 API 密钥是访问 LLM 的最简单选项

当 AI 服务为组织内的许多用户提供服务时,API 密钥必须是组织范围的,以便组织内的每个人都能让其数据由 LLM 进行分析。API 密钥是必须妥善保管的长期令牌。
如果您的应用程序与使用 OpenId Connect 提供商(例如托管 LLM 的 Google)登录的用户合作,那么安全地管理具有较长生命周期的 API 密钥是一项安全挑战,而这项挑战可以通过利用 Google 身份验证期间获得的有时限访问令牌来避免。此外,组织范围的 API 密钥不能用于代表当前登录的用户访问 Google 服务。

如您所见,使用访问令牌访问 Gemini 看起来与使用 API 密钥类似。区别在于,访问令牌由当前用户身份验证范围界定,有时限,并且可以用于访问需要访问令牌的任何 Google 服务。
使用访问令牌完美契合集成安全概念:用户使用 OpenId Connect 授权码流登录您的应用程序,应用程序将在此流程中获得的访问令牌传播到 Google 服务,以便代表已认证用户访问它们,而 Google Gemini 服务只是这些服务之一。用户注销,会话以及相应的 ID 和访问令牌将被删除。
安全上的一个额外好处是,在身份验证完成后,Google 会要求当前用户授权您的应用程序代表用户使用生成式 API。
因此,我们将使用访问令牌。
Quarkus LangChain4j 支持两个 Gemini 模型提供商:AI Gemini 和 Vertex AI Gemini。 AI Gemini 可以接受 API 密钥和访问令牌,而 Vertex AI Gemini 需要访问令牌。
目前,我们将使用 Vertex AI Gemini,但我们计划很快切换到 AI Gemini,因为它配置更简单。
Gemini 个人助理
好的,让我们来创建我们的安全 Gemini 个人助理。
架构
Gemini 个人助理是一款仅对通过 Google 身份验证的用户可用的 WebSockets 聊天机器人。

登录应用程序的用户可以访问个人助理机器人,并要求它帮助分析和管理未来几天或几周的日程安排。
个人助理能够以用户的名字问好,并代表用户访问 Google 日历 API,告知日程安排,添加新事件,修改事件并就冲突发出警告。
我们将重点介绍关键依赖项、配置属性和代码片段。
您可以在此处找到完整的项目源代码。
Maven 依赖项
添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId> (1)
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId> (2)
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-token-propagation</artifactId> (3)
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-oidc-model-auth-provider</artifactId> (4)
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-vertex-ai-gemini</artifactId> (5)
</dependency>
1 | quarkus-websockets-next 是支持聊天机器人 WebSockets 所必需的。 |
2 | quarkus-oidc 是保护使用 AI 服务的 Quarkus 端点所必需的。 |
3 | quarkus-rest-client-oidc-token-propagation 支持将访问令牌传播到 Google Calendar 等 Google 服务。 |
4 | quarkus-langchain4j-oidc-model-auth-provider 支持将访问令牌传播到 Vertex AI Gemini 等远程模型提供商。 |
5 | quarkus-langchain4j-vertex-ai-gemini 引入了 Vertex AI Gemini 模型提供商扩展。 |
配置
接下来,我们进行配置
# Google OpenId Connect configuration:
quarkus.oidc.provider=google (1)
quarkus.oidc.client-id=${GOOGLE_CLIENT_ID} (2)
quarkus.oidc.credentials.secret=${GOOGLE_CLIENT_SECRET} (2)
quarkus.oidc.authentication.extra-params.scope=https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/calendar (3)
# Gemini configuration
quarkus.langchain4j.vertexai.gemini.chat-model.model-id=gemini-2.0-flash (4)
quarkus.langchain4j.vertexai.gemini.location=europe-west1
quarkus.langchain4j.vertexai.gemini.project-id=${GOOGLE_PROJECT_ID} (5)
quarkus.langchain4j.vertexai.gemini.log-requests=true
quarkus.langchain4j.vertexai.gemini.log-responses=true
quarkus.rest-client.google-calendar-api.url=https://www.googleapis.com/calendar/v3 (6)
1 | 需要 Google 身份验证。 |
2 | 请遵循Quarkus OIDC 文档中描述的步骤在 Google Cloud 中注册 Quarkus 应用程序,使用生成的应用程序客户端 ID 和密钥,并记下 Google 项目 ID。 |
3 | 请求在用户身份验证后签发的访问令牌具有访问 Gemini 提供的生成式 API 和 Calendar API 的权限,以支持 AI 服务工具。届时将要求用户授权已注册的应用程序代表用户访问这些 API。 |
4 | 配置 Gemini 模型 ID。请注意,未配置 API 密钥:quarkus-langchain4j-oidc-model-auth-provider 依赖项将确保将当前 Google 用户的访问令牌传播到 Google Gemini。 |
5 | 指定 Google 项目 ID。 |
6 | 为 Google Calendar 工具配置 Calendar API 基 URL。 |
实现
现在我们来创建 AI 服务
package org.acme.gemini;
import org.acme.gemini.PersonalAssistantResource.PersonalAssistantTools;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import jakarta.enterprise.context.SessionScoped;
@RegisterAiService(tools = { PersonalAssistantTools.class, GoogleCalendarClient.class })
@SessionScoped
public interface PersonalAssistantService {
@SystemMessage(""" (1)
You are a personal assistant.
Your tasks are:
- Provide the currently logged-in user with an information about the scheduled events after the {{timeMin}} but before the {{timeMax}} date and time.
- Get the list of the available calendars and ask the user which calendar the user would like to check; remember the user's choice.
- Use the calendar id field as a calendarId value in all the tool operations with the chosen calendar but show only calendar summary to the user.
- Help the user to schedule other events during this period, advise about any event conflicts.
- Let the user know which calendar contains a given event.
- Be polite but do not hesitate to be informal sometimes to make the user smile.
The event is represented as a JSON object and has the following fields:
summary is the event summary
description is the event description
location is the event location
start is the event start date and time JSON object
end is the event end date and time JSON object
id is the eventId that can be used for accessing, deleting or modifying this event.
Both start and end JSON objects have the following fields:
date date with the first 4 digits representing a year, next 2 ones - a month, and the last 2 ones - a day, for example, 2025-03-21.
dateTime RFC3339 date and time timestamp with mandatory time zone offset, for example, 2011-06-03T10:00:00-07:00, 2011-06-03T10:00:00Z.
timeZone time zone.
The date and time is represented as a RFC3339 timestamp with mandatory time zone offset, for example, 2011-06-03T10:00:00-07:00, 2011-06-03T10:30:30Z.
In the timestamp such as 2011-06-03T10:00:00Z, the year is 2011, the month - 06 (June), the day - 3rd day of the month, the hour is '10', the minutes - '30', and the seconds - '30'.
'Z' indicates a GMT time zone.
To calculate an event duration, deduct the event's start date and time from its end date and time.
Typically, calculating a difference in hours and minutes is enough for most events.
The calendar is represented as a JSON object and has the following fields:
id is the calendarId
summary is the calendar summary
description is the calendar description
location is the calendar location
timeZone is the calendar time zone
primary is the calendar primary boolean status
""")
String assist(@UserMessage String question, String timeMin, String timeMax); (2)
}
1 | 系统提示是此 AI 服务最重要的功能。它需要大量调整才能充分发挥 Gemini 的潜力。例如,请注意 logged-in user (登录用户)文本与下面其中一个工具描述匹配,以便 Gemini 获取与当前登录用户相关的信息。提供 Gemini 可能需要使用的 API 的精确信息非常重要。请在您使用 Gemini 个人助理进行实验时尝试进一步调整。 |
2 | 请求 Gemini 在提供的时界内评估有关日程安排的问题。 |
Gemini 个人助理依赖两个工具来获取用户特定的登录信息
package org.acme.gemini;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
@Authenticated (1)
public class PersonalAssistantTools {
@Inject
@IdToken
JsonWebToken identity; (2)
@Tool("Returns the first name and the family name of the logged-in user.")
public String getLoggedInUserName() { (3)
return identity.getName();
}
@Tool("Returns email address of the logged-in user.")
public String getEmailAddressOfLoggedInUser() { (4)
return identity.getClaim(Claims.email);
}
}
1 | 只有在已通过身份验证的用户启动 Gemini 查询时,才能访问工具。 |
2 | 使用 Google 身份验证授权码流身份验证期间获得的 ID 令牌作为用户身份表示。 |
3 | 使用当前用户身份获取用户的全名,以便 Gemini 向用户问好。 |
4 | 返回当前登录用户的电子邮件地址。例如,Gemini 可以使用此工具查找描述与用户电子邮件地址匹配的主要日历。 |
它还使用 Google Calendar REST 客户端工具来处理用户关于日程安排的请求。
package org.acme.gemini;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestQuery;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.oidc.token.propagation.AccessToken;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@RegisterRestClient(configKey = "google-calendar-api")
@AccessToken (1)
@Path("/")
public interface GoogleCalendarClient {
@GET
@Path("/users/me/calendarList")
@Produces(MediaType.APPLICATION_JSON)
@Tool("Get calendars list")
Calendars getCalendars(); (2)
@GET
@Path("/calendars/{calendarId}/events")
@Produces(MediaType.APPLICATION_JSON)
@Tool("Get events")
Events getEvents(@PathParam("calendarId") String calendarId, @RestQuery("timeMin") String timeMin, @RestQuery("timeMax") String timeMax); (3)
@PUT
@Path("/calendars/{calendarId}/events/{eventId}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tool("Update or move event")
Events updateEvent(@PathParam("calendarId") String calendarId, @PathParam("eventId") String eventId, Event event); (4)
// Other methods are omitted for brewity
public static record Calendars(List<Calendar> items) { (5)
}
public static record Calendar(String id, String summary, String description, String location, String timeZone, boolean primary) {
}
public static record Events(List<Event> items) { (5)
}
public static record Event(String summary, String description, String location, Start start, End end, String id) {
}
public static record Start(String date, ZonedDateTime dateTime, String timeZone) {
}
public static record End(String date, ZonedDateTime dateTime, String timeZone) {
}
}
1 | 使用 Google 身份验证授权码流身份验证期间获得的访问令牌来代表用户访问 Calendar API。仅通过一个注解即可实现令牌传播,这由 quarkus-rest-client-oidc-token-propagation 支持。 |
2 | 用于获取日历列表的 REST 客户端方法工具 |
3 | 用于获取事件列表的 REST 客户端方法工具 |
4 | 用于更新事件的 REST 客户端方法工具 |
5 | 工具参数类型,这些类型也在系统提示中进行了描述。 |
下一个任务是确保 Gemini 个人助理仅对已通过身份验证的用户可用。
package org.acme.gemini;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@WebSocket(path = "/assistant")
@Authenticated (1)
public class PersonalAssistantResource {
private static final Logger log = Logger.getLogger(PersonalAssistantResource.class);
PersonalAssistantService assistant;
public PersonalAssistantResource(PersonalAssistantService assistant) {
this.assistant = assistant;
}
@Inject
SecurityIdentity identity;
@OnOpen (2)
public String onOpen() {
return "Hello, " + identity.getPrincipal().getName() + ", I'm your Personal Assistant, how can I help you?";
}
@OnTextMessage (3)
public String onMessage(String question) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
ZonedDateTime minDateTime = Instant.now().atZone(ZoneId.of("GMT"));
String timeMin = minDateTime.format(formatter);
ZonedDateTime maxDateTime = minDateTime.plusDays(30);
String timeMax = maxDateTime.format(formatter);
return assistant.assist(question, timeMin, timeMax);
}
}
1 | 要求只有在用户已通过身份验证且安全身份已绑定到连接的情况下,WebSocket 升级才能成功。 |
2 | 向用户问好;此时 Gemini 个人助理尚未忙碌。 |
3 | 要求 Gemini 个人助理在提供的时界内处理用户关于日程安排的查询。 |
Google 登录、注销和用户交互支持
请查看完整的 Gemini 个人助理 项目源代码,了解 Google 登录、注销和其他用户交互是如何管理的。我们在此不详述,因为其实现方式与 Gemini 个人助理的工作没有直接关系。
试用
现在是时候看看 Gemini 个人助理真正能做什么了。
在开发模式下启动应用程序。
mvn quarkus:dev
然后访问 https://:8080

登录 Google 后,您将收到问候,并可以选择使用 Gemini 个人助理。

选择个人助理图标后,您将收到个人助理的问候。让我们问它一些关于日程安排的事情。

个人助理已在工具的帮助下成功获取了用户名和用户日历列表。
让我们要求它检查主日历。

我要求它使用我的电子邮件地址的那个,助理通过提供我电子邮件地址的工具找到了它。有时,个人助理可以仅凭系统提示识别出哪个日历是主日历,因为它告知日历的 primary
字段设置为 true
是主日历。但有时它需要提示。
现在它提供了日程更新。

实际上,碰巧我的朋友刚打电话,要求将我们预定的午餐推迟一小时,让我们要求 Gemini 个人助理来完成。

我可以确认我的主日历已成功更新,事件已重新安排到晚一小时。我们将在本文稍后介绍如何处理具有副作用的工具。
让我们问一个关于另一个日历上事件的问题。

个人助理正在学习,因此可能需要一些帮助才能找到正确的日历。

现在它很乐意提供更新。

它还主动提出帮助将其添加到(另一个)日历。假设“是”,但我担心它可能会与其他主日历上的事件冲突。

Gemini 个人助理向我保证,不,没有冲突。

我们可以与友好的 Gemini 个人助理继续对话。
Gemini 个人助理有多 Agentic?
因此,Gemini 个人助理帮助我们查询日程安排、修改事件和检查冲突。
您可能会问,这真的是 Agentic AI 吗,Agentic AI 被期望使用复杂的推理和迭代规划来自主解决复杂的多步骤问题?
就日历而言,如果您日程繁忙,来自多个日历的事件,那么拥有一个可以帮助您管理日程的 AI 代理就是 Agentic AI。
在此演示中,我们仅提出了问题并得到了答案。
但是,不难想象一个应用程序在后台运行(自主)日历检查,并在新事件添加到任何一个日历时向登录用户广播消息。该代理可以检查本地数据库或其他 Google 服务(如 GMail)中的用户特定数据,相应地通知用户,帮助用户对来自多个来源的数据同时做出反应。
Gemini 个人助理已准备好为组织内尽可能多的用户提供帮助的基础模块已就绪:它可以访问用户身份。它可以使用用户特定数据做到什么程度,天空才是极限。
安全注意事项
我们已经处理了几个重要的安全注意事项。
显而易见的考虑是,您不希望允许未经身份验证的访问 LLM,而 LLM 处理敏感数据。这是常识,并非 AI 安全领域特有。
更棘手的问题是如何防止 LLM 在调用具有副作用的工具时出错。例如,如果您查看允许修改事件的 Google Calendar REST API 客户端工具,您会注意到 calendarId
参数 - 代理从日历列表中找到特定日历的 ID。用户如何免受事件被移动到错误日历的影响?
Quarkus LangChain4j 团队正在研究各种选项,例如 Guardrails 和工具的幻觉策略。
您也可以让您的工具在实际调用之前进行检查。例如,与其拥有一个可以更新日历的声明式 REST 客户端工具,不如拥有一个注入 Google Calendar REST 客户端的 Tool bean,并强制仅当更新的是 PersonalAssistant
日历时才允许日历更新事件调用。
就实际的聊天机器人实现而言,确保安全的 WebSockets HTTP 升级至关重要,但这因 JavaScript WebSockets API 不强制执行同源要求而变得复杂。尽管如此,使用自定义票证系统等技术,并结合使用安全的 wss:
方案,可以帮助最大限度地降低风险。有关更多详细信息,请参阅 Quarkus LangChain4j 存储库中的 secure-sql-chatbot
演示。
请注意,使用 WebSockets 不是实现个人助理的先决条件。您可以使用 JAX-RS、Qute、SSE 替代。