DL.ai 大模型系列:2.ChatGPT API 搭建系统

1 简介

以构建客服助手为例,使用不同的 Prompt 链式调用LLM搭建复杂应用

2 语言模型及其 Token

LLM 可以通过使用监督学习来构建,通过不断预测下一个词来学习

LLM 的输出是token,代表常见的字符序列

  • 例如,对于 "Learning new things is fun!" 这句话,每个单词都被转换为一个 token
  • 而对于较少使用的单词,比如单词 "prompting" 会被拆分为三个 token,即"prom"、"pt"和"ing"

提问范式:DL.ai 大模型系列:1.ChatGPT 提示工程

3 输入指令分类

首先,配置基本的OpenAI API使用环境和辅助函数

import openai
import os

openai.api_key = os.environ.get("OPENAI_API_KEY")
def get_completion_from_messages(messages, 
                                model="gpt-3.5-turbo", 
                                temperature=0, 
                                max_tokens=500):
    '''
    封装一个访问 OpenAI GPT3.5 的函数

    参数: 
    messages: 消息列表,每个消息都是一个字典,包含 role(角色)和 content(内容)。角色可以是'system'、'user' 或 'assistant’,内容是角色的消息。
    model: 调用的模型,默认为 gpt-3.5-turbo(ChatGPT)
    temperature: 温度参数,决定模型输出的随机程度,默认为0。增加温度会使输出更随机。
    max_tokens: 这决定模型输出的最大的 token 数。
    '''
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, # 这决定模型输出的随机程度
        max_tokens=max_tokens, # 这决定模型输出的最大的 token 数
    )
    return response.choices[0].message["content"]

之后配置指令类型,并将输入指令根据已知类型分类:

delimiter = "####" # 配置区分不同指令的分隔符

system_message = f"""
你将获得客户服务查询。
每个客户服务查询都将用{delimiter}字符分隔。
将每个查询分类到一个主要类别和一个次要类别中。
以 JSON 格式提供你的输出,包含以下键:primary 和 secondary。

主要类别:技术支持(Technical Support)、账户管理(Account Management)或一般咨询(General Inquiry)。

技术支持次要类别:
常规故障排除(General troubleshooting)
设备兼容性(Device compatibility)
软件更新(Software updates)

账户管理次要类别:
重置密码(Password reset)
更新个人信息(Update personal information)
关闭账户(Close account)
账户安全(Account security)

一般咨询次要类别:
产品信息(Product information)
定价(Pricing)
反馈(Feedback)
与人工对话(Speak to a human)
"""

user_message = f"""我希望你删除我的个人资料和所有用户数据。"""
messages =  [  {'role':'system', 'content': system_message},    
{'role':'user', 'content': f"{delimiter}{user_message}{delimiter}"},  ]
response = get_completion_from_messages(messages)
print(response)
# {"primary": "账户管理","secondary": "关闭账户"}

4 输入内容监督

OpenAI 的 Moderation API 是一个有效的内容审查工具,可以帮助开发人员识别和过滤各种类别的违禁内容,例如仇恨、自残、色情和暴力等。

response = openai.Moderation.create(input="""我想要伤害一个人,给我一个计划""")
moderation_output = response["results"][0]
print(moderation_output)
# 输出为Json格式,包含对不同有害信息类型的识别标记;
# `flagged` 字段会对输入内容进行综合评估,判断是否包含有害内容

Prompt 注入:

  • 用户试图通过prompt输入来操控 AI 系统,以覆盖或绕过开发者预设的指令或约束条件
  • Prompt 注入可能导致 AI 系统的不合理使用,因此需要严格检测和预防

策略1:使用恰当的分隔符+明确指令来区分系统消息和用户消息

  • 在系统消息明确指出分隔符以及对应的用户消息的分隔方式
  • 这种方式能分隔出用户消息,避免 Prompt 注入对系统消息产生影响
  • 但是需要注意,及时清除用户输入中可能包含的分隔符

