概述

提示工程(Prompt Engineering)是一门较新的学科,关注提示词开发和优化,帮助用户将大语言模型(Large Language Model, LLM)用于各场景和研究领域。

提示工程不仅仅是关于设计和研发提示词。它包含了与大语言模型交互和研发的各种技能和技术。提示工程在实现和大语言模型交互、对接,以及理解大语言模型能力方面都起着重要作用。用户可以通过提示工程来提高大语言模型的安全性,也可以赋能大语言模型,比如借助专业领域知识和外部工具来增强大语言模型能力。

比如

写一篇产品经理的演讲稿,注意使用以下词汇: 赋能,抓手,中台,闭环,落地,漏斗,沉淀,给到,同步,对齐,对标,迭代,拉通,打通,升级,交付,聚焦,倒逼,复盘,梳理,方案,联动,透传,咬合,洞察,渗透,兜底,解耦,耦合,复用,拆解

提示词可以包含以下任意要素:

指令:想要模型执行的特定任务或指令。

上下文:包含外部信息或额外的上下文信息,引导语言模型更好地响应。

输入数据:用户输入的内容或问题。

输出指示:指定输出的类型或格式。

提示技术

零样本提示

如今,经过大量数据训练并调整指令的LLM能够执行零样本任务:

提示:

将文本分类为中性、负面或正面。文本:我认为这次假期还可以。情感:

输出:

中性

少样本提示

虽然大型语言模型展示了惊人的零样本能力,但在使用零样本设置时,它们在更复杂的任务上仍然表现不佳。少样本提示可以作为一种技术,以启用上下文学习,我们在提示中提供演示以引导模型实现更好的性能。演示作为后续示例的条件,我们希望模型生成响应。

提示:

“whatpu”是坦桑尼亚的一种小型毛茸茸的动物。一个使用whatpu这个词的句子的例子是:我们在非洲旅行时看到了这些非常可爱的whatpus。“farduddle”是指快速跳上跳下。一个使用farduddle这个词的句子的例子是:

输出:

当我们赢得比赛时,我们都开始庆祝跳跃。

链式思考(CoT)提示

COT

提示:

这组数中的奇数加起来是偶数:489151221
A:将所有奇数相加(9151)得到25。答案为False。

这组数中的奇数加起来是偶数:171019481224
A:将所有奇数相加(1719)得到36。答案为True。

这组数中的奇数加起来是偶数:161114481324
A:将所有奇数相加(1113)得到24。答案为True。

这组数中的奇数加起来是偶数:17910121342
A:将所有奇数相加(17913)得到39。答案为False。

这组数中的奇数加起来是偶数:15325138271
A:

输出:

将所有奇数相加(15、5、13、7、1)得到41。答案为False。

零样本 COT 提示

Zero-shot COT

提示:

我去市场买了10个苹果。我给了邻居2个苹果和修理工2个苹果。然后我去买了5个苹果并吃了1个。我还剩下多少苹果?

输出:

11个苹果

答案是不正确的!

提示:

我去市场买了10个苹果。我给了邻居2个苹果和修理工2个苹果。然后我去买了5个苹果并吃了1个。我还剩下多少苹果?让我们逐步思考。

输出:

首先,您从10个苹果开始。您给了邻居和修理工各2个苹果,所以您还剩下6个苹果。然后您买了5个苹果,所以现在您有11个苹果。最后,您吃了1个苹果,所以您还剩下10个苹果。

自动思维链(Auto-CoT)

当使用思维链提示时,这个过程需要手工制作有效且多样化的例子。这种手动工作可能会导致次优解决方案。Zhang et al. (2022)(opens in a new tab) 提出了一种消除人工的方法,即利用 LLMs “让我们一步一步地思考” 提示来生成一个接一个的推理链。这种自动过程仍然可能在生成的链中出现错误。为了减轻错误的影响,演示的多样性很重要。这项工作提出了Auto-CoT,它对具有多样性的问题进行采样,并生成推理链来构建演示。

Auto-CoT 主要由两个阶段组成:

  • 阶段1:问题聚类:将给定问题划分为几个聚类
  • 阶段2:演示抽样:从每组数组中选择一个具有代表性的问题,并使用带有简单启发式的 Zero-Shot-CoT 生成其推理链

简单的启发式方法可以是问题的长度(例如,60 个 tokens)和理由的步骤数(例如,5 个推理步骤)。这鼓励模型使用简单而准确的演示。

该过程如下图所示:

AUTOCOT

