普通视图

发现新文章,点击刷新页面。
昨天以前Bright LGM's Blog

LLM 时代的集成意图识别实践

作者 Bright LGM
2025年3月31日 20:00

意图识别是构建可靠人机交互系统的核心技术,其意义在于通过精准判断用户需求,为后续处理提供确定性基础。

尽管大型语言模型(LLM)具备强大的生成能力,但其输出可能生成错误、矛盾甚至违规信息。在金融、医疗、法律等监管严格领域,直接使用 LLM 结果可能引发合规风险,甚至导致重大经济损失或法律纠纷。

因此,多数严谨系统仍采用传统分层架构:首先通过意图识别技术解析用户输入的核心诉求(例如判断用户是查询账户余额、还是咨询理财问题);随后根据预设规则执行对应业务流程,调用专门的数据接口或知识库进行响应。

这种架构的优势在于:意图识别环节可过滤无效请求并拦截高风险输入,处理环节则通过结构化流程确保合规性与结果可靠性,既发挥人工智能的辅助作用,又通过分层设计规避技术不确定性带来的潜在危害。

因此,意图识别仍然是构建智能系统非常重要的一环。

意图识别的技术选择

BERT 类模型

意图识别是一个典型的文本分类问题。在 LLM 之前,广泛采用的是基于 BERT 类或预训练 BERT 类模型进行训练或微调的方法。这类方法通过大量语料训练,能够较好地理解语义,准确识别用户意图。

但是这类方法也存在明显的局限性。因为为了构建这样的模型,我们不得不:

  1. 花时间搜集准备大量语料,并进行标注
  2. 在有 GPU 的专用训练设备上面训练
  3. 部署在有 GPU 的专用推理设备上

这些步骤都将花费大量的时间,并且,由于模型是专用的,线上推理时也容易引起资源浪费问题。

基于 LLM 技术

在 LLM 的时代,意图识别有没有什么更好的做法呢?

LLM 能被广泛接受的原因之一是其强大的通用性。通过将任务描述成提示词,理论上同一个 LLM 能处理任何的问题。因此,LLM 的使用门槛极低,无需收集和标注数据,无需训练和部署模型,只需要将任务描述成提示词即可。

意图识别任务当然也可以用 LLM 提示词来表示。看起来,用 LLM 来识别意图可能是 LLM 时代一个相当不错的选择。准备语料、训练、部署这些繁琐的步骤全部被编写提示词替代了。

基于 RAG 技术

除了上述两种常见的技术之外,还有什么其他方法吗?

LLM 时代另一个被广泛使用的 RAG 技术(Retrieval Augmented Generation)事实上也可以用于处理分类问题。RAG 中的一个关键技术点是基于语义计算相关性。

一个直接的想法是,是不是可以:1. 搜集所有分类的真实文本,组织成一个文本库;2. 在文本库中检索待预测的文本;3. 找到结果中最高相关性的文本对应的分类作为预测分类。

看起来也是可行的,而且它也具备很多优势:

  1. 利用通用模型的能力,无需重头开始训练模型
  2. 结果具备可解释性
  3. 具备很强的可工程优化性,在发现预测不好的文本时,直接将此文本放入文本库即可提升下次预测的准确度

基于 Embedding+MLP 小模型方案

事实上,完全基于相关度的分类也有其问题,其中比较严重的可能是泛化能力偏弱。即,如果文本库中没有相似的说法,则很难正确分类。

有什么办法可以提升泛化能力呢?

我们注意到 RAG 的相关性计算过程是:

  1. 预先将文本库中的所有文本全部编码为向量存储起来
  2. 在预测时,先用同样的模型将待预测文本编码为一个向量,然后再用此和向量和文本库中的全部向量计算相似度

文本的向量化是 RAG 中的关键一环,事实上,生成的文本向量(也称做文本嵌入,即 Embedding)不仅可以用于计算相似度,还可以用来进行分类。

我们可以设计一个简单的 MLP 模型(Multi-Layer Perceptron,即多层感知器模型,是一个简单的多层全连接的神经网络模型),输入为文本向量,输出为分类标签。

这样,我们就可以在预测时,先将文本编码为向量,然后用 MLP 模型进行分类。

这个方案的优势在于:

  1. 依然可以利用通用模型的能力,无需重头开始训练模型
  2. 泛化能力更强,即使文本库中没有相似的说法,也可以通过 MLP 模型进行分类
  3. 由于利用了大部分的通用文本向量模型的能力,MLP 模型的规模可以做得比较小,从而使得 MLP 模型的训练和推理都非常快

集成的意图识别方案

上面介绍了常用的意图识别技术,可以看到有很多各有优劣的技术。那么,在实践时要怎么选择呢?

一般而言,可以综合这些技术,设计一个优势互补的集成的意图识别方案,以便满足实际产品运营过程中的多种不同的诉求,比如:

  1. 如何快速(如分钟级)而稳定地解决某些特定情况(如某些 ML 模型表现不太好的例子)的表现
  2. 在达到较高准确度的同时提升响应速度
  3. 在分布外(不同于预先识别到的场景及语料)的场景下表现也要相对较好

在真实场景中落地时,可以如下结合这些技术:

1
2
3
4
5
6
7
8
9
flowchart TB
用户输入 --> 基于RAG的相似度查找
基于RAG的相似度查找 --> 是否找到高相似度文本{{是否找到高相似度文本}}
是否找到高相似度文本 --> |YES|输出对应的意图结果
是否找到高相似度文本 --> |NO|基于Embedding+MLP进行意图识别
基于Embedding+MLP进行意图识别 --> 检查模型置信度{{检查模型置信度}}
检查模型置信度 --> |高|输出对应的意图结果
检查模型置信度 --> |低|基于LLM进行意图识别
基于LLM进行意图识别 --> 输出对应的意图结果

由于 BERT 类模型可以被更简单通用的基于 Embedding+MLP 的技术替代,所以,这里就可以不考虑选择 BERT 类模型了。

这样的方案的优势在于:

  1. 通过基于 RAG 的相似度查找,可以快速(响应时间约在 300ms 左右)定位到与用户输入高度相似的历史文本,从而提高意图识别的准确性和效率。
  2. 在基于 Embedding+MLP 进行意图识别时,可复用上一步生成的文本向量,提高泛化性的同时,满足效率要求(10ms 左右)
  3. 通过上述两个步骤,希望能处理掉 80%以上的请求,即,可将 80%以上的使用场景响应时间都控制在 300ms 左右
  4. 对于另外的约 20%比较难的用户输入,则采用基于 LLM 进行意图识别,可以利用 LLM 的推理能力输出更准确的结果。这一步耗时通常较长,一般在 1~2s,但是由于只处理 20%的请求,对整体的响应时间影响较小。
  5. 对于整体上表现都不够好的用户输入,可以及时补充到 RAG 的文本库中,如果标注及时,可完成分钟级的意图识别准确度更新

可见,上述方案综合了各类技术的优劣,形成了一个具备高工程落地可行性的方案。

方案的实现

上述集成方案中的基于 RAG 的相似度查找和基于 LLM 进行意图识别两个步骤看起来难度都不算高。具备一定的调用通用大模型能力的背景即可快速完成。

下面主要分享一下工程实现过程中常常会碰到的几个问题。

语料准备

不管是在基于 RAG 的相似度查找,还是在基于 Embedding+MLP 小模型的步骤中都需要用到语料。

  • 基于 RAG 的相似度查找需要预先构建一个文本语料库
  • 基于 Embedding+MLP 小模型则需要构建训练语料及验证语料数据集

事实上基于 LLM 的技术也至少需要一个用于评估的数据集语料。

所以,语料的准备常常是意图识别中最重要也是最复杂的一部分内容。那么,在 LLM 的时代,有没有什么更高效的方法来准备语料呢?

我们当然可以利用 LLM 强大的文本生成能力来辅助快速构建语料了!

具体来说,我们可以先给出一些分类的标签及其说明,然后让 LLM 根据这些标签生成对应的语料。

例如,假设我们有一个“交易查询”的意图,我们可以使用提示词:

用户想要在系统中查询订单详情。假设你是这样的用户,有哪些可能的消息发送到智能客服?以列表的形式返回,列举20个,尽可能包含可能的各种句式、表达方式。可能涉及时间范围、金额范围、商家等。在每一行输出一个,除了消息之外,不要输出任何其他字符

把这样的消息发送给 GPT,看看其生成的语料质量如何,如果不满意则调整一下提示词继续。

在优化好提示词之后,就可以使用 API 调用 LLM 的接口来批量生成语料了。

除此之外,还可以针对搜集到的真实语料用 LLM 进行扩展,可以采用类似这样的提示词:

“{input}”在这一句话里面,{intent_def}。生成10个意思相近的表达。只需输出十行,不要输出任何额外的字符

使用这样的方法,可以快速得到成千上万的语料,然后就可以在此基础上训练模型了。

当然,LLM 生成的语料还是有偏的(跟真实语料不一致),准备一份真实语料用于验证模型表现还是很有必要的。这类语料可以不用太多,因此也相对容易搜集。

持续的语料优化

在模型上线之后,可以通过收集用户与智能客服的交互数据,不断优化语料库和模型。

例如,可以将用户输入的语句进行人工标注,然后与模型预测的结果进行对比,如果预测结果不准确,则可以将这些语句加入到 RAG 语料库中,并可以重新训练 MLP 模型。

当然,也可以用 LLM 自动改写这些识别不好的用户输入,在扩大语料范围和适用性后,再加入 RAG 语料库或重训 MLP。

同时,更进一步,可以识别这些语句的语言模式,然后利用 LLM 参考此模式去生成语料。

比如用户可能不想继续上一个意图,而提出了新意图。典型的输入比如“算了,我不想处理交易了,帮我查询一下如何申购理财产品吧”。

则可以编写提示词:

用户之前在操作功能“{last_function}”,现在办完了或者不想办理这个了,改为想要办理另一个功能“{function}”。假设你是这样的用户,有哪些可能的消息发送到智能客服?以列表的形式返回,列举10个,尽可能包含可能的各种句式、表达方式。在每一行输出一个,除了消息之外,不要输出任何其他字符

利用上述提示词可以让 LLM 批量生成此类的语料,从而处理掉一类的问题。

辅助意图的设计

由于意图识别常常基于单轮的用户输入来实现,而用户的对话具有持续性。因此,很多时候常常不能根据单轮的用户输入推断出有目的性的意图。

比如,用户想要补充信息,输入“日期就用上个月的吧”,此时是无法将输入映射到一个确定的目的性特别强的意图上去。

但是,如果结合上下文,我们就可以知道用户这是在补充信息,而当前的目的性意图与上一轮的是一致的。

因此,在设计意图时,除了设计那些目的性很强的意图之外,还需要设计一些辅助意图,用来描述用户可能的对话行为。在模型识别到此类意图时,可以通过对话的状态保持找到当前的真实意图。

此类辅助意图可以有:

  1. 提供信息
  2. 表达感谢
  3. 确认信息
  4. 否认信息
  5. 转人工

MLP 小模型的训练和部署

MLP 模型的代码实现、训练和评估,可能相对有一定门槛。

这里也可以借助 LLM 强大的代码能力辅助我们完成工作。

编写如下提示词发给 LLM,可以帮我们自动完成大部分的 MLP 编码工作:

帮我生成代码完成功能:1. 从一系列文本文件中读取文本,文件名为分类名,文件内容每一行为改分类对应的语料2. 调用bge模型对这些文本生成embedding,并将embedding缓存到一个指定的文件中3. 构建一个mlp模型,利用embedding的结果构建一个分类器,实现多分类任务,并考虑样本在多个分类中不平衡的情况4. 实现训练和eval代码

在得到代码之后,可能需要将调用 bge 模型的过程改成调用通用的模型 API。由于 MLP 模型很小,可以在普通的开发机器上完成模型的训练。

在我的场景中,训练一个包含 12w 左右训练数据的模型,采用三层隐层维度 256 的 MLP 模型,只需要 7 分钟左右即可训练完成,准确率可以达到 97%左右(取决于语料质量),而模型大小只有 1.5MB 左右。

模型训练完成之后,如何部署到应用中呢?

模型训练的代码一般使用PyTorch或者Tensorflow这类框架编写完成。但是在部署这类小模型时如果也依赖这类框架执行推理计算,则显得大材小用了。

事实上,对于这类简单的 MLP 模型,我们可以直接用如numpy这类向量计算库完成推理。

将以下提示词发送给 LLM,就可以得到numpy版本的推理代码:

我有一个MLP模型代码如下:...请编写一个函数将模型参数保存为numpy格式,然后再编写另一个函数使用numpy加载这个模型的参数进行推理预测

得到的代码稍加改动就可集成到模型意图识别的流程中了。

总结

本文介绍了在 LLM 时代如何搭建一个实用的集成意图识别系统。

通过对常用的意图识别技术手段的分析,结合各种技术的优势,取长补短,分享了一个实用的设计。以最大范围使用通用模型的能力为原则,在落地实现时,可以有效降低工作量。

另外,使用 LLM 生成训练数据和模型代码,我们大大简化了开发过程,并对系统持续演进和优化提供了思路。

外部性与程序设计

作者 Bright LGM
2024年11月23日 20:00

经济学里面有一个名为“外部性“的概念。外部性是指一个人或企业的行为对其他人或企业产生的影响。

外部性可以是正的,也可以是负的。比如,一个企业的生产活动可能会产生污染,这就是一种负的外部性,对周围的环境和居民造成了伤害。相反,一个企业的生产活动也可能带来正面的外部性,比如提高周围地区的就业机会或改善周围地区的基础设施。

外部性对于组织职责划分的启示

利用这个概念,我们可以用来分析组织的职责划分。

什么样的职责应该由下级组织负责?我们说,当某一件事没有产生外部性或产生了正的外部性,那么下级组织应该负责。此时如果上级组织非得介入,则反而会增加下级组织的沟通成本,降低效率。因为上级组织往往由于掌握不了足够的细节,而要求下级组织频繁的汇报信息。

但是,当某一件事产生了负的外部性,那么上级组织应该负责。此时,下级组织往往没有驱动力去解决这个问题,因为这会增加下级组织的成本,但是却不会增加下级组织的收益。

举个例子。比如,企业在生产过程中赚到了钱,顺便改善了周边的经济环境,创造了正的外部性,很高兴。但是对于产生污染这样的负外部性,企业就不太会在意,且没有驱动力去解决,因为这会增加企业的成本。这时,作为上级组织的政府就应该介入,协调企业和周围居民的矛盾,并负责督促企业处理污染,避免对周围环境和居民造成伤害。

外部性对于程序设计的启示

在了解了外部性及其应用之后,我发现它对程序设计也有很大的启发。

无外部性或负外部性与高内聚

其一是在类或模块的职责划分上。如果一个类或模块的行为不会对其他类或模块产生影响,那这个行为就应该让这个类或模块自己处理。我们常常说的内聚性就是这个道理。如果一个类可以基于自己管理的数据独立完成某个功能,那么这个功能就应该由这个类自己实现,而不是由调用它的类来插手。

当我们看到在某一个类的方法中直接修改另一个对象的内部状态时,就应该警惕,因为维护这个状态可能是另一个对象自己的职责。越俎代庖很可能破坏了另一个对象的内聚性,并增加了系统的耦合度。

而当一个类或者一个模块的行为对其他类或者模块产生了影响,就产生了外部性。

如果这种外部性是正的,那么我们可以说这个函数或者模块是“无害的”,无需处理,并应极力鼓励。比如,某一个类优化了内部的算法,使得整个系统的性能提高了,这就是一种正的外部性。我们应该经常鼓励这样的优化。

但是,如果这种外部性是负的,那么我们就需要特别警惕,并考虑如何处理这种负外部性。这样的负外部性常常隐藏较深难以发现。

负外部性与上层协调

举一个大家经常碰到的例子。在基于数据库的后端程序开发中,我们常常需要从数据库中读取数据构建领域对象。JPA 可以帮我们自动完成这个过程。但是,如果我们不注意,JPA 可能会产生一种负的外部性,即 N+1 问题。

N+1 问题是指,当我们从数据库中读取一个领域对象时,JPA 会自动为这个领域对象的每一个关联对象发送一个额外的查询。如果这个领域对象有 N 个关联对象,那么就会发送 N+1 个查询。这将导致性能问题。

JPA 默认在非所有者端默认使用一种叫做“惰性加载”的模式来处理关联对象。惰性加载是指,当我们从数据库中读取一个领域对象时,不会立即查询关联对象,而是等到我们真正需要使用关联对象时,再发送查询。大多时候,这种模式是有效的,因为我们可能并不需要使用所有的关联对象。但是,如果我们需要使用所有的关联对象,那么就会产生 N+1 问题。

这种负外部性很难在领域对象内部自己解决,因为它不知道调用者何时会访问到哪些关联对象。

为了解决这个问题,我们可以在对应的 repository 中显示地定义一个方法,用于一次性加载所有关联对象(或通过参数指定需要加载哪些关联对象)。这样,我们就可以在需要的时候,在调用方(上级组织)显式地加载所有关联对象。在实现时,我们可以使用 JPA 的 fetch join 特性,在查询领域对象时,同时查询关联对象。这样,就可以避免 N+1 问题。

下面是一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Entity
public class Order {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Date orderDate;

@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

}

@Entity
public class OrderItem {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String productName;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;

}

@Repository
public class OrderRepository {

@PersistenceContext
private EntityManager entityManager;

public Optional<Order> findOrderByIDWithItems(Long id) {
TypedQuery<Order> query = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id",
Order.class)
.setParameter("id", id);

List<Order> resultList = query.getResultList();
return resultList.isEmpty() ? Optional.empty() : Optional.of(resultList.get(0));
}

}

上述代码 findOrderByIDWithItems 被设计为供上层调用,其实现过程中:

  • SELECT o FROM Order o: 指定了我们要查询的主实体是 Order,并将其别名命名为 o
  • LEFT JOIN FETCH o.items: 这是 fetch join 的关键部分。这里我们执行了一次 左连接 (LEFT JOIN) 来包括所有订单。这意味着当你遍历结果列表中的每个订单并访问其 items 集合时,不会触发额外的数据库查询,因为所有必要的数据都已经在初始查询中被加载了。
    在发现 N+1 问题时,如果我们想在领域对象内部解决这个问题就很困难。此时应该改变思路,通过提供接口让上层组织中显式地调用,这个问题就迎刃而解了。

总结

外部性这一经济学概念在软件设计中也有鲜活的体现。通过识别和分析系统中的外部性,我们有以下启示:

  • 无外部性或正外部性:类或者模块的行为对其他部分没有影响(无外部性)或产生积极影响(正外部性),则其职责应尽量内聚,由自己处理,并鼓励正外部性优化。
  • 负外部性:行为对其他部分产生了负面影响,需要通过上层组织进行协调、解决。通过显式地提供清晰的接口,上层组织就可以灵活处理问题。

每日一思

作者 Bright LGM
2024年8月1日 04:00

2024-07-31: 最后一天

团队早会,轮到他更新昨日的工作内容。

如往常一样,他保持着端正的坐姿,双手放在桌子下面。转头面向电视会议屏幕,身体往前倾。

“嘿嘿嘿”,他短暂地停了约有 0.5 秒钟,咧着嘴,“嘿嘿嘿”。

“我昨天没什么更新,在整理交接的文档。”他停了约有 1 秒钟,“但我这边也没有人可以交接。”

“我这边今天最后一天了,以后的测试就靠大家自己了。”

会议室很安静,一点声音也没有。一秒,两秒。

主持人终于切换到下一个人。

明天,他的座位就空了,每次早上一到办公室的时候都能见到的身影从明天开始就见不到了。

明天,他的座位就空了,经常下班还能瞥见到的忙碌的身影从明天开始就见不到了。

以后,非工作时间的会议也总是能保持准时参加的身影也见不到了。

每日一思

作者 Bright LGM
2024年7月26日 04:00

2024-07-25: T恤衫

打开衣柜,在叠起来的一摞 T 恤衫里面翻找,一件自己买的,一件公司文化衫,又一件公司文化衫,还有一件公司文化衫。

穿什么呢?抽出一件背过来叠起的黑色 T 恤,打开一看,是公司文化衫。这一件应该有三年以上了,前面印的 disruptive thinking 及机器人图案已经开裂并快要掉落下来了。

旧了。算了,换一件吧,我把它放了回去。正准备开始继续找,又犹豫了,还是这件吧,不知道还能穿几回!

穿上这件文化衫准备出门,在门口的镜子面前停了一下,衣服上面的图案的色彩还是鲜艳的。

每日一思

作者 Bright LGM
2023年12月31日 04:00

2023-12-30: 他不害羞

电梯里,他睁着大眼睛,抬起头,目不转睛地望着旁边的阿姨,一直望着阿姨,一直望着阿姨。我们都看着他,忍不住笑。他突然开始朝着阿姨笑,没有发出声音。阿姨也看向他,看他望了很久,也开始对他笑,“小朋友真可爱!”

电梯里,几位叔叔阿姨一起同行。电梯门开了,叔叔出电梯了。“叔叔拜拜,叔叔拜拜!”他对着电梯门大声说。电梯门又开了。“阿姨拜拜,阿姨拜拜!”电梯里面进来一个人,阿姨没有下电梯。姥姥说,“阿姨还没到。”

电梯门开了,我们到了。我们走出电梯。“叔叔拜拜,阿姨拜拜,爷爷奶奶拜拜!”

每日一思

作者 Bright LGM
2023年12月25日 04:00

2023-12-24: 童言

“小鸡怎么叫?”“小鸡叽jī叽jī叽jī。”

“小狗怎么叫?”“汪wàng汪wàng”

“大公鸡怎么叫?”“咯gé咯gē咯gé。”

“母鸡怎么叫?”“母鸡咯gé咯gē哒dà。”

“关guán门啦。”在看了很多遍小兔子乖乖的视频之后,他经常这样说。

有一次,家里人围坐在一起教他说话,一个人说,“妈妈好棒”,另一个人说,“妈妈好笨”,到他嘴里,最终变成了“妈妈好beng!”

平淡与不平淡

作者 Bright LGM
2023年12月17日 20:00

早上送完人回到家,快两岁的小孩子已经起床穿好了衣裳,家里人正在准备早餐。

“要吃饼饼”,小孩子指着一袋子蛋卷说。看看给他准备的牛奶马上就要好了,就对他说,“我们先喝牛牛好不好,喝完牛牛再吃饼饼”。小孩子一改以往得不到就吵闹的样子,嘴上说着“喝完牛牛再吃饼饼”,往摇奶器那边走过去。