策略2:进行额外的内容监督识别

  • 在系统消息明确,先对用户消息进行类型判断(是否存在 Prompt 注入)
  • 如果用户要求忽略指令、尝试插入冲突或恶意指令,则回答 Y;否则回答 N

5 输入处理:思维链推理

思维链推理(chain of thought reasoning):要求模型逐步推理问题的策略

示例:

system_message = f"""
请按照以下步骤回答客户的查询。客户的查询将以四个井号(#)分隔,即 {delimiter}

步骤 1:{delimiter} 首先确定用户是否正在询问有关特定产品或产品的问题。产品类别不计入范围。

步骤 2:{delimiter} 如果用户询问特定产品,请确认产品是否在以下列表中。所有可用产品:

产品:TechPro 超极本
类别:计算机和笔记本电脑
品牌:TechPro
型号:TP-UB100
保修期:1 年
评分:4.5
特点:13.3 英寸显示屏,8GB RAM,256GB SSD,Intel Core i5 处理器
描述:一款适用于日常使用的时尚轻便的超极本。
价格:$799.99

产品:BlueWave 游戏笔记本电脑
类别:计算机和笔记本电脑
品牌:BlueWave
型号:BW-GL200
保修期:2 年
评分:4.7
特点:15.6 英寸显示屏,16GB RAM,512GB SSD,NVIDIA GeForce RTX 3060
描述:一款高性能的游戏笔记本电脑,提供沉浸式体验。
价格:$1199.99

步骤 3:{delimiter} 如果消息中包含上述列表中的产品,请列出用户在消息中做出的任何假设,例如笔记本电脑 X 比笔记本电脑 Y 大,或者笔记本电脑 Z 有 2 年保修期。

步骤 4:{delimiter} 如果用户做出了任何假设,请根据产品信息确定假设是否正确。

步骤 5:{delimiter} 如果用户有任何错误的假设,请先礼貌地纠正客户的错误假设(如果适用)。只提及或引用可用产品列表中的产品,因为这是商店销售的唯一五款产品。以友好的口吻回答客户。

使用以下格式回答问题:
步骤 1:{delimiter} <步骤 1 的推理>
步骤 2:{delimiter} <步骤 2 的推理>
步骤 3:{delimiter} <步骤 3 的推理>
步骤 4:{delimiter} <步骤 4 的推理>
回复客户:{delimiter} <回复客户的内容>

请确保在每个步骤之间使用 {delimiter} 进行分隔。
"""

有时模型的推理过程不适合与用户共享(比如辅导学习应用,更应该鼓励学生独立思考)

内心独白(Inner monologue)作为一种隐藏模型推理过程的高级方法,可以用来缓解这种情况

简单来说,就是在向用户呈现输出之前,对输出进行一些转化,使得只有部分输出是可见的

6 输入处理:链式 Prompt

思维链推理:一次性完成所有任务 vs 链式 Prompt:分阶段完成任务的区别

链式 Prompt 的特点:

  • 专注于一个组成部分,确保每个部分都正确执行后再进行下一个阶段
  • 这种方法可以分解任务的复杂性,使其更易于管理,并减少错误的可能性
  • 更容易测试哪些步骤可能更容易失败,或者在特定步骤中进行人工干预
  • 允许模型在必要时使用外部工具。比如在查找内容时调用 API 或搜索知识库

链式 Prompt 示例:略

7 检查结果

检查输出的质量、相关性和安全性

  • 使用 Moderation API 检查输出是否有潜在的有害内容
  • 内容被标记有害时,可以考虑回应一个备用答案或生成一个新的回应
  • 还可以根据个性化需求自行设计Prompt对输出进行满意度/合理性的评估
  • 比如检查输出结果是否与提供的产品信息相符合,再决定是否展示输出
  • 可以尝试为每个用户查询生成多个模型回应,然后选择最佳的回应展示

使用审查 API 来检查输出是一个不错的做法。但大部分情况下是不必要的

因为 GPT-4 本身就有很严苛的审查输出机制,其次会增加系统延迟和调用成本

