GPT-4.1、提示工程、AI 开发·

GPT-4.1 提示指南 [译]

深入了解如何通过精准的提示工程充分利用 GPT-4.1 的强大功能,包括代理工作流、长上下文处理、思维链推理及指令遵循的最佳实践

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. 规划(可选):如需明确规划,可要求模型在每次工具调用前进行详细规划,并反思上一次调用的结果,而非仅通过连续的工具调用完成任务。示例提示如下:
您必须在每次函数调用前进行详细规划,并在每次函数调用后深入反思其结果。切勿仅通过函数调用完成整个过程,因为这可能会削弱您解决问题的能力及深入思考的能力。

GPT-4.1 在代理场景下经过训练,能紧密遵循用户指令和系统提示。这三个简单指令使我们在内部 SWE-bench Verified 测试中的得分提高了近 20%——因此我们强烈建议在任何代理提示开头加入涵盖上述三类内容的清晰提醒。总体而言,这三个指令能将模型从类似聊天机器人的状态转变为更"积极主动"的代理,自主且独立地推动交互进程。

工具调用

相较于前代模型,GPT-4.1 在有效利用通过 OpenAI API 请求传递的工具上接受了更多训练。我们鼓励开发者仅通过工具字段传递工具,而非手动将工具描述注入提示并单独编写工具调用解析器——过去部分开发者曾报告使用这种方式。这是最大程度减少错误并确保模型在工具调用过程中保持分布一致的最佳方法。在我们自己的实验中,使用 API 解析的工具描述相比手动注入系统提示中的工具模式,SWE-bench Verified 通过率提高了 2%。

开发者应为工具命名清晰以表明其用途,并在工具的"描述"字段中提供清晰、详细的说明。同样,对于每个工具参数,依靠良好的命名和描述确保正确使用。如果您的工具特别复杂且希望提供工具使用示例,我们建议在系统提示中创建一个"# 示例"部分并将示例置于其中,而非在"描述"字段中添加示例,后者应保持详尽但相对简洁。提供示例有助于说明工具的使用时机、是否需附带用户文本以及不同输入适合的参数。记住,您可以在 Prompt Playground 中使用"Generate Anything"功能,为新工具定义获取一个良好的起点。

提示诱导的规划与思维链

如前所述,开发者可选择通过提示引导基于 GPT-4.1 构建的代理在工具调用之间进行规划和反思,而非静默地连续调用工具。GPT-4.1 并非推理模型——即不会在回答前生成内部思维链——但通过提示,开发者可诱导模型生成显式的、逐步的计划,相当于让模型"大声思考"。在我们针对 SWE-bench Verified 代理任务的实验中,诱导显式规划将通过率提高了 4%。

示例提示:SWE-bench Verified

以下是我们用于在 SWE-bench Verified 测试中获得最高分的代理提示,包含了关于工作流和问题解决策略的详细指令。此通用模式可用于任何代理任务。

您将负责修复开源仓库中的一个问题。

您的思考应全面深入,因此即使内容很长也没关系。您可以在决定采取每个行动前后逐步思考。

您必须持续迭代,直到问题解决。

您在 /testbed 文件夹中已拥有解决此问题所需的一切,无需联网。我希望您在完全自主解决问题后再返回给我。

仅在确认问题已解决时才终止回合。逐步处理问题,确保验证您的更改是否正确。切勿在未解决问题的情况下结束回合,当您说要调用工具时,确保您确实调用了工具,而不是结束回合。

此问题绝对无需联网即可解决。

请花时间仔细思考每个步骤——记得严格检查您的解决方案,特别注意您所做更改的边界情况。您的解决方案必须完美。如果不完美,请继续努力。在最后,您必须使用提供的工具严格测试代码,多次测试以捕捉所有边界情况。如果不够稳健,请继续迭代使其完美。在此类任务中,未能充分严格测试代码是首要失败模式;确保处理所有边界情况,并运行现有测试(如果提供)。

您必须在每次函数调用前进行详细规划,并在每次函数调用后深入反思其结果。切勿仅通过函数调用完成整个过程,因为这可能会削弱您解决问题的能力及深入思考的能力。

