LLM应用落地实施手册

背景

自ChatGPT诞生以来,各个企业都开始尝试引入LLM落地实施"智能"应用,而目前并没有太多文章系统地介绍应该怎么落地实施一个基于LLM的应用,到底应该做哪些步骤。本人从2023年12月份开始,陆陆续续开发了3个LLM应用的项目了。这几个项目都是会话型的应用,都借助了LLM的能力,所以想趁着记忆还算新鲜,来总结一下这类项目的一些落地实施经验。最后面我会以最近的一个项目做的事情来作为案例,供大家学习和探讨。

LLM应用的简介

LLM应用的场景

在了解怎么去落地实施LLM应用前,我们最好先看看它一般长什么样。LLM的能力本质上可以算是传统NLP任务的升级版,在问了LLM传统的NLP(Natural Language Processing,自然语言处理)任务按照应用场景划分有哪些之后,它给了我一个比较完善的答案:

  • 文本分类 (Text Classification): 将文本划分到预定义的类别中(如垃圾邮件过滤、情感分类、主题分类等)。
  • 信息抽取 (Information Extraction): 从非结构化文本中提取结构化信息(如事件抽取、知识图谱构建等)。
  • 文本生成 (Text Generation): 根据输入生成自然语言文本(如机器翻译、故事生成等)。
  • 对话系统 (Dialogue Systems / Chatbots): 与用户进行自然语言交互的系统。
  • 问答系统 (Question Answering): 根据用户提出的问题,从文本或知识库中找到答案。
  • 机器翻译 (Machine Translation): 将一种自然语言的文本自动翻译成另一种自然语言的文本。
  • 文本摘要 (Text Summarization): 生成文本的简洁概括。
  • 自然语言推理 (Natural Language Inference, NLI): 判断两个给定句子之间的逻辑关系(如蕴含、矛盾、中立)。
  • 文本编辑 (Text Editing): 自动修改和改进文本,如语法纠错、风格优化等。

我稍微进行了整理:

  • 信息抽取任务:
    • 实体提取
    • 关系提取
    • 知识图谱构建
  • 无反馈信号的生成类任务:
    • 文本摘要
    • 文本编辑
    • 机器翻译
    • 故事生成
  • 有反馈信号的生成类任务:
    • 代码生成
    • SQL生成
  • 分类任务:
    • 主题分类
    • 情感分类
    • 垃圾邮件过滤
    • 自然语言推理
  • 问答系统:
    • 问答系统
  • 对话系统
    • 对话系统

LLM应用的分类

可以看到前面几类场景我都给加上了"任务"二字,这里可以引出我对于LLM应用的分类,分别是会话型应用和任务型应用。会这么划分也是因为在这些项目上见识到了会话型应用的"复杂性",在落地实施项目前,对项目复杂性的理解也能帮助我们去规划和设计我们的项目。

任务型LLM应用

我对于任务型LLM应用的定义是:该应用没有一个用户可以对话的入口,应用的输入来自系统数据。

这类应用的基本特征包含:

  • 输入数据的复杂性比较低:输入的数据形式、分布基本上是确定的。
  • 都是批处理:批处理一般能节省成本(各大提供LLM API的厂商大多针对批处理任务提供了一定量的优惠;本地模型对于批处理任务也能提供更高的吞吐、更好的资源利用)。
  • 响应时间没有严格要求:一般批处理都是在固定时间段统一执行的,只要能在一个大的时间窗口执行完就行,不太需要考虑响应时间的限制。
  • 任务会分多阶段:只要应用稍微复杂一点,单次LLM的调用就没法完全满足应用的需求了,就需要拆分小任务,进行多次的LLM调用。

我做过的比较典型的该类应用大多是和信息抽取相关的,比如:

  • 在客服场景下,从一堆电话信息中抽取关键的信息来帮助客服能快速回忆起上一通电话的内容。
  • 通过信息抽取,来构建知识图谱,辅助提升问答质量。

会话型LLM应用

我对于会话型LLM应用的定义是:该应用的唯一入口是聊天框,应用的输入来自用户的任意文本。

