💡 这节课会带给你

  1. 了解提示工程的跨时代意义,改「精确逻辑」习惯为「模糊执行」习惯
  2. 掌握提示工程的核心方法论,比 99% 的人达成更好效果
  3. 掌握提示调优的基本方法,了解它在实际生产中的应用
  4. 掌握防止Prompt注入的方法,AI更安全

开始上课!

1.什么是提示工程(Prompt Engineering)

提示工程也叫「指令工程」。

  • Prompt 就是你发给 ChatGPT 的指令,比如「讲个笑话」、「用 Python 编个贪吃蛇游戏」、「给男/女朋友写封情书」等
  • 貌似简单,但意义非凡
    • Prompt 是 AGI 时代的「编程语言」
    • Prompt 工程是 AGI 时代的「软件工程」
    • 提示工程师是 AGI 时代的「程序员」
  • 学会提示工程,就像学用鼠标、键盘一样,是 AGI 时代的基本技能
  • 专门的「提示工程师」不会长久,因为每个人都要会「提示工程」

思考:

如果人人都会,那我们的优势是什么?

1.1、我们在「提示工程」上的优势

我们懂「大模型只会基于概率生成下一个字」这个原理,所以知道:

  • 为什么有的指令有效,有的指令无效
  • 为什么同样的指令有时有效,有时无效
  • 怎么提升指令有效的概率

我们懂编程:

  • 知道哪些问题用提示工程解决更高效,哪些用传统编程更高效
  • 能完成和业务系统的对接,把效能发挥到极致

1.2、使用 Prompt 的两种目的

  1. 获得具体问题的具体结果,比如「我该学 Vue 还是 React?」「PHP 为什么是最好的语言?」
  2. 固化一套 Prompt 到程序中,成为系统功能的一部分,比如「每天生成本公司的简报」「AI 客服系统」「基于公司知识库的问答」

前者主要通过 ChatGPT、ChatALL 这样的界面操作。后者就要动代码了。我们会专注于后者,因为:

  1. 后者更难,掌握后能轻松搞定前者
  2. 后者是我们的独特优势

1.3、Prompt 调优

找到好的 prompt 是个持续迭代的过程,需要不断调优。

  • 从人的视角:说清楚自己到底想要什么
  • 从机器的视角:不是每个细节它都能猜到你的想法,它猜不到的,你需要详细说
  • 从模型的视角:不是每种说法它都能完美理解,需要尝试和技巧

二、Prompt 的构成

  • 指示: 对任务进行描述
  • 上下文: 给出与任务相关的其它背景信息(尤其在多轮交互中)
  • 例子: 必要时给出举例,学术中称为 one-shot learning, few-shot learning 或 in-context learning;实践证明其对输出正确性有帮助
  • 输入: 任务的输入信息;在提示词中明确的标识出输入
  • 输出: 输出的格式描述,以便后继模块自动解析模型的输出结果,比如(JSON、XML)

2.1、设定一个业务场景来讲解上述知识

业务场景:办理流量包的智能客服

流量包产品:

名称 流量(G/月) 价格(元/月) 适用人群
经济套餐 10 50 无限制
畅游套餐 100 180 无限制
无限套餐 1000 300 无限制
校园套餐 200 150 在校生

2.2、对话系统的基本模块(简介)

2.prompt - 图1

对话流程举例: 2.prompt - 图2

2.3、用Prompt实现上述模块功能

环境搭建

调试 prompt 的过程其实在图形界面里开始会更方便,但为了方便演示和大家上手体验,我们直接在代码里调试。

  1. # 加载环境变量
  2. import openai
  3. import os
  4. from dotenv import load_dotenv, find_dotenv
  5. _ = load_dotenv(find_dotenv()) # 读取本地 .env 文件,里面定义了 OPENAI_API_KEY
  6. openai.api_key = os.getenv('OPENAI_API_KEY')
  1. # 基于 prompt 生成文本
  2. def get_completion(prompt, model="gpt-3.5-turbo"):
  3. messages = [{"role": "user", "content": prompt}]
  4. response = openai.ChatCompletion.create(
  5. model=model,
  6. messages=messages,
  7. temperature=0, # 模型输出的随机性,0 表示随机性最小
  8. )
  9. return response.choices[0].message["content"]

