跳转至

05 动手实战:根据用户发问查询数据

你好,我是叶伟民。

今天这节课,我们继续动手实战,让大模型能够根据用户提问来查询数据,也就是在前面提到的“发问部分”,添加获取结构化数据查询参数的代码。

这节课是整个实战案例的核心部分,而且通用性很强,很多从数据库里检索知识的场景,你都可以参考这节课的思路来完成。

基础部分

我们继续打开实战案例1\改造前\home\rag.py 文件,在尾部添加以下代码。

def 获取结构化数据查询参数(用户输入):
    结构化数据 = 对话模式(构造解析用户输入并返回结构化数据用的messages(用户输入))
    查询参数 = json.loads(结构化数据)
    return 查询参数

这段代码估计你现在已经很容易理解了。第2行代码是通过对话模式将用户输入转化为查询参数。其中对话模式我们在第4节课讲过,结构化数据相关概念我们在第3节课讲过。

大模型返回的是人类能够识别的字符串,而不是程序可以识别的形式。所以第2行代码结构化数据这个变量的值其实是字符串格式,因此我们需要通过第3行代码的json.loads函数,将结构化数据真正转化为程序真正可以识别的查询参数。

构造messages

前面我们知道了对话模式函数会接收messages参数,那么如何构造这个messages呢?

其实就是下面这个函数。我们需要在实战案例1\改造前\home\rag.py 文件尾部添加它。

def 构造解析用户输入并返回结构化数据用的messages(用户输入):
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果:

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

代码里面的内容我们第3节课讨论过,如果你想不起来了,可以去回顾一下。

在views.py导入

然后我们打开实战案例1\改造前\home\view.py 文件,在顶部第1行导入刚才的函数。

from .rag import *

细心的同学可能注意到了,这里的 import 后面跟着是 ,而不是像其他行一样明确的函数名。 表示rag.py文件里面的所有函数,所以使用 * 能够一次性将rag.py文件的所有函数都导入进来,这是一劳永逸的做法。

不能正确返回怎么办?

然而以上函数是跑不通的,因为太简单了,我们需要补充多一点内容。所以我们需要使用第3节课里返回整数的方法,让大模型做选择题。

我们需要给出一系列选项,然后让大模型回答正确选项。于是代码就变成了下面这个模样。

def 构造解析用户输入并返回结构化数据用的messages(用户输入):
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果。注意,模块部分请按以下选项返回对应序号:
   1. 销售对账
   2. 报价单
   3. 销售订单
   4. 送货单
   5. 退货单
   6. 其他

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

我们在第4行添加了一句话“注意,模块部分请按以下选项返回对应序号”。然后在第5行到第10行给出了一系列选项,让大模型做选择题。

因为我们采用了序号来表示模块,所以还需要修改查询部分的代码。我们需要打开实战案例1\改造前\home\search.py文件,将第5行代码改成使用序号来判断。

from .models import 销售入账记录

def 查询(查询参数):
    if '模块' in 查询参数:
        if 查询参数['模块'] == 1: #'销售对账'
            if '客户名称' in 查询参数:
                客户 = 查询参数['客户名称'].strip()
                return 销售入账记录.objects.filter(客户__icontains=客户)

添加示例

因为我们用的是免费的模型,所以完成前面的工作还不够,还需要提供一些示例给大模型。

添加示例后的代码就变成了下面这样。第12行到第15行就是我们添加的示例。

def 构造解析用户输入并返回结构化数据用的messages(用户输入):
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果。注意,模块部分请按以下选项返回对应序号:
   1. 销售对账
   2. 报价单
   3. 销售订单
   4. 送货单
   5. 退货单
   6. 其他

  示例1:
  用户:客户北京极客邦有限公司的款项到账了多少?
  系统:
  {{'模块':1,'客户名称':'北京极客邦有限公司'}}

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

添加更多示例

那么如果给了大模型一个示例,它还是无法正确输出。这时候有什么办法解决呢?这个问题简单,一个示例不够,那就给多几个示例,但是每个示例应该是不一样的,这样才有效果。

现在我们就来添加更多示例。后面代码中,第17行到第25行就是我们添加的更多示例。

def 构造解析用户输入并返回结构化数据用的messages(用户输入):
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果。注意,模块部分请按以下选项返回对应序号:
   1. 销售对账
   2. 报价单
   3. 销售订单
   4. 送货单
   5. 退货单
   6. 其他

  示例1:
  用户:客户北京极客邦有限公司的款项到账了多少?
  系统:
  {{'模块':1,'客户名称':'北京极客邦有限公司'}}

  示例2:
  用户:你好
  系统:
  {{'模块':6,'其他数据',None}}

  示例3:
  用户:最近一年你过得如何?
  系统:
  {{'模块':6,'其他数据',None}}

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

对大模型结果进一步处理

