项目背景
众所周知,JSON是目前广泛使用的数据交互格式之一,当大模型能够稳定输出Json格式的数据之后,我们就可以拿输出的数据与前端、数据库、操作系统、物联网终端设备等等实现很多交互的行为动作,从而做出更好玩更有价值的大模型原生应用。
本项目以⚡问卷生成
⚡任务为例,演示如何控制ERNIE Bot的格式化输出,并将输出结果与前端交互,实现从Prompts直接生成问卷网页的效果。
难点分析
本项目的难点主要在于多层嵌套数据结构体
的控制生成。
- 单层数据结构:
单层数据结构是类似于{key: value, key: value, …}
这样只有一层键值对关系的,相对来说比较简单,生成可控度高,不容易出错。
{
"address": "北京市朝阳区XXX路XXX号",
"date": "2023-06-25",
"email": "zhangsan@example.com",
"idcode": "110101199003077777",
"name": "张三",
"phone": "13800000000",
"sex": "男"
}
- 多层嵌套数据结构:
多层嵌套数据是比较复杂的数据结构,如下例子所示,在address
的第一层级下,嵌套了第二层级的city
、area
、road
和detail
字段,在真实业务场景中,数据结构体往往是多层级嵌套,字段多,嵌套关系也比较复杂,因此该类数据结构体生成的难度比较大,容易出现一些纰漏导致数据解析不正确而报错。
{
"address": {
"city": "北京市",
"area": "朝阳区",
"road": "XXX路",
"detail": "XXX号"
},
"date": "2023-06-25",
"email": {
"common": "zhangsan@example.com",
"backup": "zhangsan@example1.com"
},
"idcode": "110101199003077777",
"name": "张三",
"phone": "13800000000",
"sex": "男"
}
本项目的问卷生成任务,本质上就是生成一个多层嵌套数据结构体的数据,下面开始给大家演示和解析代码。
开始上手
1. 安装依赖
In [ ]
!pip install erniebot --upgrade
!pip install llm2json
2. 配置ERNIE Bot
⚠ 注意:请配置ERNIE Bot的access_token,否则将无法继续运行。
In [2]
import erniebot
erniebot.api_type = "aistudio"
erniebot.access_token = "xxxxxxxxxxxxxxxxxxx"
def ernieChat(content):
response = erniebot.ChatCompletion.create(model="ernie-4.0",
messages=[{"role": "user", "content": content}])
return response.get_result()
3. 定义数据结构
我们需要做好数据结构体的定义,一份问卷的生成结构至少有两层。
- 第一层
WenJuan
是title
(问卷标题)、description
(问卷描述)和最核心的data
(问题列表)结构体。 - 第二层是对
data
嵌套数据的定义,在data
下面有若干个问题和选项,并且问题类型有单选题、多选题、填空题等等,因此这里需要针对问题定义一个新的对象Question
,第一个键是types
,用于确定问题类型,它是整数型的数据(1为单选,2为多选,3为填空);第二个是question
,定义问题;第三个是choices
问题对应的选项内容,数据类型是列表list。
In [ ]
from typing import List
from llm2json.prompts.schema import BaseModel, Field
class Question(BaseModel):
types: int = Field(description = "问题类型,1为单选,2为多选,3为填空")
question: str = Field(description = "问题内容")
choices: List[str] = Field(description = "选项内容")
class WenJuan(BaseModel):
title: str = Field(description = "问卷标题")
description: str = Field(description = "问卷描述")
data: List[Question] = Field(description = "问题列表")
4. 定义正例
定义完数据结构之后,我们其实可以直接执行了。但因为多层嵌套的数据结构体比较复杂,因此我们最好给LLMs一个正确示例,让LLMs生成的输出结果更加完美和稳定。
In [6]
correct_example = '''
{
"title": "问卷标题",
"description": "问卷描述",
"data":[
{
"types": 1,
"question": "问题(单选)"
"choices": ["选项1", "选项2", "选项3"]
},
{
"types": 2,
"question": "问题(多选)"
"choices": ["选项1", "选项2", "选项3"]
},
{
"types": 3,
"question": "问题(填空)"
},
]
}
'''
5. 定义Prompt任务模板
In [ ]
from llm2json.prompts import Templates
t = Templates(prompt="""
请你根据主题<{topic}>,设计一份问卷。
问卷描述需要简单说明该问卷调研的目的。
问卷题型需包含单选、多选和填空题,对应types分别为1、2、3。
如果题目类型为填空题,该题不需要返回choices字段。
出题题型顺序请随机生成。
题目总数为{num}道题。
""",
field=WenJuan,
correct_example=correct_example)
6. 测试生成
以文心一言用户反馈
作为问卷的主题,生成一份包含10道题的问卷。
In [ ]
template = t.invoke(topic="文心一言用户反馈", num="10")
ernieResult = ernieChat(template)
In [10]
from llm2json.output import JSONParser
parser = JSONParser()
result = parser.to_dict(ernieResult)
from pprint import pprint
pprint(result)
{'data': [{'choices': ['18岁以下', '18-25岁', '26-35岁', '36-45岁', '46岁及以上'], 'question': '您的年龄是?', 'types': 1}, {'choices': ['社交媒体', '官方网站', '朋友推荐', '线下活动', '其他'], 'question': '您通常通过哪些方式了解文心一言的最新动态和产品信息?', 'types': 2}, {'question': '请描述您对文心一言产品的整体印象。', 'types': 3}, {'choices': ['是', '否'], 'question': '您是否使用过文心一言的其他相关产品?', 'types': 1}, {'choices': ['用户界面', '功能体验', '性能速度', '客户服务', '其他'], 'question': '您认为文心一言的哪些方面需要改进?', 'types': 2}, {'question': '请提供您对文心一言产品的任何建议或意见。', 'types': 3}, {'choices': ['非常愿意', '比较愿意', '中立', '不太愿意', '非常不愿意'], 'question': '您是否愿意推荐文心一言产品给您的朋友或家人?', 'types': 1}, {'choices': ['价格', '品牌知名度', '用户评价', '功能特点', '客户服务'], 'question': '以下哪些因素会影响您选择使用文心一言的产品?', 'types': 2}, {'choices': ['非常满意', '比较满意', '中立', '不太满意', '非常不满意'], 'question': '您对文心一言产品的整体满意度如何?', 'types': 1}, {'question': '如果您有任何其他问题或需要补充的信息,请在下方留言。', 'types': 3}], 'description': '这份问卷旨在了解您对文心一言产品的使用体验和满意度,以便我们更好地改进产品和服务。感谢您的参与!', 'title': '文心一言用户反馈'}
这里我们已经得到了JSON格式的数据,那么我们下面就与前端结合,将数据渲染。前端的核心代码主要是对 问卷类型的判断 ,然后根据问卷类型也就是types
的值匹配不同的表单组件。
为了开发方便,前端我用的是Ant Design Vue
模板。
<div class="choices">
<!--单选题-->
<div v-if="item.types==1">
<a-radio-group v-model:value="item.choices.keys">
<a-radio v-for="choice in item.choices" :value="choice">
{{ choice }}
</a-radio>
</a-radio-group>
</div>
<!--多选题-->
<div v-else-if="item.types==2">
<a-checkbox-group
:options="item.choices" />
</div>
<!--填空题-->
<div v-else-if="item.types==3">
<a-input style="max-width:300px"/>
</div>
</div>
前端完整源代码见项目目录的
frontend.zip
压缩包。
7. 服务化部署
在上述程序代码的基础上,我们给后端代码套上Flask引擎,即可完成后端的部署上线。
import json
import erniebot
from typing import List
from llm2json.prompts import Templates
from llm2json.prompts.schema import BaseModel, Field
from llm2json.output import JSONParser
from flask import Flask, request, make_response
from flask_cors import CORS
erniebot.api_type = "aistudio"
erniebot.access_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
app = Flask(__name__)
CORS(app, resources={r"/*": {}})
def ernieChat(content):
response = erniebot.ChatCompletion.create(model="ernie-4.0",
messages=[{"role": "user", "content": content}])
return response.get_result()
class Question(BaseModel):
question: str = Field(description = "问题内容")
choices: List[str] = Field(description = "选项内容")
class WenJuan(BaseModel):
title: str = Field(description = "问卷标题")
description: str = Field(description = "问卷描述")
types: int = Field(description = "问题类型,1为单选,2为多选,3为填空")
data: List[Question] = Field(description = "问题列表")
correct_example = '''
{
"title": "问卷标题",
"description": "问卷描述",
"data":[
{
"types": 1,
"question": "问题(单选)"
"choices": ["选项1", "选项2", "选项3"]
},
{
"types": 2,
"question": "问题(多选)"
"choices": ["选项1", "选项2", "选项3"]
},
{
"types": 3,
"question": "问题(填空)"
},
]
}
'''
def generateQuestionnaire(topic, nums):
t = Templates(prompt="""
请你根据主题<{topic}>,设计一份问卷。
问卷描述需要简单说明该问卷调研的目的。
问卷题型需包含单选、多选和填空题,对应types分别为1、2、3。
如果题目类型为填空题,该题不需要返回choices字段。
出题题型顺序请随机生成。
题目总数为{num}道题。
""",
field=WenJuan,
correct_example=correct_example)
template = t.invoke(topic=topic, num=str(nums))
ernieResult = ernieChat(template)
parser = JSONParser()
result = parser.to_dict(ernieResult)
return result
@app.route('/')
def index():
return 'welcome to my webpage!'
@app.route('/generate', methods=['POST'])
def generate():
topic = request.json.get('topic')
nums = request.json.get('nums')
result = generateQuestionnaire(topic, nums)
return make_response(json.dumps(result), 200)
if __name__=="__main__":
app.run(port=2024,host="0.0.0.0",debug=True)
后端完整源代码见项目目录的
web.py
和wj.py
代码文件。