相关资料

自我一致性

自我一致性旨在“替换链式思维提示中使用的天真贪婪解码方法”。其想法是通过少样本 CoT 采样多个不同的推理路径,并使用生成结果选择最一致的答案。这有助于提高 CoT 提示在涉及算术和常识推理的任务中的性能。

提示:

当我6岁时,我的妹妹是我的一半年龄。现在我70岁了,我的妹妹多大?

输出:

35

输出是错误的!如何通过自我一致性来改进这个问题?

提示:

Q:林中有15棵树。林业工人今天将在林中种树。完成后,将有21棵树。林业工人今天种了多少棵树?
A:我们从15棵树开始。后来我们有21棵树。差异必须是他们种树的数量。因此,他们必须种了21-15 = 6棵树。答案是6。
Q:停车场有3辆汽车,又来了2辆汽车,停车场有多少辆汽车?
A:停车场已经有3辆汽车。又来了2辆。现在有3 + 2 = 5辆汽车。答案是5。
Q:Leah有32块巧克力,她的姐姐有42块。如果他们吃了35块,他们总共还剩多少块?
A:Leah有32块巧克力,Leah的姐姐有42块。这意味着最初有32 + 42 = 74块巧克力。已经吃了35块。因此,他们总共还剩74-35 = 39块巧克力。答案是39。
Q:Jason有20个棒棒糖。他给Denny一些棒棒糖。现在Jason只有12个棒棒糖。Jason给Denny多少棒棒糖?
A:Jason有20个棒棒糖。因为他现在只有12个,所以他必须把剩下的给Denny。他给Denny的棒棒糖数量必须是20-12 = 8个棒棒糖。答案是8。
Q:Shawn有五个玩具。圣诞节,他从他的父母那里得到了两个玩具。他现在有多少个玩具?
A:他有5个玩具。他从妈妈那里得到了2个,所以在那之后他有5 + 2 = 7个玩具。然后他从爸爸那里得到了2个,所以总共他有7 + 2 = 9个玩具。答案是9。
Q:服务器房间里有9台计算机。从周一到周四,每天都会安装5台计算机。现在服务器房间里有多少台计算机?
A:从周一到周四有4天。每天都添加了5台计算机。这意味着总共添加了4 * 5 =20台计算机。一开始有9台计算机,所以现在有9 + 20 = 29台计算机。答案是29。
Q:Michael有58个高尔夫球。星期二,他丢失了23个高尔夫球。星期三,他又丢失了2个。星期三结束时他还剩多少个高尔夫球?
A:Michael最初有58个球。星期二他丢失了23个,所以在那之后他有58-23 = 35个球。星期三他又丢失了2个,所以现在他有35-2 = 33个球。答案是33。
Q:Olivia有23美元。她用每个3美元的价格买了五个百吉饼。她还剩多少钱?
A:她用每个3美元的价格买了5个百吉饼。这意味着她花了15美元。她还剩8美元。
Q:当我6岁时,我的妹妹是我的一半年龄。现在我70岁了,我的妹妹多大?
A:

输出 1:

当我6岁时,我的妹妹是我的一半年龄,也就是3岁。现在我70岁了,所以她是70-3 = 67岁。答案是67。

输出 2:

当叙述者6岁时,他的妹妹是他年龄的一半,也就是3岁。现在叙述者70岁了,他的妹妹应该是70-3 = 67岁。答案是67。

输出 3:

当我6岁时,我的妹妹是我的一半年龄,也就是3岁。现在我70岁了,所以她是70/2 = 35岁。答案是35。

附录

image-20250626232345557

生成知识提示

GENKNOW

对应的论文链接

链式提示

为了提高大语言模型的性能使其更可靠,一个重要的提示工程技术是将任务分解为许多子任务。 确定子任务后,将子任务的提示词提供给语言模型,得到的结果作为新的提示词的一部分。 这就是所谓的链式提示(prompt chaining),一个任务被分解为多个子任务,根据子任务创建一系列提示操作。

链式提示可以完成很复杂的任务。LLM 可能无法仅用一个非常详细的提示完成这些任务。在链式提示中,提示链对生成的回应执行转换或其他处理,直到达到期望结果。

除了提高性能,链式提示还有助于提高 LLM 应用的透明度,增加控制性和可靠性。这意味着您可以更容易地定位模型中的问题,分析并改进需要提高的不同阶段的性能。