添加了多个示例之后,大模型终于能理解我们要干什么了。然而由于我们用的大模型是免费的,一分价钱一分货,这个免费的大模型往往会返回以下结果。

```json
{"模块": 1, "客户名称": "广州神机妙算有限公司"}
以上结果程序是无法识别的,那么下一步如何处理呢?

简单!我们专门针对这种情况多添加一个函数,进一步处理 **AI返回的结果**。这里你先有个印象就行,我们后续章节再详细展开。

```python
def 对AI结果进一步处理(AI结果):
  处理后结果 = AI结果.replace("```json", '').replace("```", '') # 去掉json格式之外无关的内容
  return 处理后结果

然后我们把这个函数的调用加到对话模式里面。其中第18行就是调用代码。

def 对话模式(messages):
  url = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-lite-8k?access_token=" + get_access_token()
  
  json_obj = {
      "messages": messages,
  }

  playload= json.dumps(json_obj)
  headers = {
      'Content-Type': 'application/json'
  }
  
  response = requests.request("POST", url, headers=headers, data=playload)
  json_result = json.loads(response.text)
  if "error_code" in json_result:
    return json_result["error_msg"] + ":" + playload
  else:
    处理后结果 = 对AI结果进一步处理(json_result["result"])
  return 处理后结果

让大模型不要那么啰嗦

处理到现在,我们终于能够获得程序可以识别的结果了。

{"模块": 1, "客户名称": "广州神机妙算有限公司"}

然而大模型经常会画蛇添足添加更多内容。比如后面这样:

我们识别的结果是:{"模块": 1, "客户名称": "广州神机妙算有限公司"}

像这种内容,对AI结果进一步处理的函数来说,也是很难处理的。那怎么办呢?

简单!我们让大模型不要那么啰嗦就可以了。我们只需在后面第4行里多加一个相应的指令。

def 构造解析用户输入并返回结构化数据用的messages(用户输入):
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果,除此之外不要返回其他内容。注意,模块部分请按以下选项返回对应序号:
   1. 销售对账
   2. 报价单
   3. 销售订单
   4. 送货单
   5. 退货单
   6. 其他

  示例1:
  用户:客户北京极客邦有限公司的款项到账了多少?
  系统:
  {{'模块':1,'客户名称':'北京极客邦有限公司'}}

  示例2:
  用户:你好
  系统:
  {{'模块':6,'其他数据',None}}

  示例3:
  用户:最近一年你过得如何?
  系统:
  {{'模块':6,'其他数据',None}}

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

这么做之后,大模型啰嗦的次数少了很多。然而并不能完全杜绝意外发生。

重试

遇到这种情况,我们可以重试。我们回到获取结构化数据查询参数这个函数,添加重试代码。

def 获取结构化数据查询参数(用户输入):
  重试总次数 = 2
  当前重试次数 = 0
  while 当前重试次数 <= 重试总次数:
    try:
      结构化数据 = 对话模式(构造解析用户输入并返回结构化数据用的messages(用户输入))
      查询参数 = json.loads(结构化数据)
      return 查询参数
    except:
      当前重试次数 += 1
  
  return None

其中第3行到第6行,以及第10行和第11行就是重试的代码。一般来说,重试总次数为2会比较合适,因为重试太多的话会导致用户要等很久,影响用户体验。当然你也可以根据你的实际情况来修改。

可能有同学会好奇,为什么重试策略会有用?因为在软件程序中,同样的代码再运行一遍,还是会得到同样的结果。

这就要从大模型的原理说起了。因为本质上,大模型是按照概率来生成输出结果的。那么这一次输出的结果,就可能跟上一次不一样,所以使用重试策略就可能奏效。

以上方法试过都不行怎么办?

如果以上方法试过都不行怎么办?

根据经验,以上这么多方法综合使用能解决大部分问题。如果还是不行,那就是量的问题。我们对症下药。

如果示例不够多,那就加示例;如果对大模型结果的进一步处理不足,那就添加对应代码;如果大模型还是出现其他意外,那就参考“让大模型不那么啰嗦”那一节,加入更多指令。

然而这里又引出了一个新的问题,用户在使用我们系统的时候,你并没有在旁边看着,怎么知道以上方法都不行呢?你又怎么知道如何改进呢?这些问题我先卖个关子,我们第7节课再探讨。

结合之前的用户输入

讲到现在,我们的程序可以根据后面这个问题来正确获得查询参数了。

客户广州神机妙算有限公司的款项到账了多少

返回结果如下。

{"模块": 1, "客户名称": "广州神机妙算有限公司"}

然而这时大模型还不能根据正确获得查询参数。比如用户提问“还剩多少”,大模型就无法做出查询动作,因为仅仅根据这句话,大模型无法知道从哪个模块去查询数据。

那怎么办呢?我们把之前的问题整合进来去查询就可以了,也就是变成这样。

