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行导入刚才的函数。
细心的同学可能注意到了,这里的 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
对大模型结果进一步处理
添加了多个示例之后,大模型终于能理解我们要干什么了。然而由于我们用的大模型是免费的,一分价钱一分货,这个免费的大模型往往会返回以下结果。
以上结果程序是无法识别的,那么下一步如何处理呢?
简单!我们专门针对这种情况多添加一个函数,进一步处理 **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 处理后结果
让大模型不要那么啰嗦
处理到现在,我们终于能够获得程序可以识别的结果了。
然而大模型经常会画蛇添足添加更多内容。比如后面这样:
像这种内容,对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节课再探讨。
结合之前的用户输入
讲到现在,我们的程序可以根据后面这个问题来正确获得查询参数了。
返回结果如下。
然而这时大模型还不能根据正确获得查询参数。比如用户提问“还剩多少”,大模型就无法做出查询动作,因为仅仅根据这句话,大模型无法知道从哪个模块去查询数据。
那怎么办呢?我们把之前的问题整合进来去查询就可以了,也就是变成这样。
那么我们的代码就需要做相应的修改。其中第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来获取程序可以识别的结构化结果。我们从最简单的形式开始,让大家对其核心代码有一个基本的认识。
第二件事情是当大模型不能正确返回结构化结果时,都有哪些处理方法。我们通过不同方法,一步步来指导大模型输出程序想要的结构化结果,具体包括添加示例、对大模型结果进一步处理、让大模型不要那么啰嗦、重试。
现在我们可以根据用户的提问从数据库里面查询出数据了,下一节课我们将根据这些数据去回答用户的提问,敬请期待。
思考题
这节课的代码只支持销售管理模块,如果需要支持其他模块,例如生产管理模块,那该如何处理?
欢迎你在留言区和我交流互动,如果这节课对你有启发,也推荐分享给身边更多朋友。
- 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