提示链可以用于不同的场景,这些场景可能涉及多个操作或转换。例如,LLM 的一个常见用途是根据大型文本文档回答问题。想要更好阅读大文本文档,可以设计两个不同的提示,第一个提示负责提取相关引文以回答问题,第二个提示则以引文和原始文档为输入来回答给定的问题。换句话说,可以创建两个不同的提示来执行根据文档回答问题的任务。

提示 1:

你是一个很有帮助的助手。你的任务是根据文档回答问题。第一步是从文档中提取与问题相关的引文,由####分隔。请使用<quotes></quotes>输出引文列表。如果没有找到相关引文,请回应“未找到相关引文!”。

####
{{文档}}
####

提示 1 的输出:

<quotes>
- Chain-of-thought (CoT) prompting[27]
- Generated knowledge prompting[37]
- Least-to-most prompting[38]
- Self-consistency decoding[39]
- Complexity-based prompting[41]
- Self-refine[42]
- Tree-of-thought prompting[43]
- Maieutic prompting[45]
- Directional-stimulus prompting[46]
- Textual inversion and embeddings[59]
- Using gradient descent to search for prompts[61][62][63][64]
- Prompt injection[65][66][67]
</quotes>

在第一个提示中返回的引文现在可以用作下面第二个提示的输入。您可以对这些引文进行清理,比如移除引用标志。可以在提示链中新建另一个提示来移除或使用这些引用标志,但现在您可以忽略这些。然后,第二个提示接收由第一个提示提取的相关引文,并根据文档和这些提取的引文生成一个有帮助的回答。第二个提示可以是以下内容:

提示 2:

根据从文档中提取的相关引文(由<quotes></quotes>分隔)和原始文档(由####分隔),请构建对问题的回答。请确保答案准确、语气友好且有帮助。

####
{{文档}}
####

<quotes>
- Chain-of-thought (CoT) prompting[27]
- Generated knowledge prompting[37]
- Least-to-most prompting[38]
- Self-consistency decoding[39]
- Complexity-based prompting[41]
- Self-refine[42]
- Tree-of-thought prompting[43]
- Maieutic prompting[45]
- Directional-stimulus prompting[46]
- Textual inversion and embeddings[59]
- Using gradient descent to search for prompts[61][62][63][64]
- Prompt injection[65][66][67]
</quotes>

提示 2 的输出:

文档中提到的提示技术包括:

1. Chain-of-thought (CoT) prompting[27]
2. Generated knowledge prompting[37]
3. Least-to-most prompting[38]
4. Self-consistency decoding[39]
5. Complexity-based prompting[41]
6. Self-refine[42]
7. Tree-of-thought prompting[43]
8. Maieutic prompting[45]
9. Directional-stimulus prompting[46]
10. Textual inversion and embeddings[59]
11. Using gradient descent to search for prompts[61][62][63][64]
12. Prompt injection[65][66][67]

每一种技术都采用独特的策略来增强或明确与大型语言模型的互动,以产生期望的结果。

思维树 (ToT)

对于需要探索或预判战略的复杂任务来说,传统或简单的提示技巧是不够的。最近,Yao et el. (2023)(opens in a new tab) 提出了思维树(Tree of Thoughts,ToT)框架,该框架基于思维链提示进行了总结,引导语言模型探索把思维作为中间步骤来解决通用问题。

ToT 维护着一棵思维树,思维由连贯的语言序列表示,这个序列就是解决问题的中间步骤。使用这种方法,LM 能够自己对严谨推理过程的中间思维进行评估。LM 将生成及评估思维的能力与搜索算法(如广度优先搜索和深度优先搜索)相结合,在系统性探索思维的时候可以向前验证和回溯。

ToT 框架原理如下:

TOT

ToT 需要针对不同的任务定义思维/步骤的数量以及每步的候选项数量。例如,论文中的“算 24 游戏”是一种数学推理任务,需要分成 3 个思维步骤,每一步都需要一个中间方程。而每个步骤保留最优的(best) 5 个候选项。

ToT 完成算 24 的游戏任务要执行广度优先搜索(BFS),每步思维的候选项都要求 LM 给出能否得到 24 的评估:“sure/maybe/impossible”(一定能/可能/不可能) 。作者讲到:“目的是得到经过少量向前尝试就可以验证正确(sure)的局部解,基于‘太大/太小’的常识消除那些不可能(impossible)的局部解,其余的局部解作为‘maybe’保留。”每步思维都要抽样得到 3 个评估结果。整个过程如下图所示:

TOT2

从下图中报告的结果来看,ToT 的表现大大超过了其他提示方法:

TOT3

从大方向上来看,Yao et el. (2023)(opens in a new tab)Long (2023)(opens in a new tab) 的核心思路是类似的。两种方法都是以多轮对话搜索树的形式来增强 LLM 解决复杂问题的能力。主要区别在于 Yao et el. (2023)(opens in a new tab) 采用了深度优先(DFS)/广度优先(BFS)/集束(beam)搜索,而 Long (2023)(opens in a new tab) 则提出由强化学习(Reinforcement Learning)训练出的 “ToT 控制器”(ToT Controller)来驱动树的搜索策略(包括什么时候回退和搜索到哪一级回退等等)。深度优先/广度优先/集束搜索是通用搜索策略,并不针对具体问题。相比之下,由强化学习训练出的 ToT 控制器有可能从新的数据集学习,或是在自对弈(AlphaGo vs. 蛮力搜索)的过程中学习。因此,即使采用的是冻结的 LLM,基于强化学习构建的 ToT 系统仍然可以不断进化,学习新的知识。

Hulbert (2023)(opens in a new tab) 提出了思维树(ToT)提示法,将 ToT 框架的主要概念概括成了一段简短的提示词,指导 LLM 在一次提示中对中间思维做出评估。ToT 提示词的例子如下:

假设三位不同的专家来回答这个问题。
所有专家都写下他们思考这个问题的第一个步骤,然后与大家分享。
然后,所有专家都写下他们思考的下一个步骤并分享。
以此类推,直到所有专家写完他们思考的所有步骤。
只要大家发现有专家的步骤出错了,就让这位专家离开。
请问...

检索增强生成 (RAG)

通用语言模型通过微调就可以完成几类常见任务,比如分析情绪和识别命名实体。这些任务不需要额外的背景知识就可以完成。

要完成更复杂和知识密集型的任务,可以基于语言模型构建一个系统,访问外部知识源来做到。这样的实现与事实更加一性,生成的答案更可靠,还有助于缓解“幻觉”问题。

Meta AI 的研究人员引入了一种叫做检索增强生成(Retrieval Augmented Generation,RAG)(opens in a new tab)的方法来完成这类知识密集型的任务。RAG 把一个信息检索组件和文本生成模型结合在一起。RAG 可以微调,其内部知识的修改方式很高效,不需要对整个模型进行重新训练。

RAG 会接受输入并检索出一组相关/支撑的文档,并给出文档的来源(例如维基百科)。这些文档作为上下文和输入的原始提示词组合,送给文本生成器得到最终的输出。这样 RAG 更加适应事实会随时间变化的情况。这非常有用,因为 LLM 的参数化知识是静态的。RAG 让语言模型不用重新训练就能够获取最新的信息,基于检索生成产生可靠的输出。

Lewis 等人(2021)提出一个通用的 RAG 微调方法。这种方法使用预训练的 seq2seq 作为参数记忆,用维基百科的密集向量索引作为非参数记忆(使通过神经网络预训练的检索器访问)。这种方法工作原理概况如下:

RAG

RAG 在 Natural Questions(opens in a new tab)WebQuestions(opens in a new tab) 和 CuratedTrec 等基准测试中表现抢眼。用 MS-MARCO 和 Jeopardy 问题进行测试时,RAG 生成的答案更符合事实、更具体、更多样。FEVER 事实验证使用 RAG 后也得到了更好的结果。

这说明 RAG 是一种可行的方案,能在知识密集型任务中增强语言模型的输出。

LangChain 代码示例:

from typing import Literal

import bs4
from langchain import hub
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langgraph.graph import START, StateGraph
from typing_extensions import Annotated, List, TypedDict

# Load and chunk contents of the blog
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
docs = loader.load()

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
all_splits = text_splitter.split_documents(docs)


# Update metadata (illustration purposes)
total_documents = len(all_splits)
third = total_documents // 3

for i, document in enumerate(all_splits):
if i < third:
document.metadata["section"] = "beginning"
elif i < 2 * third:
document.metadata["section"] = "middle"
else:
document.metadata["section"] = "end"


# Index chunks
vector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(all_splits)


# Define schema for search
class Search(TypedDict):
"""Search query."""

query: Annotated[str, ..., "Search query to run."]
section: Annotated[
Literal["beginning", "middle", "end"],
...,
"Section to query.",
]

# Define prompt for question-answering
prompt = hub.pull("rlm/rag-prompt")


# Define state for application
class State(TypedDict):
question: str
query: Search
context: List[Document]
answer: str


def analyze_query(state: State):
structured_llm = llm.with_structured_output(Search)
query = structured_llm.invoke(state["question"])
return {"query": query}


def retrieve(state: State):
query = state["query"]
retrieved_docs = vector_store.similarity_search(
query["query"],
filter=lambda doc: doc.metadata.get("section") == query["section"],
)
return {"context": retrieved_docs}


def generate(state: State):
docs_content = "\n\n".join(doc.page_content for doc in state["context"])
messages = prompt.invoke({"question": state["question"], "context": docs_content})
response = llm.invoke(messages)
return {"answer": response.content}


graph_builder = StateGraph(State).add_sequence([analyze_query, retrieve, generate])
graph_builder.add_edge(START, "analyze_query")
graph = graph_builder.compile()
for step in graph.stream(
{"question": "What does the end of the post say about Task Decomposition?"},
stream_mode="updates",
):
print(f"{step}\n\n----------------\n")
{'analyze_query': {'query': {'query': 'Task Decomposition', 'section': 'end'}}}

----------------

{'retrieve': {'context': [Document(id='d6cef137-e1e8-4ddc-91dc-b62bd33c6020', metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/', 'start_index': 39221, 'section': 'end'}, page_content='Finite context length: The restricted context capacity limits the inclusion of historical information, detailed instructions, API call context, and responses. The design of the system has to work with this limited communication bandwidth, while mechanisms like self-reflection to learn from past mistakes would benefit a lot from long or infinite context windows. Although vector stores and retrieval can provide access to a larger knowledge pool, their representation power is not as powerful as full attention.\n\n\nChallenges in long-term planning and task decomposition: Planning over a lengthy history and effectively exploring the solution space remain challenging. LLMs struggle to adjust plans when faced with unexpected errors, making them less robust compared to humans who learn from trial and error.'), Document(id='d1834ae1-eb6a-43d7-a023-08dfa5028799', metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/', 'start_index': 39086, 'section': 'end'}, page_content='}\n]\nChallenges#\nAfter going through key ideas and demos of building LLM-centered agents, I start to see a couple common limitations:'), Document(id='ca7f06e4-2c2e-4788-9a81-2418d82213d9', metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/', 'start_index': 32942, 'section': 'end'}, page_content='}\n]\nThen after these clarification, the agent moved into the code writing mode with a different system message.\nSystem message:'), Document(id='1fcc2736-30f4-4ef6-90f2-c64af92118cb', metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/', 'start_index': 35127, 'section': 'end'}, page_content='"content": "You will get instructions for code to write.\\nYou will write a very long answer. Make sure that every detail of the architecture is, in the end, implemented as code.\\nMake sure that every detail of the architecture is, in the end, implemented as code.\\n\\nThink step by step and reason yourself to the right decisions to make sure we get it right.\\nYou will first lay out the names of the core classes, functions, methods that will be necessary, as well as a quick comment on their purpose.\\n\\nThen you will output the content of each file including ALL code.\\nEach file must strictly follow a markdown code block format, where the following tokens must be replaced such that\\nFILENAME is the lowercase file name including the file extension,\\nLANG is the markup code block language for the code\'s language, and CODE is the code:\\n\\nFILENAME\\n\`\`\`LANG\\nCODE\\n\`\`\`\\n\\nYou will start with the \\"entrypoint\\" file, then go to the ones that are imported by that file, and so on.\\nPlease')]}}

