AI Agents实战——智能体推理与评估

258 阅读35分钟

本章内容包括:

  • 使用各种提示工程技术扩展大语言模型的功能
  • 使用启用推理的大语言模型提示工程技术
  • 采用评估提示缩小范围并识别未知问题的解决方案

在我们已经研究了定义智能体语义记忆组件的记忆与检索模式之后,现在可以探讨智能体的最后一个也是最关键的组件:规划。规划包含多个方面,从推理、理解、评估到反馈。

为了探索如何通过提示使大语言模型进行推理、理解和规划,我们将展示如何通过提示工程激发推理,并将其扩展到规划。语义内核(SK)提供的规划解决方案包括多种规划形式。本章最后将通过将自适应反馈整合到新规划器中来结束本章内容。

图 10.1 展示了我们将在本章中讨论的高级提示工程策略,并说明它们与我们将要讨论的各种技术之间的关系。图中展示的每种方法都将在本章中进行探讨,从解决方案/直接提示的基础内容(图左上角)到自一致性和思维树(ToT)提示(图右下角)。

image.png

10.1 理解直接解决方案提示

直接解决方案提示通常是用户在向大语言模型提问或解决特定问题时使用的首种提示工程形式。考虑到任何大语言模型的使用,这些技术可能看起来显而易见,但它们值得回顾,以建立思维和规划的基础。在下一节中,我们将从头开始,提出问题并期待答案。

10.1.1 问答提示

在本章的练习中,我们将使用提示流来构建和评估各种技术。(我们已经在第9章中广泛介绍了这个工具,如果需要复习,可以参考该章节。)提示流是理解这些技术如何工作并探索规划和推理过程流动的绝佳工具。

打开 Visual Studio Code (VS Code),进入第10章的源文件夹。为该文件夹创建一个新的虚拟环境,并安装 requirements.txt 文件中的依赖项。如果需要帮助设置本章的 Python 环境,请参考附录B。

我们将查看提示流中的第一个流程,位于 prompt_flow/question-answering-prompting 文件夹中。打开 flow.dag.yaml 文件,并在可视化编辑器中查看,如图10.2所示。在右侧,你将看到各个组件的流程。顶部是 question_answer LLM 提示,接下来是两个嵌入(Embedding)组件,最后是用于进行评估的 LLM 提示,名为 evaluate

image.png

列出10.1的分解显示了流的结构和组件,使用了一种简化的YAML伪代码形式。你还可以看到各个组件的输入输出以及运行该流程的示例输出。

列出10.1 问答提示流

   输入:
        context  : 要提问的内容
        question : 针对内容提问的问题
        expected : 期望的答案

   LLM:问答(用于提问的提示)
        输入:
               context  question
        输出:
               问题的预测/答案

   嵌入(Embeddings):使用LLM嵌入模型创建文本的嵌入表示

     Embedding_predicted: 嵌入问答LLM的输出
     Embedding_expected: 嵌入期望答案的输出

   Python:评估(用于测量嵌入相似度的Python代码)
     输入:
            Embedding_predicted输出
            Embedding_expected输出
     输出:
            预测与期望之间的相似度分数

   输出:
        context: -> input.context
        question: -> input.question
     expected: -> input.expected
     predicted: -> output.question_answer
     evaluation_score: output.evaluation

示例输出

{
    "context": "Back to the Future (1985)…",
    "evaluation_score": 0.9567478002354606,
    "expected": "Marty traveled back in time 30 years.",
    "predicted": "Marty traveled back in time 30 years from 1985 to 1955 in the movie "Back to the Future."",
    "question": "How far did Marty travel back in time in the movie Back to the Future (1985)"
}

在运行此流程之前,确保你的LLM块配置正确。这可能需要你设置与你选择的LLM的连接。如果需要复习如何完成配置,请参考第9章。如果你不使用OpenAI,需要为LLM和嵌入块配置你的连接。

配置完LLM连接后,通过可视化编辑器中的播放按钮或YAML编辑器窗口中的Test(Shift-F5)链接运行流程。如果一切连接并配置正确,你应该会看到类似列出10.1中的输出。

在VS Code中打开question_answer.jinja2文件,如列出10.2所示。此列出展示了基本的问答型提示。在这种类型的提示中,系统消息描述了基本规则并提供了回答问题的上下文。在第4章中,我们探讨了检索增强生成(RAG)模式,此提示遵循类似的模式。

列出10.2 question_answer.jinja2

system:
Answer the users question based on the context below. Keep the answer 
short and concise. Respond "Unsure about answer" if not sure about the 
answer.

Context: {{context}}     #1

user:
Question: {{question}}     #2
#1 Replace with the content LLM should answer the question about.
#2 Replace with the question.

这个练习展示了使用LLM针对内容提问的简单方法。然后,使用相似度匹配分数来评估问题的回答。从列出10.1中的输出可以看出,LLM很好地回答了关于上下文的问题。在下一节中,我们将探讨使用直接提示的类似技术。

10.1.2 实现少量示例提示

少量示例提示与问答提示类似,但提示的构成更多地是提供一些示例,而不是事实或上下文。这使得大语言模型能够适应之前未见过的模式或内容。虽然这种方法听起来像是问答,但其实现方式有很大不同,而且结果可能非常强大。

