跳转至

28 文本分类和实体提取

你好,我是陈旭。

第26讲中,我们探讨了低代码平台与大模型技术结合的两种模式:Copilot模式和Agent模式。Copilot模式以指令式AI助手为核心,依赖大模型的意图分类和实体提取能力,帮助用户快速完成任务,尤其适合低代码平台初学者,不但降低了学习成本,而且能有效解决重复性和简单逻辑性任务。

然而,对于复杂逻辑任务,Copilot模式的表现仍受限于低代码平台的可视化操作特性。而Agent模式则进一步提升了AI的智能水平,通过目标驱动实现自动任务规划和执行,用户只需描述最终目标,AI即可完成任务。这种模式转变了用户在开发中的角色,从具体操作转为监督与授权,显著提升了开发效率。同时,Agent模式是对Copilot模式的扩展,其复杂任务的拆解和执行仍需要依赖各类工具的协作。

我们将首先专注于构建一个低代码助手,通过实现这些基础能力为后续发展奠定基础。随着系统的不断完善,这个助手可以逐步演进为一个具备更强大能力的低代码智能体,支持更复杂的任务场景。

意图分类实体提取作为自然语言处理的基础功能,可以帮助系统更准确地理解用户的需求,并进一步触发相应的操作。掌握这两个基础的NLP处理技巧,正是我们构建低代码助手的第一步。

其中,意图分类主要将用户的要求映射为特定动作,而实体提取则进一步按照动作的需要提取出必需的参数值对。有了动作以及必需的参数后,我们就可以构建出一个大概能用的小助手了。

一般来说,接下来就要介绍提示词工程等一系列内容了,但是咱这个专栏毕竟不是专门讲解大模型的,为了加快进度,我决定跳过这部分内容,直接进入实操环节。虽然接下来的内容我们会利用一个库来辅助我们的开发工作,并不需要很多提示词工程的知识。当然,如果你了解过提示词相关知识,对理解以后的内容是很有好处的。

意图分类

意图分类的目标是分析用户输入的自然语言文本,并将其归类为某一预定义的意图。这里有一个重要的隐含信息,那就是意图都是预设的,而且是可枚举的。这不难理解,对于低代码这样的具体系统,用户对它所作的操作,总是可以预知的、可枚举的。这个信息可以大幅降低我们意图分类的实现难度。

下面是一些意图分类的示例:

  • 这个功能什么时候能上线? (提问)
  • 我刚买的手机屏幕有瑕疵,怎么办? (投诉)
  • 新版本的界面设计很不错,点赞! (反馈)
  • 今天天气真好,出去散步了。 (其他)

这是一个很好的例子,很显然的,我们对用户输入句子分类了后,就可以采用相应流程来继续处理了。尽管各个流程差异可能很大,但基本只要按部就班就可以妥善处理用户的要求。低代码助手也是这样的处理流程。

接下来我们就来看看怎么实现。

我要隆重介绍 DSPy 这个库,它非常适合用来做意图分类,以及马上要做实体提取。

这里有必要介绍一下这个库。一般我们都是使用提示词来激发大模型处理自然语言的,提示词就是一长串的文字。根据我的实践经验来看,这些提示词是非常不好管理的,最主要的一点是,你实际上是将一部分代码逻辑通过提示词的方式表达出来了。

问题在于,这些文字逻辑非常脆弱

我经常发现,稍微修改了一些提示词的编写方式,即使对人类来说毫无相干的文字修改,也会对大模型的行为造成很大的影响。而且这个变化一般非常难以测试,这非常令人头疼,这导致了很多功能bug。

文字逻辑的另一个大缺陷是,你很难编写出同时支持多种大模型的提示词。反过来说,提示词和大模型是强耦合关系。这就导致,一旦你换了另一个大模型(哪怕相同参数尺寸),你会发现使用提示词几乎都要重写!这简直是噩梦。

DSPy 采用了另一种思路来和大模型打交道,它是用于编程(而不是提示)语言模型的框架,提供了用于优化其提示和权重的算法。DSPy 将重点从修补提示字符串,转移到了使用结构化和声明性自然语言模块进行编程

简而言之就是,在DSPy 框架下,你看到的基本都是代码,几乎看不到提示词。这在某种程度上有效地解决了我前面介绍的、使用大模型过程里两个最令人头疼的问题。这样一来,我们才有可能将低代码助手这样的大模型应用做得更加复杂,也更加实用。

我们回到意图分类的实现来。如果你能去读一读 DSPy 的文档再继续,那会有更好的效果。我会尽量说得简单明了一些。

class Classify(dspy.Signature):
    """对句子进行分类。"""


    sentence: str = dspy.InputField(desc="输入句子")
    sentiment: Literal['科技', '教育', '健康', '建议', '其他'] = dspy.OutputField(
        desc="只输出句子类型,并且必须是 '科技'、'教育'、'健康'、'娱乐'或'其他' 之一"
    )
    confidence: float = dspy.OutputField(desc="分类的置信度 (0-1)")


# 创建分类器
classify = dspy.Predict(Classify)


res = classify(sentence="最新的AI模型已经可以生成非常逼真的图片了。")
print(res)