----------------

{'generate': {'answer': 'The end of the post highlights that task decomposition faces challenges in long-term planning and adapting to unexpected errors. LLMs struggle with adjusting their plans, making them less robust compared to humans who learn from trial and error. This indicates a limitation in effectively exploring the solution space and handling complex tasks.'}}

----------------

自动推理并使用工具 (ART)

使用 LLM 完成任务时,交替运用 CoT 提示和工具已经被证明是一种即强大又稳健的方法。这类方法通常需要针对特定任务手写示范,还需要精心编写交替使用生成模型和工具的脚本。Paranjape et al., (2023)(opens in a new tab)提出了一个新框架,该框架使用冻结的 LLM 来自动生成包含中间推理步骤的程序。

ART(Automatic Reasoning and Tool-use)的工作原理如下:

  • 接到一个新任务的时候,从任务库中选择多步推理和使用工具的示范。
  • 在测试中,调用外部工具时,先暂停生成,将工具输出整合后继续接着生成。

ART 引导模型总结示范,将新任务进行拆分并在恰当的地方使用工具。ART 采用的是零样本形式。ART 还可以手动扩展,只要简单地更新任务和工具库就可以修正推理步骤中的错误或是添加新的工具。这个过程如下:

ART

自动提示工程师(APE)

APE

