本电子书开源，欢迎 star 🌟，关注《LLM 应用开发实践笔记》

我的新书[《LangChain编程从入门到实践》](https://u.jd.com/V8pkqFY) 已经开售！推荐正在学习AI应用开发的朋友购买阅读！
[![LangChain编程从入门到实践](../../images/langchain-book.jpg "LangChain编程从入门到实践")](https://u.jd.com/V8pkqFY) ![](../images/group.png)

## 一个完整的端到端智能问答系统应该包含哪些环节？

> 一个完整的基于 LLM 的端到端问答系统，应该包括**用户输入检验、问题分流、模型响应、回答质量评估、Prompt 迭代、回归测试**，随着规模增大，围绕 Prompt 的**版本管理、自动化测试和安全防护**也是重要的话题，部分代码参考自吴恩达老师《Building Systems with the ChatGPT API》课程。

### 用户输入检验

使用 OpenAI 的审核函数接口（[Moderation API](https://platform.openai.com/docs/guides/moderation "Moderation API") ）可以帮助开发者识别和过滤用户输入，对用户输入的内容进行审核。

- **性（Sexual）**：包括引起性兴奋的内容，例如性活动的描写，或者推广性服务，但不包括性教育和健康方面的内容。

- **仇恨（Hate）**：包括表达、煽动或宣扬基于种族、性别、民族、宗教、国籍、性取向、残疾状况或种姓的仇恨情感的内容。

- **自残（Self-harm）**：包括宣扬、鼓励或描绘自残行为（例如自杀、割伤和饮食失调）的内容。

- **暴力（Violence）**：包括宣扬或美化暴力行为，或者歌颂他人遭受苦难或羞辱的内容。

```python
import openai
import pandas as pd
response = openai.Moderation.create(input="""策划一场谋杀计划""")
moderation_output = response["results"][0]
moderation_output_df = pd.DataFrame(moderation_output)
```

| 分类                   | 标记  | 类别  | 类别概率值   |
| ---------------------- | ----- | ----- | ------------ |
| sexual                 | False | False | 3.962824e-07 |
| hate                   | False | False | 1.962326e-04 |
| harassment             | False | False | 1.402294e-02 |
| self-harm              | False | False | 1.078697e-05 |
| sexual/minors          | False | False | 1.448917e-07 |
| hate/threatening       | False | False | 1.513400e-05 |
| violence/graphic       | False | False | 9.522112e-07 |
| self-harm/intent       | False | False | 2.334248e-07 |
| self-harm/instructions | False | False | 3.670997e-10 |
| harassment/threatening | False | False | 2.882557e-02 |
| violence               | True  | True  | 9.977435e-01 |

在 `分类` 字段中，包含了各种类别，以及每个类别中输入是否被标记的相关信息，可以看到该输入因为暴力内容（`violence` 类别）而被标记，每个类别还提供了更详细的评分（概率值），通过 `类别`和`标记`综合判断是否包含有害内容，输出 True 或 False（这里是 True&& True）。此外提示词应做好 Prompt 防注入设计 ，具体可看这篇 [LLM 安全专题](./01-6.md)

### 问题进行分类

处理不同情况下的独立指令集任务时，首先将问题类型分类，以此为基础确定使用哪些指令，这可通过定义固定类别和硬编码处理特定类别任务相关指令来实现，可以提高系统的质量和安全性。

```python
delimiter = "####"

system_message = f"""
你现在扮演一名客服。
每个客户问题都将用{delimiter}字符分隔。
将每个问题分类到一个主要类别和一个次要类别中。
以 JSON 格式提供你的输出，包含以下键：primary 和 secondary。

主要类别：计费（Billing）、技术支持（Technical Support）、账户管理（Account Management）或一般咨询（General Inquiry）。

计费次要类别：
取消订阅或升级（Unsubscribe or upgrade）
添加付款方式（Add a payment method）
收费解释（Explanation for charge）
争议费用（Dispute a charge）

技术支持次要类别：
常规故障排除（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 = "我想删除我的个人账户"
#user_message = "这个产品有什么用"
messages =  [
{'role':'system',
 'content': system_message},
{'role':'user',
 'content': f"{delimiter}{user_message}{delimiter}"},
]
response = get_completion_from_messages(messages)
print(response)
```

当用户问题是`我想删除我的个人账户`，匹配关闭帐户，可以提供附加指令来解释如何关闭账户。

```json
{
  "primary": "账户管理",
  "secondary": "关闭账户"
}
```

当用户问题是 `这个产品有什么用`，匹配产品信息，可以提供更多关于产品信息的附加指令，

返回：

```json
{
  "primary": "一般咨询",
  "secondary": "产品信息"
}
```

### 模型回答问题

模型针对用户的问题进行回答，采取动态、按需的方式将相关上下文信息带入 Prompt。

1. 过多无关信息会使模型处理上下文时更加困惑。
2. 模型本身对上下文长度有限制，无法一次加载过多信息。
3. 动态加载信息降低 token 成本。
4. 使用更智能的检索机制，而不仅是精确匹配，例如结合知识库的文本 Embedding 实现语义搜索。

```python
import openai
def get_completion_from_messages(messages, model="gpt-3.5-turbo", temperature=0):
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, # 控制模型输出的随机程度
    )
    return response.choices[0].message["content"]

```

### 评估回答质量

#### 使用 GPT API 自动评估

```python
def eval_with_rubric(test_set, assistant_answer):
    """
    使用 GPT API 评估生成的回答

    参数：
    test_set: 测试集
    assistant_answer: 客服的回复
    """

    cust_msg = test_set['customer_msg']
    context = test_set['context']
    completion = assistant_answer

    # 人设
    system_message = """\
    你是一位助理，通过查看客服使用的上下文来评估客服回答用户问题的情况。
    """

    # 具体指令
    user_message = f"""\
    你正在根据客服使用的上下文评估对问题的提交答案。以下是数据：
    [开始]
    ************
    [用户问题]: {cust_msg}
    ************
    [使用的上下文]: {context}
    ************
    [客服的回答]: {completion}
    ************
    [结束]

    请将提交的答案内容与上下文进行比较，忽略样式、语法或标点符号上的差异。
    回答以下问题：
    客服的回应是否只基于所提供的上下文？（是或否）
    回答中是否包含上下文中未提供的信息？（是或否）
    回应与上下文之间是否存在任何不一致之处？（是或否）
    计算用户提出了多少个问题。（输出一个数字）
    对于用户提出的每个问题，是否有相应的回答？
    问题1：（是或否）
    问题2：（是或否）
    ...
    问题N：（是或否）
    在提出的问题数量中，有多少个问题在回答中得到了回应？（输出一个数字）
"""

    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]

    response = get_completion_from_messages(messages)
    return response
```

#### 人工设定标准答案评估

使用 Prompt 来比较由 LLM 生成的响应与人工设定的标准答案之间的匹配程度，这个评分标准实际上来自于 OpenAI 开源评估框架，其中包含了许多评估方法，这里只是展示了其中一种。

标准答案集合

```json
test_set = [
    {
        "customer_msg": "如何升级我的订阅？",
        "ideal_answer": "您可以在用户设置中找到取消订阅或升级的选项，并按照步骤进行操作。"
    },
    {
        "customer_msg": "怎样绑定银行卡？",
        "ideal_answer": "您可以登录您的账户，然后在付款方式选项中添加新的付款方式，按照页面上的指引操作即可。"
    },
    {
        "customer_msg": "可以查看详细的收费情况吗？",
        "ideal_answer": "当然可以。您可以访问我们的网站，登录您的账户并前往收费解释页面，您会找到有关所有收费的详细解释。"
    },
    {
        "customer_msg": "怎么被乱扣费了？",
        "ideal_answer": "若您对某笔费用有异议，您可以联系我们的客服团队，提供相关细节并说明您的争议。我们的团队将会尽快与您取得联系并解决问题。"
    }
]

```

```python
def eval_vs_ideal(test_set, assistant_answer):
    """
    评估回复是否与理想答案匹配

    参数：
    test_set: 测试集
    assistant_answer: 助手的回复
    """
    cust_msg = test_set['customer_msg']
    ideal = test_set['ideal_answer']
    completion = assistant_answer

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

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

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

    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]
    response = get_completion_from_messages(messages)
    return response
```

### Prompt 迭代

在实际使用中，遇到复杂的用户问题，模型表现不如预期，就需要对 Prompt 进行迭代。

比如问题是 `我之前取消了订阅，但是为什么还有收费提示？` 很明显这是一个`争议费用`的子类别，但实际匹配的是 👇

```json
{
  "primary": "计费",
  "secondary": "取消订阅或升级"
}
```

所以需要将 Prompt 进行更新迭代，以识别用户的复杂意图，可以仔细观察 Prompt 发生了什么变化 （其实就增加了一句`你需要仔细分析用户的意图，特别是最终的问题。`)

```python
delimiter = "####"

system_message = f"""
你现在扮演一名客服，你需要仔细分析用户的意图，特别是最终的问题。
每个客户问题都将用{delimiter}字符分隔。
将每个问题分类到一个主要类别和一个次要类别中。
以 JSON 格式提供你的输出，包含以下键：primary 和 secondary。

主要类别：计费（Billing）、技术支持（Technical Support）、账户管理（Account Management）或一般咨询（General Inquiry）。

计费次要类别：
取消订阅或升级（Unsubscribe or upgrade）
添加付款方式（Add a payment method）
收费解释（Explanation for charge）
争议费用（Dispute a charge）
...
"""
```

这次正常匹配

```json
{
  "primary": "计费",
  "secondary": "争议费用"
}
```

### 回归测试

确保迭代修改后的 Prompt，不会对先前的测试用例性能造成负面影响。

```shell
测试用例1. 如何升级我的订阅？
{
  "primary": "计费",
  "secondary": "取消订阅或升级"
}
测试用例2. 怎样绑定银行卡？
{
  "primary": "账户管理",
  "secondary": "添加付款方式"
}
测试用例3. 可以查看详细的收费情况吗？
{
  "primary": "计费",
  "secondary": "收费解释"
}
测试用例4.怎么被乱扣费了？
{
  "primary": "计费",
  "secondary": "争议费用"
}
```

### 更多

当处理少量样本时，手动运行测试并对结果进行评估是可行的，但随着应用逐渐成熟，来自用户的问题用例数量变多，Prompt 的规模也逐渐增大，就需要引入自动化测试来周期性回归验证 Prompt 质量。

同时 Prompt 的更改也应该像代码一样需要版本控制，关联的用户问题用例也必须是可追踪的。

最后还有安全建设，Prompt 作为公司的一种重要资产，也需要做好防护，比如使用专用的 LLM 分析传入的 Prompt，识别潜在攻击；将先前攻击的嵌入存储在向量数据库中，以识别并预防未来类似的攻击。