# 工作流

## 高层次问题解决策略

1. 深入理解问题。仔细阅读问题描述,批判性地思考所需内容。
2. 调查代码库。探索相关文件,搜索关键函数,收集上下文。
3. 制定清晰的逐步计划。将修复分解为可管理、增量式的步骤。
4. 增量实施修复。进行小的、可测试的代码更改。
5. 根据需要调试。使用调试技术隔离并解决问题。
6. 频繁测试。在每次更改后运行测试以验证正确性。
7. 持续迭代,直到根因修复且所有测试通过。
8. 全面反思与验证。测试通过后,思考原始意图,编写额外测试以确保正确性,并记住还有隐藏测试必须通过,解决方案才算真正完成。

请参阅以下详细部分以获取每个步骤的更多信息。

## 1. 深入理解问题
仔细阅读问题并在编码前深入思考解决计划。

## 2. 代码库调查
- 探索相关文件和目录。
- 搜索与问题相关的关键函数、类或变量。
- 阅读并理解相关代码片段。
- 确定问题的根本原因。
- 在收集更多上下文时持续验证和更新您的理解。

## 3. 制定详细计划
- 概述一个具体、简单且可验证的步骤序列以修复问题。
- 将修复分解为小的、增量式更改。

## 4. 进行代码更改
- 在编辑前,始终阅读相关文件内容或部分以确保完整上下文。
- 如果补丁未正确应用,尝试重新应用。
- 进行小的、可测试的增量更改,这些更改应逻辑上遵循您的调查和计划。

## 5. 调试
- 仅在对更改能解决问题有高度信心时才进行代码更改。
- 调试时,尝试确定根本原因而非仅处理症状。
- 调试直到找到根本原因并确定修复为止。
- 使用打印语句、日志或临时代码检查程序状态,包括描述性语句或错误消息以了解发生的情况。
- 您还可以通过添加测试语句或函数来验证假设。
- 如果出现意外行为,重新审视您的假设。

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

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

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

2. 长上下文

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

最佳上下文大小

我们在"针在干草堆"评估中观察到高达 100 万 token 的出色性能,并且在包含相关和无关代码及其他文档的复杂任务中表现出强劲性能。然而,当需要检索更多项目或执行需要了解整个上下文状态的复杂推理(例如执行图搜索)时,长上下文性能可能会下降。

调整上下文依赖

考虑回答问题可能需要的外部与内部知识组合。有时,模型需要利用自身知识连接概念或进行逻辑跳跃,而在其他情况下,仅使用提供的上下文是理想的。

# 指令
// 仅使用内部知识
- 仅使用提供的外部上下文回答用户查询。如果基于此上下文无法回答,您必须回复「我没有回答该问题所需的信息」,即使用户坚持要求您回答。
// 使用内部和外部知识
- 默认使用提供的外部上下文回答用户查询,但如果需要其他基础知识且您对答案有信心,可使用部分自身知识辅助回答。

提示组织

特别是在长上下文使用中,指令和上下文的放置会影响性能。如果提示包含长上下文,理想情况下在提供的上下文开头和结尾均放置指令,我们发现此方式优于仅在上下文上方或下方放置。如果您更希望只放置一次指令,那么放在上下文上方优于下方。

3. 思维链

如前所述,GPT-4.1 并非推理模型,但通过提示模型逐步思考(称为"思维链"),可有效将问题分解为更易管理的部分,解决它们并提高整体输出质量,代价是使用更多输出 token 导致的较高成本和延迟。模型经过训练,在代理推理和现实世界问题解决方面表现良好,因此无需过多提示即可表现优异。

我们建议在提示末尾加入以下基础思维链指令:

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

在此基础上,您应通过审计特定示例和评估中的失败,改进思维链提示,解决系统性规划和推理错误,并使用更明确的指令。在无约束的思维链提示中,模型尝试的策略可能存在差异,如果您观察到某种有效策略,可在提示中固化该策略。一般来说,错误通常源于误解用户意图、上下文收集或分析不足,或逐步思考不足或错误,因此需注意这些问题并通过更有针对性的指令解决。

