2024年4月

前言

在Excel中创建的大多数商业报告不是单页的文档,而是包含了多个上下文相关的信息,这些信息被存储在多个工作表中。例如我们的一些地区销售报告、按部门分类的员工记录、每家店铺的库存清单等。

然而,随着Excel文件中工作表数量的增加,要在单一文档内导航和管理数据会变得十分具有挑战性。此外,因为这些Excel文档包含了并非所有相关方都应看到的信息,所以在不同的相关方之间共享这样的Excel文档不是一个合理的方案。因此,有必要将这些多工作表的Excel文档分割成若干个较小的文档,以便更容易管理、提高效率和数据隔离。

尽管Excel在功能上占据主导地位,但它不提供原生的函数来分割工作表。你需要手动进行此操作,或者使用VBA。虽然手动方法效率低且容易出错,但VBA方法会使文件变得容易受攻击。

GrapeCity Documents for Excel
(以下简称 GcExcel )提供了几种通过Java语言编程的方法,可以将Excel文件中的多个工作表分割成独立的Excel文档,例如:

  • 通过从源工作簿复制到目标工作簿来分割工作表
  • 通过从源工作簿移动到目标工作簿来分割工作表
  • 通过从源文件删除不需要的工作表并将其另存为新文档来分割工作表
  • 通过从源到目标工作簿复制并粘贴内容等方式分割工作表

在这篇博客中,小编将为大家介绍如何通过编程方法将工作表从源工作簿复制到目标工作簿来实现分割。

应用案例

假设你现在作为你公司的首席销售官,准备了一个Excel报表,其中按区域分别在不同的工作表上包含了销售分析信息。你想要将这份报告分享给区域销售经理们。但是,你希望将各个工作表分离成独立的Excel文件,以隔离不同区域的分析细节,如下图所示:

让我们看看如何通过使用GcExcel通过3个简单的步骤为每个工作表生成独立的Excel文件。

我们需要在遵循GcExcel的产品文档中描述的规则的情况下于IDEA中使用GcExcel。

步骤1- 初始化工作簿

第一步是加载你想要拆分工作表的源工作簿。使用GcExcel,你可以通过IWorkbook接口的Open方法加载Excel文件,如下所示:

Workbook workbook = new Workbook();
workbook.open("input.xlsx");

步骤2 - 添加拆分Excel工作表的逻辑

完成第一步后,紧接着是添加拆分源工作簿中工作表的逻辑,包括:

  • 创建一个循环,遍历源工作簿中的每个工作表,并按需进行拆分
  • 初始化一个临时工作簿
  • 使用IWorksheets接口的Copy方法将当前工作表从源工作簿复制到临时工作簿的末尾
  • 删除临时工作簿中的默认工作表
//创建一个循环
for (IWorksheet worksheet : workbook.getWorksheets()) {
    //初始化一个临时工作簿,粘贴工作表,删除临时工作表
    Workbook tempWorkbook = new Workbook();
    worksheet.copy(tempWorkbook);
    tempWorkbook.getWorksheets().get("Sheet1").delete();
}

步骤3 - 保存拆分的Excel工作表

最后,使用IWorkbook接口的Save方法将拆分的工作表保存为独立的Excel或其他格式的文件,如下所示:

tempWorkbook.save(worksheet.getName()+".xlsx",SaveFileFormat.Xlsx);// Excel文件
tempWorkbook.save(worksheet.getName()+".pdf",SaveFileFormat.Pdf);// PDF文件

完整的代码片段如下所述:

Workbook workbook = new Workbook();
workbook.open("test.xlsx");
for (IWorksheet worksheet : workbook.getWorksheets()) {
    Workbook tempWorkbook = new Workbook();
    worksheet.copy(tempWorkbook);
    tempWorkbook.getWorksheets().get("Sheet1").delete();
    tempWorkbook.save(worksheet.getName()+".xlsx",SaveFileFormat.Xlsx);
}

除此之外,GcExcel还提供了一些来自IWorksheets接口的其他方法,比如CopyAfter和CopyBefore,以满足特定Excel需求的工作表拆分。此外,它还提供了Move、MoveAfter和MoveBefore方法,通过将工作表从源Excel文件移动到目标Excel文件来拆分工作表。

总结

Excel文件解决方案工具(GcExcel)是一个强大的工具,设计用于使用Java简化Excel文档的创建、读取和编辑。它提供了一种简单高效的方式来以编程方式处理Excel文档。有了现成的API,你可以仅用几行代码就完成所需的Excel操作,如拆分工作表一样。编程方式处理Excel文档可以节省宝贵的时间和确保数据的精确性。如果您想了解更多的信息,欢迎
点击这里
查看。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

数据驱动创新:以Java编程方式定制数据透视表

Java批量操作Excel文件实践

我构建的预测模型

在过去的一段时间里我抓去了小宇宙内上万条播客节目的首日播放量的数据,并利用这些数据构建了一个用于预测播客节目播放量的模型。包含以下六个输入参数:

  • 节目发布于一周中的哪一天
  • 节目发布于一天中的哪个时段
  • 节目所属播客的订阅数
  • 节目所属播客的听众女性占比
  • 节目所属播客的听众占比最高的城市

下图左侧是利用 Tensorflow Visor 渲染的参数与播放量的散点关系图,右侧是将训练过程可视化之后的效果。

你可以在
这里
找到原始数据与模型有关代码。当然因为只是作为学习和娱乐之用,我不保证数据的绝对有效和采集方式的绝对科学。

在继续阅读下面的文字之前,你需要阅读上述链接中的源代码,代码非常简单不过百行左右。

困境

在 100 行的代码中只有不到一半代码切实与编译、训练模型有关,剩下的则是关于数据转换、可视化等等。

如果你之前没有 Tensorflow 的有关经验,会发现代码可以被看懂却无法理解:我知道它在创建对象,我知道它在执行异步操作,但什么是 meanSquaredError?什么是 tensor?什么是 dense?

这是 Tensorflow 带来的最直接的反直觉体验:在传统的编程领域,编程语言或者框架带来差异微乎其微,但是在这里却让人步履维艰——原因在于:1)
过往的的编程经验无法迁移至此
,因为它无关响应,无关跨功能需求;2)在传统的开发工作中代码是贯穿所有的主心骨,而
在 Tensorflow 前代码已然沦落为配角,前期知识的积累才弥足珍贵
。理解二者尤为重要,因为它意味着过往你学习一门技术的方法在此不再有效。

例如本文所抓取的数据都由一个部署在 K8S 上的 NodeJS 应用程序完成。在此之前我从未使用过 K8S。但这并未给我带来太大困恼,使用 Google 搜索 “k8s tutorial” 或者 “k8s nodejs” 就可以找到足够多的入门教程让我跌跌撞撞的将一个 NodeJS 在 K8S 上运行起来(但我依然推荐系统性的学习,这里推荐异步社区引进的图书
《Kubernetes修炼手册》
。英文原版在亚马逊上名列 Cloud Computing 类目排名第一, 我是在阅读完毕一半的英文版之后才发现的有中文版。)。