零-shot、一次-shot和少量-shot学习

机器学习和人工智能的一个圣杯是能够用尽可能少的样本训练模型。例如,在传统的视觉模型中,通常需要输入数百万张图片来帮助识别猫和狗之间的差异。

一个一次-shot模型只需要一张图片就可以训练。例如,可以展示一张猫的图片,模型便能识别任何猫的图片。少量-shot模型只需要几个样本来训练模型。当然,零-shot指的是在没有任何先前示例的情况下识别某样东西的能力。大语言模型是高效的学习者,可以执行这三种类型的学习。

在VS Code中打开prompt_flow/few-shot-prompting/flow.dag.yaml并使用可视化编辑器。大部分流程与之前图10.2中展示的相似,差异在列出10.3中突显出来,显示了YAML伪代码表示。这个流程与之前的主要区别在于输入和LLM提示。

列出10.3 少量示例提示流

   输入:
       statement  : 介绍上下文并要求输出
       expected : 期望的答案
   LLM: few_shot (用于提问的提示)
       输入:statement
       输出:对statement的预测/答案

   嵌入(Embeddings):使用LLM嵌入模型创建文本的嵌入表示
        Embedding_predicted: 嵌入少量示例LLM的输出
        Embedding_expected: 嵌入期望答案的输出

   Python:评估(用于测量嵌入相似度的Python代码)
        输入:
               Embedding_predicted 输出
               Embedding_expected 输出
        输出:
            预测与期望之间的相似度分数

输出:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.few_shot
        evaluation_score: output.evaluation

示例输出

{
    "evaluation_score": 0.906647282920417,     #1
    "expected": "We ate sunner and watched the setting sun.",
    "predicted": "After a long hike, we sat by the lake and enjoyed a peaceful sunner as the sky turned brilliant shades of orange and pink.",     #2
    "statement": "A sunner is a meal we eat in Cananda at sunset, please use the word in a sentence"     #3
}
#1 评估分数表示期望与预测之间的相似度。
#2 使用sunner在句子中
#3 这是一个错误的陈述,但目的是让LLM使用该词,就像它是真实的。

通过按Shift-F5或点击可视化编辑器中的播放/测试按钮运行该流程。你应该会看到类似列出10.3中的输出,其中LLM在给定初始陈述的情况下正确地使用了单词“sunner”(一个虚构的词)。

这个练习展示了如何使用提示来改变LLM的行为,使其与已学到的内容相反。我们正在改变LLM理解为准确的内容。此外,我们还使用这种修改后的视角来引导LLM使用一个虚构的词。

在VS Code中打开few_shot.jinja2提示,如列出10.4所示。该列出展示了如何设置一个简单的角色——一个古怪的词典编纂者,并提供了它之前定义和使用的单词示例。提示的基础允许LLM扩展示例并使用其他词语产生类似的结果。

列出10.4 few_shot.jinja2

system:
You are an eccentric word dictionary maker. You will be asked to 
construct a sentence using the word.
The following are examples that demonstrate how to craft a sentence using 
the word.
A "whatpu" is a small, furry animal native to Tanzania. 
An example of a sentence that uses the word whatpu is:     #1
We were traveling in Africa and we saw these very cute whatpus.
To do a "farduddle" means to jump up and down really fast. An example of a 
sentence that uses the word farduddle is:
I was so excited that I started to farduddle.     #2

Please only return the sentence requested by the user.   #3

user:
{{statement}}    #4
#1 Demonstrates an example defining a made-up word and using it in a sentence
#2 Demonstrates another example
#3 A rule to prevent the LLM from outputting extra information
#4 The input statement defines a new word and asks for the use.

你可以说我们在这里强迫LLM“幻觉”,但这种技术是修改行为的基础。它允许构建提示来引导LLM做出与其学到的内容相反的行为。这种提示的基础还为其他形式的行为修改技术奠定了基础。从改变LLM的感知和背景能力开始,我们将在下一节展示一个最终的直接解决方案示例。

10.1.3 使用零-shot提示提取概括

零-shot提示或学习是指生成一种提示,使得LLM能够进行概括。这种概括是嵌入在LLM中的,并通过零-shot提示进行展示,其中没有提供任何示例,而是给出一组指导方针或规则来引导LLM。

使用这种技术非常简单,并且能够很好地引导LLM根据其内部知识生成回复,而不需要其他上下文。这是一种微妙而强大的技术,它将LLM的知识应用到其他应用中。结合其他提示策略,这种技术在替代其他语言分类模型(例如识别文本中的情感或情绪的模型)方面表现出色。

在VS Code的提示流可视化编辑器中打开prompt_flow/zero-shot-prompting/flow.dag.yaml。这个流程与之前图10.1中的几乎完全相同,但在实现上略有不同,如列出10.5所示。

列出10.5 零-shot提示流

   输入:
        statement  : 需要分类的语句
        expected : 语句的期望分类

    LLM: zero_shot (用于分类的提示)
        输入:statement
        输出:给定语句的预测分类

    嵌入(Embeddings):使用LLM嵌入模型创建文本的嵌入表示

    Embedding_predicted: 嵌入零-shot LLM的输出
    Embedding_expected: 嵌入期望答案的输出

    Python:评估(用于测量嵌入相似度的Python代码)
        输入:
               Embedding_predicted 输出
               Embedding_expected 输出
        输出:
            预测与期望之间的相似度分数

   输出:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.zero_shot
        evaluation_score: output.evaluation