2.3.1、实现一个NLU

任务描述+输入

  1. # 任务描述
  2. instruction = """
  3. 你的任务是识别用户对手机流量套餐产品的选择条件。
  4. 每种流量套餐产品包含三个属性:名称,月费价格,月流量。
  5. 根据用户输入,识别用户在上述三种属性上的倾向。
  6. """
  7. # 用户输入
  8. input_text = """
  9. 办个100G的套餐。
  10. """
  11. # 这是系统预置的 prompt。魔法咒语的秘密都在这里
  12. prompt = f"""
  13. {instruction}
  14. # 用户输入:
  15. {input_text}
  16. """
  17. response = get_completion(prompt)
  18. print(response)

用户在流量套餐产品的选择条件上的倾向为:

  • 名称:用户倾向选择100G的套餐。
  • 月费价格:用户未提及对月费价格的倾向。
  • 月流量:用户倾向选择100G的套餐。

约定输出格式

  1. # 输出描述
  2. output_format = """
  3. 以JSON格式输出
  4. """
  5. # 稍微调整下咒语
  6. prompt = f"""
  7. {instruction}
  8. {output_format}
  9. 用户输入:
  10. {input_text}
  11. """
  12. response = get_completion(prompt)
  13. print(response)

{
“名称”: “100G套餐”,
“月费价格”: “未知”,
“月流量”: “100G”
}

把描述定义的更精细

  1. instruction = """
  2. 你的任务是识别用户对手机流量套餐产品的选择条件。
  3. 每种流量套餐产品包含三个属性:名称(name),月费价格(price),月流量(data)。
  4. 根据用户输入,识别用户在上述三种属性上的倾向。
  5. """
  6. # 输出描述
  7. output_format = """
  8. 以JSON格式输出。
  9. 1. name字段的取值为string类型,取值必须为以下之一:经济套餐、畅游套餐、无限套餐、校园套餐 或 null;
  10. 2. price字段的取值为一个结构体 或 null,包含两个字段:
  11. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  12. (2) value, int类型
  13. 3. data字段的取值为取值为一个结构体 或 null,包含两个字段:
  14. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  15. (2) value, int类型或string类型,string类型只能是'无上限'
  16. 4. 用户的意图可以包含按price或data排序,以sort字段标识,取值为一个结构体:
  17. (1) 结构体中以"ordering"="descend"表示按降序排序,以"value"字段存储待排序的字段
  18. (2) 结构体中以"ordering"="ascend"表示按升序排序,以"value"字段存储待排序的字段
  19. 只输出中只包含用户提及的字段,不要猜测任何用户未直接提及的字段,不输出值为null的字段。
  20. """
  21. #input_text = "办个100G以上的套餐"
  22. #input_text = "我要无限量套餐"
  23. input_text = "有没有便宜的套餐"
  24. prompt = f"""
  25. {instruction}
  26. {output_format}
  27. 用户输入:
  28. {input_text}
  29. """
  30. response = get_completion(prompt)
  31. print(response)

{
“name”: “经济套餐”
}

加入例子:让输出更稳定

  1. examples = """
  2. 便宜的套餐:{"sort":{"ordering"="ascend","value"="price"}}
  3. 有没有不限流量的:{"data":{"operator":"==","value":"无上限"}}
  4. 流量大的:{"sort":{"ordering"="descend","value"="data"}}
  5. 100G以上流量的套餐最便宜的是哪个:{"sort":{"ordering"="ascend","value"="price"},"data":{"operator":">=","value":100}}
  6. 月费不超过200的:{"price":{"operator":"<=","value":200}}
  7. 就要月费180那个套餐:{"price":{"operator":"==","value":180}}
  8. 经济套餐:{"name":"经济套餐"}
  9. """
  10. #input_text = "办个200G的套餐"
  11. input_text = "有没有流量大的套餐"
  12. #input_text = "200元以下,流量大的套餐有啥"
  13. #input_text = "你说那个10G的套餐,叫啥名字"
  14. prompt = f"""
  15. {instruction}
  16. {output_format}
  17. 例如:
  18. {examples}
  19. 用户输入:
  20. {input_text}
  21. """
  22. response = get_completion(prompt)
  23. print(response)

