从10%到90%:一个Text-to-SQL项目的LLM应用落地全记录

Feng 7 阅读 AI技术

自ChatGPT引爆市场以来,几乎每家公司都在尝试把LLM塞进自己的业务里。但真正动手时会发现,系统性地讲清楚一个LLM应用该怎么做、分几步做的资料并不多。我从2023年12月开始陆续做了3个LLM项目,全部是会话型应用,趁着记忆还新鲜,把踩过的坑和验证过的路径整理出来。最后会用一个完整的Text-to-SQL案例串一遍流程。

先搞清楚你的应用属于哪一类

LLM的能力本质上是传统NLP任务的升级版。按应用场景分,大致有这些:文本分类、信息抽取、文本生成、对话系统、问答系统、机器翻译、文本摘要、自然语言推理、文本编辑。

我按任务特性重新归了一下:

信息抽取类:实体提取、关系提取、知识图谱构建。

无反馈信号的生成类:文本摘要、文本编辑、机器翻译、故事生成。

有反馈信号的生成类:代码生成、SQL生成。

分类类:主题分类、情感分类、垃圾邮件过滤、自然语言推理。

问答与对话类:问答系统、对话系统。

基于这个划分,我把LLM应用分成两种:任务型和会话型。这么切分是因为在实际项目中,会话型应用的复杂度明显更高,提前理解这一点对项目规划很关键。

任务型LLM应用没有用户对话入口,输入来自系统数据。它的特征很明确:输入数据格式和分布基本确定,复杂度低;通常是批处理,各大LLM API厂商对批处理都有一定优惠,本地模型批处理也能提高吞吐;响应时间没有严格要求,只要在一个大时间窗口内跑完就行;任务往往会拆成多个阶段,稍微复杂一点的应用单次LLM调用根本搞不定,必须拆成小任务多次调用。我做过比较典型的任务型应用大多和信息抽取相关,比如从客服电话记录中抽取关键信息,帮客服快速回忆上一通电话的内容;或者通过信息抽取构建知识图谱,提升问答质量。

会话型LLM应用的唯一入口是聊天框,输入来自用户的任意文本。它的特征正好相反:输入可以极其复杂,自然语言的开放性意味着你永远想象不到用户会输入什么离谱内容;涉及复杂的对话上下文,用户会指代、省略、切换意图;响应时间要尽可能短,因为直接面向用户,流式输出能缓解一部分压力,但整体响应时间还是关键;好消息是用户能参与完成任务,可以补充信息、纠正错误。

会话型应用还能继续分成3类。任务型:和任务型LLM应用类似,但输入来自用户,复杂度上限最高。问答型:对知识进行问答,大多数企业最容易上手。闲聊型:打招呼、告别、简单应答,一般集成在任务型和问答型应用里,让对话体验更舒服。

复杂度分级:你在建的是L几的应用

OpenAI在2024年7月给AGI划了5个阶段:L1是聊天机器人,L2是推理者,L3是能采取行动的Agent,L4是能辅助发明的创新者,L5是能完成组织工作的系统。他们自认为当时处于L1靠近L2的位置。

这个划分是针对AGI的,开发LLM应用时需要调整。L4和L5不在LLM应用的关注边界内,所以只看前三个阶段。Agent本身可以很复杂,我把它拆成简单Agent和复杂Agent。最终整理出4级复杂度:

L1应用:对应OpenAI的L1。基本靠LLM自身能力,单轮触发,直接输出。

L2应用:对应OpenAI的L2。开始弥补LLM自身能力的不足,对任务进行拆解,最原始的RAG应用就是这个级别最典型的场景,通过检索补足知识欠缺。

L3应用:对应OpenAI的L3。引入反思这个Agent属性。反思是Agent很重要的能力,能对环境信息做出反应,检查目标是否完成,并进行任务重试。注意:此时Agent的能力是通过编码引入的。

L4应用:也对应OpenAI的L3。完整的Agent,给它工具,让它自主规划,根据工具自主完成相关任务或问答。

以知识问答为例,看看L1到L4分别长什么样。

L1:简单知识问答,不带任何额外知识,纯靠模型自身能力,模型可能经过垂直领域微调。

配图

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

L3:在RAG中加入Agent能力,判断用户问题是否清楚,引入基于目标的反思能力,同时判断检索结果能否回答知识,否则重新检索。

L4:引入长期记忆,根据用户习惯判断其想表达的意思,主动消除歧义;能在多个知识库之间规划先去哪个找资料,再根据找到的数据去哪些知识库找详情,引入规划能力。

配图

用户体验风险:错误影响和执行时间的权衡

continue在那篇博客里提到了用户体验风险的概念,这对判断一个LLM应用是否会失败很关键。公式的核心是两个变量:错误影响和任务失败概率。

错误影响在一个LLM应用中是固定的,取决于使用场景。代码补全的用户是开发,开发对代码掌控度高,生成结果有小错误,影响相对小。而面向业务人员的Text-to-SQL就完全不同,业务人员看不懂SQL,如果返回了一个数量级差不多的错误值,他们根本看不出来,拿错误数据做判断后果很糟。

错误影响高的时候,要重点降低任务失败概率,只要任务不失败就行,牺牲一点执行时间也值得。错误影响低的时候,任务执行时间更关键,比如代码补全,需要让用户快速得到补全结果,不对就快速换下一个。

这里有个坑:很多人只盯着准确率,忽略了错误影响这个维度。同样是90%准确率,代码补全和Text-to-SQL的业务风险完全不在一个量级。

L3级应用的搭建流程

