作者:来自 Elastic Cory Mangini
本系列博文揭示了我们的现场工程团队如何使用 Elastic Stack 和生成式 AI 开发出一款可爱又高效的客户支持聊天机器人。如果你错过了本系列的第一篇博文,可以在此处找到。
检索增强生成 (RAG) 优于精细调整模型
作为工程团队,我们知道 Elastic 客户需要信任基于 AI 的生成式支持助手来提供准确且相关的答案。我们最初的概念验证表明,大型语言模型 (LLM) 基础训练不足以满足 Elastic 这样技术深度和广泛的技术需求。我们探索了为支持助手微调我们自己的模型,但出于多种原因,我们最终采用了基于 RAG 的方法。
- 处理非结构化数据更容易:微调需要问答配对,这与我们的数据集不匹配,而且在我们的数据规模下很难做到。
- 实时更新:通过访问最新文档立即整合新信息,确保响应最新且相关。
- 基于角色的访问控制:跨角色的单一用户体验根据允许的访问级别限制特定文档或来源。
- 维护更少:支持中心和支持助手上的搜索共享许多相同的底层基础架构。我们通过相同的工作努力改进了搜索结果和聊天机器人。
将支持助手理解为一个搜索问题
然后我们形成了一个假设,推动了支持助手的技术开发和测试。
提供更简洁、更相关的搜索结果作为 LLM 的背景,将最大限度地减少模型使用不相关信息回答用户问题的机会,从而产生更强烈的积极用户情绪。
为了检验我们团队的假设,我们必须在搜索的背景下重新构建我们对聊天机器人的理解。将支持聊天机器人视为图书管理员。图书管理员可以访问大量书籍(通过搜索),并且天生就了解(LLM)广泛的主题。当被问到问题时,图书管理员可能能够根据自己的知识回答,但可能需要找到适当的书籍来解决有关深层领域知识的问题。
搜索扩展了 “图书管理员” 以前所未有的方式查找书中段落的能力。杜威十进制分类法(Dewey Decimal Classification)实现了可搜索的书籍索引。个人电脑发展成为具有有限文本搜索的更好目录。通过 Elasticsearch + Lucene 的 RAG 不仅可以在整个图书馆的书籍中找到关键段落,还可以为用户综合答案,通常只需不到一分钟。该系统具有无限的可扩展性,因为通过向图书馆添加更多书籍,拥有回答给定问题所需知识的机会更大。
用户输入的措辞、提示和温度等设置(随机度)仍然很重要,但我们发现我们可以使用搜索作为一种理解用户意图的方式,并更好地增强传递给大型语言模型的上下文,以提高用户满意度。
Elastic Support 的知识库
我们为搜索和 Elastic Support Assistant 提取的知识体系取决于三个关键活动:我们的支持工程师撰写的技术支持文章、我们采集的产品文档和博客,以及丰富服务,这增加了我们混合搜索方法中每个文档的搜索相关性。
还需要注意的是,用户问题的答案通常来自多个文档中的特定段落。这是我们选择提供支持助手的重要原因。用户在多个文档中的特定段落中找到答案需要付出巨大的努力。通过提取这些信息并将其发送到大型语言模型,我们既节省了用户的时间,又以易于理解的自然语言返回答案。
技术支持文章
Elastic Support 遵循以知识为中心的服务方法,支持工程师记录他们对案例的解决方案和见解,以便我们的知识库(内部和外部)每天都在增长。这完全在后端的 Elasticsearch 和前端的 EUI Markdown 编辑器控件上运行,是 Elastic Support Assistant 的关键信息来源之一。
我们为 Support Assistant 所做的大部分工作是启用语义搜索,以便我们可以利用 ELSER。我们之前的架构有两种单独的知识文章存储方法。一种是用于面向客户的文章的 Swiftype 实例,另一种是通过 Elastic Appsearch 存储内部支持团队文章。这是我们工程团队的技术债务,因为 Elasticsearch 已经为我们需要的几个功能带来了同等效果。
我们的新架构利用文档级安全性来从单个索引源启用基于角色的访问。根据 Elastic Support Assistant 用户的不同,我们可以检索适合他们使用的文档,作为发送给 OpenAI 的上下文的一部分。这可以根据需要在未来扩展到新角色。
有时,我们还需要为支持团队注释外部文章的信息。为了满足这一需求,我们开发了一个名为 private context 的 EUI 插件。它会在文章中查找以文本块开头的多行 private context 标签,使用正则表达式解析它们以查找 private context 块,然后将它们处理为称为 AST 节点(类型为 privateContext)的特殊事物。
{private-context}
Notes written here only appear to the Support team.
This might also include internal Links (e.g. GitHub or Case number)
{private-context}
这些变化的结果产生了一个索引,我们可以使用该索引与 ELSER 一起进行语义搜索,并根据用户的角色对信息进行细致的访问。
我们执行了多次索引迁移,从而为两种用例生成了单个文档结构。每个文档包含四大类字段。通过将元数据和文章内容存储在同一个 JSON 文档中,我们可以根据需要高效地利用不同的字段。
对于支持助手中的混合搜索方法,我们使用 title 和 summary 字段进行语义搜索,并在更大的 content 字段上使用 BM25。这使支持助手既能快速搜索,又能与我们将作为上下文传递给 OpenAI GPT 的文本高度相关。
提取产品文档、博客和搜索实验室内容
尽管我们的技术支持知识库有超过 2,800 篇文章,但我们知道有些问题这些文章无法解答 Elastic 支持助手的用户问题。例如:What new features would be available if I upgraded from Elastic Cloud 8.11 to 8.14? 不会出现在技术支持文章中,因为这不是一个故障修复问题,也不会出现在 OpenAI 模型中,因为 8.14 已经过了模型训练日期截止日期。
我们选择通过包含更多官方 Elastic 来源(例如所有版本的产品文档、Elastic 博客、搜索/安全/可观察性实验室和 Elastic 入门指南)作为语义搜索实现的来源(类似于此示例)来解决这个问题。通过使用语义搜索在相关时检索这些文档,我们使支持助手能够回答更广泛的问题。
提取过程包括数十万份文档,并处理跨 Elastic 属性的复杂站点地图。我们选择使用名为 Crawlee 的抓取和自动化库来处理保持知识库最新所需的规模和频率。
四个爬虫作业中的每一个都在 Google Cloud Run 上执行。我们之所以选择这样做,是因为作业的超时时间为 24 小时,并且可以在不使用 Cloud Tasks 或 PubSub 的情况下安排它们。我们的需求导致总共有四个作业并行运行,每个作业都有一个可以捕获特定类别文档的基本 URL。在抓取网站时,我们建议从没有重叠内容的基本 URL 开始,以避免提取重复项。这必须与在过高的级别抓取和提取对你的知识存储无用的文档保持平衡。例如,我们抓取 https://elastic.com/blog 和 https://www.elastic.co/search-labs/blog 而不是 elastic.co/,因为我们的目标是技术文档。
即使有了正确的基本 URL,我们也需要考虑 Elastic 产品文档的不同版本(我们的知识库中有 114 个主要/次要版本)。首先,我们为产品页面构建了目录,以便加载和缓存产品的不同版本。我们的技术堆栈是 Typescript 与 Node.js 以及 Elastic 的 EUI 的组合,用于前端组件。
export const fetchTableOfContents = async (currentUrl: string) => {
const tocUrl = currentUrl.replace(/\/current\/.*$/, '/current/toc.html');
const response = await fetch(tocUrl, {
headers: { Authorization: ELASTIC_CO_AUTH_HEADER },
});
if (!response.ok) {
throw new Error(
`Failed to fetch [${tocUrl}] with status [${response.status}]`
);
}
const dom = new JSDOM(await response.text());
return dom.window.document;
};
然后,我们加载产品页面的目录并缓存产品的版本。如果产品版本已缓存,则该函数将不执行任何操作。如果产品版本未缓存,则该函数还会将产品页面的所有版本加入队列,以便它可以抓取产品文档的所有版本。
请求处理程序
由于我们抓取的文档结构可能千差万别,我们为每种文档类型创建了一个请求处理程序。请求处理程序告诉抓取工具将哪个 CSS 解析为文档主体。这为我们存储在 Elasticsearch 中的文档创建了一致性,并捕获了相关的文本。这对于我们的 RAG 方法尤其重要,因为任何填充文本也是可搜索的,并且可能会因我们发送给 LLM 的上下文而错误地返回。
此策略的一个风险是,如果维护网络空间的团队对 css 选择器进行了未经通知的更改,则抓取工具将返回内容的 null 结果。因此,提取是我们使用支持助手的可观察性策略监控的一个领域。本系列后面的第 5 篇博客将介绍这一点。
博客请求处理程序
此示例是我们最直接的请求处理程序。我们指定爬虫程序应查找与提供的参数匹配的 div 元素。该 div 内的任何文本都将被提取为生成的 Elasticsearch 文档的内容。
const body =
document.querySelector('div.title-text-one-column.container')
?.textContent ?? '';
产品文档请求处理程序
在此产品文档示例中,多个 css 选择器包含我们想要提取的文本,为每个选择器提供了一个可能性列表。这些匹配参数中的一个或多个中的文本将包含在生成的文档中。
const body =
// the ',' is an "OR" condition for different variations of the selector
document.querySelector(
'#preamble div.sectionbody, #content div.part, #content div.section, #content div.chapter'
)?.textContent ?? '';
爬虫还允许我们配置和发送授权标头(header),以防止它被拒绝访问所有 Elastic 产品文档版本的页面。由于我们需要预测支持助手的用户可能会询问任何版本的 Elastic,因此捕获足够的文档以解释每个版本中的细微差别至关重要。产品文档确实有一些重复的内容,因为给定的页面可能不会在多个产品版本中更改。我们通过微调搜索查询来处理这个问题,除非用户另有指定,否则默认为最新的产品文档。第四篇博客将详细介绍这一点。
丰富文档来源
Elastic 的整个知识库包含超过 300,000 份文档。这些文档的元数据类型千差万别,甚至根本没有。这就需要我们丰富这些文档,以便搜索能够容纳针对这些文档的更大范围的用户问题。在这种规模下,团队需要使丰富文档的过程自动化、简单化,并且能够填充现有文档并在创建新文档时按需运行。我们选择使用 Elastic 作为向量数据库,并启用 ELSER 来支持我们的语义搜索,并使用生成式人工智能来填补元数据空白。
ELSER
Elastic ELSER(Elastic Learned Sparse Embedding Retrieval)通过将 Elastic 文档转换为增强搜索相关性和准确性的丰富嵌入来丰富 Elastic 文档。这种先进的嵌入机制利用机器学习来理解数据中的上下文关系,超越了传统的基于关键字的搜索方法。这种转换允许更快地检索相关信息,即使是从像我们这样的大型复杂数据集中检索。
ELSER 之所以成为我们团队的明确选择,是因为它易于设置。我们下载并部署了模型,创建了摄取管道并重新索引了我们的数据。结果是丰富的文档。
如何安装和运行支持诊断故障排除实用程序是一篇流行的技术支持文章。ELSER 计算了 title 和 summary 的向量数据库嵌入,因为我们将它们与语义搜索一起使用作为混合搜索方法的一部分。结果存储在 Elastic 文档中作为 ml 字段。
如何安装和运行的向量嵌入...
ml 字段中的嵌入存储为关键字和向量对。发出搜索查询时,它也会转换为嵌入。嵌入接近查询嵌入的文档被视为相关文档,并会进行检索和排名。以下示例是 title 字段 “
How to install and run the support diagnostics troubleshooting utility
” 的 ELSER 嵌入的样子 。虽然下面只显示了 title,但该字段还将包含 summary 的所有向量嵌入。
"content_title_expanded": {
"predicted_value": {
"software": 0.61586595,
"run": 0.9742636,
"microsoft": 0.3588995,
"utilities": 0.12577513,
"quest": 0.3432038,
"required": 0.092967816,
"patch": 0.027755933,
"download": 0.17489636,
"problem": 0.18334787,
"tab": 0.23204291,
"should": 0.16826244,
"connection": 0.022697305,
"diagnostic": 1.9365064,
"issue": 0.5547295,
"test": 0.29294458,
"diagnosis": 0.5534036,
"check": 0.5284877,
"version": 0.30520722,
"tool": 1.0934049,
"script": 0.1992606,
"driver": 0.38722745,
"install": 1.1497828,
"phone": 0.04624318,
"reset": 0.005169715,
"support": 2.2761922,
"repair": 0.010929011,
"utility": 1.5231297,
"update": 0.54428,
"troubles": 0.71636456,
"manual": 0.5289214,
"virus": 0.33769864,
"tools": 0.32632887,
"network": 0.44589242,
"button": 0.57999945,
"administrator": 0.49048838,
"patient": 0.09216453,
"installation": 0.43931308,
"##hoot": 1.3843145,
"supporting": 0.06898438,
"deployment": 0.17549558,
"detection": 0.0026870596,
"manager": 0.1197038,
"probe": 0.19564733,
"suite": 0.31462,
"service": 0.47528896,
"report": 0.12370042,
"setup": 0.91139555,
"assist": 0.008046827,
"step": 1.1416973,
"window": 0.17856373,
"outlook": 0.03414659,
"supported": 0.45036858,
"customer": 0.2380667
}
}
摘要和问题
语义搜索的有效性取决于文档摘要的质量。我们的技术支持文章有由支持工程师编写的摘要,但我们提取的其他文档没有。考虑到我们提取的知识规模,我们需要一个自动化流程来生成这些摘要。最简单的方法是提取每个文档的前 280 个字符并将其用作摘要。我们对此进行了测试,发现这会导致搜索相关性较差。
我们团队的一位工程师想出了一个主意,改用人工智能来做这件事。我们创建了一项新服务,利用 OpenAI GPT3.5 Turbo 来补充我们所有在提取时缺少摘要的文档。将来,我们打算测试其他模型的输出,以找出我们可能在最终摘要中看到的改进。由于我们有一个 GPT3.5 Turbo 的私有实例,我们选择使用它来在所需的规模下保持低成本。
服务本身很简单,是找到并微调有效提示(prompt)的结果。提示为大型语言模型提供了一组总体方向,然后为每个任务提供了一组特定的方向子集。虽然更复杂,但这使我们能够创建一个 Cloud Run 作业,该作业循环遍历我们知识库中的每个文档。循环在转到下一个文档之前执行以下任务。
- 使用提示和文档 content 字段中的文本向 LLM 发送 API 调用。
- 等待完成的响应(或妥善处理任何错误)。
- 更新文档中的 summary 和 questions 字段。
- 运行下一个文档。
Cloud Run 允许我们控制并发工作者的数量,这样我们就不会使用分配给我们的 LLM 实例的所有线程。这样做会导致支持助手的任何用户超时,因此我们选择在几周内补充现有的知识库 —— 从最新的产品文档开始。
创建总体摘要
提示的这一部分输出尽可能简洁的摘要,同时仍保持准确性。我们通过要求 LLM 多次检查其生成的文本并根据源文档检查其准确性来实现这一点。指示了特定的指导方针,以便每个文档的输出都一致。用一篇文章亲自尝试这个提示,看看它可以生成什么样的结果。然后更改一个或多个指导方针并在新的聊天中运行提示,以观察输出的差异。
const promptsByGenerationStrategy = {
[GenerationStrategy.AbstractiveSummarizer]: `
For \`generate[ì]\`, you will generate increasingly concise, entity-dense summaries of \`data\`, considering content fields only,respecting the instructions in \`generate[i].prompt\` definition and the meaning of target \`generate[i].name\` field.
Repeat the following 2 steps 3 times.
Step 1. Identify 1-3 informative entities (";" delimited) from the article which are missing from the previously generated summary.
Step 2. Write a new, denser summary of identical length which covers every entity and detail from the previous summary plus the missing entities.
A missing entity is:
- relevant to the main story,
- specific yet concise (5 words or fewer),
- novel (not in the previous summary),
- faithful (present in the article),
- anywhere (can be located anywhere in the article).
Guidelines:
- The first summary should be long yet highly non-specific, containing little information beyond the entities marked as missing. Use overly verbose language and fillers (e.g., "this article discusses") to reach ~80 words.
- Make every word count: rewrite the previous summary to improve flow and make space for additional entities.
- Make space with fusion, compression, and removal of uninformative phrases like "the article discusses".
- The summaries should become highly dense and concise yet self-contained, i.e., easily understood without the article.
- Missing entities can appear anywhere in the new summary.
- Never drop entities from the previous summary. If space cannot be made, add fewer new entities.
Output: \`generatedField[i]\` with the resulting string.
`,
创建第二个摘要类型
我们创建第二个摘要,使我们能够搜索代表文章的整个文本的特定段落。在此用例中,我们尝试保持更接近文档中已有的关键句子的输出。
[GenerationStrategy.ExtractiveSummarizer]: `
For \`generate[ì]\`, you will generate a summary with key sentences from the article that cover the main topics.
`,
创建一组相关问题
除了摘要之外,我们还要求 GPT 生成一组与文档相关的问题。这将以多种方式使用,包括为用户提供基于语义搜索的建议。我们还在测试将问题集纳入 Elastic Support Assistant 的混合搜索方法的相关性,以便我们搜索标题、摘要、正文内容和问题集。
[GenerationStrategy.QuestionSummarizer]: `
For \`generate[ì]\`, you will generate a key-term dense list of questions covering most subjects that \`data\` can answer with confidence.
- Each individual question must be complete and specific, always containing the related subjects.
- Don't explicit the existence of \`data\` in any way within the question (e.g. "Does the document provide...", "Does the article have...").
- Cover different levels of abstraction/specificity.
- Make versions explcit whenever suitable.
- Consider content fields only.
- Format: \`\${Q1}? \${Q2}?...? \${QN}?\`.
- Respect the instructions in \`generate[i].prompt\` definition and the meaning of target \`generate[i].name\` field.
Output: \`generatedField[i]\` with the resulting string.
`,
支持助理演示
尽管后端运行了大量任务和查询,但我们仍选择保持聊天界面本身的简单易用。成功的支持助理将顺畅地工作并提供用户可以信赖的专业知识。我们的 alpha 版本如下所示。
主要学习要点
构建我们当前知识库的过程并非一帆风顺。作为一个团队,我们每天都会测试新的假设,并观察我们的支持助理用户的行为以了解他们的需求。我们经常将代码推送到生产环境并衡量其影响,以便我们能够避免出现小故障,而不是功能和项目级别的故障。
更小、更精确的上下文使 LLM 响应更具确定性。
我们最初将较大的文本段落作为用户问题的上下文。这降低了结果的准确性,因为大型语言模型通常会忽略关键句子而选择那些没有回答问题的句子。这使得搜索变成了一个问题,既要找到正确的文档,又要找到这些文档如何与用户问题相匹配。
RBAC 策略对于管理特定角色可以访问的数据至关重要。文档级安全性减少了我们的基础设施重复,降低了部署成本并简化了我们需要运行的查询。
作为一个团队,我们很早就意识到我们的技术债务会阻碍我们为支持助手提供令人喜爱的体验。我们与产品工程师密切合作,并制定了使用最新 Elastic 功能的蓝图。我们将撰写一篇关于从 Swiftype 和 Appsearch 过渡的深入博客,以详细说明这一经验。敬请期待!
一个搜索查询无法涵盖用户问题的潜在范围。有关此内容的更多信息,请参阅第 4 部分(RAG 聊天机器人的搜索和相关性调整)。
我们测量了用户对回复的情绪,并学会了更有效地解读用户意图。实际上,用户问题背后的搜索问题是什么?
了解用户搜索的内容对于我们丰富数据起着关键作用。
即使文档规模达到数十万份,我们仍然会发现记录知识中存在差距。通过分析用户趋势,我们可以确定何时添加新类型的来源,并更好地丰富现有数据,以便我们将来自多个来源的上下文打包在一起,以供 LLM 进一步阐述。
下一步是什么?
在撰写本文时,我们已经为索引中的 300,000 多个文档和超过 128,000 个人工智能生成的摘要建立了向量嵌入,平均每个文档有 8 个问题。考虑到我们只有约 8,000 篇带有人工撰写摘要的技术支持文章,这对我们的语义搜索结果来说是 10 倍的改进。
Field Engineering 有一个路线图,其中列出了扩展我们的知识库的新方法,并扩展了我们的显式搜索界面和 Elastic Support Assistant 在技术上的可能性。例如,我们计划为技术图表创建采集和搜索策略,并为 Elastic 员工采集 Github 问题。
创建知识源只是我们与 Elastic Support Assistant 合作的第一步。请在此处的第一篇博客中阅读有关我们最初的 GenAI 实验的信息。在第三篇博客中,我们深入探讨了用户体验的设计和实施。随后,我们的第四篇博客将揭示我们调整搜索相关性的策略,以便为 LLM 提供最佳背景。请继续关注,获取更多关于你自己的生成式 AI 项目的见解和灵感!
准备好自己尝试了吗?开始免费试用。
想要获得 Elastic 认证?了解下一期 Elasticsearch 工程师培训何时开始!
原文:GenAI for Customer Support with Elastic ELSER — Search Labs
版权归原作者 Elastic 中国社区官方博客 所有, 如有侵权,请联系我们删除。