{“sort”:{“ordering”:”descend”,”value”:”data”}}

改变习惯,优先用 Prompt 解决问题

用好prompt可以减轻预处理和后处理的工作量和复杂度。

划重点:一切问题先尝试用 prompt 解决,往往有四两拨千斤的效果

2.3.2、实现上下文DST

在Prompt中加入上下文

  1. instruction = """
  2. 你的任务是识别用户对手机流量套餐产品的选择条件。
  3. 每种流量套餐产品包含三个属性:名称(name),月费价格(price),月流量(data)。
  4. 根据对话上下文,识别用户在上述属性上的倾向。识别结果要包含整个对话的信息。
  5. """
  6. # 输出描述
  7. output_format = """
  8. 以JSON格式输出。
  9. 1. name字段的取值为string类型,取值必须为以下之一:经济套餐、畅游套餐、无限套餐、校园套餐 或 null;
  10. 2. price字段的取值为一个结构体 或 null,包含两个字段:
  11. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  12. (2) value, int类型
  13. 3. data字段的取值为取值为一个结构体 或 null,包含两个字段:
  14. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  15. (2) value, int类型或string类型,string类型只能是'无上限'
  16. 4. 用户的意图可以包含按price或data排序,以sort字段标识,取值为一个结构体:
  17. (1) 结构体中以"ordering"="descend"表示按降序排序,以"value"字段存储待排序的字段
  18. (2) 结构体中以"ordering"="ascend"表示按升序排序,以"value"字段存储待排序的字段
  19. 只输出中只包含用户提及的字段,不要猜测任何用户未直接提及的字段。不要输出值为null的字段。
  20. """
  21. #DO NOT OUTPUT NULL-VALUED FIELD!
  22. examples = """
  23. 客服:有什么可以帮您
  24. 用户:100G套餐有什么
  25. {"data":{"operator":">=","value":100}}
  26. 客服:有什么可以帮您
  27. 用户:100G套餐有什么
  28. 客服:我们现在有无限套餐,不限流量,月费300元
  29. 用户:太贵了,有200元以内的不
  30. {"data":{"operator":">=","value":100},"price":{"operator":"<=","value":200}}
  31. 客服:有什么可以帮您
  32. 用户:便宜的套餐有什么
  33. 客服:我们现在有经济套餐,每月50元,10G流量
  34. 用户:100G以上的有什么
  35. {"data":{"operator":">=","value":100},"sort":{"ordering"="ascend","value"="price"}}
  36. 客服:有什么可以帮您
  37. 用户:100G以上的套餐有什么
  38. 客服:我们现在有畅游套餐,流量100G,月费180元
  39. 用户:流量最多的呢
  40. {"sort":{"ordering"="descend","value"="data"},"data":{"operator":">=","value":100}}
  41. """
  42. input_text="哪个便宜"
  43. #input_text="无限量哪个多少钱"
  44. #input_text="流量最大的多少钱"
  45. context = f"""
  46. 客服:有什么可以帮您
  47. 用户:有什么100G以上的套餐推荐
  48. 客服:我们有畅游套餐和无限套餐,您有什么价格倾向吗
  49. 用户:{input_text}
  50. """
  51. prompt = f"""
  52. {instruction}
  53. {output_format}
  54. {examples}
  55. {context}
  56. """
  57. response = get_completion(prompt)
  58. print(response)

{“sort”:{“ordering”=”ascend”,”value”=”price”},”data”:
{“operator”:”>=”,”value”:100}}

(1)用Prompt实现DST不是唯一选择
  • 优点: 节省开发量
  • 缺点: 调优相对复杂,最好用动态例子(讲Embedding时再review这个点)
(2)也可以用Prompt实现NLU,用传统方法维护DST
  • 优点: DST环节可控性更高
  • 缺点: 需要结合业务know-how设计状态更新机制(解冲突)

