随着能够处理复杂任务的大型语言模型(LLM)的崛起,上下文工程受到了广泛关注。最初,关于这一话题的讨论大多围绕提示工程展开:为单一任务调整单个提示以获得最佳性能。然而,随着LLM能力的增强,提示工程已经演变为上下文工程:优化输入LLM的所有数据,以在复杂任务上实现最大性能。
本文将深入探讨智能体上下文工程,即专门为智能体优化上下文。这与传统的上下文工程不同,因为智能体通常需要执行更长时间的任务序列。鉴于智能体上下文工程是一个广泛的话题,本文将深入探讨以下列出的主题,并撰写后续文章涵盖更多内容。
- 具体的上下文工程技巧
- 缩短/总结上下文
- 工具使用
在深入探讨上下文工程的具体细节之前,首先说明为什么智能体上下文工程很重要。我将从两个方面进行阐述:
- 为什么使用智能体
- 为什么智能体需要上下文工程
为什么使用智能体
首先,我们使用智能体是因为它们比静态的LLM调用更能胜任某些任务。智能体可以接收用户的查询,例如:修复这个用户报告的Bug {bug报告}。这在单次LLM调用中是不可行的,因为你可能需要更好地理解Bug(也许需要询问报告Bug的人),需要理解Bug出现在代码的哪个位置,并且可能需要获取一些错误信息。这就是智能体的用武之地。
智能体可以查看Bug,调用工具向用户提出后续问题,例如:这个Bug发生在应用程序的哪个位置?然后,智能体可以在代码库中找到该位置,运行代码以读取错误日志,并实施修复。所有这些都需要在解决问题之前进行一系列的LLM调用和工具调用。
为什么智能体需要上下文工程
现在我们知道了为什么需要智能体,但为什么智能体需要上下文工程呢?主要原因是,当LLM的上下文包含更多相关信息且噪音(不相关信息)更少时,其性能总是更好。此外,当智能体执行一系列工具调用时(例如,在发生Bug时获取错误日志),其上下文会迅速累积。这会导致上下文膨胀,即LLM的上下文包含大量不相关信息。我们需要从LLM的上下文中移除这些噪音信息,并确保所有相关信息都存在于LLM的上下文中。
具体的上下文工程技巧
智能体上下文工程建立在传统上下文工程的基础之上。因此,我提出几个改进上下文的重要点:
- 少样本学习
- 结构化提示
- 逐步推理
这些是上下文工程中常用的三种技术,通常能提高LLM的性能。
少样本学习
少样本学习是一种常用方法,即在你将任务交给智能体执行之前,提供类似任务的示例。这有助于模型更好地理解任务,通常能提高性能。
下面你可以看到两个提示示例。第一个示例展示了一个零样本提示,我们直接向LLM提问。考虑到这是一个简单的任务,LLM很可能得出正确答案;然而,对于更困难的任务,少样本学习会有更大的效果。在第二个提示中,你可以看到我提供了几个如何计算数学的例子,这些例子也用XML标签包裹。这不仅帮助模型理解它正在执行什么任务,还有助于确保答案格式的一致性,因为模型通常会以与少样本示例中提供的相同格式进行响应。
# zero-shot
prompt = "123+150等于多少?"
# few-shot
prompt = """
<example>"10+20等于多少?" -> "30" </example>
<example>"120+70等于多少?" -> "190" </example>
123+150等于多少?
"""
结构化提示
拥有结构化提示也是上下文工程中极其重要的一部分。在上面的代码示例中,你可以看到我使用了<example> … </example>这样的XML标签。你也可以使用Markdown格式化来增强提示的结构。我经常发现,先写一个提示的总体大纲,然后将其输入LLM进行优化和适当的组织,是设计良好提示的好方法。
你可以使用指定的工具进行提示优化,也可以简单地将非结构化的提示输入ChatGPT并要求它改进你的提示。此外,如果你描述当前提示在哪些情况下遇到困难,你会得到更好的提示。
例如,如果你有一个数学智能体,在加法、减法和除法方面表现很好,但在乘法方面有困难,你应该将这些信息添加到你的提示优化器中。
逐步推理
逐步推理是另一种强大的上下文工程方法。你提示LLM在尝试解决问题之前,先一步一步地思考如何解决问题。为了获得更好的上下文工程效果,你可以将本节涵盖的所有三种方法结合起来,如下例所示:
# few-shot + structured + step-by-step reasoning
prompt = """
<example>"10+20等于多少?" -> "为了回答用户的请求,我必须把这两个数字加起来。我可以这样做:先加每个数字的最后两位:0+0=0。然后我再把最后两位加起来得到1+2=3。答案是:30" </example>
<example>"120+70等于多少?" -> "为了回答用户的请求,我必须从后往前把数字加起来。我从0+0=0开始。然后做2+7=9,最后做1+0=1。答案是:190" </example>
123+150等于多少?
"""
这将有助于模型更好地理解示例,通常能进一步提高模型性能。
缩短上下文
当你的智能体已经运行了几个步骤,例如请求用户输入、获取一些信息等,你可能会遇到LLM上下文填满的情况。在达到上下文限制并丢失所有超过此限制的令牌之前,你应该缩短上下文。
总结是缩短上下文的好方法;然而,总结有时会切掉上下文的重要部分。你的上下文的前半部分可能不包含任何有用信息,而后半部分则包含几个必需的段落。这也是智能体上下文工程困难的部分原因。
为了执行上下文缩短,你通常会使用另一个LLM,我将其称为缩短LLM。这个LLM接收上下文并返回其缩短版本。缩短LLM最简单的版本只是总结上下文并返回。但是,你可以采用以下技术来改进缩短效果:
- 确定是否可以将上下文的某些完整部分切除(特定文档、先前的工具调用等)
- 一个为分析当前任务而优化的提示调优缩短LLM,它能考虑所有可用相关信息,并仅返回与解决任务相关的信息
确定是否可以切除完整部分
在尝试缩短上下文时,你应该做的第一件事是找到可以完全切除的上下文区域。
例如,LLM之前可能获取了一个用于解决先前任务的文档,并且你已经有了任务结果。这意味着该文档不再相关,应该从上下文中移除。如果LLM获取了其他信息(例如通过关键词搜索),并且LLM自己总结了搜索的输出,也可能发生这种情况。在这种情况下,你应该从上下文中移除旧的搜索输出。
简单地移除这些完整的上下文部分可以大大缩短上下文。然而,你需要记住,移除可能在以后任务中相关的上下文可能不利于智能体的性能。
因此,正如某机构在他们的上下文工程文章中所指出的那样,你应该首先优化召回率,确保LLM缩短器永远不会移除未来相关的上下文。当你达到近乎完美的召回率时,可以开始关注精确率,即逐步移除与解决当前任务不再相关的上下文。
提示调优的缩短LLM
我还建议创建一个提示调优的缩短LLM。为此,你首先需要创建一个上下文测试集,以及给定当前任务所需的缩短上下文。这些示例最好是从与你的智能体的真实用户交互中获取。
接下来,你可以针对总结LLM上下文的任务进行提示优化(甚至微调)缩短LLM,以保留上下文的重要部分,同时移除不再相关的其他部分。
工具
区分智能体与一次性LLM调用的主要点之一在于它们对工具的使用。我们通常为智能体提供一系列工具,以增强其解决任务的能力。这类工具的示例包括:
- 对文档语料库执行关键词搜索
- 根据电子邮件获取用户信息
- 将数字相加的计算器
这些工具简化了智能体必须解决的问题。智能体可以执行关键词搜索以获取额外的(通常是必需的)信息,或者可以使用计算器将数字相加,这比使用下一个令牌预测来相加数字要一致得多。
在将工具置于智能体的上下文中时,请记住一些技术以确保正确的工具使用:
- 描述清晰的工具(人类能理解吗?)
- 创建特定的工具
- 避免膨胀
- 仅显示相关工具
- 信息丰富的错误处理
描述清晰的智能体工具
第一个,也可能是最重要的注意事项,是让系统中的工具描述清晰。你定义的工具应该为所有输入参数和返回类型提供类型注释。它还应该有一个好的函数名和文档字符串中的描述。下面你可以看到一个糟糕的工具定义与一个好的工具定义的示例:
# 糟糕的工具定义
def calculator(a, b):
return a+b
# 良好的工具定义
def add_numbers(a: float, b: float) -> float:
"""将两个数字相加的函数。在你需要将两个数字相加的任何时候使用。
参数:
a: float
b: float
返回值
float
"""
return a+b
上面代码中的第二个函数更容易让智能体理解。正确描述工具将使智能体更擅长理解何时使用该工具,以及何时其他方法更好。
描述清晰的工具的衡量标准是:
一个以前从未见过这些工具的人类,仅通过查看函数及其定义,就能理解这些工具吗?
特定工具
你还应尝试使你的工具尽可能具体。当你定义模糊的工具时,LLM很难理解何时使用该工具并确保LLM正确使用它。
例如,与其为智能体定义一个从数据库获取信息的通用工具,不如提供提取特定信息的具体工具。
糟糕的工具:
- 从数据库获取信息
- 输入
- 要检索的列
- 用于查找信息的数据库索引
- 输入
更好的工具:
- 从数据库获取所有用户的信息(无输入参数)
- 获取属于给定客户ID的按日期排序的文档列表
- 获取所有用户及其过去24小时内所采取操作的聚合列表
然后,当你看到需要时,可以定义更具体的工具。这使得智能体更容易获取相关信息到其上下文中。
避免膨胀
你还应不惜一切代价避免膨胀。对于函数,有两种主要方法可以实现这一点:
- 函数应返回结构化输出,并可选择性地仅返回结果的子集
- 避免不相关的工具
对于第一点,我将再次使用关键词搜索的例子。在执行关键词搜索时,例如针对某中心的Elastic Search,你会收到大量信息,有时结构不那么清晰。
# 糟糕的函数返回
def keyword_search(search_term: str) -> str:
# 执行关键词搜索
# 结果: [{"id": ..., "content": ..., "createdAt": ..., ...}, {...}, {...}]
return str(results)
# 良好的函数返回
def _organize_keyword_output(results: list[dict], max_results: int) -> str:
output_string = ""
num_results = len(results)
for i, res in enumerate(results[:max_results]): # 最多返回 max_results 个结果
output_string += f"文档编号 {i}/{num_results}. ID: {res["id"]}, 内容: {res["content"]}, 创建于: {res["createdAt"]}"
return output_string
def keyword_search(search_term: str, max_results: int) -> str:
# 执行关键词搜索
# 结果: [{"id": ..., "content": ..., "createdAt": ..., ...}, {...}, {...}]
organized_results = _organize_keyword_output(results, max_results)
return organized_results
在糟糕的示例中,我们只是将关键词搜索返回的原始字典列表字符串化。更好的方法是使用一个单独的辅助函数将结果组织成结构化的字符串。
你还应确保模型可以仅返回结果的子集,如max_results参数所示。这对模型有很大帮助,特别是对于像关键词搜索这样的函数,它可能潜在地返回数百个结果,并立即填满LLM的上下文。
我的第二点是关于避免不相关的工具。你可能会遇到很多工具的情况,其中许多工具只会在特定步骤中对智能体相关。如果你知道某个工具在给定时间对智能体不相关,你应该将该工具排除在上下文之外。
信息丰富的错误处理
在为智能体提供工具时,信息丰富的错误处理至关重要。你需要帮助智能体理解它做错了什么。通常,Python提供的原始错误消息冗长且不易理解。
以下是工具中错误处理的一个好例子,其中智能体会被告知错误是什么以及如何处理。例如,当遇到速率限制错误时,我们告诉智能体在重试之前要休眠。这极大地简化了智能体的问题,因为它不需要自己推理必须休眠。
def keyword_search(search_term: str) -> str:
try:
# 关键词搜索
results = ...
return results
except requests.exceptions.RateLimitError as e:
return f"速率限制错误: {e}。你应该在重试前运行 time.sleep(10)。"
except requests.exceptions.ConnectionError as e:
return f"发生连接错误: {e}。网络可能已断开,请使用 inform_user 函数通知用户此问题。"
except requests.exceptions.HTTPError as e:
return f"发生HTTP错误: {e}。函数因HTTP错误而失败。这通常是由于访问问题引起的。请在使用此函数之前确保已进行验证"
except Exception as e:
return f"发生意外错误: {e}"
你应该为所有函数设置这样的错误处理,并记住以下几点:
- 错误消息应能说明发生了什么
- 如果你知道特定错误的解决方法(或潜在的解决方法),请告知LLM在发生错误时如何行动(例如:如果是速率限制错误,告诉模型运行 time.sleep())
智能体上下文工程的未来发展
在本文中,我涵盖了三个主要主题:具体的上下文工程技巧、缩短智能体的上下文以及如何为智能体提供工具。这些都是构建优秀AI智能体需要理解的基础主题。还有一些进一步的课题你应该了解更多,例如预计算信息或推理时信息检索的考虑。我将在未来的文章中讨论这个话题。智能体上下文工程将继续成为一个极其相关的主题,理解如何处理智能体的上下文对于未来AI智能体的发展至关重要。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
984

被折叠的 条评论
为什么被折叠?



