GPT-4.1 Official Tips Engineering Guide (version chinoise)
La famille de modèles GPT-4.1 offre des améliorations significatives en matière de codage, de respect des instructions et de capacités de traitement des contextes longs par rapport au GPT-4o. Plus précisément, ils sont plus performants dans les tâches de génération et de réparation de code, comprennent et exécutent des instructions complexes avec plus de précision et peuvent traiter efficacement des textes d'entrée plus longs. Ce guide d'ingénierie Hints rassemble des conseils importants issus de tests approfondis au sein d'OpenAI et est conçu pour aider les développeurs à tirer pleinement parti des capacités améliorées de cette nouvelle famille de modèles.
Bon nombre des meilleures pratiques classiques en matière de repères restent valables pour GPT-4.1, comme la fourniture d'exemples contextuels, des instructions aussi spécifiques et claires que possible, et l'orientation de la planification du modèle au moyen de repères afin de maximiser son intelligence. GPT-4.1 a été entraîné à suivre les instructions de plus près et plus littéralement que les modèles qui l'ont précédé. Les modèles précédents avaient tendance à déduire l'intention des indices de l'utilisateur et du système de manière plus vague. Cela signifie que GPT-4.1 est très contrôlable et réagit à des indices bien conçus - si le modèle ne se comporte pas comme prévu, une phrase claire et sans ambiguïté clarifiant le comportement souhaité suffit généralement à remettre le modèle sur la bonne voie. Cette caractéristique oblige les développeurs à être plus précis dans la conception des indices, mais elle offre également un contrôle sans précédent.
Quelques exemples de conseils sont fournis ci-après à titre de référence. Gardez à l'esprit que, bien que ce guide soit largement applicable, des pratiques spécifiques devront être adaptées au scénario. L'ingénierie de l'IA est par nature une discipline empirique, et les modèles linguistiques à grande échelle sont par nature non déterministes ; en plus de suivre ce guide, il est recommandé de construire des systèmes d'évaluation informatifs et d'itérer souvent pour s'assurer que les changements apportés à l'ingénierie des indices apportent des avantages tangibles à vos scénarios d'application.