示例输出

{
       "evaluation_score": 1,     #1
       "expected": "neutral",
       "predicted": "neutral",
       "statement": "I think the vacation is okay."     #2
}
#1 显示完美的评估分数 1.0
#2 我们要求LLM分类的语句

通过在VS Code的提示流可视化编辑器中按Shift-F5运行该流程。你应该会看到类似列出10.5中的输出。

现在打开zero_shot.jinja2提示,如列出10.6所示。这个提示非常简单,且没有提供任何示例来提取文本中的情感。值得注意的是,提示中甚至没有提到“情感”这个词,但LLM似乎能够理解其意图。

列出10.6 zero_shot.jinja2

system:
Classify the text into neutral, negative or positive. 
Return on the result and nothing else.     #1

user:
{{statement}}     #2
#1 提供了进行分类的基本指导
#2 要分类的文本语句

零-shot提示工程就是利用LLM基于其训练材料进行广泛概括的能力。这个练习展示了如何将LLM中的知识应用于其他任务。LLM自我上下文化和应用知识的能力可以超越其训练材料。在下一节中,我们将进一步扩展这一概念,探讨LLM如何进行推理。

10.2 提示工程中的推理

像ChatGPT这样的LLM(大语言模型)是为了作为聊天完成模型而开发的,其中文本内容被输入模型,模型的回答与完成该请求的目标一致。然而,LLM并不是为了推理、规划、思考或拥有思想而训练的。

然而,正如我们在前一节中的示例所展示的那样,LLM可以通过提示来提取其概括能力,并超越其初始设计。虽然LLM并未设计为进行推理,但输入模型的训练材料提供了推理、规划和思维的理解。因此,LLM通过扩展理解推理的概念,并能够运用推理。

推理与规划

推理是智力(无论人工还是自然)理解思维过程或通过问题进行思考的能力。智力能够理解行为的结果,并能利用这种能力推理出从一组行为中选择哪一个可以用来解决给定的任务。

规划是智力推理出行为或任务的顺序,并应用正确的参数来实现目标或结果的能力——智力计划的程度取决于问题的范围。一个智力体可能会结合多个层次的规划,从战略和战术到操作性和应急计划。

我们将研究另一组提示工程技术,这些技术允许或模拟推理行为,从而展示这种推理能力。通常,在评估推理应用时,我们希望让LLM解决它本来没有设计来解决的复杂问题。逻辑、数学和文字问题通常是这种评估的好来源。

以时间旅行为主题,解决什么样的独特问题比理解时间旅行更合适呢?图10.3展示了一个独特且具有挑战性的时间旅行问题。我们的目标是获得一种提示LLM的能力,使其能够正确解决这个问题。

image.png

时间旅行问题是被认为是思维练习,解决起来可能 deceptively difficult(看似简单但实则复杂)。图10.3中的示例对于LLM来说很难解决,但它出错的部分可能会让你感到惊讶。下一节将使用推理提示来解决这些独特的问题。

10.2.1 思维链(Chain of Thought)提示

思维链(CoT)提示是一种提示工程技术,使用一次-shot或少量-shot示例来描述推理过程和完成目标的步骤。通过推理的展示,LLM可以将这一原则概括,并通过类似的问题和目标进行推理。虽然LLM的训练目标并非推理,但我们可以通过提示工程引导模型进行推理。

在VS Code的提示流可视化编辑器中打开prompt_flow/chain-of-thought-prompting/flow.dag.yaml。这个流程的元素很简单,如图10.4所示。只有两个LLM模块,第一个使用CoT提示来解决一个复杂问题;然后,第二个LLM提示评估答案。

image.png

列出10.7 显示了描述流中块及其输入/输出的YAML伪代码,提供了更多的细节。此示例中的默认问题陈述与图10.3中的不同。

列出10.7 思维链提示流

   输入:
        statement  : 要解决的问题陈述
        expected : 问题的期望解答

   LLM: cot (用于解决问题的提示)
        输入:statement
        输出:给定问题陈述的预测答案

   LLM: evaluate_answer (用于评估解答的提示)
        输入:
               statement: -> input.statement
               expected: -> input.expected
               predicted: -> output.cot

        输出:评估问题解答的分数


   输出:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.cot
        evaluation_score: output.evaluate_answer

示例输出

{
    "evaluation_score": "0.5",     #1
    "expected": "After the final jump, Max finds himself 
in the year 1980 and he is 75 years old.",     #2
    "predicted": " Max starts in the year 2300 and 
travels backward in 40-year increments, spending 5 years 
in each period. The journeys will be as follows:
\n\n- From 2300 to 2260: Max is 25 + 5 = 30 years old.
\n- From 2260 to 2220: Max is 30 + 5 = 35 years old.
\n- From 2220 to 2180: Max is 35 + 5 = 40 years old.
\n- From 2180 to 2140: Max is 40 + 5 = 45 years old.
\n- From 2140 to 2100: Max is 45 + 5 = 50 years old.
\n- From 2100 to 2060: Max is 50 + 5 = 55 years old.
\n- From 2060 to 2020: Max is 55 + 5 = 60 years old.
\n- From 2020 to 1980: Max is 60 + 5 = 65 years old.
\n- From 1980 to 1940: Max is 65 + 5 = 70 years old.
\n- From 1940 to 1900: Max is 70 + 5"     #3
}
#1 表示给定解答的评估分数
#2 问题的期望答案
#3 预测的答案显示了解决过程和结果