这类应用的基本特征包含:

  • 输入数据可以极其复杂:源自自然语言的开放性和复杂性,用户的输入可以千奇百怪,在上生产之前,你都想象不到用户会输入哪些离谱的内容。
  • 会涉及到复杂的对话上下文:由于给了用户一个聊天框,就涉及到了上下文的信息,可能会出现用户进行指代、省略、意图切换等等复杂情况。
  • 响应时间要尽可能短:由于是直接面向用户的,所以响应时间直接影响了用户体验,通过流式输出可以缓解一部分压力,但是整体的响应时间还是要尽可能短。
  • 用户能参与完成任务:值得庆幸的是,在这类应用中,用户可以作为系统的一部分,帮助补充信息,也能进行纠错。

会话型LLM应用的分类

而会话型LLM应用也可以继续划分成3类:

  • 任务型:和任务型LLM应用类似,主要就是去执行各种任务的;不同点是,它的输入来自用户。所以这类应用可以结合任务型应用和会话型应用的复杂点,属于复杂度上限可以达到最高的一类应用。
  • 问答型:对知识进行问答,属于大多数企业都可以做、且最容易上手的一个LLM应用场景。
  • 闲聊型:打招呼与问候、告别、简单应答等,可以让对话体验更舒服。一般会集成到任务型应用和问答型应用内。

下面这张图就是我对应用分类的总结:

LLM应用的复杂度评估

前面对于一个应用的分类是从应用场景的角度出发的,虽然应用场景能部分决定复杂度,但是我们还是需要一个更清晰的视角来评估我们应用的复杂度。

OpenAI在2024年7月的时候给AGI划分了5个阶段:

  • Level 1: Chatbots, AI with conversational language
  • Level 2: Reasoners, human-level problem solving
  • Level 3: Agents, systems that can take actions
  • Level 4: Innovators, AI that can aid in invention
  • Level 5: Organizations, AI that can do the work of an organization

并且认为自己目前处于1阶段且靠近2阶段的地方。

显然,这个是对于AGI的定义,而我们要开发一个LLM应用的话,基本上会聚焦到某个具体的场景上。所以划分的方式也得做相应的调整。首先是4、5阶段不属于LLM应用的关注范围,所以我们会将视角限制在前三个阶段。而对于Agent,它能变得很复杂,所以我在复杂性划分上,会将Agent分成简单Agent和复杂Agent。最终我梳理后的复杂度划分成了4级:

  • L1应用:对应于OpenAI定义的L1。基本上就是依靠LLM的自身的能力,进行单轮LLM的调用,让LLM直接进行输出。
  • L2应用:对应于OpenAI定义的L2。就会考虑到LLM自身能力的不足,开始对任务进行拆解,最原始的RAG应用就是这个级别最典型的场景,通过检索来补足LLM的知识欠缺。
  • L3应用:对应于OpenAI定义的L3。开始引入反思这个Agent的属性。反思是Agent一个很重要的能力,它能对环境信息进行反应,检查目标是否已经完成,并进行任务的重试。有一点很重要:此时Agent的能力是通过编码引入的。
  • L4应用:也对应于OpenAI定义的L3。到这就是完整的Agent了。完整的Agent需要给它提供工具,让它自主地规划,根据工具能自主地完成任何相关的任务或问答。

L1-L4的示例

下面我将以知识问答的场景,分别来说一下L1-L4的应用会长什么样:

L1:简单知识问答

不提供任何额外的知识,纯依靠模型自身的能力进行问答,模型有可能进行过垂直领域的微调:

L2:基于RAG的知识问答

引入了RAG,通过知识库补足模型的知识欠缺,避免幻觉。

L3:简单Agent的知识问答

引入了根据目标进行反思的能力,在RAG中,需要Agent判断用户的问题是否清楚,否则需要进行澄清;也需要判断RAG的检索是否找到能回答的知识,否则再次进行检索。

L4:完整Agent的知识问答

引入长期的记忆,可以根据用户的习惯,判断其想表达的意思来主动消除用户问题中的歧义;引入规划,能在多个知识库之间规划先去哪个知识库找数据,再根据找到的数据从哪些知识库找详情数据,再...

LLM应用的用户体验风险