安安静静地喝完了牛奶,热好的烧麦和煮好的鸡蛋已经上桌。我叫上小孩子,“要不要吃包包?” 小孩子走过来,“包包好吃。” 我一边吃烧麦,一边喂给他。小孩子一改以往吃东西乱吐的习惯,开始大口吃起来。

吃完了大半个烧麦,我剥开一个鸡蛋,问他吃不吃。小孩子笑一笑看着我,“蛋波波好吃”。先喂他吃一口蛋白,再挑出一瓣蛋黄给到他。小孩子一改以往吃东西落满地食物的习惯,今天照单全收了,地板上甚至很难找到掉下来的残渣。

吃过饭就和姥姥出门了,回来已经11点。姥姥买回来一袋水果,说,“椪柑应该还好吃。” 我剥开一个准备尝一尝,小孩子也凑过来想要吃。我剥开一瓣给到他,他还是像以往一样想要双手拿一大把。我对他说,“水果一次只能吃一瓣,吃完才能吃下一瓣”。他重复我的话,然后把多余的给我,开始吃起来。吃出了一个桔子核,小孩子一改往日随地就吐的习惯,说,“桔子核要丢垃圾桶”,然后跑到垃圾桶旁边把桔子核丢了进去。

早上的时光还剩一点,小孩子玩起了识字挂图,开始点按里面的人物。“科学家,画家,服务员,老师…”,识字挂图开始发出声音。我走过去问他科学家在哪里,“在这里啊”,他说着并把手指向科学家。我又问了画家在哪里,他竟然也指对了!我满脸的惊诧。

玩了大约20分钟的识字挂图,他又去找其他的玩。这时姥姥出门丢垃圾回来,小孩子就跟着姥姥进了卧室。我在沙发上休息。过了一会儿,姥姥也到沙发上休息。突然我一晃神,怎么这么久没有听到小孩子的声音了。赶紧走过去想看看小孩子在干啥。一过去就发现,原来小孩子把房门钥匙拔下来了,正在想办法把钥匙插回去。他身高还不够,尝试了好几次都插不进去。有几次眼看要插进去了,最后还是滑到了其他地方。小孩子一点声音也没有发出,努力地尝试要把钥匙插回去,努力地尝试要把钥匙插回去。

到了午饭点,小孩子还没有插好钥匙。“快过来吃莽莽,吃完莽莽再来插钥匙好不好?” 小孩子犹豫了一会儿,就过来开始吃午饭。午饭依然很顺利地就吃了一小碗。

吃完午饭,是小孩子午睡的时间点了。我坐在椅子上,闭目养神,跟他说,“睡觉觉好舒服,睡好觉觉精神好好。你想不想睡觉啊?”小孩子起初还有点抗拒,等了一会,看我想要休息的样子,他也跑去找到姥姥,说要睡觉。然后要姥姥帮助脱下鞋子和袜子。脱下鞋袜之后,他就开始自己哼着歌,“嗯…嗯…嗯…嗯…”,这稚嫩的声音混合着姥姥的“哦…哦…哦…”,组成了一首绝佳的催眠曲。时间也就过去了几分钟,他就进入了梦乡。

不知道他的梦乡甜不甜,反正我今天已经感动到不写完上面这些文字就睡不着觉了。

转念一想,带小孩其实跟项目交付很像。有些项目交付,在表面上看,也许就只是如同今天小孩的表现一样,平平淡淡,普普通通。但是在这背后,十里地开外,也许就是波涛汹涌的洪水。如果不是身在团队中间,怎能知道这平淡和普通其实是由于团队建立起了一座阻挡洪水的高高的堤坝?

每日一思

作者 Bright LGM
2023年12月12日 04:00

2023-12-17: 传承

“搞破坏,搞破坏不乖”,在跟他说过这些之后,这几句成了他这几天的口头禅。

“瞎搞”,偶尔也能从他嘴里冒出来。

很久以前,每次在他准备打开平板电脑的时候,都会跟他说,“这个搞不得”。现在,当他发现平板电脑在旁边,想去玩一玩的时候,只要我在旁边,他都会重复之前说过的话:“这个搞不得”。

“拉粑粑要跟妈妈说”,虽然他现在还是每次拉完粑粑都不说,但是每当我们问起他为什么不说的时候,他总是重复着上面这句之前跟他说过的话。

传承,是个很奇妙的过程。

2023-12-16: 他其实学会了

和家里人一起在公园游玩,小孩子推着他自己的婴儿车往前走,这是他一直很喜欢的事情。

突然,地上有一个坑,挡住了婴儿车的轮子,推不动了。他望着姥姥说,“姥姥帮忙,姥姥帮忙!” 姥姥一脸惊诧,“哎呀呀,姥姥帮忙?真能干啊,什么时候学会叫姥姥帮忙了?什么时候学会的啊?这没人 …

2023-12-14: 又好气又好笑

家里小孩很喜欢吃蛋卷,但是有个坏毛病是喜欢每次拿一大卷,在吃的时候又总是免不了残渣掉满地。

一天中午,我特意把蛋卷掰成了一小块,递给他,小伙子用充满稚气的话说,“好大坨!” 小孩妈妈在旁边捂着嘴,禁不住地笑出声来,“哈哈哈,哈哈哈,好大坨!”

从此,小伙子记住了,“好大坨”这个词很好笑。后来很多次,他玩着玩着就突然冒出来一句:“好大坨”。

2023-12-12: 难得的安静

早上送完人回到家,小孩子已经起床穿好了衣裳,家里人正在准备早餐。

“要吃饼饼”,小孩子指着一袋子蛋卷说。看看给他准备的牛奶马上就要好了,就对他说,“我们先喝牛牛好不好,喝完牛牛再吃饼饼”。小孩子一改以往得不到就吵闹的样子,嘴上说着“喝完牛牛再吃饼饼”,往摇奶器那边走过 …

2023-12-11: 他也要做决定

今天天气不错,一阵微风吹散了笼罩城市多日的雾霾,太阳也从云层中探出头,把光明撒向大地。

去晒个太阳吧,这是个好主意。午饭之后,待孩子睡了一个午觉起来,家里人就开始收拾东西,尿不湿、水、各类纸巾、零食等等。我还特意剥了一个特别难剥的柚子。

好了,一切收拾妥当,准备出发。

小孩子快两岁了,跟着我们迈着小步子朝电梯走去。到停车场了,准备上车。爸妈把门拉开,示意小孩上车。“不上车,不上车,不坐车车,不坐车车”,小孩突然开始吵闹起来,一靠近车子就把身子往外面扭。咦?这是怎么回事,以前坐车都好好的,怎么今天突然不愿意坐车呢?

爸妈开始哄孩子,“乖,快上车我们去晒太阳,外面天气好好哦”,劝了好半天,小孩就是无动于衷。

等了好一会儿,小孩还是不愿意上车。实在没办法,大家只好收拾东西折回家去。

到了两岁的年纪,小孩子也想要自己做决定了。

每日一思

作者 Bright LGM
2023年9月12日 04:00

2023-09-16: 没有成行的钓鱼

有一个漂亮的湖,湖水里经常有会飞的野鸭子出没,你明明看到在某一处有一只野鸭子,一转眼它已不见了身影。你开始睁大眼睛四处找寻,过来好一会儿,终于在几十米开外的地方突然发现有一个鸭子头从水里钻出来。

湖边是绿油油的草坪,这里几乎没有人,你可以肆意的进去踩一踩松软的草坪,甚至坐一坐,或者打个滚,丝毫不会影响小草的生长。

湖边的树木也是经过精心设计的艺术,一大株一小株相间排列,品种各样。小鸟禁不住诱惑,高兴地在树丛间飞来飞去,叽叽喳喳似乎在向同伴述说自己的意外发现。

有一次,我和家里人在湖边散步,看到有几个年长的人在钓鱼。他们不紧不慢,悠闲的坐在湖边,水里面的线很长时间也没有动一下。但是他们不在乎,好像只是在等着那些愿意上钩的鱼儿。这让我想起了儿时钓鱼的场景,那会儿钓鱼可是我们几个小朋友最大的爱好。

我说,我也要找个机会来钓鱼。

第二次,看到类似这样的场景,我说,一定要找个机会来钓鱼。

第三次,我也这么说过。

现在,时间已经过去了一年有余,钓鱼终也没有成行。也许再也不愿意花几个小时安安静静坐在湖边了。

2023-09-15: 一个管理命令行工具的工具

开发人员在开发阶段常常需要执行很多自动化的shell命令,以帮助完成测试和调试工作。

虽然执行命令已经很快了,但还是免不了需要手动输入命令名称以及命令参数。特别是在命令和参数非常多的时候,记忆负担变得更重了。

我们实现的开发工作台(data-workbench.com)引入了这样一些方法来解决这个问题:

  • 提供一种方式将这些常用的命令分组并添加描述

  • 提供多种访问和执行这些命令的入口,如网页版搜索、ide插件版快捷键等功能

  • 在上述这些入口中,提供自动参数校验,参数历史记录等便于使用的功能

有了这些功能,不仅记忆负担大大降低,而且方便了团队里面大家共享这些命令行工具。团队效率得到极大提升。

2023-09-14: 一个开发者工具的出现

为了能及时获得开发反馈,我们开发了一个工具,将easysql编写的etl转换为impala可执行的sql代码。它给我们带来了极大的便利,定位olap引擎的impala可以非常及时的告诉我们代码是否有问题。

但是,改进似乎是没有尽头的。工具虽然好用,但是是以命令行的形式调用的。于是,为了能获得这样的开发反馈,总是需要手动的执行命令,拷贝代码,然后粘贴到impala的sql执行工具中执行一下。每改一行代码都需要重复这个工作。总是感觉很繁琐。

然后,我们又在酝酿下一次工具的迭代升级。思路非常简单,把之前需要手动完成的操作进一步自动化,使得开发者可以一键获取反馈,或者更进一步,自动在后台给我们提供开发反馈。

道家说,道生一,一生二,二生三,三生万物。回过头来看这样的工具演进路线,一个之前完全不存在的工具,似乎正在变得越来越丰富和完善。这可能是软件的发展之道。

2023-09-13: etl代码静态检查

作为数据开发的专用dsl–sql,它与其他编程语言存在着一个显著差异:其正确性严重依赖于外部环境。这带来了一些问题:

  • 无法在编码时静态检查字段有没有写错。一个字段在当前没有,不代表后续建表的时候不会重新加上;一个当前已存在的字段也可能在运行时被删除了。

  • 无法检查潜在的类型转换问题。比如,当你在按照数字类型处理数据时,如果实际上该类型为字符串,就可能由于类型隐式转换导致大量空置。

这些基本的错误常常在代码执行时才暴露出来,反馈循环太长,开发人员因此效率低下。

因此,数据开发迫切需要一个能实时连接元数据库进行静态代码检查的集成开发环境。目前有很多数据库专用开发工具可以给到这样的开发体验,比如Oracle提供的sql developer。但支持spark或hive这类大数据引擎的目前还比较少。

一个简单的实现思路是:

  1. 将sql代码转换为基于cte的一条查询语句

  2. 在最后添加一个用于输出结果的占位语句,如select 1

  3. 在查询引擎上面执行这个查询,并分析报错结果,然后映射回对应的代码行

2023-09-12: 想念