但这一套方法在 Tensorflow 面前失灵了,你所能检索到的大部分教程都存在小节开头所提及的同样问题。我甚至得出了一类规律:如果教程里不包含丁点图片,那它根本就不适用于非机器学习专业的编程人员入门。

解药

读到这里你大概明白了,机器学习本质上是一门垂直的科学,但和编程存在交集这件事让我们有了“相声不就是说话嘛,所以我有嘴我也行”的错觉

所以在了解机器学习领域基本知识,而不是单刀直入编程才是学习 Tensorflow 的首要任务。

学习什么(What)

如果我们都同意上述结论的话,那我的下一个问题是:了解到多少程度才算够?因为此时我想提醒你小心落入第二个圈套,掉进“兔子洞”中。

你可以在 YouTube 上搜索到各式各样的机器学习入门:YouTube 上最广为人知的机器学习课程非
斯坦福 Stanford CS229: Machine Learning Cours 系列
莫属,这个由前百度公司首席科学家吴恩达亲自授课的系列课程单集播放量均在百万级别以上;如果你只是想从最基本最经典的 Linear Regression 问题开始,可以选择观看
StateQuest 的系列视频

但我都不推荐它们,因为它们偏侧理论,与代码无关,要知道在这条学习路径上遇到的每个知识点都可以向专业方向延申的很远,例如在 Tensorflow 的 API 文档中不少的方法说明都保留了对有关论文的引用,但我相信在座各位学习 Tensorflow 的初衷并非是想成为职业的数据科学家,所以恰当的把握理论与实践的边界感颇为重要。

如何学习(How)

最近在折腾 Tensorflow 的同时我还在折腾 NextJS 和 K8S,对比之下我发现视频教程,或者说至少可视化这件事对学习 Tensorflow 尤为重要。

这个道理再简单不过了:只有通过图我才能告诉你什么是 hidden layer、只有通过图我才能帮你回顾起矩阵之间的计算是如何运作的。

一个实际例子是早在一年之前我就购买了图书
《JavaScript深度学习》
,当它谈到其中的 back propagation 时把我劝退了。直到一年之后当我再通过 The Coding Train 的
Neural Networks 教程
学习到 back propagation 时才发现它如此的简单。

下图是我的学习笔记,不难看出其中的图解占据了大多数内容:

向谁学习(Who)

毋庸置疑专业的科班学习一定是最好的方式。这不是什么臆想,而是缺少系统性的规划下我走了很多的弯路,要不然我怎么知道对理论的学习要适可而止,以及建议优先选择视频教程。

如果你和我一样有前端开发背景,那我推荐 The Code Train 的三个系列教程,刚好可以覆盖不同阶段的学习需求。

同时我还推荐
DigitalSreeni
的视频,它不适用于入门,但是可用作参考。他会对一些训练模型中的常见问题和概念做出讲解和进行解答

如果你想寻找一些有关机器学习相似主题的视频教程,比如专门针对 Python 语言,又或者针对 PyTorch 的,我有以下建议:

  • 不用考虑累计总时长1小时(其实我想说五小时)以下的视频教程。也许十分钟学会 React 可以,但这次不行。
  • 不考虑官方教程——大部分的官方教程像是为了存在而存在,官方不会过多的投入就好像他们知道第三方能做的更好似的,但什么都不做似乎又说不过去。虽然 Tensorflow 官方制作了一系列教程,比如
    Building recommendation systems with TensorFlow
    ,我也相信视频中的讲师是高手中的高手,但他的念稿般的表达能力让我感觉到索然无味,和上面 The Code Train 相比立见高下
  • 不考虑中文教程:无论从实效性还是从表达能力上看英文教程都更胜一筹,本质上是因为英文用户的基数足够多,概率上来说贡献者和出现精品的概率也更大。放心,技术文字比四六级阅读理解简单得多

未曾改变与已经改变的

正式的文章直至上一小节就已经结束了,最后有感而发聊聊“技术学习”这件事。

我不敢说“终身学习”是这个时代强加给每个人的必修课,但至少是对我们这个职业的“诅咒”。从 Vanilla JavaScript 到 BackboneJS,再到 React 和 TypeScript,没有人会好心好意的告诉你一个时代已经结束,请准备好迎接下一个时代的来临;也没有公司会无偿提供培训来帮助你适应新技术的更迭。“开着飞机换引擎”对于处于这个行业的人来说可能会是持续相当长时间生存状态。这与技术无关,当有一天你转型成为技术管理者或者决策者,关于团队,策略以及交付等等又会成为你清单上的新主题,我想起了贝爷的梗图:Improvise,adapt,overcome

Copilot 的出现并没有解除我们的诅咒,如果你读过我的
前一篇文章《Copilot 编程指南》
就会知道, AI 可以很好的将我们从琐碎的工作中解脱出来,但是无法代替我们绘制蓝图

幸运的是深度学习依然有效,幸运的是互联网社区中依然有无数人在进行无私的分享。十多年前我通过
博客园的专题页面

0开始学习 jQuery
,只不过如今对象换成了 Tensorflow 渠道变成了 YouTube。

亲自试错(trail and error)永远是解决此类问题的最好方式,我一向反对干瘪的技术培训,因为你无法通过读书去学会游泳。想学?
build something, build something real
.

前言

在WPF应用程序开发中,我们可以借助其强大灵活的设计能力打造出绚丽而富有创意的用户界面。然而,与这种高度定制化的界面相比,标准MessageBox却显得有些原始和古老。它的外观与现代、绚丽的应用界面格格不入,使得用户在交互中可能感到突兀或不符合预期。

本文将深入探讨如何在WPF中封装自定义MessageBox,使其能够融入应用程序整体设计,呈现出与应用界面一致、时尚而个性化的效果。突破了MessageBox的界面限制后,开发者可以为用户提供更一体化、更愉悦的界面交互体验。

一、需求分析

1.1 适应不同风格

各种软件界面的风格不尽相同,有的软件可能用第三方UI库,有的软件可能是自己写的UI效果,所以需要考虑以最小的代价适配不同界面风格。

1.2 兼容性设计

有的软件使用了默认的MessagBox效果,最好是可以实现零修改或少修改达到界面风格替换。

二、实现思路

第一步

创建一个自己的MessageBox类,找到System.Windows.MessageBox的源代码,将所有Show重载方法拷贝到自己的MessageBox类中备用。

第二步

创建一个名为CreateWindow的私有静态方法,在方法里获取Application.Current.MainWindow类的父类型,再通过反射创建Window。

第三步

创建一个用户控件,实现MessageBox.Show()中各种参数下的按钮及图标切换。