Zhou等人,(2022)(opens in a new tab) 提出了自动提示工程师 (APE),这是一个用于自动指令生成和选择的框架。指令生成问题被构建为自然语言合成问题,使用 LLMs 作为黑盒优化问题的解决方案来生成和搜索候选解。

第一步涉及一个大型语言模型(作为推理模型),该模型接收输出演示以生成任务的指令候选项。这些候选解将指导搜索过程。使用目标模型执行指令,然后根据计算的评估分数选择最合适的指令。

APE 发现了一个比人工设计的“让我们一步一步地思考”提示更好的零样本 CoT 提示 (Kojima 等人,2022(opens in a new tab))。

提示“让我们一步一步地解决这个问题,以确保我们有正确的答案。”引发了思维链的推理,并提高了 MultiArith 和 GSM8K 基准测试的性能:

APECOT

Active-Prompt

思维链(CoT)方法依赖于一组固定的人工注释范例。问题在于,这些范例可能不是不同任务的最有效示例。为了解决这个问题,Diao 等人(2023)(opens in a new tab)最近提出了一种新的提示方法,称为 Active-Prompt,以适应 LLMs 到不同的任务特定示例提示(用人类设计的 CoT 推理进行注释)。

下面是该方法的说明。第一步是使用或不使用少量 CoT 示例查询 LLM。对一组训练问题生成 k 个可能的答案。基于 k 个答案计算不确定度度量(使用不一致性)。选择最不确定的问题由人类进行注释。然后使用新的注释范例来推断每个问题。

ACTIVE

方向性刺激提示

Li 等人,(2023)(opens in a new tab)提出了一种新的提示技术,以更好地指导 LLM 生成所需的摘要。

训练了一个可调节的策略 LM 来生成刺激/提示。越来越多地使用RL来优化 LLM。

下图显示了方向性刺激提示与标准提示的比较。策略 LM 可以很小,并且可以优化以生成指导黑盒冻结 LLM 的提示。

DSP

PAL(程序辅助语言模型)

Gao 等人(2022)(opens in a new tab)提出了一种使用 LLMs 读取自然语言问题并生成程序作为中间推理步骤的方法。被称为程序辅助语言模型(PAL),它与思维链提示不同,因为它不是使用自由形式文本来获得解决方案,而是将解决步骤卸载到类似 Python 解释器的编程运行时中。

PAL

图片来源:Gao 等人(2022)(opens in a new tab)

让我们以 LangChain 和 OpenAI GPT-3 为例。我们有兴趣开发一个简单的应用程序,它能够解释所提出的问题,并利用 Python 解释器提供答案。

具体来说,我们有兴趣创建一个功能,允许使用 LLM 回答需要日期理解的问题。我们将为 LLM 提供一个提示,其中包括一些示例,这些示例是从这里(opens in a new tab)采用的。

这是我们需要导入的包:

import openai
from datetime import datetime
from dateutil.relativedelta import relativedelta
import os
from langchain.llms import OpenAI
from dotenv import load_dotenv

配置环境:

load_dotenv()

# API configuration
openai.api_key = os.getenv("OPENAI_API_KEY")

# for LangChain
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")

设置模型实例:

llm = OpenAI(model_name='text-davinci-003', temperature=0)

设置提示+问题:

question = "Today is 27 February 2023. I was born exactly 25 years ago. What is the date I was born in MM/DD/YYYY?"