一位很久没有打电话问候的奶奶,突然入梦,慈祥的脸上还是洋溢着那般笑容。

她不愿意到城市里面和我们一起生活,一个人在农村老家待得自在。

上次回去,家里显得空空的,大大的堂屋里面只有一张陈旧的小桌子和两张长条凳。但是,她一个人在家,也要收拾得干干净净,桌凳上、地上、墙壁上没有一点灰尘。

上次回家带上一岁儿子一起,一开始,小孙子还认生,不愿意跟太奶奶亲近。太奶奶很想和小孙子抱一抱,张开双臂,嘴里温和的念叨着要抱一抱,还比着手势。小孩子纠结了好一会儿,最后笑着投入太奶奶的怀抱。

在老家,有人气的家里就会受燕子的青睐,上次回去,墙壁上又筑好了两个燕子巢。

2023-09-11: 影响

跑步,总是能碰到跑在我前面的。遇到实力差不多的,总是忍不住想要跟上去,从而不自觉地加快脚步。

图书馆,看到满座都是捧起书津津有味吸取知识精华的人,总是忍不住也伸手拿起一本,沉下心来,享受阅读的乐趣。

景区,附近一个停车的地方,一位当地的老乡说,这是他们自己家修的,不收费。附近的几处停车的地方也没有收费。

景区,你随手捡起地上的塑料垃圾,准备收集起来处理掉,避免污染环境。旁边也有人效仿你开始捡起其他的垃圾。

泰戈尔说:把自己活成一道光,因为你不知道,谁会借着你的光,走出了黑暗;请保持心中的善良,因为你不知道,谁会借着你的善良,走出了绝望。

这就是影响的力量!

每日一思

作者 Bright LGM
2023年9月5日 04:00

2023-09-10: 这味道,停不下来

一锅香气四溢的辣子红油汤上桌,每人一个浅腹平底碗,装满调料。

服务员过来打汤,将一半敞口,一半过滤网的汤匙浸入辣子红油汤锅,旋转勺子底部撇开部分红油,舀起一大勺汤,过滤,倒入调料碗。

夹起一只美蛙,放入调料碗中,来回旋转几次,彻底浸入调料汁。拆下一块肉,送入口中。一辣一麻一香,以肉的位置为中心,瞬间四溢到周围的味蕾。活动牙齿,咀嚼一下,鲜嫩的蛙肉一下子散开。散开的每一小块都味道十足,带着劲道的汤汁无情地席卷整个口腔。

辣,刺激着口腔发痛;麻,让每一个口腔细胞都开始跳舞;香,带着令人幸福爽快的小分子穿越口腔和鼻腔直达脑门。

十足的辣让人想赶紧把食物咽下去,十足的麻和香又让人想把食物一直留在口中。于是,在辣味还未来得及传递到大脑时,赶紧咀嚼几次,得到美味的享受之后,辣味也快速到来,赶紧吞咽下肚。然而,大脑哪里能抵抗这种美味,立即指挥手开始拆下一块肉送入口中。

哪管他会不会长肉!哪管他是不是健康!这味道,停不下来!

2023-09-09: 宗教与诗

在很多西方人看来,每个人都应该信仰一种宗教。但是中国却很特别,一个具有数千年历史的文明古国竟然没有一个广泛信仰的宗教。

中国人如何处世?引导中国人一直向前的可能是诗。从最早的六经,到唐朝的绝句,到宋朝的词,到元朝的曲,再到现代的白话诗。

诗中的故事和情感,感动着一代又一代的人。诗中的哲学和世界观,引导着一代又一代的人。在彷徨时,诗给人以一束穿透迷雾的光;在失意时,诗给人以一种从容向前的动力;在成功时,诗警醒人前路依然不平坦。

宗教,虽然充满理性和逻辑,但总少不了神秘主义色彩。相比起来,诗是作者的想象,是脱离现实而高于现实的,这是读者在读诗之前就知道的。同时,诗中有大量的留白,需要读者去填补和想象;诗中有很多道理,需要读者结合自己的经历去体悟。

这可能就是为什么中国人的哲学既是出世的也是入世的,既追求内圣也追求外王。

2023-09-08: 负能量

小a上项目半天,说,我感觉很懵,别的团队上新人会有技术的业务的各类onboarding,这边都没有。

公司人员缩减,小a说,这两天上班心都悬着,别什么时候被约谈了。

小a遇到了一种新的语法规则,说,这个怎么这么奇怪,完全反人类,我是怎么都理解不了。

小a碰到一个不熟悉的工具,说,这个怎么这么难用,感觉很难理解,看不懂。

2023-09-07: 道德的本源

道德是一个非常抽象的概念。根据当今时代的理解,道德是社会意识形态之一,是人们共同生活及其行为的准则和规范,通过社会的或一定阶级舆论对社会生活起约束作用。(百度百科)

为什么道德是规则和意识形态?要追寻其最初的本义,需要回归老子的道德经。

经上说,“道生一,一生二,二生三,三生万物”。从这里来看,道是万物之源,并且,万物在生成过程之中,也都有“道”在其中。但是初始的“道”不同于万物之“道”,因此,“道可道,非常道”。

什么是德?在万物之中的“道”就是“德”, “德”的含义是“能力”或“品德”,它可以解释为万物本有的品质。“万物莫不尊道而贵德”, “道”是万物的由来,“德”则是万物本性的依据。

当今我们所指的道德更近似道家的“德”,即人的本性,本性具象化就是一些规律,延伸出来就是规则。

2023-09-06: 他乡遇故知

想象一下,你在一个陌生的城市,正在为生计做着事,突然一抬头,发现一个多年未见的朋友,恰好,他也看见你。

“xxx”!“xxx”!你们兴奋而又惊讶地相互叫出名字。

相约去大餐一顿。饭间,以前一起经历的往事,搞笑的尴尬的,变成现在的笑谈…

人类的情感就是这么奇妙,一次偶然的遇见,一段尘封的往事,就足以令人潸然泪下。

2023-09-05: 稳妥的技术路线

今天被一个hive分区问题给坑了。

问题表现非常奇怪,使用create table as select *…创建的表居然与原表数据量不一致!

调查了很久,发现了一些端倪:

  1. 原表为分区表,采用insert overwrite以动态分区的形式创建

  2. 查看底层文件系统发现,原表有一些分区列值为空的分区,以及分区列值为 hive__default 的分区

  3. 直接查询原表按照分区统计数量,不会出现上述不合法的分区

  4. 对新表(未分区表)按照原表的分区字段统计数量,出现了值为空及 hive__default 的数据

通过这些现象,可以了解到,可能由于之前某一次运行etl产生了一些脏数据。由于insert overwrite在动态分区场景下不会覆盖没有出现过数据的分区,所以之前的脏数据也一直被保留了下来。

在技术选择上,我一直推荐采用保守而稳妥的策略。事实上由于软件已经过于复杂了,如果再引入没必要的复杂度,那就很容易导致问题。比如,避免上述问题的一种稳妥的策略是,先truncate或drop table清除所有数据,然后再写入新数据。

一些其他常见的稳妥技术选择包括:

  1. 采用最简单和通用的语法,避免采用过于风格化的编程语言语法(如Scala过于复杂的类型推导)

  2. 采用最成熟的api,比如写在库或工具的上手文档中的那些api

  3. 保持实现的幂等性(类似纯函数),尽量隔离副作用

2023-09-04: 成就自己与成就他人

儒家之仁有两层意义。

  • 一是忠,即:己欲立而立人,己欲达而达人(《论语 雍也》)。

  • 二是恕,即:己所不欲,勿施于人。

第一点的忠,并不是愚忠(别人叫你干什么就干什么)。而是指,自己想要成功,就需要真心实意帮助别人成功。

当今社会很多人为了自己的成功不择手段,损害他人利益,这显然不是一种可持久的方式。对己难以保持平和,对人难以受到信任。

事实恰好在于,如果帮助他人成功了,自己往往也会成功。这就是:成就他人即成就自己。

每日一思

作者 Bright LGM
2023年8月29日 04:00

2023-09-03: 生命不能承受之重

汽车突然刹停,“嘭”,一个响亮的声音传入车厢里的几人耳中。

“哇!”接着传来小孩的哭声。“遭了,这下遭了!”后排的人嘴里说着话,赶紧抱起摔倒的小孩。

大家开始检查小孩的伤势,鼻子旁边有擦伤,在流血,另一只鼻子也隐隐流出血来。

事发之前,小孩站在后排中间座椅上,刹车时,被惯性带着往前倾倒。由于正对着空调出风口,直直的撞了上去。

这是多幸运?只是一点擦伤!如果没有这么幸运,那可能就是生命不能承受之重!

2023-09-02: 代码行数的错觉

最近进行的是一个数据开发重构项目,最重要任务之一是改善先前代码的质量。

经过几周时间的分析和实践,我们发现核心问题是代码重复。

团队随处能看到一大段一大段的重复代码,一个800行的sql代码消除重复之后可能只有不到200行。

消除重复带来的益处非常明显:

  1. 原来的数据计算逻辑清晰地呈现了出来,代码更容易理解,调查问题更快了

  2. 不少隐藏的bug被发现并修复

  3. 取数逻辑的一致性得到更好的保证

代码行数一直是程序员工作量的一个参考指标,不少企业甚至为每个员工设定一个代码行数的目标。因此,很多开发人员为了达成这个所谓的目标胡乱复制粘贴代码,最终导致了低下的项目质量。

这其实是关于代码行数的错觉。

事实上,在代码量指标上,不仅不应该求多,反而应该求少。能完成同样功能的代码,当然是越少越容易维护。

2023-09-01: 数据流水线自动生成

在很多数据项目中,数据流水线的配置都是靠团队手工完成。这带来了很大的工作量(特别是在开发阶段),而且容易出错。

事实上,数据流水线完全可根据etl血缘关系自动生成出来。

我们在团队中这样做:

  1. 采用airflow这样的可通过代码定义流水线的调度工具

  2. 自动解析etl文件中的依赖表,并找到对应的etl文件

  3. 根据以上依赖关系自动生成etl依赖图

  4. 将此依赖图转化为流水线代码

自动化数据流水线的生成和管理给我们带来了极大的便利,节省了团队大量的时间。团队变得更高效和敏捷了。

2023-08-31: 核污染水的数据与逻辑

日本核污染水排海事件正在成为国际关注的焦点。关于这件事有很多说法,比如日本自私论,美国阴谋论,中国过分反应论等等。

日本举出了很多数据说明核污染水无害。然而中国的逻辑很简单,如果无害就没必要排海,如果有害就更不应该排海。

在这件事上,我们的态度应该是保守的,因为它关乎全人类的健康。同时,在这件事上,过分相信数据那是极危险的,因为数据极容易作假,特别是这样复杂的专业领域,漏报一项数据就可能让性质完全不一样。

这件事上更应该相信的是简单的逻辑,中国的逻辑就是简单而显然的。

在关乎健康的重大问题上,中国是偏保守的,这让我们很放心,感觉更安全。为什么要拿安全去冒险?钱是可以赚的,安全问题发生了,就无法回头了。

2023-08-30: 无处藏身的bug

业务应用开发中大家常常遇到bug,有些bug隐藏很深,甚至连专业的qa都没法发现。比如某些并发场景下出现的bug,某些极少出现的边界场景、需求未定义的场景下的bug等等。