第四步

在第二步创建的CreateWindow方法中调用第三步创建的用户控件,组装成一个完整的Window。

第五步

在第一步中的Show()方法重载中调用第二步中的CreateWindow方法,通过传参实现各种不同的功能。

三、使用方法

由于我们使用了与System.Window.MessageBox.Show()完全一样的方法签名,所以使用方法与传统的MessagBox完全一致,甚至在老项目上面替换掉命名空间就可以直接使用,不需要改任何代码。

MessageBox.Show("这是默认效果。");
MessageBox.Show(
"这是指定标题的效果。", "确认");
MessageBox.Show(
"这是指定图标的效果(Question)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Question);
MessageBox.Show(
"这是指定图标的效果(Warning)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Warning);
MessageBox.Show(
"这是指定图标的效果(Error)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Error);
MessageBox.Show(
"这是指定图标的效果(Information)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Information);
MessageBox.Show(
"这是指定按钮的效果(OK)。", "确认", MessageBoxButton.OK);
MessageBox.Show(
"这是指定按钮的效果(OKCancel)。", "确认", MessageBoxButton.OKCancel);
MessageBox.Show(
"这是指定按钮的效果(YesNo)。", "确认", MessageBoxButton.YesNo);
MessageBox.Show(
"这是指定按钮的效果(YesNoCancel)。", "确认", MessageBoxButton.YesNoCancel);
MessageBox.Show(
"这是指定默认按钮的效果(Yes)。", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes);
MessageBox.Show(
"这是指定默认按钮的效果(No)。", "确认", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
MessageBox.Show(
"这是指定默认按钮的效果(OK)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Error, MessageBoxResult.OK);
MessageBox.Show(
"这是指定默认按钮的效果(Cancel)。", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.Cancel);

四、运行效果

3.1 自定义Window1+MaterialDesign的效果

3.2 自定义Window1+HandyControl的效果

3.3 自定义Window2+
HandyControl

RAG系列

本文介绍了RAG以及RAG pipeline的整个流程,包括请求转换、路由和请求构造、索引和检索、生成和评估等,其中引用了大量有价值的论文。

参考 Advanced RAG Series: Generation and Evaluation 中的5篇文章,并丰富了相关内容。

image

请求转换

请求转换是为了提高查询结果的准确性而对用户请求进行重构、优化的过程。

为什么需要RAG?

  • 问题1
    :LLMs并不了解你的数据,且无法获取与此相关的最新数据,它们是事先使用来自互联网的公共信息训练好的,因此并不是专有数据库的专家也不会针对该数据库进行更新。


    image
  • 问题2:上下文窗口-每个LLM都有一个tokens的最大限制(通常平均为100tokens,约75个单词),用于限制用户每次提交的tokens数据,这会导致丢失上下文窗口之外的上下文,进而影响准确性、产生检索问题和幻觉等。


    image
  • 问题3:
    中间遗失
    -即使LLMs可以一次性接收所有的数据,但它存在根据信息在文档中的位置来检索信息的问题。
    研究
    表明如果相关信息位于文档中间(而非开头或结尾时)时就会导致严重的性能降级。
    image

因此,我们需要RAG。

请求转换

请求分解

由于用户问题可能太含糊、太特殊或缺少上下文,因此LLM可能无法很好地处理这些问题。通常会建议在将请求发送到嵌入模型之前对其进行重构。下面是一些重构方式:

  • 重写-检索-读取
    : 这种
    方式
    注重对用户的查询进行重构(而不仅仅是采用retriever或reader,左侧图示)。它使用一个LLM生成一个查询,然后使用web查询引擎来检索内容(中间图示)。此外还需要在pipeline中使用一个小型语言模型来进一步对齐结果(右侧图示)。


    image
  • 对问题进行浓缩或改写
    :通常用于会话中,通过把对话改写成一个好的独立问题来给聊天机器人提供上下文。Langchain的一个提示模板示例如下:

    "Given the following conversation and a follow up question, rephrase the follow up \ question to be a standalone question.
    
    Chat History: {chat_history}
    
    Follow Up Input: {question}
    
    Standalone Question:"
    
  • RAG Fusion
    : 将RAG和倒数排名融合(
    RRF
    )结合。生成多个查询(从多个角度添加上下文),并使用倒数分数对查询结果重新排序,然后将文档和分数进行融合,从而得到更全面和准确的回答。


    image
  • Step-Back Prompting
    :这是一种更加技术性的提示技术,通过LLM的抽象来衍生出更高层次的概念和首要原则。这是一个对用户问题的迭代过程,用于生成一个"后退一步"的问题(step back question),然后使用该问题对应的回答来生成最终的回答。
    image

  • Query Expansion
    :这是一个通过为LLM提供一个查询并生成新的内容来扩展查询的过程。适用于文档检索,特别是Chain-of-Thought(CoT 思维链)提示。

    使用生成的答案进行扩展
    :该方式中,让LLM基于我们的查询生成一个假设的回答,然后将该回答追加到我们的查询中,并执行嵌入搜索。通过使用假设的答案来扩展查询,这样在进行嵌入搜索时可以检索到其他相关向量空间,进而可以提供更准确的回答。

    使用多个查询进行扩展
    :使用LLM基于我们的查询来生成额外的类似查询,然后将这些额外的查询和原始查询一起传给向量数据库进行检索,从而可以大大提升准确性。
    注意
    需要通过对提示进行迭代来评估哪些提示会产生最佳结果。


    image

伪文档(Psuedo documents)

伪文档嵌入 (HyDE)
:当向向量数据库询问问题时,该数据库有可能不会很好地标记相关性。因此更好的方式是先创建一个假设的回答,然后再查询匹配的向量。需要注意的是,虽然这比直接查询到答案的嵌入匹配要好,但对于高度不相关的问题和上下文,产生幻觉的可能性也会更高,因此需要对过程进行优化,并注意这些边缘情况。

image

作为RAG流程的第一个步骤,查询转换并不存在正确或错误的方式。这是一个带有实验性质的领域,只有通过构建才能知道哪些方式最适合你的使用场景。

路由和请求构造

路由

路由为了将请求发送到与与请求内容相关的存储。

image

由于环境中可能存在多个数据库和向量存储,而答案可能位于其中其中任何一个,因此需要对查询进行路由。基于用户查询和预定义的选择,LLM可以决定:

  • 正确的数据源

  • 需要执行的动作:例如,概括 vs 语义搜索

  • 是否并行执行多个选择,并校对结果(
    多路由功能
    )

下面是一些路由请求的方式:

  • 逻辑路由:在这种情况下,我们让LLM根据预定义的路径来决定参考知识库的哪个部分。这对于创建非确定性链非常有用,一个步骤的输出产生下一个步骤。
    该方式用于通过LLM来选择知识库


    image

    下面是一个逻辑路由的
    例子
    ,用于根据用户的编程语言来选择合适的数据源:

    from typing import Literal
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.pydantic_v1 import BaseModel, Field
    from langchain_openai import ChatOpenAI
    
    # Data model
    class RouteQuery(BaseModel):
        """Route a user query to the most relevant datasource."""
    
        datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field(
            ...,
            description="Given a user question choose which datasource would be most relevant for answering their question",
        )
    
    # LLM with function call 
    llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
    structured_llm = llm.with_structured_output(RouteQuery)
    
    # 构造提示,根据用户问题中的编程语言类型类选择合适的数据源
    system = """You are an expert at routing a user question to the appropriate data source.
    
    Based on the programming language the question is referring to, route it to the relevant data source."""
    
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "{question}"),
        ]
    )
    
    # 定义一个 router,通过给LLM(structured_llm)输入提示(prompt)来产生结果
    router = prompt | structured_llm
    

    下面是使用方式。用户给出
    question
    ,然后调用
    router.invoke
    让LLM找出合适的数据源,最终会在
    result.datasource
    中返回
    python_docs

    question = """Why doesn't the following code work:
    
    from langchain_core.prompts import ChatPromptTemplate
    
    prompt = ChatPromptTemplate.from_messages(["human", "speak in {language}"])
    prompt.invoke("french")
    """
    
    result = router.invoke({"question": question})
    
  • 语义路由:使用基于上下文的提示来增强用户查询的强大方法。可以帮助LLMs快速、经济地选择路由(可以预定义或自定义选项),产生确定性的结果。
    该方式用于通过LLM选择提示


    image

    下面是一个使用语义路由的例子,它嵌入了两个提示模板,分别用于处理物理和数学问题,然后通过匹配用户问题和提示模板的相似性程度来选择合适的提示模板,然后应用到LLM中。

    from langchain.utils.math import cosine_similarity
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.prompts import PromptTemplate
    from langchain_core.runnables import RunnableLambda, RunnablePassthrough
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    
    # 创建两个提示模板,一个用于解决物理问题,另一个用于解决数学问题
    physics_template = """You are a very smart physics professor. \
    You are great at answering questions about physics in a concise and easy to understand manner. \
    When you don't know the answer to a question you admit that you don't know.
    
    Here is a question:
    {query}"""
    
    math_template = """You are a very good mathematician. You are great at answering math questions. \
    You are so good because you are able to break down hard problems into their component parts, \
    answer the component parts, and then put them together to answer the broader question.
    
    Here is a question:
    {query}"""
    
    # 嵌入提示
    embeddings = OpenAIEmbeddings()
    prompt_templates = [physics_template, math_template]
    prompt_embeddings = embeddings.embed_documents(prompt_templates)
    
    # Route question to prompt 
    def prompt_router(input):
        # 嵌入查询
        query_embedding = embeddings.embed_query(input["query"])
        # 计算查询和提示模板的相似度
        similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
        most_similar = prompt_templates[similarity.argmax()]
        # 根据相似度来选择最合适的提示模板 
        print("Using MATH" if most_similar == math_template else "Using PHYSICS")
        return PromptTemplate.from_template(most_similar)
    
    
    chain = (
        {"query": RunnablePassthrough()}
        | RunnableLambda(prompt_router)
        | ChatOpenAI()
        | StrOutputParser()
    )
    
    # 用户提问,什么是黑洞
    print(chain.invoke("What's a black hole"))
    

更多参见
Rag From Scratch: Routing

请求构造

请求构造是为了解决针对特定类型数据库查询的问题。

image

在定义好路由之后是否就可以跨数据存储发送请求?如果使用的是非结构化数据存储就可以,但实际中,大部分数据都保存在结构化数据库中,因此在构造请求的同时需要考虑到数据库的类型。

image

我们很容易会假设和LLMs交互使用的是自然语言,但这是不对的,查询采用的语言类型取决于和数据存储的交互方式,因此在构建查询时需要牢记数据库的查询语言(从使用SQL的关系型数据库到使用相关结构化元数据的非结构化数据)。

image

a.
自查询检索器-Self-query retriever
(文本->元数据的过滤器):向量数据库中带有
清晰元数据文件
的非结构化数据可以启用此类retriever。任何用户问题(自然语言格式)都可以被拆分为一个查询和一个过滤器(如根据年、电影、艺术家)。通过提升发送到LLM的数据质量,可以大大提升Q&A的工作流表现。

image

b.
文本-> SQL:
通常LLMs对Text2SQL的表现不佳,去年有很多初创公司就将焦点放在如何解决此类问题上。从创建虚构的表和字段到用户查询中的拼写错误等原因都可能导致LLMs执行失败。鉴于这种数据库相当普遍,因此出现了很多帮助LLM准确创建SQL查询的方式,但都与特定的知识库相关,因此不存在可以用于多种数据库的通用方式。对于一个新的数据库,只能通过构建、迭代、修复来优化LLMs的表现。

  • Create Table + Select 3
    :在了解更多高级技术之前,这是获得模型在数据库上的表现基线的最直接的方法。在Prompt设计中,对于每张表,可以包括一个CREATE TABLE描述,并在一个SELECT语句中提供三个示例行。

image

  • 少量样本示例
    :为LLM提供少量Q&A示例来帮助它理解如何构建请求,通过这种方式可以提升10~15%的准确性。根据示例的质量和使用的模型,添加更多的示例可以获得更高的准确性。在有大量示例的情况下,可以将其保存在向量存储中,然后通过对输入查询进行语义搜索,动态选择其中的一部分。


    image
  • 此外还有一篇不错的
    博客
    展示了fine-tuning带来的巨大提升:
    image

  • RAG + Fine-tuning
    :相比于只是为了让语言模型能够理解而将整个schema添加到提示中,使用经过调优的schema RAG和ICL的模型可以将准确性提高20%以上。
    image

  • 用户拼写错误
    :通过搜索合适的名词而不是使用包含正确拼写的向量存储,是减少用户特定错误的一种好方法。这在早期的Text2SQL中是一个特别令人烦恼的问题。

c.
文本-> Cyphe
r

:与图数据库有关,用于表达无法使用表格形式表示的关系。Cypher是这类数据库的查询语言。text-2-Cypher 是一项复杂的任务,对于此类任务,推荐使用GPT4。

知识图谱可以
有效
提高检索的准确性

,因此数据格式不应该局限于表和2D结构。

image

下面是构造请求的
例子

Examples Data source References
Text-to-metadata-filter Vectorstores Docs
Text-to-SQL SQL DB Docs
,
blog
,
blog
Text-to-SQL+ Semantic PGVecvtor supported SQL DB Cookbook
Text-to-Cypher Graph databases Blog
,
Blog
,
Docs

请求转换、构造和路由可以帮助我们和请求的数据库进行交互。接下来,我们将进入称为索引(Indexing)的工作流程部分,在那里,我们将更深入地研究拆分、索引嵌入以及如何通过配置这些功能来实现准确检索。

索引

在LLM中,文档会以chunk进行切分,索引用于找到与查询相关的chunk

在上文中,我们讨论了在构造查询时需要考虑到与数据库交互所使用的语言。这里要讲的索引类似被查询的数据,索引的实现方式有很多种,但目的都是为了在不丢失上下文的情况下方便LLM的理解。由于对应用户问题的答案可能位于文档中的任何地方,且考虑到LLMs在实时数据、上下文窗口和"中间遗失"问题中的不足,因此有效划分chunk并将其添加到上下文中非常重要。

chunk划分和嵌入是实现准确检索的索引核心部分。简单来说,嵌入向量是将一个大而复杂的数据集转化为一组能够捕捉所嵌入的数据本质的数字。这样就可以将用户请求转化为一个嵌入向量(一组数字),然后基于语义相似性来检索信息。它们在高纬度空间的呈现如下(
相似的词的距离相近
):

image

回到chunk划分,为了方便理解,假设有一个大型文档,如电子书。你希望从这本书中解答问题,但由于它远超LLMs的上下文窗口,我们可能需要对其分块,然后将于用户问题相关的部分提供给LLM。而当执行分块时,我们不希望因为分块而丢失故事的角色上下文,此时就会用到索引。

下面是一些索引方式:

chunk优化

首先需要考虑数据本身的长度,它定义了chunk的划分策略以及使用的模型。例如,如果要嵌入一个句子,使用
sentence transformer
可能就足够了,但对于大型文档,可能需要根据tokens来划分chunk,如使用
text-embedding-ada-002

其次需要考虑的是这些嵌入向量的最终使用场景。你需要创建一个Q&A机器人?一个摘要工具?还是作为一个代理工具,将其输出导入到其他LLM中进一步处理?如果是后者,可能需要限制传递到下一个LLM的上下文窗口的输出长度。

image

下面提供了几种实现策略:

基于规则

使用分隔符(如空格、标点符号等)来切分文本:

  • 固定长度
    :最简单的方式是根据固定数目的字符来划分chunk。在这种方式下,缓解上下文遗失的方式是在每个chunk中添加重叠的部分(可自定义)。但这种方式并不理想,可以使用langchain的
    CharacterTextSplitter
    进行
    测试


    image

    然后是所谓的结构感知拆分器,即基于句子、段落等划分chunk。

  • NLTK语句分词器(
    Sentence
    Tokenizer
    )
    :将给定的文本切分为语句。这种方式虽然简单,但仍然受限于对底层文本的语义理解,即便在一开始的测试中表项良好,但在上下文跨多个语句或段落的场景下仍然不理想(而这正是我们希望LLM查询的文本)。


    image
  • Spacy
    语句分割器(

    Sentence Splitter
    )
    :另一种是基于语句进行拆分,在希望引用小型chunks时有用。但仍存在和NLTK类型的问题。


    image

递归结构感知拆分(Recursive structure aware splitting)

结合固定长度和结构感知策略可以得到递归结构感知拆分策略。Langchain文档中大量采用了这种方式,其好处是可以更好地控制上下文。为了方便语义搜索,此时chunk大小不再相等,但仍然
不适合结构化数据

image

内容感知拆分

对于非结构化数据来说,上面几种策略可能就足够了,但对于结构化数据,就需要根据结构本身类型进行拆分。这就是为什么有专门用于Markdown、LaTeX、HTML、带表格的pdf、多模式(即文本+图像等)的文本拆分器。

image

多表示索引

相比于将整个文档进行拆分,然后根据语义相似性检索出 top-k的结果,那如果将文本转换为压缩的检索单元会怎样?例如,压缩为摘要。

父文档
(Parent Document)

这种场景下可以根据用户的请求检索到最相关的chunk并传递该chunk所属的父文档,而不是仅仅将某个chunk传递给LLM。通过这种方式可以提升上下文并增强检索能力。但如果父文档比LLM的上下文窗口还要大怎么办?为了匹配上下文窗口,我们可以将较大的chunks和较小的chunks一起传递给LLM(而不是整个父文档)。

image

上面vectorstores中存储的是较小的 chunks,使用InMemoryStore存储较大的 chunk。

在使用时文档会被分为大小两种chunk,其中小chunk都有其所属的大chunk。由于"chunk越小,其表达的语义精确性更高",因此在查询时,首先检索到较小的chunk,而较小的chunk的元数据中保存了其所属的大chunk,因而可以将小chunk和其所属的大chunk一起传递给LLM。

密集检索(Dense X Retrieval)

密集检索是一种使用非语句或段落chunk进行上下文检索的新方法。在下面
论文
中,作者将其称之为"proposition",一个proposition包含:

  • 文本中的不同含义:需要捕获这些含义,这样所有propositions一起就能在语义上覆盖整个文本。
  • 最小单元:即不能再进一步拆分propositions。
  • 上下文相关和自包含:即每个propositions应该包含所有必需的上下文。

image
image

Proposition级级别的检索比语句级别的检索和篇章级别的检索分别高35%和22.5%(显著提高)。

特定嵌入

领域特定和/或高级嵌入模型。

  • Fine-tuning:对嵌入模型进行微调可以帮助改进RAG pipeline检索相关文档的能力。这里,我们使用LLM生成的查询、文本语料库以及两者之间的交叉参考映射来磅数嵌入模型理解需要查找的语料库。微调嵌入模型可以帮助提升大约5~10%的表现。

    下面是
    Jerry Liu对
    微调嵌入模型的建议:


    1. 在项目开始时,需要在嵌入文档前对基础模型进行微调
    2. 由于生产环境中的文档分布可能会发生变化,因此在微调查询适配器(query adapter)时需要确保嵌入的文档不会发生变化。
  • ColBERT
    :这是一个检索模型,它能够基于BERT在大规模数据集上实现可扩展的搜索(毫秒级别)。这里,快速和准确检索是关键。

    它会将每个段落编码为token级嵌入的矩阵(如下蓝色所示)。当执行一个搜索时,它会将用户查询编码为另一个token级嵌入的矩阵(如下绿色所示)。然后基于上下文匹使用"可扩展的向量相似性(
    MaxSim
    )操作"来匹配查询段落。

    image

    后期交互是实现快速和可扩展检索的关键。虽然交叉编码器会对每个可能的查询和文档对进行评估,从而提高准确性,但对于大规模应用程序而言,该特性会导致计算成本不断累积。为了实现大型数据集的快速检索,需要提前计算文档嵌入,因而需要平衡计算成本和检索质量。

分层索引

斯坦福大学研究人员基于不同层次的文档摘要树提出了
RAPTOR
模型,即通过对文本块的聚类进行摘要来实现更准确的检索。文本摘要涵盖了更大范围的上下文,跨越不同的尺度,包括主题理解和细粒度的内容。检索增强的文本摘要涵盖了更大范围不同主题理解和粒度的上下文。

论文声称,通过递归摘要进行检索可以显著提升模型表现。"
在涉及复杂多步骤推理的问答任务中,我们展示了最佳结果。例如,通过将RAPTOR检索与GPT-4相结合,我们可以在QuALITY基准测试的最佳表现上提高20%的绝对准确率。
"

image

llamaindex提供了这种
实现

检索

检索可以看做是对索引到的数据的进一步提炼。

在完成数据检索之后,下一步需要根据用户的请求来获取相关数据。最常见和最直接的方法是从之前索引的数据(最近的邻居)中识别并获取与用户查询在语义上最接近的chunks。类似
如下
向量空间:

image

检索并不是一步到位的,而是从查询转换开始的一些列步骤,以及如何通过检索来提升获取到相关的chunks之后的检索质量。假设我们已经根据相似性搜索,从向量数据库中找到前k个chunks,但由于大部分chunks存在重复内容或无法适应LLM的上下文窗口。因此在将chunks传递给LLM之前,我们需要通过一些检索技术来提升上下文的质量。

LLMs的世界中,并不存在一劳永逸的方法,需要根据使用场景和chunks的特性来找到合适的技术。下面是一些典型的方法:

Ranking

Reranking

如果我们想要从数据库的chunk中查找答案,可以选择Reranking,它是一种可以给LLM提供最相关上下文的有效策略。有多种实现方式:

  • 提升多样性:最常见的方法是
    最大边缘相关性
    (Maximum Marginal Relevance-MMR),这可以通过因子a)与请求的相似度或b)与已选的文档的距离等因子来实现。

    这里有必要提一下
    Haystack
    的DiversityRanker:


    1. 首先计算每个文档的嵌入向量,然后使用一个sentence-transformer模型进行查询搜索
    2. 将语义上和查询最接近的文档作为第一个选择的文档
    3. 对于剩下的每个文档,会计算和所选择文档的平均相似性
    4. 然后选择和所选择文档最不相似的文档
    5. 然后重复上述步骤,直到选出所有文档,这样就得到了一个整体多样性从高到低排序的文档列表。

    下面介绍了几种重排序的方式,称为reranker。


    image
  • LostInTheMiddleReranker
    :这是Haystack的另一个解决LLM不擅长从文档中间检索信息的问题的方法。它通过重排序来让最佳的文档位于上下文窗口的开头和结尾。建议在相关性和多样性之后再使用该Reranker。
    image


    image
  • CohereRerank
    :通过Cohere的Rerank endpoint实现,它会将一开始的搜索结果和用户的请求进行比较,并基于请求文本和文档之间的语义相似性来重新计算结果,而不仅仅根据向量的查询结果。


    image
  • bge-rerank
    :除了要选择出最适合你的数据的嵌入模型外,还需要重点关注可以和该嵌入模型配合使用的检索器(retriever)。我们使用命中率和平均倒数排名(Mean Reciprocal Rank-MRR)作为retriever的评估指标。命中率是指在前k个检索到的chunks中找到正确答案的频率,MRR是排名中最相关的文档在排名中的的位置。从下面可以看到,JinaAI-Base嵌入中用到的bge-rerank-large 或 cohere-reranker 看起来非常适用于该数据集。
    下表
    中需要注意的是,嵌入模型并不是最好的rerankers。Jina最擅长嵌入,而bge reranker则最擅长重新排序。
    image

  • mxbai-rerank-v1
    :最新的一种重排序模型是由Mixedbread团队开发的开源项目,称为SOTA。其表现声称要优于Cohere和bge-large。


    image
  • RankGPT
    :这是使用现有的LLMs(如GPT3.5)重排检索文档的方法之一,且重排质量好于Cohere。为了解决检索上下文大于LLM的上下文窗口的问题,该方法使用了一种"滑动窗口"策略,即在滑动窗口内实现渐进排名。这种方式击败了其他大部分reranker,其中包括Cohere。这种方式下需要注意的是延迟和成本,因此适用于优化小型开源模型。

    image