L3级应用的构建要点能覆盖L2和L1,L4目前模型能力还不够,我也没实现过,不瞎说。

第一步是确定链路架构。架构定了,就知道什么地方需要知识检索、检索形式是什么、Agent开发的关键点在哪、需要对哪些流程做评估、评估指标是什么。

和传统机器学习项目不同,测试集不一定一开始就建好。可以用LLM先生成大量数据,稍作修改就能当测试集。原因是LLM本身能力已经很强,像意图识别这种任务,除非意图太多,准确率能到95%以上。既然它这么准,为什么不让它帮我们搭验证集?我们只需要抛出问题,答案自动收集,最后标上对错即可。这和标注工具里常见的模型预标注思路很像。

有了测试集,就能根据测试结果改进系统:知识不足补知识,prompt不足调prompt,模型不足做微调,架构需要调整就调整。这是一个演进调优的过程,和传统机器学习开发很像。之后走UAT小批量用户验证,最终上线,上线后定期回顾生产数据,补充进测试集。

提升准确率的三个核心能力

我做过这么多LLM项目,常见错误主要来自几种:LLM训练时没见过的知识,这也是引入RAG最主要的原因;知识在用户脑子里,问题缺乏足够上下文;模型能力不足,任务太复杂或输入知识太多导致大脑过载;Prompt写得不够好、不清楚或前后矛盾。

归纳起来,LLM应用最重要的3个能力是架构、知识、模型。

配图

架构要做好两件事。任务拆解,让模型一次完成一个小任务。大模型没见过一次性完成复杂任务的数据,它没在这种数据上预训练过,但见过复杂任务拆成小任务的数据,这也是CoT有效的原因。做任务拆解就是在做人为的CoT。检索增强,让检索到的知识足够全且尽量准。

知识要做好知识工程,通过多种方式构建知识,包括BM25这种稀疏检索、语义向量检索这种稠密检索、知识图谱构建。从多种形式知识中做混合检索,才能获得最好效果。

模型层面:尽量用更好的基础模型;对模型做继续训练注入垂直领域知识,按任务具体格式做微调;优化prompt。

通用架构:任务拆解加检索增强

LLM应用的架构和传统应用不太一样,核心就是任务拆解和检索增强。

任务拆解一般分成多个Agent。入口处是意图识别Agent,功能包括:澄清用户问题中的歧义或信息缺失;通过检索获取相关信息辅助意图判断;将细化任务路由给具体Agent。后续Agent关注细化任务:做任务级别的澄清,通过检索获取完成任务所需信息。每个任务可以只是一个函数,如果变得很复杂,再做进一步拆解。

检索增强这块资料已经很多,核心就是关键字提取、多路检索、重排序、RAG Agent等,根据项目复杂度选择具体技术。

知识工程从数据原始结构出发。非结构化数据:搭建倒排索引用BM25等方法搜索;搭建语义相似向量的向量数据库;通过LLM做信息提取转成结构化数据,然后做结构化知识工程。结构化数据:对不含语义信息的数据,可以构建MinHash LSH,也可以构建知识图谱;对含语义信息的数据,额外构建向量数据库。这些方法不用全用,根据具体情况选择。

模型调优方面,Prompt调优是老生常谈:明确、细化、不带歧义;结构化Prompt;对需要推理的任务加CoT;追求极致性能时用Self Consistency。微调分两部分:想注入知识,得做CT继续训练,SFT很难注入知识;对具体小任务,拿小模型做SFT效果很好,有些任务甚至可以用更小的BERT。

评估体系与实验记录

没有评估指标,你都不知道应用什么时候算做完。写了个简单RAG,找几个问题测一下觉得效果不错,这样的应用能用吗?显然不行,需要量化指标。

对有反馈的生成类任务,主要是Text-to-SQL和Text-to-Code,可以通过编写基准SQL或单元测试来测准确率。对无反馈的生成类任务,只能通过Transformer来评估。有人觉得大模型自己又当运动员又当裁判不好,先不说可以通过微调优化模型评估能力,当Prompt变化时模型能力偏向也会变,所以这个方法可行。当然有局限性,这时候可以尝试微调。

配图

实验记录方面,和传统机器学习一样,每次运行的实验都需要记录。MLflow是个不错的选择,可以把Prompt、Temperature、Top P等参数和实验结果都记录下来。每次运行的参数、要观测的指标在总览中一目了然,运行时的验证数据和Prompt在详情页中也有记录。

Text-to-SQL实战案例

下面用一个真实案例走一遍完整流程。

第一步是用户访谈和调研,收集诉求和痛点。将诉求和场景做映射后,拆解应用类型。因为是直接面向客户经理的,所以两个场景都属于会话型应用。政策文档问答属于问答型,Text-to-SQL属于任务型。

政策问答不需要做得特别复杂,文档数量少,内容结构比较清晰。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翻译。

知识工程方面,该应用的数据主要分成:业务上下文,包括表、列的描述;默认的查询规则;数据库的值。根据他们的存储形式,结合上一节提到的原则处理即可。

评估指标见图。

配图

开发实现及优化。一切准备就绪后就是开发和调优。最开始时没有这些澄清流程,整套架构也是在一次次调整中才获得的。真是上了UAT才发现问题,上了生产才发现问题。

最终效果:该项目通过这套方法论把Text-to-SQL准确率从最初的10%(因为LLM缺乏大量场景知识和默认规则,基本都是错的)提升到了90%以上。

LLM应用的开源模型正在快速演进,欢迎一起探讨。

配图

配图

配图

配图

配图

配图

Feng
这位作者很神秘,还没有填写简介。