深入了解输入并检查问题陈述,尝试自己评估这个问题。然后,按Shift-F5运行流程。你应该会看到类似列出10.7中的输出。

现在打开cot.jinja2提示文件,如列出10.8所示。该提示给出了几个时间旅行问题的示例,并提供了经过思考和推理的解决方案。展示给LLM完成问题的步骤过程提供了推理机制。

列出10.8 cot.jinja2

system:
"In a time travel movie, Sarah travels back in time to 
prevent a historic event from happening. She arrives 
2 days before the event. After spending a day preparing, 
she attempts to change the event but realizes she has 
actually arrived 2 years early, not 2 days. She then 
decides to wait and live in the past until the event's 
original date. How many days does Sarah spend in the past 
before the day of the event?"     #1

Chain of Thought:     #2

    Initial Assumption: Sarah thinks she has arrived 2 days before the event.
    Time Spent on Preparation: 1 day spent preparing.
    Realization of Error: Sarah realizes she's actually 2 years early.
    Conversion of Years to Days: 
2 years = 2 × 365 = 730 days (assuming non-leap years).
    Adjust for the Day Spent Preparing: 730 - 1 = 729 days.
    Conclusion: Sarah spends 729 days in the past before the day of the event.

"In a sci-fi film, Alex is a time traveler who decides 
to go back in time to witness a famous historical battle 
that took place 100 years ago, which lasted for 10 days. 
He arrives three days before the battle starts. However, 
after spending six days in the past, he jumps forward in 
time by 50 years and stays there for 20 days. Then, he 
travels back to witness the end of the battle. How many 
days does Alex spend in the past before he sees the end of
 the battle?"     #3

Chain of Thought:     #4

    Initial Travel: Alex arrives three days before the battle starts.
    Time Spent Before Time Jump: Alex spends six days in the past. 
The battle has started and has been going on for 3 days (since he 
arrived 3 days early and has now spent 6 days, 3 + 3 = 6).
    First Time Jump: Alex jumps 50 years forward and stays for 20 days.
 This adds 20 days to the 6 days he's already spent in the past 
(6 + 20 = 26).
    Return to the Battle: When Alex returns, he arrives back on the same 
day he left (as per time travel logic). The battle has been going on for 
3 days now.
    Waiting for the Battle to End: The battle lasts 10 days. Since he's 
already witnessed 3 days of it, he needs to wait for 7 more days.
    Conclusion: Alex spends a total of 3 (initial wait) + 3 (before the 
first jump) + 20 (50 years ago) + 7 (after returning) = 33 days in the 
past before he sees the end of the battle.
Think step by step but only show the final answer to the statement.

user:
{{statement}}     #5

#1 给出了几个示例问题陈述
#2 该问题的解决方案,输出为推理步骤序列
#3 给出了几个示例问题陈述
#4 该问题的解决方案,输出为推理步骤序列
#5 LLM被指示解决的问题陈述

你可能会注意到图10.3中的问题解决方案也在列出10.8中作为示例提供。回顾列出10.7中的LLM回复也很有帮助,这样你可以看到LLM应用的推理步骤,得出了最终的答案。

现在,我们可以查看评估提示,看看它如何评估解答是否解决了问题。打开evaluate_answer.jinja2,如列出10.9所示,以审查使用的提示。这个提示很简单,使用了零-shot提示,并允许LLM概括如何评分期望结果和预测结果。我们可以提供示例和评分,从而将其转换为少量-shot分类的示例。

列出10.9 evaluate_answer.jinja2

system:

Please confirm that expected and predicted results are 
the same for the given problem.     #1
Return a score from 0 to 1 where 1 is a perfect match and 0 is no match.
Please just return the score and not the explanation.     #2

user:
Problem: {{problem}}     #3

Expected result: {{expected}}     #4

Predicted result: {{predicted}}     #5
#1 用于评估解答是否正确的规则
#2 指示只返回评分,不要返回解释
#3 初始问题陈述
#4 期望的解答或基础答案
#5 CoT提示之前的输出

查看列出10.7中显示的LLM输出,你可以看到评估步骤可能会让人困惑。也许可以通过建议LLM在单个陈述中提供最终答案来解决这个问题。在下一节中,我们将介绍另一个提示推理的示例。

10.2.2 零-shot CoT 提示

正如我们的时间旅行示例所示,CoT 提示在生成特定类型问题的提示时可能非常昂贵。虽然不如 CoT 提示有效,但有些技术类似于 CoT,它们不使用示例,且可以更广泛地应用。本节将研究一种直接的短语,用于引导 LLM 进行推理。