提示压缩(Prompt Compression)

这里有必要介绍一下Prompt Compression,它和Reranking关系密切。这是一种通过压缩无关信息(即和用户查询无关)来降低检索到的文档的技术,有如下方式:

  • LongLLMLingua
    :该方式基于
    Selective-Context

    LLMLingua
    框架,是用于提示压缩的SOTA方法,针对成本和延迟进行了优化。除此之外,LongLLMLingua采用了一种"
    采用问题感知由粗到细的压缩方法、文档重新排序机制、动态压缩比例以及压缩后的子序列恢复等策略来提升大型语言模型对关键信息的感知能力
    "的方式来提升检索质量。该
    方法
    特别适用于长上下文文档,解决"
    中间遗失
    "问题。


    image
  • RECOMP
    :使用"压缩器",该方法使用文本摘要作为LLMs的上下文来降低成本并提升推理质量。两个压缩器分别表示: a)提取压缩器,用于从检索的文档中选择相关语句;b)抽象压缩器,用于根据多个文档的合成信息来创建摘要。


    image
  • Walking Down the Memory Maze
    :该方法引入了MEMWALKER的概念,即按照树的格式来处理上下文。该方法会对上下文划分chunk,然后对每个部分进行概括。为了回答一个查询,该模型会沿着树结构来迭代提示,以此来找出包含问题答案的段。特别适用于较长序列。


    image

