第二章 编写提示词原则
一、环境配置
开始学习前,我们用最简单的方案使用LLM(这里我们使用ChatGPT最为我们调用的LLM)。所以我们使用 Java版的SDK(第三方库)
你也可以参考 官方文档 ,看看都有哪些内容。
自己的项目中引入SDK
<dependency>
<groupId>com.theokanning.openai-gpt3-java</groupId>
<artifactId>service</artifactId>
<version>0.14.0</version>
</dependency>
最简单的情况下,我们只要调用方法(调用了GPT接口 )即可完成很惊人的"文本生成"功能。
java
//封装一个 GPT接口的函数,作为使用prompt作为参数,返回结果为GPT返回的内容
public String getCompletion(String prompt) {
String apiKey = this.apiKey;
log.info("apiKey: {}", apiKey);
OpenAiService openAiService = new OpenAiService(apiKey, Duration.ofSeconds(10));
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest();
chatCompletionRequest.setModel("gpt-3.5-turbo");
ChatMessage chatMessage = new ChatMessage();
chatMessage.setRole("user");
chatMessage.setContent(prompt);
chatCompletionRequest.setMessages(Arrays.asList(chatMessage));
ChatCompletionResult result = openAiService.createChatCompletion(chatCompletionRequest);
ChatMessage chatMessage1 = result.getChoices().get(0).getMessage();
log.info("message: {}", chatMessage1.getContent());
}
二、两个基本原则
原则一:编写清晰,具体的指令
您应该通过提供尽可能清晰和具体的指令来表达您希望模型执行的操作。这将引导模型给出正确地输出,并降低您得到无关或不正确响应的可能性。
清晰的指令不意味着必须简短,在许多情况下,更长的 Prompt 实际上更清晰,且提供了更多上下文,也就可能产生更详细更相关的输出。
使用分隔符清晰地表示输入的不同部分
分隔符可以是:```,""
,<>
,:
,<tag> </tag>
等。
你可以使用任何明显的标点符号将特定的文本部分与 Prompt 的其余部分分开。
标记的形式不限,只需要让模型明确知道这是一个单独部分。使用分隔符可以有效避免提示词注入( Prompt injection )。
提示词注入是指如果允许用户将某些输入添加到(开发者预定义的) Prompt 中,则所提供的指令可能会与开发者想要执行的操作相冲突,从而使 LLM 遵循用户输入的指令,而非执行开发者预期的操作。
即,输入里面可能包含其他指令,会覆盖掉您的指令。
在以下的例子中,我们给出一段话并要求 GPT 进行总结,在该示例中我们使用 ```来作为分隔符。
java
String text = "您应该提供尽可能清晰、具体的指示,以表达您希望模型执行的任务。\n" +
"这将引导模型朝向所需的输出,并降低收到无关或不正确响应的可能性。\n" +
"不要将写清晰的提示词与写简短的提示词混淆。\n" +
"在许多情况下,更长的提示词可以为模型提供更多的清晰度和上下文信息,从而导致更详细和相关的输出";
String prompt = "把用三个反引号括起来的文本总结成一句话。" +
"```" + text + "```";
String message = this.getCompletion(prompt);
输出:提供清晰、具体的指示,以指导模型执行任务,并避免混淆清晰提示词和简短提示词。在许多情况下,使用更长的提示词可以提供更多清晰度和上下文信息,以获得更详细和相关的输出。
寻求结构化的输出
输出可以是 Json、HTML 等格式。
第二个策略是要求生成一个结构化的输出,这可以使模型的输出更容易被我们解析,例如,您可以在 Python 中将其读入字典或列表中。
在以下示例中,我们要求 GPT 生成三本书的标题、作者和类别,并要求 GPT 以 Json 的格式返回给我们,为便于解析,我们指定了 Json 的键。
java
String prompt = "请生成包括书名、作者和类别的三本虚构书籍清单,并以 JSON 格式提供,其中包含以下键:book_id、title、author、genre。";
String message = this.getCompletion(prompt);
输出:
json
[
{
"book_id": 1,
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"genre": "Classics"
},
{
"book_id": 2,
"title": "1984",
"author": "George Orwell",
"genre": "Dystopian"
},
{
"book_id": 3,
"title": "Harry Potter and the Sorcerer's Stone",
"author": "J.K. Rowling",
"genre": "Fantasy"
}
]
要求模型检查是否满足条件
如果任务包含不一定能满足的假设(条件),我们可以告诉模型先检查这些假设,如果不满足,则会指出并停止执行后续的完整流程。
您还可以考虑可能出现的边缘情况及模型的应对,以避免意外的结果或错误发生。
在如下示例中,我们将分别给模型两段文本,分别是制作茶的步骤以及一段没有明确步骤的文本。我们将要求模型判断其是否包含一系列指令,如果包含则按照给定格式重新编写指令,不包含则回答"未提供步骤"。
java
String test = "泡一杯茶很容易。首先,需要把水烧开。\n" +
"在等待期间,拿一个杯子并把茶包放进去。\n" +
"一旦水足够热,就把它倒在茶包上。\n" +
"等待一会儿,让茶叶浸泡。几分钟后,取出茶包。\n" +
"如果您愿意,可以加一些糖或牛奶调味。\n" +
"就这样,您可以享受一杯美味的茶了。";
String prompt = "您将获得由三个引号括起来的文本。\n" +
"如果它包含一系列的指令,则需要按照以下格式重新编写这些指令:\n" +
"\n" +
"第一步 - ...\n" +
"第二步 - ...\n" +
"...\n" +
"第N步 - ...\n" +
"\n" +
"如果文本中不包含一系列的指令,则直接写"未提供步骤"。\n" +
"\n" +
"```" + test + "```";
String message = this.getCompletion(prompt);
log.info("\nguidelines4-1:\n{}", message);
guidelines4-1:
第一步 - 把水烧开。
第二步 - 拿一个杯子并把茶包放进去。
第三步 - 把烧开的水倒在茶包上。
第四步 - 等待几分钟,让茶叶浸泡。
第五步 - 取出茶包。
第六步 - 根据个人口味加入糖或牛奶。
第七步 - 享受一杯美味的茶。
java
//不满足条件,内容中没有提供步骤信息
String test2 = "今天阳光明媚,鸟儿在歌唱。\n" +
"这是一个去公园散步的美好日子。\n" +
"鲜花盛开,树枝在微风中轻轻摇曳。\n" +
"人们外出享受着这美好的天气,有些人在野餐,有些人在玩游戏或者在草地上放松。\n" +
"这是一个完美的日子,可以在户外度过并欣赏大自然的美景";
String prompt2 = "您将获得由三个引号括起来的文本。\n" +
"如果它包含一系列的指令,则需要按照以下格式重新编写这些指令:\n" +
"\n" +
"第一步 - ...\n" +
"第二步 - ...\n" +
"...\n" +
"第N步 - ...\n" +
"\n" +
"如果文本中不包含一系列的指令,则直接写"未提供步骤"。\n" +
"\n" +
"```" + test2 + "```";
String message2 = this.getCompletion(prompt2);
log.info("\nguidelines4-2:\n{}", message2);
guidelines4-2:
未提供步骤
提供少量示例 (少样本提示词,Few-shot prompting)
即在要求模型执行实际任务之前,提供给它少量成功执行任务的示例。
例如,在以下的示例中,我们告诉模型其任务是以一致的风格回答问题,并先给它一个孩子和祖父之间的对话的例子。孩子说,"请教我何为耐心",祖父用下述风格的隐喻来回答。由于我们已经告诉模型要以一致的语气回答,因此现在我们问"请教我何为韧性",由于模型已经有了这个少样本示例( few-shot example ),它将以类似的语气回答下一个任务。
java
String prompt = "您的任务是以一致的风格回答问题。\n" +
"\n" +
"<孩子>: 教我耐心。\n" +
"\n" +
"<祖父母>: 挖出最深峡谷的河流源于一处不起眼的泉眼;最宏伟的交响乐从单一的音符开始;最复杂的挂毯以一根孤独的线开始编织。\n" +
"\n" +
"<孩子>: 教我韧性。";
String message = this.getCompletion(prompt);
log.info("\nguidelines5:\n{}", message);
guidelines5:
<祖父母>: 韧性是一种能够在逆境中坚持不懈的品质,就像竹子一样,即使遭受到狂风暴雨的折磨,也能屹立不倒。要学习韧性,你需要学会从困难中找到力量,不气馁,勇敢地面对挑战,并且不断努力,始终相信自己能够克服困难,迈向成功的道路。记住,只有经历了风雨的洗礼,才能绽放出最美丽的花朵。
给模型时间取思考
如果您发现模型推理过程过于匆忙,导致得出了错误的结论,那么您应该尝试重新构思 Prompt ,要求模型在提供最终答案之前开展思维链,或进行一系列相关推理(a chain or series of relevant reasoning)。换句话说,如果您给模型一个在短时间内或用少量文字无法完成的复杂任务,它的输出结果就容易出错。这种情况对人来说也是类似:如果您要求某人完成复杂的数学问题,又不给足够时间计算出答案,他们也可能会犯错误。因此,在这些情况下,您应该指示模型花更多时间思考问题,让它在任务上花费更多计算资源。
指定完成任务所需的步骤
接下来我们将通过给定一个复杂任务,给出完成该任务的一系列步骤,来展示这一策略的效果。
首先我们描述了杰克和吉尔的故事,并给出提示词执行以下操作:首先,用一句话概括三个反引号限定的文本。第二,将摘要翻译成法语。第三,在法语摘要中列出每个名称。第四,输出包含以下键的 JSON 对象:法语摘要和人名个数。要求输出以换行符分隔。
java
String test = "在一个迷人的村庄里,兄妹杰克和吉尔出发去一个山顶井里打水。\n" +
"他们一边唱着欢乐的歌,一边往上爬,\n" +
"然而不幸降临------杰克绊了一块石头,从山上滚了下来,吉尔紧随其后。\n" +
"虽然略有些摔伤,但他们还是回到了温馨的家中。\n" +
"尽管出了这样的意外,他们的冒险精神依然没有减弱,继续充满愉悦地探索。";
String prompt = "执行以下操作:\n" +
"1-用一句话概括下面用三个反引号括起来的文本。\n" +
"2-将摘要翻译成法语。\n" +
"3-在法语摘要中列出每个人名。\n" +
"4-输出一个 JSON 对象,其中包含以下键:French_summary,num_names。\n" +
"\n" +
"请用换行符分隔您的答案。\n" +
"\n" +
"Text:\n" +
"```" + test + "```";
String message = this.getCompletion(prompt);
log.info("\nguidelines6:\n{}", message);
guidelines6:
1-两个兄妹在山上打水,发生意外后返回家中,继续冒险。
2-Dans un village charmant, les frère et sœur Jack et Jill partent chercher de l'eau d'un puits au sommet d'une montagne.
3-Jack, Jill
4-{"French_summary": "Dans un village charmant, les frère et sœur Jack et Jill partent chercher de l'eau d'un puits au sommet d'une montagne. Jack, Jill", "num_names": 2}
上述输出仍然存在一定问题,
因此,我们将Prompt加以改进,该 Prompt 前半部分不变,同时确切指定了输出的格式。
java
String test = "在一个迷人的村庄里,兄妹杰克和吉尔出发去一个山顶井里打水。\n" +
"他们一边唱着欢乐的歌,一边往上爬,\n" +
"然而不幸降临------杰克绊了一块石头,从山上滚了下来,吉尔紧随其后。\n" +
"虽然略有些摔伤,但他们还是回到了温馨的家中。\n" +
"尽管出了这样的意外,他们的冒险精神依然没有减弱,继续充满愉悦地探索。";
String prompt = "1-用一句话概括下面用<>括起来的文本。\n" +
"2-将摘要翻译成英语。\n" +
"3-在英语摘要中列出每个名称。\n" +
"4-输出一个 JSON 对象,其中包含以下键:English_summary,num_names。\n" +
"\n" +
"请使用以下格式:\n" +
"文本:<要总结的文本>\n" +
"摘要:<摘要>\n" +
"翻译:<摘要的翻译>\n" +
"名称:<英语摘要中的名称列表>\n" +
"输出 JSON:<带有 English_summary 和 num_names 的 JSON>\n" +
"\n" +
"Text: <" + test + ">";
String message = this.getCompletion(prompt);
log.info("\nguidelines7:\n{}", message);
guidelines7:
Summary: 杰克和吉尔在一个迷人的村庄里冒险,不幸地滚下山并受伤,但仍然保持着冒险精神。
Translation: In a charming village, siblings Jack and Jill set off to fetch water from a well on top of a hill. As they climbed and sang merrily, tragedy struck - Jack tripped on a stone and tumbled down the hill, followed closely by Jill. Despite their minor injuries, they made it back to their cozy home. Undeterred by the mishap, they continued to explore joyfully.
Names: Jack, Jill
Output JSON: {"English_summary": "In a charming village, siblings Jack and Jill embark on an adventure, unfortunately rolling down the hill and getting injured, but maintaining their adventurous spirit.", "num_names": 2}
指导模型在下结论之前找出一个自己的解法
明确地指引模型在匆匆做决策之前,要自己思考出一份解决方案。有时这样会得到更好的结果。这与之前所述思想类似,即给模型时间思考。
接下来我们会给出一个问题和一份来自学生的解答,要求模型判断解答是否正确:
java
String prompt = "判断学生的解决方案是否正确。\n" +
"\n" +
"问题:\n" +
"我正在建造一个太阳能发电站,需要帮助计算财务。\n" +
"\n" +
" 土地费用为 100美元/平方英尺\n" +
" 我可以以 250美元/平方英尺的价格购买太阳能电池板\n" +
" 我已经谈判好了维护合同,每年需要支付固定的10万美元,并额外支付每平方英尺10美元\n" +
" 作为平方英尺数的函数,首年运营的总费用是多少。\n" +
"\n" +
"学生的解决方案:\n" +
"设x为发电站的大小,单位为平方英尺。\n" +
"费用:\n" +
"\n" +
" 土地费用:100x\n" +
" 太阳能电池板费用:250x\n" +
" 维护费用:100,000美元+100x\n" +
" 总费用:100x+250x+100,000美元+100x=450x+100,000美元";
String message = this.getCompletion(prompt);
log.info("\nguidelines8:\n{}", message);
guidelines8:
学生的解决方案是正确的。他通过将各个费用项相加,并将费用与发电站大小的函数进行计算,得出了首年运营的总费用为450x + 100,000 美元。
但是注意,学生的解决方案实际上是错误的。(维护费用项100x应为10x,总费用450x应为360x)
我们可以通过指导模型先自行找出一个解法来解决这个问题。
在接下来这个 Prompt 中,我们要求模型先自行解决这个问题,再根据自己的解法与学生的解法进行对比,从而判断学生的解法是否正确。同时,我们给定了输出的格式要求。通过拆分任务、明确步骤,让模型有更多时间思考,有时可以获得更准确的结果。
在这个例子中,学生的答案是错误的,但如果我们没有先让模型自己计算,那么可能会被误导以为学生是正确的。
java
String prompt = "请判断学生的解决方案是否正确,请通过如下步骤解决这个问题:\n" +
"\n" +
"步骤:\n" +
"\n" +
" 首先,自己解决问题。\n" +
" 然后将您的解决方案与学生的解决方案进行比较,并评估学生的解决方案是否正确。\n" +
" 在自己完成问题之前,请勿决定学生的解决方案是否正确。\n" +
"\n" +
"使用以下格式:\n" +
"\n" +
" 问题:问题文本\n" +
" 学生的解决方案:学生的解决方案文本\n" +
" 实际解决方案和步骤:实际解决方案和步骤文本\n" +
" 学生的解决方案和实际解决方案是否相同:是或否\n" +
" 学生的成绩:正确或不正确\n" +
"\n" +
"问题:\n" +
"\n" +
" 我正在建造一个太阳能发电站,需要帮助计算财务。 \n" +
" - 土地费用为每平方英尺100美元\n" +
" - 我可以以每平方英尺250美元的价格购买太阳能电池板\n" +
" - 我已经谈判好了维护合同,每年需要支付固定的10万美元,并额外支付每平方英尺10美元\n" +
" 作为平方英尺数的函数,首年运营的总费用是多少。\n" +
"\n" +
"学生的解决方案:\n" +
"\n" +
" 设x为发电站的大小,单位为平方英尺。\n" +
" 费用:\n" +
" 1. 土地费用:100x\n" +
" 2. 太阳能电池板费用:250x\n" +
" 3. 维护费用:100,000+100x\n" +
" 总费用:100x+250x+100,000+100x=450x+100,000\n" +
"\n" +
"实际解决方案和步骤:";
String message = this.getCompletion(prompt);
log.info("\nguidelines8-2:\n{}", message);
三、局限性
开发大模型相关应用时请务必铭记:
虚假知识:模型偶尔会生成一些看似真实实则编造的知识
虽然模型在训练过程中接触了大量的知识,但它并没有完全记住所见的信息,因此它不甚清楚自己知识的边界。这意味着它可能会尝试回答主题晦涩难懂的问题,并编造听起来合理但实际上并不正确的答案。我们称这些编造的想法为幻觉(Hallucination)。
如下示例展示了大模型的幻觉。我们要求告诉我们 Boie 公司生产的 AeroGlide UltraSlim Smart Toothbrush 产品的信息,事实上,这个公司是真实存在的,但产品是编造的,而模型一本正经地提供了它编造的知识,而且迷惑性很强。
java
String prompt = "告诉我 Boie 公司生产的 AeroGlide UltraSlim Smart Toothbrush 的相关信息";
String message = this.getCompletion(prompt);
log.info("\nguidelines9:\n{}", message);
guidelines9:
Boie公司生产的AeroGlide UltraSlim Smart Toothbrush是一款智能牙刷。以下是该产品的相关信息:
1. 设计:AeroGlide UltraSlim Smart Toothbrush采用超薄设计,刷头宽度仅为0.5毫米,可以更容易地进入牙齿间隙和较难刷洗的区域。
2. 智能功能:该牙刷配备了智能传感器和振动技术,可以提供高效的清洁效果。智能传感器可以检测刷牙的时间和压力,以确保正确的刷牙习惯。
3. 刷毛:AeroGlide UltraSlim Smart Toothbrush的刷毛采用了柔软的尼龙材料,可以温和地清洁牙齿和牙龈,同时不会损伤牙釉质。
4. 充电:该牙刷内置可充电电池,使用USB充电,方便携带和充电。
5. 应用程序:AeroGlide UltraSlim Smart Toothbrush配备了一个配套的手机应用程序,可以跟踪刷牙习惯和提供个性化的刷牙建议。
6. 清洁模式:该牙刷提供多种清洁模式,包括日常清洁、牙龈护理和敏感牙齿等模式,以满足不同用户的需求。
总体而言,Boie公司的AeroGlide UltraSlim Smart Toothbrush是一款具有超薄设计和智能功能的牙刷,旨在提供高效的清洁效果和个性化的刷牙体验。
由于很容易以假乱真,在构建自己的应用程序时尽量避免幻觉情况。幻觉是大模型的一个已知缺陷,OpenAI也在努力解决该问题。
在您希望模型根据文本生成回答时,另一种减少幻觉的策略是先要求模型获取来源于该文本的所有引用信息(任何相关引用,any relevant quotes),然后要求它基于所引用的信息来回答问题,这使得我们能根据答案追溯源文档,通常对减少幻觉非常有帮助。
Java快速转换到大模型开发:
配套课程的所有代码已经发布在:https://github.com/Starcloud-Cloud/java-langchain
课程合作请留言