在 VS Code 的提示流可视化编辑器中打开 prompt_flow/zero-shot-cot-prompting/flow.dag.yaml。这个流与之前的 CoT 非常相似,如图 10.4 所示。接下来的列表展示了描述该流的 YAML 伪代码。

列出10.10 零-shot-CoT 提示流

   输入:
        statement  : 要解决的问题陈述
        expected : 问题的期望解答

   LLM: cot (用于解决问题的提示)
        输入:statement
        输出:给定问题陈述的预测答案

   LLM: evaluate_answer (用于评估解答的提示)
        输入:
               statement: -> input.statement
               expected: -> input.expected
               predicted: -> output.cot

         输出:评估问题解答的分数

    输出:
        statement: -> input.statement
        expected: -> input.expected
        predicted: -> output.cot
        evaluation_score: output.evaluate_answer

示例输出

{
    "evaluation_score": "1",     #1
    "expected": "After the final jump, ↪
          ↪ Max finds himself in the year 1980 and 
   he is 75 years old.",     #2
    "predicted": "Max starts in… ↪
          ↪ Therefore, after the final jump, ↪
          ↪ Max is 75 years old and in the year 1980.",     #3
    "statement": "In a complex time travel …"     #4
}
#1 最终评估分数
#2 期望的答案
#3 预测的答案(步骤已省略,仅显示最终答案)
#4 初始问题陈述

通过按 Shift-F5 在 VS Code 中运行/测试流,你将看到类似列出10.10中的输出。这个示例比之前在相同问题上的示例表现更好。

现在打开 VS Code 中的 cot.jinja2 提示,如列出10.11所示。这是一个比之前示例更简单的提示,因为它仅使用零-shot。然而,提示中的一个关键短语将这个简单的提示转变为强大的推理引擎。提示中的“Let's think step by step”会触发 LLM 考虑内部上下文并展示推理过程。这样就引导 LLM 按步骤推理问题。

列出10.11 cot.jinja2

system:
You are an expert in solving time travel problems.
You are given a time travel problem and you have to solve it.
Let's think step by step.     #1
Please finalize your answer in a single statement.     #2

user:
{{statement}}     #3
#1 一个魔法短语,促使 LLM 进行推理
#2 要求 LLM 提供最终的答案陈述
#3 LLM 被要求解决的问题陈述

类似的短语,要求 LLM 思考步骤或要求它按步骤响应,也能够引导出推理过程。在下一节中,我们将展示一种类似但更加复杂的技术。

10.2.3 使用提示链按步骤解决问题

我们可以将要求LLM按步骤思考的行为扩展为一系列提示链,这些提示链迫使LLM按步骤解决问题。在本节中,我们将介绍一种名为提示链(prompt chaining)的技术,它迫使LLM按步骤处理问题。

在可视化编辑器中打开 prompt_flow/prompt-chaining/flow.dag.yaml 文件,如图 10.5 所示。提示链将解决问题时使用的推理方法分解为一系列提示。该技术迫使LLM以步骤的形式回答问题。

image.png

列出10.12 显示了描述流的 YAML 伪代码,提供了更多细节。这个流将第一个LLM块的输出链入第二个LLM块,然后再从第二个链入第三个。通过这种方式迫使LLM按步骤处理问题,可以揭示推理模式,但也可能会显得过于冗长。

列出10.12 提示链流

   输入:
        statement  : 要解决的问题陈述

   LLM: decompose_steps (用于分解问题的提示)
        输入: 
               statement: -> input.statement     #1

        输出:解决问题的步骤细分

   LLM: calculate_steps (用于计算步骤的提示)
        输入:
               statement: -> input.statement
               decompose_steps: -> output.decompose_steps     #2

        输出:每个步骤的计算结果

   LLM: calculate_solution (尝试解决问题)
        输入:
               statement: -> input.statement
               decompose_steps: -> output.decompose_steps
               calculate_steps: -> output.calculate_steps     #3

         输出:最终的解决方案陈述

   输出:
        statement: -> input.statement
        decompose_steps: -> output.decompose_steps
        calculate_steps: -> output.calculate_steps
        calculate_solution: -> output.calculate_solution

示例输出

{
    "calculate_steps": "1. The days spent by Alex",
    "decompose_steps": "To figure out the …",
    "solution": "Alex spends 13 days in the ↪
           ↪ past before the end of the battle.",     #4
    "statement": "In a sci-fi film, Alex …"    
}
#1 提示链的起始
#2 从上一步的输出注入到这一阶段
#3 从前两步的输出注入到这一阶段
#4 最终的解决方案陈述,虽然错误,但更接近正确答案

按Shift-F5运行流程,你将看到与列出10.12中类似的输出。答案仍然不正确(针对Alex的问题),但我们可以看到LLM在推理问题时所做的所有工作。

接下来,打开所有三个提示:decompose_steps.jinja2calculate_steps.jinja2calculate_solution.jinja2(分别参见列出10.13、10.14 和 10.15)。可以通过比较这三个提示,展示它们如何连接输出。

列出10.13 decompose_steps.jinja2

system:
You are a problem solving AI assistant.
Your job is to break the users problem down into smaller steps and list 
the steps in the order you would solve them.
Think step by step, not in generalities.
Do not attempt to solve the problem, just list the steps. #1

