原文 GPT-4.1 Prompting Guide

GPT-4.1 模型系列在编码、指令遵循和长上下文处理能力方面相比 GPT-4o 有了显著提升。在本提示指南中,我们整理了从大量内部测试中得出的重要提示技巧,以帮助开发者充分利用这个新模型系列的改进能力。

许多典型的最佳实践仍然适用于 GPT-4.1,例如提供上下文示例、使指令尽可能具体和清晰,以及通过提示诱导规划以最大化模型智能。然而,我们预计充分利用这个模型需要一些提示迁移。GPT-4.1 经过训练,比其前身更严格、更字面地遵循指令,而前身倾向于更自由地从用户和系统提示中推断意图。这也意味着,GPT-4.1 具有高度的可引导性,对明确指定的提示反应灵敏——如果模型行为与您期望的不同,一个坚定且明确澄清您期望行为的单句几乎总是足以引导模型回到正轨。

请继续阅读可用作参考的提示示例,并记住虽然这些指导广泛适用,但没有建议是万能的。AI 工程本质上是一门经验性学科,大型语言模型本质上是非确定性的;除了遵循本指南外,我们建议构建信息丰富的评估并经常迭代,以确保您的提示工程变更为您的用例带来好处。

1. 代理工作流

GPT-4.1 是构建代理工作流的绝佳选择。在模型训练中,我们强调提供多样化的代理问题解决轨迹,我们的模型代理框架在 SWE-bench Verified 上实现了非推理模型的最先进性能,解决了 55% 的问题。

系统提示提醒

为了充分利用 GPT-4.1 的代理能力,我们建议在所有代理提示中包含三种关键类型的提醒。以下提示专门针对代理编码工作流进行了优化,但可以轻松修改用于一般代理用例。

  1. 持久性:这确保模型理解它正在进入多消息轮次,并防止它过早地将控制权交还给用户。我们的示例如下:
1
你是一个代理 - 请继续直到用户的查询完全解决,然后结束你的轮次并交还给用户。只有在确定问题已解决时才终止你的轮次。
  1. 工具调用:这鼓励模型充分利用其工具,并减少其产生幻觉或猜测答案的可能性。我们的示例如下:
1
如果你不确定与用户请求相关的文件内容或代码库结构,请使用你的工具读取文件并收集相关信息:不要猜测或编造答案。
  1. 规划(可选):如果需要,这确保模型在文本中明确规划和反思每个工具调用,而不是仅通过链接一系列工具调用来完成任务。我们的示例如下:
1
你必须在每个函数调用之前广泛规划,并广泛反思之前函数调用的结果。不要仅通过函数调用来完成整个过程,因为这可能会损害你解决问题和深入思考的能力。

GPT-4.1 经过训练,在代理环境中对用户指令和系统提示都非常严格地响应。模型严格遵循这三个简单指令,我们的内部 SWE-bench Verified 分数提高了近 20%——因此我们强烈建议从涵盖上述三个类别的明确提醒开始任何代理提示。总的来说,我们发现这三个指令将模型从类似聊天机器人的状态转变为更加”渴望”的代理,自主且独立地推动交互向前发展。

工具调用

与之前的模型相比,GPT-4.1 在有效利用作为 OpenAI API 请求参数传递的工具方面接受了更多训练。我们鼓励开发者专门使用 tools 字段来传递工具,而不是手动将工具描述注入到提示中并为工具调用编写单独的解析器,正如一些人过去报告的那样。这是最小化错误并确保模型在工具调用轨迹期间保持分布的最佳方式——在我们自己的实验中,我们观察到使用 API 解析的工具描述与手动将模式注入系统提示相比,SWE-bench Verified 通过率提高了 2%。

开发者应该清楚地命名工具以表明其目的,并在工具的”description”字段中添加清晰、详细的描述。同样,对于每个工具参数,依靠良好的命名和描述来确保适当的使用。如果你的工具特别复杂,你想提供工具使用示例,我们建议你在系统提示中创建一个 # Examples 部分并将示例放在那里,而不是将它们添加到”description”字段中,该字段应该保持全面但相对简洁。提供示例有助于指示何时使用工具、是否在工具调用中包含用户文本,以及哪些参数适合不同的输入。记住,你可以在 Prompt Playground 中使用”Generate Anything”来为新工具定义获得良好的起点。

提示诱导规划和思维链

如前所述,开发者可以选择性地提示使用 GPT-4.1 构建的代理在工具调用之间进行规划和反思,而不是在无间断序列中静默调用工具。GPT-4.1 不是一个推理模型——意味着它在回答之前不会产生内部思维链——但在提示中,开发者可以通过使用上面显示的规划提示组件的任何变体来诱导模型产生明确的、逐步的计划。这可以被认为是模型”大声思考”。在我们对 SWE-bench Verified 代理任务的实验中,诱导明确规划使通过率提高了 4%。

