作者:来自 Elastic Andy James
本系列将带你深入了解我们如何在客户支持中使用生成式人工智能。加入我们,实时分享我们的历程,本篇文章重点介绍支持助理的可观察性。
本博客系列揭示了我们的现场工程团队如何使用 Elastic stack 和生成式 AI 开发出一个可爱而有效的客户支持聊天机器人。如果你错过了本系列的其他文章,请务必查看:
- 第 1 部分:构建我们的概念验证
- 第 2 部分:构建知识库
- 第 3 部分:为人类设计聊天机器人的聊天界面
- 第 4 部分:调整 RAG 搜索的相关性
- 发布博客:用于客户支持的 GenAI - 探索 Elastic 支持助手
我发现可观察性引人注目的地方在于,它在顺境和逆境中都有用。当一切顺利时,可观察性会为你提供指标,以展示你的工作所产生的影响。当你的系统出现问题时,可观察性会帮助你找到根本原因并尽快稳定下来。这就是我们注意到一个错误导致我们从服务器一遍又一遍地加载相同数据的原因。我们在 APM 数据中看到,我们的一个端点的吞吐量远远超过每分钟 100 笔交易,这对于我们的用户群规模来说是不合理的。当我们看到吞吐量降低到更合理的 1 TPM 时,我们可以确认修复已完成。这也是我知道我们在发布后 21 小时完成了第 100 次聊天的原因(你们喜欢看到它)。这篇文章将讨论成功发布的可观察性需求,然后讨论聊天机器人用例(例如我们的支持助理)的一些独特可观察性考虑因素。
关键可观察性组件
你需要三个主要部分。按顺序依次为状态仪表板、警报和里程碑仪表板。我们将进一步探讨这意味着什么,以及我为支持助手的启动所做的安排。这三个组件都需要一个要求:数据。因此,在我们深入研究如何处理数据以获得可操作的见解之前,让我们先看看我们如何为支持助手(以及通常为支持门户)收集这些数据。
可观测性数据收集
我们有一个专门用于监控目的的 Elastic Cloud 集群。我将要讨论的所有可观测性数据都存储和分析在这里。它与我们的生产和暂存数据 Elastic 集群是分开的,我们在那里管理应用程序数据(例如知识文章、爬取的文档)。
我们在为 API 提供服务的 Node 应用程序中运行 Elastic 的 Node APM 客户端,并运行 Filebeat 来捕获日志。我们有一个用于 console.log 和 console.error 调用的包装函数,它将 APM 跟踪信息附加到每条消息的末尾,这允许 Elastic APM 将日志数据与交易数据关联起来。有关此功能的更多详细信息,请参阅 APM 的日志页面。你将在那里找到的关键信息是 apm.currentTraceIds 的存在是为了提供你所需要的。从那里开始没有什么复杂的,只需要一点字符串格式。复制我们的。一份小礼物;来自我的团队,送给你们。
import apm from 'elastic-apm-node';/*** @param message The message to log.* @param args The arguments to forward to `console.log`.*/
export const log = (message: string, ...args: any[]): void => {let msg = message;const traceIds = apm.currentTraceIds.toString();if (traceIds) {msg = `${message} [${traceIds}]`;}console.log(msg, ...args);
};export const logError = (message: string, ...args: any[]): void => {let msg = message;const traceIds = apm.currentTraceIds.toString();if (traceIds) {msg = `${message} [${traceIds}]`;}console.error(msg, ...args);
};
我们使用 Elastic Synthetics 监控功能来检查应用程序和关键上游服务(例如 Salesforce、我们的数据集群)的活跃度。目前我们使用 HTTP 监控类型,但我们正在研究未来如何使用 Journey 监控。基本 HTTP 监控的优点在于,你只需配置要 ping 的 URL、要 ping 的频率以及从哪里 ping。在选择要检查的位置时,我们知道对于应用程序本身,我们想从世界各地的位置进行检查,并且由于有些调用直接从用户的浏览器调用到数据集群,因此我们还会从所有可用位置进行检查。但是,对于我们的 Salesforce 依赖项,我们知道我们只从我们的服务器连接到它,因此我们只从托管支持门户应用程序的位置对其进行监控。
我们还从应用程序数据 Elastic 集群发送 Stack 监控数据,并让 Azure OpenAI 集成通过运行在 GCP 虚拟机上的 Elastic Agent 从该服务发送日志和指标。
设置 Elastic APM
开始使用 Elastic APM 非常简单。让我们以支持门户的 API 服务的 APM 配置为例。
import apm from 'elastic-apm-node';/*** Initialize APM globally for this entire process.** Note: This must be the first thing done before configuring Express.** @param _apm The global `APM` instance.*/
export const initApm = (_apm: typeof apm = apm) => {// Avoid it being started multiple times (e.g., tests)if (!_apm.isStarted()) {// Other configuration related to APM is loaded in the environment_apm.start({serviceName: 'support-portal-api',});}
};
让我们来分析一下其中的一些情况。首先,我们允许自己在测试场景中注入模拟 APM 实例,并且还添加了一层保护以防止多次调用启动函数。接下来,你将看到我们正在使用环境变量来支持大多数配置选项。APM 将自动读取 ELASTIC_APM_ENVIRONMENT 环境变量来填充环境设置,ELASTIC_APM_SERVER_URL 用于 serverUrl 设置,ELASTIC_APM_SECRET_TOKEN 用于 secretToken 设置。
你可以在此处阅读完整的配置选项列表,其中包括可用于配置许多选项的环境变量的名称。我想强调设置 environment 的价值。它使我能够轻松区分来自不同环境的流量。即使你没有运行暂存环境(你确实应该这样做),在本地开发时收集 APM 也会派上用场,并且你将希望能够在大多数情况下单独查看生产和开发数据。能够通过 service.environment 进行过滤非常方便。
如果你在 Elastic Cloud 中运行,则可以按照以下步骤获取用于配置的 serverUrl 和 secretToken 的值。访问你的 Kibana 实例,然后导航到 Integrations 页面。找到 APM 集成。滚动到 APM 服务器部分以找到 APM 代理部分,你将看到包含连接信息的 Configure the agent 子部分。
状态仪表板
数据的实用性取决于你从中提取含义的能力,这就是仪表板的作用所在。使用 Elastic Cloud,默认运行 Kibana 和 Elasticsearch,因此我们的堆栈中已经有一个很棒的可视化层。那么我们想看到什么呢?使用情况、延迟、错误和容量是相当常见的数据类别,但即使在这些类别中,你的特定需求也会决定你想要为仪表板制作哪些特定的可视化效果。让我们来看看我为支持助手发布制作的状态仪表板,以用作示例。
你可能会惊讶地发现左上角的主要空间是文本的主机。Kibana 有一个 markdown 可视化,你可以使用它来添加说明,或者在我的情况下,添加一堆方便的链接,指向我们可能想要跟进仪表板中看到的内容的其他地方。顶行的其余部分显示一些摘要统计信息,例如仪表板时间范围内的聊天完成总数、唯一用户和错误。下一组可视化是时间序列图,用于检查延迟和随时间变化的使用情况。对于我们的支持助手用例,我们特别关注 RAG 搜索和聊天完成的延迟。对于使用情况,我感兴趣的是聊天完成的数量、唯一用户、回访用户以及助手用户与所有支持门户用户的比较。最后两个我留在了图片的折叠下方,因为它们包含我们决定不分享的详细信息。
我喜欢使用仪表板保存默认时间范围。它将其他用户锚定到默认视图,这在他们第一次加载仪表板时通常很有用。我将开始时间戳固定为发布上线时的大致时间,将结束时间戳固定为现在(now)。在发布窗口期间,能够看到该功能的整个生命周期真是太棒了。在某个时候,将存储的时间更新为最近的窗口(例如 “过去 30 天”)可能更有意义。
额外挑战:你能说出我们何时将模型从 GPT-4 升级到更强大的 GPT-4o 吗?
我在状态仪表板上增加了其他区域,重点关注使用最多或遇到最多错误的用户,然后还有一些随时间变化的 HTTP 状态和错误的时间序列视图。你的状态仪表板将有所不同,也应该如此。这种类型的仪表板也具有随着时间的推移而发展的趋势(在我起草这篇文章时,我的仪表板发生了明显的变化)。它的目的是成为回答有关你正在观察的功能或系统的一系列最重要的问题的答案。你将发现重要的新问题,这可能会为仪表板添加一些新的可视化效果。有时,问题变得不那么相关,或者你开始意识到它没有你预期的那么有意义,因此你可以将其删除或重新排列在其他项目之下。在我们离开此仪表板之前,让我们先绕道看看我们的聊天完成情况的 APM 跟踪,然后看看我如何使用 ES|QL 创建回访用户可视化。
APM 跟踪
如果你从未见过 Elastic APM 跟踪,那么该图像中可能有很多非常引人注目的事情。标头显示请求 URL、响应状态、持续时间、使用的浏览器。然后,当我们进入瀑布图时,我们可以看到涉及哪些服务和一些自定义跨度的细分。APM 了解此跟踪经过了我们的前端服务器(绿色跨度)和我们的 API 服务(蓝色跨度)。
自定义跨度是监控特定任务性能的好方法。在这种情况下,我们正在流式传输聊天完成,我想知道第一个生成的 token 到达需要多长时间,以及整个完成过程需要多长时间。这些跨度的平均持续时间在仪表板上绘制。这是聊天完成端点的精简片段,重点介绍自定义跨度的开始和结束。
import agent, { Span } from 'elastic-apm-node';const FIRST_GENERATION_SPAN_NAME ='Elastic Support Assistant First Generation';
const FULL_GENERATION_SPAN_NAME ='Elastic Support Assistant Full Generation';async (req, res, data): Promise<void> => { let firstGenerationSpan: Span | undefined;let fullGenerationSpan: Span | undefined;if (agent.isStarted()) {firstGenerationSpan =agent.startSpan(FIRST_GENERATION_SPAN_NAME) ?? undefined;fullGenerationSpan =agent.startSpan(FULL_GENERATION_SPAN_NAME) ?? undefined;}try {for await (const event of chatStrategy.chat({ ...data })) {if (event.type === ChatbotStreamEventType.GENERATION) {if ((firstGenerationSpan && !firstGenerationSpan?.outcome) ||firstGenerationSpan?.outcome === 'unknown') {firstGenerationSpan.outcome = 'success';}firstGenerationSpan?.end();}writeEvent(res, requestId, event);}} catch (err) {// Error handling} finally {if ((fullGenerationSpan && !fullGenerationSpan?.outcome) ||fullGenerationSpan?.outcome === 'unknown') {fullGenerationSpan.outcome = 'success';}fullGenerationSpan?.end();res.end();}
};
使用 ES|QL 可视化回访用户
当我第一次尝试可视化回访用户时,我最初的目标是得到类似于每日堆积条形图的东西,其中条形的总大小应该是当天的唯一用户数量,细分是净新用户与回访用户。这里的挑战是,计算这个需要重叠窗口(overlapping windows),这与 Kibana 可视化中的直方图工作方式不兼容。一位同事提到 ES|QL 可能有一些工具可以提供帮助。虽然我最终没有得到我最初描述的可视化,但我能够使用它来帮助我处理数据集,我可以在其中生成用户电子邮件和请求日期的唯一组合,然后可以计算每个用户访问了多少个独特的天数。从那里,我可以可视化访问量的分布。这是支持我的图表的 ES|QL 查询。
FROM traces-apm*,logs-apm*,metrics-apm*
| WHERE `service.environment`=="production"
AND `transaction.name`=="POST /api/ai/chat/completions"
AND user.email IS NOT NULL
AND user.email != "some-test-user-to-filter-out@domain.com"
| KEEP @timestamp, user.email
| EVAL date = DATE_TRUNC(1 day, @timestamp)
| EVAL userDate = CONCAT(DATE_FORMAT("yyyy-MM-dd",date),":",user.email)
| STATS userVisits = COUNT_DISTINCT(date) BY user.email
| STATS vistitCountDistro = COUNT(userVisits) BY userVisits
| SORT userVisits ASC
警报
有了状态仪表板,你就可以快速了解系统当前和一段时间内的状态。可视化中显示的指标本质上是你关心的指标,但你不能也不想整天盯着仪表板(也许发布后的头几天的兴奋让我一直盯着仪表板,但这绝对不是一个可持续的策略)。那么,让我们来谈谈警报如何让我们摆脱仪表板的束缚,同时让我们睡个好觉,因为我们知道如果出现问题,我们会收到通知,而不是在下次追逐盯着那个漂亮的仪表板的甜蜜感觉时才发现。
Elastic Observability 的一个非常方便的地方是,你在制作仪表板可视化时已经弄清楚了制作警报规则所需的细节。你应用的任何过滤器以及你可视化的特定索引中的特定字段都是配置警报规则所需的主要配置详细信息。你实际上是采用可视化定义的指标并添加阈值来决定何时触发警报。
我应该如何选择阈值?
对于某些警报,它可能是关于尝试实现团队定义的某种服务质量。在很多情况下,你希望使用可视化来建立某种预期基线,以便你可以根据你愿意容忍的与观察到的基线的偏差程度来选择阈值。
现在是时候提到你可能计划推迟集成 APM 直到开发过程结束,但我鼓励你尽早这样做。对于初学者来说,这不是一个很大的提升(正如我上面向你展示的那样)。尽早这样做的一大好处是,在开发过程中你可以捕获 APM 信息。通过捕获你可以在预期错误期间调查的详细信息,它可能有助于你在开发过程中调试某些内容,然后它还会捕获示例数据。这对于验证你的可视化(对于涉及计数的指标)以及为延迟等指标类别建立基线值都很有用。
我应该如何收到警报?
这实际上取决于警报的紧急程度。就此而言,有些警报可能需要配置不同阈值的多个警报。例如,在警告级别,你可能只会发送电子邮件,但也可能存在发送标记你团队的 Slack 消息的严重级别。我配置的仅用于电子邮件的非严重警报的示例,与我们接下来将讨论的里程碑仪表板一起使用。通过临时配置警报输出格式以使其立即触发,可以测试警报输出的格式,这是一个好主意。
确定哪些警报以被动方式通知(例如电子邮件)而不是要求立即关注(例如寻呼)的最佳做法是问自己 “是否有一套明确的步骤来响应此警报以解决它?” 如果没有明确的途径来调查或解决警报,那么寻呼某人不会增加太多价值,而只会增加噪音。坚持这一点可能很难,如果你刚刚意识到自己收到一堆无法采取行动的警报,也许可以看看能否想出一种不那么苛刻的方式来显示它们。你不希望的是,无意中训练你的团队忽略警报,因为它们通常无法采取行动。
里程碑仪表板
里程碑仪表板可能不需要与状态仪表板分开,可以将其安排为状态仪表板的一个区域,但我喜欢有单独的空间专注于突出成就。
我最想通过里程碑来强调的两个指标是独立用户和聊天完成量。我发现水平项目符号可视化适合显示具有设定范围和可选目标的仪表。我认为所有时间、过去 7 天和过去 30 天的时间窗口是标准的,但看起来很有趣,所以我并排设置了两列,每行代表不同的时间窗口。底行是按天汇总的条形图,这是一种很好的方式来查看随时间推移的增长情况。
支持助手的特殊注意事项
我们讨论了观察你启动的任何新功能或系统的基本知识,但每个项目都会有一些独特的可观察性机会,因此本博文的其余部分将讨论我们团队在开发支持助手时遇到的一些机会。如果你也在构建聊天机器人体验,其中一些可能直接适用于你的用例,但即使你的项目非常不同,这些想法也可能激发一些额外的可观察性选项和策略。
注意:我即将分享的大多数代码示例来自我们 API 层中的聊天完成请求处理程序,我们向 LLM 发送请求并将响应流回客户端。我将向你展示同一个处理程序几次,但经过编辑,仅包含与当时描述的功能相关的行。
首次生成超时
你可能还记得,在本系列的用户体验 (UX) 文章中,我们选择使用 LLM 的流式响应,以避免在生成完成之前用户需要等待全部内容显示。为了让我们的助手体验更具响应性,我们还设置了一个 10 秒的超时来获取生成文本的首个片段。了解这类错误的趋势对于判断我们的服务是否可靠或是否过载至关重要。我们注意到,在服务启动时,这种超时更容易在同时有较多用户的情况下发生。有时,这甚至会导致重试请求,超出我们为 LLM 服务配置的容量,从而给用户带来更多错误。
APM 代理在我们的服务器上运行,而首次生成的超时设置在运行于用户浏览器中的客户端代码中,因此我开始尝试监听服务器上的事件,以检测客户端发送中止信号的情况,以便可以通过 captureError
向 APM 发送错误报告。但我发现,服务器并未能察觉客户端已中止请求。我尝试监听请求、监听套接字,查阅了网络资源,最终得出的结论是,至少对我们的应用栈而言,服务器没有实用或内置的方法来识别客户端超时。
为了解决这个问题,我将超时和 AbortController
从客户端代码移到了直接与 LLM 通信的 API 层中。现在,当我们遇到超时时,服务器端可以发送错误给 APM,并提早关闭连接,从而能正常地将此情况传播给客户端。
以下是我们的请求处理程序的示例,展示了与首次生成超时相关的部分:
import agent from 'elastic-apm-node';async (req, res, data): Promise<void> => {const generationAbortController = new AbortController();const firstGenerationTimeoutId = setTimeout(() => {generationAbortController.abort();const errorMsg = '[LLM] First generation timed out';// This is the wrapper function I mentioned// that adds trace data to the logged messagelogError(errorMsg);agent.captureError(errorMsg, {// We associate the error with a custom APM Spanparent: streamingSpan,});res.end();}, FIRST_GENERATION_TIMEOUT_MS);// Write the Server-Side Events (SSE) response.try {for await (const event of chatStrategy.chat({...data,abortSignal: generationAbortController.signal,})) {clearTimeout(firstGenerationTimeoutId);writeEvent(res, requestId, event);}} catch (err) {// Clear the timeout on error or else we will also log a timeout// error incorrectly when the timeout expires.clearTimeout(firstGenerationTimeoutId);// Handle errors} finally {res.end();}
};
不幸的是,仅仅关闭来自服务器的连接就会导致客户端出现意外行为。由于没有发回正确的错误信号或任何生成的响应文本,客户端代码没有运行我们退出加载状态的代码部分。为了解决这个问题,我更新了服务器端超时,在对响应调用 end() 之前添加了一个额外的步骤。流式响应通过向客户端发送一系列与生成相关的事件来工作。有 4 种类型:Started、Generation、End 和 Error。通过在关闭连接之前添加一个额外的步骤来发送错误事件,客户端代码能够更新 UI 状态以反映错误。
让我们再次查看包含该内容的处理程序:
async (req, res, data): Promise<void> => {const generationAbortController = new AbortController();const firstGenerationTimeoutId = setTimeout(() => {generationAbortController.abort();const errorMsg = '[LLM] First generation timed out';// Send an error event to the clientwriteEvent(res, requestId, createErrorEvent(errorMsg));res.end();}, FIRST_GENERATION_TIMEOUT_MS);// Write the Server-Side Events (SSE) response.// From here on is the same.
};
首次超时错误是一种非常普通的错误,并且始终记录相同的消息。对于其他类型的错误,有许多不同的故障可能导致到达错误处理程序。为此,我们传入一个参数化的消息对象,以便 APM 将同一错误处理捕获的所有错误分组在一起,尽管错误消息会根据实际发生的错误而有所不同。我们有错误消息、错误代码以及我们使用的 LLM 的参数。
agent.captureError({message: '[LLM] Error generating response with model [%s]: %d %s',params: [model, e?.code, e?.message],
});
拒绝请求
支持助手的目标是提供帮助,但我们希望避免处理两大类输入。第一类是与获得 Elastic 产品技术支持无关的问题。我们认为,既然我们支付了 LLM 服务的账单,我们就不希望人们使用支持助手来起草电子邮件或写歌词,这是非常公平的。我们避免的第二大类是我们知道它无法很好地回答的话题。这方面的主要例子是账单问题。我们知道支持助手无法访问帮助准确回答账单问题所需的数据,当然对于像账单这样的话题,不准确的答案比没有答案更糟糕(销售团队、财务团队和律师都松了一口气 😉)。我们的方法是在用户输入之前向 prompt 添加说明,而不是单独调用第三方服务。随着我们强化需求的不断发展,我们可能会考虑添加一项服务,或者至少将决定是否尝试响应的任务拆分为专门用于做出该决定的单独 LLM 请求。
标准化响应
我不会分享很多关于我们的提示强化方法以及我们在提示中设置了哪些规则的细节,因为这篇博客是关于可观察性的,而且我觉得提示工程的现状还不能让你在不帮助恶意用户绕过它的情况下分享你的提示。话虽如此,我确实想谈谈我在制定 prompt 策略以避免上述两类问题时注意到的事情。
我成功地让它礼貌地拒绝回答某些问题,但它的回答方式并不一致。而且回复的质量也参差不齐。为了解决这个问题,我开始在提示中加入一个标准化的响应,用于拒绝请求。有了预定义的响应,聊天机器人在拒绝请求时可靠地使用了标准响应。预定义的响应存储为其自己的变量,然后在构建要发送到 LLM 的有效负载时使用该变量。让我们来看看为什么这很有用。
监控被拒绝的请求
回到可观察性,通过对被拒绝的请求进行预定义响应,它为我创造了一个机会来检查来自 LLM 的响应,并将其与包含标准化拒绝消息的变量进行比较。当我看到匹配时,我会使用 captureError 来记录它。密切关注被拒绝的请求对我们来说很重要,因为我们希望确保这些拒绝是出于正确的原因。拒绝次数激增可能表明某个用户或一组用户正在试图绕过我们的限制,以将聊天保持在 Elastic 产品技术支持的主题上。
import agent from 'elastic-apm-node';async (req, res, data): Promise<void> => {// Setup custom APM span to track streaming response// Keep track of the generated tokenslet generatedTokens: string[] = [];try {for await (const event of chatStrategy.chat({...data,abortSignal: generationAbortController.signal,})) {clearTimeout(firstGenerationTimeoutId);// Generation events build the array with the response tokensif (event.type === ChatbotStreamEventType.GENERATION) {generatedTokens.push((event.payload as StreamGenerationEvent).content);}writeEvent(res, requestId, event);}} catch (err) {// Handle errors} finally {// Check for a match betweem the generated tokens// and decline messageif (DECLINED_REQUEST_MESSAGE === generatedTokens.join('')) {captureDeclinedRequest(streamingSpan);}res.end();}
};const captureDeclinedRequest = (streamingSpan?: Span) => {const errorMsg = '[LLM] Request declined';logError(errorMsg);agent.captureError(errorMsg, {parent: streamingSpan,});
};
上面显示的策略将所有标记收集到 string[] 中,然后在响应完成时将其连接起来进行比较。我从一位同事那里听到了一个很好的优化建议。不是在流式传输期间收集标记,而是只需跟踪 DECLINED_REQUEST_MESSAGE 中的索引,然后当每个标记进入时,查看它是否与消息的下一个预期字符匹配。如果是,则继续跟踪,但如果没有匹配,你就知道这不是被拒绝的请求。这样,你就不必消耗额外的内存来缓冲整个响应。我们没有看到性能或内存问题,所以我没有更新我的策略,但这个想法太聪明了,不能不在这里提及。
减轻滥用
与上一节关于拒绝请求的内容密切相关,我们知道这些由 LLM 支持的聊天机器人系统可能成为那些想要免费访问 LLM 服务的人们的目标。因为你必须登录并拥有技术支持订阅(包含在 Elastic Cloud 中)才能访问支持助手,所以对于我们特定的发布来说,这不太令人担心,但我们希望以防万一,也许你的用例没有相同的前期限制。我们缓解滥用的两个方面是降低聊天完成端点的速率限制,以及功能标志系统,该系统具有灵活性,允许我们配置标志以阻止特定用户或组织访问特定功能。
速率限制
我们的应用程序已经对所有端点设置了通用速率限制,但是该速率限制应该非常宽松,只有在出现问题并导致大量垃圾邮件流量时才会触发。要使速率限制在应用于支持助手聊天完成端点时有意义,它必须是一个低得多的限制。同样重要的是,要让限制足够宽松,这样我们就不会惩罚热情的用户。除了我们与客户进行的 beta 测试的使用数据外,我们还为支持工程师提供了面向内部的支持助手版本,以帮助简化他们回答案例的工作流程。这给了我们一个可以作为使用预期的依据。
我查看了上周的数据,发现我们最繁忙的内部用户平均每天发送 10-20 条聊天消息,而顶级用户一天发送了超过 70 条。我还有延迟指标告诉我平均完成时间为 20 秒。如果不打开多个窗口或选项卡,单个用户一个接一个地快速提问,一分钟内发送的聊天消息不会超过 3 条。我们的应用会话在一小时后到期,因此我决定最好将我们的速率限制窗口与该长达一小时的会话窗口对齐。这意味着单个用户使用单个选项卡时,一小时内的理论最大聊天次数是 180 次。团队同意在一小时内限制 20 次聊天完成次数。这相当于我们最繁忙的内部用户在一小时内发送的聊天次数,同时将任何恶意用户的聊天次数限制为理论最大值的约 11%,具体取决于完全完成的延迟。
然后,我配置了一个警报,在聊天完成端点上查找 HTTP 429 响应,状态仪表板中还有一个表格,列出了触发限制的用户、触发次数以及最近一次示例的时间。我很高兴地报告,自发布以来的头几周,我们还没有任何人达到限制。下一节讨论了如果我们确实看到某些人似乎试图滥用该系统时我们为自己提供的应对选项。
禁用标记
在推出支持助手时,我们与一些精心挑选的客户进行了有限的 beta 测试。为了在开发过程中为部分用户启用支持助手,我们设置了一个功能标记系统来启用功能。随着发布的临近,我们意识到我们的功能标记需要进行一些升级。首先,我们希望有默认启用的功能概念(即已完全启动),其次是允许配置标记以阻止对某项功能的访问。这背后的驱动因素是,我们听说一些客户组织可能有兴趣阻止其员工使用支持助手,但我们也认识到,如果我们得出结论认为某个特定用户一直表现不佳,这也可能派上用场,我们可以切断该功能,同时让合适的 Elastic 代表尝试联系并进行对话。
上下文会产生大量有效负载
最后一部分是聊天机器人的特殊考虑,也是可观察性成功案例。在研究我们的状态仪表板时,我们开始看到 HTTP 413 状态代码返回,流量很小,但不可忽略。这意味着我们从浏览器发送的有效负载超过了我们服务器接受的配置大小。然后我们的一位开发人员偶然发现了一个可靠的聊天输入,它重现了它,这样我们就可以确认问题是我们的 RAG 搜索生成的上下文量,加上用户的输入超过了默认限制。我们增加了聊天完成端点接受的有效负载的大小,自从我们发布修复程序以来,我们再也没有看到任何具有 413 响应状态的事务。
值得注意的是,我们扩大接受的有效负载大小的修复实际上更像是一种短期补救措施,而不是长期解决方案。我们计划以更全面的方式解决这个问题,即重构我们编排 RAG 搜索和聊天完成的方式,这样我们就不会将 RAG 结果的全部内容发送回客户端以包含在完成负载中,而是只将 RAG 结果的有限元数据(如 ID 和标题)返回给客户端,然后将其与用户对完成端点的输入一起包含在请求中。完成端点将按 ID 获取搜索结果的内容,并将其与我们的提示和用户的输入相结合,以向 LLM 服务发出请求。
以下是我们为聊天完成端点配置 Express 路由的代码片段。它涉及速率限制、标志和增强的负载大小:
import express, { Express } from 'express';
import rateLimit from 'express-rate-limit';const MAX_CHAT_COMPLETIONS_PER_HOUR = parseInt(process.env.MAX_CHAT_COMPLETIONS_PER_HOUR || '20'
);const aiApis = (app: Express): void => {app.post('/api/ai/chat/completions',rateLimit({windowMs: 60 * 60 * 1000, // 1 hour// Number of requests allowed per IPmax: MAX_CHAT_COMPLETIONS_PER_HOUR,}),// This middleware enforces feature flagsensureHasAiEnabled,// Allow larger payloads for chatbot completionsexpress.json({ limit: '1mb' }), requestWithBodyAndRawResponseHandler);// Declare other AI route configurations
}
结论
理想情况下,可观察性不止一件事。它是多方面的,可以提供多个角度和观点,以创建更完整的理解。它可以并且应该随着时间的推移而发展,以填补空白或带来更深入的理解。
我希望你能从这篇博客中获得的收获是,如何为你的应用程序或功能建立可观察性的框架,Elastic Stack 如何提供实现这些监控目标的完整平台,以及如何将这些内容应用于支持助手的使用场景。遵循这些建议,那么事情就万无一失,你就能顺利推出!
准备好自己尝试一下了吗?开始免费试用。
想要获得 Elastic 认证吗?了解下一次 Elasticsearch 工程师培训何时开始!
原文:GenAI for Customer Support — Part 5: Observability - Search Labs