user:
{{statement}}     #2
#1 强制LLM只列出步骤,不包含其他内容
#2 初始问题陈述

列出10.14 calculate_steps.jinja2

system:
You are a problem solving AI assistant.
You will be given a list of steps that solve a problem.
Your job is to calculate the output for each of the steps in order.
Do not attempt to solve the whole problem,
just list output for each of the steps.     #1
Think step by step.     #2

user:
{{statement}}

{{steps}}     #3
#1 请求LLM不要解决整个问题,只列出每个步骤的输出
#2 使用魔法语句提取推理过程
#3 注入从`decompose_steps`阶段产生的步骤

列出10.15 calculate_solution.jinja2

system:
You are a problem solving AI assistant.
You will be given a list of steps and the calculated output for each step.
Use the calculated output from each step to determine the final 
solution to the problem.
Provide only the final solution to the problem in a 
single concise sentence. Do not include any steps 
in your answer.     #1

user:
{{statement}}

{{steps}}     #2

{{calculated}}     #3
#1 请求LLM输出最终的答案,而不包含任何步骤
#2 分解后的步骤
#3 已计算的步骤

在这个示例练习中,我们没有进行任何评估和评分。没有评估,我们可以看到这系列提示仍然没有成功解决之前图10.3中展示的更具挑战性的时间旅行问题。但这并不意味着这种技术没有价值,这种提示格式在解决一些复杂问题时表现得很好。

然而,我们希望找到一种推理和规划方法,能够持续解决这些复杂的问题。下一节将从推理转向评估最佳解决方案。

10.3 使用评估确保一致的解决方案

在上一节中,我们了解到,即使是推理最充分的计划,也不一定能得出正确的解决方案。此外,我们也可能没有答案来确认该解决方案是否正确。现实情况是,我们通常希望使用某种形式的评估来判断解决方案的有效性。

图10.6展示了为了让LLM进行推理和规划而设计的提示工程策略的比较。我们已经讨论了左侧的两种策略:零-shot直接提示和CoT提示。本节中的以下示例练习将探讨使用CoT和ToT技术来确保自一致性。

image.png

我们将继续关注复杂的时间旅行问题,以比较这些更先进的方法,这些方法扩展了推理和规划,并结合了评估。在下一节中,我们将评估自一致性。

10.3.1 评估自一致性提示

提示中的一致性不仅仅是降低我们传递给LLM的温度参数。通常,我们希望生成一致的计划或解决方案,同时仍然使用较高的温度来更好地评估计划的所有变体。通过评估多个不同的计划,我们可以更好地了解解决方案的整体价值。

自一致性提示是一种为给定问题生成多个计划/解决方案的技术。然后,评估这些计划,并接受更频繁或更一致的计划。假设生成了三个计划,其中两个相似,但第三个不同。使用自一致性技术,我们将前两个计划评估为更一致的答案。

在VS Code的提示流可视化编辑器中打开 prompt_flow/self-consistency-prompting/flow.dag.yaml。流程图展示了图10.7中提示生成流程的简单性。旁边是自一致性评估流程。

image.png

提示流使用直接非循环图(DAG)格式来执行流逻辑。DAG是一种非常好的展示和执行流逻辑的方法,但由于它们是非循环的(即不能重复),所以无法执行循环。然而,由于提示流提供了批处理机制,我们可以利用这个机制来模拟流中的循环或重复。

参考图10.6,我们可以看到自一致性过程在收集结果并确定最佳计划/回复之前处理输入三次。我们可以应用相同的模式,但使用批处理来生成输出。然后,评估流程将汇总结果并确定最佳答案。

在VS Code中打开 self-consistency-prompting/cot.jinja2 提示模板(参见列出10.16)。该列出已被简化,因为我们之前已看到部分内容。这个提示使用两个(少量-shot提示)CoT示例来展示推理过程给LLM。

列出10.16 self-consistency-prompting/cot.jinja2

system:

"In a time travel movie, Sarah travels back… "     #1

Chain of Thought:

    Initial Assumption:      #2
    Conclusion: Sarah spends 729 days in the past before the day of the event.

"In a complex time travel movie plot, Max, a 25 year old…"     #3

Chain of Thought:
    Starting Point: Max starts      #4
    Conclusion: After the final jump, 
Max finds himself in the year 1980 and he is 75 years old.
Think step by step,
 but only show the final answer to the statement.     #5

user:
{{statement}}
#1 Sarah 时间旅行问题
#2 示例CoT,已删减以简化内容
#3 Max 时间旅行问题
#4 示例CoT,已删减以简化内容
#5 最终指导和声明,约束输出

在VS Code中打开 self-consistency-prompting/flow.dag.yaml 文件。通过点击可视化编辑器中的批量运行(Batch Run,烧瓶图标)来以批处理模式运行示例。图10.8展示了逐步过程:

  • 点击批量运行。
  • 选择JSON Lines(JSONL)输入。
  • 选择 statements.jsonl
  • 点击运行链接。

image.png

提示 如果需要回顾该过程,请参阅第9章,其中对这个过程进行了更详细的说明。

列出10.17 显示了以批处理模式执行流程后的JSON输出。statements.jsonl 文件包含五个相同的Alex时间旅行问题条目。使用相同的条目使我们能够模拟对重复条目执行五次提示。