示例提示:SWE-bench Verified

下面,我们分享用于在 SWE-bench Verified 上获得最高分数的代理提示,该提示包含关于工作流和问题解决策略的详细指令。这种通用模式可用于任何代理任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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. 进行代码更改
- 在编辑之前,始终读取相关文件内容或部分以确保完整上下文。
- 如果补丁未正确应用,尝试重新应用。
- 进行小的、可测试的、增量更改,这些更改逻辑上来自你的调查和计划。

## 5. 调试
- 仅在你高度确信它们可以解决问题时进行代码更改
- 调试时,尝试确定根本原因而不是解决症状
- 调试所需时间以识别根本原因并确定修复
- 使用打印语句、日志或临时代码检查程序状态,包括描述性语句或错误消息以了解发生了什么
- 为了测试假设,你还可以添加测试语句或函数
- 如果发生意外行为,重新审视你的假设。

## 6. 测试
- 使用 `!python3 run_tests.py`(或等效)频繁运行测试。
- 每次更改后,通过运行相关测试验证正确性。
- 如果测试失败,分析失败并修改你的补丁。
- 如果需要,编写额外测试以捕获重要行为或边界情况。
- 在最终确定之前确保所有测试通过。

## 7. 最终验证
- 确认根本原因已修复。
- 审查你的解决方案的逻辑正确性和健壮性。
- 迭代直到你极其确信修复完成且所有测试通过。

## 8. 最终反思和额外测试
- 仔细反思用户的原始意图和问题陈述。
- 考虑可能未被现有测试覆盖的潜在边界情况或场景。
- 编写需要通过的额外测试以完全验证你的解决方案的正确性。
- 运行这些新测试并确保它们都通过。
- 注意还有额外的隐藏测试也必须通过,解决方案才能成功。
- 不要仅仅因为可见测试通过就假设任务完成;继续完善直到你确信修复是健壮和全面的。
"""

PYTHON_TOOL_DESCRIPTION = """此函数用于在有状态的 Jupyter notebook 环境中执行 Python 代码或终端命令。python 将响应执行输出或在 60.0 秒后超时。此会话的互联网访问已禁用。不要进行外部网络请求或 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": {
"strict": True,
"type": "object",
"properties": {
"input": {
"type": "string",
"description": " 你希望执行的 Python 代码、终端命令(以感叹号开头)或 apply_patch 命令。",
}
},
"required": ["input"],
},
}

# 额外的框架设置:
# - 将你的仓库添加到 /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"请回答以下问题:\nBug: Typerror..."
)

response.to_dict()["output"]

2. 长上下文

GPT-4.1 具有高性能的 100 万 token 输入上下文窗口,适用于各种长上下文任务,包括结构化文档解析、重新排序、在忽略无关上下文的同时选择相关信息,以及使用上下文执行多跳推理。

最佳上下文大小

我们观察到在针在干草堆评估中,直到我们完整的 100 万 token 上下文都有很好的性能,我们观察到在具有相关和无关代码以及其他文档混合的复杂任务中有很强的性能。然而,长上下文性能可能会随着需要检索更多项目或执行需要了解整个上下文状态的复杂推理(例如执行图搜索)而降低。

调整上下文依赖

考虑回答你的问题可能需要的内部与外部世界知识的混合。有时模型使用一些自己的知识来连接概念或进行逻辑跳跃很重要,而在其他情况下,只使用提供的上下文是可取的。

1
2
3
4
5
# 指令
// 用于内部知识
- 仅使用提供的外部上下文中的文档来回答用户查询。如果你基于此上下文不知道答案,你必须回应"我没有回答该问题所需的信息",即使用户坚持要你回答问题。
// 用于内部和外部知识
- 默认情况下,使用提供的外部上下文来回答用户查询,但如果需要其他基本知识来回答,并且你对答案有信心,你可以使用一些自己的知识来帮助回答问题。

提示组织

特别是在长上下文使用中,指令和上下文的放置会影响性能。如果你的提示中有长上下文,理想情况下将你的指令放在提供上下文的开始和结束,因为我们发现这比仅在上下或下方表现更好。如果你希望只将指令放在一次,那么在提供的上下文上方比下方效果更好。

3. 思维链

如上所述,GPT-4.1 不是一个推理模型,但提示模型逐步思考(称为”思维链”)可以是模型将问题分解为更易管理的部分、解决它们并提高整体输出质量的有效方式,但代价是使用更多输出 token 的更高成本和延迟。该模型经过训练,在代理推理和现实世界问题解决方面表现良好,因此它不应该需要太多提示就能表现良好。

我们建议从提示末尾的这个基本思维链指令开始:

1
2
3
...

首先,仔细逐步思考回答查询需要哪些文档。然后,打印出每个文档的标题和 ID。然后,将 ID 格式化为列表。