2.3.3、实现NLG和对话策略

我们先把刚才的能力串起来,构建一个简单的客服机器人

  1. # 加载环境变量
  2. import openai
  3. import os, json, copy
  4. from dotenv import load_dotenv, find_dotenv
  5. _ = load_dotenv(find_dotenv()) # 读取本地 .env 文件,里面定义了 OPENAI_API_KEY
  6. openai.api_key = os.getenv('OPENAI_API_KEY')
  7. instruction = """
  8. 你的任务是识别用户对手机流量套餐产品的选择条件。
  9. 每种流量套餐产品包含三个属性:名称(name),月费价格(price),月流量(data)。
  10. 根据用户输入,识别用户在上述三种属性上的倾向。
  11. """
  12. # 输出描述
  13. output_format = """
  14. 以JSON格式输出。
  15. 1. name字段的取值为string类型,取值必须为以下之一:经济套餐、畅游套餐、无限套餐、校园套餐 或 null;
  16. 2. price字段的取值为一个结构体 或 null,包含两个字段:
  17. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  18. (2) value, int类型
  19. 3. data字段的取值为取值为一个结构体 或 null,包含两个字段:
  20. (1) operator, string类型,取值范围:'<='(小于等于), '>=' (大于等于), '=='(等于)
  21. (2) value, int类型或string类型,string类型只能是'无上限'
  22. 4. 用户的意图可以包含按price或data排序,以sort字段标识,取值为一个结构体:
  23. (1) 结构体中以"ordering"="descend"表示按降序排序,以"value"字段存储待排序的字段
  24. (2) 结构体中以"ordering"="ascend"表示按升序排序,以"value"字段存储待排序的字段
  25. 只输出中只包含用户提及的字段,不要猜测任何用户未直接提及的字段。
  26. DO NOT OUTPUT NULL-VALUED FIELD! 确保输出能被json.loads加载。
  27. """
  28. examples = """
  29. 便宜的套餐:{"sort":{"ordering"="ascend","value"="price"}}
  30. 有没有不限流量的:{"data":{"operator":"==","value":"无上限"}}
  31. 流量大的:{"sort":{"ordering"="descend","value"="data"}}
  32. 100G以上流量的套餐最便宜的是哪个:{"sort":{"ordering"="ascend","value"="price"},"data":{"operator":">=","value":100}}
  33. 月费不超过200的:{"price":{"operator":"<=","value":200}}
  34. 就要月费180那个套餐:{"price":{"operator":"==","value":180}}
  35. 经济套餐:{"name":"经济套餐"}
  36. """
  37. class NLU:
  38. def __init__(self):
  39. self.prompt_template = f"{instruction}\n\n{output_format}\n\n{examples}\n\n用户输入:\n__INPUT__"
  40. def _get_completion(self, prompt, model="gpt-3.5-turbo"):
  41. messages = [{"role": "user", "content": prompt}]
  42. response = openai.ChatCompletion.create(
  43. model=model,
  44. messages=messages,
  45. temperature=0, # 模型输出的随机性,0 表示随机性最小
  46. )
  47. semantics = json.loads(response.choices[0].message["content"])
  48. return { k:v for k,v in semantics.items() if v }
  49. def parse(self, user_input):
  50. prompt = self.prompt_template.replace("__INPUT__",user_input)
  51. return self._get_completion(prompt)
  52. class DST:
  53. def __init__(self):
  54. pass
  55. def update(self, state, nlu_semantics):
  56. if "name" in nlu_semantics:
  57. state.clear()
  58. if "sort" in nlu_semantics:
  59. slot = nlu_semantics["sort"]["value"]
  60. if slot in state and state[slot]["operator"] == "==":
  61. del state[slot]
  62. for k, v in nlu_semantics.items():
  63. state[k] = v
  64. return state
  65. class MockedDB:
  66. def __init__(self):
  67. self.data = [
  68. {"name":"经济套餐","price":50,"data":10,"requirement":None},
  69. {"name":"畅游套餐","price":180,"data":100,"requirement":None},
  70. {"name":"无限套餐","price":300,"data":1000,"requirement":None},
  71. {"name":"校园套餐","price":150,"data":200,"requirement":"在校生"},
  72. ]
  73. def retrieve(self, **kwargs):
  74. records = []
  75. for r in self.data:
  76. select = True
  77. if r["requirement"]:
  78. if "status" not in kwargs or kwargs["status"]!=r["requirement"]:
  79. continue
  80. for k, v in kwargs.items():
  81. if k == "sort":
  82. continue
  83. if k == "data" and v["value"] == "无上限":
  84. if r[k] != 1000:
  85. select = False
  86. break
  87. if "operator" in v:
  88. if not eval(str(r[k])+v["operator"]+str(v["value"])):
  89. select = False
  90. break
  91. elif str(r[k])!=str(v):
  92. select = False
  93. break
  94. if select:
  95. records.append(r)
  96. if len(records) <= 1:
  97. return records
  98. key = "price"
  99. reverse = False
  100. if "sort" in kwargs:
  101. key = kwargs["sort"]["value"]
  102. reverse = kwargs["sort"]["ordering"] == "descend"
  103. return sorted(records,key=lambda x: x[key] ,reverse=reverse)
  104. class DialogManager:
  105. def __init__(self, prompt_templates):
  106. self.state = {}
  107. self.session = [
  108. {
  109. "role": "system",
  110. "content": "你是一个手机流量套餐的客服代表,你叫小瓜。可以帮助用户选择最合适的流量套餐产品。"
  111. }
  112. ]
  113. self.nlu = NLU()
  114. self.dst = DST()
  115. self.db = MockedDB()
  116. self.prompt_templates = prompt_templates
  117. def _wrap(self,user_input,records):
  118. if records:
  119. prompt = self.prompt_templates["recommand"].replace("__INPUT__",user_input)
  120. r = records[0]
  121. for k,v in r.items():
  122. prompt = prompt.replace(f"__{k.upper()}__",str(v))
  123. else:
  124. prompt = self.prompt_templates["not_found"].replace("__INPUT__",user_input)
  125. for k,v in self.state.items():
  126. if "operator" in v:
  127. prompt = prompt.replace(f"__{k.upper()}__",v["operator"]+str(v["value"]))
  128. else:
  129. prompt = prompt.replace(f"__{k.upper()}__",str(v))
  130. return prompt
  131. def _call_chatgpt(self, prompt, model="gpt-3.5-turbo"):
  132. session = copy.deepcopy(self.session)
  133. session.append({"role": "user", "content": prompt})
  134. response = openai.ChatCompletion.create(
  135. model=model,
  136. messages=session,
  137. temperature=0,
  138. )
  139. return response.choices[0].message["content"]
  140. def run(self, user_input):
  141. #调用NLU获得语义解析
  142. semantics = self.nlu.parse(user_input)
  143. print("===semantics===")
  144. print(semantics)
  145. #调用DST更新多轮状态
  146. self.state = self.dst.update(self.state,semantics)
  147. print("===state===")
  148. print(self.state)
  149. #根据状态检索DB,获得满足条件的候选
  150. records = self.db.retrieve(**self.state)
  151. #拼装prompt调用chatgpt
  152. prompt_for_chatgpt = self._wrap(user_input, records)
  153. print("===gpt-prompt===")
  154. print(prompt_for_chatgpt)
  155. #调用chatgpt获得回复
  156. response = self._call_chatgpt(prompt_for_chatgpt)
  157. #将当前用户输入和系统回复维护入chatgpt的session
  158. self.session.append({"role": "user", "content": user_input})
  159. self.session.append({"role": "assistant", "content": response})
  160. return response

