如何进行有效的代理上下文工程
如何进行有效的代理上下文工程
原文:
towardsdatascience.com/how-to-perform-effective-agentic-context-engineering/
在这篇文章中,我将深入探讨代理上下文工程,这是针对代理优化上下文的过程。这与传统的上下文工程不同,因为代理通常会在较长时间内执行一系列任务。由于代理上下文工程是一个大主题,我将在本文中深入探讨以下主题,并撰写后续文章来涵盖更多主题。
-
专门的上下文工程提示
-
缩短/总结上下文
-
工具使用
为什么应该关注代理上下文工程

这张信息图突出了本文的主要内容。我首先会讨论为什么你应该关注代理上下文工程。然后我会转向代理上下文工程中的具体主题,例如缩短上下文、上下文工程短提示和工具使用。图片由 ChatGPT 提供。
在深入探讨上下文工程的细节之前,我会介绍为什么代理上下文工程很重要。我会从两个方面来讨论这个问题:
-
为什么我们使用代理
-
为什么代理需要上下文工程
为什么我们使用代理
首先,我们使用代理是因为它们比静态 LLM 调用更有能力执行任务。代理可以接收用户的查询,例如:
修复这个用户报告的错误 {错误报告}
在单个 LLM 调用中,这是不可行的,因为你需要更好地理解这个错误(可能需要询问报告错误的人),你需要了解错误发生在代码的哪个位置,可能还需要获取一些错误信息。这就是代理发挥作用的地方。
代理可以查看错误,调用工具向用户提出后续问题,例如:这个错误发生在应用程序的哪个位置?然后代理可以在代码库中找到那个位置,运行代码本身来读取错误日志,并实施修复。这一切都需要在解决问题之前进行一系列的 LLM 调用和工具调用。
为什么代理需要上下文工程
因此,我们现在知道为什么我们需要代理,但为什么代理需要上下文工程呢?主要原因在于,当 LLM 的上下文包含更多相关信息和更少的噪声(无关信息)时,它们的表现总是更好。此外,当代理执行一系列工具调用时,例如在出现错误时获取错误日志,他们的上下文会迅速累积。这会导致上下文膨胀,即 LLM 的上下文中包含大量无关信息。我们需要从 LLM 的上下文中移除这些噪声信息,并确保所有相关信息都存在于 LLM 的上下文中。
特定的上下文工程技巧
代理上下文工程建立在传统上下文工程之上。因此,我包括一些重要点来提高您的上下文。
-
少样本学习
-
结构化提示
-
步骤推理
这些是在上下文工程中常用的三种技术,它们通常能提高大型语言模型(LLM)的性能。
少样本学习
少样本学习是一种常用的方法,在向代理提供它需要执行的任务之前,先包含一些类似任务的示例。这有助于模型更好地理解任务,通常会增加性能。
下面你可以看到两个提示示例。第一个示例显示了一个零样本提示,其中我们直接向 LLM 提问。考虑到这是一个简单的任务,LLM 很可能会得到正确答案;然而,在更困难的任务上,少样本学习将产生更大的影响。在第二个提示中,你可以看到我提供了几个数学操作的示例,这些示例也被包裹在 XML 标签中。这不仅有助于模型理解它正在执行的任务,还有助于确保一致的答案格式,因为模型通常会以与提供的少样本示例相同的格式进行响应。
# zero-shot
prompt = "What is 123+150?"
# few-shot
prompt = """
<example>"What is 10+20?" -> "30" </example>
<example>"What is 120+70?" -> "190" </example>
What is 123+150?
"""
结构化提示
结构化提示也是上下文工程中极其重要的一个部分。在上面的代码示例中,你可以看到我使用了 XML 标签
您可以使用指定的工具,如 Anthropic 的提示优化器,但您也可以简单地将您的非结构化提示输入到 ChatGPT 中,并要求它改进您的提示。此外,如果您描述了当前提示难以应对的场景,您将获得更好的提示。
例如,如果你有一个数学代理,它在加法、减法和除法方面做得很好,但在乘法方面有困难,你应该将这一信息添加到你的提示优化器中。
步骤推理
步骤推理是另一种强大的上下文工程方法。在尝试解决问题之前,你提示 LLM 逐步思考如何解决问题。为了获得更好的上下文工程,你可以结合本节中涵盖的所有三种方法,如下面的示例所示:
# few-shot + structured + step-by-step reasoning
prompt = """
<example>"What is 10+20?" -> "To answer the user request, I have to add up the two numbers. I can do this by first adding the last two digits of each number: 0+0=0\. I then add up the last two digits and get 1+2=3\. The answer is: 30" </example>
<example>"What is 120+70?" -> "To answer the euser request, I have to add up the digits going backwards to front. I start with: 0+0=0\. Then I do 2+7=9, and finally I do 1+0=1\. The answer is: 190" </example>
What is 123+150?
"""
这将帮助模型更好地理解示例,这通常还会进一步提高模型性能。
缩短上下文
当你的代理操作了几步,例如,请求用户输入,获取一些信息等,你可能会遇到 LLM 上下文填满的情况。在达到上下文限制并丢失所有超出限制的标记之前,你应该缩短上下文。
摘要是缩短上下文的绝佳方式;然而,摘要有时会裁剪掉上下文中的重要部分。你的上下文的前半部分可能不包含任何有用的信息,而后半部分则包含几个必要的段落。这也是为什么代理上下文工程困难的部分原因。
要执行上下文缩短,你通常会使用另一个 LLM,我将称之为缩短 LLM。这个 LLM 接收上下文并返回其缩短版本。缩短 LLM 最简单的版本只是总结上下文并返回。然而,你可以采用以下技术来提高缩短效果:
-
确定上下文中的一些整体部分是否可以被裁剪掉(特定的文档、之前的工具调用等)
-
一个针对当前任务优化的提示调整缩短 LLM,分析所有相关可用信息,并仅返回与解决任务相关的信息
确定是否可以裁剪掉整体部分
当你尝试缩短上下文时,首先应该找到可以完全裁剪掉的上下文区域。
例如,如果 LLM 可能之前获取了一个文档,用于解决之前的任务,其中你有任务结果。这意味着文档不再相关,应该从上下文中移除。这也可能发生在 LLM 通过关键词搜索获取其他信息的情况下,LLM 已经总结了搜索的输出。在这种情况下,你应该从关键词搜索中移除旧输出。
简单地移除这些整体上下文部分可以在缩短上下文方面取得很大进展。然而,你需要记住,移除可能对后续任务相关的上下文可能会对代理的性能产生不利影响。
因此,正如 Anthropic 在其关于上下文工程的文章中指出的,你应该首先优化召回率,确保 LLM 缩短器永远不会删除未来相关的上下文。当你几乎达到完美的召回率时,你就可以开始关注精确度,即删除更多不再与解决当前任务相关的上下文。