continue在这篇博客中提到了用户体验风险的概念,这个概念对于我们如何去考虑一个LLM应用是否会失败(成功如攀登高峰,每一步都至关重要;失败似堤坝溃决,一处漏洞足以致命)很重要。

下面是用户体验风险的公式:

先从错误影响说起,在一个LLM应用中,错误影响往往是固定的,取决于使用场景。

代码补全就是一个错误影响比较小的好例子,因为代码补全的用户是开发,开发本身对于代码的掌控程度是很高的,对于生成出的代码有那么一点小错误,开发也可以很好地定位错误并解决。

而面向业务人员的Text-to-SQL就是错误影响很大的例子,业务人员压根不知道SQL是否正确,如果不是返回的结果有数量级上的差错,那么业务人员将很难看出错误,以错误数据作为判断的依据就会产生很糟糕的结果。

所以,在错误影响比较高的情况下,要重点降低任务失败的概率,只要任务不失败(结果错误),就不存在错误影响了。这个时候牺牲一点执行时间也是值得的。

如果错误影响比较小,那么就可以权衡一下任务失败概率和任务执行时间,看什么情况下这个公式的风险值最小,还是拿代码补全来说,这个场景下,任务执行时间就很关键,我们需要让用户快速得到补全代码,如果不对,快速换下一个结果,一部分的错误是可以接受的。

L3级LLM应用的构建方式

在确定并分析完场景之后,接下来讲一下L3级LLM应用的构建方式(L3级应用能涵盖L2、L1的构建要点,而L4级目前感觉模型能力还不够,也是我没有落地过这个级别的应用,就不瞎说了)

如上图所示,第一步就是确定架构,这里的架构主要指的是LLM应用的流程架构,确定流程架构后就知道什么地方需要进行知识检索,知识检索的形式也确定下来了,就可以进行知识工程的开发;同时也知道了Agent开发的关键点有什么,可以进行Agent开发了;还能知道我们需要对哪些步骤进行评估,以及评估的指标是什么,确定评估体系了。

和传统机器学习、深度学习项目不同的是,测试集的构建并不一定要在一开始就构建好(当然如果有那肯定更好),我们可以通过LLM先生成一大堆的数据,对这些数据进行小小的修改就可以作为测试集了。能这么操作的原因是LLM本身的能力已经很强了,尤其是像意图识别这种任务,除非涉及到的意图太多了,不然它的准确率都能在95%以上。既然它准确率能这么高,我们为啥不让它来帮我们构建测试集呢。我们只需要抛出问题就行了,答案能自动收集起来,最后标上对错即可。这个思想和标注工具中常见的借助模型进行预标注的概念很像。

有了测试集后,我们就可以依靠测试集的测试结果,来改进我们的系统了,知识不足就补知识,prompt不足就调prompt,模型不足就微调优化,架构需要调整就进行调整。这就是一个迭代优化的过程,和传统机器学习或深度学习的模型开发就很像了。

之后就和普通的应用开发一样,先经过UAT小批量用户的验证后最终上线,上线后需要定期回顾生产数据,补充进测试集。

LLM应用如何提升任务的准确率

在我做过的这么多LLM项目中,常见的错误主要来自以下几种:

  1. 某些数据是LLM在训练时没有见过的,这也是引入RAG最主要的原因,而RAG本身又可以设计得很复杂。
  2. 某些知识在用户脑子中,提供的问题缺乏足够的上下文,没法回答
  3. 模型能力不足,任务太过复杂或者输入的知识太多,导致模型"大脑过载"了
  4. Prompt写得不够好,写得不够清晰,或者前后有矛盾

据此,我总结了一下LLM应用最重要的3个能力,分别是架构、知识、模型:

  • 架构是指:
    • 要做好任务拆解,让模型一次执行一小个任务。为什么CoT有效,原因也是大模型并没有见过一次性完成复杂任务的数据,它没在这样的数据上预训练过,但是它见过复杂任务拆分成的小任务的数据,这它是擅长的。进行任务拆解就是在做人为的CoT。
    • 要做好检索增强,让检索到的知识足够全,并且尽可能准。
  • 知识是指:
    • 做好知识工程,通过多种方式进行知识构建,包括BM25这种稀疏检索知识构建、语义向量检索这种稠密检索知识构建、以及知识图谱构建。同时从多种形式知识中进行混合检索,才能取得最好的效果。
  • 模型是指:
    • 尽量使用更好的基础模型
    • 对模型进行继续训练注入垂直领域知识、按照任务的具体格式进行微调
    • 优化prompt