RAG-fusion

这是
Adrian Raudaschl
提出的一种方法,使用
Reciprocal Rank Fusion (RRF)
和生成查询来提高检索质量。该方法会使用一个LLM根据输入生成多个用户查询,并为每个查询运行一个向量搜索,最后根据RRF聚合并优化结果。在最后一步中,LLM会使用查询和重排序列表来生成最终输出。这种方法在响应查询时具有更好的深度,因此非常流行。

image

改进

CRAG
(Corrective Retrieval Augmented Generation)

该方法旨在解决从静态和有限数据中进行次优检索的局限性,它使用一个轻量的检索评估器以及外部数据源(如web搜索)来补充最终的生成步骤。检索评估器会对检索的文档和输入进行评估,并给出一个置信度,用于触发下游的knowledge动作。

image


下面
报告结果可以看出其极大提升了基于RAG方法的表现:

image

FLARE
(Forward Looking Active Retrieval)

该方法特别适用于长文本生成,它会生成一个临时的"下一个语句",并根据该语句是否包含
低概率tokens
来决定是否执行检索。该方法可以很好地决定什么时候进行检索,可以降低幻觉以及非事实的输出。

image

上图展示了FLARE的工作原理。当用户输入
x
,并检索出结果Dx,FLARE会迭代生成一个临时的"下一句"(灰色字体),然后检查它是否包含低概率的tokens(下划线表示),如果包含(步骤2和3),则系统会检索相关的文档并重新生成语句。