从那里,你应该通过审计你特定示例和评估中的失败,并用更明确的指令解决系统性规划和推理错误来改进你的思维链(CoT)提示。在无约束的 CoT 提示中,它尝试的策略可能有差异,如果你观察到一种效果很好的方法,你可以在提示中编码该策略。一般来说,错误往往来自误解用户意图、上下文收集或分析不足,或逐步思考不足或不正确,所以注意这些并尝试用更有主见的指令解决它们。

这是一个示例提示,指示模型更系统地专注于分析用户意图并在继续回答之前考虑相关上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 推理策略
1. 查询分析:分解和分析查询,直到你对其可能询问的内容有信心。考虑提供的上下文以帮助澄清任何模糊或令人困惑的信息。
2. 上下文分析:仔细选择和分析大量潜在相关文档。优化召回 - 如果有些无关紧要也没关系,但正确的文档必须在此列表中,否则你的最终答案将是错误的。每个的分析步骤:
a. 分析:分析它可能与回答查询相关或不相关的分析。
b. 相关性评级:[高、中、低、无]
3. 综合:总结哪些文档最相关以及原因,包括相关性评级为中等或更高的所有文档。

# 用户问题
{user_question}

# 外部上下文
{external_context}

首先,仔细逐步思考回答查询需要哪些文档,严格遵循提供的推理策略。然后,打印出每个文档的标题和 ID。然后,将 ID 格式化为列表。

4. 指令遵循

GPT-4.1 表现出出色的指令遵循性能,开发者可以利用这一点来精确塑造和控制其特定用例的输出。开发者经常广泛提示代理推理步骤、响应语气和声音、工具调用信息、输出格式、要避免的主题等。然而,由于模型更字面地遵循指令,开发者可能需要包含关于做什么或不做什么的明确规范。此外,为其他模型优化的现有提示可能不会立即与此模型一起工作,因为现有指令被更严格地遵循。

这演示了虚构客户服务代理的最佳实践。观察规则的多样性、具体性、使用额外部分获得更多细节,以及一个示例来演示整合所有先前规则的精确行为。

