在上一篇文章中,我们提到可以将整段的文本转换成带有大纲的要点格式,但是并没有讲怎么将这个生成的大纲“合并”到原来的文段上,这一篇中我就来阐述一下其中的技术细节。整体上这个任务会拆解为3个小任务:
- 解析带大纲的 Markdown 语法树
- 使用嵌入模型计算要点和正文段落的相似度
- 参考 LCS 算法实现要点和段落匹配
后面分节依次讲解。
1. 解析 Markdown 语法树
Markdown 是一种较为常见、方便书写的文本格式,同时也是上一阶段LLM任务的输出格式。这里就需要提取出它的标题信息
标题层级、以及每个标题下的文本要点。
直接写匹配的代码会比较费时,于是我就在网上寻找是否有类似的 Python 库,找到了不少,重点列几个。
首先在 Stackoverflow 上找到一个类似的 【parse and traverse】 需求 python - Parse and traverse elements from a Markdown file - Stack Overflow,其高赞的回答引起了我的兴趣,因为他说并不推荐 Python-Markdown 并且他自己还是 Python-Markdown 的作者。如此的坦诚让我对它的回答产生了兴趣。
库名 | 项目地址 |
Python-Markdown | |
Mistune | |
Mistletoe |
1.1 解析成 AST
我顺着这个回答找到了 Mistune 这个库,并翻到了它的教程指引。总统来说,它的作用是将一段 markdown 文本渲染为 html 格式,也可以通过插件的方式,提供更多渲染功能。然而,Mistune 还提供了一种解析 AST (Abstract Syntax Tree) 的方式:
import mistune markdown = mistune.create_markdown(renderer='ast') text = 'hello **world**' markdown(text)
通过传入 ast,就可以将示例文本解析如下:
# ==> [ { 'type': 'paragraph', 'children': [ {'type': 'text', 'raw': 'hello '}, {'type': 'strong', 'children': [{'type': 'text', 'raw': 'world'}]} ] } ]
这个基本上就是我想要的功能了,通过读取 AST 树,就可以了解到其构成,获取想要的内容。
1.2 从 AST 还原
虽然解析的问题解决了,但是此刻我依旧不知道如何从一个修改过的 AST 还原成 markdown,因为最终这个任务需要生成图文,那自然 markdown 格式是最方便的。
于是我顺着那个老哥的推荐继续向下,找到了 Mistletoe 这个库。并且找到了这个库作者写给开发者的文档,就是在这个文档中,他明确提到了 AST 的运作原理。在文档的最后,他举了一个从 Markdown 生成 Markdown 的例子。
假设你有一些Markdown,你想做些处理,然后再次输出为Markdown。由于Markdown的文本性质,通常可以使用文本搜索和替换工具来实现这一点。但并不总是如此。
例如,如果要替换纯文本中的文本片段,而不是嵌入的代码示例中的文本片段,则搜索并替换方法将不起作用。
在这种情况下,就可以使用 mistletoe 的
MarkdownRenderer
:- 将Markdown解析为AST(通常保存在
Document
标记中)。
- 对AST进行修改。
- 使用
MarkdownRenderer.render()
渲染回Markdown。
而实际运行的代码如下:
import mistletoe with open("README.md", "r") as fin: with MarkdownRenderer() as renderer: document = mistletoe.Document(fin) process_ast(document) md = renderer.render(document) print(md)
借助这个库,就可以提取出标题、段落等信息,为下一步的输入以及最后的拼接还原,做好了充足的准备。
2. 计算要点和文本的相似度
举例来说,如下是一段生成的带大纲的要点内容,以及其原始内容文本内容。前者是由后者概括总结生成的。当下的任务是,把右侧内容以一种合理的形式填充到左侧的框架梗概之中。
但是下面例子中,左侧大纲文本只有7个要点(slot),而右侧则有 9 个段落,两者不相等,要如何映射过去呢?
[带大纲的文本]
利用企业微信搭建AI机器人
- 文章介绍了如何使用企业微信创建AI机器人,为企业和个人提供各种服务。
注册企业微信
- 提供了注册企业微信的步骤,包括随意填写企业信息,录入管理员信息并进行扫码验证。
创建企业微信机器人
- 说明了创建应用的过程,以创建谷歌Gemini机器人为例。
设置可信域名
- 针对未认证企业,建议使用华为云函数进行免费验证,并详细解释了验证步骤。
集成AI大模型
- 介绍了Chat CPT on WeChat项目,支持多种AI技术和多模态功能。
接口示例
- 强调了Google Gemini AI的新颖性和免费API特性。
扩展功能集成
- 推荐了NAS Tools用于电影下载功能,以及Chat GLM3和Home Assistant结合实现智能家居控制。
[原始内容,有删节]
这不是普通的微信好友,这是我的家族企业。我们点击查看详情,实际上其中只有我一个是真实用户,其余皆为AI机器人。[…]
本期内容我们将分享如何免费注册一家一人制企业,从而“白嫖”企业微信的所有功能。[…]
首先,我们来看看第一步——注册企业微信。此处附有链接,只需点击进入。[…]
接下来,我们要开始创建企业微信机器人:点击“管理应用”,再选择“创建应用”。为了紧跟潮流,我在本期视频中将以创建一个谷歌Gemini机器人为例 […]
待应用创建完毕后,向下查找并点击“设置可信域名”,这是非常关键的一环。如果是刚创建的企业,尚未获得认证,那么可以借助华为云函数来进行免费验证。[…]
创建完云函数后,进入“设置”选项卡,点击“触发器”,并依次点击“创建触发器”→“创建分组”,[…]
回到企业微信后台,在“可信域名”位置粘贴之前生成的那个URL,注意要去掉末尾斜杠并将请求方法从HTTP改为无特定要求的状态。[…]
在企业微信端确认保存更改后,显示“修改成功”,这意味着我们的企业微信机器人已经具备开启所有功能的能力。[…] 我选用的是最新推出的Google Gemini AI,它不仅新颖而且现阶段其API仍保持完全免费状态。
受限于篇幅,本期主要聚焦于对接AI大模型的实现步骤。如果需要集成电影下载功能,推荐查看NAS Tools工具;而要实现智能家居控制的话,可以通过组合Chat GLM3和Home Assistant来达成目标。[…]
2.1 计算文本向量嵌入
对于任意的文段,很难确保两者数量是相等的,于是便需要一个方法,能够计算文本之间的相似度。从文本中分词并使用关键词匹配,是一种不错的做法。而另一种做法,则是使用文本向量嵌入。
文本向量嵌入是一种表示技术,它将文本(如单词、短语或文档)转化为一系列的实数向量。这种向量的特点是,语义相近的文本在向量空间中的距离也相近,这使得机器能够理解和处理文本数据。这项技术常常用于自然语言处理(NLP)的各种任务,如情感分析、文本分类和推荐系统等。
现如今,已经有很多开源的模型可以提供文本向量嵌入的计算。如果不想本地部署,也可以调用 OpenAI Embedding 的接口来实现。
经过计算,左侧信息对应的文本嵌入向量是这样的:
要点对应层级 | 文字内容 | 文本嵌入向量 |
H1 | 文章介绍了如何使用企业微信创建AI机器人,为企业和个人提供各种服务。 | [-0.03085 -0.04855 -0.0344 ... -0.01874 0.0305 -0.0206 ] |
H2 | 提供了注册企业微信的步骤,包括随意填写企业信息,录入管理员信息并进行扫码验证。 | [-0.006527 -0.07294 -0.02509 ... -0.00958 0.02565 -0.02151 ] |
H2 | 说明了创建应用的过程,以创建谷歌Gemini机器人为例。 | [-0.06097 -0.06647 0.03268 ... 0.002405 -0.01404 -0.02138 ] |
H3 | 针对未认证企业,建议使用华为云函数进行免费验证,并详细解释了验证步骤。 | [ 0.01604 -0.0347 -0.03897 ... 0.007103 -0.00497 -0.03986 ] |
H2 | 介绍了Chat CPT on WeChat项目,支持多种AI技术和多模态功能。 | [ 0.00418 -0.022 -0.02782 ... -0.0759 0.04807 -0.0474 ] |
H3 | 强调了Google Gemini AI的新颖性和免费API特性。 | [-0.03232 -0.0179 -0.007637 ... -0.02066 -0.02895 -0.00454 ] |
H2 | 推荐了NAS Tools用于电影下载功能,以及Chat GLM3和Home Assistant结合实现智能家居控制。 | [-0.00862 0.003813 -0.02989 ... 0.01499 -0.02501 -0.06464 ] |
同样的,对于右侧的文本,也可用类似的方式去计算其文本嵌入向量,结果如下:
段落编号 | 文字内容 | 文本嵌入向量 |
1 | 这不是普通的微信好友,这是我的家族企业。[…] | [ 0.03986 -0.02347 0.01912 ... -0.02864 0.01813 -0.0288 ] |
2 | 本期内容我们将分享如何免费注册一家一人制企业。[…] | [ 0.000532 -0.05658 -0.002401 ... -0.02426 0.01541 -0.02089 ] |
3 | 首先,我们来看看第一步——注册企业微信。[…] | [ 0.0499 -0.03812 0.0083 ... -0.001751 0.00808 0.0067 ] |
4 | 接下来,我们要开始创建企业微信机器人:点击“管理应用”,再选择“创建应用”。[…] | … |
5 | 待应用创建完毕后,向下查找并点击“设置可信域名”。[…] | … |
6 | 创建完云函数后,进入“设置”选项卡,点击“触发器”。[…] | … |
7 | 回到企业微信后台,在“可信域名”位置粘贴之前生成的那个URL。[…] | [ 0.01271 0.02037 0.03073 ... -0.00922 0.0354 -0.02211 ] |
8 | 在企业微信端确认保存更改后,显示“修改成功”。[…] | [-0.001373 -0.01369 0.01886 ... -0.012764 0.02904 -0.0561 ] |
9 | 受限于篇幅,本期主要聚焦于对接AI大模型的实现步骤。[…] | [-0.010544 0.00959 -0.00388 ... 0.0705 -0.01765 -0.0657 ] |
2.2 计算相似度矩阵
这里的向量都是归一化长度的,这意味着我们可以用向量内积的方式,去度量它们的相似度。内积越大,说明向量越相似,也就说明两者对应的文本内容也就越解决。
为此,我计算了两两之间的内积,制作画成了下表的格式。
ㅤ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
H1 | 0.764 | 0.585 | 0.589 | 0.763 | 0.5103 | 0.4211 | 0.5127 | 0.622 | 0.4983 |
H2 | 0.6216 | 0.733 | 0.7925 | 0.623 | 0.639 | 0.453 | 0.625 | 0.62 | 0.387 |
H2 | 0.6343 | 0.5166 | 0.5615 | 0.844 | 0.503 | 0.4036 | 0.4524 | 0.5864 | 0.4282 |
H3 | 0.5166 | 0.5913 | 0.585 | 0.5273 | 0.882 | 0.6484 | 0.8047 | 0.531 | 0.414 |
H2 | 0.5454 | 0.3489 | 0.3853 | 0.4539 | 0.337 | 0.3816 | 0.3486 | 0.6406 | 0.561 |
H3 | 0.4023 | 0.302 | 0.2756 | 0.5435 | 0.385 | 0.3499 | 0.285 | 0.3857 | 0.365 |
H2 | 0.39 | 0.3076 | 0.317 | 0.43 | 0.3735 | 0.3801 | 0.3372 | 0.4087 | 0.781 |
如果将每一列的最大值用着重颜色标出,就会发现形成了一条从左上角到右下角的蜿蜒曲折的通路。这恰好就是将文段嵌入到大纲的一种方式。而且你会发现,由于文段嵌入的顺序性,这条曲折通路只能向右平移、或者是向下延申,而不能反方向行进,不然就会造成嵌入的混乱。
尽管在表格中还有一些稍大的值散落在通路周围,但是为了绕路选择他们,就会出现“捡了芝麻,丢了西瓜”的情况,在整体上是非最优的。
3. 要点和段落匹配
为了找到要点和段落的匹配,这个问题其实很像 LCS (Longest Common SubSequence) 问题,只不过原来问题中的定义是0或1的整数,这里变成0~1之间的浮点数。为了解决这类问题,通常的做法是动态规划。
动态规划是一种用于求解最优化问题的算法策略。它的基本思想是将一个复杂的问题分解为一系列简单的子问题,同时保存子问题的答案,避免重复计算。这种方法只适用于满足“无后效性”和“最优子结构”条件的问题。无后效性是指子问题的解一旦确定,就不再改变,不受在这之后做出的决定的影响。最优子结构是指问题的最优解包含其子问题的最优解。
因此,只需确定其最优子问题是什么。我们需要找到一条从左上角到右下角的路径,并且使得这条路径途径的数值加起来尽可能地大。
考虑文本长度固定,但是要点个数递增。当要点数目只有1个时,答案是平凡的。因此可作为初始的结构。我们考虑如下的递推关系:
对于路径上的任意一个节点,其上一个节点有三种可能:
- 从左边:这代表这多个文段对应者同一个要点的情况,是可能的
- 从左上:这代表着切换到下一个要点和文段间的对应关系
- 从上方:这代表着一个文段对应着多个要点(为了方便计数,这里会取其中相似度最高的那个要点作为输出值,而不是简单的相加)
并且根据其最大值实际出现的分支,确定一个方向,最终计算结果如下。
局部分数求和,以及反推路径: [[0.76 * 1.35 ← 1.94 ← 2.70 ← 3.21 ← 3.63 ← 4.14 ← 4.77 ← 5.27 ←] [0.62 ↑ 1.50↖ 2.29 ← 2.91 ← 3.55 ← 4.00 ← 4.63 ← 5.25 ← 5.64 ←] [0.62 ↑ 1.28 ↑ 2.06↖ 3.13↖ 3.64 ← 4.04 ← 4.49 ← 5.21↖ 5.68↖] [0.50 ↑ 1.28 ↑ 2.06 ↑ 2.82 ↑ 4.02↖ 4.66 ← 5.47 ← 6.00 ← 6.41 ←] [0.50 ↑ 1.04 ↑ 1.86 ↑ 2.74 ↑ 3.47 ↑ 4.40↖ 5.01↖ 6.11↖ 6.67 ←] [0.36 ↑ 0.99 ↑ 1.75 ↑ 2.74 ↑ 3.47 ↑ 4.37 ↑ 4.95 ↑ 5.86 ↑ 6.47↖] [0.35 ↑ 0.99 ↑ 1.75 ↑ 2.63 ↑ 3.46 ↑ 4.37 ↑ 4.95 ↑ 5.86 ↑ 6.64↖]]
由此,我们便通过定量计算的方式,得到了和上一节中的那条最短路径。