一、前言
前面几节我们分别学习了很多构建 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()就会容易很多。