尝试运行以下 notebook 单元格 - 你应该看到用户消息和工具调用,用户消息应该以问候开始,然后回显他们的答案,然后提到他们即将调用工具。尝试更改指令来塑造模型行为,或尝试其他用户消息,以测试指令遵循性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
SYS_PROMPT_CUSTOMER_SERVICE = """你是 NewTelco 的有用客户服务代理,帮助用户高效地满足他们的请求,同时严格遵守提供的指导方针。

# 指令
- 始终用"你好,你已联系到 NewTelco,我能帮你什么?"问候用户
- 在回答关于公司、其产品或服务或用户账户的事实性问题之前,始终调用工具。仅使用检索的上下文,永远不要依赖你自己的知识来回答这些问题。
- 但是,如果你没有足够的信息来正确调用工具,请向用户询问你需要的信息。
- 如果用户请求,升级到人工。
- 不要讨论禁止的主题(政治、宗教、有争议的时事、医疗、法律或财务建议、个人对话、内部公司运营,或对任何人员或公司的批评)。
- 在适当时依赖示例短语,但永远不要在同一对话中重复示例短语。随意变化示例短语以避免听起来重复,并使其更适合用户。
- 始终遵循新消息的提供输出格式,包括对检索的政策文档中任何事实陈述的引用。
- 如果你要调用工具,始终在调用工具之前和之后向用户发送适当的消息。
- 在所有回复中保持专业和简洁的语气,并在句子之间使用表情符号。
- 如果你已解决用户的请求,询问是否还有其他可以帮忙的

# 精确响应步骤(每次响应)
1. 如有必要,调用工具来满足用户期望的行动。始终在调用工具之前和之后向用户发送消息,让他们了解情况。
2. 在你的用户回复中
a. 使用积极倾听并回显你听到用户要求的内容。
b. 根据上述指导方针适当回应。

# 示例短语
## 转移禁止主题
- "对不起,但我无法讨论该主题。还有其他我可以帮助你的吗?"
- "这不是我能够提供信息的内容,但我很乐意帮助你解决其他问题。"

## 调用工具前
- "为了帮助你,我只需要验证你的信息。"
- "让我为你检查一下——请稍等。"
- "我现在为你检索最新详情。"

## 调用工具后
- "好的,这是我找到的:[回复]"
- "所以我找到的是:[回复]"

# 输出格式
- 始终包含你对用户的最终回复。
- 在提供来自检索上下文的事实信息时,始终在相关陈述后立即包含引用。使用以下引用格式:
- 对于单个来源:[NAME](ID)
- 对于多个来源:[NAME](ID), [NAME](ID)
- 仅提供关于此公司、其政策、其产品或客户账户的信息,并且仅基于上下文中提供的信息。不要回答此范围之外的问题。

# 示例
## 用户
你能告诉我你们的家庭计划选项吗?

## 助手回复 1
### 消息
"你好,你已联系到 NewTelco,我能帮你什么? 😊🎉

你想了解我们的家庭计划选项。 🤝 让我为你检查一下——请稍等。 🚀"

### 工具调用
lookup_policy_document(topic="family plan options")

// 工具调用后,助手会跟进:

## 助手回复 2(工具调用后)
### 消息
"好的,这是我找到的: 🎉 我们的家庭计划允许最多 5 条线路共享数据,每条额外线路享受 10% 折扣 [家庭计划政策](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="国际服务要多少钱?我要去法国。",
# input="为什么我上个月的账单这么高?"
)

response.to_dict()["output"]

5. 一般建议

提示结构

作为参考,这是构建提示的良好起点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 角色和目标

# 指令

## 更详细指令的子类别

# 推理步骤

# 输出格式

# 示例
## 示例 1

# 上下文

# 最终指令和逐步思考的提示

添加或删除部分以满足你的需求,并进行实验以确定对你的使用最优的内容。

分隔符

以下是为提示选择最佳分隔符的一些一般指导原则。请参考长上下文部分以获取该上下文类型的特殊考虑。

  1. Markdown:我们建议从这里开始,使用 markdown 标题作为主要部分和子部分(包括更深的层次结构,到 H4+)。使用内联反引号或反引号块来精确包装代码,并根据需要使用标准编号或项目符号列表。

  2. XML:这些也表现良好,我们改进了此模型对 XML 中信息的遵循。XML 便于精确包装包括开始和结束的部分,为标签添加元数据以获得额外上下文,并启用嵌套。以下是使用 XML 标签在示例部分中嵌套示例的示例,每个都有输入和输出:

1
2
3
4
5
6
<examples>
<example1 type="Abbreviate">
<input>San Francisco</input>
<output>- SF</output>
</example1>
</examples>
  1. JSON 高度结构化,模型在编码上下文中特别理解良好。但它可能更冗长,并且需要字符转义,这会增加开销。

专门用于向输入上下文添加大量文档或文件的指导:

  • XML 在我们的长上下文测试中表现良好。
    • 示例:<doc id='1' title='The Fox'>The quick brown fox jumps over the lazy dog</doc>
  • 这种格式由 Lee 等人提出(ref),在我们的长上下文测试中也表现良好。
    • 示例:ID: 1 | TITLE: The Fox | CONTENT: The quick brown fox jumps over the lazy dog
  • JSON 表现特别差。
    • 示例:[{'id': 1, 'title': 'The Fox', 'content': 'The quick brown fox jumped over the lazy dog'}]

模型经过训练,能够稳健地理解各种格式的结构。一般来说,使用你的判断并思考什么将提供清晰的信息并”突出”给模型。例如,如果你检索的文档包含大量 XML,基于 XML 的分隔符可能效果较差。

注意事项

  • 在一些孤立的情况下,我们观察到模型抵制产生很长、重复的输出,例如,逐个分析数百个项目。如果这对你的用例是必要的,强烈指示模型完整输出此信息,并考虑分解问题或使用更简洁的方法。
  • 我们看到一些罕见的并行工具调用不正确的情况。我们建议测试这个,并考虑将 parallel_tool_calls 参数设置为 false,如果你看到问题。

附录:生成和应用文件差异

开发者向我们提供反馈,准确和格式良好的差异生成是为编码相关任务提供动力的关键能力。为此,GPT-4.1 系列相对于之前的 GPT 模型具有显著改进的差异功能。此外,虽然 GPT-4.1 在给定清晰指令和示例的情况下在生成任何格式的差异方面都有很强的性能,但我们在这里开源一个推荐的差异格式,模型在这方面接受了大量训练。我们特别希望对于刚开始的开发者,这将消除你自己创建差异的大部分猜测工作。

应用补丁

请参阅下面的示例,了解正确应用我们推荐的工具调用的提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 命令后,python 总是会说"Done!",无论补丁是否成功应用。但是,你可以通过查看在"Done!"输出之前打印的任何警告或日志行来确定是否有问题和错误。
"""

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 工具的参考实现。你需要使其可执行并作为 apply_patch 从模型将执行命令的 shell 中可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
#!/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 格式,我们在测试中发现,Aider 的 polyglot 基准测试中使用的 SEARCH/REPLACE(查找/替换)diff 格式,以及一种没有内部转义的伪 XML 格式,都有很高的成功率。

这些 diff 格式有两个关键共同点:(1)它们不使用行号;(2)它们都明确提供了需要被替换的精确代码和用于替换的新代码,并且两者之间有清晰的分隔符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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>
"""

本文由AI生成,内容仅供参考。在实际部署前,请根据具体环境进行测试和验证。