考虑到需要优化的变量数目,检索可能比较棘手,但通过对用户场景和数据类型进行映射,可以大大降低成本、延迟并提升准确性。

下面将面对文本生成(Generation),并思考一些评估策略。

生成和评估

生成(generation)

使用索引和检索可以保证输出的完整性,接下来,需要在为用户生成结果之前对结果进行评估,并通过决策步骤来触发相应的动作。Language Agents (
CoALA
) 的认知架构是一个很好的框架,可以将其放在上下文中,通过对检索到的信息的评估来获得一组可能性。如下图所示:

image

有如下几种方法可以实现这一动作选择决策程序:

Corrective RAG (CRAG)

在上一章(检索)中有提到过该方法,但它与本章节内容有所重叠,因此有必要展开介绍一下。

image

CRAG使用轻量级"检索评估器"来增强generation,检索评估器会为每个检索到的文档生成一个置信值。该值确定了需要触发的检索动作。例如,评估器可以根据置信值来为检索到的文档标记到三个桶(正确、模糊、不正确)中的某个桶中。

如果所有检索到的文档的置信值都低于阈值,retriever会认为"不正确",然后会通过触发外部知识源(如web搜索)动作来生成合格的结果。

如果至少有一个检索到的文档的置信值大于阈值,则会假设此次检索是"正确的",然后会触发 knowledge refinement来改进检索到的文档。 knowledge refinement需要将文档拆分为"知识条(knowledge strips)",然后根据相关性对每个strip打分,最相关的知识会被重新组合为generation使用的内部知识。