列出10.17 自一致性提示批处理执行输出

{
    "name": "self-consistency-prompting_default_20240203_100322_912000",
    "created_on": "2024-02-03T10:22:30.028558",
    "status": "Completed",
    "display_name": "self-consistency-prompting_variant_0_202402031022",
    "description": null,
    "tags": null,
    "properties": {
        "flow_path": "…prompt_flow/self-consistency-prompting",     #1
        "output_path": "…/.promptflow/.runs/self-
↪ consistency-prompting_default_20240203_100322_912000",     #2
        "system_metrics": {
            "total_tokens": 4649,
            "prompt_tokens": 3635,
            "completion_tokens": 1014,
            "duration": 30.033773
        }
    },
    "flow_name": "self-consistency-prompting",
    "data": "…/prompt_flow/self-consistency-prompting/
↪ statements.jsonl",     #3
    "output": "…/.promptflow/.runs/self-consistency-↪
↪ prompting_default_20240203_100322_912000/flow_outputs"
}

#1 执行流的路径
#2 包含流输出的文件夹(请注意此路径)
#3 用于批量执行流的数据

你可以通过按住Ctrl键并点击列出10.17中突出显示的输出链接来查看流。这将打开VS Code的另一个实例,显示一个包含所有运行输出的文件夹。接下来,我们想检查最一致的答案。幸运的是,提示流中的评估功能可以帮助我们通过相似度匹配来识别一致的答案。

在VS Code中打开 self-consistency-evaluation/flow.dag.yaml(参见图10.7)。该流程嵌入了预测答案,然后使用聚合来确定最一致的答案。

从流程中打开 consistency.py 文件,如列出10.18所示。这个工具功能的代码计算所有答案对的余弦相似度。然后,它找到最相似的答案,记录并输出该答案。

列出10.18 consistency.py

from promptflow import tool
from typing import List
import numpy as np
from scipy.spatial.distance import cosine