但是在数据开发中,这些bug往往无处遁形。究其原因,有:

  1. 数据开发通常要并行处理大量数据,并发问题极易在开发阶段就暴露出来

  2. 数据开发通常要处理生产系统积累的全部数据,其背后覆盖了几乎所有业务场景

  3. 数据开发以专用的sql语言为主要开发语言,如果代码本身有问题,从结果上很容易发现(比如结果集数据量不对,统计上不符合业务直觉等等)

因此,数据开发人员在编写代码时应当极为谨慎,因为bug很容易暴露出来,而出现bug后也得由自己处理,总也跑不掉。

2023-08-29: 自动化一切

如何让团队变得更敏捷,也许应该从自动化一切开始。

自动化大部分工作之后,团队的各类开发规范就不至于只停留在纸面上,而是通过工具自动化得到保证。团队也不会因为一些低级错误而耗费大量的时间。有了自动化加持,团队可以集中精力解决重要问题,从而实现效率更高、质量更好。

就像先进机器是工业化时代的生产力一样,自动化工具就是敏捷团队的生产力。

如何推进团队工作自动化?要点在于在团队内部建立这样的意识,当每个团队成员在完成开发任务的时候都想着是不是可以把一些工作自动化的时候,团队的自动化水平就会越来越高。

自动化一切要求团队成员具备很强的技术基础,不过,这正好是技术人员努力前进的方向。

2023-08-28: 诱惑

小孩到了一岁半的年纪,吃饭慢慢变成一件令人头疼的事情。

勺子自己拿不太稳,同时专注力也不够,吃两口之后就动来动去,上蹿下跳。

家里人没办法只能喂饭,喂一口,又跑了,得跟着追。有时候喂快了没吃完,或者他只是单纯抗拒食物,到嘴里面了,也直接吐出来,吐一地。

最近发现一个特别有效 …

每日一思

作者 Bright LGM
2023年8月15日 04:00

2023-08-20: 在紧张中创造

反思一下个人的内容产出频率,我注意到一个很有意思的现象:往往工作最紧张、压力最大的那一个阶段,产出却是最多的。

为何?紧张的时期,大脑一直处于思考的状态,所以新的想法就很多;当渐渐进入到一个平稳的时期,创造力似乎就渐渐离我远去。

为什么春秋战国时期有百家争鸣,各类新思潮层出不穷?大概是因为那个时代是一个局势紧张的时代,各诸侯国之间战事不断,不仅要求发展还求如何得人心。

古希腊半岛位于各个文明圈的中央,也正是由于和周围的城邦不断的进行交流和碰撞才产生了西方哲学的萌芽。

所以,站在人类的历史长河看,有竞争的乱世也并不是一件坏事,因为它常常是破旧立新产生突破的时代。

2023-08-19: 记忆里的人

在记忆里她一直是一位很慈祥的母亲。

她很会做饭,曾经在一个山清水秀的煤矿厂里给厂子里面的人做饭;数次春节到她们家,也总是能有一大桌子菜等着大饱口福。

她也很慷慨,每次春节到她们家总少不了一个大红包;平时去也是各类零食水果从不间断。

她勤劳且持家,每次去她们家总是能看到干净整洁的房间;只要她在,总是在忙里忙外收拾这准备那。

很多年没常见面了,以后回去也见不到了。

2023-08-18: 半梦半醒

我左手抱着小孩,有一种沉甸甸的感觉。他今天很安静,没有吵闹。客厅的墙、过道、地板看起来都非常的清晰。我没有戴眼镜。家里人都正常在家休息,有的在沙发上半躺着,有的在房间其他角落走动。

我能清晰的知道这不是现实。但眼前的事物是那么的清晰和真实,即便在我的身体自然移动时,这些事物也可以正常地跟随切换。

我想挑战一下这自动在脑海里出现的场景,看看它的能力极限在哪里。

走进房间的过道,进入一间卧室。没错,是我们家的卧室!床和柜子的摆放都是没问题的。甚至连门上面的贴画上面的字都清晰映入眼帘。

穿出过道,进入了一个黑黢黢的拐角处,朝房间里面望去。这好像是好几年前的住过的房间。床和家具是深色木质的,被子铺在床上。看起来是被人直接在床上面躺过而没有将被子展平,有很多褶皱。衣柜的一边柜门贴着一张四方形的福字贴,里面有老婆很久以前写的八个字,字非常清晰。和我有关,读后感觉很感动。这个场景渐渐勾起了我更多的回忆。。。

2023-08-17: 程序员终极提效工具

最近的数据开发项目中,我们引入了一个etl代码编译解析工具,它可以自动分析代码为语法树,并从中提取代码相关信息。

然后,我发现有很多日常开发需要手工完成的事情都可以用它来自动完成,比如:

  • 自动生成建表语句

  • 自动生成数据血缘分析图

  • 自动提取输入表、输出表,辅助生成输入输出处理代码

  • 自动代码格式化

  • 自动重构

  • 自动提示待重构的代码

-…

有了语言级的信息支持,有一种思路一下子被打开的感觉。随着一个一个工具的实现,工作效率也是蹭蹭蹭往上涨。

程序员的终极提效工具是什么?除却GPT这种自动写代码的黑科技之外,那可能就是编程语言级的自动分析优化和代码生成了。

回想很多语言提出的元编程思想(比如rust),其实与这里的解法一致,不过它们大多由官方提供了支持。

2023-08-16: 当学习成为负担

最近团队在专业业务领域知识积累不太够,于是我们组织大家一起学习。

由于学习通常在工作时间之外,这势必会占用一些工作之外的其他时间。

这就导致了一个矛盾,团队成员可能不得不放弃一些个人安排,投入更多时间到学习中。

看起来学习成为了团队的一种负担。

但是,学习真的是负担吗?

回顾一下之前的学习过程就可以知道,学习常常能给人一种充实感。学到新知识了,我们会感觉获得更多能力了,变得更强大了。所以,学习常常是获得快乐的一种方式,而不是负担。

为什么会有人觉得是负担?可能是当代学以致用的观念胜过了学以致知。当我们将自己定位为一个学习的人,成长的人,坚持终身学习,把学习作为人生的一种状态,它也就不是负担了。

所以,是不是负担,关键在于个人的心态。

2023-08-15: 二级混沌系统

准确的预测可以帮我们更好的计划。比如,预测明天的天气,可以帮助我们计划旅游目的地;预测将来的行业趋势,可以帮助我们更好的选择职业。

随着科技的进步,越来越多的系统的原理被揭示出来,变得可以被准确预测。

然而有一类系统不可被准确预测,那就是二级混沌系统。

比如股市的预测,如果有一个方法宣称它可以准确预测股票的走势,那大家就可以完全根据它的预测结果进行股票交易。其结果反而将推翻之前的预测。

在这类系统中,预测会受到本身预测结果的影响,因而不可能准确预测。这就是二级混沌系统。

类似的系统还有很多,比如物理学中的测不准原理,政治预测,地图上的拥堵预测等等。

当下流行一句话,我预判了你的预判,这是指人思维也具有这样的特性。

在这样的系统中,一个科学的做法是什么?可能是要有多种完全不同的预测方法,然后不同的人选择不同的方法来做预测。这样一来,就有可能大家共赢。

2023-08-14: 动态的制度

在一个组织中,一项制度的确定一定是为了解决某一类问题,其初衷通常是好的。其操作过程也常常是因为得到大多数人的支持而形成。但制度常常解决某些问题却引出来另一些问题,这就是上面的政策引出来的下面的对策。

所以,制度的出台应该着眼于解决现实问题。但制度却不应该是死的制度,应该根据情况随时调整。对待调整的制度的态度应不亚于最初制定制度的态度,否则制度往往流于形式或导致更严重的问题。

钱穆在《中国历代政治得失》中说,应该用制度迁就现实,而不是现实迁就制度。直接生搬硬套拿来的制度不一定是好的制度,因为它没有着眼于具体的现实问题。而从汉唐一成不变的制度最终导致没落,又可说明制度需常改常新。

每日一思

作者 Bright LGM
2023年8月9日 04:00

2023-08-13: 会计准则与领域词典

财务是一个很有意思的部门,它的职责是记录和管理企业内部的各类经济事务。由于企业内部几乎所有活动都要和钱相关,因此也都需要和财务相关。故而,企业几乎所有业务活动所涉及的(高层次)知识都将流入财务系统,财务系统事实上成了企业内部的知识中心。

这对于软件开发有什么启示?

领域驱动设计建议我们逐步构建一个领域词典,以便于让大家对于系统的理解达成一致。如何构建这个词典?

企业财务管理需要符合国家会计准则,而国家会计准则则对于社会上的各类企业业务做了较为明确的高层次划分及规范制定。比如,会计科目中关于原材料采购业务定义了这几类相关科目:银行存款/应付账款/应交税费用于钱款流转事务,在途物资/原材料用于物品运输与仓储事务,生产成本/制造费用/管理费用则用于和原材料相关的各类生产消耗事务。从这里所涉及到的一些术语可以了解到会计准则中的知识几乎覆盖了企业所有核心业务过程。

基于以上分析,领域词典是不是可以基于标准的会计准则中的知识来定义?

会计准则作为一个国家颁布的企业经济行为标准,经过了社会广泛调研,可以覆盖绝大多数的关键场景,并且相关的解释和研究非常丰富,实在是不可多得的材料。

虽如此,会计准则之于领域词典也有局限。因为会计准则中的知识是仅停留在高层次,其应用范围也将限于高层次(比如架构、系统对接接口等),细节或较低层次的内容则还是应该根据具体系统相关的业务确定。

2023-08-12: on中的条件和where中的条件

sql语言的表达能力非常强,但在某些场景下看起来功能相同的代码却有着非常细微的差别,稍不留神可能就被坑了几个小时的调试时间。

比如,某一个条件写在表的连接条件中和写在连接后的where条件中有什么区别?

示例如下:

  • 写在on:select * from a left join b on a.id=b.id and a.cond=1

  • 写在where:select * from a left join b on a.id=b.id where a.cond=1

思考一下,第一个的结果集里面会出现a.cond!=1的数据吗?

2023-08-11: 用编程语言的性能提升你的效率

最近项目中需要搜集一个公开产品的文档,以便快速为团队补充相关知识。通过实现一个简单的爬虫,解析网页,获取信息就可以支持。

第一个版本,我采用了最近用得最多的python实现。代码很快写完,但是一运行就发现了一个较大的问题,网页解析太慢了!一个20MB的网页需要解析十分钟,而这样的网页总共有好几百个。没想到,本来以为可以快速完成的任务,结果输在了程序性能上!在Python这条路上折腾好长时间,无果。

由于nodejs天生就对网页解析和元素查找很擅长,所以,考虑改为nodejs实现。于是,花了一小时左右,程序搞定。上真实网页一测,性能简直不要太好,20MB的网页解析只用了不到5秒。于是这个任务在采用nodejs重新实现之后,顺利地在两小时内完成了。

我们在开发程序的时候,一般都先入为主采用自己最擅长的开发语言,希望通过熟练度来提升效率。但是每一种开发语言都有其擅长和不擅长的领域。了解这些,就可能可以通过编程语言的性能带来颠覆式的效率提升。

2023-08-09: 基于代码复用的数据工程

业界普遍采用以sql为主的数据开发语言进行数据开发,依靠数仓分层来避免重复计算,这一复用方式可以称之为数据复用。但数据复用的灵活性较差,可能会形成一个非常复杂的数据任务流水线,并且,当我们想独立运行某一个etl的时候,我们不得不执行所有依赖的任务。