这张图突出了如何优化你的提示调整。首先,你专注于优化召回率,确保所有相关上下文在总结后仍然保留。然后在第二阶段,你开始专注于精确度,从代理的记忆中删除不那么相关的上下文。图片由 Google Gemini 提供。
提示调整的缩短 LLM
我还建议创建一个提示调整的缩短 LLM。为此,你首先需要创建一个测试集,包括上下文和所需的缩短上下文,给定一个任务。这些示例最好是从与你的代理的真实用户交互中获取的。
继续来说,你可以提示优化(甚至微调)用于总结 LLM 上下文的缩短 LLM,以保留重要的上下文部分,同时删除不再相关的上下文部分。
工具
将代理与一次性的 LLM 调用区分开来的主要点之一是它们使用工具。我们通常为代理提供一系列他们可以使用来提高代理解决任务能力的工具。此类工具的例子包括:
-
在文档语料库上执行关键词搜索
-
根据用户的电子邮件获取用户信息
-
一个用于相加数字的计算器
这些工具简化了代理需要解决的问题。代理可以进行关键词搜索以获取额外的(通常所需的)信息,或者可以使用计算器来相加数字,这比使用下一个标记预测来加数字更一致。
这里有一些技巧要记住,以确保在代理的上下文中提供工具时正确使用工具:
-
描述良好的工具(人类能否理解?)
-
创建特定的工具
-
避免膨胀
-
只显示相关的工具
-
信息的错误处理
描述良好的代理工具
第一,也许是最重要的注意事项,是在你的系统中拥有描述良好的工具。你定义的工具应该对所有输入参数有类型注解,并有一个返回类型。它还应该有一个好的函数名和 docstring 中的描述。以下是一个差的工具定义示例,与一个好的工具定义示例:
# poor tool definition
def calculator(a, b):
return a+b
# good tool definition
def add_numbers(a: float, b: float) -> float:
"""A function to add two numbers together. Should be used anytime you have to add two numbers together.
Takes in parameters:
a: float
b: float
Returns
float
"""
return a+b
上述代码中的第二个函数对代理来说更容易理解。正确描述工具将使代理更好地理解何时使用工具,以及何时其他方法更好。
描述良好的工具的基准是:
一个以前从未见过这些工具的人类,能否仅通过查看函数及其定义就理解这些工具?
特定的工具
你还应该尽量使你的工具尽可能具体。当你定义模糊的工具时,LLM 很难理解何时使用该工具,并确保 LLM 正确使用该工具。
例如,与其为代理定义一个从数据库中检索信息的通用工具,不如提供特定的工具来提取特定的信息。
差的工具:
-
从数据库中获取信息
-
输入
-
要检索的列
-
通过数据库索引查找信息
-
更好的工具:
-
从数据库中检索所有用户的信息(无输入参数)
-
获取属于给定客户 ID 的按日期排序的文档列表
-
获取过去 24 小时内所有用户及其所采取行动的汇总列表
当你需要更具体的工具时,你可以定义它们。这使得代理更容易将其相关信息的上下文检索出来。
避免膨胀
你应该不惜一切代价避免膨胀。有两种主要方法可以通过函数来实现这一点:
-
函数应返回结构化输出,并且可选地只返回结果的一个子集
-
避免无关的工具
对于第一个要点,我再次以关键词搜索为例。当执行关键词搜索时,例如,针对 AWS Elastic Search,你会收到很多信息,有时结构不是那么清晰。
# bad function return
def keyword_search(search_term: str) -> str:
# perform keyword search
# results: {"id": ..., "content": ..., "createdAt": ..., ...}, {...}, {...}]
return str(results)
# good function return
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 return max_results
output_string += f"Document number {i}/{num_results}. ID: {res["id"]}, content: {res["content"]}, created at: {res["createdAt"]}"
return output_string
def keyword_search(search_term: str, max_results: int) -> str:
# perform keyword search
# results: {"id": ..., "content": ..., "createdAt": ..., ...}, {...}, {...}]
organized_results = _organize_keyword_output(results, max_results)
return organized_results
在不好的例子中,我们只是将关键词搜索返回的原始字典列表转换为字符串。更好的方法是有一个单独的辅助函数来将结果结构化为有结构的字符串。
你还应该确保模型只能返回结果的一个子集,例如使用 max_results 参数所示。这对模型帮助很大,尤其是对于像关键词搜索这样的功能,它可能会返回 100 多个结果,立即填满 LLM 的上下文。
我的第二个要点是避免无关的工具。你可能遇到的情况是,你有许多工具,其中许多工具可能只适用于代理在特定步骤中使用。如果你知道某个工具在某个特定时间对代理不相关,你应该将工具排除在上下文之外。
信息丰富的错误处理
当为代理提供工具时,信息丰富的错误处理至关重要。你需要帮助代理理解它做错了什么。通常,Python 提供的原始错误消息膨胀且不易理解。
下面是工具中错误处理的一个很好的例子,其中代理被告知错误是什么以及如何处理它。例如,当遇到速率限制错误时,我们告诉代理在再次尝试之前具体地暂停。这大大简化了代理的问题,因为它不需要自己推理出它需要暂停。
def keyword_search(search_term: str) -> str:
try:
# keyword search
results = ...
return results
except requests.exceptions.RateLimitError as e:
return f"Rate limit error: {e}. You should run time.sleep(10) before retrying."
except requests.exceptions.ConnectionError as e:
return f"Connection error occurred: {e}. The network might be down, inform the user of the issue with the inform_user function."
except requests.exceptions.HTTPError as e:
return f"HTTP error occurred: {e}. The function failed with http error. This usually happens because of access issues. Ensure you validate before using this function"
except Exception as e:
return f"An unexpected error occurred: {e}"
你应该为所有函数都具备这样的错误处理,同时注意以下要点:
-
错误消息应该提供有关发生情况的信息
-
如果你知道特定错误的修复方法(或潜在修复方法),告诉 LLM 在发生错误时如何行动(例如:如果发生速率限制错误,告诉模型运行 time.sleep())
未来的代理上下文工程
在这篇文章中,我涵盖了三个主要主题:特定上下文工程技巧、缩短代理的上下文,以及如何为你的代理提供工具。这些都是你需要了解的基础性主题,以构建一个优秀的 AI 代理。还有一些其他主题你应该了解更多,例如预先计算信息或推理时信息检索的考虑。我将在未来的文章中介绍这个主题。代理上下文工程将继续是一个超级相关的话题,理解如何处理代理的上下文,现在和将来都是未来 AI 代理发展的基础。
👉 我的免费资源
🚀 使用 LLMs 将你的工程能力提升 10 倍(免费 3 天电子邮件课程)
👉 在社交平台上找到我:
📩 订阅我的通讯
🧑💻 取得联系
🔗 领英
✍️ Medium

浙公网安备 33010602011771号