尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

LLM-Cookbook 学习——搭建基于 ChatGPT 的问答系统> Chaining Prompts

LLM-Cookbook 学习——搭建基于 ChatGPT 的问答系统> Chaining Prompts 一、本章主要学习什么上一章我们学习了Chain of Thought 思维链核心思想是复杂问题 ↓ 拆成多个推理步骤 ↓ 逐步处理 ↓ 得到最终答案而这一章进一步提出如果一个任务非常复杂为什么一定要在一次 LLM 调用中把所有步骤都做完我们完全可以把复杂任务拆成多个独立任务然后Prompt 1 ↓ 结果 1 ↓ Python / 数据库 / API ↓ Prompt 2 ↓ 结果 2 ↓ 最终答案这就是Prompt Chaining 链式提示 / 提示链Datawhale 本章将 Prompt Chaining 定义为把复杂任务拆成多个简单 Prompt并让不同步骤分别承担明确的子任务本章的示例围绕“识别商品 → 查询商品数据库 → 生成客服回答”展开。二、什么是 Prompt ChainingPrompt Chaining 可以简单理解成把一个复杂任务拆成多个 Prompt让前一步的结果成为后一步的输入。例如我们想建立一个电商客服。用户问请告诉我 SmartX ProPhone 和 FotoSnap Camera 的信息 另外介绍一下你们的电视。我们当然可以直接写一个超级大的 Prompt请理解用户的问题 找到对应商品 判断所属类别 查询商品参数 比较商品信息 最后回答用户……然后一次性让 LLM 完成。但 Prompt Chaining 会把它拆成用户问题 │ ↓ Prompt 1 提取用户提到的商品和类别 │ ↓ [ Smartphone, Camera, Television ] │ ↓ Python 查询产品数据库 │ ↓ 获得详细产品信息 │ ↓ Prompt 2 根据查询结果回答用户 │ ↓ 最终客服回答所以最简单的公式就是Prompt Chaining Prompt₁ → Result₁ → Prompt₂ → Result₂ → Prompt₃ ...现代 LLM 工作流中也经常使用这种固定的顺序式结构一个 LLM 调用处理前一步的输出再交给下一步处理。它特别适合能够清晰拆成固定子任务的问题。三、为什么不直接用一个超级大的 PromptDatawhale 本章总结了几个原因。3.1 每个 Prompt 可以只负责一件事例如Prompt 1 只负责识别商品而Prompt 2 只负责组织客服回答每一步任务都比较明确。如果一个 Prompt 同时要求识别 检索 判断 计算 翻译 生成 审核整个任务会复杂很多。3.2 更容易调试假设最终答案错了。如果只有用户 ↓ 超级 Prompt ↓ 错误答案我们很难判断到底哪里错了但是 Prompt ChainingStep 1 商品识别结果 Step 2 数据库查询结果 Step 3 最终生成结果我们可以逐步检查商品名字识别错了 ↓ 还是数据库查错了 ↓ 还是最终回答生成错了因此系统的可观察性 可调试性会更好。Anthropic 在当前的 Agent/Workflow 工程实践中也将 Prompt Chaining 作为一种基础 workflow并指出中间步骤还可以增加程序化检查来判断流程是否仍然正常。四、Prompt Chaining 和上一章 CoT 有什么区别这是这两章最容易混淆的地方。可以先看这个表对比Chain of ThoughtPrompt Chaining中文思维链链式提示核心一个任务内部进行多步推理把任务拆成多个独立步骤LLM 调用通常可以一次通常多次中间步骤主要存在于推理过程可以保存为程序变量能否调用数据库通常不是重点可以能否调用 API通常不是重点可以能否插入 Python不方便非常方便调试相对困难可以逐步调试更像什么思考过程工作流水线可以粗略记成Chain of Thought一次 LLM 调用 ┌──────────────────────────┐ │ Step 1 │ │ ↓ │ │ Step 2 │ │ ↓ │ │ Step 3 │ │ ↓ │ │ Answer │ └──────────────────────────┘Prompt ChainingLLM 1 ↓ Python ↓ Database ↓ LLM 2 ↓ API ↓ LLM 3所以我更建议这样记CoT 是“一个模型怎么分步骤想”。Prompt Chaining 是“整个程序怎么分步骤做”。对于任务路径比较固定的问题Prompt Chaining 属于典型的 Workflow如果后续步骤改为由模型动态决定则会逐渐接近 Agent。五、本章最终要实现什么系统这一章构建的是一个电子产品客服系统完整过程如下用户问题 │ ↓ ┌────────────────────┐ │ Prompt 1信息抽取 │ └────────────────────┘ │ ↓ 商品 商品类别 │ ↓ Python 解析 LLM 输出 │ ↓ ┌────────────────────┐ │ 商品数据库 JSON │ └────────────────────┘ │ ↓ 查询商品信息 │ ↓ Relevant Information │ ↓ ┌────────────────────┐ │ Prompt 2生成回答 │ └────────────────────┘ │ ↓ 最终答案Datawhale 这一章实际就是依次实现“提取产品和类别 → 读取products_zh.json→ 根据名称或类别查询 → 将查询结果提供给模型 → 生成最终答案”。六、第一步从用户问题中提取商品和类别首先from tool import get_completion_from_messages delimiter ####这里我们之前已经学习过get_completion_from_messages是作者自己封装的 LLM 调用函数。七、System Prompt 在做什么这一章首先要求模型从用户输入中找出 ① 产品类别 ② 具体产品例如规定Computers and Laptops Smartphones and Accessories Televisions and Home Theater Systems Gaming Consoles and Accessories Audio Equipment Cameras and Camcorders同时把允许识别的商品名称也提前提供给模型。最重要的规则是只能从允许的类别和产品中选择。而不是让 LLM 自己创造商品。Datawhale 的 Prompt 同时要求模型只返回一个可以解析的列表每个元素包含category和products两个字段如果没有找到匹配内容则返回空列表。八、为什么要限制模型只能输出固定商品比如用户说介绍一下 SmartX ProPhone。模型可以稳定返回[ { category: Smartphones and Accessories, products: [SmartX ProPhone] } ]而不是随便输出这个用户想了解一款手机。但后面的Python 程序需要读取结果所以最好得到固定字段 固定格式 固定类别也就是Structured Output 结构化输出九、第一个例子教程中用户输入类似user_message_1 请告诉我关于 smartx pro phone 和 fotosnap camera 的信息。 另外请告诉我关于你们 TVs 的情况。 然后构造messages [ { role: system, content: system_message }, { role: user, content: f{delimiter}{user_message_1}{delimiter} } ]发送给模型。最后模型返回类似[ { category: Smartphones and Accessories, products: [SmartX ProPhone] }, { category: Cameras and Camcorders, products: [ FotoSnap DSLR Camera, FotoSnap Mirrorless Camera, FotoSnap Instant Camera ] }, { category: Televisions and Home Theater Systems, products: [ CineView 4K TV, CineView 8K TV, CineView OLED TV, SoundMax Home Theater, SoundMax Soundbar ] } ]这里模型暂时没有回答用户问题。它只完成识别用户提到了什么这是 Prompt Chaining 特别重要的思想每一步只完成自己的任务不急着生成最终答案。该结果正是课程示例第一阶段的输出。十、为什么用户只说 FotoSnap Camera却返回三个 Camera因为用户没有给出完整具体型号而系统知道 FotoSnap 品牌下存在FotoSnap DSLR Camera FotoSnap Mirrorless Camera FotoSnap Instant Camera所以第一阶段把这些候选产品都提取了出来。这进一步说明LLM 第一步 不是在生成答案而是在做Query Understanding 查询理解十一、第二个案例没有匹配商品怎么办教程又给出user_message_2 我的路由器不工作了 但是商品列表里没有 Router于是模型返回[]这是 Python 的空列表 empty list它告诉后面的程序没有找到对应产品这是一个非常好的设计思想不确定时不要编一个产品而是返回明确的“无结果”。教程明确规定未找到商品或类别时输出空列表并用路由器问题演示了[]的结果。十二、第二步读取产品数据库产品数量比较多所以教程没有把所有详细信息直接塞进 System Prompt而是放进products_zh.json然后import json with open(products_zh.json, r) as file: products json.load(file)这一段非常值得理解。十三、import json是什么import json导入 Python 自带的json模块。主要用于处理JSON 数据例如{ name: TechPro, price: 799.99 }Python 可以通过json模块读取这种数据。十四、with open(...)是什么with open(products_zh.json, r) as file:意思是打开products_zh.json文件并把这个文件暂时叫作file。其中r表示read 读取模式因此open(products_zh.json, r)就是以读取方式打开这个 JSON 文件。十五、json.load(file)是什么products json.load(file)意思是从 JSON 文件中读取数据并转换成 Python 对象。假设文件里{ TechPro Ultrabook: { 价格: 799.99, 评分: 4.5 } }经过json.load(file)以后products就会成为 Python 中类似{ TechPro Ultrabook: { 价格: 799.99, 评分: 4.5 } }这样的dictionary 字典所以JSON 文件 ↓ json.load() ↓ Python 字典课程正是通过这种方式读取products_zh.json中的商品信息。十六、第三步根据产品名称查询教程定义def get_product_by_name(name): return products.get(name, None)这个函数非常简单。例如get_product_by_name(TechPro Ultrabook)就相当于products.get(TechPro Ultrabook, None)十七、.get(name, None)是什么意思假设products { 电脑: 5000, 手机: 3000 }执行products.get(电脑, None)得到5000因为电脑这个 key 存在。但如果products.get(电视, None)因为不存在电视就返回None所以dictionary.get(key, 默认值)可以理解成找得到就返回对应数据找不到就返回默认值。这一章默认值设置成None也就是没有找到Datawhale 使用的get_product_by_name()正是通过products.get(name, None)完成精确产品名查找。十八、第四步根据类别查询商品接下来还有def get_products_by_category(category): return [ product for product in products.values() if product[类别] category ]它叫List Comprehension 列表推导式十九、先把列表推导式展开这段[ product for product in products.values() if product[类别] category ]实际上相当于result [] for product in products.values(): if product[类别] category: result.append(product) return result逻辑就是遍历所有产品 ↓ 检查产品类别 ↓ 是不是我要的类别 ↓ 是 ↓ 加入结果列表所以一句话列表推导式就是把简单的for if append()压缩成一行。二十、products.values()是什么假设products { A: { 价格: 100 }, B: { 价格: 200 } }那么products.keys()得到的是A B而products.values()得到{价格: 100} {价格: 200}也就是字典所有 value。在本章中products.values()就是所有商品的详细信息。因此for product in products.values()表示把产品数据库里的商品一个一个拿出来检查。二十一、if product[类别] category是什么意思例如product { 名称: TechPro Ultrabook, 类别: 电脑和笔记本 }那么product[类别]得到电脑和笔记本如果category 电脑和笔记本那么product[类别] category就是电脑和笔记本 电脑和笔记本结果True于是这个产品就会被加入结果列表。二十二、到目前为止发生了什么现在我们的系统已经完成用户 给我介绍手机 ↓ Prompt 1 ↓ 识别 Smartphones and Accessories ↓ Python ↓ get_products_by_category() ↓ JSON 商品数据库 ↓ 获取所有手机产品注意这里非常重要LLM 负责理解自然语言Python 负责精确查询数据。而不是让 LLM凭记忆猜产品信息二十三、第五步把 LLM 输出的字符串转换成 Python List第一步模型返回的是类似[{category:Smartphones..., ...}]注意它看起来像 Python List但实际上首先是一个字符串。也就是type(category_and_product_response_1)很可能是str而不是list所以我们还不能直接for item in response:按照列表元素处理。因此教程定义def read_string_to_list(input_string):将字符串转换为真正的 Python List。二十四、逐行理解read_string_to_list()代码大致如下def read_string_to_list(input_string): if input_string is None: return None try: input_string input_string.replace( , \ ) data json.loads(input_string) return data except json.JSONDecodeError: print(Error: Invalid JSON string) return None我们一步一步看。二十五、if input_string is Noneif input_string is None: return None意思就是如果根本没有输入就不继续执行了。例如input_string None那么函数直接return None这属于一种输入检查二十六、为什么要.replace(, \)模型可能返回[ { category: Computers } ]这里使用单引号 但是严格 JSON 要求字符串使用双引号 应该写成[ { category: Computers } ]所以input_string.replace(, \)就是把 替换成 二十七、json.loads()和刚才的json.load()有什么区别这是特别容易搞混的两个函数。json.load()json.load(file)读取文件例如products.jsonjson.loads()多一个s这里的s可以帮助记忆string它处理的是字符串例如text { name: Tom } data json.loads(text)结果data { name: Tom }所以函数输入json.load()文件json.loads()字符串这个区别非常值得记。二十八、为什么这里有try / except因为json.loads()要求字符串格式必须正确。如果模型突然输出这是我的结果 [ ... ]里面多了一些乱七八糟的文本JSON 解析可能失败。于是try:表示尝试正常解析。如果解析失败except json.JSONDecodeError:就执行print(Error: Invalid JSON string) return None因此try ↓ 正常路线 except ↓ 出错时备用路线这也是 LLM 应用中非常重要的工程思想不要假设模型每一次输出格式都 100% 正确。二十九、第六步根据解析结果真正查询数据库接下来教程定义def generate_output_string(data_list):这个函数的作用可以直接翻译成根据模型找到的产品名和类别去真正的产品数据库中把详细信息拿出来。整体逻辑模型输出 [ 产品 A, 类别 B ] ↓ generate_output_string() ↓ 查询数据库 ↓ 产品详细信息课程中的该函数会优先处理products字段如果没有具体产品而只有category则查询该类别下的商品。三十、output_string 函数一开始output_string 创建了一个空字符串后面查询出商品以后不断output_string ...也就是不断往字符串后面追加产品信息。三十一、for data in data_listfor data in data_list:表示把模型返回的分类结果一个一个取出来。例如data_list [ { category: Phone, products: [A] }, { category: Camera, products: [B, C] } ]第一次data是{ category: Phone, products: [A] }第二次则是 Camera。三十二、判断有没有具体产品if products in data and data[products]:这句话有两个条件。条件 1products in data检查字典有没有products这个 key。条件 2data[products]检查产品列表是不是非空。两个都满足以后开始按照产品名称查询。三十三、继续遍历具体产品products_list data[products] for product_name in products_list: product get_product_by_name(product_name)例如products_list [ FotoSnap DSLR Camera, FotoSnap Mirrorless Camera ]那么程序依次执行get_product_by_name( FotoSnap DSLR Camera )然后get_product_by_name( FotoSnap Mirrorless Camera )最后从数据库获得两个产品的详细信息。三十四、json.dumps()又是什么查询出来一个 Python 字典product { 名称: SmartX ProPhone, 价格: 899.99 }教程执行json.dumps( product, indent4, ensure_asciiFalse )注意这里又出现一个dumpsjson.loads()vsjson.dumps()可以这样记loads JSON 字符串 ↓ Python 对象而dumps Python 对象 ↓ JSON 字符串也就是JSON String │ │ json.loads() ↓ Python dict │ │ json.dumps() ↓ JSON String三十五、indent4是什么indent4表示JSON 输出的时候缩进 4 个空格。没有indent4可能是{name:A,price:100,rating:4.5}加入后{ name: A, price: 100, rating: 4.5 }主要是更容易阅读三十六、ensure_asciiFalse是什么如果不设置ensure_asciiFalse中文字符有时可能表示成\u4ea7\u54c1设置ensure_asciiFalse之后就能够比较自然地保留产品这样的中文字符。所以json.dumps( product, indent4, ensure_asciiFalse )可以简单理解把 Python 字典转换成比较漂亮、正常显示中文的 JSON 字符串。三十七、如果没有具体产品只有类别呢代码还有elif category in data:例如用户问你们有哪些电视没有指定CineView 4K TV只是说电视那么系统就执行get_products_by_category(category_name)把Televisions and Home Theater Systems下面的产品全部查询出来。所以整个逻辑是有具体产品 │ ├── Yes │ ↓ │ 按产品名称查 │ └── No ↓ 有类别 ↓ 按类别查三十八、为什么这里大量使用 Python而不是全部交给 LLM例如查价格SmartX ProPhone我们可以问 LLM它多少钱但 LLM 可能记错 过时 幻觉 猜一个数字更加可靠的方法是LLM 负责理解 用户想找 SmartX ProPhone ↓ Python 负责 查数据库 ↓ 数据库 返回 $899.99 ↓ LLM 负责 把这个信息组织成自然语言也就是说LLM 擅长 自然语言理解 信息抽取 生成文本 Python 擅长 精确逻辑 循环 条件判断 数据操作 数据库擅长 保存真实信息 精确查询Prompt Chaining 的一个核心价值就是不用逼着 LLM 做所有事情而是让不同组件做自己最擅长的工作。Datawhale 在本章后半部分也特别强调提示链可以结合外部工具和信息检索机制而不是一次性把所有资料交给模型。三十九、第七步把查询到的信息交给第二个 Prompt现在我们已经有product_information_for_user_message_1里面包含用户真正相关的商品详细信息接着messages [ { role: system, content: system_message }, { role: user, content: user_message_1 }, { role: assistant, content: f 相关产品信息: {product_information_for_user_message_1} } ]然后final_response get_completion_from_messages(messages)此时才真正生成用户看到的最终回答。教程就是通过把检索到的相关产品信息再次放入对话上下文然后调用模型生成最终客服回复。四十、这里为什么又调用了一次 LLM第一次get_completion_from_messages()任务是识别商品和类别第二次get_completion_from_messages()任务是根据真实商品信息回答用户所以LLM Call 1 ↓ Information Extraction 信息抽取和LLM Call 2 ↓ Response Generation 回答生成完全是两个不同任务。这就是Prompt Chaining四十一、把整个代码流程串起来这一章最重要的流程是用户自然语言 │ ↓ ┌────────────────┐ │ Prompt 1 │ │ 产品 / 类别抽取 │ └────────────────┘ │ ↓ 字符串形式列表 │ ↓ read_string_to_list() │ ↓ Python List │ ↓ generate_output_string() │ ↓ products_zh.json │ ↓ 商品真实信息 │ ↓ ┌────────────────┐ │ Prompt 2 │ │ 生成最终回答 │ └────────────────┘ │ ↓ 用户回答四十二、数据类型是怎么变化的这一点也很值得记。一开始用户输入 ↓ str第一次 LLM 返回[{category: ...}] ↓ str经过read_string_to_list()变成list列表里面dict然后查询products ↓ dict再通过json.dumps()变成str最后传给Prompt 2因此str ↓ LLM ↓ str ↓ json.loads() ↓ list / dict ↓ 数据库 ↓ dict ↓ json.dumps() ↓ str ↓ LLM ↓ 最终 str四十三、为什么不把整个产品数据库直接塞给 LLM假设数据库只有20 个产品可能问题还不大。但如果10000 个产品难道每次用户问SmartX ProPhone 多少钱都把10000 个产品全部放进 Prompt显然不合理。更加合理的是用户问 SmartX ↓ 识别 SmartX ↓ 只检索 SmartX ↓ 把 SmartX 信息放入 Context而不是整个数据库 ↓ 全部放入 ContextDatawhale 本章总结中特别强调“动态、按需提供信息”而不是把所有可能相关的信息一次性加载进模型上下文。现代 Agent 的 Context Engineering 也采用非常类似的思想保留轻量索引或引用需要时再动态加载真正相关的数据以减少无关上下文造成的信息污染。四十四、这是不是已经有一点像 RAG 了现在这个流程用户问题 ↓ 识别相关商品 ↓ 数据库检索 ↓ 把检索结果给 LLM ↓ 生成答案其实已经非常接近Retrieval-Augmented Generation RAG 检索增强生成区别在于教程这里采用精确商品名称 / 类别查询JSON 数据库而更加典型的 RAG 往往会使用Embedding Vector Database Semantic Search进行语义检索。Datawhale 本章结尾也进一步提到了 Embedding与精确名称匹配相比Embedding 可以支持更加模糊的语义检索。四十五、为什么 Embedding 更灵活现在的函数get_product_by_name( SmartX ProPhone )要求名称比较准确。如果用户说我想买一个拍照比较好的手机这里没有直接出现SmartX ProPhone传统精确匹配就比较困难。Embedding 可以把“拍照好的手机”转换成向量然后寻找语义上相似的信息。于是可能找到SmartX ProPhone 特色 12MP dual camera所以可以理解成精确查询 名字基本要对上而Embedding 检索 意思接近也可能找到本章用 Embedding 作为更高级的信息检索方向进行了介绍。四十六、Prompt Chaining 一定更省 Token 吗这里要对原教程的表述稍微补充一下。Prompt Chaining 的确可以做到每一步只加载当前需要的信息从而减少大量无关 Context但是Prompt Chaining 并不意味着总 Token 成本一定更低。因为调用一次 LLM变成调用两次 三次 甚至更多次可能会增加因此真正的权衡是更简单、更明确的子任务 更好的可控性 更方便调试 vs. 更多模型调用 更高延迟 可能更高总成本现代 Workflow 工程实践通常把 Prompt Chaining 看作一种“用额外延迟换取更高任务准确性和可控性”的模式而不是简单理解为一定更省钱。四十七、Prompt Chaining 和 Agent 的区别这里也非常重要。本章的流程基本是开发者提前写死的Step 1 商品识别 ↓ 必须执行 Step 2 数据库查询 ↓ 必须执行 Step 3 生成回答所以它属于Workflow程序知道下一步是什么Agent 则更像用户问题 ↓ LLM 判断 ↓ 我现在需要查询数据库吗 │ ├─ Yes → 查询数据库 │ └─ No ↓ 还需要搜索吗 │ ├─ Yes → 搜索 │ └─ No ↓ 是否已经完成也就是说Prompt Chaining / Workflow 开发者 决定流程而Agent LLM 根据任务动态决定流程现在的 Agent 工程定义通常也会区分这两者Workflow 使用预定义代码路径组织 LLM 和工具而 Agent 则由模型动态决定自己的执行过程和工具使用。四十九、把这一章思想应用到 Datasheet如果以后做 Datasheet 参数提取其实可以完全照这个结构设计。例如用户帮我从 Datasheet 中生成 Input Leakage Current 测试项。Prompt 1任务识别识别参数 Parameter: Input Leakage Current Category: DC Characteristics然后↓Retrieval从 Datasheet / RAG 知识库中查询Input Leakage Current得到Symbol: IIH / IIL VIN: 0 V ~ VDD Max: ±1 μA TA: 25°C然后↓Python把字段解析为{ parameter: Input Leakage Current, condition: VIN 0V to VDD, max: ±1, unit: μA }然后↓Prompt 2要求根据提供的真实 Datasheet 参数 生成测试项。 不得补充不存在的信息。最后↓Output{ Test_Item: Input Leakage Current, Condition: VIN 0V to VDD, Limit: ±1 μA }所以整个系统Datasheet / 用户请求 ↓ 意图 / 参数识别 ↓ 知识库检索 ↓ Python 结构化处理 ↓ 参数验证 ↓ LLM 生成 Test Item本质上就是一个Prompt Chain而不是把整本 Datasheet 所有规则 所有任务 一次性塞进一个 Prompt五十、本章代码中最应该掌握的 Python这一章出现了很多 Python 知识。建议重点记住json.load(file)JSON 文件 → Python 对象json.loads(string)JSON 字符串 → Python 对象json.dumps(obj)Python 对象 → JSON 字符串dictionary.get(key, None)查字典找不到返回Nonefor x in list:遍历列表[ x for x in data if condition ]列表推导式try: ... except: ...正常执行 异常处理if products in data:判断字典中有没有某一个 key
返回列表