这段代码定义了一个用于分类的class,它预设了科技/教育等几个类别,我们的目的是将用户输入的每一句话都归到一个最佳类别中去。

我们需要通过后面的代码来获得一个分类器。

classify = dspy.Predict(Classify)

然后就可以对任何一句话做分类了。我测试过ChatGPT和另一个野鸡模型,运行结果是一样的。

完整的代码,我放在了这个仓库里,你可以通过超链接获取。

克隆的方法和上一讲一样,注意克隆下来后,先执行 git checkout lecture-28命令来切换到这讲专用的tag。代码在 server/src/classification.py 这个文件里。

实体提取

实体提取旨在从用户输入中识别并提取特定信息。例如,对于句子“我想明天预订一张从北京到上海的机票”,提取的实体可能包括日期“明天”、出发地 “北京” 和目的地 “上海”。

实体提取过程一般发生在意图分类之后。就像前面说的,意图分类是为了找到处理用户输入的文字的最佳流程。找到了这个流程之后,就需要找出执行这个流程所需的各种参数实体以及对应值了。脱离了具体流程的参数实体是没有意义的。比如前面的“北京”这个参数实体,只有在识别出用户是要订票的前提下,你才能知道它的实际意义。

同样的,我在这里也给你准备了一些示例代码。

class ExtractInfo(dspy.Signature):
    """Extract structured information from text."""


    text: str = dspy.InputField()
    title: str = dspy.OutputField()
    headings: list[str] = dspy.OutputField()
    entities: list[dict[str, str]] = dspy.OutputField(desc="实体及其元数据的列表")


module = dspy.Predict(ExtractInfo)
text = "2024年12月18日,李华带着他的宠物狗“毛球”来到北京的天安门广场,准备参加由“阳光公益组织”举办的慈善义卖活动。活动从上午10点持续到下午3点,吸引了许多人参加。李华买了一本《Python编程入门》,售价50元,并捐赠了一些旧衣物。随后,他前往星巴克点了一杯拿铁咖啡,准备整理下午的工作计划。他计划在明天与张强一起去上海参加一个科技展览,该展览由“未来科技协会”主办,将展示最新的AI技术成果。"
response = module(text=text)

通过response.entities就可以获得这句话中的各种实体了。完整的代码在 server/src/info_extration.py 你可以读一读,试着去运行一下。

人类通过自然语言和包括低代码助手在内的AI交互时,往往都是比较随意的。最直接的体现是,人类往往没有耐心,也不可能严谨到在一句话里将所有参数实体都表达出来。所以,我们的系统需要容忍人类的这种不严谨的表达行为,否则这将是一个极其糟糕的反人类AI助手。

一个具体的处理流程不仅定义了参数实体的实际意义,还定义了我们需要哪些参数实体,包括哪些是必须的,哪些是可选的,这一点一般不容易注意到。如何妥善处理所需的参数实体,将对人机交互体验产生非常大的影响。

一个比较好的处理方式是,从已有的自然语言输入中,尽可能提取已知的参数实体和对应值,然后再判断是否还有必选的参数实体没有对应值。若有,则直接推送一个问题给人类来询问所需的参数值。然后重复这个过程,直到所有的必选参数都有合法值为止。这是一个很自然的多轮对话,而且对话的节奏是在AI助手的掌握之中。根据我的经验,除非是测试人员,正常用户都会配合AI助手提供所需的各种参数值。

评估和优化

前面给的方法都是侧重于思维方面的,当你从工程方面去实施的时候,你会发现问题依然不少,其中最令人头疼的就是准确率的问题。

意图分类和实体提取都有准确率问题。过低的准确率会导致AI助手不可用!虽然这是一个难题,但解决的办法有很多。使用更聪明的模型是选项之一,但是更高的费用,更长的推理时延是绕不过去的代价。我们需要一种更高性价比的解决方法。

DSPy 提供了解决方法,根据它的文档以及我们的实测,在模型不变的前提下,经过它的优化后,准确率大概能提高15%~25%,这是很可观的。

能给准确的提升效果数据,意味着它能给出量化的评估结果,这是非常棒的。对于比较正规的项目化运作的产品来说,可量化是非常重要的品控手段,同时也是可信的产品宣传的重要手段。

我准备了一些示例代码,你需要先执行 git checkout lecture-28-1命令来切换到正确的tag,然后打开这个文件 server/src/test_evaluate.py。我们准备了 CoNLL-2003 数据集,该数据集通常用于实体提取任务。通常可用直接从Hugging Face上下载这个数据集,但是国内的网络环境会导致你大概率下载失败,所以我把这个数据集也下载好,放在了配套的代码仓库中了。

def load_conll_dataset() -> dict:
    """
    Loads the CoNLL-2003 dataset into train, validation, and test splits.
    
    Returns:
        dict: Dataset splits with keys 'train', 'validation', and 'test'.
    """
    os.environ["HF_DATASETS_CACHE"] = "../datasets"
    return load_dataset("conll2003", trust_remote_code=True)

在这份代码中,我通过设置HF_DATASETS_CACHE环境来直接读取仓库里的数据集,以避免网络下载超时的问题。

