一、前言

前面几节我们分别学习了很多构建 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()

就会容易很多。

 

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