LLM应用的架构思路

LLM应用的架构和传统应用的架构不太一样;前面已经说了,主要是做任务拆解和检索增强。

上图是我对好几个项目进行总结后,提炼出来的一个比较通用的架构。

在任务拆解部分,一般会分成多个Agent:

  • 入口处一般是一个意图识别Agent,其功能主要有:
    • 澄清用户问题中的歧义,或信息缺失
    • 通过检索获取相关的信息,来辅助意图识别的判断
    • 将具体任务路由给具体的Agent
  • 后续的Agent则更关注具体的任务:
    • 做任务级别的澄清
    • 通过检索获取到执行任务所相关的信息
    • 进行进一步的任务拆解,这时候每个任务可以只是一个函数,如果变得很复杂了,也可以是一个其他的Agent

对于检索增强部分,网上已经有大批大批的资料专门介绍了,这边就不展开讲了。主要就是关键字提取、多路检索、重排序、RAG Agent等,根据项目的复杂度,选择具体要用的技术即可。

LLM应用的知识工程

对于知识工程方面,可以从数据原始的结构形式出发

  • 非结构化数据:
    • 构建倒排索引支持BM25等方法的搜索
    • 构建语义相似向量的向量数据库
    • 通过LLM进行信息提取,转成结构化数据,然后进行结构化数据的知识工程
  • 结构化数据:
    • 对不含语义信息的数据,可以构建MinHash LSH,也可以构建知识图谱
    • 对于含语义信息的数据,可以额外多构建向量数据库

上面的这些方法可以不用都使用,根据具体的场景、数据,选择合适的使用即可。

LLM应用的模型的优化

针对Prompt优化部分,也都是一些老生常谈的东西了:

  • Prompt要明确、具体,不要自带歧义
  • 结构化Prompt
  • 对需要推理才能获得更好性能的任务进行CoT
  • 当追求极致性能的时候,使用Self Consistency

针对模型微调主要分两部分:

  • 想要给模型注入知识,还是得做CT(继续训练),SFT(监督微调)还是很难注入知识。
  • 对于一些具体的小任务,拿小点的模型进行SFT效果还是很好的,甚至有些任务可以使用更小的BERT。

LLM应用的迭代优化过程

建立评估指标

一切优化的前提都是有评估指标。没有评估指标,你都不知道你的应用什么时候才算做完:写了个简单的RAG,找了几个问题测试了一下,发现效果还不错,这样的应用可用么?显然是不行的,我们需要有量化的指标来判断咱们得RAG系统到底是否可用.

我针对上面列出来的任务,都给定了评估指标:

  • 对于有反馈的生成类任务,主要指的是Text-to-SQL和Text-to-Code这种,我们可以通过编写标准的SQL,或者编写单元测试来测试任务的准确率。
  • 对于无反馈的生成类任务,就只能借助大模型来评估了。有人可能会觉得这样大模型自己又当运动员又当裁判不好,先不说我们可以通过微调来优化模型评估的能力,当Prompt变化时,模型的能力偏向也会变化的,所以该方法还是可行的。当然肯定也会存在一定的局限性,这个时候,就可以尝试微调了。

实验记录

和传统的机器学习、深度学习一样,LLM应用也是需要将每次运行的实验都记录在案的,MLflow就是一个很不错的工具,可以帮助我们把Prompt、Temperature、Top P等参数以及实验运行的结果都记录下来。下图就是MLflow某个实验的多次运行结果的截图,每次运行的参数、要监控的指标都可以在总览中很直观的看到,每次运行时的测试数据和Prompt在详情页中也会记录:

基于LLM的Text-to-SQL应用的落地实施案例

接下来我会从一个真实的案例出发,过一遍整个LLM应用构建的流程。

梳理需求和痛点

第一步是做用户访谈和调研,收集用户的需求和痛点。上面就是我们针对这次的业务用户进行访谈后收集到的需求。将需求和场景做一下映射就得到了上图。

应用类型