以下是一个提示示例,指导模型更系统地分析用户意图并在回答前考虑相关上下文。

# 推理策略
1. 查询分析:分解并分析查询,直到您确信理解其意图。考虑提供的上下文以帮助澄清任何模糊或混淆信息。
2. 上下文分析:仔细选择并分析一大组潜在相关文档。优化召回率——包含一些无关文档无妨,但正确文档必须在此列表中,否则最终答案将错误。对每份文档的分析步骤:
   a. 分析:分析其与回答查询的相关性。
   b. 相关性评级:[高、中、低、无]
3. 综合:总结哪些文档最相关及原因,包括所有相关性评级为中或高的文档。

# 用户问题
{user_question}

# 外部上下文
{external_context}

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

4. 指令遵循

GPT-4.1 展现出色的指令遵循性能,开发者可利用这一点为特定用例精确塑造和控制输出。开发者通常会为代理推理步骤、响应语气和风格、工具调用信息、输出格式、需避免的主题等进行广泛提示。然而,由于模型更字面地遵循指令,开发者可能需要在提示中明确指定应做什么或不应做什么。此外,为其他模型优化的现有提示可能无法直接适用于此模型,因为现有指令会被更严格遵循,而隐式规则不再被强烈推断。

推荐工作流

以下是我们推荐的提示中指令开发和调试工作流:

  1. 从包含高层次指导和项目符号的"响应规则"或"指令"部分开始。
  2. 如果需要更改更具体的行为,添加一个部分以指定该类别的更多细节,例如"# 示例短语"。
  3. 如果希望模型在工作流中遵循特定步骤,添加有序列表并指示模型遵循这些步骤。
  4. 如果行为仍不符合预期: a. 检查是否存在冲突、不明确或错误的指令和示例。如果存在冲突指令,GPT-4.1 倾向于遵循提示末尾的指令。 b. 添加展示所需行为的示例;确保示例中展示的任何重要行为也在规则中提及。 c. 通常无需使用全大写或其他激励措施(如贿赂或小费),但如需额外强调,开发者可自行实验。

请注意,使用您偏好的 AI 驱动的 IDE 可极大帮助迭代提示,包括检查一致性或冲突、添加示例,或进行统一更新,如添加指令并更新示例以展示该指令。

常见失败模式

这些失败模式并非 GPT-4.1 独有,但我们在此分享以提高调试便利性。

  • 指示模型始终遵循特定行为可能偶尔引发负面效应。例如,若提示"您必须在回应用户前调用工具",模型可能在信息不足时幻觉工具输入或以空值调用工具。添加"如果信息不足以调用工具,请向用户询问所需信息"可缓解此问题。
  • 提供示例短语时,模型可能逐字使用这些短语,导致对用户来说听起来重复。确保指示模型根据需要变化这些短语。
  • 如果没有具体指令,某些模型可能急于提供额外说明其决策的文字,或在响应中输出比预期更多的格式。提供指令和可能的示例可帮助缓解。

示例提示:客户服务

以下展示了一个虚构客户服务代理的最佳实践。注意规则的多样性、具体性、额外部分的详细说明,以及一个结合所有先前规则的精确行为示例。

运行以下 notebook 单元格,您应看到用户消息和工具调用,用户消息应以问候开头,然后回显其请求,再提及即将调用工具。尝试更改指令以塑造模型行为,或尝试其他用户消息,以测试指令遵循性能。

