自从 ChatGPT 在 2023 年初激发了公众的想象力以来,人们对基于大型语言模型的应用程序商业化的兴趣激增。最有趣的应用之一是创建专家聊天系统,该系统可以回答来自专有知识库的自然语言查询。
该领域最流行的技术之一是[检索增强生成](https://retrieval Augmented Generation aws),即 RAG,它使用文档嵌入来查找与用户查询相关的项目,然后再使用大型语言模型生成一个答案。
该技术非常强大,因为它允许极其便宜和快速的查找,为知识库随时间变化和发展提供极大的灵活性,以及高度知情和准确的答案,大大减少幻觉和错误。
要更深入地分析 RAG 系统以及如何实施该系统,您可以在此处阅读我的上一篇文章。
尽管 RAG 系统非常强大,但其架构也存在一些严重的限制。我们在上一篇文章中探讨了一些限制,并提出了改进架构的方法。
今天,我们将探讨基于嵌入的架构的另一个限制,并提出一种解决该架构限制的方法。
假设我们是一家出版物,想要创建一个允许读者和客户提问的聊天界面。
当然,我们将能够回答诸如“您对 X 有何看法?”之类的问题。或“你对 Y 说了什么?”通过简单的 RAG 实现,但是当您处理诸如“您对 2021 年的 X 有何看法?”之类的问题时,RAG 架构确实开始陷入困境。或“2021 年至 2023 年间,您对 Y 的承保范围有何变化?”
基于嵌入的 RAG 面临的挑战之一是嵌入模型通常无法以系统的方式对元数据进行编码,因此任何需要了解发布日期或作者姓名等信息的查找都会给您的 RAG 系统带来相当大的麻烦。
我们可以通过利用大型语言模型最令人兴奋的功能之一——代码生成来解决这个问题。我们将查看现实世界的出版物,设计一个基于 LLM 的算法来改进 RAG 架构,并基于该算法构建一个聊天机器人。
今天,我们将关注CB Insights Newsletter,这是一份涵盖初创公司和技术的热门每日新闻通讯。作为 CB Insights 的前全栈开发人员,我经常期待在工作日结束时看到创始人独特的智慧和洞察力。
今天,我们将使用 CB Insights 时事通讯存档作为基础数据来构建一个聊天机器人,该机器人可以以优于基于普通嵌入的 RAG 实现的方式回答基于元数据的自然语言查询。
具体来说,我们希望聊天机器人能够回答以下问题:
让我们开始吧!
为了完成这项任务,我们将使用以下技术:
如果您阅读了我的其他文章,那么我将使用 Python 来编写本文中的大部分代码就不足为奇了。 Python 具有出色的网络抓取、数据处理和 OpenAI 集成,所有这些我们都将在今天的项目中利用。
SQL 是一种查询语言,允许用户与几个主要的关系数据库进行交互,包括 SQLite、MySQL、PostgreSQL 和 SQL Server。该语言是数据库的一组指令,涉及如何在将数据返回给用户之前检索、组合和操作数据。
LLM 代码生成是一项在过去几个月中受到广泛关注的技术,因为包括 GPT 3.5、GPT 4 和 LLaMa 2 在内的多个基础模型已经证明了能够生成极其复杂的代码来响应自然语言查询。
经过专门训练和调整的系统,例如 GitHub 的 Copilot,能够通过使用专门为代码生成而设计的模型来编写非常智能的代码,但是经过适当提示的通用 GPT 模型在编写代码时已经具有出色的功能。
语义嵌入是大多数 RAG 实现的支柱。通过使用一系列自然语言技术,我们可以将自然语言文本转换为在语义向量空间中表示文本内容的数字向量。
然后,我们可以使用向量代数来操纵这些嵌入,从而使我们能够使用数学方法确定两个文本语料库之间的关系。
GPT-4 拥有 1.7 万亿个参数,是当今市场上最强大的基于 Transformer 的大型语言模型。 GPT-4 能够理解大量文本、复杂推理,并针对困难的提示生成长期令人信服的答案。
GPT-3.5 是 GPT-4 的小得多的表亲,是在 ChatGPT 席卷全球时为 ChatGPT 提供动力的模型。它能够处理极其复杂的提示,并且它缺乏纯粹的推理能力,但它在速度和成本节省方面得到了弥补。
对于更简单的任务,GPT3.5 在性能和准确性之间取得了平衡。
在构建人工智能之前,我们需要获取数据。为此,我们可以使用 CB Insights 的时事通讯存档页面 [ https://www.cbinsights.com/newsletter/ ],其中收集了过去的时事通讯。
要获取所有链接,我们可以使用 Python 的 requests 和漂亮的 soup 库,如下所示:
import requests from bs4 import BeautifulSoup res = requests.get('https://www.cbinsights.com/newsletter/') soup = BeautifulSoup(res.text) article_links = [[i.text, i['href']] for i in soup.find_all('a') if 'campaign-archive' in i['href'] ]
获得链接后,我们可以转到每个链接并下载文章 HTML。借助 Python 的列表理解,我们可以用一行代码完成此操作:
article_soups = [BeautifulSoup(requests.get(link[1]).text) for link in article_links]
这需要一段时间,但最终所有链接都应该被删除。
现在,我们可以使用 BeautifulSoup 来提取相关部分:
import re # SEO optimizations cause some articles to appear twice so we dedupe them. # We also remove some multiple newlines and unicode characters. def get_deduped_article_tables(article_table): new_article_tables = [] for i in article_table: text_content = re.sub(r'\n{2,}', '\n', i.replace('\xa0', '').strip()) if text_content not in new_article_tables or text_content == '': new_article_tables.append(text_content) return new_article_tables result_json = {} for soup_meta, soup_art in zip(article_links, article_soups): article_tables = [] cur_article = [] for table in soup_art.find_all('table'): if table.attrs.get('mc:variant') == 'Section_Divider': article_tables.append(get_deduped_article_tables(cur_article)) cur_article = [] else: cur_article.append(table.text) article_tables.append(get_deduped_article_tables(cur_article)) result_json[soup_meta[0]] = article_tables
让我们做更多处理,并将其转换为 DataFrame:
import pandas as pd result_rows = [] for article_name, article_json in result_json.items(): article_date = article_json[0][1] for idx, tbl in enumerate(article_json[1:]): txt = '\n'.join(tbl).strip() if txt != '': result_rows.append({ 'article_name': article_name, 'article_date': article_date, 'idx': idx, 'text': txt, }) df = apd.DataFrame(result_rows)
如果您检查数据框,您应该看到如下所示的内容:
当我们拥有数据时,我们还可以生成文章的嵌入。使用 OpenAI 的 ada 嵌入模型,这非常容易。
import openai EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [YOUR KEY] df['embedding'] = df['text'].map(lambda txt: openai.Embedding.create(model=EMBEDDING_MODEL, input=[txt])['data'][0]['embedding'])
现在我们已经有了用于本练习的数据,让我们将数据加载到数据库中。在本练习中,我们将使用 SQLite,它是一个轻量级、独立的数据库系统,与 Python 一起打包。
请注意,在生产环境中,您可能希望使用适当的数据库实例,例如 MySQL 或 PostgreSQL,并对我们在这里使用的 SQL 进行细微调整,但一般技术将保持不变。
要实例化并加载数据库,只需在 Python 中运行以下命令即可。请注意,除了文章文本和嵌入之外,我们还保存一些元数据,即发布日期。
另请注意,与大多数其他 SQL 数据库不同,SQLite3 使用动态类型系统,因此我们不必在创建查询中指定数据类型。
import sqlite3 import json con = sqlite3.connect("./cbi_article.db") cur = con.cursor() cur.execute("CREATE TABLE article(name, date, idx, content, embedding_json)") con.commit() rows = [] for _, row in df.iterrows(): rows.append([row['article_name'], row['article_date'], row['idx'], row['text'], json.dumps(row['embedding'])]) cur.executemany("INSERT INTO article VALUES (?, ?, ?, ?, ?)", rows) con.commit()
让我们尝试查询数据。
res = cur.execute(""" SELECT name, date, idx FROM article WHERE date >= DATE('now', '-2 years'); """) res.fetchall()
应该产生类似的结果:
看起来不错!
现在我们已经将数据加载到 SQLite 数据库中,我们可以进入下一阶段。请记住,仅嵌入 RAG 实施的挑战之一是缺乏灵活的元数据查找功能。
然而,现在我们已经将元数据加载到 SQL 数据库中,我们可以使用 GPT 的代码生成功能来执行灵活的元数据查找。
为了生成SQL代码,我们可以使用一些简单的提示工程。
response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] )
请注意以下提示工程 1) 我们给出数据库模式但保持简单。 2)我们指定返回列。 3) 我们指定可用于过滤的列。 4) 我们指定 SQL 风格。此提示应生成如下 SQL 代码:
SELECT * FROM article WHERE date BETWEEN '2022-06-01' AND '2022-08-31'
现在,由于返回值不确定,有时您会在生成的代码中遇到一些特殊情况。为了处理这些情况,我们可以简单地使用一个 try-catch 循环来尝试并重新生成数据。当然,我们不想无限地这样做,因此如果我们无法在三次尝试中生成正确的 SQL,我们将退出并退回到普通 RAG。
我们可以像这样实现过滤器:
res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall()
这是一个相对粗略的过滤器,因此在生产用例中,您可能希望对相关性和 SQL 正确性进行更多检查,但这对于我们的示例来说已经足够了。
关于人工智能安全性的快速说明,在运行从人工智能返回的代码时,我们应该小心,特别是如果用户输入用作提示的一部分。
如果我们不清理输出,我们就很容易受到即时工程攻击,即用户试图操纵人工智能生成更新或删除语句。
因此,在计算机上运行代码之前,我们应该始终检查以确保输出符合我们的预期。
运行以下代码查看检索结果:
df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res])
现在,您应该看到以下结果:
现在我们已经有了元数据查找的结果,剩下的就很简单了。我们计算所有检索结果的余弦相似度,如下所示:
from openai.embeddings_utils import cosine_similarity q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed))
现在,我们可以选取前 10 名时事通讯,并使用即时工程来回答这个问题。我们在这里选择了 10 个,因为每个新闻通讯摘录都相对较短。
如果您正在处理较长的文章,您可能会希望使用较少的文章,或者使用奖励部分中介绍的技术。
answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] )
这应该会为您提供类似于以下内容的结果:
2022 年夏季,各种时事通讯讨论了金融科技的未来。随着 2022 年第 2 季度的资金在 2021 年达到高点后,急剧下降至 2020 年的水平,金融科技的发展明显放缓。2022 年第 2 季度的报告强调了全球金融科技投资的下降。
然而,随着人们注意到金融科技向早期初创企业的重大转变,尤其是在支付领域,金融科技的未来似乎充满希望。由于资金在 2021 年高峰后恢复正常,支付行业的全球投资从 2022 年第一季度下降了 43%,至 2022 年第二季度的 5.1B 美元。
2022 年迄今为止,该领域的新进入者吸引了更高的交易份额 (63%),这标志着投资者对初创企业的兴趣。另据报道,金融科技给零售银行带来的竞争日益激烈,迫使它们优先考虑核心服务的数字化。
银行业的应对措施是使用聊天机器人和客户分析平台等技术,重点关注客户体验的增强,特别是移动银行。所有这些都表明金融科技行业未来充满活力和竞争力。
这相当不错!如果您通过查看答案提示来对答案进行事实核查,您会发现统计数据全部来自源材料。您还会注意到,GPT4 能够忽略源材料中的一些特殊格式。这显示了法学硕士在数据汇总系统方面的灵活性。
在此过程中可能遇到的问题之一是,当语料库非常大时,最终的提示可能会非常大。如果您使用 GPT-4,这可能会代价高昂,但很长的提示也有可能使模型感到困惑。
为了解决这个问题,我们可以使用 GPT-3.5 预处理各个文章,压缩我们在最后一步发送到 GPT-4 的最终提示。
summarization_prompt = ''' Summarize the following passage and extract only portions that are relevant to answering the user question. Passage: ======= %s ======= User Questions: %s ''' (row['content'], user_question) response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a summarizer of tech industry reports"}, {"role": "user", "content": summarization_prompt}, ] )
然后,我们可以将摘要放入提示中,与将纯文章放入最终提示中相比,可以节省大量成本。
现在我们已经编写了 Python 代码,让我们将其打包为一个简单的 Web 应用程序。
用 Flask 将代码打包为后端 API 相对简单。只需创建一个函数,并将其链接到 Flask,如下所示:
import requests from bs4 import BeautifulSoup import re import pandas as pd import sqlite3 import json import openai from openai.embeddings_utils import cosine_similarity from flask import Flask, request, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [Your OpenAI Key] db_location = [Location of your SQLite DB] def process_user_query(user_question): con = sqlite3.connect(db_location) cur = con.cursor() user_question = 'What did you say about the future of the fintech industry in summer of 2022?' res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall() df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res]) q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed)) answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] ) return response.choices[0].message['content'] @app.route('/process_user_question', methods=["POST"]) def process_user_question(): return jsonify({ 'status': 'success', 'result': process_user_query(request.json['user_question']) }) app.run()
这实际上就是我们需要为后端做的全部事情!
因为我们只有一个端点,并且应用程序中不需要大量状态,所以前端代码应该非常简单。还记得在过去的文章中,我们设置了一个带有路由的 React 应用程序,它允许我们在特定的路由上渲染组件。
只需按照该文章中的说明设置 React.JS 项目,然后在您选择的路径上添加以下组件:
import React, {useState, useEffect} from 'react'; import axios from 'axios'; const HNArticle = () => { const [result, setResult] = useState(''); const [message, setMessage] = useState(''); const [question, setQuestion] = useState(''); const askQuestion = () => { axios.post("http://127.0.0.1:5000/process_user_question", {user_question: question}) .then(r => r.data) .then(d => { console.log(d); setResult(d.result); }); } return <div className="row" style={{marginTop: '15px'}}> <div className="col-md-12" style={{marginBottom: '15px'}}> <center> <h5>Hackernoon CB Insights Demo</h5> </center> </div> <div className="col-md-10 offset-md-1 col-sm-12 col-lg-8 offset-lg-2" style={{marginBottom: '15px'}}> <ul className="list-group"> <li className="list-group-item"> <h6>Your Question</h6> <p><input className="form-control" placeholder="Question" value={question} onChange={e => setQuestion(e.target.value)} /></p> <p>{message}</p> <p> <button className="btn btn-primary" onClick={askQuestion}>Ask</button> </p> </li> {result? <li className="list-group-item"> <h6>Response</h6> {result.split("\n").map((p, i) => <p key={i}>{p}</p>)} </li>: ''} </ul> </div> </div>; } export default HNArticle;
运行代码,你应该看到这样的界面:
提出一个问题,过了一会儿,您应该看到输出:
瞧!我们已经成功构建了一个聊天机器人,它具有超越普通 RAG 系统的高级查询功能!
在今天的文章中,我们构建了一个具有强大代码生成功能的聊天机器人。这是许多人工智能先驱现在正在构建的新型法学硕士应用程序的一个例子,它可以利用数据、编程语言和自然语言理解来构建具有专业知识的生成人工智能系统。
这些专业系统是法学硕士应用程序解锁商业可行性的关键,这些应用程序希望提供超出 OpenAI 和 Anthropic 等平台提供商直接提供的价值。
代码生成只是最近一代商用大型语言模型实现的技术之一。
如果您对如何将法学硕士商业化有想法,或者想就人工智能进行对话,请随时通过LinkedIn或GitHub联系。在过去的一年里,我与读者进行了许多富有洞察力的对话,并期待更多!