当检索评估器对自己的判断没有信心时会标记为"模糊",此时会同时采用上述策略。见如下决策树:

image

上述方法跨了四个数据来生成最佳结果。下表中可以看到,CRAG显著优于RAG。self-CRAG则使这些差距更加明显,并且展示了CRAG作为RAG流水线"即插即用"的适应性。另外一个CRAG由于其他方法(如Self-RAG)的点是它可以灵活地替换底层LLM,如果未来可能要采用更加强大的LLM,这一点至关重要。

image

CRAG的一个明显限制是它严重依赖检索评估器的质量,并且容易受到网络搜索可能引入的偏见的影响。因此可能需要对检索评估器进行微调,且需要分配必要的"护栏"来确保输出的质量和准确性。

Self-RAG

这是另一种可以提升LLM的质量和事实性,同时保持其多功能性的框架。与其检索固定数目的段落(不管这些段落是相关还是不相关的),该框架则更关注按需检索和自我反省。

image

  1. 将任一LLM训练为可以对自己生成的结果进行自我反省的模型(通过一种称为reflection tokens(retrieval 和 critique) 的特殊tokens)。检索是由retrieval token触发的,该token是由LLM根据输入提示和之前的生成结果输出的。
  2. 然后LLM会通过并行处理这些检索到的段落来并发生成结果。该步骤会触发LLM生成用于评估这些结果的critique tokens。
  3. 最后根据"真实性"和"相关性"来选择用于最终generation的最佳结果。论文中描述了算法,其中tokens的定义如下:
    image

下面Langchain的
示意图
展示了基于上述定义的reflection token的Self-RAG 推理决策算法。

image

就表现而言,无论是否进行检索,Self-RAG都显著优于基线(参考CRAG的Benchmark)。需要注意的是,可以通过在CRAG之上构建Self-CRAG来进一步提升模型表现。

该框架的成本和延迟受到LLM调用的数量的限制。可以通过一些变通方法,比如一次生成一代,而不是两次为每个块生成一代进行优化。

RRR (Rewrite-Retrieve-Read)

RRR
模式
[Ma et al., 2023a]
引入了 重写-检索-读写 流程,利用LLM来强化rewriter模块,从而实现对检索查询的微调,进而提高
reader
的下游任务表现。

该框架假设用户查询可以被LLM进一步优化(及重写),从而实现更精确的检索。虽然这种方式可以通过LLMs的查询重写过程来提升表现,但也存在推理错误或无效查询之类的问题,因此可能不适用于部署在生产环境中。

image

为了解决上图中描述的问题,需要在rewriter上添加一个可训练的小型LLM(如上图红色部分所示)来持续提升训练的表现,使之兼具可扩展性和高效性。这里的训练包含两个阶段:"预热"和增强学习,其关键是将可训练模块集成到较大的LLM中。

评估

RAG pipeline中实现评估的方式有很多种,如使用一组Q&A作为测试数据集,并将输出与实际答案进行对比验证。但这种方式的缺点是不仅需要花费大量时间,并且增加了针对数据集本身优化pipeline的风险(而非可能存在很多边缘场景的现实世界)。

RAGAs

RAGAs
(RAG Assessment的简称)是一个用于评估RAG pipeline的开源框架,它可以通过如下方式评估pipeline:

  • 提供基于"
    ground truth
    "(真实数据)来生成测试数据的方式
  • 为检索和generation阶段以独立或端到端的方式提供基于指标的评估

image

它使用如下RAG特征作为指标:

  1. 真实性
    :事实一致性
  2. 回答相关性
    :问题和查询的相关性
  3. 上下文准确性
    :校验相关chunks的排名是否更高
  4. 批判(Critique)
    :根据预定义的特征(例如无害和正确性)评估提交
  5. 上下文回顾
    :通过比较ground truth和上下文来校验是否检索到所有相关信息
  6. 上下文实体回顾
    :评估上下文检索到的和ground truth中的实体数目
  7. 上下文相关性
    :检索到的上下文和提示的相关性
  8. 回答语义相似性
    :生成的答案和实际的语义相似性
  9. 回答正确性
    :评估生成答案与实际答案的准确性和一致性

上面提供了一组可以全面评估RAG的列表,推荐阅读
Ragas
文档。

Langsmith

Langchain的Langsmith可以作为上面上下文中的可观测性和监控工具。

LangSmith
是一个可以帮助调试、测试、评估和监控基于任何LLM空间构建的chains和agents平台。

通过将Langsmith和RAG进行结合可以帮助我们更深入地了解结果。通过Langsmith评估logging和tracing部分可以更好地了解到对哪个阶段进行优化可以提升retriever或generator。

image

DeepEval

另一种值得提及的评估框架称为DeepEval。它提供了14种以上涵盖RAG和微调的指标,包括G-Eval、RAGAS、Summarization、Hallucination、Bias、Toxicity 等。

这些指标的自解释信很好地说明了指标评分的能力,使之更易于调试。这是跟上面的RAGAs的关键区别。

此外,它还有一些不错的特性,如集成
Pytest
(开发者友好),其模块化组件等也是开源的。

Evaluate → Iterate → Optimize

通过生成和评估阶段,我们不仅可以部署一个RAG系统,还可以评估、迭代并针对我们设计的特定场景进行优化。

