前言
在上一年年末 ChatGPT 刚火的时分我就有一个主意,它能不能帮我读一下不流畅难明的稳妥条款,告知我它终究在讲什么?终究什么病能赔多少钱?甚至能告知我里边是不是藏有一些坑? 可是当我把条款内容仿制到 ChatGPT 时,咱们会发现,它直接告知你:“太长了,它受不了”。
当咱们自己翻开 openai 的文档(platform.openai.com/docs/api-re…),咱们才明白:哦,原来它承受的最大长度是 4096
个 tokens
。但这个 一个 token
终究是多长呢?暂时还不知道,横竖便是有这么个上限。很显然,咱们的稳妥条款远远的超越了它的上限,由于我才仿制两三页的内容它就 Error
了。
但咱们仍是纳闷,不应该啊,ChatGPT 不应该很强吗?它的官方比方可是摆了几十个事例,看网上的各种文章,它好像在文字与编码范畴,远超绝大数人类,怎样会连个稳妥条款都无法承受。
我想从这个事例中看看有没有其他路子,可惜的确没有适宜的事例能处理我这种超长文本的诉求。所以我停止了这个主意,并先回家过了个高兴的新年。
但在最近,在我的不屑但或许没啥含义的努力下,我简直完结了这个主意。先放几个截图给咱们看看。
问蚂蚁爆款「好医保长时刻医疗」几个问题的答案: 问市道上很火的「达尔文7号重疾」的问题及答案: 假如你细心看,你会发现,它现已能非常精确的答复这几个许多稳妥小白常问的问题了。 那我终究是怎样实现的呢?这篇文章来一探终究。
先纠正一下
在我开端正文之前,先让 ChatGPT 跟咱们做个简略介绍。 所以本文标题其实不对,精确说应该是「怎样让 openai 的 API 帮我读懂稳妥条款」。由于我其实是调用了 openai 供给的 API 能力来满足需求的。更精确来说是调用了其 GPT-3 的一些模型,而不是挂代理直接问 ChatGPT。但为了大部分读者简单理解,就先取一个不恰当的标题了。
后文中,我将会以 GPT 通用指代我所调用的 openai 的 API 服务。
中心处理计划
话说在新年回来后,ChatGPT 仍愈演愈烈,因而我又来了点儿兴趣,并测验把 GPT 接入我一个年久失修的个人大众号。就在这个接入的过程中,为了处理接入遇到的不少问题,我看了不少文档。果然是开卷有益,实干兴邦啊。过程中我又接触学习了一些有用常识。其间最重要的是两个点常识:
其一是:GPT 的多轮对话是怎样实现的? 其实很简略,便是把历史对话都存起来,然后依照时序从头拼接,再加上这次的问题,合并一起作为 prompt
再传给 GPT 即可。
其二便是,怎样让 GPT 理解超长文本常识并做问题答复? 我在逛 openai 官方文档的时分,发现了其实人家早早就想到了这个问题,并贴心的准备好了教程文档。这在我上一篇 ChaGPT 的文章中也已提到: 大众号怎样接入 ChatGPT 及 一些感触
- How to build an AI that can answer questions about your website:platform.openai.com/docs/tutori…
- Question Answering using Embeddings:github.com/openai/open…
它的思路其实很好理解,详细来说,主要是分几步:
-
先将巨量的文档常识拆块,并运用 openai 供给的
Embeddings
能力将该部分内容向量化,并做映射存储。向量化的意图是为了做两部分文本的类似性匹配。关于它的文档在这:platform.openai.com/docs/guides… -
当用户发问时,将用户的「发问文本」也做向量化。
-
遍历已拆块并向量化的文档内容,将之与向量化后的「发问文本」做内容类似性比较,找到最为类似的文档内容向量。
-
依据之前的映射关系,找到这段「向量」映射着的原始文档内容块。并把这个内容块作为上下文传给 GPT。
-
GPT 会依据这段上下文答复用户的发问。
原来如此,那么我只需把稳妥条款分段向量化,再依据用户发问匹配到相应的那段内容再答复不就好了吗。简略,上手吧。
把大象放进冰箱需要几步?
这个问题好像正如「把大象放入冰箱」。描绘起来很简略,真实要做起来就寸步难行。
在咱们面前最大的问题便是,终究怎样把这个文档做切割?
最简略的计划自然是,把稳妥条款按页码一页一页分块,假如一页内容也超了,那咱们就半页半页分块。 但这疏忽了一个最大的问题,就像大象的各个器官并非水平均分分布一样,常识内容并非是按页码切割的。一个常识或许第三页正好起了个标题,第四页才是详细的描绘。而向量化匹配的时分,却或许只匹配到第三页的内容。比方这个「好医保长时刻医疗」的职责革除条款,就很简单丢掉下半部分的革除职责,形成答复精确性下降。 除此外,这样的切割还简单让 GPT “学坏”。由于粗暴的按页切割,很简单把无关的常识传给 GPT,导致它或许会由于这些无关的信息回来过错的答案。比方如下关于用户信息告知的条款: 前一页内容如下: 后一页内容如下: 假如你询问的问题是:“假如投保时年龄填写过错,理赔时会怎样样”。 那很有或许你只会将第一页内容传给 GPT,它将会告知你保司不承当任何职责,并不退回稳妥费。 而用我实现的服务所拼接的常识块,得到的版本答案如下: 显然这个问题得到了精确答复。
以上两个事例比较生动的说明晰切割的重要性。
怎样切割文档
懂得了许多道理,也仍旧过不好这一生。 – ChatGPT也不知道是谁说的
怎样切割文档?其实这个也很好想计划,仅仅比较难搞。 稳妥条款是有文章结构的,只需咱们能够按文章标题给文档做结构化就好了。 终究文档就会成为这样的一个文档树:
interface INode {
title: string;
content: string;
children: INode[]
}
type DocTree = INode[]
然后咱们在深度遍历这个文档树,去辨认每个节点所包含的一切内容的长度,到达必定阈值就剪下来作为一个「常识块」。这就像剪一个西兰花 ,按自己能够含进去的巨细,一朵朵剪下来。
经过这样的手法,咱们就能在满足常识文本长度的约束下,切下最为接连完好的常识内容。 这其实很简略,但假如必定要装逼取个算法名的话,那我称之为:西兰花算法。
但在咱们切割西兰花之前,还有一个棘手的问题,怎样把一个条款文档先变成一棵西兰花(一颗文档树)?
第 0 步:先明白tokens
咋回事
由于后文许多内容都跟这个tokens
相关,所以我必须得提早介绍一下。
有时刻的同学能够直接看官网介绍文档:
没时刻的同学能够继续听我简略总结一下:
-
tokens
不是指prompt
字符串的长度; -
token
指的是一段话中或许被分出来的词汇。比方:i love you
,便是三个token
,分别为 「i」「love」「you」。 - 不同言语
token
核算不一样,比方中文的「我爱你」其实是算 5 个token
,由于它会先把内容转成unicode
。读过我大众号那篇文章的同学,你们就会知道,有些 emoji 的token
长度会超出你的幻想。 - 你能够用这个网站在线体会你的文字的
token
长度:platform.openai.com/tokenizer - 在
node.js
环境中,你能够用 gpt-3-encoder 这个 npm 包来核算tokens
的长度。
OK,把握这些常识就满足理解咱们后文的内容了。
第 1 步:标题的辨认
咱们能够先看看市道比较火爆的医疗与重疾险产品的条款。发现其实稳妥大部分条款是有必定格式标准的。简直都是嵌套数字标题 + 内容。那是否能够依据必定的规矩,辨认出那部分是标题,然后依据标题做切割即可?比方说,依据 「数字 + ? + 数字?」的正则做匹配。 尽管我正则写不来,可是 ChatGPT 写的来呀:
const text = '1 React 1.1 react hooks 的运用技巧 1.2 react suspense 的效果 2 Vue 2.1 Vue compostion api 的运用技巧';
const regex = /(\d+\.?\d*)\s(\w+)/g;
const matches = text.matchAll(regex);
const result = [];
for (const match of matches) {
result.push(match[1] + ' ' + match[2]);
}
console.log(result);
// output
['1 React', '1.1 react', '1.2 react', '2 Vue', '2.1 Vue']
尽管它的答复不行完美,可是根本够咱们继续下一步编码了。所以我测验把 PDF 的全文内容仿制出来,并做切割。然后我就会发现几个很费事的地方:
- 数字不是只在标题中呈现,正文中也很简单呈现各种数字。
- 有些注释内容,也有数字+内容的呈现
所以咱们仿制出来的文本是这样的:
module.exports = `2.3 等候期
自本合同收效(或最终复效)之日起 90 日内,被稳妥人因意外伤害4以外的原因, 被稳妥人因意外伤害产生上述情形的,无等候
被确诊患有本合同约定的轻症疾病5、中症疾病6、严重疾病7的,咱们不承当稳妥
职责,这 90 日的时刻称为等候期。
期。
轻症疾病 中症疾病
严重疾病
本合同的稳妥职责分为根本部分和可选部分。
,本合 ,退还
等候期内,咱们的详细做法见下表:
等候期内产生的情形
咱们的做法
不承当本合同“2.4 稳妥职责”中约定的稳妥职责
同继续有用
不承当本合同“2.4 稳妥职责”中约定的稳妥职责
您已交的本合同稳妥费8(不计利息),
本合同终止
2.4 稳妥职责
1 保单收效对应日:本合同收效日每年(或半年、季、月)的对应日为保单年(或半年、季、月)收效对应日。若当月 无对应的同一日,则以该月最终一日为保单收效对应日。
2 保单年度:自本合同收效日或年收效对应日零时起至下一个年收效对应日零时止为一个保单年度。
3 稳妥费约定交纳日:分期交纳稳妥费的,首期稳妥费后的年交、半年交、季交或月交稳妥费约定交纳日分别为本合同
的保单年收效对应日、半年收效对应日、季收效对应日或月收效对应日。`
所以,假如仅仅粗暴的依据某种标题规矩来做切割,那咱们只会得到紊乱的成果。
那咱们人眼是怎样从页面中知道它是标题的呢?咱们自然是依据这个案牍的方位、巨细,综合了咱们的历史经验来判别它是不是标题。也便是说,要想真实从一段文本中做很好的标题辨认以及内容切割,必需要获取这段文本的其他元数据。
我的下意识,自然是希望还有 AI 的能力。我把 PDF 转图片,都传给某个 AI,它很聪明,帮我 OCR 辨认文档并做好了充分的文档结构化。
但我在 openai 官网并没有找到这样的 api 能力供给。由于我的 AI 储备非常单薄,我也很难在网上找到能够满足我诉求的开源东西。并且依据我很或许不成熟的感觉,我感觉现在练习出来的开源 AI 模型,顶多仅仅辨认出文字以及文字地点的肯定方位,也很难帮我直接把文档给依照标题结构化了。真有这样的需求,或许需要我自己准备大量材料来练习。这好像再一次难倒了我。
所以我又想到了 pdf.js
这个东西。咱们 C端 部分投保协议便是利用这个东西包,把 PDF 文档转成 DOM 烘托到页面上。尽管我之前并没有运用过,但我信任它必定能够拿到 PDF 上许多元数据,否则不或许做到还原成 DOM 去烘托。我甚至想,它有没有或许直接帮我转成一颗 依据标题现已结构化好的 DOM 树。
在我运用pdf.js
后,我发现,方才略微想的有点多了,但也满足用了。它能把 PDF 文档的文字块以及这个文字块的文字与巨细信息 解构出来。比方这样:
[{
"str": "2.4",
"dir": "ltr",
"width": 13.2,
"height": 10.56,
"transform": [10.56, 0, 0, 10.56, 346.03, 285.05],
"fontName": "g_d0_f1",
"hasEOL": false,
"pageNum": 4
},
{
"str": " 稳妥职责",
"dir": "ltr",
"width": 42.24,
"height": 10.56,
"transform": [10.56, 0, 0, 10.56, 364.39, 285.05],
"fontName": "g_d0_f12",
"hasEOL": false,
"pageNum": 4
}]
其间的 width
与height
决定了文字块的巨细,transform
决定了文字块在文档上的肯定方位信息。pdf.js
也是依据这些信息,把 PDF 内容以肯定方位与巨细一个个的转成 DOM 并制作在网页上。它不理解前后语序与内容成果,它仅仅粗暴的拼装。
但这对我来说现已够用了,有了这些信息,我就能分析出哪些文字块是标题,哪些文字块是正文的正常数字,哪些内容块是底部的注释内容。比方说:
- 呈现最多的字体巨细,有理由信任这便是正文字体巨细
- 继续呈现的一个很靠左的 X 坐标,且该坐标内容根本是数字,有理由信任,这便是数字标题或数字注释地点的 X 坐标
- 尽管契合上述第二条规矩,但却比正文字体小许多,有理由信任,这是注释前的数字
等等等等吧,除此外,咱们还需要判别什么时分到注释内容,什么是页码内容。由于这些内容都要做一些特别处理。别的便是不同文档或许有些特别的边界状况要处理一下。
尽管说这仍旧很人肉,不智能,但至少能把路走通了。至于有些不是以 x.x.x 这样的数字做标题的文档,比方:第一章、第一节什么的,仍是能拓宽的,但就先不考虑了。
第 2 步:过长内容摘要化
工作走到这一步,大问题就没有了。但实践使用的时分,咱们仍是会发现一个小问题,便是许多末节的内容其实比较长,咱们能做类似性映射的常识块其实往往不仅一块。当咱们拼接多块常识的时分,内容又超出了。而假如咱们只拼接一块内容,常识又不行完好。这又让咱们抓耳挠腮了。
我细心看了看这些末节的内容,我觉得,其实这段文本,要是用文言文来说,或许还能够再短一点(汉语真是博大精深)。可是我觉得假如让 GPT 帮我把它转成文言文的话,用户发问的问题很或许就映射不到了。当然,我也真的试了一下,发现 text-davinci-003
这个模型好像在文言文范畴也不太行,稳妥条款它很难转成文言文。
但我有了别的一个思路,便是稳妥条款其实废话仍是有些多的,我能够让 GPT 帮我做一些摘要性的总结,且尽量不丢掉最中心的有用常识。在我网上搜索这块相关的常识时,发现 NLP 范畴有一种叫「命名实体辨认(baike.baidu.com/item/%E5%91…)」的技术,常用于搜索引擎、信息提取、问答体系中。不管三七二十一了,openai 这么强壮,那我就这么让它帮我这么做吧。
async function getSummary({ content, tokenLength }) {
const promptContext = `'''{{content}}'''依据命名实体辨认构建内容摘要:`;
const contentTokenLength = tokenLength || encode(content).length;
const promptContextTokenLength = encode(promptContext).length;
const completion = await openai.createCompletion({
model: 'text-davinci-003',
prompt: promptContext.replace('{{content}}', content),
// 1000 ~ 4096,最大也不能超越1000
max_tokens: Math.min(
4096 - contentTokenLength - promptContextTokenLength,
1000,
),
temperature: 0,
});
return strip(completion?.data?.choices?.[0].text, ['\n']);
}
实践测试下来,这样的方法相比直接总结摘要,从终究效果来看,回来的成果会安稳许多,且回来的常识不会只提到一半。详细原因也不懂,有资深的大佬能够协助指点一下。
经过这样摘要化以后,咱们就能把一段较长的常识文本给有用缩短。当用户问起相关常识时,能够调用更多的常识块来答复用户。
第 3 步:超长内容极限紧缩
工作走到这一步,你或许认为就真没啥问题了。但实践上咱们又遇到了个小费事。便是有部分末节的内容仍旧仍是太长了。就像一颗基因变异的西兰花 。
我现已剪到最小的分支了,但这个最小的分支仍旧超越了max_tokens
的约束。这又难倒我了,现在我该怎样切割它?这好像回到了我最开端遇到的问题。
不过好在,这些变异的西兰花并没有动画灵能百分百中的那么夸大,大部分还仅仅 略超 max_tokens
一些,简直不会超越其两倍。而自己调查这些超出去的内容,往往是两种类型。
- 较长的表格,比方药品列表,如下图1。
- 一些职责或疾病的详细介绍,如下图2。
咱们发现这些末节的内容,其实并不适宜切割。比方药品列表要是切割成两块接近max_tokens
的常识内容,一次性问答只能获取其间一块常识。这就会导致答复过错。比方你问有多少种药品能够报销,它自然会算错。职责也是一样。
但这些末节有别的一个方向,便是紧缩内容。里边有许多文字其实是类似的,比方一堆的社保目录内/外
。比方职责内容中频繁呈现的:恶性肿瘤``稳妥金``被稳妥人
等等。咱们只需做一本字典,把这些很长的重复性文字,用别的一种特别的较短的字符指代。这段长文本就会瞬间被紧缩到较短的文本,咱们再连同字典一起发给 GPT,让它再翻译回来并做摘要化,所以就绕过了max_tokens
的约束。
但问题又来了,说的简单,代码怎样知道哪些文字是一段词语?假如代码不知道哪些文字是一段词语,又怎样做字典映射。总不能自己先把一切或许的词汇都预先想好吧。尽管稳妥有一些专业术语能够提早预设,但总归有更多的不知道的。
这就引出了 NLP 范畴的别的一门技术,分词。很开心的是,在中文范畴,且在 node.js 生态中,有一个比较好用的分词东西「结巴分词-github.com/yanyiwu/nod…」。 不出意外,这也是 ChatGPT 告知我的。
运用这个结巴分词,咱们就能够把一段内容切割成一个个词汇,一起也支撑传入用户预设的词汇字典。这样咱们就能知道哪些词汇在一段文本中被重复运用屡次。对于这些词汇,咱们再用一个最短的字符去映射它。
const nodejieba = require('nodejieba');
nodejieba.load({
userDict: './userdict.utf8',
});
const longText = '相学长白日吃饭,相学长正午也吃饭,相学长晚上还吃饭';
const words = nodejieba.cut(longText);
console.log(words);
// output
['相学长','白日','吃饭',',','相学长','正午','也','吃饭',',','相学长','晚上','还','吃饭'];
为了映射的字符尽量的短,我也是挠了一下脑袋,原本最简略便是一个特别字符加上从1
递加的数字就好了,比方这样:*${index}
。可是这个方法经过我实测,紧缩完的tokens
效果还不行极致。考虑到咱们都是根本是中文环境,我终究挑选了 26个字母巨细写 + 24个拉丁字母巨细写作为索引:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
依据第 0 步的常识,咱们知道,千万别用 emoji 去做字典索引。
这样咱们就得到最多100个索引,当然假如内容中已有呈现详细的字母,最好仍是针对该段内容除掉该字母。经过实践测试,这样的紧缩效果会比数字映射法略微好一些。且经过实测,这样问 openai 仍旧能得到正确答案。举个比方:
上文中的,相学长白日吃饭,相学长正午也吃饭,相学长晚上还吃饭
会被转化成,a白日b,a正午也b,a晚上还b|上文中,a:相学长,b:吃饭
咱们把这句话拿去问 GPT:相学长每天都在做什么
。它能给出正确的答复:相学长每天都在吃饭
。
除了字典法紧缩外,其实还有一个也比较显著的手法。便是把全角字符悉数转成半角字符。在我的实践测试中,一段 8247 个tokens
长度的内容。换半角相比不换半角,能多紧缩 580 个tokens
,简直是效果惊人!
其实不仅仅超越max_tokens
的文本需要紧缩。我主张超越 3000 tokens
的文本都得紧缩一下。由于 openai 最大的 4096 个token
约束。并非是约束 prompt
。而是约束 prompt
+ 它的答案。也便是说,当咱们做摘要化的时分,假如咱们供给的原始内容越长,它能回来的摘要就越短。这显然不契合咱们的诉求。所以,尽管文章中这儿写着是第三步,但实践操作时,紧缩其实是第二步,紧缩需要在摘要化之前。
也是由于max_tokens
的核算涵盖了 GPT 的答复内容,所以当咱们依据用户发问拼接常识块的时分,不能依照 max_tokens
的约束去打满内容,尽量留出 几百到一千的 tokens
给 GPT 做答复。
在我实操过程中呢,其实还存在一个文档的内容,怎样紧缩也紧缩不到预期的长度。我的确挑选了躲避,由于这段内容是无数个疾病的详细介绍,我骗自己说这些详细介绍并没太大用。因而终究我做了一个特别处理,假如是这个超长的疾病介绍,我就只保留了疾病标题,去掉了疾病的内容。
针对这种,再紧缩也处理不了的问题,我现在的确还没找到非常好的解法。
终究经过咱们对 PDF 文档的切割、紧缩、末节内容摘要化、转成嵌套文档树,终究再上一个西兰花算法。咱们就能完结对这个 PDF 文档的合理切割了。终究咱们再把切割后的内容做向量化处理,就能实现一个比较好的依据超长保单文档的稳妥产品问答服务。
其实其他范畴的文档也差不多,只需这个文档结构比较好切割。
代码已开源
相关代码开源,有兴趣的同学自己下载继续研讨吧~ github.com/wuomzfx/pdf…
关于终究怎样做向量化、怎样做匹配,我在本文就不多说了,这个仍是比较简单了。包含其他还有一些特别的处理,比方怎样把注释内容拼接到正文里。这些都能够在源码中便利寻找到。其他或许还略微需要一点东西常识的,便是 node 中怎样做两个 embedding
向量的类似性匹配。用 @stblib/blas
这个 npm 包就行。DEMO 示例:
const ddot = require('@stdlib/blas/base/ddot');
const x = new Float64Array(questionEmbedding);
const y = new Float64Array(knowledgeEmbedding);
const result = ddot(x.length, x, 1, y, 1),
假如还有哪里不明白的,欢迎评论区或许先测验问下 ChatGPT~
最终一点小感悟
感觉人工智能的年代真的要到来了,连我这种 AI 小白,好像都现已能完结一个或许真的能投入运用的服务。我再整个小程序,糊个页面,把一些异常容错机制再完善完善。再略微整个爬虫,从稳妥行业协会网站帮用户快捷找到相关的稳妥条款。我简直就能实现一个协助用户答复稳妥产品的使用了。
亦或许,我能够自己预设一些问题。经过这些问题,我能够从稳妥条款中结构化出许多有用的信息,比方保额保费、职责细节、投保年限、续保年限等等。结构化之后,我又能够直接做不同产品的比照,依据用户的要求引荐比较适宜的稳妥产品。这是一件挺有或许的工作,我测验把之前的两个问答作为比照再次问 GPT 引荐哪款产品,它的答复比较中肯且有用。
总之,新的 AI 基础设施,现已能成为现在大部分工程师的有利东西。在某些笔直范畴做一些深入研讨,经过这些东西,AI 就能发挥出意想不到的效果,咱们能够快速的产出各种有意思的产品。就好像 HTML5 跟 小程序 带来一系列有意思的 轻量APP 一样。信任,AI 浪潮在这两年就要席卷而来了~~