将垂直知识加入prompt,以使其准确回答

  1. prompt_templates = {
  2. "recommand" : "用户说:__INPUT__ \n\n向用户介绍如下产品:__NAME__,月费__PRICE__元,每月流量__DATA__G。",
  3. "not_found" : "用户说:__INPUT__ \n\n没有找到满足__PRICE__元价位__DATA__G流量的产品,询问用户是否有其他选择倾向。"
  4. }
  5. dm = DialogManager(prompt_templates)
  1. response = dm.run("流量大的")
  2. #response = dm.run("300太贵了,200元以内有吗")
  3. print("===response===")
  4. print(response)

===semantics=== {‘price’: {‘operator’: ‘<=’, ‘value’: 200}} ===state=== {‘sort’: {‘ordering’: ‘descend’, ‘value’: ‘data’}, ‘price’: {‘operator’: ‘<=’, ‘value’: 200}} ===gpt-prompt=== 用户说:300太贵了,200元以内有吗

向用户介绍如下产品:畅游套餐,月费180元,每月流量100G。 ===response=== 小瓜:非常抱歉,我们的无限套餐可能确实有些贵了。如果您的预算在200元以内,我可以向您推荐我们的畅游套餐。这个套餐每月只需支付180元,您将享受到每月100G的流量。虽然相比无限套餐流量稍少一些,但对于一般的上网需求来说已经足够了。如果您对这个套餐感兴趣,我可以帮您办理。还有其他的套餐选项,您有其他的需求吗?