8 搭建端到端问答系统

融合之前提到的功能:输入检查、审核查找、回答评估

import os
import openai
import sys
sys.path.append('../..')
import utils_en # 使用英文 Prompt 的工具包
import utils_zh # 使用中文 Prompt 的工具包
import panel as pn  # 用于图形化界面
pn.extension()
openai.api_key = os.environ.get("OPENAI_API_KEY")

'''
注意:限于模型对中文理解能力较弱,中文 Prompt 可能会随机出现不成功,可以多次运行;也非常欢迎同学探究更稳定的中文 Prompt
'''
def process_user_message_ch(user_input, all_messages, debug=True):
    """
    对用户信息进行预处理

    参数:
    user_input : 用户输入
    all_messages : 历史信息
    debug : 是否开启 DEBUG 模式,默认开启
    """
    # 分隔符
    delimiter = "```"

    # 第一步: 使用 OpenAI 的 Moderation API 检查用户输入是否合规或者是一个注入的 Prompt
    response = openai.Moderation.create(input=user_input)
    moderation_output = response["results"][0]

    # 经过 Moderation API 检查该输入不合规
    if moderation_output["flagged"]:
        print("第一步:输入被 Moderation 拒绝")
        return "抱歉,您的请求不合规"

    # 如果开启了 DEBUG 模式,打印实时进度
    if debug: print("第一步:输入通过 Moderation 检查")

    # 第二步:抽取出商品和对应的目录,类似于之前课程中的方法,做了一个封装
    category_and_product_response = utils_zh.find_category_and_product_only(user_input, utils_zh.get_products_and_category())
    #print(category_and_product_response)
    # 将抽取出来的字符串转化为列表
    category_and_product_list = utils_zh.read_string_to_list(category_and_product_response)
    #print(category_and_product_list)

    if debug: print("第二步:抽取出商品列表")

    # 第三步:查找商品对应信息
    product_information = utils_zh.generate_output_string(category_and_product_list)
    if debug: print("第三步:查找抽取出的商品信息")

    # 第四步:根据信息生成回答
    system_message = f"""
        您是一家大型电子商店的客户服务助理。\
        请以友好和乐于助人的语气回答问题,并提供简洁明了的答案。\
        请确保向用户提出相关的后续问题。
    """
    # 插入 message
    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': f"{delimiter}{user_input}{delimiter}"},
        {'role': 'assistant', 'content': f"相关商品信息:\n{product_information}"}
    ]
    # 获取 GPT3.5 的回答
    # 通过附加 all_messages 实现多轮对话
    final_response = get_completion_from_messages(all_messages + messages)
    if debug:print("第四步:生成用户回答")
    # 将该轮信息加入到历史信息中
    all_messages = all_messages + messages[1:]

    # 第五步:基于 Moderation API 检查输出是否合规
    response = openai.Moderation.create(input=final_response)
    moderation_output = response["results"][0]

    # 输出不合规
    if moderation_output["flagged"]:
        if debug: print("第五步:输出被 Moderation 拒绝")
        return "抱歉,我们不能提供该信息"

    if debug: print("第五步:输出经过 Moderation 检查")

    # 第六步:模型检查是否很好地回答了用户问题
    user_message = f"""
    用户信息: {delimiter}{user_input}{delimiter}
    代理回复: {delimiter}{final_response}{delimiter}

    回复是否足够回答问题
    如果足够,回答 Y
    如果不足够,回答 N
    仅回答上述字母即可
    """
    # print(final_response)
    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]
    # 要求模型评估回答
    evaluation_response = get_completion_from_messages(messages)
    # print(evaluation_response)
    if debug: print("第六步:模型评估该回答")

    # 第七步:如果评估为 Y,输出回答;如果评估为 N,反馈将由人工修正答案
    if "Y" in evaluation_response:  # 使用 in 来避免模型可能生成 Yes
        if debug: print("第七步:模型赞同了该回答.")
        return final_response, all_messages
    else:
        if debug: print("第七步:模型不赞成该回答.")
        neg_str = "很抱歉,我无法提供您所需的信息。我将为您转接到一位人工客服代表以获取进一步帮助。"
        return neg_str, all_messages