基于代码复用的数据工程是指采用复用代码的方式进行数据工程中的复用。它采用以计算资源换取维护成本的方式帮我们节省开支。在大量数据量没那么大的场景下更适用。

目前很多框架可以支持这一使用方式,比如dbt,easysql都有相应的支持。

2023-08-08: 终点却是过程

人们常常奔着一个目标往前冲,甚至能不分昼夜,不顾身体。

但是等目标按照预期实现了之后,却觉得心里一下子空虚起来,不知道该干什么了。

有多少软件功能在实现之后就成了它的坟墓?无人问津的程度甚至不如当初开发阶段的测试使用频率。

也许有时候终点恰恰是过程,是奋力向上的有着充实感受的过程。而目的的意义在于为我们提供一个参与这个过程的机会。

每日一思

作者 Bright LGM
2023年8月1日 04:00

2023-08-06: 弄清需求背后的原因

家里孩子到了1岁半的年纪,开始喜欢抓人和咬人。经常没来由的给人脸上身上来一爪或者来一口,弄得家里人满身伤痕。家里人为此苦恼不已。

一开始我也不理解他的行为,采取打回去或者抓回去的办法,并且表现出很凶的样子试图进行教育。然而,教育的时候他总是表现出听不懂的样子,重复数次也没有效果。

在网上搜索一番之后,开始弄清楚了背后的原因:

  • 身体发育到特定阶段,用嘴和手来感知这个世界

  • 以此寻求关注

  • 以此发泄情绪

  • 以此表达喜欢

在理解这些之后,终于释然,现在会更注意用手护住自己,并且根据尝试平和地把他的想表达的东西说出来,引导他进行正常表达。

了解一开始不理解的事物的原因可以帮助我们更好的应对。

在软件开发过程中也是一样。很多开发人员总是只遵循需求描述进行开发,而极少思考背后的原因,即业务价值。所以,他们常常觉得很多需求实现起来很别扭,最终形成了我们在代码里面看到的很奇怪的注释或者条件判断。这样的软件常常满是bug,维护起来非常痛苦。这就像直接针对小孩抓人这件事用表现得很凶的样子教育孩子一样,自己很痛苦,效果也很差。长期下来,软件就渐渐腐坏掉。

如果我们可以尝试去弄清楚需求背后的原因(业务价值),就可以了解到可能有更符合设计的实现,也可以了解到将来可能的演进方向,那就更可能达到简单的架构和优雅的实现。

2023-08-05: 人类的本质差异

地球上有很多动物,为什么人类可以站在食物链最顶端?

是因为人类会语言?很多动物其实也会,猫狗的不同叫声代表不同意思,鸟类可以模仿人讲话,其音域甚至更广。

是因为人可以直立行走,操作工具?很多猴子、猩猩,甚至乌鸦也可以。

《人类简史》中提到人与其他动物的本质差异可能在于人类可以创造一些原本不存在的概念,并通过交流让大家广泛相信,从而聚集起大规模的群体,比如国家与军队。这个群体由于数量上可以大大超过其他动物形成的群体,而具有极强的优势。正是这个优势让肌肉并不发达,体格并不高大的人类站在了食物链最顶端。

2023-08-04: sql代码的重构

随着大数据越来越广泛的应用,基于sql的etl代码也越来越长。当代码达到数百行规模时,就急需要引入整洁代码(clean code)这类高质量编码实践,否则很容易变得不可维护。

重构是实现高质量代码的重要手段,如何针对etl进行代码重构呢?最重要的是需要有一款强大的sql代码重构工具。它应该具备这样一些功能:

  • 能将中间子查询抽取为cte格式的临时视图

  • 能修改子查询或cte视图的别名

  • 能修改子查询或cte视图中的字段名

  • 能自动进行代码格式化

目前似乎还没有特别匹配这些需求的工具,期待它的出现…

2023-08-03: 理性限制的建立与破除

康德认为这世界有很多二律背反问题,比如,正题:世界上的一切都是由单纯的部分复合而成;反题:世界上的一切都是复合的,没有单纯的东西。这两个论断都可以用理性来证明,却又是相互矛盾的结论。所以,理性是有限制的,不应该用于超验的自在之物上。

到了谢林和黑格尔,理性的限制被认为是理性的怯弱。矛盾不是不可调和的,而是所有事物发展过程的中必然出现的阶段。但是,随着认识的深入,矛盾和对立终将达到同一。这世界本质是发展和运动的,总是将一轮一轮的经历从同一到差别到对立,再到矛盾最后回到同一的过程,这也是黑格尔的肯定、否定和否定之否定。否定之否定是最初的肯定的升华。

到这里,终于能理解为什么马克思主义哲学的第一句话是:世界是物质的,物质是运动的。

2023-08-02: 早上六点钟

早上六点钟的世界是什么样子?机械化的工作时间可能已经将我们和这个时段划清了界限,但最近的生活节奏让我有机会出去看一看。

马路上的汽车要少一些,但还是络绎不绝,在红绿灯前面排着队。

公路清理工作已经开始,洒水车、扫地车、环卫工人已经就位,赶在天还不热的时候为大家创造一个更干净的环境。

住所旁边有一个运动公园,公园里已经有很多健身的人,有的开着节奏舒缓的音乐打着太极,有人沿着跑道散步,更多的是身着运动装跑步的。

我是去公园跑步的,跟着同跑的人流向前。大家节奏不一,脚步声噼里啪啦的,偶尔超过几个速度慢一点的,偶尔被几个速度快的超过。

唯物主义哲学认为世界是物质的,物质决定精神。但不管物质如何变化,精神却是自由的,如果没有早起的精神意识,那也看不到早上六点钟的世界。

2023-08-01: 没有测试代码的自动化测试

数据测试一直是一个难题,需要建表,构造数据,运行etl,对比结果等繁琐的步骤。手工构造测试场景不仅耗时巨大,而且难以维护。

有没有一种轻量级的自动化测试方式?

可以这样来做:

  • 根据etl代码生成数据血缘图

  • 从图里面提取表和字段

  • 自动根据输入表和字段创建空表

  • 针对空表运行etl

  • 以输出空表为标准,对比结果

这种测试无法验证逻辑的正确性,但可以验证语法、udf、相互引用关系等基础正确性。

胜在无需编写任何一行测试代码即可运行程序,非常轻量,适合往大数据平台提交任务之前先在本地验证。

这种方法通过极低成本在早期提供反馈来提高开发效率,值得尝试。

2023-07-31: 突破时空的染色

康德在描述我们认识事物的过程时,认为我们认识任何自然事物都会带着主观的色彩。最基础的主观色彩就是时间和空间。时间、空间是自我与生俱来的先天直观形式,只要我们对事物进行感知,我们就必须把它放在空间和时间中。

当我们带着主观的色彩去认识事物的时候,事物就不是完全自由实在的了。至于完全自在的事物,那是不可知的。

结合弗洛伊德的意识和潜意识理论,这种先天直观形式其实是一种潜意识,它确实会给我们认识事物带来障碍,但通过训练我们也是可以认识并突破这种障碍的。所以,完全自在的事物依然是有可能可知的。

那么,我们等等看黑格尔是怎么说的。

每日一思

作者 Bright LGM
2023年7月18日 04:00

2023-07-23: 天真与成长

以前我们家小孩每次出电梯到时候都要给一起乘电梯的人表达再见,扬起手挥一挥,然后嘴巴里面不停的说着拜拜拜拜。碰到热心的邻居,也会笑着回复他跟他拜拜。他因此收获了无数人的喜欢。我们都说他是小社牛。

最近出差回来慢慢发现他已经越来越少表现出这样的行为了。

为什么会这样?可能是因为成长。这几个月是他智力高速发展的几个月,慢慢地他开始与我们能有更多的互动了。他学会了走路,能认识身边的人,还能说几句简单的话,还会自己吃葡萄并把葡萄皮吐出来丢到垃圾桶。他似乎已经开始以一个自主的主体参与世界的活动了。

随着快速的成长,原来天真的小社牛真的要慢慢变成理性的社会人?当我们成长之后,还有多少儿时的天真可以保留下来?

2023-07-22: 看代码没有信心的时候,就去搞懂业务

遗留代码的维护是件令人头疼的事。我们常常要面对多年以前的杂乱无章的代码,它们可能非常长,各类名称可能毫无意义,可能几乎没有文档。

如果直接从代码入手,妄图从代码中发现真理,那可能要落入一个爬不出去的陷阱。满是细节的代码就像是一片迷雾森林,你在其中绕来绕去也找不到一条清晰的出路。

换个思路出发会更容易,那就是从业务出发。事先可以完全抛离代码,只是从业务角度去理解当时的需求和目的。业务角度的需求和目的就像是一个个灯塔,可以为我们指引方向,而有了方向,在就不至于迷失在满是细节的迷雾森林。

2023-07-21: 依赖干系人管理

项目里面的干系人可以按照利益相关性和权利大小分为四个象限,对于利益相关度高权利低的人,通常采取的策略是随时告知。

但如果这些人是你的依赖方,就需要谨慎,因为有可能他们不依赖你,你依赖他们,你的成败需要他们支持,而他们的成败不需要你的支持。

此时,由于他们可能不关心你的项目,所以告知的意义未必大。你找他们协助,他们有可能善意的提供帮助,但绝没有直接的义务。

如何处理?一是以各种方式表达感谢搞好关系,二就是上升到利益相关度高权利高的干系人协助处理。

2023-07-20: 食不知味的感觉

酒店的早餐算是十分丰盛了,超过二十种食材随意挑选。然而,最近越来越发现面对这么多丰盛的食物却不知道选哪种。常常随意拿几个,狼吞虎咽下肚,事后竟然连这些美味的味道都回忆不起来了。

是什么原因?大概是因为早餐太急,想一想,好像很久很久没有一个早上能什么事都不想,什么安排也不做,就仅仅是吃早餐。完全放松下来,认认真真做这件事,细嚼慢咽品味每一粒米饭和每一片蔬菜的味道。

2023-07-18: 从业务入手讲方案

当需要为一个复杂系统提供方案时,需要从业务角度理解问题,不要陷入具体细节。抽象的上层业务模型通常是简单而一致的,这就是主线,基于这个主线就可以给出技术上合理的方案。

财务系统可能涉及几千个科目,每个科目的取数逻辑就是细节,非常多。但是财务统计本身是很简单的,就是简单的求和、取余额等。设计系统时,需要抓住这个主线,避免陷入细节。

2023-07-17: 应对数据开发复杂性有没有更好的方案?

DataMesh数据网格的思路是让业务系统开发团队来提供数据产品供下游消费。这些数据产品往往是聚合了很多数据表得到的一个可以隐藏底层复杂性的数据宽表。

由于这些中间数据产品来自于该系统的开发团队,而他们天然具备对该系统业务和数据的深刻认知,所以维护起来天然是得心应手的。

对于数据开发团队,他们基于这些中间数据产品去构建集成的数据分析报表。在报表开发过程中可以将知识限定于构建集成报表的范围,不至于扩大到各个复杂的底层系统中。

经过这样的职责划分,不至于让数据开发团队知识过载,各团队可以各司其职将最终的数据产品构建出来。

每日一思

作者 Bright LGM
2023年7月13日 04:00