客户广州神机妙算有限公司的款项到账了多少还剩多少

那么我们的代码就需要做相应的修改。其中第2行和第3行就是把之前的输入都加上。

def 构造解析用户输入并返回结构化数据用的messages(之前的用户输入,用户输入):
  if 之前的用户输入 is not None and len(之前的用户输入.strip()) > 0:
    用户输入 = 之前的用户输入 + 用户输入
  messages=[
  {"role": "user", "content": f"""
  请根据用户的输入返回json格式结果,除此之外不要返回其他内容。注意,模块部分请按以下选项返回对应序号:
   1. 销售对账
   2. 报价单
   3. 销售订单
   4. 送货单
   5. 退货单
   6. 其他

  示例1:
  用户:客户北京极客邦有限公司的款项到账了多少?
  系统:
  {{'模块':1,'客户名称':'北京极客邦有限公司'}}

  示例2:
  用户:你好
  系统:
  {{'模块':6,'其他数据',None}}

  示例3:
  用户:最近一年你过得如何?
  系统:
  {{'模块':6,'其他数据',None}}

  用户:{用户输入}
  系统:
  """},
  ]
  return messages

那么如何获取之前的输入呢?答案是从数据库的对话记录里面获取。

from .models import 对话记录

def 获取之前的用户输入():
  之前的用户输入 = ""
  之前的messages = 对话记录.objects.filter(已结束=False).order_by('created_time')
  for current in 之前的messages:
    if current.role == 'user' and current.content is not None:
      之前的用户输入 += current.content
  return 之前的用户输入

其中第5行就是获取数据库对话记录的代码。然后第7行判断如果是用户的输入,就会获取它。

我们的获取结构化数据查询参数函数也要做相应的修改。我们在第7行添加了一个传入参数。

def 获取结构化数据查询参数(用户输入):
  之前的用户输入 = 获取之前的用户输入()
  重试总次数 = 2
  当前重试次数 = 0
  while 当前重试次数 <= 重试总次数:
    try:
      结构化数据 = 对话模式(构造解析用户输入并返回结构化数据用的messages(之前的用户输入,用户输入))
      查询参数 = json.loads(结构化数据)
      return 查询参数
    except:
      当前重试次数 += 1
  
  return None

小结

好了,今天这一讲到这里就结束了,最后我们来回顾一下。这一讲我们学会了两件事情。

第一件事情是如何构造messages来获取程序可以识别的结构化结果。我们从最简单的形式开始,让大家对其核心代码有一个基本的认识。

第二件事情是当大模型不能正确返回结构化结果时,都有哪些处理方法。我们通过不同方法,一步步来指导大模型输出程序想要的结构化结果,具体包括添加示例、对大模型结果进一步处理、让大模型不要那么啰嗦、重试。

现在我们可以根据用户的提问从数据库里面查询出数据了,下一节课我们将根据这些数据去回答用户的提问,敬请期待。

思考题

这节课的代码只支持销售管理模块,如果需要支持其他模块,例如生产管理模块,那该如何处理?

欢迎你在留言区和我交流互动,如果这节课对你有启发,也推荐分享给身边更多朋友。

精选留言(6)
  • welfred 👍(1) 💬(1)

    请问老师,提示词使用markdown格式是否会更好呢?还是没差?

    2024-09-11

  • overland 👍(0) 💬(2)

    请教下老师,这个提问到查询的动作在哪里,如何实现,好像没讲到,全是讲的是直接拿到数据库结果了,丢进大模型了,那这块如何查询这块有讲吗?

    2024-11-08

  • 无处不在 👍(0) 💬(1)

    记得大模型出来前,我们做这种输入查询是通过NLP做的,提前把一些数据库中的词设置好词性,然后输入的时候,根据词性识别出来公司名称和指标,要是大模型在早出来1年就好了。大模型时代解决了很多问题

    2024-10-26

  • 峰回路转 👍(0) 💬(1)

    这里是不是可以把 表名称跟查询字段也加上 {{'模块':1,'客户名称':'北京极客邦有限公司',table_name:'xxx',field_name:'xxx'}},这样后面可以做动态sql 执行

    2024-10-23

  • lost 👍(0) 💬(0)

    ```请根据用户的输入返回json格式结果,除此之外不要返回其他内容。注意,模块部分请按以下选项返回对应序号: 1. 销售对账 2. 报价单 3. 销售订单 4. 送货单 5. 退货单 6. 其他 示例1: xxxxx ``` 如果这个mis系统很复杂,模块非常多,比如有成百上千个,这个时候提示词包括示例可能会非常非常的大,不可能一下子就传给大模型! 请教一下老师,这种情况应该怎么处理

    2024-11-19

  • Geek_fbf3a3 👍(0) 💬(0)

    课后打卡:可以添加生产模块的示例吧

    2024-11-06