LLM-Cookbook 学习——搭建基于 ChatGPT 的问答系统>搭建一个带评估的端到端问答系统
一、前言
前面几节我们分别学习了很多构建 LLM 应用需要的模块,例如:
-
Classification:对用户输入进行分类;
-
Moderation:检查输入是否安全;
-
Chain of Thought:处理复杂问题;
-
Chaining Prompts:将复杂任务拆成多个步骤;
-
Check Outputs:检查模型最终生成的答案。
但是前面的内容大多是在单独介绍某一个模块。
这一节开始把这些模块真正连接起来,构建一个完整的:
End-to-End Question Answering System——端到端问答系统
所谓“端到端”,可以简单理解成:
用户提出问题
↓
系统自动完成中间所有处理
↓
最终返回答案
用户不需要关心中间经历了多少个 Prompt、多少次检索和多少次模型调用。
这一节实现的客服系统核心流程为:
用户输入
↓
输入安全检查
↓
提取商品和商品类别
↓
查询商品资料
↓
LLM 生成回答
↓
输出安全检查
↓
LLM 评价回答质量
↓
返回答案 / 转人工客服
这已经非常接近一个实际 LLM 应用的基本架构。
二、本节最核心的函数:process_user_message_ch()
这一节最重要的代码就是:
process_user_message_ch()
从名字就可以理解:
process
↓
处理
user_message
↓
用户消息
因此它的作用就是:
接收一条用户消息,然后经过完整的问答流程,最后返回处理后的回答。
函数大致定义为:
def process_user_message_ch(
user_input,
all_messages,
debug=True
):
这里有三个参数。
user_input
表示:
用户当前输入的问题
例如:
user_input = "请介绍一下 SmartX ProPhone"
all_messages
表示:
之前所有的对话历史
它的作用非常重要。
如果没有:
all_messages
那么每次用户提问对于模型来说都是一次新的对话。
例如:
用户:
SmartX ProPhone 多少钱?
助手:
899.99 美元。
用户:
它支持 5G 吗?
第二句话中的:
它
到底指什么?
如果没有前面的聊天记录,模型可能不知道。
但是通过:
all_messages
把历史对话一起发送给模型,模型就能够知道:
它 = SmartX ProPhone
因此:
all_messages是这个系统实现多轮对话的重要基础。
debug=True
这个参数表示:
是否开启调试模式
如果:
debug=True
程序就会不断输出:
第一步:输入通过 Moderation 检查
第二步:抽取出商品列表
第三步:查找抽取出的商品信息
……
这样做最大的作用就是:
方便开发者知道程序现在运行到了哪一步。
如果系统出现错误,就可以快速定位到底是哪一步出问题。
三、整个问答系统的七个步骤
这一节的代码虽然比较长,但其实只需要抓住一个核心:
process_user_message_ch()就是在按照顺序执行七个步骤。
可以先建立一个整体认识:
用户问题
│
↓
① Moderation 输入检查
│
↓
② 提取商品和类别
│
↓
③ 查询商品信息
│
↓
④ 生成回答
│
↓
⑤ Moderation 输出检查
│
↓
⑥ LLM 自评
│
┌────┴────┐
│ │
Y N
│ │
↓ ↓
⑦返回答案 转人工客服
下面逐步分析。
四、第一步:检查用户输入
逻辑可以简化成:
moderation_result = moderation(user_input)
if moderation_result["flagged"]:
return "抱歉,您的请求不合规"
也就是:
用户输入
↓
Moderation
↓
是否违规?
┌────┴────┐
│ │
是 否
│ │
↓ ↓
拒绝 继续
五、第二步:从问题中提取商品和商品类别
如果用户输入通过检查,程序开始分析:
用户到底在问哪些商品?
课程使用:
utils_zh.find_category_and_product_only(...)
来完成这一步。
例如用户输入:
请告诉我 SmartX ProPhone 和 FotoSnap Camera 的信息,
另外介绍一下你们的电视。
模型需要识别:
SmartX ProPhone
↓
Phones and Accessories
FotoSnap Camera
↓
Cameras and Camcorders
TV
↓
Televisions and Home Theater Systems
也就是说,这一步不是直接回答问题,而是在完成:
信息抽取(Information Extraction)
可以理解为:
自然语言
↓
结构化信息
例如原始问题:
“介绍一下 SmartX ProPhone”
经过处理后可能变成类似:
[
{
"category": "Smartphones",
"products": ["SmartX ProPhone"]
}
]
六、read_string_to_list() 是干什么的?
接下来课程中又调用:
utils_zh.read_string_to_list(...)
这里很容易产生疑问:
前面不是已经识别出商品了吗?为什么还要再转换?
因为 LLM 返回的内容本质上通常还是:
字符串 String
看起来即使像:
[
{"category": "手机", "products": ["SmartX ProPhone"]}
]
它也可能只是:
一串文本
而不是 Python 真正能够直接操作的:
list
所以:
read_string_to_list()
相当于进行:
模型输出的字符串
↓
Python 数据结构
↓
list
这样程序后面才更方便进行遍历、查询和处理。
七、第三步:根据商品名称查询商品资料
拿到商品列表以后,接下来调用:
utils_zh.generate_output_string(...)
获取真正的商品信息。
例如前面只知道:
SmartX ProPhone
现在需要查出:
品牌:SmartX
型号:SX-PP10
屏幕:6.1 英寸
存储:128GB
摄像头:12MP
网络:5G
价格:899.99 美元
……
这一过程可以理解为:
用户问题
↓
提取商品名称
↓
商品数据库
↓
找到对应商品资料
这一步非常关键。
因为我们不希望 LLM 完全依靠自己训练时学到的知识回答。
我们希望:
先找到可信的业务数据,再让 LLM 根据这些数据回答。
这种思想实际上已经与 RAG 非常接近:
Retrieval
↓
检索资料
Generation
↓
根据资料生成回答
八、第四步:让 LLM 根据资料生成最终回答
拿到商品资料以后,就可以真正让 LLM 回答用户问题了。
首先定义:
system_message
它负责告诉模型:
你是谁?
你应该用什么语气?
你应该怎样回答?
例如系统设定模型是:
一家大型电子商店的客户服务助理
并要求:
语气友好
回答简洁
乐于帮助用户
这里再次体现了:
System Message
↓
规定模型角色和行为
而用户真正的问题则放在:
User Message
中。
九、messages 为什么包含三种内容?
这一部分构造的 messages 很值得理解。
整体可以理解成:
messages = [
system_message,
用户的问题,
查询得到的商品资料
]
从模型视角来看:
System:
你是一名电子商店客服。
User:
用户问了 SmartX ProPhone。
Assistant / Context:
这是与这个问题有关的商品资料。
→ 请根据这些信息生成回答。
所以 LLM 并不是凭空回答。
它实际上同时拥有:
用户问题
+
系统规则
+
商品资料
最终:
final_response
就是模型生成的客服答案。
十、为什么是 all_messages + messages?
课程中一个非常值得注意的地方是:
get_completion_from_messages(
all_messages + messages
)
这里的:
+
不是数学上的加法。
因为:
all_messages
和:
messages
都是列表。
Python 中:
list1 + list2
表示:
把两个列表拼接起来。
例如:
a = [1, 2]
b = [3, 4]
print(a + b)
得到:
[1, 2, 3, 4]
所以:
all_messages + messages
实际上就是:
以前的聊天历史
+
当前这一轮消息
↓
完整对话上下文
最后一起交给 LLM。
这就是这个系统实现:
多轮上下文对话
的重要机制。
十一、为什么又要更新 all_messages?
模型回答后:
all_messages
还需要继续更新。
原因很简单:
这一轮对话结束
↓
这一轮也成为“历史”
↓
下一轮模型需要看到
例如:
第一轮:
用户:SmartX ProPhone 多少钱?
AI:899.99 美元。
下一轮:
用户:它有保修吗?
如果第一轮已经保存进:
all_messages
模型就可以结合历史判断:
“它”
=
SmartX ProPhone
所以多轮对话本质上就是不断进行:
新消息
↓
加入 history
↓
下一轮再次发送 history
十二、messages[1:] 是什么意思?
代码中还会看到类似:
messages[1:]
这是 Python 的:
切片 Slice
假设:
messages = [
"system",
"user",
"product_information"
]
那么:
messages[1:]
意思就是:
从下标 1 开始
一直取到最后
结果:
[
"user",
"product_information"
]
因为 Python 下标从:
0
开始。
所以:
messages[0] → system
messages[1] → user
messages[2] → product_information
这里使用:
messages[1:]
就是把:
当前这一轮需要保存的内容
添加到历史消息中。
十三、第五步:再次使用 Moderation 检查模型输出
这一步正好对应上一节:
Check Outputs
虽然用户输入已经通过 Moderation,但这不意味着 LLM 最终生成的回答一定没有问题。
因此:
用户输入
检查一次。
模型生成:
final_response
之后再检查一次。
形成:
Moderation
↓
用户输入 → LLM → 模型回答
↓
Moderation
因此系统实际上有:
入口安全检查
+
出口安全检查
如果最终回答被标记:
flagged = True
程序就不会直接把原回答发给用户。
而是返回一个更加安全的提示。
十四、第六步:让模型评价自己的回答
安全检查通过以后,还没有结束。
因为:
安全
≠
回答正确
也不代表:
安全
≠
真正回答了用户问题
因此系统又进行了一次:
Response Evaluation
即:
模型评价模型生成的答案
程序会把:
用户问题
+
刚刚生成的回答
再次交给 LLM。
然后问:
这个回答是否足够回答用户的问题?
要求模型只返回:
Y
或者:
N
这实际上就是:
第一次调用 LLM
↓
Generator
生成答案
第二次调用 LLM
↓
Evaluator
评价答案
因此:
LLM 不仅负责生成
LLM 还可以负责评价
这也是上一节学习的 LLM-as-a-Judge 思想在完整系统中的实际使用。
十六、为什么写 "Y" in evaluation_response?
课程使用类似:
if "Y" in evaluation_response:
而不是:
if evaluation_response == "Y":
区别在于:
==
要求:
两边内容必须完全一样。
例如:
evaluation_response = "Yes"
那么:
evaluation_response == "Y"
结果是:
False
但是:
"Y" in "Yes"
结果:
True
课程这么做的目的,是为了应对 LLM 偶尔没有严格按照 Prompt 只输出:
Y
而输出:
Yes
的情况。
因此这里的:
in
表示:
检查某个字符串是否包含在另一个字符串中。
例如:
"a" in "apple"
得到:
True
十七、第七步:回答通过就返回,否则转人工
经过前面所有步骤后,最终进行判断。
如果评价结果为:
Y
说明模型认为:
回答充分解决了用户问题
于是:
直接把 final_response 返回给用户
如果结果是:
N
程序不会强行把这个答案发送出去。
而是告诉用户:
当前无法提供合适答案,
将转接人工客服。
所以最终逻辑就是:
LLM评价
│
┌──────┴──────┐
│ │
Y N
│ │
↓ ↓
返回模型答案 转人工客服
这体现了一个非常重要的系统设计思想:
模型无法可靠完成任务时,系统应该允许失败和降级,而不是强制让模型给出答案。
十八、把七步流程完整串起来
到这里就可以把:
process_user_message_ch()
理解成下面这个“大函数”:
process_user_message_ch()
输入:
用户问题
历史对话
↓
① 输入 Moderation
↓
② 识别商品和类别
↓
③ 查询商品数据库
↓
④ LLM 根据资料回答
↓
⑤ 输出 Moderation
↓
⑥ LLM 判断回答质量
↓
⑦ 返回答案 / 转人工
输出:
最终回答
更新后的聊天历史
它自己并不负责完成所有具体任务,而是:
调用各种不同的小模块
↓
按照一定顺序组织它们
↓
最终完成整个业务流程
这其实已经非常接近 Agent 和 Workflow 中的:
Orchestration
即:
任务编排。
十九、collect_messages_ch() 又是什么?
完成核心问答逻辑以后,课程还定义了:
collect_messages_ch()
这个函数主要负责:
接收界面上的用户输入,并调用前面的问答系统。
注意它和:
process_user_message_ch()
功能不一样。
可以这样区分:
collect_messages_ch()
↓
负责“界面层”
process_user_message_ch()
↓
负责“业务逻辑层”
例如:
用户在输入框打字
↓
collect_messages_ch()
↓
process_user_message_ch()
↓
生成回答
↓
collect_messages_ch()
↓
把回答显示在网页上
二十、global context 是什么意思?
代码中出现:
global context
这里表示:
函数内部要使用函数外部定义的
context变量。
context 保存的是:
聊天历史
例如:
context = [
{
"role": "system",
"content": "You are Service Assistant"
}
]
然后每进行一轮聊天:
用户消息
+
助手回答
都会加入:
context
于是:
context
就越来越长。
形成:
System
User 1
Assistant 1
User 2
Assistant 2
User 3
Assistant 3
……
这样就可以实现连续多轮聊天。
二十二、使用 Panel 制作简单聊天界面
前面虽然已经实现问答系统,但是仍然只能:
print(response)
这样使用显然不够直观。
所以课程最后使用:
Panel
制作一个简单的图形化聊天界面。
首先:
import panel as pn
然后创建输入框:
pn.widgets.TextInput(...)
可以理解成网页上的:
┌─────────────────────┐
│ 在这里输入问题…… │
└─────────────────────┘
再创建按钮:
pn.widgets.Button(...)
类似:
┌──────────────┐
│ Service │
│ Assistant │
└──────────────┘
用户输入问题以后点击按钮:
点击按钮
↓
collect_messages_ch()
↓
process_user_message_ch()
↓
生成回答
↓
显示在页面
这样,一个最基础的聊天机器人界面就搭建出来了。
二十三、pn.bind() 是什么意思?
课程中还有:
pn.bind(...)
可以简单理解成:
把某个界面操作和某个 Python 函数绑定起来。
例如:
点击按钮
↓
触发 collect_messages_ch()
用户不是直接调用:
collect_messages_ch()
而是:
用户点击按钮
↓
Panel 自动调用函数
于是 Python 后端逻辑和网页界面连接了起来。
二十六、如果某一步效果不好怎么办?
课程最后还提出了一个很重要的思想。
搭建完系统不意味着:
项目结束
反而意味着:
现在终于可以开始系统地测试它了。
例如测试大量问题以后发现:
问题1:
商品名称经常提取错误
那么应该优化:
第二步
如果发现:
问题2:
商品找对了,但是回答经常遗漏信息
就应该优化:
第四步 Prompt
如果发现:
问题3:
Evaluator 经常错误判断
则应该优化:
第六步评价标准
因此整个过程实际上是:
搭建系统
↓
测试
↓
发现错误
↓
定位错误发生在哪一步
↓
修改对应步骤
↓
再次测试
形成:
Build
↓
Evaluate
↓
Improve
↓
Evaluate Again
二十七、对“评估”这个概念的新理解
以前看到:
Evaluation
可能第一反应是:
准确率是多少?
但是对于 LLM 系统来说,仅仅看一个准确率远远不够。
一个完整系统可能需要分别评估:
输入分类是否正确?
↓
商品识别是否正确?
↓
检索资料是否正确?
↓
生成答案是否正确?
↓
安全审核是否正确?
↓
Evaluator 是否判断正确?
也就是说:
除了评价最终答案,还应该评价整个 Pipeline 中的各个中间步骤。
这也是后面两章:
Evaluation Part 1
Evaluation Part 2
继续深入讨论的内容。
二十八、这一节可以抽象成一个通用 LLM 系统
虽然课程演示的是:
电子产品客服
但是去掉具体商品名称以后,整个架构其实非常通用:
用户问题
↓
安全检查
↓
理解用户需求
↓
检索相关资料
↓
LLM 根据资料回答
↓
答案安全检查
↓
答案质量评价
↓
返回最终结果
所以:
商品数据库
完全可以替换成:
企业知识库
PDF 文档
论文数据库
芯片 Datasheet
法律文档
医疗指南
产品说明书
整个系统架构依然成立。
二十九、结合 RAG 理解这一节
学习到这里以后,其实已经可以看到 RAG 的基本影子。
RAG:
Retrieval-Augmented Generation
即:
检索增强生成
核心思想:
用户问题
↓
Retrieval
检索相关资料
↓
将资料放入 Prompt
↓
Generation
LLM 根据资料生成回答
而本节:
用户问题
↓
提取商品
↓
查询商品资料
↓
LLM 根据商品资料回答
实际上已经具有:
Retrieval
+
Generation
两个关键步骤。
只不过课程中的数据规模比较小,所以使用普通函数查找商品。
当数据变成:
几百份 PDF
几千份文档
几十万段文本
以后,就需要更加完整的:
Embedding
+
Vector Database
+
Retrieval
+
LLM
也就是后面常见的 RAG 架构。
三十、本节代码中几个容易混淆的变量
| 变量 | 含义 |
|---|---|
user_input |
用户当前输入的问题 |
all_messages |
历史聊天记录 |
delimiter |
Prompt 内容分隔符 |
category_and_product_response |
模型识别出的商品和类别 |
category_and_product_list |
转换后的 Python 列表 |
product_information |
查询得到的商品详细资料 |
system_message |
定义 AI 身份和回答规则 |
messages |
当前准备发送给模型的消息 |
final_response |
LLM 最终生成的客服回答 |
evaluation_response |
LLM 对回答质量的评价 |
context |
图形聊天界面中持续保存的聊天历史 |
debug |
是否显示各步骤运行信息 |
把这些变量理解清楚以后,再阅读:
process_user_message_ch()
就会容易很多。
更多推荐

所有评论(0)