1. les flux de travail agentiques
GPT-4.1 est idéal pour construire des flux de travail pour les agents. Dans le cadre de la formation au modèle, l'accent est mis sur la fourniture de diverses voies de résolution de problèmes par les agents. Le cadre de test d'agent utilisé pour le modèle a obtenu la meilleure performance parmi les modèles de non-inférence dans le benchmark SWE-bench Verified (une mesure importante de la capacité d'un modèle à résoudre des problèmes réels de génie logiciel), en résolvant le problème 55%.
Rappels de l'invite du système
Afin de tirer pleinement parti des capacités de l'agent GPT-4.1, il est recommandé d'inclure trois types de rappels clés dans toutes les invites de l'agent. Les invites suivantes sont optimisées spécifiquement pour les flux de codage des agents, mais peuvent être facilement modifiées pour convenir à d'autres cas d'utilisation génériques des agents.
- Persistance. Veiller à ce que le modèle comprenne qu'il entre dans une interaction de messages à plusieurs tours et l'empêcher de rendre prématurément le contrôle à l'utilisateur. Exemple :
你是一个代理 - 请持续工作,直到用户的查询完全解决,然后再结束你的回合并将控制权交还给用户。只有当你确定问题已解决时才能终止你的回合。
- Appel d'outils. Encouragez le modèle à utiliser pleinement ses outils et réduisez la probabilité qu'il hallucine ou devine les réponses. Exemple :
如果你不确定与用户请求相关的文件内容或代码库结构,请使用你的工具读取文件并收集相关信息:不要猜测或编造答案。
- Planification [facultatif]. Si vous le souhaitez, cela permet de s'assurer que le modèle planifie et réfléchit explicitement à chaque appel d'outil dans le texte, plutôt que de simplement passer par une séquence d'appels d'outils pour accomplir la tâche. Exemple :
你必须在每次函数调用前进行详尽的规划,并对先前函数调用的结果进行深入反思。不要仅通过函数调用来完成整个过程,这可能会影响你解决问题和进行有洞察力思考的能力。
Dans le scénario de l'agent, GPT-4.1 réagit très étroitement aux commandes de l'utilisateur et aux invites du système. L'adhésion stricte du modèle à ces trois commandes simples a amélioré le score interne de SWE-bench Verified de près de 20% - il est donc fortement recommandé que toutes les invites de l'agent commencent par un rappel explicite qui couvre les trois catégories. Dans l'ensemble, ces trois directives ont transformé le modèle d'un état semblable à celui d'un chatbot en un agent plus "proactif", capable de conduire l'interaction de manière autonome et indépendante.
Appels d'outils
Par rapport aux modèles précédents, le GPT-4.1 a reçu plus de formation sur l'utilisation efficace des outils transmis en tant que paramètres de demande de l'API OpenAI. Il est fortement recommandé aux développeursse spécialiserUtilisez le champ tools pour transmettre les outils, plutôt que d'injecter manuellement des descriptions d'outils dans les indices et d'écrire un analyseur séparé pour gérer les appels d'outils, comme certains développeurs l'ont signalé par le passé. C'est la meilleure façon de minimiser les erreurs et de s'assurer que le modèle reste dans la distribution dans le chemin d'appel d'outil - des expériences internes ont observé une amélioration de 2% dans le taux de réussite du SWE-bench Verified en utilisant des descriptions d'outils analysées par l'API par rapport à l'injection manuelle de modèles dans les hints du système. Cela confirme une fois de plus la fiabilité de l'utilisation des fonctionnalités standard de l'API.
Les développeurs doivent nommer clairement l'outil pour indiquer son utilisation et ajouter une description claire et détaillée dans le champ "description" de l'outil. De même, pour chaque paramètre de l'outil, une bonne dénomination et une bonne description sont nécessaires pour garantir une utilisation appropriée. Si l'outil est particulièrement complexe et que vous souhaitez fournir des exemples d'utilisation, il est recommandé de créer une section "Exemples #" dans l'invite du système et d'y placer les exemples, plutôt que de les ajouter au champ "description", qui doit rester détaillé mais relativement concis. Les exemples permettent d'illustrer quand utiliser l'outil, s'il faut inclure le texte de l'utilisateur à côté de l'appel de l'outil et quels paramètres utiliser pour les différentes entrées. N'oubliez pas qu'il est possible d'utiliser l'outil Prompt Playground La fonction "Générer n'importe quoi" de la section "Générer n'importe quoi" constitue un bon point de départ pour la définition de nouveaux outils.
Planification et chaîne de pensée induites par l'incitation
Comme nous l'avons déjà mentionné, les développeurs peuvent éventuellement inviter les agents construits avec GPT-4.1 à planifier et à réfléchir entre les invocations de l'outil, plutôt que d'invoquer silencieusement l'outil dans une séquence ininterrompue.GPT-4.1 n'est pas un modèle de raisonnement - ce qui signifie qu'il ne génère pas de chaîne de pensée interne avant de répondre. -- mais le développeur peut utiliser n'importe quelle variante du composant d'invite de planification montré ci-dessus dans l'invite pour guider le modèle afin qu'il produise un plan explicite, étape par étape. On peut considérer que le modèle "réfléchit à voix haute". Lors d'expériences avec la tâche SWE-bench Verified agent, la planification explicite guidée a augmenté le taux de réussite de 4%. Toutefois, il convient de noter que cette approche augmente la longueur de la réponse et le nombre de réponses. jeton la consommation, ce qui a une incidence sur les coûts et les délais.
Exemple d'astuce : banc d'essai SWE vérifié
Les astuces utilisées par l'agent pour obtenir le meilleur score au banc d'essai SWE vérifié sont présentées ci-dessous, accompagnées d'instructions détaillées sur le déroulement des opérations et les stratégies de résolution des problèmes. Ce modèle générique peut être utilisé pour n'importe quelle tâche d'agent.
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. contexte long
GPT-4.1 dispose d'une fenêtre contextuelle d'entrée de 1 million de jetons très performante pour une variété de tâches à contexte long, y compris l'analyse de documents structurés, le réordonnancement, la sélection d'informations pertinentes tout en ignorant le contexte non pertinent, et le raisonnement multi-sauts à l'aide du contexte.
Taille optimale du contexte
Le modèle donne de bons résultats dans les évaluations de type "aiguille dans une botte de foin" allant jusqu'à 1 million d'éléments et donne de très bons résultats dans les tâches complexes contenant un mélange de code et d'autres documents liés et non liés. Cependant, les performances des contextes longs peuvent se dégrader lorsque davantage d'éléments doivent être récupérés ou lorsqu'un raisonnement complexe reposant sur l'état de l'ensemble du contexte doit être effectué (par exemple, lors d'une recherche de graphe). En outre, le traitement de contextes très longs peut augmenter de manière significative le coût et la latence des appels d'API, et les développeurs doivent faire des compromis lorsqu'ils les utilisent.
Ajustement de la dépendance au contexte
Examinez dans quelle mesure la réponse à une question peut nécessiter un mélange de contexte externe et de connaissances internes du modèle. Parfois, le modèle doit utiliser ses propres connaissances pour relier des concepts ou faire des sauts logiques, alors que dans d'autres cas, il est censé n'utiliser que le contexte fourni.
# 指令 # 仅使用内部知识: # 仅使用提供的外部上下文中的文档来回答用户查询。如果根据此上下文你不知道答案,你必须回答“我没有回答该问题所需的信息”,即使用户坚持让你回答问题。 # 结合内外部知识: # 默认情况下,使用提供的外部上下文来回答用户查询,但如果需要其他基础知识来回答,并且你对答案有信心,则可以使用一些你自己的知识来帮助回答问题。
Organisation rapide
L'emplacement des instructions et du contexte peut affecter les performances, en particulier dans le cas de l'utilisation d'un contexte long. Si l'invite contient un long contexte, l'idéal est de placer l'instruction dans la sectiondébut et finLes tests ont montré que cette méthode est plus efficace que celle qui consiste à placer l'instruction au-dessus ou au-dessous. Si l'on préfère ne placer l'instruction qu'une seule fois, il faut alors la placer dans le contexte fournici-dessusCela fonctionne mieux que de le placer en dessous.
3. la chaîne de pensée
Comme mentionné ci-dessus, GPT-4.1 n'est pas un modèle de raisonnement, mais le fait d'inciter le modèle à réfléchir étape par étape (connu sous le nom de "chaîne de pensée" ou CoT) peut être efficace pour aider le modèle à décomposer le problème en parties plus faciles à gérer, à les résoudre et à améliorer la qualité globale de la sortie. Cela se fait au prix d'une augmentation des coûts et des temps de latence liés à l'utilisation d'un plus grand nombre de jetons de sortie. Le modèle est formé pour être performant en matière de raisonnement d'agent et de résolution de problèmes du monde réel, il n'a donc pas besoin de beaucoup d'incitations pour obtenir de bons résultats.
Il est recommandé de commencer par ajouter cette instruction de base sur la chaîne de pensée à la fin de l'invite :
...首先,仔细地一步步思考需要哪些文档来回答查询。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
Sur cette base, les messages-guides de la chaîne de pensée (CoT) devraient être améliorés en examinant des exemples spécifiques et des échecs dans l'évaluation, et les erreurs systématiques de planification et de raisonnement devraient être abordées par le biais d'instructions plus explicites. Dans les invites de la chaîne de pensée sans contrainte, il peut y avoir des différences dans les stratégies essayées par le modèle, et si l'on observe qu'une certaine approche fonctionne bien, cette stratégie peut être consolidée dans l'invite. En général, les erreurs proviennent d'une mauvaise compréhension de l'intention de l'utilisateur, d'une collecte ou d'une analyse insuffisante du contexte, ou d'un raisonnement étape par étape insuffisant ou incorrect. Le fait d'ordonner au modèle de produire une chaîne de pensée augmente le temps de réponse et la consommation de jetons, il faut donc être conscient du coût.
Vous trouverez ci-dessous un exemple d'invite demandant au modèle de se concentrer plus systématiquement sur l'analyse de l'intention de l'utilisateur et de prendre en compte le contexte pertinent avant de poursuivre la réponse.
# 推理策略 1. 查询分析:分解并分析查询,直到你确信它可能在问什么。考虑提供的上下文以帮助澄清任何模糊或令人困惑的信息。 2. 上下文分析:仔细选择并分析大量可能相关的文档。优化召回率——有些不相关也没关系,但正确的文档必须在此列表中,否则最终答案将是错误的。每个文档的分析步骤: a. 分析:分析它与回答查询的相关性如何。 b. 相关性评级:[高, 中, 低, 无] 3. 综合:总结哪些文档最相关及其原因,包括所有相关性评级为中或更高的文档。 # 用户问题 {user_question} # 外部上下文 {external_context} 首先,仔细地一步步思考需要哪些文档来回答查询,严格遵守提供的推理策略。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
4. l'instruction suivante
GPT-4.1 fait preuve d'une excellente adhérence aux instructions, que les développeurs peuvent utiliser pour façonner et contrôler précisément la sortie afin de l'adapter à leur cas d'utilisation particulier. Les développeurs sont souvent fortement sollicités pour les étapes d'inférence de l'agent, le ton et le style de la réponse, les informations d'appel de l'outil, les formats de sortie, les sujets à éviter, etc. Cependant, comme le modèle suit les instructions de manière plus littérale, les développeurs peuvent avoir besoin d'inclure des spécifications explicites sur ce qu'il faut faire ou ne pas faire. En outre, les conseils existants optimisés pour d'autres modèles peuvent ne pas être directement applicables à ce modèle, étant donné que les instructions existantes sont suivies de plus près et que les règles implicites ne sont plus déduites aussi fortement. Cela signifie que le développeur doit concevoir les conseils avec plus de soin, mais qu'il acquiert également plus de contrôle.
Flux de travail recommandé
Les étapes suivantes sont recommandées pour le développement et le débogage des commandes dans les invites :
- Commencez par une section "règles de réponse" ou "directives" générale contenant des orientations de haut niveau et des points clés.
- Si vous souhaitez modifier un comportement plus spécifique, ajoutez une section pour spécifier plus de détails sur la classe, comme la phrase d'exemple #.
- Si vous souhaitez que le modèle suive des étapes spécifiques dans son flux de travail, ajoutez une liste ordonnée et demandez au modèle de suivre ces étapes.
- Si le comportement ne répond toujours pas aux attentes :
a. Vérifiez qu'il n'y a pas d'instructions et d'exemples contradictoires, peu clairs ou incorrects. S'il existe des instructions contradictoires, le GPT-4.1 préfère suivre les instructions les plus proches de la fin de l'invite.
b. Ajouter des exemples qui démontrent le comportement souhaité ; veiller à ce que tout comportement significatif démontré dans les exemples soit également mentionné dans la règle.
c. L'utilisation de toutes les majuscules ou de mesures incitatives telles que les pots-de-vin ou les pourboires n'est généralement pas nécessaire. Il est recommandé de ne pas les utiliser au début, mais seulement lorsque cela est nécessaire pour une invite particulière. Notez que si une invite existante contient ces conseils, le GPT-4.1 risque de se focaliser sur elle de manière trop stricte.
Remarque : l'utilisation de votre IDE préféré assisté par l'IA peut être très utile pour itérer sur les invites, notamment pour vérifier la cohérence ou les conflits, ajouter des exemples ou effectuer des mises à jour cohérentes telles que l'ajout d'une commande et la mise à jour de l'exemple pour démontrer la commande.
Modes de défaillance courants
Ces modes de défaillance ne sont pas propres à GPT-4.1, mais sont présentés ici à des fins de compréhension générale et de débogage.
- Le fait de demander au modèle de toujours suivre un comportement particulier peut parfois avoir un effet néfaste. Par exemple, si l'on dit "vous devez appeler l'outil avant de répondre à l'utilisateur", le modèle peut halluciner les entrées de l'outil ou appeler l'outil avec des valeurs nulles s'il ne dispose pas de suffisamment d'informations. L'ajout de la mention "Si vous n'avez pas assez d'informations pour appeler l'outil, demandez à l'utilisateur les informations dont vous avez besoin" devrait permettre de remédier à cette situation.
- En fournissant des exemples de phrases, le modèle peut utiliser ces références mot pour mot et donner à l'utilisateur l'impression d'être répétitif. Veillez à ce que le modèle d'instruction soit modifié en fonction des besoins.
- En l'absence d'instructions spécifiques, certains modèles peuvent être tentés de fournir du texte supplémentaire pour expliquer leurs décisions ou de formater davantage la réponse que prévu. Des instructions et éventuellement des exemples sont fournis pour remédier à ce problème.
Exemple d'exercice : Service clientèle
Il s'agit d'une démonstration des meilleures pratiques pour un agent fictif du service clientèle. Observez la variété des règles, la spécificité, l'utilisation de sections supplémentaires pour fournir plus de détails, et un exemple pour démontrer le comportement exact combinant toutes les règles précédentes.
Essayez d'exécuter le code suivant - vous devriez voir un message d'utilisateur et un appel d'outil, et le message d'utilisateur devrait commencer par une salutation, puis reformuler la réponse de l'utilisateur, suivie d'une référence à un appel d'outil à venir. Essayez de modifier les instructions pour modeler le comportement du modèle, ou essayez d'autres messages utilisateur pour tester que les instructions suivent les performances.
# 译者注:原文使用 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. conseils généraux
Structure de l'invitation
À titre de référence, voici une bonne structure de départ pour l'élaboration des messages-guides.
# 角色和目标 # 指令 ## 更详细指令的子类别 # 推理步骤 # 输出格式 # 示例 ## 示例 1 # 上下文 # 最终指令和引导逐步思考的提示
Ajoutez ou supprimez des sections en fonction de vos besoins et expérimentez pour déterminer ce qui est optimal pour votre cas d'utilisation.
Délimiteurs
Voici quelques conseils généraux pour choisir le meilleur séparateur pour une invite. Pour des considérations particulières concernant ce type de contexte, voir la section Contexte long.
- Markdown. Il est recommandé à partir de maintenant d'utiliser des titres Markdown pour indiquer les sections et sous-sections principales (y compris les hiérarchies plus profondes, jusqu'à H4+). Utilisez des guillemets en ligne ou des blocs de guillemets pour envelopper le code avec précision, et utilisez la numérotation standard ou des listes à puces si nécessaire.
- XML. XML facilite l'habillage précis d'une section contenant un début et une fin, permet d'ajouter des métadonnées aux balises pour fournir un contexte supplémentaire et prend en charge l'imbrication. Les exemples suivants illustrent l'utilisation de balises XML pour imbriquer des exemples à l'intérieur d'une section type, chacun ayant des entrées et des sorties :
<examples> <example1 type="Abbreviate"> <input>San Francisco</input> <output>- SF</output> </example1> </examples>
- JSON. Le format JSON est très structuré et le modèle est bien compris, en particulier dans un contexte de codage. Cependant, il peut être plus verbeux et nécessite l'échappement de caractères, ce qui ajoute de la surcharge.
Un guide spécifique pour ajouter un grand nombre de documents ou de fichiers au contexte d'entrée :
- XML donne de bons résultats dans les tests à long terme.
- Exemple : L'agile renard brun saute par-dessus le chien paresseux
- Ce format a été proposé par Lee et al.consultation), donne également de bons résultats dans les tests à long terme.
- Exemple : ID : 1 | Title : Fox | Content : Agile brown fox jumps over lazy dog (Le renard brun agile saute par-dessus le chien paresseux)
- Les performances de JSON sont particulièrement médiocres.
- Exemple : [{"id" : 1, "title" : "Fox", "content" : "Agile brown fox jumps over lazy dog"}]
Les modèles sont formés pour comprendre la structure des différents formats de manière robuste. En général, faites preuve de discernement et réfléchissez à ce qui fournit des informations claires que le modèle "remarquera". Par exemple, si vous récupérez des documents qui contiennent beaucoup de XML, les séparateurs basés sur XML peuvent être moins efficaces.
Mises en garde
- Dans certains cas isolés, il a été observé que le modèle résiste à la production de résultats très longs et répétitifs, par exemple l'analyse de centaines d'éléments un par un. Si cela est nécessaire pour votre cas d'utilisation, demandez fermement au modèle de produire ces informations dans leur intégralité et envisagez de décomposer le problème ou d'utiliser une approche plus concise.
- A vu quelques rares cas d'appels d'outils parallèles incorrects. Il est recommandé de tester cela et, si un problème est détecté, d'envisager de déplacer l'option appels d'outils parallèles Le paramètre est fixé à false.
- Des contextes et des chaînes de pensée extrêmement longs peuvent augmenter de manière significative le coût et la latence des appels à l'API et doivent être évalués avec soin.
Annexe : Comparaison des différences entre les fichiers générés et les fichiers appliqués (Diffs)
Les commentaires des développeurs ont indiqué que la génération de disparités (diff) précises et bien formatées est une capacité clé pour soutenir les tâches liées au codage. À cette fin, la famille de modèles GPT-4.1 offre des améliorations significatives dans les capacités de comparaison des différences par rapport aux modèles GPT précédents. En outre, bien que GPT-4.1 soit capable de générer des différences dans n'importe quel format lorsqu'on lui donne des instructions et des exemples clairs, un format de diff recommandé est disponible ici, sur lequel les modèles ont été largement entraînés. Nous espérons que cela aidera les développeurs débutants à créer leurs propres formats de comparaison.
Appliquer le patch
L'exemple ci-dessous donne des conseils sur l'application correcte d'un appel à l'outil de recommandation.
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"], }, }
Implémentation de référence : apply_patch.py
Il s'agit d'une implémentation de référence de l'outil apply_patch utilisé dans le cadre de la formation au modèle. Vous devez le rendre exécutable et le rendre disponible en tant qu'apply_patch dans le shell où le modèle exécutera des commandes :
#!/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()
Autres formats de diffusion efficaces
Si vous souhaitez expérimenter différents formats de diff, des tests ont montré que le format de diff SEARCH/REPLACE utilisé dans les tests multi-langues d'Aider, ainsi qu'un format pseudo-XML sans échappement interne, ont tous deux un taux de réussite élevé.
Ces formats diff ont deux aspects clés en commun : (1) ils n'utilisent pas de numéros de ligne et (2) ils fournissent à la fois le code exact à remplacer et le code exact à utiliser pour le remplacer, avec des séparateurs clairs entre les deux.
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> """
© déclaration de droits d'auteur
L'article est protégé par le droit d'auteur et ne doit pas être reproduit sans autorisation.
Articles connexes
Pas de commentaires...