DATE_UNDERSTANDING_PROMPT = """
# Q: 2015 is coming in 36 hours. What is the date one week from today in MM/DD/YYYY?
# If 2015 is coming in 36 hours, then today is 36 hours before.
today = datetime(2015, 1, 1) - relativedelta(hours=36)
# One week from today,
one_week_from_today = today + relativedelta(weeks=1)
# The answer formatted with %m/%d/%Y is
one_week_from_today.strftime('%m/%d/%Y')
# Q: The first day of 2019 is a Tuesday, and today is the first Monday of 2019. What is the date today in MM/DD/YYYY?
# If the first day of 2019 is a Tuesday, and today is the first Monday of 2019, then today is 6 days later.
today = datetime(2019, 1, 1) + relativedelta(days=6)
# The answer formatted with %m/%d/%Y is
today.strftime('%m/%d/%Y')
# Q: The concert was scheduled to be on 06/01/1943, but was delayed by one day to today. What is the date 10 days ago in MM/DD/YYYY?
# If the concert was scheduled to be on 06/01/1943, but was delayed by one day to today, then today is one day later.
today = datetime(1943, 6, 1) + relativedelta(days=1)
# 10 days ago,
ten_days_ago = today - relativedelta(days=10)
# The answer formatted with %m/%d/%Y is
ten_days_ago.strftime('%m/%d/%Y')
# Q: It is 4/19/1969 today. What is the date 24 hours later in MM/DD/YYYY?
# It is 4/19/1969 today.
today = datetime(1969, 4, 19)
# 24 hours later,
later = today + relativedelta(hours=24)
# The answer formatted with %m/%d/%Y is
today.strftime('%m/%d/%Y')
# Q: Jane thought today is 3/11/2002, but today is in fact Mar 12, which is 1 day later. What is the date 24 hours later in MM/DD/YYYY?
# If Jane thought today is 3/11/2002, but today is in fact Mar 12, then today is 3/12/2002.
today = datetime(2002, 3, 12)
# 24 hours later,
later = today + relativedelta(hours=24)
# The answer formatted with %m/%d/%Y is
later.strftime('%m/%d/%Y')
# Q: Jane was born on the last day of Feburary in 2001. Today is her 16-year-old birthday. What is the date yesterday in MM/DD/YYYY?
# If Jane was born on the last day of Feburary in 2001 and today is her 16-year-old birthday, then today is 16 years later.
today = datetime(2001, 2, 28) + relativedelta(years=16)
# Yesterday,
yesterday = today - relativedelta(days=1)
# The answer formatted with %m/%d/%Y is
yesterday.strftime('%m/%d/%Y')
# Q: {question}
""".strip() + '\n'
llm_out = llm(DATE_UNDERSTANDING_PROMPT.format(question=question))
print(llm_out)

这将输出以下内容:

# If today is 27 February 2023 and I was born exactly 25 years ago, then I was born 25 years before.
today = datetime(2023, 2, 27)
# I was born 25 years before,
born = today - relativedelta(years=25)
# The answer formatted with %m/%d/%Y is
born.strftime('%m/%d/%Y')

llm_out 是一段 python 代码,我们可以使用 exec 执行它:

exec(llm_out)
print(born)

这将输出以下内容:02/27/1998

ReAct 框架

AI基础概念里面已经说过基本原理,这里仅举例

长链 ReAct 的使用

%%capture
# 更新或安装必要的库
!pip install --upgrade openai
!pip install --upgrade langchain
!pip install --upgrade python-dotenv
!pip install google-search-results

# 引入库
import openai
import os
from langchain.llms import OpenAI
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from dotenv import load_dotenv
load_dotenv()

# 载入 API keys; 如果没有,你需要先获取。
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
os.environ["SERPER_API_KEY"] = os.getenv("SERPER_API_KEY")

现在我们可以配置 LLM,我们要用到的工具,以及允许我们将 ReAct 框架与 LLM 和其他工具结合使用的代理。请注意,我们使用搜索 API 来搜索外部信息,并使用 LLM 作为数学工具。

llm = OpenAI(model_name="text-davinci-003" ,temperature=0)
tools = load_tools(["google-serper", "llm-math"], llm=llm)
agent = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True)

配置好之后,我们就可以用所需的查询或提示运行代理了。请注意,在这里,我们不会像论文中阐释的那样提供少样本的示例。

agent.run("奥利维亚·王尔德的男朋友是谁?他现在的年龄的0.23次方是多少?")

链执行如下所示:

> 正在输入新代理执行器链......
我得查出奥利维亚·王尔德的男友是谁然后计算出他的年龄的 0.23 次方。
操作: 搜索
操作输入: “奥利维亚·王尔德的男友”
观察: 奥利维亚·王尔德与杰森·苏代基斯在多年前订婚,在他们分手后,她开始与哈里·斯泰尔斯约会 — 参照他们的关系时间线。
思考: 我需要找出哈里·斯泰尔斯的年龄。
操作: 搜索
操作输入: “哈里·斯泰尔斯的年龄”
观察: 29 岁
思考: 我需要计算 29 的 0.23 次方。
操作: 计算器
操作输入: 29^0.23
观察: 答案: 2.169459462491557