接下来就是分析应用类型。由于是直接面向客户经理的,所以这俩都属于会话型应用,政策文档问答明显属于问答型,Text-to-SQL则属于任务型。

应用复杂度

经过前期的需求确认,政策问答不需要做得特别复杂,文档数量比较少,内容也都比较有结构,所以定在了L2级别。而该场景的Text-to-SQL涉及到了大量的业务、概念,数据也很复杂,定制的规则也很多,所以定在了L3级别,而且其业务价值更高,所以属于本次的重点。

用户体验风险

首先还是从错误影响开始分析,政策文档问答的错误影响属于中等,通过返回引用内容,可以将错误影响下降到低。所以只需要平衡好响应时间和错误率即可。

而Text-to-SQL的错误影响很高,由于这是面向客户经理的,他们看不懂SQL,如果SQL错了,返回了一个差不多数量级的值,他们就会产生错误的判断,对业务影响比较大。第一步就是想办法降低错误影响,能想到的一个解决方案就是让LLM来解释生成的SQL,现有的LLM对于解释SQL还是比较在行的。这样就把整体的错误影响降低到了中,甚至是低。只有在解释SQL的LLM出问题的时候,才会产生比较高的错误影响。

然后就是本次场景下的Text-to-SQL错误率会很高,原因就是前面提到的那些复杂性。所以本次项目的重点还是落在降低错误率。除了通过上一节提到的手段来降低错误率,还可以使用一些工程手段来降低。比如将数据合成一张大宽表来比较模型做过多的join、将枚举值都换成中文,来避免模型进行枚举值转换的时候出现幻觉、将一些很复杂的指标计算提前计算好等等。

Text-to-SQL架构

接下来就是梳理架构,由于我们是一个会话型应用,肯定需要一个意图识别的Agent,同时它需要去澄清一些用户带有歧义的问题,我最喜欢拿来举例的就是:帮我查询一下购买了A产品和B产品的客户,这句话本身就有两种解释,一种是交集,就是同时购买了A产品和B产品的客户;另外一种是并集,就是购买了A产品或购买了B产品的客户。这就需要Agent帮我们去澄清。

然后就是有时候用户的问题既可以算是文档问答,也可以算是Text-to-SQL,所以还需要通过检索增强的方式,将具体的数据检索回来辅助LLM判断属于哪个意图。当检索回来的内容显示两种意图都可能出现时,就需要抛问题给用户,到底是想要做什么了。

当确定是Text-to-SQL的意图后,就可以让Text-to-SQL的Agent进行澄清了。这次澄清的内容是任务相关的,一个简单的例子是:用户想查询肯德基XXX店的销量,但是叫作XXX店的可能有XXX一店、XXX二店。那就需要让用户澄清一下,具体查的是哪个店。这里的澄清,是只有在我们知道是Text-to-SQL任务,然后通过数据库的值进行召回,发现存在歧义后才可能澄清的。所以两个地方的澄清都是必要的。

澄清完就可以进入到任务的主流程中了,在Text-to-SQL的场景下,就是做好Schema Linking,也就是选表、选列、补充描述、补充数据库的候选值供where/having/case when等语句使用。

接着就是SQL生成,这里可以用Self Consistency来进一步降低错误率。然后可以用一个Revise Agent对语法错误进行修正。

最终让LLM进行SQL的翻译。

Text-to-SQL的知识工程

该应用的数据主要分成:

  • 业务上下文:
    • 表、列的描述
    • 默认的查询规则
  • 数据库的值

根据他们的存储形式,再结合上一节提到的的原则处理就行。

建立评估指标

这里的指标就不详细展开说了,看图就很明确了。

开发落地及优化

一切准备就绪,接下来就是开发加优化了。我们这套架构也是在一次次调整中财获得的,最开始的时候,是没有这些澄清步骤,没有数据召回辅助意图识别判断的。真就是上了UAT才发现...,上了生产才发现...

效果

该项目通过该方法论最终将Text-to-SQL的准确率从一开始的10%(由于LLM缺乏大量的业务知识和默认规则,所以基本都是错的),提升到了90%+。

结束语

LLM应用的开放范式正在快速迭代,欢迎大家一起来探讨探讨。