步骤 10 - 可观测性和容错
在上一步中,我们介绍了防护栏,允许我们使用防护栏来缓解提示注入。虽然防止提示注入肯定很重要,但确保在出现问题时,我们能够快速识别问题并优雅地处理故障也很重要。
为此,我们将通过在我们的应用程序中实现日志记录、跟踪和指标来为我们的 LLM 交互添加可观测性。此外,我们将通过实施重试和回退机制来为我们的 LLM 交互添加容错能力。
可观测性
可观测性的 3 个主要支柱是日志记录、跟踪和指标。在以下部分中,我们将探讨如何实现可观测性,以深入了解我们应用程序的行为,特别是关于其与 LLM 的交互。使用 Quarkus 实现这些功能是一个简单的过程,可以轻松集成到您现有的 Quarkus 应用程序中。
此步骤的最终代码位于 step-10
目录中。
日志记录
为了确保我们的 LLM 交互得到监控和记录,我们需要在我们的应用程序中实现日志记录。这将允许我们跟踪与模型的每次交互的输入和输出,以及发生的任何错误或异常。您可能已经注意到,在本实验中,您实际上已经在之前的步骤中记录了与模型的交互。
继续并检查 src/main/resources
目录中的 application.properties 文件。您将看到 2 个属性(如果您没有看到它们,请继续添加它们)
quarkus.langchain4j.openai.chat-model.log-requests=true
quarkus.langchain4j.openai.chat-model.log-responses=true
log-requests
属性启用对模型的所有请求的日志记录,而 log-responses
属性启用对从模型收到的所有响应的日志记录。这些日志提供了有关 LLM 如何与您的应用程序交互以及出现的任何问题的宝贵见解。如果您还没有使用 ./mvnw quarkus:dev
启动 Quarkus Dev Mode,请继续启动它,然后转到 https://:8080 并在屏幕右下角打开聊天界面。向机器人发送一条指令,然后返回您的控制台。您将看到一系列与 LLM 之间的请求/响应,其中包含大量信息,例如 url、headers,以及正文中的模型调用、消息、温度、tokens 等。
09:50:54 INFO traceId=cb938581635e7777244c57bc4ece04db, parentId=d7888051b1772651, spanId=f92dfd63091f4efa, sampled=true [io.qu.la.op.co.OpenAiRestApi$OpenAiClientLogger] (vert.x-eventloop-thread-5) Request:
- method: POST
- url: https://api.openai.com/v1/chat/completions
- headers: [Accept: application/json], [Authorization: Be...1B], [Content-Type: application/json], [User-Agent: langchain4j-openai], [content-length: 2335]
- body: {
"model" : "gpt-4o",
"messages" : [ {
"role" : "system",
"content" : "You are a customer support agent of a car rental company 'Miles of Smiles'.\nYou are friendly, polite and concise.\nIf the question is unrelated to car rental, you should politely redirect the customer to the right department.\n\nToday is 2025-01-10.\n"
}, {
"role" : "user",
"content" : "what services are available?\nPlease, only use the following information:\n- United States of America.\n2. The Services\n- from within any country in the world, of applications, websites, content, products, and services\n- liable for any modification, suspension or discontinuation of the Services.\n"
} ],
"temperature" : 1.0,
"top_p" : 1.0,
"max_tokens" : 1000,
"presence_penalty" : 0.0,
"frequency_penalty" : 0.0,
"tools" : [ {
"type" : "function",
"function" : {
"name" : "cancelBooking",
"description" : "Cancel a booking",
"parameters" : {
"type" : "object",
"properties" : {
"bookingId" : {
"type" : "integer"
},
"customerFirstName" : {
"type" : "string"
},
"customerLastName" : {
"type" : "string"
}
},
"required" : [ "bookingId", "customerFirstName", "customerLastName" ]
}
}
}, {
"type" : "function",
"function" : {
"name" : "listBookingsForCustomer",
"description" : "List booking for a customer",
"parameters" : {
"type" : "object",
"properties" : {
"customerName" : {
"type" : "string"
},
"customerSurname" : {
"type" : "string"
}
},
"required" : [ "customerName", "customerSurname" ]
}
}
}, {
"type" : "function",
"function" : {
"name" : "getBookingDetails",
"description" : "Get booking details",
"parameters" : {
"type" : "object",
"properties" : {
"bookingId" : {
"type" : "integer"
},
"customerFirstName" : {
"type" : "string"
},
"customerLastName" : {
"type" : "string"
}
},
"required" : [ "bookingId", "customerFirstName", "customerLastName" ]
}
}
} ]
}
09:50:55 INFO traceId=cb938581635e7777244c57bc4ece04db, parentId=d7888051b1772651, spanId=f92dfd63091f4efa, sampled=true [io.qu.la.op.co.OpenAiRestApi$OpenAiClientLogger] (vert.x-eventloop-thread-5) Response:
- status code: 200
- headers: [Date: Fri, 10 Jan 2025 08:50:55 GMT], [Content-Type: application/json], [Transfer-Encoding: chunked], [Connection: keep-alive], [access-control-expose-headers: X-Request-ID], [openai-organization: user-qsgtnhp4stba6axsc0rzfyum], [openai-processing-ms: 1531], [openai-version: 2020-10-01], [x-ratelimit-limit-requests: 500], [x-ratelimit-limit-tokens: 30000], [x-ratelimit-remaining-requests: 499], [x-ratelimit-remaining-tokens: 28713], [x-ratelimit-reset-requests: 120ms], [x-ratelimit-reset-tokens: 2.572s], [x-request-id: req_33432b46a09d2e3e4918cdc085747825], [strict-transport-security: max-age=31536000; includeSubDomains; preload], [CF-Cache-Status: DYNAMIC], [Set-Cookie: __...ne], [X-Content-Type-Options: nosniff], [Set-Cookie: _c...ne], [Server: cloudflare], [CF-RAY: 8ffb6c11687983dd-BRU], [alt-svc: h3=":443"; ma=86400]
- body: {
"id": "chatcmpl-Ao51CDrgIYFN25RIK4GJAdvQjp5tY",
"object": "chat.completion",
"created": 1736499054,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Our car rental services, under the name \"Miles of Smiles,\" are available for users within the United States. These services include a variety of applications, websites, content, products, and services related to car rental. Please feel free to ask any specific questions about our car rental options!",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 228,
"completion_tokens": 60,
"total_tokens": 288,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_b7d65f1a5b"
}
默认情况下,日志会输出到控制台。在生产系统中,控制台输出通常会被转发到日志聚合服务,以便可以以更高级的方式集中和搜索日志。我们稍后会介绍一个日志收集系统,但首先让我们看看如何从我们的应用程序收集指标。
指标
通过使用指标和冷冰冰的数字来深入了解我们应用程序的性能和行为也很重要。使用这些指标,我们可以创建有意义的图形、仪表板和警报。
目前在 Quarkus 中收集指标的首选方法是使用 micrometer 项目。您可以通过将 quarkus-micrometer
扩展添加到 pom.xml 来添加指标收集。然后,您需要添加一个特定的收集器扩展来相应地格式化指标。在下面的示例中,我们包含了用于通用 OpenTelemetry 的 quarkus-micrometer-registry-otlp
扩展。此扩展也导入了 quarkus-micrometer,因此无需显式指定它。将以下依赖项添加到您的代码中
<!-- Export metrics for OpenTelemetry compatible collectors -->
<dependency>
<groupId>io.quarkiverse.micrometer.registry</groupId>
<artifactId>quarkus-micrometer-registry-otlp</artifactId>
<version>3.2.4</version>
</dependency>
默认情况下,Quarkus 会默认为您收集各种有用的指标,例如 CPU 和内存使用情况、垃圾回收统计信息等。LangChain4j 扩展还将添加有关 LLM 交互的有用指标。例如
# HELP langchain4j_aiservices_seconds_max
# TYPE langchain4j_aiservices_seconds_max gauge
langchain4j_aiservices_seconds_max{aiservice="CustomerSupportAgent",method="chat",} 0.0
langchain4j_aiservices_seconds_max{aiservice="PromptInjectionDetectionService",method="isInjection",} 0.0
# HELP langchain4j_aiservices_seconds
# TYPE langchain4j_aiservices_seconds summary
langchain4j_aiservices_seconds_count{aiservice="CustomerSupportAgent",method="chat",} 1.0
langchain4j_aiservices_seconds_sum{aiservice="CustomerSupportAgent",method="chat",} 2.485171837
langchain4j_aiservices_seconds_count{aiservice="PromptInjectionDetectionService",method="isInjection",} 1.0
langchain4j_aiservices_seconds_sum{aiservice="PromptInjectionDetectionService",method="isInjection",} 0.775163834
您还可以通过添加自己的自定义指标来自定义指标收集。您可以在 Quarkus Micrometer 文档中找到有关如何使用 Quarkus Micrometer 的更多信息。
跟踪
跟踪是可观测性的另一个重要方面。它涉及跟踪请求和响应在您的应用程序中的流动,并识别可能指示问题的任何异常或不一致之处。它还允许您识别瓶颈和应用程序中需要改进的领域。例如,您可以跟踪调用模型所花费的时间,并识别任何花费时间超过预期的请求。然后,您可以将这些跟踪追溯到您代码中的特定日志条目或行。
跟踪还可以帮助您检测应用程序行为随时间推移发生的异常情况,例如流量突然增加或响应时间下降。
Quarkus 实现了 OpenTelemetry 项目以实现跟踪功能,允许您从 LangChain4j 应用程序收集和分析跟踪数据。您可以使用 OpenTelemetry API 将跟踪发送到跟踪服务,例如 Jaeger、Zipkin 或 Tempo,然后可以使用这些服务进行监控和调试。
要将 OpenTelemetry(以及跟踪)添加到您的应用程序,您需要将 opentelemetry 扩展添加到您的 pom.xml 文件中。您可以选择添加 opentelemetry-jdbc 依赖项以从 JDBC 查询收集跟踪数据。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-jdbc</artifactId>
</dependency>
通过将这些扩展添加到您的应用程序,Quarkus 在设置和配置 OpenTelemetry API 方面为您做了很多繁重的工作,包括将跟踪发送到跟踪服务。Quarkus LangChain4j 会自动与 OpenTelemetry 扩展集成,以收集有关您与 LLM 交互的跟踪信息。
您可以通过例如设置跟踪服务的端点和标头,以及跟踪的格式来配置 opentelemetry 跟踪功能
# quarkus.otel.exporter.otlp.traces.endpoint=https://:4317
quarkus.otel.exporter.otlp.traces.headers=authorization=Bearer my_secret
quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId}, parentId=%X{parentId}, spanId=%X{spanId}, sampled=%X{sampled} [%c{2.}] (%t) %s%e%n
# enable tracing db requests
quarkus.datasource.jdbc.telemetry=true
您可能会注意到在上面的示例中,跟踪端点已被注释掉。如果您有正在运行的跟踪服务,您可以相应地设置端点。在生产环境中,您可能会使用环境变量或 ConfigMap 或类似的东西来覆盖此值。但在我们的例子中,我们将使用 Quarkus Dev Service 来捕获和可视化跟踪,以及日志和指标。
用于在本地机器上可视化收集的可观测性数据的工具
在生产环境中,您的组织可能已经设置了工具来收集可观测性数据,但 Quarkus 提供了几种方法来可视化和搜索本地机器上收集的数据。
Quarkus Otel LGTM Dev Service
Quarkus 提供了一个实验性的新 Dev Service,以帮助在一个中心位置可视化您的所有 OpenTelemetry 可观测性数据。它基于开源 LGTM 堆栈,LGTM 代表 Loki(日志聚合)、Grafana(图形工具)、Tempo(跟踪聚合)和 Prometheus(指标聚合)。通过添加 quarkus-observability-devservices-gtm
扩展,这组工具将自动(或者我们可以说是“神奇地”?)在其各自的容器中启动并连接到您的应用程序的可观测性端点。
在您的 pom.xml
中添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-observability-devservices-lgtm</artifactId>
<scope>provided</scope>
</dependency>
在 application.properties 中,让我们启用 OpenTelemetry 跟踪和日志收集功能
quarkus.otel.logs.enabled=true
quarkus.otel.traces.enabled=true
# Disable LTGM in test mode
%test.quarkus.observability.enabled=false
现在刷新浏览器中的聊天机器人应用程序并再次与机器人交互以生成一些新的可观测性数据。请注意,应用程序可能需要更长的时间才能启动,因为 Quarkus 正在后台启动 LGTM Dev Services 容器。
在生成一些数据后,让我们去 Grafana 中浏览这些数据。Dev Service 公开一个随机端口。找到它的最简单方法是转到 Quarkus Dev UI (https://:8080/q/dev-ui) 并单击“Dev Services”菜单项。
找到 grafana.endpoint
并在另一个浏览器选项卡中打开 url。如果需要,使用 admin/admin 登录。
首先让我们浏览 dev service 创建的提供的自定义指标仪表板。转到左侧菜单中的“Dashboards”。您会注意到 2 个仪表板,一个用于 OTLP,一个用于 Prometheus。还记得我们如何同时添加 Micrometer OpenTelemetry 和 Prometheus 注册表扩展吗?它们都反映在这里。随意浏览仪表板。如果您在图中没有看到太多数据,您可能需要在屏幕右上角选择一个较短的时间跨度和/或创建更多的聊天请求。
您还可以通过转到 Explore > Metrics 来找到所有指标的聚合(包括 LangChain4j 相关的指标)
现在让我们浏览查询功能以查找特定数据。单击 Explore
菜单项。将打开一个交互式查询窗口。在“Outline”旁边,您将看到 Prometheus 在下拉列表中被选中。选择 gen_ai_client_estimated_cost_total
。然后,在标签过滤器中,选择 currency
和值 USD
。最后,单击 Run query 按钮以查看结果。您应该看到对模型最新调用的估计成本聚合。这是一个基于典型 ChatGPT 调用成本的实验性功能。
现在让我们看看如何从 Tempo 获取我们的跟踪。在同一个查询窗口中,在“Outline”旁边,选择 Tempo
而不是 Prometheus。然后,单击查询类型旁边的 Search
。您将在下面看到一个表格,其中列出了最新的跟踪 ID 和它们相关的服务。
单击任何一个跟踪以打开有关它们的更多详细信息。您将看到一个 span 列表,这些 span 表示请求和响应的不同部分,并且可能还包括数据库查询,具体取决于您选择的跟踪。继续浏览这些跟踪和 span 一段时间,以熟悉启用 OpenTelemetry 扩展时自动跟踪的数据。确保还单击 Node Graph
以查看请求的流程。
最后,展开一个(或多个)span 元素。您将看到有关代码中特定调用的详细信息,并且您还会看到一个按钮“Logs for this span”。这允许您查看与该特定 span 相关的日志。如果您没有看到任何日志,请尝试另一个 span。
容错
由于在我们的应用程序中引入了可观测性,我们现在可以很好地了解我们的应用程序,如果出现问题,我们(希望)应该能够相当快地查明问题。
虽然如果出现问题,我们可以检索有关我们应用程序的大量详细信息,但这很棒,但用户仍然会受到影响,并且可能会收到一条难看的错误消息。
在接下来的部分中,我们将为我们应用程序的 LLM 调用添加容错能力,以便在出现问题时,我们能够优雅地处理它。
最终,调用 LLM 与进行传统的 REST 调用没有什么不同。如果您熟悉 MicroProfile,您可能知道它有一个关于如何实现容错的规范。Quarkus 使用 quarkus-smallrye-fault-tolerance
扩展实现此功能。继续并将其添加到您的 pom.xml 中
<!-- Fault Tolerance -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
Microprofile 容错规范定义了 3 个主要的容错功能
- Timeout - 允许您设置 LLM 调用失败前的最长时间。
- Fallback - 允许您在出现错误时调用回退方法
- Retry - 允许您设置如果出现错误应重试调用的次数,以及调用之间的延迟
现在我们所要做的就是使用以下注释来注释我们的 dev.langchain4j.quarkus.workshop.CustomerSupportAgent
AI 服务
package dev.langchain4j.quarkus.workshop;
import jakarta.enterprise.context.SessionScoped;
import org.eclipse.microprofile.faulttolerance.ExecutionContext;
import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.faulttolerance.FallbackHandler;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import dev.langchain4j.service.SystemMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.ToolBox;
import io.quarkiverse.langchain4j.guardrails.InputGuardrails;
@SessionScoped
@RegisterAiService
public interface CustomerSupportAgent {
@SystemMessage("""
You are a customer support agent of a car rental company 'Miles of Smiles'.
You are friendly, polite and concise.
If the question is unrelated to car rental, you should politely redirect the customer to the right department.
Today is {current_date}.
""")
@InputGuardrails(PromptInjectionGuard.class)
@ToolBox(BookingRepository.class)
@Timeout(5000)
@Retry(maxRetries = 3, delay = 100)
@Fallback(CustomerSupportAgentFallback.class)
String chat(String userMessage);
public static class CustomerSupportAgentFallback implements FallbackHandler<String> {
private static final String EMPTY_RESPONSE = "Failed to get a response from the AI Model. Are you sure it's up and running, and configured correctly?";
@Override
public String handle(ExecutionContext context) {
return EMPTY_RESPONSE;
}
}
}
就这样。要测试实现的容错能力,我们需要“破坏”我们的应用程序。您可以关闭您的 wifi,将 @Timeout 值设置为非常低的值(例如 10),或者您可以将推理服务器 url 设置为无法解析的内容,例如
创建混乱的首选方式由您决定 :)。完成此操作后,运行您的应用程序并使用不同的输入对其进行测试。您应该看到当 LLM 未能在指定超时时间内生成响应时,将调用回退方法。这演示了我们应用程序的容错能力。
结论
在此步骤中,我们引入了可观测性以检索有关应用程序状态、性能和行为的有用信息。这对于生产级应用程序来说至关重要,无论它是否使用 AI。我们还了解到 Quarkus LangChain4j 提供了相对简单的方法,不仅可以向应用程序添加可观测性,还可以查阅它产生的数据。
我们还介绍了混沌工程技术来模拟应用程序中的故障并观察我们的回退机制如何响应。这是确保我们的应用程序可以优雅地处理意外情况的关键步骤。