增加约束:改变语气、口吻

  1. ext = "很口语,亲切一些。不用说“抱歉”。直接给出回答,不用在前面加“小瓜说:”。NO COMMENTS. NO ACKNOWLEDGEMENTS."
  2. prompt_templates = { k : v+ext for k, v in prompt_templates.items() }
  3. dm = DialogManager(prompt_templates)
  1. #response = dm.run("流量大的")
  2. response = dm.run("300太贵了,200元以内有吗")
  3. print("===response===")
  4. print(response)

===semantics===
{‘price’: {‘operator’: ‘<=’, ‘value’: 200}}
===state===
{‘sort’: {‘ordering’: ‘descend’, ‘value’: ‘data’}, ‘price’: {‘operator’: ‘<=’, ‘value’: 200}}
===gpt-prompt===
用户说:300太贵了,200元以内有吗

向用户介绍如下产品:畅游套餐,月费180元,每月流量100G。很口语,亲切一些。不用说“抱歉”。直接给出回答,不用在前面加“小瓜说:”。NO COMMENTS. NO ACKNOWLEDGEMENTS.
===response===
畅游套餐是您的不错选择!它每月只需180元,您将享受到100G的流量。这样您可以畅快地上网、观看视频和使用各种应用,而不用担心流量不够用。如果您对这个套餐感兴趣或有其他问题,随时告诉我哦!

以例子的形式实现对话策略

  1. ext = "\n\n遇到类似问题,请参照以下回答:\n你们流量包太贵了\n亲,我们都是全省统一价哦。"
  2. prompt_templates = { k : v+ext for k, v in prompt_templates.items() }
  3. dm = DialogManager(prompt_templates)
  1. response = dm.run("这流量包太贵了")
  2. print("===response===")
  3. print(response)

===semantics===
{‘sort’: {‘ordering’: ‘ascend’, ‘value’: ‘price’}}
===state===
{‘sort’: {‘ordering’: ‘ascend’, ‘value’: ‘price’}}
===gpt-prompt===
用户说:这流量包太贵了

向用户介绍如下产品:经济套餐,月费50元,每月流量10G。很口语,亲切一些。不用说“抱歉”。直接给出回答,不用在前面加“小瓜说:”。NO COMMENTS. NO ACKNOWLEDGEMENTS.

遇到类似问题,请参照以下回答:
你们流量包太贵了
亲,我们都是全省统一价哦。 ===response===
我们了解您的担忧,但是我们的流量套餐价格都是全省统一的哦。不过,如果您觉得当前的流量包价格有些高,我们还有一个经济套餐,每月只需支付50元,就可以享受10G的流量。这个套餐性价比非常高,您可以考虑一下哦。如果您有其他需求或者疑问,随时告诉我,我会尽力帮助您的。

划重点:
  1. 这里的例子可以动态添加
  2. 具体方法我们放在LangChain课程中结合Embedding一起讲解
作业:
      尝试自己通过NLU和Policy实现给在校生推荐“校园套餐”