Die GPT-4.1-Modellfamilie bietet im Vergleich zu GPT-4o erhebliche Verbesserungen bei der Codierung, der Befolgung von Anweisungen und der Verarbeitung langer Kontexte. Insbesondere ist die Leistung bei der Codegenerierung und bei Reparaturaufgaben besser, das Verständnis und die Ausführung komplexer Anweisungen sind genauer, und längere Eingabetexte können effizient verarbeitet werden. Dieser technische Leitfaden enthält wichtige Tipps aus umfangreichen Tests innerhalb von OpenAI und soll Entwicklern helfen, die erweiterten Fähigkeiten dieser neuen Modellfamilie voll auszuschöpfen.
Viele der klassischen Best Practices für das Cueing gelten auch für den GPT-4.1, z. B. kontextbezogene Beispiele, möglichst spezifische und klare Anweisungen und die Steuerung der Planung des Modells durch Hinweise, um seine Intelligenz zu maximieren. Um das volle Potenzial des Modells auszuschöpfen, sind jedoch möglicherweise einige Anpassungen an den bestehenden Hinweisen erforderlich.GPT-4.1 wurde darauf trainiert, Anweisungen genauer und wörtlicher zu befolgen als seine Vorgängermodelle. Frühere Modelle neigten dazu, die Intention von Benutzer- und Systemhinweisen eher lose abzuleiten. Das bedeutet, dass GPT-4.1 in hohem Maße steuerbar ist und auf gut gestaltete Hinweise reagiert - wenn sich das Modell nicht wie erwartet verhält, genügt in der Regel ein eindeutiger und unmissverständlicher Satz, der das gewünschte Verhalten verdeutlicht, um das Modell wieder in die richtige Bahn zu lenken. Diese Eigenschaft erfordert von den Entwicklern mehr Präzision bei der Gestaltung von Hinweisen, bietet aber auch eine beispiellose Kontrolle.
Im Folgenden finden Sie einige Beispiele für Hinweise. Beachten Sie, dass dieser Leitfaden zwar im Großen und Ganzen anwendbar ist, dass aber spezifische Praktiken an das jeweilige Szenario angepasst werden müssen. ai-Engineering ist von Natur aus eine empirische Disziplin, und groß angelegte Sprachmodelle sind von Natur aus nicht deterministisch; zusätzlich zur Befolgung dieses Leitfadens wird empfohlen, aussagekräftige Evaluierungssysteme zu konstruieren und häufig zu iterieren, um sicherzustellen, dass Änderungen beim Cue-Engineering greifbare Vorteile für Ihre Anwendungsszenarien bringen.
1. agenturische Arbeitsabläufe
GPT-4.1 ist ideal für den Aufbau von Agenten-Workflows. Bei der Modellschulung liegt der Schwerpunkt auf der Bereitstellung verschiedener Problemlösungspfade für Agenten. Das für das Modell verwendete Agenten-Testing-Framework erzielte die beste Leistung unter den Nicht-Inferenzmodellen im SWE-bench Verified Benchmark (ein wichtiges Maß für die Fähigkeit eines Modells, reale Software-Engineering-Probleme zu lösen), indem es das Problem 55% löste.
Systemaufforderungserinnerungen
Um die Vorteile der Agentenfunktionen von GPT-4.1 voll auszuschöpfen, wird empfohlen, drei wichtige Arten von Erinnerungen in alle Agentenaufforderungen aufzunehmen. Die folgenden Aufforderungen sind speziell für Agenten-Codierungs-Workflows optimiert, können aber leicht an andere generische Agenten-Anwendungsfälle angepasst werden.
- Beharrlichkeit. Stellen Sie sicher, dass das Modell weiß, dass es sich in einer Mehrrunden-Interaktion befindet, und verhindern Sie, dass es die Kontrolle vorzeitig an den Benutzer zurückgibt. Beispiel:
你是一个代理 - 请持续工作,直到用户的查询完全解决,然后再结束你的回合并将控制权交还给用户。只有当你确定问题已解决时才能终止你的回合。
- Werkzeug-Rufen. Ermutigen Sie das Modell, seine Werkzeuge voll auszuschöpfen, und verringern Sie die Wahrscheinlichkeit, dass es halluziniert oder Antworten errät. Beispiel:
如果你不确定与用户请求相关的文件内容或代码库结构,请使用你的工具读取文件并收集相关信息:不要猜测或编造答案。
- Planung [fakultativ]. Falls gewünscht, stellt dies sicher, dass das Modell jeden Werkzeugaufruf im Text explizit plant und reflektiert und nicht nur eine Abfolge von Werkzeugaufrufen durchläuft, um die Aufgabe zu erledigen. Beispiel:
你必须在每次函数调用前进行详尽的规划,并对先前函数调用的结果进行深入反思。不要仅通过函数调用来完成整个过程,这可能会影响你解决问题和进行有洞察力思考的能力。
Im Agentenszenario reagiert GPT-4.1 sehr genau auf Benutzerbefehle und Systemaufforderungen. Die strikte Befolgung dieser drei einfachen Befehle durch das Modell verbesserte die interne SWE-Bench Verified-Punktzahl um fast 20% - es wird also dringend empfohlen, dass jede Agentenaufforderung mit einer ausdrücklichen Erinnerung beginnt, die alle drei Kategorien abdeckt. Insgesamt haben diese drei Anweisungen das Modell von einem Chatbot-ähnlichen Zustand in einen "proaktiven" Agenten verwandelt, der in der Lage ist, die Interaktion selbstständig und unabhängig zu steuern.
Werkzeugaufrufe
Im Vergleich zu den Vorgängermodellen hat GPT-4.1 mehr Schulung in der effektiven Nutzung von Tools erhalten, die als OpenAI-API-Anforderungsparameter übergeben werden. Es wird dringend empfohlen, dass EntwicklerspezialisierenVerwenden Sie das Werkzeugfeld, um Werkzeuge zu übergeben, anstatt Werkzeugbeschreibungen manuell in Hints einzufügen und einen separaten Parser zu schreiben, um Werkzeugaufrufe zu verarbeiten, wie einige Entwickler in der Vergangenheit berichtet haben. Dies ist der beste Weg, um Fehler zu minimieren und sicherzustellen, dass das Modell innerhalb der Verteilung im Werkzeugaufrufpfad bleibt - bei internen Experimenten wurde eine 2% höhere SWE-Bench Verified Pass Rate bei Verwendung von API-geparsten Werkzeugbeschreibungen gegenüber der manuellen Injektion von Mustern in die System-Hints beobachtet. die Zuverlässigkeit der Verwendung von Standard-API-Funktionen.
Die Entwickler sollten das Werkzeug eindeutig benennen, um seine Verwendung anzugeben, und eine klare, detaillierte Beschreibung in das Feld "Beschreibung" des Werkzeugs eingeben. Auch bei den einzelnen Parametern des Werkzeugs ist eine gute Benennung und Beschreibung wichtig, um eine angemessene Verwendung zu gewährleisten. Wenn es sich um ein besonders komplexes Werkzeug handelt und Sie Beispiele für die Verwendung des Werkzeugs geben möchten, empfiehlt es sich, in der Eingabeaufforderung des Systems einen Abschnitt #-Beispiele zu erstellen und die Beispiele dort zu platzieren, anstatt sie in das Feld "Beschreibung" einzufügen, das detailliert, aber relativ knapp gehalten werden sollte. Anhand von Beispielen können Sie veranschaulichen, wann das Werkzeug zu verwenden ist, ob neben dem Werkzeugaufruf ein Benutzertext eingefügt werden soll und welche Parameter für verschiedene Eingaben zu verwenden sind. Denken Sie daran, dass es möglich ist, das Prompt Spielplatz Die Funktion "Beliebig generieren" im Abschnitt "Beliebig generieren" bietet einen guten Ausgangspunkt für neue Werkzeugdefinitionen.
Prompting-induzierte Planung & Gedankenkette
Wie bereits erwähnt, können Entwickler Agenten, die mit GPT-4.1 erstellt wurden, optional dazu auffordern, zwischen den Werkzeugaufrufen zu planen und zu reflektieren, anstatt das Werkzeug stillschweigend in einer ununterbrochenen Sequenz aufzurufen.GPT-4.1 ist kein schlussfolgerndes Modell - das heißt, es erzeugt keine interne Gedankenkette, bevor es antwortet -- Der Entwickler kann jedoch eine beliebige Variante der oben gezeigten Komponente der Planungsaufforderung in der Aufforderung verwenden, um das Modell zur Erstellung eines expliziten Schritt-für-Schritt-Plans anzuleiten. Dies kann man sich als "lautes Denken" des Modells vorstellen. In Experimenten mit der SWE-Bench Verified-Agent-Aufgabe erhöhte die explizite Planung die Erfolgsquote um 4%. Es ist jedoch zu beachten, dass dieser Ansatz die Länge der Antwort und die Anzahl der Antworten erhöht. Token Verbrauch, was sich auf die Kosten und Verzögerungen auswirkt.
Beispiel-Tipp: SWE-Bench Geprüft
Die Agenten-Tipps, die verwendet wurden, um die höchste Punktzahl bei SWE-bench Verified zu erreichen, werden im Folgenden zusammen mit detaillierten Anweisungen zu Arbeitsabläufen und Problemlösungsstrategien vorgestellt. Dieses generische Modell kann für jede Agentenaufgabe verwendet werden.
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 笔记本环境中执行 Python 代码或终端命令。python 将响应执行的输出,或者在 60.0 秒后超时。此会话的互联网访问已被禁用。不要发出外部 Web 请求或 API 调用,因为它们会失败。就像在 Jupyter 笔记本中一样,你也可以通过调用此函数并传入以感叹号 (!) 开头的终端命令来执行终端命令。 此外,为了完成此任务,你可以调用此函数并将 `apply_patch` 命令作为输入。`apply_patch` 实际上允许你对文件执行 diff/patch 操作,但 diff 规范的格式对此任务是唯一的,因此请仔细注意这些说明。要使用 `apply_patch` 命令,你应该将以下结构的消息作为 "input" 传递: %%bash apply_patch <<"EOF" *** Begin Patch [你的补丁内容] *** End Patch EOF 其中 [你的补丁内容] 是你补丁的实际内容,使用以下 V4A diff 格式指定。 *** [操作] File: [文件路径] -> 操作可以是 Add、Update 或 Delete 之一。 对于需要更改的每个代码片段,重复以下内容: [之前的上下文] -> 有关上下文的进一步说明见下文。 - [旧代码] -> 在旧代码前加上减号。 + [新代码] -> 在新的替换代码前加上加号。 [之后的上下文] -> 有关上下文的进一步说明见下文。 关于 [之前的上下文] 和 [之后的上下文] 的说明: - 默认情况下,在每次更改的上方和下方各显示 3 行代码。如果一个更改距离上一个更改在 3 行之内,则不要在第二个更改的 [之前的上下文] 行中重复第一个更改的 [之后的上下文] 行。 - 如果 3 行上下文不足以在文件中唯一标识代码片段,请使用 @@ 运算符指示代码片段所属的类或函数。例如,我们可能有: @@ class BaseClass [3 行前置上下文] - [旧代码] + [新代码] [3 行后置上下文] - 如果一个代码块在类或函数中重复次数过多,以至于即使单个 @@ 语句和 3 行上下文也无法唯一标识代码片段,你可以使用多个 `@@` 语句跳转到正确的上下文。例如: @@ class BaseClass @@ def method(): [3 行前置上下文] - [旧代码] + [新代码] [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"], }, } # 额外的测试框架设置: # - 将你的仓库添加到 /testbed # - 将你的问题添加到第一条用户消息中 # - 注意:尽管我们对 python、bash 和 apply_patch 使用了单个工具,但通常建议定义更细粒度的、专注于单一功能的工具 # response = client.chat.completions.create( # 译者注:原文用 responses.create,此处修正为 chat.completions.create # messages=[ # {"role": "system", "content": SYS_PROMPT_SWEBENCH}, # {"role": "user", "content": "Please answer the following question:\nBug: Typerror..."} # ], # model="gpt-4.1-2025-04-14", # tools=[python_bash_patch_tool], # tool_choice="auto" # 译者注:添加 tool_choice # ) # response_message = response.choices[0].message # tool_calls = response_message.tool_calls # print(response_message) # 译者注:原文直接输出 response.to_dict()["output"],此处模拟打印消息和工具调用 # if tool_calls: # print(tool_calls)
# 模拟输出 (基于原文示例) # 消息内容: # {'role': 'assistant', 'content': '感谢你的报告,但是“Typerror”太模糊了,我无法立即开始调试。\n\n**为了取得进展,我需要:**\n1. 找到确切的错误消息文本(例如 `'TypeError: ...'`)。\n2. 找到错误发生在哪个文件以及哪一行/函数/类。\n3. 弄清楚是什么触发了错误(测试文件、用法、重现步骤)。\n4. 找到根本原因和细节。\n\n**下一步:**\n- 调查错误/日志/测试输出文件,查找 Python `TypeError` 消息。\n- 检查相关代码段中存在问题的类型用法。\n- 如果可能,在本地重现该错误。\n\n**计划:**\n- 首先,我将在 `/testbed` 目录中搜索可能包含完整错误消息和堆栈跟踪的测试文件和日志输出。\n\n让我们先列出 `/testbed` 目录的内容来寻找线索。'} # 工具调用: # [ChatCompletionMessageToolCall(id='call_frnxyJgKi5TsBem0nR9Zuzdw', function=Function(arguments='{"input":"!ls -l /testbed"}', name='python'), type='function')]
2. langer Kontext
GPT-4.1 verfügt über ein hochleistungsfähiges 1M-Token-Eingabekontextfenster für eine Vielzahl von langen Kontextaufgaben, einschließlich des Parsens strukturierter Dokumente, der Neuordnung, der Auswahl relevanter Informationen bei gleichzeitigem Ignorieren irrelevanten Kontexts und der Multi-Hop-Inferenz unter Verwendung von Kontext.
Optimale Kontextgröße
Das Modell schneidet bei "Nadel im Heuhaufen"-Bewertungen von bis zu 1 Mio. Token gut ab und ist sehr leistungsstark bei komplexen Aufgaben mit einer Mischung aus verwandtem und nicht verwandtem Code und anderen Dokumenten. Die Leistung bei langen Kontexten kann sich jedoch verschlechtern, wenn mehr Elemente abgerufen werden müssen oder wenn komplexe Schlussfolgerungen gezogen werden müssen, die sich auf den Zustand des gesamten Kontexts stützen (z. B. bei der Durchführung einer Graphensuche). Darüber hinaus kann der Umgang mit sehr langen Kontexten die Kosten und die Latenzzeit von API-Aufrufen erheblich erhöhen, so dass die Entwickler bei deren Verwendung Kompromisse eingehen müssen.
Abstimmung der Kontextabhängigkeit
Bedenken Sie, inwieweit die Beantwortung einer Frage eine Mischung aus externem Kontext und internem Wissen des Modells erfordern kann. Manchmal muss das Modell sein eigenes Wissen nutzen, um Konzepte zu verbinden oder logische Sprünge zu machen, während in anderen Fällen erwartet wird, dass es nur den bereitgestellten Kontext nutzt.
# 指令 # 仅使用内部知识: # 仅使用提供的外部上下文中的文档来回答用户查询。如果根据此上下文你不知道答案,你必须回答“我没有回答该问题所需的信息”,即使用户坚持让你回答问题。 # 结合内外部知识: # 默认情况下,使用提供的外部上下文来回答用户查询,但如果需要其他基础知识来回答,并且你对答案有信心,则可以使用一些你自己的知识来帮助回答问题。
Zeitnahe Organisation
Insbesondere bei der Verwendung langer Kontexte kann die Platzierung von Anweisungen und Kontext die Leistung beeinträchtigen. Wenn ein langer Kontext in der Eingabeaufforderung enthalten ist, platzieren Sie die Anweisung idealerweise imAnfang und Endezu platzieren, da Tests ergeben haben, dass dies besser funktioniert als die Platzierung über oder unter der Anweisung. Wenn die Anweisung nur ein einziges Mal platziert werden soll, wird sie in dem angegebenen Kontext platziertüberDas funktioniert besser, als wenn man sie unterlegt.
3. die Gedankenkette
Wie bereits erwähnt, ist GPT-4.1 kein Inferenzmodell, aber die Aufforderung an das Modell, schrittweise zu denken (bekannt als "Chain of Thought" oder CoT), kann dem Modell dabei helfen, das Problem in handlichere Teile zu zerlegen, diese zu lösen und die Gesamtqualität der Ausgabe zu verbessern. Dies geschieht jedoch auf Kosten höherer Kosten und Latenzzeiten, die mit der Verwendung von mehr Output-Tokens verbunden sind. Das Modell ist so trainiert, dass es als Agent gut argumentieren und reale Probleme lösen kann, so dass es nicht viel Ansporn braucht, um gute Leistungen zu erbringen.
Es wird empfohlen, am Ende der Aufforderung diese grundlegende Anweisung für die Gedankenkette einzufügen:
...首先,仔细地一步步思考需要哪些文档来回答查询。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
Auf dieser Grundlage sollten Chain of Thought (CoT)-Aufforderungen verbessert werden, indem spezifische Beispiele und Fehler in der Bewertung untersucht werden, und systematische Planungs- und Argumentationsfehler sollten durch explizitere Anweisungen angegangen werden. Bei uneingeschränkten CoT-Aufforderungen kann es Unterschiede in den Strategien geben, die das Modell ausprobiert, und wenn sich ein Ansatz als erfolgreich erweist, kann diese Strategie in der Aufforderung verankert werden. Im Allgemeinen sind Fehler auf ein Missverständnis der Absicht des Benutzers, unzureichende Kontexterfassung oder -analyse oder unzureichendes oder falsches schrittweises Denken zurückzuführen, so dass es wichtig ist, sich dieser Probleme bewusst zu sein und zu versuchen, sie mit direkteren Anweisungen anzugehen. Wenn Sie das Modell anweisen, eine Gedankenkette auszugeben, erhöht sich die Reaktionszeit und der Token-Verbrauch, seien Sie sich also der Kosten bewusst.
Nachfolgend finden Sie ein Beispiel für eine Aufforderung, die das Modell anweist, sich systematischer auf die Analyse der Benutzerabsicht zu konzentrieren und den relevanten Kontext zu berücksichtigen, bevor es mit der Antwort fortfährt.
# 推理策略 1. 查询分析:分解并分析查询,直到你确信它可能在问什么。考虑提供的上下文以帮助澄清任何模糊或令人困惑的信息。 2. 上下文分析:仔细选择并分析大量可能相关的文档。优化召回率——有些不相关也没关系,但正确的文档必须在此列表中,否则最终答案将是错误的。每个文档的分析步骤: a. 分析:分析它与回答查询的相关性如何。 b. 相关性评级:[高, 中, 低, 无] 3. 综合:总结哪些文档最相关及其原因,包括所有相关性评级为中或更高的文档。 # 用户问题 {user_question} # 外部上下文 {external_context} 首先,仔细地一步步思考需要哪些文档来回答查询,严格遵守提供的推理策略。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
4. folgende Unterweisung
GPT-4.1 zeichnet sich durch eine hervorragende Befolgung der Anweisungen aus, die Entwickler nutzen können, um die Ausgabe genau auf ihren speziellen Anwendungsfall abzustimmen und zu steuern. Die Entwickler werden häufig nach den Schritten der Agenteninferenz, dem Antwortton und -stil, den Informationen zum Werkzeugaufruf, den Ausgabeformaten, den zu vermeidenden Themen usw. gefragt. Da das Modell jedoch Anweisungen eher wörtlich befolgt, müssen die Entwickler möglicherweise explizite Angaben darüber machen, was zu tun oder zu lassen ist. Außerdem sind bestehende Hinweise, die für andere Modelle optimiert wurden, möglicherweise nicht direkt auf dieses Modell anwendbar, da bestehende Anweisungen genauer befolgt werden und implizite Regeln nicht mehr so stark abgeleitet werden. Dies bedeutet, dass die Entwickler Hinweise sorgfältiger entwerfen müssen, aber auch mehr Kontrolle erhalten.
Empfohlener Arbeitsablauf
Im Folgenden werden die empfohlenen Arbeitsabläufe für die Entwicklung und das Debugging der Befehle in den Prompts beschrieben:
- Beginnen Sie mit einem übergreifenden Abschnitt "Reaktionsregeln" oder "Richtlinien" mit übergeordneten Leitlinien und Kernpunkten.
- Wenn Sie ein spezifischeres Verhalten ändern möchten, fügen Sie einen Abschnitt hinzu, in dem Sie weitere Einzelheiten über die Klasse angeben, wie z. B. den Beispielsatz #.
- Wenn Sie möchten, dass das Modell bestimmte Schritte in seinem Arbeitsablauf befolgt, fügen Sie eine geordnete Liste hinzu und weisen Sie das Modell an, diese Schritte zu befolgen.
- Wenn das Verhalten immer noch nicht den Erwartungen entspricht:
a. Suchen Sie nach widersprüchlichen, unklaren oder falschen Anweisungen und Beispielen. Wenn es widersprüchliche Anweisungen gibt, zieht GPT-4.1 es vor, den Anweisungen zu folgen, die näher am Ende der Aufforderung liegen.
b. Fügen Sie Beispiele hinzu, die das gewünschte Verhalten demonstrieren; stellen Sie sicher, dass jedes signifikante Verhalten, das in den Beispielen gezeigt wird, auch in der Regel erwähnt wird.
c. Die Verwendung von Caps oder Anreizen wie Bestechungsgeldern oder Trinkgeldern ist normalerweise nicht erforderlich. Es wird empfohlen, diese zunächst nicht zu verwenden, sondern nur, wenn es für einen bestimmten Prompt notwendig ist. Wenn eine bestehende Aufforderung diese Tipps enthält, kann dies dazu führen, dass sich GPT-4.1 zu sehr auf diese Aufforderung konzentriert.
Hinweis: Die Verwendung der von Ihnen bevorzugten KI-gestützten IDE kann sehr hilfreich sein, um die Eingabeaufforderungen zu wiederholen, einschließlich der Überprüfung auf Konsistenz oder Konflikte, des Hinzufügens von Beispielen oder der Durchführung kohärenter Aktualisierungen, wie z. B. das Hinzufügen eines Befehls und die Aktualisierung des Beispiels zur Veranschaulichung des Befehls.
Häufige Fehlermodi
Diese Fehlermodi gelten nicht nur für GPT-4.1, werden hier aber zum allgemeinen Verständnis und zur Fehlersuche aufgeführt.
- Die Anweisung an das Modell, immer ein bestimmtes Verhalten zu zeigen, kann sich manchmal nachteilig auswirken. Wenn zum Beispiel gesagt wird: "Du musst das Werkzeug aufrufen, bevor du dem Benutzer antwortest", kann es passieren, dass das Modell die Eingaben des Werkzeugs halluziniert oder das Werkzeug mit Nullwerten aufruft, wenn es nicht genügend Informationen hat. Der Zusatz "Wenn Sie nicht über genügend Informationen verfügen, um das Werkzeug aufzurufen, fragen Sie den Benutzer nach den benötigten Informationen" sollte diese Situation entschärfen.
- Bei der Angabe von Beispielsätzen kann es vorkommen, dass das Modell diese Verweise wortwörtlich verwendet und sich für den Benutzer zu wiederholen beginnt. Stellen Sie sicher, dass das Anleitungsmodell nach Bedarf geändert wird.
- Wenn es keine spezifischen Anweisungen gibt, kann es vorkommen, dass einige Modelle darauf bedacht sind, zusätzlichen Text zur Erklärung ihrer Entscheidungen zu liefern oder mehr Formatierungen in der Antwort auszugeben als erwartet. Um dies zu vermeiden, werden Anweisungen und möglicherweise Beispiele gegeben.
Beispiel Aufforderung: Kundendienst
Hier werden die besten Praktiken für einen fiktiven Kundenbetreuer aufgezeigt. Beachten Sie die Vielfalt der Regeln, die Spezifizität, die Verwendung zusätzlicher Abschnitte, um mehr Details zu liefern, und ein Beispiel, um das genaue Verhalten zu demonstrieren, das alle vorherigen Regeln kombiniert.
Versuchen Sie, den folgenden Code auszuführen - Sie sollten eine Benutzernachricht und einen Werkzeugaufruf sehen, und die Benutzernachricht sollte mit einer Begrüßung beginnen, dann die Antwort des Benutzers wiedergeben, gefolgt von einem Verweis auf einen bevorstehenden Werkzeugaufruf. Versuchen Sie, die Anweisungen zu ändern, um das Verhalten des Modells zu beeinflussen, oder probieren Sie andere Benutzernachrichten aus, um zu testen, ob die Anweisungen der Leistung entsprechen.
# 译者注:原文使用 notebook cell 运行,此处仅提供 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_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% 的折扣 [家庭套餐政策](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.chat.completions.create( # 译者注:原文用 responses.create,此处修正为 chat.completions.create # messages=[ # {"role": "system", "content": SYS_PROMPT_CUSTOMER_SERVICE}, # {"role": "user", "content": "国际服务要多少钱?我要去法国旅行。"}, # # {"role": "user", "content": "为什么我上个月的账单这么高?"} # ], # model="gpt-4.1-2025-04-14", # tools=[get_policy_doc, get_user_acct], # tool_choice="auto" # 译者注:添加 tool_choice # ) # response_message = response.choices[0].message # tool_calls = response_message.tool_calls # print(response_message) # 译者注:原文直接输出 response.to_dict()["output"],此处模拟打印消息和工具调用 # if tool_calls: # print(tool_calls)
# 模拟输出 (基于原文示例) # 消息内容: # {'role': 'assistant', 'content': "您好,这里是 NewTelco,有什么可以帮您?🌍✈️\n\n您想了解去法国旅行期间的国际服务费用。🇫🇷 让我为您查询最新的详细信息——请稍等片刻。🕑"} # 工具调用: # [ChatCompletionMessageToolCall(id='call_cF63DLeyhNhwfdyME3ZHd0yo', function=Function(arguments='{"topic":"international service cost France"}', name='lookup_policy_document'), type='function')]
5. allgemeine Hinweise
Struktur der Aufforderung
Als Referenz finden Sie hier eine gute Ausgangsstruktur für die Erstellung von Prompts.
# 角色和目标 # 指令 ## 更详细指令的子类别 # 推理步骤 # 输出格式 # 示例 ## 示例 1 # 上下文 # 最终指令和引导逐步思考的提示
Fügen Sie je nach Bedarf Abschnitte hinzu oder entfernen Sie welche und experimentieren Sie, um herauszufinden, was für Ihren Anwendungsfall optimal ist.
Begrenzungszeichen
Im Folgenden finden Sie einige allgemeine Richtlinien für die Auswahl des besten Trennzeichens für eine Eingabeaufforderung. Spezielle Überlegungen zu diesem Kontexttyp finden Sie im Abschnitt "Langer Kontext".
- Markdown. Es wird empfohlen, von nun an Markdown-Überschriften zu verwenden, um größere Abschnitte und Unterabschnitte zu kennzeichnen (einschließlich tieferer Hierarchien, bis zu H4+). Verwenden Sie Inline-Backquotes oder Backquote-Blöcke, um den Code präzise zu umbrechen, und verwenden Sie bei Bedarf Standardnummerierungen oder Aufzählungslisten.
- XML. Diese Tags funktionieren ebenfalls gut, und GPT-4.1 hat die Einhaltung der Informationen in XML verbessert. XML erleichtert die präzise Umhüllung eines Abschnitts, der einen Anfang und ein Ende enthält, ermöglicht das Hinzufügen von Metadaten zu Tags, um zusätzlichen Kontext zu liefern, und unterstützt die Verschachtelung. Im Folgenden finden Sie Beispiele für die Verwendung von XML-Tags zur Verschachtelung von Beispielen innerhalb eines Beispielabschnitts, jeweils mit Eingaben und Ausgaben:
<examples> <example1 type="Abbreviate"> <input>San Francisco</input> <output>- SF</output> </example1> </examples>
- JSON. Das JSON-Format ist stark strukturiert, und das Modell wird vor allem im Zusammenhang mit der Codierung gut verstanden. Es kann jedoch ausführlicher sein und erfordert eine Zeichenumwandlung, was zusätzlichen Aufwand bedeutet.
Ein Leitfaden speziell für das Hinzufügen einer großen Anzahl von Dokumenten oder Dateien in den Eingabekontext:
- XML schneidet in langen Kontexttests gut ab.
- Beispiel: Schlauer brauner Fuchs springt über faulen Hund
- Dieses von Lee et al. vorgeschlagene Format (Beratung), schneidet auch in langen Kontexttests gut ab.
- Beispiel: ID: 1 | Titel: Fuchs | Inhalt: Flinker brauner Fuchs springt über faulen Hund
- JSON schneidet besonders schlecht ab.
- Beispiel: [{"id": 1, "title": "Fox", "content": "Agile brown fox jumps over lazy dog"}]
Modelle sind darauf trainiert, die Struktur verschiedener Formate zuverlässig zu verstehen. Im Allgemeinen sollten Sie sich überlegen, was klare Informationen liefert, die dem Modell "auffallen". Wenn Sie zum Beispiel Dokumente abrufen, die viel XML enthalten, sind XML-basierte Trennzeichen möglicherweise weniger effektiv.
Vorbehalte
- In einigen Einzelfällen wurde beobachtet, dass das Modell sich weigert, sehr lange, sich wiederholende Ausgaben zu produzieren, z. B. die Analyse von Hunderten von Elementen nacheinander. Wenn dies für Ihren Anwendungsfall notwendig ist, weisen Sie das Modell nachdrücklich an, diese Informationen vollständig auszugeben, und erwägen Sie eine Zerlegung des Problems oder einen prägnanteren Ansatz.
- In einigen seltenen Fällen kam es zu fehlerhaften parallelen Werkzeugaufrufen. Es wird empfohlen, dies zu testen und, falls ein Problem gefunden wird, die Verschiebung der parallele_tool_aufrufe Der Parameter wird auf false gesetzt.
- Extrem lange Kontexte und Gedankenketten können die Kosten und die Latenzzeit von API-Aufrufen erheblich erhöhen und müssen daher sorgfältig geprüft werden.
Anhang: Vergleich von Unterschieden zwischen generierten und angewandten Dateien (Diffs)
Das Feedback der Entwickler hat gezeigt, dass die genaue und gut formatierte Generierung von Disparitäten (Diffs) eine Schlüsselfunktion zur Unterstützung von Codierungsaufgaben ist. Zu diesem Zweck bietet die GPT-4.1-Modellfamilie im Vergleich zu früheren GPT-Modellen erhebliche Verbesserungen bei den Vergleichsmöglichkeiten. Darüber hinaus ist GPT-4.1 in der Lage, Diffs in jedem Format zu erzeugen, wenn klare Anweisungen und Beispiele gegeben werden. Es ist zu hoffen, dass dies vor allem angehenden Entwicklern hilft, das Rätselraten bei der Erstellung eigener Vergleichsformate zu vermeiden.
Patch anwenden
Im folgenden Beispiel finden Sie Tipps zur korrekten Anwendung eines Aufrufs des Empfehlungstools.
APPLY_PATCH_TOOL_DESC="""这是一个自定义实用程序,可以更方便地添加、删除、移动或编辑代码文件。`apply_patch` 实际上允许你对文件执行 diff/patch 操作,但 diff 规范的格式对此任务是唯一的,因此请仔细注意这些说明。要使用 `apply_patch` 命令,你应该将以下结构的消息作为 "input" 传递: %%bash apply_patch <<"EOF" *** Begin Patch [你的补丁内容] *** End Patch EOF 其中 [你的补丁内容] 是你补丁的实际内容,使用以下 V4A diff 格式指定。 *** [操作] File: [文件路径] -> 操作可以是 Add、Update 或 Delete 之一。 对于需要更改的每个代码片段,重复以下内容: [之前的上下文] -> 有关上下文的进一步说明见下文。 - [旧代码] -> 在旧代码前加上减号。 + [新代码] -> 在新的替换代码前加上加号。 [之后的上下文] -> 有关上下文的进一步说明见下文。 关于 [之前的上下文] 和 [之后的上下文] 的说明: - 默认情况下,在每次更改的上方和下方各显示 3 行代码。如果一个更改距离上一个更改在 3 行之内,则不要在第二个更改的 [之前的上下文] 行中重复第一个更改的 [之后的上下文] 行。 - 如果 3 行上下文不足以在文件中唯一标识代码片段,请使用 @@ 运算符指示代码片段所属的类或函数。例如,我们可能有: @@ class BaseClass [3 行前置上下文] - [旧代码] + [新代码] [3 行后置上下文] - 如果一个代码块在类或函数中重复次数过多,以至于即使单个 @@ 语句和 3 行上下文也无法唯一标识代码片段,你可以使用多个 `@@` 语句跳转到正确的上下文。例如: @@ class BaseClass @@ def method(): [3 行前置上下文] - [旧代码] + [新代码] [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= { "type": "function", # 译者注:原文 tool 定义缺少 type="function" "name": "apply_patch", "description": APPLY_PATCH_TOOL_DESC, "parameters": { "type": "object", "properties": { "input": { "type": "string", "description": " 你希望执行的 apply_patch 命令。", } }, "required": ["input"], }, }
Referenzimplementierung: apply_patch.py
Dies ist eine Referenzimplementierung des apply_patch-Tools, das als Teil der Modellschulung verwendet wird. Sie müssen es ausführbar machen und als apply_patch in der Shell verfügbar machen, in der das Modell Befehle ausführt:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # 译者注:添加 utf-8 编码声明 """ 一个自包含的 **纯 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",)): # ---------- UPDATE ---------- # 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: ") # 译者注:原文这里没有处理 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 # 译者注:补充 move_path 赋值 self.patch.actions[path] = action continue # ---------- DELETE ---------- # 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 # ---------- ADD ---------- # 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()}") # 译者注:以下查找逻辑原文实现较复杂且有潜在bug,简化处理 # if def_str.strip(): # 查找 @@ 定义行 # ... 原文复杂的查找逻辑 ... 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"在 {index} 处无效的 {'EOF ' if eof else ''}上下文:\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_to_add: 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_to_add.append(s[1:]) # 去掉开头的 '+' return PatchAction(type=ActionType.ADD, new_file="\n".join(lines_to_add)) # --------------------------------------------------------------------------- # # 辅助函数 # --------------------------------------------------------------------------- # 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) - len(context) + 1): if lines[i : i + len(context)] == context: return i, 0 # 忽略行尾空白匹配 context_rstrip = [s.rstrip() for s in context] for i in range(start, len(lines) - len(context) + 1): if [s.rstrip() for s in lines[i : i + len(context)]] == context_rstrip: return i, 1 # 增加少量模糊度 # 忽略首尾空白匹配 context_strip = [s.strip() for s in context] for i in range(start, len(lines) - len(context) + 1): if [s.strip() for s in lines[i : i + len(context)]] == context_strip: return i, 100 # 增加较多模糊度 return -1, 0 def find_context( lines: List[str], context: List[str], start: int, eof: bool ) -> Tuple[int, int]: """查找上下文,处理 EOF 情况和模糊匹配""" if eof: # 如果是文件末尾,优先尝试从末尾精确匹配 new_index, fuzz = find_context_core(lines, context, len(lines) - len(context)) if new_index != -1: return new_index, fuzz # 如果末尾精确匹配失败,再从 start 开始查找,并增加大量模糊度 new_index, fuzz = find_context_core(lines, context, start) return new_index, fuzz + 10_000 # 增加大量模糊度表示 EOF 匹配失败 # 非 EOF 情况,直接从 start 开始查找 return find_context_core(lines, context, start) def peek_next_section( lines: List[str], index: int ) -> Tuple[List[str], List[Chunk], int, bool]: """预读下一个代码块,返回上下文行、块列表、结束索引和是否到达文件末尾""" context_lines: List[str] = [] # 译者注:原文变量名 old 不清晰 del_lines: List[str] = [] ins_lines: List[str] = [] chunks: List[Chunk] = [] mode = "keep" # keep, add, delete 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("***") and not s.startswith("*** End of File"): # 译者注:修正检查逻辑 raise DiffError(f"无效行: {s}") index += 1 last_mode = mode raw_line = s # 保留原始行用于可能的错误报告 if s == "": # 译者注:处理空行,原文处理为 " " 可能不妥 s = "" # 保持为空行 mode = "keep" # 空行视为上下文保留 elif s.startswith("+"): mode = "add" s = s[1:] elif s.startswith("-"): mode = "delete" s = s[1:] elif s.startswith(" "): mode = "keep" s = s[1:] else: # 允许没有前导 +/-/space 的行作为上下文,兼容某些 diff 格式 mode = "keep" # raise DiffError(f"无效行: {raw_line}") # 译者注:放宽限制 # 当模式从 add/delete 切换到 keep 时,保存之前的 chunk if mode == "keep" and last_mode != "keep": if ins_lines or del_lines: chunks.append( Chunk( # 块的原始索引是当前上下文行数减去删除的行数 orig_index=len(context_lines) - len(del_lines), del_lines=del_lines, ins_lines=ins_lines, ) ) del_lines, ins_lines = [], [] # 重置 # 根据模式收集行 if mode == "delete": del_lines.append(s) context_lines.append(s) # 删除的行也属于原始上下文 elif mode == "add": ins_lines.append(s) # 增加的行不属于原始上下文 elif mode == "keep": context_lines.append(s) # 处理循环结束后剩余的最后一个 chunk if ins_lines or del_lines: chunks.append( Chunk( orig_index=len(context_lines) - len(del_lines), del_lines=del_lines, ins_lines=ins_lines, ) ) is_eof = False if index < len(lines) and lines[index] == "*** End of File": index += 1 # 消耗 EOF 标记 is_eof = True if index == orig_index and not is_eof: # 如果索引未移动且不是 EOF raise DiffError("此段落中没有任何内容") return context_lines, chunks, index, is_eof # --------------------------------------------------------------------------- # # 补丁 → 提交 和 提交应用 # --------------------------------------------------------------------------- # def _get_updated_file(text: str, action: PatchAction, path: str) -> str: """根据 PatchAction 更新文件内容""" if action.type is not ActionType.UPDATE: raise DiffError("_get_updated_file 使用了非更新操作调用") orig_lines = text.split("\n") dest_lines: List[str] = [] orig_consumed_index = 0 # 指向原始文件中已处理到的行的下一个索引 sorted_chunks = sorted(action.chunks, key=lambda c: c.orig_index) # 按原始索引排序块 for chunk in sorted_chunks: if chunk.orig_index < orig_consumed_index: raise DiffError( f"{path}: 块重叠于 {orig_consumed_index} > {chunk.orig_index}" ) if chunk.orig_index > len(orig_lines): raise DiffError( f"{path}: 块原始索引 {chunk.orig_index} 超出文件长度 {len(orig_lines)}" ) # 添加上一个块结束到当前块开始之间的原始行 dest_lines.extend(orig_lines[orig_consumed_index : chunk.orig_index]) # 应用当前块的更改:添加插入的行 dest_lines.extend(chunk.ins_lines) # 更新原始文件的消耗索引:跳过被删除的行 orig_consumed_index = chunk.orig_index + len(chunk.del_lines) # 验证删除的行是否与原文匹配(可选,增加健壮性) # expected_del = orig_lines[chunk.orig_index : orig_consumed_index] # if expected_del != chunk.del_lines: # # 可以选择报错或记录警告 # print(f"警告: {path} 在索引 {chunk.orig_index} 处删除的行不匹配") # print(f"预期: {expected_del}") # print(f"实际: {chunk.del_lines}") # 添加最后一个块之后的所有剩余原始行 dest_lines.extend(orig_lines[orig_consumed_index:]) return "\n".join(dest_lines) def patch_to_commit(patch: Patch, orig: Dict[str, str]) -> Commit: """将解析后的 Patch 对象转换为 Commit 对象""" commit = Commit() for path, action in patch.actions.items(): if action.type is ActionType.DELETE: if path not in orig: # 再次检查文件是否存在 raise DiffError(f"尝试删除不存在的文件: {path}") 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(f"ADD 操作缺少文件内容: {path}") if path in orig: # 检查文件是否已存在 raise DiffError(f"尝试添加已存在的文件: {path}") commit.changes[path] = FileChange( type=ActionType.ADD, new_content=action.new_file ) elif action.type is ActionType.UPDATE: if path not in orig: # 再次检查文件是否存在 raise DiffError(f"尝试更新不存在的文件: {path}") 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]: """将补丁文本解析为 Patch 对象""" # 译者注:原文 splitlines() 未处理不同换行符,改为 split('\n') lines = text.split('\n') # 移除可能的空行或仅包含空白的行 lines = [line for line in lines if line.strip() or line == ""] # 保留真正的空行 # 检查开始和结束标记 if not lines: raise DiffError("空的补丁文本") if not Parser._norm(lines[0]).startswith("*** Begin Patch"): raise DiffError("无效的补丁文本 - 缺少开始标记") # 结束标记可能后面有空行,从后向前查找 end_index = -1 for i in range(len(lines) - 1, -1, -1): if Parser._norm(lines[i]) == "*** End Patch": end_index = i break if end_index == -1: raise DiffError("无效的补丁文本 - 缺少结束标记") # 只解析标记之间的内容 parser = Parser(current_files=orig, lines=lines[1:end_index], index=0) parser.parse() return parser.patch, parser.fuzz def identify_files_needed(text: str) -> List[str]: """识别补丁文本中需要读取(更新或删除)的文件路径""" # 译者注:原文 splitlines() 问题同上 lines = text.split('\n') update_prefix = "*** Update File: " delete_prefix = "*** Delete File: " files = [] for line in lines: norm_line = Parser._norm(line) if norm_line.startswith(update_prefix): files.append(line[len(update_prefix):].strip()) elif norm_line.startswith(delete_prefix): files.append(line[len(delete_prefix):].strip()) return list(set(files)) # 去重 def identify_files_added(text: str) -> List[str]: """识别补丁文本中需要添加的文件路径""" # 译者注:原文 splitlines() 问题同上 lines = text.split('\n') add_prefix = "*** Add File: " files = [] for line in lines: norm_line = Parser._norm(line) if norm_line.startswith(add_prefix): files.append(line[len(add_prefix):].strip()) return list(set(files)) # 去重 # --------------------------------------------------------------------------- # # 文件系统辅助函数 # --------------------------------------------------------------------------- # def load_files(paths: List[str], open_fn: Callable[[str], str]) -> Dict[str, str]: """加载文件内容""" content = {} for path in paths: try: content[path] = open_fn(path) except FileNotFoundError: raise DiffError(f"加载文件失败:找不到文件 {path}") except Exception as e: raise DiffError(f"加载文件 {path} 时出错: {e}") return content def apply_commit( commit: Commit, write_fn: Callable[[str, str], None], remove_fn: Callable[[str], None], rename_fn: Callable[[str, str], None], # 译者注:增加重命名函数 ) -> None: """将 Commit 应用到文件系统""" # 先处理重命名/删除,避免冲突 moves = [] deletes = [] for path, change in commit.changes.items(): if change.type is ActionType.DELETE: deletes.append(path) elif change.type is ActionType.UPDATE and change.move_path: moves.append((path, change.move_path)) # 如果目标路径也是要删除的文件,则先删除 if change.move_path in commit.changes and commit.changes[change.move_path].type is ActionType.DELETE: remove_fn(change.move_path) # 确保目标位置是空的 # 执行删除 for path in deletes: # 如果文件同时是移动的源文件,则不在此处删除,在移动后删除 if not any(m[0] == path for m in moves): remove_fn(path) # 执行写入和移动 for path, change in commit.changes.items(): if change.type is ActionType.ADD: if change.new_content is None: raise DiffError(f"ADD 更改 {path} 缺少内容") write_fn(path, change.new_content) elif change.type is ActionType.UPDATE: if change.new_content is None: raise DiffError(f"UPDATE 更改 {path} 缺少新内容") if change.move_path: # 先写入临时文件或直接写入目标路径,然后删除源文件 # 为简单起见,先写入目标,再删除源 write_fn(change.move_path, change.new_content) if path != change.move_path: # 避免删除自身 remove_fn(path) # 删除原始文件 else: # 原地更新 write_fn(path, change.new_content) def process_patch( text: str, open_fn: Callable[[str], str], write_fn: Callable[[str, str], None], remove_fn: Callable[[str], None], rename_fn: Callable[[str, str], None], # 译者注:增加重命名函数 ) -> str: """处理补丁文本的核心逻辑""" # 预检查开始标记 if not Parser._norm(text.split('\n', 1)[0]).startswith("*** Begin Patch"): raise DiffError("补丁文本必须以 *** Begin Patch 开头") # 识别需要操作的文件 paths_needed = identify_files_needed(text) paths_added = identify_files_added(text) # 用于检查冲突 # 检查添加的文件是否已存在(如果需要严格模式) # for added_path in paths_added: # try: # open_fn(added_path) # 尝试打开 # raise DiffError(f"尝试添加的文件已存在: {added_path}") # except FileNotFoundError: # pass # 不存在是预期情况 # 加载需要读取的文件内容 orig_files = load_files(paths_needed, open_fn) # 解析补丁文本 patch, _fuzz = text_to_patch(text, orig_files) # 将补丁转换为提交对象 commit = patch_to_commit(patch, orig_files) # 应用提交到文件系统 apply_commit(commit, write_fn, remove_fn, rename_fn) return "Done!" # --------------------------------------------------------------------------- # # 默认文件系统辅助函数 # --------------------------------------------------------------------------- # def open_file(path: str) -> str: """默认的文件读取函数""" try: with open(path, "rt", encoding="utf-8") as fh: return fh.read() except FileNotFoundError: raise # 重新引发,让 load_files 处理 except Exception as e: raise DiffError(f"读取文件 {path} 时出错: {e}") def write_file(path: str, content: str) -> None: """默认的文件写入函数""" try: target = pathlib.Path(path) target.parent.mkdir(parents=True, exist_ok=True) with target.open("wt", encoding="utf-8", newline='\n') as fh: # 译者注:指定 newline fh.write(content) except Exception as e: raise DiffError(f"写入文件 {path} 时出错: {e}") def remove_file(path: str) -> None: """默认的文件删除函数""" try: pathlib.Path(path).unlink(missing_ok=True) # 允许删除不存在的文件 except Exception as e: # 对于删除操作,打印警告而不是中断可能更友好 print(f"警告:删除文件 {path} 时出错: {e}", file=sys.stderr) # raise DiffError(f"删除文件 {path} 时出错: {e}") def rename_file(old_path: str, new_path: str) -> None: """默认的文件重命名函数""" try: target = pathlib.Path(new_path) target.parent.mkdir(parents=True, exist_ok=True) pathlib.Path(old_path).rename(target) except FileNotFoundError: raise DiffError(f"重命名失败:源文件 {old_path} 不存在") except Exception as e: raise DiffError(f"重命名文件 {old_path} 到 {new_path} 时出错: {e}") # --------------------------------------------------------------------------- # # 命令行入口点 # --------------------------------------------------------------------------- # def main() -> None: import sys patch_text = sys.stdin.read() if not patch_text: print("请通过标准输入传递补丁文本", file=sys.stderr) sys.exit(1) # 译者注:添加退出码 try: result = process_patch(patch_text, open_file, write_file, remove_file, rename_file) # 译者注:传入 rename_file print(result) sys.exit(0) # 译者注:成功退出码 except DiffError as exc: print(f"错误: {exc}", file=sys.stderr) # 译者注:添加错误前缀 sys.exit(1) # 译者注:错误退出码 except Exception as e: # 捕捉其他意外错误 print(f"发生意外错误: {e}", file=sys.stderr) sys.exit(2) if __name__ == "__main__": main()
Andere wirksame Diff-Formate
Wenn Sie mit verschiedenen Diff-Formaten experimentieren möchten, haben Tests ergeben, dass sowohl das SEARCH/REPLACE-Diff-Format, das in den mehrsprachigen Aider-Benchmarks verwendet wird, als auch ein Pseudo-XML-Format ohne internes Escaping eine hohe Erfolgsquote aufweisen.
Diese Diff-Formate haben zwei wesentliche Aspekte gemeinsam: (1) sie verwenden keine Zeilennummern, und (2) sie geben sowohl den genauen Code an, der ersetzt werden soll, als auch den genauen Code, der zum Ersetzen verwendet werden soll, mit klaren Trennzeichen zwischen den beiden.
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> """