接下来要定义量度和评估函数,评估函数使用 DSPy 的 Evaluate 实用程序来处理结果的并行性和可视化。在 DSPy 中,评估程序的性能对于迭代开发至关重要。一个好的评估框架使我们能够:

  • 衡量我们计划输出的质量。
  • 将输出与真值标签进行比较。
  • 确定需要改进的领域。

具体代码如下所示。

def extraction_correctness_metric(example: dspy.Example, prediction: dspy.Prediction, trace=None) -> bool:
    return prediction.extracted_people == example.expected_extracted_people


evaluate_correctness = dspy.Evaluate(
    devset=test_set,
    metric=extraction_correctness_metric,
    num_threads=12,
    display_progress=True,
    display_table=True,
    provide_traceback=True
)
evaluate_correctness(people_extractor, devset=test_set)

evaluate_correctness函数会返回当前的准确率,输出的值与不同的模型有关。

然后,我们就能优化模型了,MIPROv2是一个强大的优化器,它的优化过程大概是:

  1. 使用我们指定的大模型来自动调整提示词指令;
  2. 基于前面初始化好的训练集来构建Few-Shot示例。

这些动作都是它自动完成的,我们只需要配置好参数就可以了。

mipro_optimizer = dspy.MIPROv2(
    metric=extraction_correctness_metric,
    auto="medium",
)
optimized_people_extractor = mipro_optimizer.compile(
    people_extractor,
    trainset=train_set,
    max_bootstrapped_demos=4,
    requires_permission_to_run=False,
    minibatch=False
)

如果你有过自己手工优化提示词的经验,你会发现DSPy提供的优化方法十分独特,你甚至都看不到任何提示词,而只是调试一段代码。在实际使用过程中,这个环节最关键的工作就是如何构建训练数据集。在这个示例中,我们使用了现成的数据集,但是在具体的业务场景中,数据集往往需要人工构建,这样才能确保数据集的质量,从而切实提升优化效果。

分享一个我节约时间的经验给你。你可以仔细构建30条左右的训练数据集,然后让ChatGPT o1这样比较聪明的模型帮你合成额外的数据集出来,这样可以节约不少时间。值得注意的是,如果你训练使用的模型也是相同的模型,那么这个方法可能无法奏效。我们有自研的7B模型,这个小尺寸模型比ChatGPT o1要笨很多,在这个情况下,更聪明的ChatGPT o1模型合成的训练集才能有所帮助。

然后,使用前面评估初始数据集的相同方法,对优化后的效果重新评估,你就可以获知优化后,准确率提升了多少百分点了。如果达不到预期,则应该调整训练数据集,然后再次执行优化,直到效果满意为止。

最后,DSPy还可以将优化后的配置给保存下来。

optimized_people_extractor.save("optimized_extractor.json")

你可以将这个JSON配置发布到线上环境中,或者发布到你的软件版本中。在运行时,可以将优化的结果给load到实体提取程序中,这样就可以避免浪费时间从头开始执行优化了。

loaded_people_extractor = dspy.ChainOfThought(PeopleExtraction)
loaded_people_extractor.load("optimized_extractor.json")
loaded_people_extractor(tokens=["Italy", "recalled", "Marcello", "Cuttitta"]).extracted_people

小结

今天这讲我们讲解了意图分类和实体提取,这两个动作可以说是一切基于AI的应用的基础。意图分类主要是要搞清楚用户的目的,而实体提取则是要找出达到用户目的所需的必要参数实体和值。

当然,大模型也不是万能的,因此,我们不可能实现用户的任何意图,作为一个Copilot,我们能支持的用户意图数量就更加有限了,在Copilot模式下,我们只允许用户提出我们预设的、有限数量的意图。

相对的,Agent模式下,我们将会引入一些推理能力更强的大模型,它可以基于已有能力,包括对用户过去操作的理解以及用户的提示,来推断出实现预设之外的用户意图的步骤。这是Agent模式和Copilot模式很重要的一个区别。

一般的大模型应用,都是基于提示词的,也就是基于一段提示文字,特别是意图分类和实体提取这两个动作,它们往往都包含一些分类逻辑和提取方法或步骤,这就意味着有一些强逻辑的内容需要通过自然语言来实现,这种方式会导致程序变得更加难以维护。此外,大模型对输入的提示信息比较敏感,常常一些对人类无关的措辞或者顺序的修改,也会引起大模型反馈效果(如准确率)的变化。

为了解决这个问题,参考我们的经验,我推荐你使用类似DSPy这样的框架来实现意图识别和实体提取,它通过编码的方式,而不是自然语言的方式来实现,这样使得软件可维护性更强。

当然,DSPy不能完全替代文字提示词的方式,随着以后我们低代码Copilot的实现,依然会有许多不少地方需要使用文字提示。

思考题

基于你的低代码编辑器功能,尝试着归纳为若干大类,并给每个大类找出最常用的3个意图,以及给每个意图找出必须的参数,包括可选和必选参数。你能找出哪些意图和参数实体来呢?请在评论区里给我留言吧。

我是陈旭,我们下一讲再见。