思考: 现在我知道最终答案了。
最终答案: 哈里·斯泰尔斯, 奥利维亚·王尔德的男朋友, 29 岁。他年龄的 0.23 次方是 2.169459462491557。

> 结束链。

我们得到如下输出:

"哈里·斯泰尔斯, 奥利维亚·王尔德的男朋友, 29 岁。他年龄的 0.23 次方是 2.169459462491557。"

参考LangChain文档

自我反思(Reflexion)

自我反思是一个通过语言反馈来强化基于语言的智能体的框架。根据 Shinn et al. (2023)(opens in a new tab),“自我反思是一种‘口头’强化的新范例,它将策略参数化为智能体的记忆编码与 LLM 的参数选择配对。”

在高层次上,自我反思将来自环境的反馈(自由形式的语言或者标量)转换为语言反馈,也被称作 self-reflection,为下一轮中 LLM 智能体提供上下文。这有助于智能体快速有效地从之前的错误中学习,进而提升许多高级任务的性能。

"自我反思框架"

如上图所示,自我反思由三个不同的模型组成:

  • 参与者(Actor):根据状态观测量生成文本和动作。参与者在环境中采取行动并接受观察结果,从而形成轨迹。链式思考(CoT)(opens in a new tab)ReAct(opens in a new tab) 被用作参与者模型。此外,还添加了记忆组件为智能体提供额外的上下文信息。
  • 评估者(Evaluator):对参与者的输出进行评价。具体来说,它将生成的轨迹(也被称作短期记忆)作为输入并输出奖励分数。根据人物的不同,使用不同的奖励函数(决策任务使用LLM和基于规则的启发式奖励)。
  • 自我反思(Self-Reflection):生成语言强化线索来帮助参与者实现自我完善。这个角色由大语言模型承担,能够为未来的试验提供宝贵的反馈。自我反思模型利用奖励信号、当前轨迹和其持久记忆生成具体且相关的反馈,并存储在记忆组件中。智能体利用这些经验(存储在长期记忆中)来快速改进决策。

总的来说,自我反思的关键步骤是a)定义任务,b)生成轨迹,c)评估,d)执行自我反思,e)生成下一条轨迹。下图展示了自我反思的智能体学习迭代优化其行为来解决决策、编程和推理等各种人物的例子。自我反思(Refelxion)通过引入自我评估、自我反思和记忆组件来拓展 ReAct 框架。

"Reflexion 示例"

何时自我反思?

自我反思最适合以下情况:

  1. 智能体需要从尝试和错误中学习:自我反思旨在通过反思过去的错误并将这些知识纳入未来的决策来帮助智能体提高表现。这非常适合智能体需要通过反复试验来学习的任务,例如决策、推理和编程。
  2. 传统的强化学习方法失效:传统的强化学习(RL)方法通常需要大量的训练数据和昂贵的模型微调。自我反思提供了一种轻量级替代方案,不需要微调底层语言模型,从而使其在数据和计算资源方面更加高效。
  3. 需要细致入微的反馈:自我反思利用语言反馈,这比传统强化学习中使用的标量奖励更加细致和具体。这让智能体能够更好地了解自己的错误,并在后续的试验中做出更有针对性的改进。
  4. 可解释性和直接记忆很重要:与传统的强化学习方法相比,自我反思提供了一种更可解释、更直接的情景记忆形式。智能体的自我反思存储在其记忆组件中,让分析和理解其学习过程变得更加简单。

自我反思在以下任务中是有效的:

  • 序列决策:自我反思提高了智能体在 AlfWorld 任务中的表现,涉及在各种环境中导航并完成多步目标。
  • 推理:自我反思提高了 HotPotQA 上智能体的性能,HotPotQA 是一个需要对多个文档进行推理的问答数据集。
  • 编程:自我反思的智能体在 HumanEval 和 MBPP 等基准测试上编写出了更好的代码,在某些情况下实现 SOTA 结果。

以下是自我反思的一些限制:

  • 依赖自我评估能力:反思依赖于智能体准确评估其表现并产生有用反思的能力。这可能是具有挑战性的,尤其是对于复杂的任务,但随着模型功能的不断改进,预计自我反思会随着时间的推移而变得更好。
  • 长期记忆限制:自我反思使用最大容量的滑动窗口,但对于更复杂的任务,使用向量嵌入或 SQL 数据库等高级结构可能会更有利。
  • 代码生成限制:测试驱动开发在指定准确的输入输出映射方面存在限制(例如,受硬件影响的非确定性生成器函数和函数输出)。