GPT-4.1 发布啦!速来抢先学习官方发布的 4.1 提示词指南!本次更新强化了 agent 能力和工具调用能力。原文链接:GPT-4.1 Prompting Guide
GPT-4.1 模型系列相较于 GPT-4o,在编码、指令遵循和长上下文处理能力上实现了显著进步。在本 Prompt 指南中,我们整理了一系列通过大量内部测试得出的重要 Prompt 技巧,旨在帮助开发者充分利用这一新模型系列的增强能力。
许多典型的最佳实践仍然适用于 GPT-4.1,例如提供上下文示例、使指令尽可能具体清晰,以及通过 Prompt 引导模型进行规划以最大化其智能。然而,我们预计要充分发挥此模型的性能,需要进行一些 Prompt 迁移。与倾向于更宽泛地推断用户和系统提示意图的前代模型相比,GPT-4.1 经过训练,能够更严格、更忠实地遵循指令。然而,这也意味着 GPT-4.1 具有高度的可引导性,并且对明确指定的 Prompt 反应灵敏------如果模型行为与您的预期不同,通常只需一句坚定而明确地阐述您期望行为的句子,就足以将模型引导回正轨。
请继续阅读 Prompt 示例以供参考,并请记住,虽然本指南具有广泛适用性,但没有一种建议是万能的。AI 工程本质上是一门经验性学科,而大型语言模型具有固有的不确定性;除了遵循本指南外,我们建议构建信息丰富的评估体系并经常迭代,以确保您的 Prompt 工程变更能够为您的用例带来益处。
[1. Agentic 工作流](#1. Agentic 工作流 "#1-agentic-workflows")
GPT-4.1 非常适合构建 Agentic 工作流。在模型训练中,我们强调提供多样化的 Agentic 问题解决路径,我们为该模型设计的 Agentic 工具链在 SWE-bench Verified 基准测试中,为非推理模型实现了顶尖水平 (state-of-the-art) 的性能,解决了 55% 的问题。
系统提示要点
为了充分利用 GPT-4.1 的 Agentic 能力,我们建议在所有 Agent Prompt 中包含三种关键类型的提醒。以下 Prompt 专为 Agentic 编码工作流优化,但可以轻松修改以适应通用的 Agentic 用例。
- 持续性 (Persistence):这确保模型理解它正在进入一个多轮消息交互,并防止它过早地将控制权交还给用户。我们的示例如下:
scss
你是一个代理 (agent) - 请持续工作直到用户的查询完全解决,然后再结束你的回合并将控制权交还给用户。只有当你确定问题已解决时,才能终止你的回合。
-
工具调用 (Tool-calling):这鼓励模型充分利用其工具,并减少其产生幻觉或猜测答案的可能性。我们的示例如下:
如果你不确定与用户请求相关的文件内容或代码库结构,请使用你的工具读取文件并收集相关信息:不要猜测或编造答案。
-
规划 (Planning) [可选]:如果需要,这确保模型在文本中明确地对每次工具调用进行规划和反思,而不是仅仅通过链接一系列工具调用来完成任务。我们的示例如下:
你必须在每次函数调用之前进行详尽的规划,并对先前函数调用的结果进行详尽的反思。不要仅仅通过进行函数调用来完成整个过程,因为这可能会损害你解决问题和进行有洞察力思考的能力。
在 Agentic 场景中,GPT-4.1 经过训练,能够非常严格地响应用户指令和系统提示。模型严格遵循了这三条简单的指令,使我们在内部 SWE-bench Verified 评分上提升了近 20%------因此我们强烈建议在任何 Agent Prompt 的开头都加上清晰的提醒,涵盖上述三个类别。总的来说,我们发现这三条指令将模型从类似聊天机器人的状态转变为一个更"主动"的代理,能够自主、独立地推动交互进行。
工具调用
与之前的模型相比,GPT-4.1 在有效利用通过 OpenAI API 请求作为参数传递的工具方面接受了更多训练。我们鼓励开发者专门使用 tools
字段来传递工具,而不是像过去一些人报告的那样,手动将工具描述注入到 Prompt 中并编写单独的解析器来处理工具调用。这是最小化错误并确保模型在工具调用轨迹中保持在分布内 (in distribution) 的最佳方式------在我们自己的实验中,我们观察到,使用 API 解析的工具描述相比手动将 schema 注入系统提示,SWE-bench Verified 通过率提高了 2%。
开发者应清晰地命名工具以表明其用途,并在工具的 "description" 字段中添加清晰、详细的描述。同样,对于每个工具参数 (param),依靠良好的命名和描述来确保适当的使用。如果你的工具特别复杂,并且你想提供工具使用的示例,我们建议你在系统提示中创建一个 # Examples
部分并将示例放在那里,而不是将它们添加到应保持详尽但相对简洁的 "description" 字段中。提供示例有助于说明何时使用工具、是否在工具调用旁包含用户文本,以及针对不同输入哪些参数是合适的。请记住,你可以在 Prompt Playground 中使用"生成任何内容" (Generate Anything) 功能来为你的新工具定义获得一个良好的起点。
[通过 Prompt 引导规划与思维链](#通过 Prompt 引导规划与思维链 "#prompting-induced-planning--chain-of-thought")
如前所述,开发者可以选择性地提示使用 GPT-4.1 构建的代理在工具调用之间进行规划和反思,而不是以不间断的序列静默调用工具。GPT-4.1 不是一个推理模型------意味着它在回答之前不会产生内部思维链 (chain of thought)------但在 Prompt 中,开发者可以通过使用上面展示的"规划" Prompt 组件的任何变体来引导模型产生明确的、分步的计划。这可以被认为是模型的"自言自语式思考"。在我们对 SWE-bench Verified Agentic 任务的实验中,引导明确的规划使通过率提高了 4%。
[示例 Prompt:SWE-bench Verified](#示例 Prompt:SWE-bench Verified "#sample-prompt-swe-bench-verified")
下面,我们分享了用于在 SWE-bench Verified 上取得最高分的 Agentic Prompt,其中包含有关工作流程和问题解决策略的详细说明。这种通用模式可用于任何 Agentic 任务。
python
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ.get(
"OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"
)
)
SYS_PROMPT_SWEBENCH = """
你将负责修复一个来自开源代码仓库的问题。
你的思考过程应该周全,所以长一点也没关系。你可以在决定采取每个行动之前和之后逐步思考。
你必须持续迭代,直到问题解决为止。
即使没有互联网连接,你解决这个问题所需的一切都已在 /testbed 文件夹中。我希望你在回复我之前完全自主地解决这个问题。
只有当你确定问题已解决时,才能终止你的回合。请逐步解决问题,并确保验证你的更改是正确的。在没有解决问题的情况下,绝不结束你的回合,并且当你说要进行工具调用时,请确保你确实进行了工具调用,而不是结束你的回合。
这个问题绝对可以在没有互联网的情况下解决。
慢慢来,仔细思考每一步------记住要严格检查你的解决方案,并注意边界情况,特别是你所做的更改。你的解决方案必须是完美的。如果不是,请继续努力。最后,你必须使用提供的工具严格测试你的代码,并多次测试,以捕捉所有边缘情况。如果它不够健壮,请进行更多迭代,使其完美。未能足够严格地测试代码是这类任务的头号失败模式;确保你处理了所有边缘情况,并运行现有的测试(如果提供的话)。
你必须在每次函数调用之前进行详尽的规划,并对先前函数调用的结果进行详尽的反思。不要仅仅通过进行函数调用来完成整个过程,因为这可能会损害你解决问题和进行有洞察力思考的能力。
# 工作流程
## 高级问题解决策略
1. 深入理解问题。仔细阅读问题描述,批判性地思考需要做什么。
2. 调查代码库。探索相关文件,搜索关键函数,并收集上下文信息。
3. 制定清晰、分步的计划。将修复分解为可管理的、增量的步骤。
4. 增量实施修复。进行小规模、可测试的代码更改。
5. 根据需要进行调试。使用调试技术隔离和解决问题。
6. 频繁测试。每次更改后运行测试以验证正确性。
7. 迭代直至根本原因被修复且所有测试通过。
8. 全面反思和验证。测试通过后,思考最初的意图,编写额外的测试以确保正确性,并记住还有隐藏的测试也必须通过,解决方案才算真正完成。
有关每个步骤的更多信息,请参阅下面的详细部分。
## 1. 深入理解问题
在编码之前,仔细阅读问题描述并认真思考解决方案。
## 2. 代码库调查
- 探索相关文件和目录。
- 搜索与问题相关的关键函数、类或变量。
- 阅读并理解相关的代码片段。
- 确定问题的根本原因。
- 在收集更多上下文信息时,不断验证和更新你的理解。
## 3. 制定详细计划
- 勾勒出修复问题的具体、简单且可验证的步骤顺序。
- 将修复分解为小的、增量的更改。
## 4. 进行代码更改
- 在编辑之前,务必阅读相关文件内容或部分,以确保掌握完整的上下文。
- 如果补丁 (patch) 未正确应用,请尝试重新应用。
- 进行小规模、可测试、增量的更改,这些更改应在逻辑上源于你的调查和计划。
## 5. 调试
- 只有在你非常有信心代码更改能解决问题时才进行更改。
- 调试时,尝试确定根本原因,而不是处理表面症状。
- 根据需要进行长时间调试,以确定根本原因并找到修复方法。
- 使用打印语句、日志或临时代码来检查程序状态,包括描述性语句或错误消息,以了解正在发生的情况。
- 为了检验假设,你也可以添加测试语句或函数。
- 如果出现意外行为,重新审视你的假设。
## 6. 测试
- 使用 `!python3 run_tests.py`(或等效命令)频繁运行测试。
- 每次更改后,通过运行相关测试来验证正确性。
- 如果测试失败,分析失败原因并修改你的补丁。
- 如果需要,编写额外的测试来捕捉重要的行为或边缘情况。
- 确保所有测试在最终确定前都通过。
## 7. 最终验证
- 确认根本原因已修复。
- 审查你的解决方案的逻辑正确性和健壮性。
- 持续迭代,直到你非常有信心修复已完成且所有测试通过。
## 8. 最终反思和额外测试
- 仔细反思用户的原始意图和问题陈述。
- 思考现有测试可能未覆盖的潜在边缘情况或场景。
- 编写需要通过才能完全验证解决方案正确性的额外测试。
- 运行这些新测试并确保它们全部通过。
- 请注意,还有额外的隐藏测试必须通过,解决方案才能成功。
- 不要仅仅因为可见的测试通过就认为任务已完成;继续完善,直到你确信修复是健壮和全面的。
"""
PYTHON_TOOL_DESCRIPTION = """此函数用于在有状态的 Jupyter notebook 环境中执行 Python 代码或终端命令。`python` 将响应执行的输出,或者在 60.0 秒后超时。此会话的互联网访问已禁用。不要发出外部 Web 请求或 API 调用,因为它们会失败。就像在 Jupyter notebook 中一样,你也可以通过调用此函数并传入一个以感叹号开头的终端命令来执行终端命令。
此外,为了完成此任务,你可以调用此函数并输入一个 `apply_patch` 命令作为输入。`apply_patch` 实际上允许你对文件执行 diff/patch 操作,但 diff 规范的格式是此任务特有的,因此请仔细注意这些说明。要使用 `apply_patch` 命令,你应该将以下结构的消息作为 "input" 传递:
%%bash
apply_patch <<"EOF"
*** Begin Patch
[YOUR_PATCH]
*** End Patch
EOF
其中 [YOUR_PATCH] 是你的补丁的实际内容,使用以下 V4A diff 格式指定。
*** [ACTION] File: [path/to/file] -> ACTION 可以是 Add、Update 或 Delete 之一。
对于每个需要更改的代码片段,重复以下内容:
[context_before] -> 有关上下文的进一步说明,请参见下文。
- [old_code] -> 在旧代码前加上减号。
+ [new_code] -> 在新的替换代码前加上加号。
[context_after] -> 有关上下文的进一步说明,请参见下文。
关于 [context_before] 和 [context_after] 的说明:
- 默认情况下,在每次更改的上方和下方各显示 3 行代码。如果一个更改距离上一个更改在 3 行以内,则不要在第二个更改的 [context_before] 行中重复第一个更改的 [context_after] 行。
- 如果 3 行上下文不足以在文件中唯一标识代码片段,请使用 @@ 运算符指示该片段所属的类或函数。例如,我们可能有:
@@ class BaseClass
[3 行前置上下文]
- [old_code]
+ [new_code]
[3 行后置上下文]
- 如果一个代码块在一个类或函数中重复次数过多,以至于即使单个 @@ 语句和 3 行上下文也无法唯一标识该代码片段,你可以使用多个 `@@` 语句跳转到正确的上下文。例如:
@@ class BaseClass
@@ def method():
[3 行前置上下文]
- [old_code]
+ [new_code]
[3 行后置上下文]
注意,在这种 diff 格式中,我们不使用行号,因为上下文足以唯一标识代码。下面显示了一个你可能作为 "input" 传递给此函数以应用补丁的消息示例。
%%bash
apply_patch <<"EOF"
*** Begin Patch
*** Update File: pygorithm/searching/binary_search.py
@@ class BaseClass
@@ def search():
- pass
+ raise NotImplementedError()
@@ class Subclass
@@ def search():
- pass
+ raise NotImplementedError()
*** End Patch
EOF
文件引用只能是相对路径,绝不能是绝对路径。运行 apply_patch 命令后,无论补丁是否成功应用,`python` 都会返回 "Done!"。但是,你可以通过查看在输出 "Done!" 之前打印的任何警告或日志行来判断是否存在问题和错误。
"""
python_bash_patch_tool = {
"type": "function",
"name": "python",
"description": PYTHON_TOOL_DESCRIPTION,
"parameters": {
"type": "object",
"strict": True,
"properties": {
"input": {
"type": "string",
"description": " 您希望执行的 Python 代码、终端命令(以感叹号开头)或 apply_patch 命令。",
}
},
"required": ["input"],
},
}
# Additional harness setup:
# - Add your repo to /testbed
# - Add your issue to the first user message
# - Note: Even though we used a single tool for python, bash, and apply_patch, we generally recommend defining more granular tools that are focused on a single function
# 额外的测试环境设置:
# - 将你的代码仓库添加到 /testbed
# - 将你的问题添加到第一条用户消息中
# - 注意:尽管我们为 python、bash 和 apply_patch 使用了单个工具,但通常我们建议定义更细粒度的、专注于单一功能的工具
response = client.responses.create(
instructions=SYS_PROMPT_SWEBENCH,
model="gpt-4.1-2025-04-14",
tools=[python_bash_patch_tool],
input=f"Please answer the following question:\nBug: Typerror..." # 请回答以下问题:\n错误:Typerror...
)
response.to_dict()["output"]
json
[
{
"id": "msg_67fe92df26ac819182ffafce9ff4e4fc07c7e06242e51f8b",
"content": [
{
"annotations": [],
"text": "感谢您的报告,但是"Typerror"这个信息太模糊了,我无法立即开始调试。\n\n**为了取得进展,我需要:**\n1. 找到确切的错误消息文本(例如 `'TypeError: ...'`)。\n2. 找到错误发生在哪个文件以及哪一行/函数/类。\n3. 弄清楚是什么触发了错误(测试文件、用法、复现步骤)。\n4. 找到根本原因和详细信息。\n\n**后续步骤:**\n- 调查错误/日志/测试输出文件,查找 Python `TypeError` 消息。\n- 检查相关代码段中存在问题的类型用法。\n- 如果可能,在本地复现该错误。\n\n**计划:**\n- 首先,我将在 `/testbed` 目录中搜索可能包含完整错误消息和堆栈跟踪的测试文件和日志输出。\n\n让我们先列出 `/testbed` 目录的内容,寻找线索。",
"type": "output_text"
}
],
"role": "assistant",
"status": "completed",
"type": "message"
},
{
"arguments": "{\"input\":\"!ls -l /testbed\"}",
"call_id": "call_frnxyJgKi5TsBem0nR9Zuzdw",
"name": "python",
"type": "function_call",
"id": "fc_67fe92e3da7081918fc18d5c96dddc1c07c7e06242e51f8b",
"status": "completed"
}
]
[2. 长上下文](#2. 长上下文 "#2-long-context")
GPT-4.1 拥有性能优异的 1M token 输入上下文窗口,适用于各种长上下文任务,包括结构化文档解析、重排序、在忽略无关上下文的同时选择相关信息,以及利用上下文进行多跳推理。
最佳上下文大小
我们观察到,在高达 1M token 的完整上下文长度下,"大海捞针" (needle-in-a-haystack) 评估表现非常出色,并且在包含相关和无关代码及其他文档混合的复杂任务中也观察到非常强的性能。然而,随着需要检索的项目增多,或者需要执行依赖于整个上下文状态知识的复杂推理(例如执行图搜索),长上下文性能可能会下降。
调整上下文依赖程度
考虑回答您的问题可能需要的外部知识与内部世界知识的混合比例。有时,模型利用自身的一些知识来连接概念或进行逻辑跳跃很重要,而在其他情况下,最好只使用提供的上下文。
markdown
# 指令
// 仅依赖内部知识
- 仅使用提供的外部上下文中的文档来回答用户查询。如果根据此上下文你不知道答案,即使在用户坚持要求你回答的情况下,你也必须回应"我没有回答该问题所需的信息"。
// 结合内部和外部知识
- 默认情况下,使用提供的外部上下文来回答用户查询,但如果需要其他基础知识来回答,并且你对答案有信心,你可以使用一些自己的知识来帮助回答问题。
[Prompt 组织](#Prompt 组织 "#prompt-organization")
特别是在长上下文使用中,指令和上下文的位置会影响性能。如果您的 Prompt 中有长上下文,理想情况下将您的指令放在所提供上下文的开头和结尾,因为我们发现这比仅放在上方或下方效果更好。如果您只希望指令出现一次,那么放在所提供上下文的上方比放在下方效果更好。
[3. 思维链](#3. 思维链 "#3-chain-of-thought")
如上所述,GPT-4.1 不是一个推理模型,但提示模型逐步思考(称为"思维链",Chain of Thought)可以是一种有效的方式,让模型将问题分解成更易于处理的部分,解决它们,并提高整体输出质量,但代价是使用更多输出 token 带来的更高成本和延迟。该模型经过训练,擅长进行 Agentic 推理和解决现实世界问题,因此不需要太多提示就能表现良好。
我们建议在您的 Prompt 末尾使用这个基本的思维链指令作为起点:
scss
...
首先,仔细地一步步思考需要哪些文档来回答查询。然后,打印出每个文档的标题 (TITLE) 和 ID。接着,将这些 ID 格式化为一个列表。
在此基础上,您应该通过审查特定示例和评估中的失败案例来改进您的思维链 (CoT) Prompt,并通过更明确的指令来解决系统性的规划和推理错误。在无约束的 CoT Prompt 中,模型尝试的策略可能会有变化,如果您观察到一种效果良好的方法,可以在 Prompt 中将其固化。一般来说,错误往往源于对用户意图的误解、上下文收集或分析不足,或者分步思考不足或不正确,因此请留意这些问题,并尝试通过更具主见性的指令来解决它们。
以下是一个示例 Prompt,指示模型在着手回答之前,更有条理地专注于分析用户意图并考虑相关上下文。
markdown
# 推理策略
1. 查询分析:分解并分析查询,直到你确信其意图。考虑提供的上下文以帮助澄清任何模糊或令人困惑的信息。
2. 上下文分析:仔细选择并分析大量可能相关的文档。优化召回率------有些文档不相关也没关系,但正确的文档必须在此列表中,否则你的最终答案将是错误的。每个文档的分析步骤:
a. 分析:分析该文档与回答查询的相关性。
b. 相关性评级:[高, 中, 低, 无]
3. 综合:总结哪些文档最相关及其原因,包括所有相关性评级为中或更高的文档。
# 用户问题
{user_question}
# 外部上下文
{external_context}
首先,严格遵循提供的推理策略,仔细地一步步思考需要哪些文档来回答查询。然后,打印出每个文档的标题 (TITLE) 和 ID。接着,将这些 ID 格式化为一个列表。
[4. 指令遵循](#4. 指令遵循 "#4-instruction-following")
GPT-4.1 展现出卓越的指令遵循性能,开发者可以利用这一点来精确塑造和控制其特定用例的输出。开发者经常为 Agentic 推理步骤、回应的语气和风格、工具调用信息、输出格式、要避免的主题等提供详尽的 Prompt。然而,由于模型更严格地遵循指令,开发者可能需要包含关于该做什么或不该做什么的明确规范。此外,为其他模型优化的现有 Prompt 可能无法直接用于此模型,因为现有指令被更严格地遵循,而隐含的规则不再被强烈推断。
推荐工作流程
以下是我们推荐的开发和调试 Prompt 中指令的工作流程:
-
从一个包含高级指导和要点的总体"响应规则"或"指令"部分开始。
-
如果您想更改更具体的行为,请添加一个部分来为该类别指定更多细节,例如
# Sample Phrases
(示例短语)。 -
如果您希望模型在其工作流程中遵循特定步骤,请添加一个有序列表并指示模型遵循这些步骤。
-
如果行为仍然不如预期:
- 检查是否存在冲突的、不明确的或错误的指令和示例。如果存在冲突的指令,GPT-4.1 倾向于遵循更靠近 Prompt 末尾的指令。
- 添加演示期望行为的示例;确保示例中演示的任何重要行为也在您的规则中被引用。
- 通常不需要使用全大写或其他激励措施,如贿赂或小费。我们建议开始时不使用这些,仅在对您的特定 Prompt 必要时才采用。请注意,如果您现有的 Prompt 包含这些技巧,可能会导致 GPT-4.1 过分关注它。
请注意,使用您偏好的 AI 驱动的 IDE 对于迭代 Prompt 非常有帮助,包括检查一致性或冲突、添加示例,或进行连贯的更新,例如添加一条指令并更新示例以演示该指令。
常见失败模式
这些失败模式并非 GPT-4.1 所独有,但我们在此分享以供普遍了解和方便调试。
- 指示模型始终遵循特定行为有时会引发负面效果。例如,如果被告知"在回应用户之前必须调用工具",当信息不足时,模型可能会虚构工具输入或使用 null 值调用工具。添加"如果你没有足够的信息来调用工具,请向用户询问你需要的信息"应该可以缓解这种情况。
- 当提供示例短语时,模型可能会逐字使用这些引语,并开始让用户觉得重复。请确保指示模型根据需要进行变化。
- 如果没有具体说明,一些模型可能急于提供额外的文字来解释其决定,或者在响应中输出比期望更多的格式。提供指令和可能的示例以帮助缓解这种情况。
[示例 Prompt:客户服务](#示例 Prompt:客户服务 "#example-prompt-customer-service")
这演示了一个虚构客户服务代理的最佳实践。请注意规则的多样性、具体性、使用附加部分提供更多细节,以及一个示例来演示包含所有先前规则的精确行为。
尝试运行以下 notebook 单元格------您应该会看到一条用户消息和一个工具调用,并且用户消息应该以问候开始,然后复述用户的回答,接着提到他们即将调用一个工具。尝试更改指令来塑造模型行为,或者尝试其他用户消息,以测试指令遵循性能。
python
SYS_PROMPT_CUSTOMER_SERVICE = """你是一名乐于助人的 NewTelco 客户服务代理,帮助用户高效地完成请求,同时严格遵守提供的指南。
# 指令
- 总是以"您好,这里是 NewTelco,有什么可以帮您?"来问候用户。
- 在回答有关公司、其服务或产品,或用户账户的事实性问题之前,务必调用工具。对于任何这些问题,只能使用检索到的上下文,绝不依赖自己的知识。
- 但是,如果你没有足够的信息来正确调用工具,请向用户询问你需要的信息。
- 如果用户要求,将问题升级给人工客服。
- 不要讨论禁止的话题(政治、宗教、有争议的时事、医疗、法律或财务建议、个人对话、公司内部运营,或对任何人或公司的批评)。
- 适当时依赖示例短语,但绝不在同一次对话中重复使用同一个示例短语。可以随意变化示例短语,以避免听起来重复,并使其更适合用户。
- 对于新消息,始终遵循提供的输出格式,包括对从检索到的政策文档中引用的任何事实性陈述进行引用。
- 如果你打算调用一个工具,总是在调用工具之前和之后向用户发送适当的消息。
- 在所有回应中保持专业和简洁的语气,并在句子之间使用表情符号。
- 如果你已经解决了用户的请求,询问是否还有其他可以帮助的事情。
# 精确的回应步骤(针对每个回应)
1. 如有必要,调用工具以完成用户期望的操作。始终在调用工具之前和之后向用户发送消息,让他们了解进展。
2. 在你给用户的回应中
a. 使用积极倾听,复述你听到的用户请求。
b. 根据上述指南做出适当的回应。
# 示例短语
## 回避禁止话题
- "很抱歉,我无法讨论那个话题。还有其他我可以帮助您的吗?"
- "这不是我能提供信息的内容,但我很乐意帮助您解决任何其他问题。"
## 调用工具之前
- "为了帮助您解决这个问题,我需要核实一下您的信息。"
- "让我为您查一下------请稍等片刻。"
- "我现在就为您检索最新的详细信息。"
## 调用工具之后
- "好的,这是我找到的信息:[回应]"
- "那么,这是我找到的信息:[回应]"
# 输出格式
- 始终包含你给用户的最终回应。
- 当提供来自检索上下文的事实信息时,总是在相关陈述之后立即包含引用。使用以下引用格式:
- 单个来源:[名称](ID)
- 多个来源:[名称](ID), [名称](ID)
- 仅提供有关本公司、其政策、其产品或客户账户的信息,并且仅当这些信息基于上下文中提供的信息时才提供。不要回答超出此范围的问题。
# 示例
## 用户
你能告诉我你们的家庭套餐选项吗?
## 助理回应 1
### 消息
"您好,这里是 NewTelco,有什么可以帮您?😊🎉\n\n您想了解我们的家庭套餐选项。🤝 让我为您查一下------请稍等片刻。🚀"
### 工具调用
lookup_policy_document(topic="family plan options")
// 工具调用后,助理将跟进:
## 助理回应 2(工具调用后)
### 消息
"好的,这是我找到的信息:🎉 我们的家庭套餐最多允许 5 条线路共享数据,每增加一条线路可享受 10% 的折扣 [Family Plan Policy](ID-010)。📱 今天还有其他我可以帮助您的吗?😊"
"""
get_policy_doc = {
"type": "function",
"name": "lookup_policy_document",
"description": "按主题或关键字查找内部文档和政策的工具。",
"parameters": {
"strict": True,
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "在公司政策或文档中搜索的主题或关键字。",
},
},
"required": ["topic"],
"additionalProperties": False,
},
}
get_user_acct = {
"type": "function",
"name": "get_user_account_info",
"description": "获取用户账户信息的工具",
"parameters": {
"strict": True,
"type": "object",
"properties": {
"phone_number": {
"type": "string",
"description": "格式为 '(xxx) xxx-xxxx'",
},
},
"required": ["phone_number"],
"additionalProperties": False,
},
}
response = client.responses.create(
instructions=SYS_PROMPT_CUSTOMER_SERVICE,
model="gpt-4.1-2025-04-14",
tools=[get_policy_doc, get_user_acct],
input="How much will it cost for international service? I'm traveling to France.", # 国际漫游服务费用是多少?我要去法国旅行。
# input="Why was my last bill so high?" # 我上个月的账单为什么这么高?
)
response.to_dict()["output"]
json
[
{
"id": "msg_67fe92d431548191b7ca6cd604b4784b06efc5beb16b3c5e",
"content": [
{
"annotations": [],
"text": "您好,这里是 NewTelco,有什么可以帮您?🌍✈️\n\n您想了解去法国旅行期间的国际服务费用。🇫🇷 让我为您查一下最新的详细信息------请稍等片刻。🕑",
"type": "output_text"
}
],
"role": "assistant",
"status": "completed",
"type": "message"
},
{
"arguments": "{\"topic\":\"international service cost France\"}",
"call_id": "call_cF63DLeyhNhwfdyME3ZHd0yo",
"name": "lookup_policy_document",
"type": "function_call",
"id": "fc_67fe92d5d6888191b6cd7cf57f707e4606efc5beb16b3c5e",
"status": "completed"
}
]
[5. 通用建议](#5. 通用建议 "#5-general-advice")
[Prompt 结构](#Prompt 结构 "#prompt-structure")
作为参考,以下是构建 Prompt 的一个良好起点。
markdown
# 角色和目标
# 指令
## 更详细指令的子类别
# 推理步骤
# 输出格式
# 示例
## 示例 1
# 上下文
# 最终指令和引导逐步思考的提示
根据您的需求添加或删除部分,并通过实验确定最适合您用法的方式。
分隔符
以下是为您的 Prompt 选择最佳分隔符的一些通用指南。对于长上下文类型,请参阅"长上下文"部分的特殊注意事项。
- Markdown:我们建议从这里开始,并使用 markdown 标题来标记主要部分和子部分(包括更深的层次结构,到 H4+)。使用行内反引号或反引号代码块来精确包裹代码,并根据需要使用标准的编号或项目符号列表。
- XML:这些也表现良好,并且我们改进了此模型对 XML 中信息的遵循度。XML 便于精确包裹一个包含开始和结束的部分,向标签添加元数据以提供额外上下文,并支持嵌套。以下是使用 XML 标签在示例部分中嵌套示例的例子,每个示例都有输入和输出:
xml
<examples>
<example1 type="Abbreviate">
<input>San Francisco</input>
<output>- SF</output>
</example1>
</examples>
- JSON 具有高度结构化,模型尤其在编码上下文中能够很好地理解。但是它可能更冗长,并且需要字符转义,这会增加开销。
专门针对向输入上下文添加大量文档或文件的指南:
- XML 在我们的长上下文测试中表现良好。
- 示例:
<doc id=1 title="狐狸">那只敏捷的棕色狐狸跳过了懒惰的狗</doc>
- 示例:
- 由 Lee 等人提出的这种格式 (参考文献),在我们的长上下文测试中也表现良好。
- 示例:
ID: 1 | TITLE: 狐狸 | CONTENT: 那只敏捷的棕色狐狸跳过了懒惰的狗
- 示例:
- JSON 表现尤其差。
- 示例:
[{"id": 1, "title": "狐狸", "content": "那只敏捷的棕色狐狸跳过了懒惰的狗"}]
- 示例:
该模型经过训练,能够稳健地理解各种格式的结构。通常,请运用您的判断力,思考什么能提供清晰的信息并能让模型"注意到"。例如,如果您检索的文档包含大量 XML,那么基于 XML 的分隔符效果可能会较差。
注意事项
- 在一些孤立的情况下,我们观察到模型抵制产生非常长、重复的输出,例如,逐一分析数百个项目。如果这对于您的用例是必需的,请强烈指示模型完整输出此信息,并考虑分解问题或使用更简洁的方法。
- 我们看到了一些罕见的并行工具调用不正确的实例。我们建议对此进行测试,如果您遇到问题,可以考虑将 parallel_tool_calls 参数设置为 false。
[附录:生成和应用文件 Diff](#附录:生成和应用文件 Diff "#appendix-generating-and-applying-file-diffs")
开发者向我们反馈,准确且格式良好的 diff 生成是支持编码相关任务的关键能力。为此,GPT-4.1 系列相对于之前的 GPT 模型,在 diff 能力上有了显著提升。此外,虽然 GPT-4.1 在给定清晰指令和示例的情况下,在生成任何格式的 diff 方面都具有强大的性能,但我们在此开源一种推荐的 diff 格式,模型已在该格式上进行了广泛训练。我们希望,特别是对于刚起步的开发者来说,这将大大减少您自己创建 diff 时的猜测工作。
[应用补丁 (Apply Patch)](#应用补丁 (Apply Patch) "#apply-patch")
请参阅下面的示例,了解正确应用我们推荐的工具调用的 Prompt。
python
APPLY_PATCH_TOOL_DESC = """这是一个自定义工具,可以更方便地添加、删除、移动或编辑代码文件。`apply_patch` 实际上允许你对文件执行 diff/patch 操作,但 diff 规范的格式是此任务特有的,因此请仔细注意这些说明。要使用 `apply_patch` 命令,你应该将以下结构的消息作为 "input" 传递:
%%bash
apply_patch <<"EOF"
*** Begin Patch
[YOUR_PATCH]
*** End Patch
EOF
其中 [YOUR_PATCH] 是你的补丁的实际内容,使用以下 V4A diff 格式指定。
*** [ACTION] File: [path/to/file] -> ACTION 可以是 Add、Update 或 Delete 之一。
对于每个需要更改的代码片段,重复以下内容:
[context_before] -> 有关上下文的进一步说明,请参见下文。
- [old_code] -> 在旧代码前加上减号。
+ [new_code] -> 在新的替换代码前加上加号。
[context_after] -> 有关上下文的进一步说明,请参见下文。
关于 [context_before] 和 [context_after] 的说明:
- 默认情况下,在每次更改的上方和下方各显示 3 行代码。如果一个更改距离上一个更改在 3 行以内,则不要在第二个更改的 [context_before] 行中重复第一个更改的 [context_after] 行。
- 如果 3 行上下文不足以在文件中唯一标识代码片段,请使用 @@ 运算符指示该片段所属的类或函数。例如,我们可能有:
@@ class BaseClass
[3 行前置上下文]
- [old_code]
+ [new_code]
[3 行后置上下文]
- 如果一个代码块在一个类或函数中重复次数过多,以至于即使单个 @@ 语句和 3 行上下文也无法唯一标识该代码片段,你可以使用多个 `@@` 语句跳转到正确的上下文。例如:
@@ class BaseClass
@@ def method():
[3 行前置上下文]
- [old_code]
+ [new_code]
[3 行后置上下文]
注意,在这种 diff 格式中,我们不使用行号,因为上下文足以唯一标识代码。下面显示了一个你可能作为 "input" 传递给此函数以应用补丁的消息示例。
%%bash
apply_patch <<"EOF"
*** Begin Patch
*** Update File: pygorithm/searching/binary_search.py
@@ class BaseClass
@@ def search():
- pass
+ raise NotImplementedError()
@@ class Subclass
@@ def search():
- pass
+ raise NotImplementedError()
*** End Patch
EOF
"""
APPLY_PATCH_TOOL = {
"name": "apply_patch",
"description": APPLY_PATCH_TOOL_DESC,
"parameters": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": " 您希望执行的 apply_patch 命令。",
}
},
"required": ["input"],
},
}
参考实现:apply_patch.py
这是我们在模型训练中使用 apply_patch
工具的参考实现。您需要将其设为可执行文件,并使其在模型将执行命令的 shell 中可用作 apply_patch
:
python
#!/usr/bin/env python3
"""
A self-contained **pure-Python 3.9+** utility for applying human-readable
"pseudo-diff" patch files to a collection of text files.
"""
from __future__ import annotations
import pathlib
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Callable,
Dict,
List,
Optional,
Tuple,
Union,
)
# --------------------------------------------------------------------------- #
# Domain objects
# --------------------------------------------------------------------------- #
class ActionType(str, Enum):
ADD = "add"
DELETE = "delete"
UPDATE = "update"
@dataclass
class FileChange:
type: ActionType
old_content: Optional[str] = None
new_content: Optional[str] = None
move_path: Optional[str] = None
@dataclass
class Commit:
changes: Dict[str, FileChange] = field(default_factory=dict)
# --------------------------------------------------------------------------- #
# Exceptions
# --------------------------------------------------------------------------- #
class DiffError(ValueError):
"""Any problem detected while parsing or applying a patch."""
# --------------------------------------------------------------------------- #
# Helper dataclasses used while parsing patches
# --------------------------------------------------------------------------- #
@dataclass
class Chunk:
orig_index: int = -1
del_lines: List[str] = field(default_factory=list)
ins_lines: List[str] = field(default_factory=list)
@dataclass
class PatchAction:
type: ActionType
new_file: Optional[str] = None
chunks: List[Chunk] = field(default_factory=list)
move_path: Optional[str] = None
@dataclass
class Patch:
actions: Dict[str, PatchAction] = field(default_factory=dict)
# --------------------------------------------------------------------------- #
# Patch text parser
# --------------------------------------------------------------------------- #
@dataclass
class Parser:
current_files: Dict[str, str]
lines: List[str]
index: int = 0
patch: Patch = field(default_factory=Patch)
fuzz: int = 0
# ------------- low-level helpers -------------------------------------- #
def _cur_line(self) -> str:
if self.index >= len(self.lines):
raise DiffError("Unexpected end of input while parsing patch")
return self.lines[self.index]
@staticmethod
def _norm(line: str) -> str:
"""Strip CR so comparisons work for both LF and CRLF input."""
return line.rstrip("\r")
# ------------- scanning convenience ----------------------------------- #
def is_done(self, prefixes: Optional[Tuple[str, ...]] = None) -> bool:
if self.index >= len(self.lines):
return True
if (
prefixes
and len(prefixes) > 0
and self._norm(self._cur_line()).startswith(prefixes)
):
return True
return False
def startswith(self, prefix: Union[str, Tuple[str, ...]]) -> bool:
return self._norm(self._cur_line()).startswith(prefix)
def read_str(self, prefix: str) -> str:
"""
Consume the current line if it starts with *prefix* and return the text
**after** the prefix. Raises if prefix is empty.
"""
if prefix == "":
raise ValueError("read_str() requires a non-empty prefix")
if self._norm(self._cur_line()).startswith(prefix):
text = self._cur_line()[len(prefix) :]
self.index += 1
return text
return ""
def read_line(self) -> str:
"""Return the current raw line and advance."""
line = self._cur_line()
self.index += 1
return line
# ------------- public entry point -------------------------------------- #
def parse(self) -> None:
while not self.is_done(("*** End Patch",)):
# ---------- UPDATE ---------- #
path = self.read_str("*** Update File: ")
if path:
if path in self.patch.actions:
raise DiffError(f"Duplicate update for file: {path}")
move_to = self.read_str("*** Move to: ")
if path not in self.current_files:
raise DiffError(f"Update File Error - missing file: {path}")
text = self.current_files[path]
action = self._parse_update_file(text)
action.move_path = move_to or None
self.patch.actions[path] = action
continue
# ---------- DELETE ---------- #
path = self.read_str("*** Delete File: ")
if path:
if path in self.patch.actions:
raise DiffError(f"Duplicate delete for file: {path}")
if path not in self.current_files:
raise DiffError(f"Delete File Error - missing file: {path}")
self.patch.actions[path] = PatchAction(type=ActionType.DELETE)
continue
# ---------- ADD ---------- #
path = self.read_str("*** Add File: ")
if path:
if path in self.patch.actions:
raise DiffError(f"Duplicate add for file: {path}")
if path in self.current_files:
raise DiffError(f"Add File Error - file already exists: {path}")
self.patch.actions[path] = self._parse_add_file()
continue
raise DiffError(f"Unknown line while parsing: {self._cur_line()}")
if not self.startswith("*** End Patch"):
raise DiffError("Missing *** End Patch sentinel")
self.index += 1 # consume sentinel
# ------------- section parsers ---------------------------------------- #
def _parse_update_file(self, text: str) -> PatchAction:
action = PatchAction(type=ActionType.UPDATE)
lines = text.split("\n")
index = 0
while not self.is_done(
(
"*** End Patch",
"*** Update File:",
"*** Delete File:",
"*** Add File:",
"*** End of File",
)
):
def_str = self.read_str("@@ ")
section_str = ""
if not def_str and self._norm(self._cur_line()) == "@@":
section_str = self.read_line()
if not (def_str or section_str or index == 0):
raise DiffError(f"Invalid line in update section:\n{self._cur_line()}")
if def_str.strip():
found = False
if def_str not in lines[:index]:
for i, s in enumerate(lines[index:], index):
if s == def_str:
index = i + 1
found = True
break
if not found and def_str.strip() not in [
s.strip() for s in lines[:index]
]:
for i, s in enumerate(lines[index:], index):
if s.strip() == def_str.strip():
index = i + 1
self.fuzz += 1
found = True
break
next_ctx, chunks, end_idx, eof = peek_next_section(self.lines, self.index)
new_index, fuzz = find_context(lines, next_ctx, index, eof)
if new_index == -1:
ctx_txt = "\n".join(next_ctx)
raise DiffError(
f"Invalid {'EOF ' if eof else ''}context at {index}:\n{ctx_txt}"
)
self.fuzz += fuzz
for ch in chunks:
ch.orig_index += new_index
action.chunks.append(ch)
index = new_index + len(next_ctx)
self.index = end_idx
return action
def _parse_add_file(self) -> PatchAction:
lines: List[str] = []
while not self.is_done(
("*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:")
):
s = self.read_line()
if not s.startswith("+"):
raise DiffError(f"Invalid Add File line (missing '+'): {s}")
lines.append(s[1:]) # strip leading '+'
return PatchAction(type=ActionType.ADD, new_file="\n".join(lines))
# --------------------------------------------------------------------------- #
# Helper functions
# --------------------------------------------------------------------------- #
def find_context_core(
lines: List[str], context: List[str], start: int
) -> Tuple[int, int]:
if not context:
return start, 0
for i in range(start, len(lines)):
if lines[i : i + len(context)] == context:
return i, 0
for i in range(start, len(lines)):
if [s.rstrip() for s in lines[i : i + len(context)]] == [
s.rstrip() for s in context
]:
return i, 1
for i in range(start, len(lines)):
if [s.strip() for s in lines[i : i + len(context)]] == [
s.strip() for s in context
]:
return i, 100
return -1, 0
def find_context(
lines: List[str], context: List[str], start: int, eof: bool
) -> Tuple[int, int]:
if eof:
new_index, fuzz = find_context_core(lines, context, len(lines) - len(context))
if new_index != -1:
return new_index, fuzz
new_index, fuzz = find_context_core(lines, context, start)
return new_index, fuzz + 10_000
return find_context_core(lines, context, start)
def peek_next_section(
lines: List[str], index: int
) -> Tuple[List[str], List[Chunk], int, bool]:
old: List[str] = []
del_lines: List[str] = []
ins_lines: List[str] = []
chunks: List[Chunk] = []
mode = "keep"
orig_index = index
while index < len(lines):
s = lines[index]
if s.startswith(
(
"@@",
"*** End Patch",
"*** Update File:",
"*** Delete File:",
"*** Add File:",
"*** End of File",
)
):
break
if s == "***":
break
if s.startswith("***"):
raise DiffError(f"Invalid Line: {s}")
index += 1
last_mode = mode
if s == "":
s = " "
if s[0] == "+":
mode = "add"
elif s[0] == "-":
mode = "delete"
elif s[0] == " ":
mode = "keep"
else:
raise DiffError(f"Invalid Line: {s}")
s = s[1:]
if mode == "keep" and last_mode != mode:
if ins_lines or del_lines:
chunks.append(
Chunk(
orig_index=len(old) - len(del_lines),
del_lines=del_lines,
ins_lines=ins_lines,
)
)
del_lines, ins_lines = [], []
if mode == "delete":
del_lines.append(s)
old.append(s)
elif mode == "add":
ins_lines.append(s)
elif mode == "keep":
old.append(s)
if ins_lines or del_lines:
chunks.append(
Chunk(
orig_index=len(old) - len(del_lines),
del_lines=del_lines,
ins_lines=ins_lines,
)
)
if index < len(lines) and lines[index] == "*** End of File":
index += 1
return old, chunks, index, True
if index == orig_index:
raise DiffError("Nothing in this section")
return old, chunks, index, False
# --------------------------------------------------------------------------- #
# Patch → Commit and Commit application
# --------------------------------------------------------------------------- #
def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
if action.type is not ActionType.UPDATE:
raise DiffError("_get_updated_file called with non-update action")
orig_lines = text.split("\n")
dest_lines: List[str] = []
orig_index = 0
for chunk in action.chunks:
if chunk.orig_index > len(orig_lines):
raise DiffError(
f"{path}: chunk.orig_index {chunk.orig_index} exceeds file length"
)
if orig_index > chunk.orig_index:
raise DiffError(
f"{path}: overlapping chunks at {orig_index} > {chunk.orig_index}"
)
dest_lines.extend(orig_lines[orig_index : chunk.orig_index])
orig_index = chunk.orig_index
dest_lines.extend(chunk.ins_lines)
orig_index += len(chunk.del_lines)
dest_lines.extend(orig_lines[orig_index:])
return "\n".join(dest_lines)
def patch_to_commit(patch: Patch, orig: Dict[str, str]) -> Commit:
commit = Commit()
for path, action in patch.actions.items():
if action.type is ActionType.DELETE:
commit.changes[path] = FileChange(
type=ActionType.DELETE, old_content=orig[path]
)
elif action.type is ActionType.ADD:
if action.new_file is None:
raise DiffError("ADD action without file content")
commit.changes[path] = FileChange(
type=ActionType.ADD, new_content=action.new_file
)
elif action.type is ActionType.UPDATE:
new_content = _get_updated_file(orig[path], action, path)
commit.changes[path] = FileChange(
type=ActionType.UPDATE,
old_content=orig[path],
new_content=new_content,
move_path=action.move_path,
)
return commit
# --------------------------------------------------------------------------- #
# User-facing helpers
# --------------------------------------------------------------------------- #
def text_to_patch(text: str, orig: Dict[str, str]) -> Tuple[Patch, int]:
lines = text.splitlines() # preserves blank lines, no strip()
if (
len(lines) < 2
or not Parser._norm(lines[0]).startswith("*** Begin Patch")
or Parser._norm(lines[-1]) != "*** End Patch"
):
raise DiffError("Invalid patch text - missing sentinels")
parser = Parser(current_files=orig, lines=lines, index=1)
parser.parse()
return parser.patch, parser.fuzz
def identify_files_needed(text: str) -> List[str]:
lines = text.splitlines()
return [
line[len("*** Update File: ") :]
for line in lines
if line.startswith("*** Update File: ")
] + [
line[len("*** Delete File: ") :]
for line in lines
if line.startswith("*** Delete File: ")
]
def identify_files_added(text: str) -> List[str]:
lines = text.splitlines()
return [
line[len("*** Add File: ") :]
for line in lines
if line.startswith("*** Add File: ")
]
# --------------------------------------------------------------------------- #
# File-system helpers
# --------------------------------------------------------------------------- #
def load_files(paths: List[str], open_fn: Callable[[str], str]) -> Dict[str, str]:
return {path: open_fn(path) for path in paths}
def apply_commit(
commit: Commit,
write_fn: Callable[[str, str], None],
remove_fn: Callable[[str], None],
) -> None:
for path, change in commit.changes.items():
if change.type is ActionType.DELETE:
remove_fn(path)
elif change.type is ActionType.ADD:
if change.new_content is None:
raise DiffError(f"ADD change for {path} has no content")
write_fn(path, change.new_content)
elif change.type is ActionType.UPDATE:
if change.new_content is None:
raise DiffError(f"UPDATE change for {path} has no new content")
target = change.move_path or path
write_fn(target, change.new_content)
if change.move_path:
remove_fn(path)
def process_patch(
text: str,
open_fn: Callable[[str], str],
write_fn: Callable[[str, str], None],
remove_fn: Callable[[str], None],
) -> str:
if not text.startswith("*** Begin Patch"):
raise DiffError("Patch text must start with *** Begin Patch")
paths = identify_files_needed(text)
orig = load_files(paths, open_fn)
patch, _fuzz = text_to_patch(text, orig)
commit = patch_to_commit(patch, orig)
apply_commit(commit, write_fn, remove_fn)
return "Done!"
# --------------------------------------------------------------------------- #
# Default FS helpers
# --------------------------------------------------------------------------- #
def open_file(path: str) -> str:
with open(path, "rt", encoding="utf-8") as fh:
return fh.read()
def write_file(path: str, content: str) -> None:
target = pathlib.Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("wt", encoding="utf-8") as fh:
fh.write(content)
def remove_file(path: str) -> None:
pathlib.Path(path).unlink(missing_ok=True)
# --------------------------------------------------------------------------- #
# CLI entry-point
# --------------------------------------------------------------------------- #
def main() -> None:
import sys
patch_text = sys.stdin.read()
if not patch_text:
print("Please pass patch text through stdin", file=sys.stderr)
return
try:
result = process_patch(patch_text, open_file, write_file, remove_file)
except DiffError as exc:
print(exc, file=sys.stderr)
return
print(result)
if __name__ == "__main__":
main()
[其他有效的 Diff 格式](#其他有效的 Diff 格式 "#other-effective-diff-formats")
如果您想尝试使用不同的 diff 格式,我们在测试中发现,Aider 的 polyglot 基准测试中使用的 SEARCH/REPLACE diff 格式,以及一种没有内部转义的伪 XML 格式,都具有很高的成功率。
这些 diff 格式有两个共同的关键方面:(1)它们不使用行号,(2)它们同时提供要替换的确切代码和用于替换它的确切代码,并且两者之间有清晰的分隔符。
ini
SEARCH_REPLACE_DIFF_EXAMPLE = """
path/to/file.py
```
>>>>>>> SEARCH
def search():
pass
=======
def search():
raise NotImplementedError()
<<<<<<< REPLACE
"""
PSEUDO_XML_DIFF_EXAMPLE = """
<edit>
<file>
path/to/file.py
</file>
<old_code>
def search():
pass
</old_code>
<new_code>
def search():
raise NotImplementedError()
</new_code>
</edit>
"""