2023-07-15: 新时代的家族关系如何维系?

城市化让更多人搬进了封闭的钢筋混凝土小区,以前亲密的家族成员也分散到了全国各地。

在以前家族成员聚集到一起的时候,总能经常有些活动,比如谁谁过生日,大家就一起聚个餐,谁谁要办个什么事,大家就一起棒棒忙。亲密的亲戚关系于是得以维系。

现在离得远了,很多亲戚经常几年也没联系。如何继续这样的关系?

我们有一个家族微信群,每当有人过生日,大家就在里面发红包,十块八块大家有个心意就是了,过生日的人也会回一个随机红包,让大家一起热闹热闹。借此机会,大家也聊聊近况。由于大家过生日都这样做,一个十多个人的群里面,竟然每个月都至少能有一次这样的活动。

新时代下家族关系的维持可能需要我们去寻找这样的一些仪式。

2023-07-14: 基于ChatGPT进行ETL代码重构

复杂的数据分析场景背后常常使用复杂的ETL进行支撑。然而,由于工程化的缺失,基于SQL的ETL代码很容易变得难以理解。在经历团队人员流动之后,这一问题变得更为显著。如何优化已有的复杂ETL?ChatGPT也许可以帮到我们。以下场景中ChatGPT可以发挥作用。

代码格式化。ChatGPT可以很好的完成代码格式化,相比于学习和配置特定的代码格式化工具,ChatGPT可以用自然语言给出格式化的规范,并在无规范的地方使用行业通用的做法。

代码注释。ChatGPT可以帮我们理解代码并添加注释。当碰到难以理解的逻辑时,ChatGPT可以结合元数据,告诉我们代码的行为及背后的原因。

基于CTE的代码优化。ChatGPT还可以将子查询转化为CTE格式,我们可以基于此功能把多级嵌套的子查询分离出来,并给它一个名字辅助理解。

基于谓词下推的优化。将筛选条件尽可能放在查数据的源头可以有效提升性能,在提示ChatGPT去进行代码性能优化时,它可以帮助实现这一优化。

2023-07-13: 架构设计

最近读了一本书《重塑心灵》,讲的是身心语法程序学(NLP)的知识。NLP里有十二条前提假设,其中之一是:在任何一个系统里,最灵活的部分便是最能影响大局的部分。

最初读到这里的时候有一些诧异。联想到软件架构设计,在我们通常的认知里,架构就是那个最能影响大局的部分,那么架构到底是最稳定还是最灵活的部分?

如果架构是稳定的,它限定了一个框架,那么灵活性就在于填入框架中的细节,那么细节便会成为最能影响大局的部分。所以,细节比架构更重要。

如果架构是最灵活的,那么它就可以被经常调整,那么架构设计就应该在每一天的日常开发过程中进行。

总结起来,我们要么获得了一个不重要的架构,要么把架构变成日常开发中的每一张故事卡。

2023-07-12: 当面沟通的力量

在我还青春年少的时候,大家经常调侃失追女孩的情景说:屌丝有三废,在吗,忙不,早点睡;女神有三宝,干嘛,呵呵,去洗澡。

仔细分析一下为什么屌丝追女孩很失败,我发现这其中有一个重要的原因,屌丝是用短消息和女神交流的。屌丝没法从女神的“干嘛”中捕获到女神今天的心情,也没法知道女神当前的状态。

试想,即便还是用这样的开场白,但是屌丝直接一个电话或者视频打过去,会是什么场景?只要女神真的不是直接拒绝你,你就可以从她的话语、表情或者手势中感知到非常多的信息。她烦躁就听听她的吐槽,她无聊时就跟她讲讲网上查到的笑话,她在娱乐时就远程加入,交流于是如丝般顺滑的进行了下去。

为什么当面交流比起短消息更有效?佛洛依德的潜意识理论认为意识和潜意识就像冰山,意识是水上面的很小一部分,潜意识是水下面的大部分。当面交流时,潜意识帮助我们捕捉了大部分的信息,因而可以帮助我们更有效的交流。

回想起昨晚因为客户的一句拒绝性的短消息回复而失眠,今天见面交流时又能谈的很投机。谁说不是呢?

每日一思

作者 Bright LGM
2023年7月3日 04:00

2023-07-02:

去过成都环球中心海洋乐园的人一定会惊叹于这片人造海滩的宏伟,400多米的海岸线竟然硬生生的在一个商场里面给造了出来。海洋乐园前几年是封闭运营,必须买票才能进场到海滩边,我去过几次,发现都是门可罗雀的景象,大概是它的高冷向世人阻挡了它的宏伟。今天带上小孩一起来环球中心闲逛,发现海洋乐园竟然开放了,不用买票就可以走到海滩边。作为一个在环球中心闲逛的游客,看到一层一层卷起的巨浪,听着浪花冲向海滩的哗哗声及沙滩上人们的欢呼声,竟也想买张票下水体验一番了。封闭还是开放?到底要如何选择?

用半小时给Hexo加个短链

作者 Bright LGM
2023年1月12日 04:00

Hexo博客有好几年了,Markdown的格式、简洁的风格、静态的特性、众多的插件,一直让我用起来很舒服。

不过,有个问题困扰了我很长时间,那就是,Hexo生成的博客的永久链接比较长,在社交平台上面分享不太方便。

有不少第三方的短链服务,有的还可以帮助追踪链接的点击次数等。比如:https://www.shorturl.at/ https://app.bitly.com/bbt2/ 等。

使用这些短链服务倒是比较容易,但也会碰到不少问题,比如:

  • 生成的url的域名改为了短链服务的域名,不利于用户识别
  • 短链服务要是关闭了,之前发出去的链接就没法用了。(由于短链服务难以盈利,事实上很多类似业务都关闭了,比如Google的短链服务goo.gl就已经下线了)
  • 短链服务难以支持全球多个区域的访问

于是,这就催生了我想自己实现一个短链的想法。

短链服务本身并不复杂,本质上只需要对目标链接生成一个足够短的稳定hash值并保存此值与原链接的映射即可。

短链生成

我配置的博客永久链接格式为:year/:month/:day/:title/。考虑到一天的博客数量一般不会超过一篇,所以,基于日期进行编码即可得到一个不错的hash值,剩余的部分可以选择编码为1~2个防碰撞的字符。

基于这些分析,我快速获得了一个生成稳定hash的方法,对应的Python代码如下:

1
2
3
4
5
6
7
def shorten(post_name: str):
# 获取博客日期信息,并取与最早的博客时间(`2010-01-01`)的天数差
days = (datetime.strptime(post_name[:10], '%Y-%m-%d') - datetime.strptime('2010-01-01', '%Y-%m-%d')).days
# 剩余部分,用md5编码,并取前两位
hash_suffix = hashlib.md5(post_name[10:].encode('utf8')).hexdigest()[:2]
# 将天数和前两位的hash值连接起来,取Base64的值即得到短链
return base64.b64encode(f'{days}{hash_suffix}'.encode('utf8')).decode('utf8').strip('=')

运行测试可以发现,原来的博客2022-07-28-modelling-examples被编码为了NDU5MTYx。还不错,只有8位了。

集成到Hexo

如何集成到Hexo呢?

我的基本想法是:

  • 用户点击短链跳转到博客首页
  • 博客首页根据短链找到博客永久地址,然后跳转到永久地址

于是最终短链URL可以设计为:https://brightliao.com/#/POST_HASH。即,将博客的hash值放到URL的Hashbang中。这就可以让浏览器直接打开首页了。

在首页中,咱们还需要嵌入一段JavaScript用于读取此hash值,并跳转到具体的博客中。

实现

前面分析了思路,以下是具体实现。

本着经济适用的原则,我想以最快的速度来实现,尽量把代码编写和调试时间控制在半小时内(实际花了约一小时o(╥﹏╥)o)。

如果你也是用的Hexo,可以直接拿走,如果不是,仅供参考。

为了便于大家理解,下图是整个短链的生成及运行流程:

Short Link For Hexo

涉及代码有两段:

  • JavaScript代码:判断短链并跳转
  • Python代码:读取博客,生成短链到真实地址的映射表,更新到前端可访问的文件中,并更新文件版本,防止缓存

JavaScript代码

我用了next主题,Hexo在生成静态文件时,会在每个文件中包含文件themes/next/source/js/next-boot.js,这个文件就是我们要保存映射表及对应的JavaScript代码的地方!

在文件next-boot.js的最后添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
//...
var links = {}
try {
(function(){
if (window.location.hash) {
var link = window.location.hash.substring(2);
if (links[link]) {
window.location.href=links[link];
}
}
})()
} catch (e) {}

此处的links变量是保存映射表的地方,在有新博客时,Python代码会更新此处的代码。

代码将读取当前的URL,并查找链接表,如果找到则跳转,找不到则不做任何处理。

Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import os
from datetime import datetime
import base64
import json
import re
import hashlib


curdir = os.path.dirname(os.path.abspath(__file__))


def shorten(post_name: str):
days = (datetime.strptime(post_name[:10], '%Y-%m-%d') - datetime.strptime('2010-01-01', '%Y-%m-%d')).days
hash_suffix = hashlib.md5(post_name[10:].encode('utf8')).hexdigest()[:2]
return base64.b64encode(f'{days}{hash_suffix}'.encode('utf8')).decode('utf8').strip('=')


def create_links():
links = {}
postdir = os.path.join(curdir, 'source/_posts')
for f in os.listdir(postdir):
f = os.path.join(postdir, f)
if os.path.isdir(f):
for p in os.listdir(f):
if (p.endswith('.md') or p.endswith('markdown') ) and not os.path.isdir(os.path.join(f, p)) and re.match(r'^[\d]{4}-[\d]{2}-[\d]{2}.*', p):
long_link = f'/{p[0:4]}/{p[5:7]}/{p[8:10]}/{p[11:-3] if p.endswith(".md") else p[11:-9]}/'
short_link = shorten(p)
if short_link in links:
raise Exception(f'found short link {short_link} -> {long_link} conflict with existing {links[short_link]}')
links[short_link] = long_link
items = sorted(list(links.items()), key=lambda item: item[1])
for item in items:
print(f'{item[1]}: https://brightliao.com/#/{item[0]}')
print(f'found {len(links)} links.')
return links


def update_bootjs(links):
bootjs = os.path.join(curdir, 'themes/next/source/js/next-boot.js')
if not os.path.exists(bootjs):
raise Exception('bootjs file not found at: ' + bootjs)
with open(bootjs, 'r') as f:
content = f.read()
rex = r'\nvar links = {[^\n]*}\s*\n'
if not re.search(rex, content):
raise Exception('rex not found in bootjs')
content = re.sub(rex, f'\nvar links = {json.dumps(links)}\n', content, count=1)
with open(bootjs, 'w') as f:
f.write(content)
print('updated file: ' + bootjs)


def update_indexjs():
indexjs = os.path.join(curdir, 'themes/next/layout/_scripts/index.swig')
if not os.path.exists(indexjs):
raise Exception('indexjs file not found at: ' + indexjs)
with open(indexjs, 'r') as f:
content = f.read()
rex = r"'next-boot\.js\?([\d]+)'"
if not re.search(rex, content):
raise Exception('rex not found in indexjs')
nextver = int(re.match(r".*'next-boot\.js\?([\d]+)'.*", content, re.DOTALL).groups()[0]) + 1
with open(indexjs, 'w') as f:
f.write(re.sub(rex, f"'next-boot.js?{nextver}'", content, count=1))
print('updated file: ' + indexjs)