@tool
def consistency(texts: List[str],
                embeddings: List[List[float]]) -> str:
    if len(embeddings) != len(texts):
        raise ValueError("The number of embeddings ↪
       ↪ must match the number of texts.")

    mean_embedding = np.mean(embeddings, axis=0)     #1
    similarities = [1 - cosine(embedding, mean_embedding) ↪
                ↪ for embedding in embeddings]     #2
    most_similar_index = np.argmax(similarities)     #3

    from promptflow import log_metric
    log_metric(key="highest_ranked_output", value=texts[most_similar_index])     #4

    return texts[most_similar_index]     #5

#1 计算所有嵌入的均值
#2 计算每对嵌入的余弦相似度
#3 找到最相似答案的索引
#4 将输出记录为度量
#5 返回最相似答案的文本

我们也需要在批处理模式下运行评估流。打开 self-consistency-evaluation/flow.dag.yaml 文件并在VS Code中运行流(点击烧瓶图标)。然后,选择“现有运行”(Existing Run)作为流输入,并在提示时选择你刚刚执行的第一个或最后一个运行作为输入。

同样,在流程完成处理后,你将看到类似于列出10.17中的输出。Ctrl+点击输出文件夹链接以打开VS Code的新实例,显示结果。找到并在VS Code中打开 metric.json 文件,如图10.9所示。

image.png

图10.9中显示的答案对于此运行仍然不正确。你可以继续进行更多批量运行提示和/或增加批量中的运行次数,然后评估流,看看是否得到更好的答案。这个技术通常对于更直接的问题更有帮助,但仍然无法推理出复杂的问题。

自一致性使用一种反思的方法来评估最可能的思维。然而,最可能的思维并不总是最好的。因此,我们必须在下一节中考虑一种更全面的方法。

10.3.2 评估思维树提示

如前所述,思维树(ToT)提示,如图10.6所示,结合了自评估和提示链技术。因此,它将规划的序列分解为一系列提示,但在链中的每个步骤,它都会进行多次评估。这就创建了一棵树,可以在每个层次进行执行和评估,可以是广度优先或深度优先执行。

图10.10展示了使用广度优先或深度优先执行树的区别。不幸的是,由于提示流的DAG执行模式,我们无法快速实现深度优先方法,但广度优先方法完全可以正常工作。

image.png

在VS Code中打开 tree-of-thought-evaluation/flow.dag.yaml 文件。流程的可视化图如图10.11所示。这个流程像广度优先的思维树(ToT)模式一样运行——流程将一系列提示串联在一起,要求LLM在每个步骤返回多个计划。

image.png

由于该流程以广度优先的方式执行,因此每个节点的输出也会被评估。流程中的每个节点都使用一对语义函数——一个生成答案,另一个评估答案。语义函数是一个自定义的Python流块,处理多个输入并生成多个输出。

列出10.19 显示了 semantic_function.py 工具。这个通用工具在此流程的多个块中被重用。它还展示了来自语义内核(SK)的嵌入功能,供提示流直接使用。

列出10.19 semantic_function.py

@tool
def my_python_tool(
    input: str,
    input_node: int,
    history: str,
    semantic_function: str,
    evaluation_function: str,
    function_name: str,
    skill_name: str,
    max_tokens: int,
    temperature: float,
    deployment_name: str,
    connection: Union[OpenAIConnection, 
                      AzureOpenAIConnection],     #1
) -> str:
    if input is None or input == "":     #2
        return ""

    kernel = sk.Kernel(log=sk.NullLogger())
    # 设置内核和LLM连接的代码省略

    function = kernel.create_semantic_function(
                             semantic_function,                                               
                             function_name=function_name,
                             skill_name=skill_name,
                             max_tokens=max_tokens,
                             temperature=temperature,
                             top_p=0.5)     #3
    evaluation = kernel.create_semantic_function(
                             evaluation_function,        
                             function_name="Evaluation",
                             skill_name=skill_name,
                             max_tokens=max_tokens,
                             temperature=temperature,
                             top_p=0.5)     #4

    async def main():
        query = f"{history}\n{input}"
        try:
            eval = int((await evaluation.invoke_async(query)).result)
            if eval > 25:     #5
                return await function.invoke_async(query)    #6
        except Exception as e:
            raise Exception("Evaluation failed", e)

       try:
        result = asyncio.run(main()).result
        return result
    except Exception as e:
        print(e)
        return ""

#1 使用联合类型以支持不同类型的LLM连接
#2 检查输入是否为空或None;如果是,则不执行该函数
#3 设置用于创建计划的生成函数
#4 设置评估函数
#5 执行评估函数,确定输入是否足够好继续执行
#6 如果评估分数足够高,则生成下一步

语义函数工具在树的专家、节点和答案块中使用。在每个步骤中,函数会检查是否有文本输入。如果没有文本,块将返回并不执行。将空文本传递给块意味着上一个块未通过评估。通过在每个步骤之前进行评估,ToT短路了它认为无效的计划执行。

这可能是一个复杂的模式,刚开始可能不容易理解,因此请在VS Code中运行该流程。列出10.20 仅显示运行后的答案节点输出;这些结果可能与你看到的有所不同,但应该是类似的。返回空文本的节点要么是评估失败,要么是它们的父节点失败。

列出10.20 来自思维树评估流程的输出

{
    "answer_1_1": "",     #1
    "answer_1_2": "",
    "answer_1_3": "",
    "answer_2_1": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_2_2": "",     #2
    "answer_2_3": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_3_1": "",     #3
    "answer_3_2": "Alex spends a total of 29 days in the past before he 
sees the end of the battle.",
    "answer_3_3": "Alex spends a total of 9 days in the past before he 
sees the end of the battle."
}

#1 表示第一个节点的计划无效,未执行
#2 节点2的计划和答案2未通过评估,未运行
#3 该节点的计划未通过评估,未运行

列出10.20 中的输出显示了只有选定的节点经过评估。在大多数情况下,评估的节点返回了可能有效的答案。没有输出的地方意味着该节点本身或其父节点无效。当兄弟节点都返回空值时,父节点的评估失败。

如我们所见,ToT适用于复杂问题,但可能不太实用。执行这个流程可能需要多达27次调用LLM来生成输出。实际上,它可能只需要进行一半的调用,但这仍然意味着要回答一个问题,可能需要十次或更多次的调用。

10.4 练习

通过以下练习来提升你对材料的理解:

练习 1 — 创建直接提示、少量示例提示和零-shot提示
目标 — 为LLM创建三种不同的提示,分别总结一篇最近的科学文章:一种使用直接提示,一种使用少量示例提示,最后一种使用零-shot提示。
任务:

  • 比较每种方法生成的总结的有效性。
  • 比较每种方法生成的总结的准确性。

练习 2 — 构建推理提示
目标 — 设计一组需要LLM解决逻辑谜题或难题的提示。
任务:

  • 关注提示的结构如何影响LLM的推理过程。
  • 关注相同的结构如何影响答案的正确性。

练习 3 — 评估提示技术
目标 — 开发一个评估提示,要求LLM预测一个假设实验的结果。
任务:

  • 创建一个后续提示,评估LLM预测的准确性,并提供关于其推理过程的反馈。

总结

  • 直接解决方案提示 是一种基础方法,使用提示引导LLM解决特定问题或任务,强调清晰的问答结构的重要性。
  • 少量示例提示 为LLM提供一些示例,帮助其处理新的或未见过的内容,突出其使模型适应不熟悉模式的能力。
  • 零-shot学习和提示 展示了LLM如何从训练中概括并解决问题,而无需显式示例,展示了其理解和应用新知识的内在能力。
  • 思维链提示 引导LLM通过推理过程一步步解决复杂问题,说明如何从模型中引出详细的推理。
  • 提示链 将一个问题分解为一系列相互关联的提示,展示了如何将复杂的问题解决过程结构化为可管理的步骤,以便LLM处理。
  • 自一致性 是一种提示技术,生成多个解决方案并通过评估选择最一致的答案,强调一致性在获得可靠结果中的重要性。
  • 思维树提示 结合了自评估和提示链,创造了一种综合的策略来解决复杂问题,使得能够系统地探索多个解决路径。
  • 高级提示工程策略 提供了关于复杂技术的见解,如使用CoT和ToT进行自一致性,提供了提高LLM生成解决方案准确性和可靠性的方法。