LLM-Cookbook 学习——搭建基于 ChatGPT 的问答系统>搭建一个带评估的端到端问答系统
2026/8/22 12:05:13 网站建设 项目流程

一、前言

前面几节我们分别学习了很多构建 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历史聊天记录
delimiterPrompt 内容分隔符
category_and_product_response模型识别出的商品和类别
category_and_product_list转换后的 Python 列表
product_information查询得到的商品详细资料
system_message定义 AI 身份和回答规则
messages当前准备发送给模型的消息
final_responseLLM 最终生成的客服回答
evaluation_responseLLM 对回答质量的评价
context图形聊天界面中持续保存的聊天历史
debug是否显示各步骤运行信息

把这些变量理解清楚以后,再阅读:

process_user_message_ch()

就会容易很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询