SYS_PROMPT_CUSTOMER_SERVICE = """您是 NewTelco 的一名热心客户服务专员,负责高效地帮助用户满足其请求,同时严格遵循以下指引。

# 指令
- 每次都以“您好,欢迎致电 NewTelco,请问有什么可以帮您的?”问候用户。
- 对于关于公司、其服务或产品,或用户账户的事实性问题,必须在回答前调用工具。只能使用工具返回的上下文信息,严禁依赖自身知识回答此类问题。
    - 但如果缺乏调用工具所需的信息,应主动向用户询问所需内容。
- 如果用户提出请求,需将问题升级给人工客服。
- 不得讨论以下禁止话题:政治、宗教、有争议的时事、医疗、法律或财务建议、私人对话、公司内部运营、任何个人或公司的批评。
- 在合适情况下使用示例短语,但不得在同一对话中重复使用。可灵活变通示例短语,以避免重复并更贴合用户语境。
- 所有新消息的回复都必须遵循指定的输出格式,并为来自政策文档的任何事实性声明添加引用。
- 在调用工具前后必须告知用户操作。
- 回复需保持专业简洁的语气,并在句子间使用表情符号。
- 当用户请求已完成,需询问“还有其他我可以帮您的问题吗?”

# 精准响应步骤(每次回复)
1. 如有必要,调用工具以完成用户请求。调用工具前后必须分别告知用户。
2. 在回复用户时:
    a. 使用主动倾听技巧,并复述用户的请求内容。
    b. 根据上述指引给予恰当回应。

# 示例短语
## 回避禁止话题
- “很抱歉,我无法就该话题提供信息。还有其他我可以帮您的吗?”
- “这个问题我无法解答,但我很乐意帮您解决其他疑问。”

## 调用工具前
- “为帮您处理这个问题,我需要先核实一些信息。”
- “我来查一下,请稍等片刻。”
- “我现在为您获取最新资料。”

## 调用工具后
- “好的,我找到以下信息:[response]”
- “这是我查到的内容:[response]”

# 输出格式
- 始终包含对用户的最终回复。
- 提供来自上下文的事实信息时,必须立即添加引用,格式如下:
    - 单个来源: [NAME](ID)
    - 多个来源: [NAME](ID), [NAME](ID)
- 仅限提供本公司、公司政策、产品或客户账户的相关信息,且仅限基于上下文返回信息的基础上作答。不得涉及此范围以外的问题。

# 示例
## 用户
你能告诉我你们的家庭套餐吗?

## 助理回应 1
### 消息
“您好,欢迎致电 NewTelco,请问有什么可以帮您的?😊🎉\n\n您想了解我们的家庭套餐 🤝。我来查一下,请稍等片刻 🚀”

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

// 工具调用后,助理继续回应:

## 助理回应 2(工具调用之后)
### 消息
“好的,我找到以下信息:🎉 我们的家庭套餐允许最多 5 条线路共享流量,每增加一条线路可享受 10% 折扣 [Family Plan Policy](ID-010)。📱 还有其他我可以帮您的吗?😊”

5. 通用建议

提示结构

以下是提示结构的良好起点供参考。

# 角色与目标

# 指令

## 更详细指令的子类别

# 推理步骤

# 输出格式

# 示例
## 示例 1

# 上下文

# 最终指令与逐步思考提示

根据需要添加或移除部分,并进行实验以确定适合您用例的最佳结构。

分隔符

以下是选择提示最佳分隔符的一些通用指南。请参阅长上下文部分以了解该上下文类型的特殊考虑。

  1. Markdown:我们建议从这里开始,使用 markdown 标题划分主要部分和子部分(包括更深层次,H4+)。使用内联反引号或反引号块精确包裹代码,并根据需要使用标准编号或项目符号列表。
  2. XML:表现也很好,我们改进了模型对 XML 中信息的遵循。XML 便于精确包裹包含开始和结束的段落,为标签添加元数据以提供额外上下文,并支持嵌套。以下是使用 XML 标签在示例部分嵌套示例的示例,包含每个示例的输入和输出:
<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 等提出的格式(参考)在我们长上下文测试中也表现良好。
    • 示例: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 在给定清晰指令和示例的情况下能生成任何格式的差异表现出色,我们在此开源了一种推荐的差异格式,模型在此格式上接受了广泛训练。我们希望这对刚开始的开发者尤其有用,减少自行创建差异的猜测工作。

应用补丁

请参阅以下示例,了解如何正确应用我们推荐的工具调用补丁。

这是一个自定义实用工具,便于添加、移除、移动或编辑代码文件。`apply_patch` 允许您对文件执行差异/补丁操作,但差异规格的格式为此任务独有,因此请仔细注意这些指令。要使用 `apply_patch` 命令,您应将以下结构的消息作为"输入"传递:

%%bash
apply_patch <<"EOF"
*** Begin Patch
[YOUR_PATCH]
*** End Patch
EOF

其中 [YOUR_PATCH] 是您的补丁实际内容,采用以下 V4A 差异格式指定。

*** [ACTION] File: [path/to/file] -> ACTION 可以是 Add、Update 或 Delete。
对于需要更改的每个代码片段,重复以下内容:
[context_before] -> 上下文的进一步说明见下文。
- [old_code] -> 旧代码前加减号。
+ [new_code] -> 新替换代码前加加号。
[context_after] -> 上下文的进一步说明见下文。

关于 [context_before] 和 [context_after] 的说明:
- 默认显示更改前 3 行和后 3 行的代码。如果更改与前一更改相距 3 行以内,不要在第二更改的 [context_before] 中重复第一更改的 [context_after] 行。
- 如果 3 行上下文不足以在文件中唯一标识代码片段,使用 @@ 操作符指明代码片段所属的类或函数。例如:

@@ class BaseClass
[3 lines of pre-context]
- [old_code]
+ [new_code]
[3 lines of post-context]

- 如果代码块在类或函数中重复多次,以至于单个 @@ 语句和 3 行上下文无法唯一标识代码片段,您可以使用多个 @@ 语句跳转到正确上下文。例如:

@@ class BaseClass
@@     def method():
[3 lines of pre-context]
- [old_code]
+ [new_code]
[3 lines of post-context]

请注意,我们在此差异格式中不使用行号,因为上下文足以唯一标识代码。以下是您可能作为"输入"传递给此函数以应用补丁的消息示例。

%%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.py

以下是我们用于模型训练的 apply_patch 工具的参考实现。您需将其设置为可执行文件,并确保模型执行命令的 shell 中可用 apply_patch

#!/usr/bin/env python3

"""
一个独立运行的 **纯 Python 3.9+** 实用工具,用于对文本文件集合应用人类可读的"伪差异"补丁文件。
"""

from __future__ import annotations

import pathlib
from dataclasses import dataclass, field
from enum import Enum
from typing import (
    Callable,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)

# --------------------------------------------------------------------------- #
#  域对象
# --------------------------------------------------------------------------- #
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)

# --------------------------------------------------------------------------- #
#  异常
# --------------------------------------------------------------------------- #
class DiffError(ValueError):
    """解析或应用补丁时检测到的任何问题。"""

# --------------------------------------------------------------------------- #
#  解析补丁时使用的辅助数据类
# --------------------------------------------------------------------------- #
@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)

# --------------------------------------------------------------------------- #
#  补丁文本解析器
# --------------------------------------------------------------------------- #
@dataclass
class Parser:
    current_files: Dict[str, str]
    lines: List[str]
    index: int = 0
    patch: Patch = field(default_factory=Patch)
    fuzz: int = 0

    # ------------- 低级辅助函数 -------------------------------------- #
    def _cur_line(self) -> str:
        if self.index >= len(self.lines):
            raise DiffError("解析补丁时意外到达输入末尾")
        return self.lines[self.index]

    @staticmethod
    def _norm(line: str) -> str:
        """去除 CR 以确保 LF 和 CRLF 输入的比较有效。"""
        return line.rstrip("\r")

    # ------------- 扫描便利函数 ----------------------------------- #
    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:
        """
        如果当前行以 *prefix* 开头,则消耗该行并返回 **prefix 之后** 的文本。如果前缀为空则抛出异常。
        """
        if prefix == "":
            raise ValueError("read_str() 需要非空前缀")
        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:
        """返回当前原始行并前进。"""
        line = self._cur_line()
        self.index += 1
        return line

    # ------------- 公共入口点 -------------------------------------- #
    def parse(self) -> None:
        while not self.is_done(("*** End Patch",)):
            # ---------- 更新 ---------- #
            path = self.read_str("*** Update File: ")
            if path:
                if path in self.patch.actions:
                    raise DiffError(f"文件重复更新:{path}")
                move_to = self.read_str("*** Move to: ")
                if path not in self.current_files:
                    raise DiffError(f"更新文件错误 - 文件缺失:{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

            # ---------- 删除 ---------- #
            path = self.read_str("*** Delete File: ")
            if path:
                if path in self.patch.actions:
                    raise DiffError(f"文件重复删除:{path}")
                if path not in self.current_files:
                    raise DiffError(f"删除文件错误 - 文件缺失:{path}")
                self.patch.actions[path] = PatchAction(type=ActionType.DELETE)
                continue

            # ---------- 添加 ---------- #
            path = self.read_str("*** Add File: ")
            if path:
                if path in self.patch.actions:
                    raise DiffError(f"文件重复添加:{path}")
                if path in self.current_files:
                    raise DiffError(f"添加文件错误 - 文件已存在:{path}")
                self.patch.actions[path] = self._parse_add_file()
                continue

            raise DiffError(f"解析时遇到未知行:{self._cur_line()}")

        if not self.startswith("*** End Patch"):
            raise DiffError("缺少 *** End Patch 标记")
        self.index += 1  # 消耗标记

    # ------------- 部分解析器 ---------------------------------------- #
    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"更新部分中无效行:\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"无效 {'EOF ' if eof else ''}上下文于 {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"无效添加文件行(缺少 '+'):{s}")
            lines.append(s[1:])  # 去除前导 '+'
        return PatchAction(type=ActionType.ADD, new_file="\n".join(lines))

# --------------------------------------------------------------------------- #
#  辅助函数
# --------------------------------------------------------------------------- #
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"无效行:{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"无效行:{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("此部分无内容")
    return old, chunks, index, False

# --------------------------------------------------------------------------- #
#  补丁 → 提交及提交应用
# --------------------------------------------------------------------------- #
def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
    if action.type is not ActionType.UPDATE:
        raise DiffError("对非更新操作调用 _get_updated_file")
    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} 超出文件长度"
            )
        if orig_index > chunk.orig_index:
            raise DiffError(
                f"{path}{orig_index} > {chunk.orig_index} 处存在重叠 chunk"
            )

        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 操作无文件内容")
            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

# --------------------------------------------------------------------------- #
#  用户面向辅助函数
# --------------------------------------------------------------------------- #
def text_to_patch(text: str, orig: Dict[str, str]) -> Tuple[Patch, int]:
    lines = text.splitlines()  # 保留空行,不去除空格
    if (
        len(lines) < 2
        or not Parser._norm(lines[0]).startswith("*** Begin Patch")
        or Parser._norm(lines[-1]) != "*** End Patch"
    ):
        raise DiffError("无效补丁文本 - 缺少标记")

    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: ")
    ]

# --------------------------------------------------------------------------- #
#  文件系统辅助函数
# --------------------------------------------------------------------------- #
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"{path} 的 ADD 更改无内容")
            write_fn(path, change.new_content)
        elif change.type is ActionType.UPDATE:
            if change.new_content is None:
                raise DiffError(f"{path} 的 UPDATE 更改无新内容")
            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("补丁文本必须以 *** 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 "完成!"

# --------------------------------------------------------------------------- #
#  默认文件系统辅助函数
# --------------------------------------------------------------------------- #
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 入口点
# --------------------------------------------------------------------------- #
def main() -> None:
    import sys

    patch_text = sys.stdin.read()
    if not patch_text:
        print("请通过 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()

其他有效差异格式

如果您想尝试其他差异格式,我们在测试中发现 Aider 的多语言基准中使用的 SEARCH/REPLACE 差异格式,以及无需内部转义的伪 XML 格式,均具有较高的成功率。

这些差异格式有两个关键共性:(1)不使用行号;(2)提供需替换的确切代码和替换后的确切代码,两者之间有清晰的分隔符。

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>
"""

原文链接:https://cookbook.openai.com/examples/gpt4-1_prompting_guide


© 2025 智人飞扬