if __name__ == '__main__':
links = create_links()
update_bootjs(links)
update_indexjs()

上述update_bootjs函数的功能即为更新next-boot.js文件中的映射表。使用正则表达式\nvar links = {[^\n]*}\s*\n可以匹配到保存映射表的代码行,更新时将其替换即可。替换时,在该位置写入JSON字符串,供JavaScript代码读取。

为防止缓存,可以在更新映射表之后,重新生成一下next-boot.js文件的版本。update_indexjs函数即可辅助完成此功能。

加载next-boot.js的文件位于themes/next/layout/_scripts/index.swig,同样,用正则表达式提取现有版本,并替换为递增之后版本即可。
注意在第一次运行此脚本之前,需要手动修改一下index.swig文件,将next-boot.js替换为next-boot.js?1

进行上述代码修改之后,每次增加博客时,只需要运行一下上述Python文件即可添加新博客的短链,同时原有短链依然有效。

运行此Python文件会打印出所有的博客对应的短链地址,复制分享即可!

效果

最后看一下最终效果,试试看点击链接https://brightliao.com/#/NDUyNjc2是否能跳转到博客https://brightliao.com/2022/05/24/5-properties-of-good-code-cupid/?

突破ChatGPT的知识限制

作者 Bright LGM
2023年6月3日 04:00

作为一个一直对AI技术很感兴趣的软件开发工程师,早在深度学习开始火起来的15、16年,我也开始了相关技术的学习。当时还组织了公司内部同样有兴趣的同学一起研究,最终的成果汇集成几次社区中的分享以及几篇学习文章(见这里)。

从去年OpenAI发布ChatGPT以来,AI的能力再次惊艳了世人。在这样的一个时间节点,重新去学习相关技术显得很有必要。

ChatGPT的内容很多,我计划采用一个系列,多篇文章来分享学习我自己学习过程中的一些理解。本系列文章,我将站在一个普通开发人员的角度展开,希望对想了解ChatGPT技术原理的普通开发者们有帮助。

ChatGPT本身就具备很丰富的知识,所以ChatGPT自身实际上就是一个很好的学习渠道,我也将借助ChatGPT来学习ChatGPT。

这是此系列的第六篇,突破ChatGPT的知识限制。

文章内容

ChatGPT的局限性

上一篇文章我们深入分析了ChatGPT如何结合奖励模型和强化学习算法进行自动优化。 OpenAI开放ChatGPT模型给大家使用,随着大家使用越多,产生的对话就越多,ChatGPT就能自动优化得更智能。

虽然ChatGPT能力很强,但是其局限性也很明显,对于没有纳入训练数据的知识它是不知道的,从而也没法给出我们期望的回复。但由于我们的日常工作往往要基于大量的ChatGPT不知道的背景知识进行判断和决策,所以很多场景我们也没法简单的通过向ChatGPT提问来得到想要的答案。

如何突破ChatGPT的知识限制,让ChatGPT具备更强大的能力,以便可以更好的辅助我们完成工作?这就是本文想要讨论的问题。

扩充ChatGPT的知识的方法

基础模型优化

如何让ChatGPT认知到这些复杂的背景知识?最简单的想法是把这些知识整理成文本,制作成数据集,然后让ChatGPT进行模型优化。

如何优化呢?前面ChatGPT模型训练内容我们分析了ChatGPT类模型的训练过程,分为三个阶段:基础语言模型训练、监督微调及指令微调。其中监督微调及指令微调阶段的数据量小,主要是解决模型的指令跟随问题。如果想在ChatGPT之上加入背景知识,看起来应该在基础语言模型训练阶段实施。

进行基础语言模型调优理论上是可行的,但是实践时会遇到很多问题。比如:

  • 如果新数据中存在错误、噪音或不准确的信息,将可能对模型的性能产生负面影响
  • 使用这些新数据进行训练时,必须要非常小心的调整学习率,否则可能让ChatGPT遗忘之前学习到的通用知识,变得更笨
  • 基础模型优化可能会影响第二和第三阶段的优化结果,使得ChatGPT没法很好的进行问答
  • 模型本身太大,进行模型调优需要的计算资源更大,以目前的算力成本来看,很难负担得起

目前来看,这一方法的可操作性不强。当然,在将来某一天,模型优化到一定程度,或者算力成本降低到一定程度时,也许这一方案也变得可行了。

长上下文支持

另一种扩充ChatGPT知识的方案是想办法增加模型的上下文支持能力。模型支持的上下文越长,表示我们可以输入给模型的内容越多,从而可以让ChatGPT基于更多的背景知识回答问题。

目前ChatGPT可以支持的上下文长度为4k或16k,而GPT-4支持的上下文长度为8k或32k(参考这里)。

从前面文章对于ChatGPT原理的分析来看,要想将上下文变长并不是一件容易的事。主要的挑战有:

  • 显存消耗增长:由于计算自注意力结果时需要计算并保存所有上下文计算出来的中间结果,增加上下文长度会导致模型需要更多的显存来存储这些中间计算结果。
  • 计算复杂度增加:随着上下文长度的增加,每一个训练样本的计算复杂度都会增加,计算时间和计算资源的需求也会增加。
  • 长期依赖问题:更长的上下文导致信息在序列中的传播路径更长,这可能导致模型难以捕捉到远距离的依赖关系。

然而,最重要的可能是并没有如此高质量的数据集供模型训练。特别是针对监督微调及指令微调阶段的数据集。

有很多分析和研究表明(参考这里),如果模型在4k上下文的数据集上面训练,是很难直接让它支持超过4k长度上下文的。具体表现就是模型性能严重下降。这就是所谓的模型的上下文长度外推能力

目前有一些方法来增强模型的外推能力,如文章《语言大模型100K上下文窗口的秘诀》中提到的有:

  • ALiBi位置编码: 分两个阶段进行训练,首先在2K个词元的上下文长度上训练基本模型,然后在更长的上下文(例如65K)上进行微调。对于网络模型,则移除位置正弦嵌入,采用线性偏置注意力代替(通过附加一个与当前词元距离具有比例关系的惩罚来修正查询出来的注意力分数)。
  • 稀疏注意力机制:基于并非所有长上下文内容都是相互关联的这一假设,在计算注意力分数时仅考虑部分词元,具体实现有滑动窗口技术BigBird
  • FlashAttention:在GPU的注意力层实现时,采用IO更高效的优化手段,以便可以支持快速训练与预测
  • 多查询注意力:优化模型中间结果缓存,在推理过程中能够显著加快增量注意力分数的计算

最近的很多模型更新显示业界大家都在尝试实现更长的上下文支持。如:

  • Anthropic在5.11日发布新的Cloude模型,表示支持100K上下文
  • OpenAI也在近期更新了GPT-3.5及GPT-4模型的上下文长度支持,分别从4K增加到16K和从8K增加到32K
  • 国内清华系近期开源的ChatGLM2-6B模型的上下文长度从2K扩展到了32K

显然,这已经是大模型开发厂商们正在努力的方向。

基于文档搜索的知识扩充

长上下文固然可以提高模型的背景知识,但在企业应用中,我们往往拥有海量的背景知识,全部依靠长上下文还是显得力不从心。

为了支持海量背景知识,可以对比参考我们人类解决问题的方法。一种常见的步骤是:

  1. 查找相关文档,进行大量调研
  2. 汇总各类信息形成基本的解决方案
  3. 执行

可以发现,即便人类所具有的背景知识也是有限的,人类也可以很好的解决问题。这是因为人可以主动的去查找相关信息,然后汇总形成方案。

这一思路就是基于搜索的思路,它是目前可操作性更强的方案。

在ChatGPT类的模型中应用搜索机制就变成:

  1. 将所有知识文档搜集起来,然后拆分为小的文档,比如一篇文档2000字
  2. 对每一篇2000字的小文档进行向量化(采用大语言模型将其编码为向量,并使得这个向量与可能的问题具备较高的相关度),然后存储起来
  3. 在用户提出一个问题之后,将问题采用相同的方式编码为向量
  4. 将问题对应的向量与所有知识小文档向量计算相关度,选出相关度最大的几个文档
  5. 将这些选中的小文档提取出来,构造一个提示语传给大语言模型,让大语言模型根据这些上下文回答问题

ChatGPT文档查询,图片来自:https://github.com/imClumsyPanda/langchain-ChatGLM

目前有很多开源库支持以这样的方式来扩充ChatGPT类模型的知识。比如LlamaIndexLangChain等。

它们的主要特点是:

  1. 支持很多直接可用的经过验证文档向量化模型(如OpenAI的Ada模型、基于LLAMA的各类开源模型等)
  2. 提供多种方式实现上述第4步中的相关文档查找,比如基于向量存储库、基于树索引等
  3. 提供了一些常见的小文档使用模式及对应的上述第5步中的提示语,比如直接组合小文档的方式,基于每个小文档一步一步优化答案的方式,并行基于每个小文档生成回复然后一次性组合的方式等,详见这里

一个简单的用LangChain实现的用例只需要几行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA
from langchain.document_loaders import TextLoader

loader = TextLoader("path/to/your/long-text-doc")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)

openai_key = '...'
embeddings = OpenAIEmbeddings(openai_api_key=openai_key)
docsearch = Chroma.from_documents(texts, embeddings)

docsearch.similarity_search_with_relevance_scores('你的问题')

qa = RetrievalQA.from_chain_type(llm=OpenAI(openai_api_key=openai_key), chain_type="refine", retriever=docsearch.as_retriever())

qa.run("你的问题")

虽然看起来基于搜索的方案是目前给大语言模型注入知识的最佳方案,但实际效果却未也必能达到预期。

从实现原理来看,有很多因素可以导致效果下降,比如大文档切分时将关键的文本句子拆分为了不合理的小文档,文档搜索漏掉了某些关键的文档,搜索出来的相关但非关键的文档排名更靠前,组合得到的提示语过于简单等。

事实上,人类在决策时大量还是基于记忆而非搜索的知识的,这部分可能只能通过第一种方式(基础模型优化)才能更好的解决。

总结

本文分享了ChatGPT类大语言模型在当下应用时的局限性,并分析了改善的方法。虽然目前大语言模型还远未达到完美,但我们可以看到很多聪明的大脑正在为解决这个问题贡献极具创意的方案,相信不久的将来这些问题都会迎刃而解!

将来的大语言模型的应用会是怎样的?大胆的猜测一下,大概是每个人一个模型,每个团队一个模型,每个公司一个模型吧,这些大语言模型具备不同程度的背景知识,可以更精准更贴心的帮助我们更智慧的解决问题。

自ChatGPT发布以来,很多人认为这是一个人类走向通用人工智能的突破,也有一些人认为它其实没什么本质的改进。有很多人对自己的职业发展产生了很深的焦虑感,也有很多人感觉触碰到了科幻世界中的未来,还有很多人觉得又是一个可以好好捞一把的机会。

也许每个人都有必要去了解一下机器学习技术的原理,这样才能形成对它的理性的认知。

ChatGPT的内容很多,我计划采用一个系列,多篇文章来分享学习我自己学习过程中的一些理解。本系列文章,我将站在一个普通开发人员的角度展开,希望对想了解ChatGPT技术原理的普通开发者们有帮助。

这是此系列的第六篇,突破ChatGPT的知识限制。

参考

❌
❌