随着自动化测试和人工智能技术的迅猛发展,软件测试行业正经历着前所未有的变革。这一变革不仅重塑了行业的生态,也引发了人们对软件测试职业未来的深思。

在知乎,有一个有趣的问题引发了广泛的讨论:为什么互联网公司不选择开除测试团队,转而采取众包模式,让广大网民参与测试,每找到一个漏洞就奖励100元呢?

“失业的风”最终从前端吹到了测试……

一、如果真的让大众来测会怎么样?

“开除测试,转而让大众来测”这种看似简单明快的决策,其背后涉及到的深层次问题和风险往往被人们所忽视。在理想情况下,老板通过开除专业的测试团队,转而利用大众的智慧和力量来进行产品测试,确实可以节省一大笔开支。同时,大众通过参与测试,不仅能够获得一定的报酬,还能在测试过程中提出有价值的建议和问题,为产品的改进提供助力。

这种情况下,双方都能从中受益,看似是一个完美的解决方案。然而,现实往往并不如理想那么美好。

  • 质量与效率问题

大众测试虽然可以节省成本,但往往难以保证测试的质量和效率。一般情况下, 开发人员和测试人员会并行配合,在开发的同时进行测试以保证产品能够快速迭代。与专业测试团队相比,测试者的参与度和热情也很难持续保持高涨,这可能会影响到测试的进度和效果。产品只有等待开发全部结束后才能够让大众进行测试,这就会拉长产品进入市场的战线。

  • 安全问题

大众测试也存在一定的风险。由于测试者的身份和背景各不相同,他们可能会在产品测试过程中泄露敏感信息或恶意攻击系统。这不仅会对公司的业务造成损失,还可能引发法律纠纷和声誉风险。

  • 创新问题

从长期来看,过度依赖大众测试可能会对公司的创新能力产生负面影响。专业测试团队在测试过程中不仅能够发现问题,还能提出有针对性的改进建议。而大众测试者往往只关注表面问题,很难提出具有深度和洞察力的建议。这可能会导致公司在产品设计和改进方面陷入僵局,难以取得突破性的进展。

二、 软件测试真的可有可无吗?

在当今的软件开发过程中,软件测试的地位愈发重要。然而,这并不意味着所有的开发者或决策者都对其给予足够的重视。
实际上,我们可以从这个问题中窥见一种观念:软件测试岗位似乎被视为可有可无的存在。
这种观念的存在,不仅揭示了软件测试在某些团队中的边缘化地位,也反映出对软件质量保障和用户体验的忽视。

在《敏捷成功》一书中,Mike Cohn提出了“测试金字塔”这一重要概念,
它通过视觉隐喻向我们展示了不同层次的测试。从下到上的三层分别:

  • 单元测试(代码层面)
  • 服务测试(模块间API测试)
  • UI测试(端到端测试)

测试金字塔构建了一个健康、快速和可维护的测试组合,这让产品保持在一个稳定状态,使得用户的使用体验良好,满足用户绝大部分需求。
我们不难看出,问题中所说的Bug更多停留在测试金字塔的最顶层UI测试,这只占了全部测试的10%。

我们必须认识到,
软件测试并不是一项简单的、可有可无的任务。
相反,它是对软件质量和稳定性的关键保障。测试工程师负责在软件开发的不同阶段进行各种测试,包括功能测试、性能测试、安全测试等,以确保软件在各种场景下都能正常工作,满足用户的需求。在这个过程中,他们不仅要找出软件中存在的问题,还要提出改进建议,帮助开发团队提升软件质量。

然而,由于一些开发者或决策者缺乏对软件测试的深入了解,他们可能认为测试只是开发过程中的一个附加环节,甚至将其视为一种负担。这种观念不仅忽视了软件测试在保障软件质量方面的重要作用,还可能导致开发过程中的问题无法及时发现和解决,最终影响软件的稳定性和用户体验。

因此,产品开发的早期阶段需要将测试交给专业的内部团队,产品成熟阶段可以考虑进行公开的测试活动,例如很多游戏在正式上线之前会抽取部分玩家进行公测,以确保游戏在上线后能提高用户的使用感。

随着软件行业的不断发展,软件测试的重要性日益凸显。越来越多的企业和团队开始意识到,只有将软件测试作为开发过程中不可或缺的一部分,才能真正确保软件的质量和用户体验。例如,许多大型互联网公司都设立了专门的测试团队,负责全面覆盖软件测试的各个方面。同时,随着自动化测试技术的发展,测试工程师也能够更高效地完成测试任务,为开发团队提供更好的支持。

因此,我们应该摒弃将软件测试视为可有可无的观念,重新认识其在软件开发过程中的重要地位。只有这样,我们才能在激烈的市场竞争中保持竞争力,为用户提供更加优质的产品和服务。

三、 禅道团队如何做?

软件一旦上线,用户的使用就是对软件的不断测试。用户在使用过程中,可能会遇到各种各样的问题,如界面卡顿、功能缺陷、数据丢失等。这些问题不仅影响了用户的使用体验,还可能对软件的品牌形象造成负面影响。因此,了解与产品性能相关的用户痛点至关重要。

为了深入了解用户在使用软件过程中的真实体验,开发者需要建立一个有效的用户反馈渠道
。这一渠道不仅可以帮助开发者及时获取用户的反馈和建议,还能够为产品优化提供宝贵的数据支持。通过用户的反馈,开发者可以了解到哪些功能最受欢迎,哪些功能存在缺陷,以及用户在使用过程中的具体痛点。


禅道团队
为例,

我们建立了多元化的沟通渠道,包括技术问答、论坛、QQ群、邮件等,以便及时捕捉用户的声音。

(禅道官网论坛)

(禅道客户QQ群)

对于这些反馈,我们会有不同的处理方式:

  • 如果能够直接解答,我们的技术支持同事会直接给解决方案。
  • 如果是功能性改进意见或是Bug,我们会通过禅道项目管理软件记录到反馈模块中,然后安排专门的同事对反馈进行梳理。 如果是需要紧急处理的,我们会转成工单,安排应急响应小组的同事给临时解决方案。临时解决方案会以补丁的方式交付给我们的客户,然后再转成需求或Bug进行相应的处理。
  • 如果反馈里面不需要紧急处理的,我们会判断是需求还是Bug,然后进行相应的转化。需求和Bug会根据优先级来进行排期,进入正常的产品研发流程。
  • 当发布之后,我们会通知相应的反馈者。

四、 写在最后

在探讨专业岗位的态度问题时,我们不得不提及一个经常被误解和误用的概念——反智主义。反智主义,简而言之,就是对知识和智慧的蔑视和拒绝。

在现今这个快速发展的社会中,对待任何一个专业岗位,我们都应保持一颗敬畏之心,将反智主义视为一种玩笑的态度,而绝非我们行事的准则。