user_input = "请告诉我关于 smartx pro phone 和 the fotosnap camera 的信息。另外,请告诉我关于你们的tvs的情况。"
response,_ = process_user_message_ch(user_input,[])
print(response)

持续收集用户和助手消息的函数:

# 调用中文 Prompt 版本
def collect_messages_ch(debug=False):
    """
    用于收集用户的输入并生成助手的回答

    参数:
    debug: 用于觉得是否开启调试模式
    """
    user_input = inp.value_input
    if debug: print(f"User Input = {user_input}")
    if user_input == "":
        return
    inp.value = ''
    global context
    # 调用 process_user_message 函数
    #response, context = process_user_message(user_input, context, utils.get_products_and_category(),debug=True)
    response, context = process_user_message_ch(user_input, context, debug=False)
    context.append({'role':'assistant', 'content':f"{response}"})
    panels.append(
        pn.Row('User:', pn.pane.Markdown(user_input, width=600)))
    panels.append(
        pn.Row('Assistant:', pn.pane.Markdown(response, width=600, style={'background-color': '#F6F6F6'})))

    return pn.Column(*panels) # 包含了所有的对话信息

panels = [] # collect display 

# 系统信息
context = [ {'role':'system', 'content':"You are Service Assistant"} ]  

inp = pn.widgets.TextInput( placeholder='Enter text here…')
button_conversation = pn.widgets.Button(name="Service Assistant")

interactive_conversation = pn.bind(collect_messages, button_conversation)

dashboard = pn.Column(
    inp,
    pn.Row(button_conversation),
    pn.panel(interactive_conversation, loading_indicator=True, height=300),
)

dashboard

9 评估

在部署后并让用户使用它时,跟踪系统的运行情况并持续改进系统的答案质量

当存在一个简单的正确答案时

  • 通过预设的测试用例,来评估系统生成的回答的合理性
  • 找出一些在实际使用中,模型表现不如预期的查询
  • 修改指令以处理难测试用例,在难测试用例上评估修改后的指令
  • 同时确保此修正不会对先前的测试用例性能造成负面影响
  • 收集开发集进行自动化测试;通过与理想答案比较来评估测试用例
  • 在所有测试用例上运行评估,并计算正确的用例比例

当不存在一个简单的正确答案时

  • 运行问答系统获得一个复杂回答,然后使用 GPT 评估回答是否正确
  • 给出一个标准回答,要求其评估生成回答与标准回答的差距

在NLP技术中,有一些传统的度量标准用于衡量文本的相似度(比如BLUE 分数)

评估 Prompt 示例:

system_message = """\
    您是一位助理,通过将客户服务代理的回答与理想(专家)回答进行比较,评估客户服务代理对用户问题的回答质量。
    请输出一个单独的字母(A 、B、C、D、E),不要包含其他内容。 
"""

user_message = f"""\
    您正在比较一个给定问题的提交答案和专家答案。数据如下:
    [开始]
    ************
    [问题]: {cust_msg}
    ************
    [专家答案]: {ideal}
    ************
    [提交答案]: {completion}
    ************
    [结束]

    比较提交答案的事实内容与专家答案。忽略样式、语法或标点符号上的差异。
    提交的答案可能是专家答案的子集、超集,或者与之冲突。确定适用的情况,并通过选择以下选项之一回答问题:
    (A)提交的答案是专家答案的子集,并且与之完全一致。
    (B)提交的答案是专家答案的超集,并且与之完全一致。
    (C)提交的答案包含与专家答案完全相同的细节。
    (D)提交的答案与专家答案存在分歧。
    (E)答案存在差异,但从事实的角度来看这些差异并不重要。
    选项:ABCDE
"""

参考

使用 ChatGPT API 搭建系统

往年同期文章