GPT-4.1 모델 제품군은 코딩, 명령어 준수, 긴 컨텍스트 처리 기능이 GPT-4o보다 크게 향상되었습니다. 특히 코드 생성 및 복구 작업에서 더 나은 성능을 발휘하고 복잡한 명령어를 더 정확하게 이해하고 실행하며 긴 입력 텍스트를 효율적으로 처리할 수 있습니다. 이 Hints 엔지니어링 가이드는 OpenAI의 광범위한 테스트에서 얻은 중요한 팁을 제공하며, 개발자가 이 새로운 모델 제품군의 향상된 기능을 최대한 활용할 수 있도록 돕기 위해 작성되었습니다.
문맥에 맞는 예시 제공, 가능한 한 구체적이고 명확한 지침 제공, 모델의 지능을 극대화하기 위한 힌트를 통한 모델 계획 안내 등 기존의 많은 큐잉 모범 사례는 GPT-4.1에도 여전히 유효합니다. 그러나 모델의 잠재력을 최대한 발휘하려면 기존 힌트에 대한 일부 조정이 필요할 수 있으며, GPT-4.1은 이전 모델보다 더 문자 그대로 지시를 따르도록 훈련되었습니다. 이전 모델은 사용자 및 시스템 단서의 의도를 더 느슨하게 추론하는 경향이 있었습니다. 즉, GPT-4.1은 잘 설계된 단서에 대해 고도로 제어 가능하고 반응이 빠르며, 모델이 예상대로 작동하지 않을 경우 원하는 동작을 명확하게 설명하는 모호하지 않은 문장으로 모델을 다시 정상 궤도로 돌릴 수 있습니다. 이러한 특성으로 인해 개발자는 단서를 더욱 정확하게 설계해야 하지만, 전례 없는 제어 기능을 제공합니다.
참조를 위해 몇 가지 샘플 힌트가 다음에 제공됩니다. 이 가이드는 광범위하게 적용 가능하지만 구체적인 사례는 시나리오에 맞게 조정해야 합니다. AI 엔지니어링은 본질적으로 경험적 학문이며 대규모 언어 모델은 본질적으로 비결정적이기 때문에 이 가이드를 따르는 것 외에도 유익한 평가 시스템을 구축하고 자주 반복하여 큐 엔지니어링 변경 사항이 애플리케이션 시나리오에 실질적인 이점을 가져올 수 있도록 하는 것이 좋습니다.

1. 에이전트 워크플로
GPT-4.1은 상담원 워크플로우 구축에 이상적입니다. 모델 훈련에서는 다양한 상담원 문제 해결 경로를 제공하는 데 중점을 둡니다. 이 모델에 사용된 에이전트 테스트 프레임워크는 SWE 벤치 검증 벤치마크(실제 소프트웨어 엔지니어링 문제를 해결하는 모델의 능력을 측정하는 중요한 척도)에서 비추론 모델 중 최고의 성능을 달성하여 55% 문제를 해결했습니다.
시스템 프롬프트 알림
GPT-4.1의 상담원 기능을 최대한 활용하려면 모든 상담원 프롬프트에 세 가지 주요 유형의 미리 알림을 포함시키는 것이 좋습니다. 다음 프롬프트는 상담원 코딩 워크플로우에 특별히 최적화되어 있지만 다른 일반적인 상담원 사용 사례에 맞게 쉽게 수정할 수 있습니다.
- 지속성. 모델이 다중 라운드 메시지 상호 작용을 시작하고 있음을 이해하고 사용자에게 제어권을 조기에 다시 넘기지 않도록 하세요. 예시:
你是一个代理 - 请持续工作,直到用户的查询完全解决,然后再结束你的回合并将控制权交还给用户。只有当你确定问题已解决时才能终止你的回合。
- 도구 호출. 모델이 도구를 최대한 활용하도록 장려하고 모델이 환각에 빠지거나 답을 추측할 가능성을 줄이도록 합니다. 예시:
如果你不确定与用户请求相关的文件内容或代码库结构,请使用你的工具读取文件并收集相关信息:不要猜测或编造答案。
- 계획 [선택 사항]. 원하는 경우 모델이 작업을 완료하기 위해 일련의 도구 호출을 거치는 것이 아니라 텍스트에서 각 도구 호출을 명시적으로 계획하고 반영할 수 있습니다. 예시:
你必须在每次函数调用前进行详尽的规划,并对先前函数调用的结果进行深入反思。不要仅通过函数调用来完成整个过程,这可能会影响你解决问题和进行有洞察力思考的能力。
에이전트 시나리오에서 GPT-4.1은 사용자 명령과 시스템 프롬프트에 매우 밀접하게 반응합니다. 이 세 가지 간단한 명령을 엄격하게 준수하는 모델의 내부 SWE 벤치 검증 점수가 거의 20%까지 향상되었으므로 모든 상담원 프롬프트는 세 가지 범주를 모두 포함하는 명시적인 알림으로 시작하는 것이 좋습니다. 전반적으로 이 세 가지 지침은 모델을 챗봇과 같은 상태에서 자율적이고 독립적으로 상호작용을 주도할 수 있는 보다 '능동적인' 에이전트로 변화시켰습니다.
도구 호출
이전 모델에 비해 GPT-4.1은 OpenAI API 요청 매개변수로 전달되는 도구의 효과적인 사용에 대해 더 많은 교육을 받았습니다. 개발자는 다음을 적극 권장합니다.전문화과거 일부 개발자들이 보고한 것처럼 도구 설명을 힌트에 수동으로 삽입하고 도구 호출을 처리하기 위해 별도의 파서를 작성하는 대신 도구 필드를 사용하여 도구를 전달하세요. 이는 오류를 최소화하고 모델이 도구 호출 경로의 분포 내에 유지되도록 하는 가장 좋은 방법이며, 내부 실험에서 모델을 시스템 힌트에 수동으로 주입하는 것보다 API 파싱된 도구 설명을 사용하는 SWE 벤치 검증 통과율이 2% 향상되는 것을 관찰했으며, 이는 다시 한 번 다음을 확인했습니다. 표준 API 기능 사용의 신뢰성을 다시 한 번 확인했습니다.
개발자는 도구의 용도를 알 수 있도록 도구의 이름을 명확하게 지정하고 도구의 '설명' 필드에 명확하고 자세한 설명을 추가해야 합니다. 마찬가지로 각 도구 매개변수에 대해서도 올바른 이름과 설명을 붙여야 적절한 사용을 보장할 수 있습니다. 도구가 특히 복잡하여 도구 사용 예시를 제공하려는 경우에는 '설명' 필드에 예시를 추가하는 대신 시스템 프롬프트에 # 예시 섹션을 만들고 여기에 예시를 배치하는 것이 좋으며, 예시는 상세하지만 비교적 간결하게 작성해야 합니다. 예제를 제공하면 언제 도구를 사용해야 하는지, 도구 호출 옆에 사용자 텍스트를 포함할지, 다른 입력에 어떤 매개변수를 사용할지 설명하는 데 도움이 됩니다. 도구를 사용할 때 프롬프트 놀이터 '무엇이든 생성' 섹션의 '무엇이든 생성' 기능은 새로운 도구 정의를 위한 좋은 출발점이 될 수 있습니다.
프롬프트 유도 계획 및 생각의 사슬
앞서 언급했듯이 개발자는 GPT-4.1로 구축된 에이전트에게 중단 없는 순서로 도구를 자동으로 호출하는 대신 도구 호출 사이에 계획하고 반영하도록 선택적으로 프롬프트할 수 있습니다.GPT-4.1은 추론 모델이 아니므로 대답하기 전에 내부 사고 체인을 생성하지는 않습니다. -- 하지만 개발자는 위 프롬프트에 표시된 계획 프롬프트 구성 요소의 모든 변형을 사용하여 모델이 명시적인 단계별 계획을 생성하도록 안내할 수 있습니다. 이것은 모델이 "큰 소리로 생각하기"라고 생각할 수 있습니다. SWE 벤치 검증 상담원 작업 실험에서 명시적 계획 안내는 합격률을 4%까지 높였습니다. 하지만 이 접근 방식은 응답 길이와 응답 수를 증가시킨다는 점에 유의해야 합니다. 토큰 소비하여 비용과 지연에 영향을 미칩니다.
예제 팁: SWE 벤치 검증
SWE 벤치 검증에서 최고 점수를 받는 데 사용된 상담원 팁이 워크플로 및 문제 해결 전략에 대한 자세한 안내와 함께 아래에 공유되어 있습니다. 이 일반 모델은 모든 상담원 작업에 사용할 수 있습니다.
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. 긴 컨텍스트
GPT-4.1은 구조화된 문서 구문 분석, 재정렬, 관련 없는 컨텍스트를 무시하면서 관련 정보 선택, 컨텍스트를 사용한 멀티홉 추론 등 다양한 긴 컨텍스트 작업을 위한 고성능 1백만 토큰 입력 컨텍스트 창을 제공합니다.
최적의 컨텍스트 크기
이 모델은 최대 1백만 개의 토큰에 대한 "건초더미에서 바늘 찾기" 평가에서 잘 작동하며, 관련 코드와 관련 없는 코드 및 기타 문서가 혼합된 복잡한 작업에서 매우 강력하게 작동합니다. 그러나 더 많은 항목을 검색해야 하거나 전체 컨텍스트의 상태에 따라 복잡한 추론을 수행해야 하는 경우(예: 그래프 검색 수행)에는 긴 컨텍스트 성능이 저하될 수 있습니다. 또한 매우 긴 컨텍스트를 처리하면 API 호출의 비용과 지연 시간이 크게 증가할 수 있으므로 개발자는 이를 사용할 때 절충안을 만들어야 합니다.
컨텍스트 의존성 조정
질문에 답하기 위해 외부 컨텍스트와 모델의 내부 지식이 어느 정도 필요한지 고려하세요. 때로는 모델이 개념을 연결하거나 논리적 비약을 하기 위해 자체 지식을 사용해야 하는 반면, 다른 경우에는 제공된 컨텍스트만 사용해야 하는 경우도 있습니다.
# 指令 # 仅使用内部知识: # 仅使用提供的外部上下文中的文档来回答用户查询。如果根据此上下文你不知道答案,你必须回答“我没有回答该问题所需的信息”,即使用户坚持让你回答问题。 # 结合内外部知识: # 默认情况下,使用提供的外部上下文来回答用户查询,但如果需要其他基础知识来回答,并且你对答案有信心,则可以使用一些你自己的知识来帮助回答问题。
프롬프트 구성
특히 긴 문맥을 사용하는 경우 명령어와 문맥의 배치가 성능에 영향을 미칠 수 있습니다. 프롬프트에 긴 컨텍스트가 있는 경우 제공된 컨텍스트의시작과 끝를 사용하는 것이 테스트 결과 위나 아래에 배치하는 것보다 더 효과적이라는 사실이 밝혀졌습니다. 인스트럭션을 한 번만 배치하는 것이 기본 설정인 경우 제공된 컨텍스트에 배치하면 됩니다.위아래에 놓는 것보다 더 잘 작동합니다.
3. 생각의 사슬
위에서 언급했듯이 GPT-4.1은 추론 모델이 아니지만, 모델에 단계별 사고("생각의 연쇄" 또는 CoT라고 함)를 유도하면 모델이 문제를 더 관리하기 쉬운 부분으로 세분화하여 해결하고 출력의 전반적인 품질을 개선하는 데 효과적일 수 있습니다. 하지만 더 많은 출력 토큰을 사용하는 데 따른 비용과 지연 시간이 증가한다는 대가가 따릅니다. 이 모델은 에이전트의 추론과 실제 문제 해결에 능숙하도록 훈련되어 있으므로 많은 지시가 필요하지 않습니다.
프롬프트의 마지막에 이 기본적인 생각의 사슬 지침을 추가하는 것으로 시작하는 것이 좋습니다:
...首先,仔细地一步步思考需要哪些文档来回答查询。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
이를 바탕으로 평가의 구체적인 사례와 실패 사례를 검토하여 생각의 연쇄(CoT) 프롬프트를 개선하고, 보다 명확한 지침을 통해 체계적인 계획 및 추론 오류를 해결해야 합니다. 제약이 없는 CoT 프롬프트에서는 모델이 시도하는 전략에 차이가 있을 수 있으며, 특정 접근 방식이 잘 작동하는 것으로 관찰되면 해당 전략이 프롬프트에서 확고해질 수 있습니다. 일반적으로 오류는 사용자의 의도에 대한 오해, 불충분한 맥락 수집 또는 분석, 불충분하거나 잘못된 단계별 사고에서 비롯되는 경향이 있으므로 이러한 문제를 인식하고 보다 직접적인 지시를 통해 해결하려고 노력하는 것이 중요합니다. 모델에 일련의 사고를 출력하도록 지시하면 응답 시간과 토큰 소비가 증가하므로 비용에 유의해야 합니다.
다음은 모델이 답변을 계속하기 전에 사용자 의도를 분석하고 관련 컨텍스트를 고려하는 데 보다 체계적으로 집중하도록 지시하는 샘플 프롬프트입니다.
# 推理策略 1. 查询分析:分解并分析查询,直到你确信它可能在问什么。考虑提供的上下文以帮助澄清任何模糊或令人困惑的信息。 2. 上下文分析:仔细选择并分析大量可能相关的文档。优化召回率——有些不相关也没关系,但正确的文档必须在此列表中,否则最终答案将是错误的。每个文档的分析步骤: a. 分析:分析它与回答查询的相关性如何。 b. 相关性评级:[高, 中, 低, 无] 3. 综合:总结哪些文档最相关及其原因,包括所有相关性评级为中或更高的文档。 # 用户问题 {user_question} # 外部上下文 {external_context} 首先,仔细地一步步思考需要哪些文档来回答查询,严格遵守提供的推理策略。然后,打印出每个文档的标题和 ID。接着,将 ID 格式化为一个列表。
4. 다음 지침
GPT-4.1은 뛰어난 명령어 준수 성능을 보여주며, 개발자는 이를 통해 특정 사용 사례에 맞게 출력을 정밀하게 형성하고 제어할 수 있습니다. 개발자는 종종 상담원 추론 단계, 응답 톤 및 스타일, 툴 호출 정보, 출력 형식, 피해야 할 주제 등에 대해 많은 메시지를 받게 됩니다. 하지만 이 모델은 문자 그대로 지침을 따르기 때문에 개발자는 해야 할 일이나 하지 말아야 할 일에 대한 명시적인 사양을 포함해야 할 수도 있습니다. 또한 다른 모델에 최적화된 기존 힌트는 이 모델에 직접 적용되지 않을 수 있는데, 이는 기존 지침을 더 면밀히 따르고 암시적 규칙이 더 이상 강력하게 추론되지 않기 때문입니다. 즉, 개발자는 힌트를 더 신중하게 설계해야 할 뿐만 아니라 더 많은 통제권을 확보해야 합니다.
권장 워크플로
다음은 프롬프트에서 명령을 개발하고 디버깅하는 데 권장되는 워크플로입니다:
- 높은 수준의 지침과 핵심 사항이 포함된 포괄적인 '대응 규칙' 또는 '지침' 섹션부터 시작하세요.
- 보다 구체적인 동작을 변경하려면 # 예시 문구처럼 클래스에 대한 자세한 내용을 지정하는 섹션을 추가하세요.
- 모델이 워크플로우의 특정 단계를 따르도록 하려면 정렬된 목록을 추가하고 모델에 해당 단계를 따르도록 지시하세요.
- 그래도 동작이 기대에 미치지 못하는 경우:
a. 상충되거나 불분명하거나 잘못된 지침과 예시가 있는지 확인합니다. 상충되는 지침이 있는 경우 GPT-4.1은 프롬프트 끝에 가까운 지침을 따르는 것을 선호합니다.
b. 원하는 행동을 보여주는 예시를 추가하고, 예시에서 보여지는 중요한 행동이 규칙에도 참조되도록 합니다.
c. 뇌물이나 팁과 같은 모든 상한선 또는 인센티브의 사용은 일반적으로 필요하지 않습니다. 처음에는 사용하지 말고 특정 프롬프트에 필요한 경우에만 사용하는 것이 좋습니다. 기존 프롬프트에 이러한 팁이 포함되어 있으면 GPT-4.1이 너무 엄격하게 적용될 수 있다는 점에 유의하세요.
참고: 선호하는 AI 지원 IDE를 사용하면 일관성이나 충돌을 확인하고, 예제를 추가하거나, 명령을 추가하고 예제를 업데이트하여 명령을 시연하는 등 일관된 업데이트를 하는 등 프롬프트를 반복하는 데 매우 유용할 수 있습니다.
일반적인 장애 모드
이러한 오류 모드는 GPT-4.1에만 있는 것은 아니지만 일반적인 이해와 디버깅을 위해 여기서는 공유합니다.
- 모델에 항상 특정 행동을 따르도록 지시하는 것은 때때로 해로운 결과를 초래할 수 있습니다. 예를 들어 "사용자에게 응답하기 전에 반드시 도구를 호출해야 한다"고 지시하면 모델이 도구 입력을 착각하거나 정보가 충분하지 않은 경우 null 값으로 도구를 호출할 수 있습니다. "도구를 호출할 정보가 충분하지 않은 경우 사용자에게 필요한 정보를 요청하세요"를 추가하면 이러한 상황을 완화할 수 있습니다.
- 예시 문구를 제공할 때 모델이 이러한 참조를 그대로 사용하면 사용자에게 반복적으로 느껴질 수 있습니다. 필요에 따라 지침 모델이 변경되는지 확인하세요.
- 구체적인 지침이 없는 경우 일부 모델은 자신의 결정을 설명하기 위해 추가 텍스트를 제공하거나 예상보다 많은 서식을 응답에 출력하려고 할 수 있습니다. 이러한 문제를 완화하는 데 도움이 되는 지침과 예제가 제공됩니다.
프롬프트 예시: 고객 서비스
가상의 고객 서비스 상담원에 대한 모범 사례를 보여줍니다. 다양한 규칙, 구체성, 추가 섹션을 사용하여 더 자세한 내용을 제공하고, 이전의 모든 규칙을 결합한 정확한 동작을 보여주는 예제를 관찰하세요.
다음 코드를 실행해 보세요. 사용자 메시지와 도구 호출이 표시되어야 하며, 사용자 메시지는 인사말로 시작한 다음 사용자의 응답을 다시 말하고 다음 도구 호출에 대한 참조가 이어져야 합니다. 지침을 변경하여 모델 동작을 구체화하거나 다른 사용자 메시지를 시도하여 지침이 성능을 따르는지 테스트해 보세요.
# 译者注:原文使用 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. 일반적인 조언
프롬프트 구조
참고로 프롬프트를 작성하기 위한 좋은 시작 구조는 다음과 같습니다.
# 角色和目标 # 指令 ## 更详细指令的子类别 # 推理步骤 # 输出格式 # 示例 ## 示例 1 # 上下文 # 最终指令和引导逐步思考的提示
필요에 따라 섹션을 추가하거나 제거하고 실험을 통해 사용 사례에 가장 적합한 것이 무엇인지 결정하세요.
구분 기호
다음은 프롬프트에 가장 적합한 구분 기호를 선택하기 위한 몇 가지 일반적인 가이드라인입니다. 이 문맥 유형에 대한 특별한 고려 사항은 긴 문맥 섹션을 참조하세요.
- 마크다운. 여기부터는 마크다운 제목을 사용하여 주요 섹션과 하위 섹션(최대 H4+의 더 깊은 계층 구조 포함)을 표시하는 것이 좋습니다. 인라인 역따옴표 또는 역따옴표 블록을 사용하여 코드를 정확하게 래핑하고 필요에 따라 표준 번호 매기기 또는 글머리 기호 목록을 사용하세요.
- XML. 이러한 태그의 성능도 뛰어나며, GPT-4.1에서는 XML의 정보 준수가 개선되어 시작과 끝이 포함된 섹션을 정확하게 래핑할 수 있고, 메타데이터를 태그에 추가하여 추가적인 컨텍스트를 제공할 수 있으며, 중첩을 지원할 수 있습니다. 다음은 XML 태그를 사용하여 각각 입력과 출력이 있는 샘플 섹션 내에서 예제를 중첩하는 예제입니다:
<examples> <example1 type="Abbreviate"> <input>San Francisco</input> <output>- SF</output> </example1> </examples>
- JSON. JSON 형식은 고도로 구조화되어 있으며 특히 코딩 맥락에서 잘 이해되는 모델입니다. 하지만 장황할 수 있고 이스케이프 문자를 사용해야 하므로 오버헤드가 추가됩니다.
입력 컨텍스트에 많은 문서나 파일을 추가하기 위한 가이드입니다:
- XML은 긴 컨텍스트 테스트에서 우수한 성능을 발휘합니다.
- 예: 민첩한 갈색 여우가 게으른 개를 뛰어넘다
- 이 형식은 Lee 등이 제안한 형식입니다(상담), 긴 컨텍스트 테스트에서도 우수한 성능을 발휘합니다.
- 예: ID: 1 | 제목: 여우 | 내용: 게으른 개를 뛰어넘는 민첩한 갈색 여우
- JSON은 특히 성능이 좋지 않습니다.
- 예: [{"id": 1, "title": "Fox", "content": "민첩한 갈색 여우가 게으른 개를 뛰어넘는다"}]
모델은 다양한 형식의 구조를 정확하게 이해하도록 훈련받습니다. 일반적으로 무엇이 모델이 '알아차릴 수 있는' 명확한 정보를 제공하는지 판단하여 생각하세요. 예를 들어, XML이 많이 포함된 문서를 검색하는 경우에는 XML 기반 구분 기호가 덜 효과적일 수 있습니다.
주의 사항
- 일부 고립된 사례에서는 모델이 수백 개의 항목을 하나씩 분석하는 등 매우 길고 반복적인 출력을 생성하는 것을 거부하는 것으로 관찰되었습니다. 사용 사례에 필요한 경우 모델에 이 정보를 모두 출력하도록 강력하게 지시하고 문제를 세분화하거나 보다 간결한 접근 방식을 사용하는 것을 고려하세요.
- 드물게 병렬 도구 호출이 잘못되는 경우를 발견했습니다. 이 문제를 테스트해 보고 문제가 발견되면 PARALLEL_TOOL_CALLS 매개 변수가 false로 설정되어 있습니다.
- 매우 긴 컨텍스트와 생각의 사슬은 API 호출의 비용과 지연 시간을 크게 증가시킬 수 있으므로 신중하게 평가해야 합니다.
부록: 생성된 파일과 적용된 파일 간의 차이점 비교(Diffs)
개발자들의 피드백에 따르면 정확하고 형식이 잘 갖춰진 차이(diff) 생성은 코딩 관련 작업을 지원하기 위한 핵심 기능입니다. 이를 위해 GPT-4.1 모델 제품군은 이전 GPT 모델에 비해 차이점 비교 기능이 크게 개선되었습니다. 또한 GPT-4.1은 명확한 지침과 예제가 주어지면 어떤 형식의 차이점도 잘 생성하지만, 여기에서는 모델이 광범위하게 학습된 권장 차이점 형식이 오픈 소스로 제공됩니다. 특히 초보 개발자가 자체적인 Diff 비교 형식을 만드는 데 있어 추측을 배제하는 데 도움이 될 것으로 기대됩니다.
패치 적용
추천 도구 호출을 올바르게 적용하는 방법에 대한 팁은 아래 예시를 참조하세요.
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"], }, }
참조 구현: apply_patch.py
이것은 모델 학습의 일부로 사용되는 apply_patch 도구의 참조 구현입니다. 이를 실행 가능한 것으로 만들고 모델이 명령을 실행할 셸에서 apply_patch로 사용할 수 있도록 설정해야 합니다:
#!/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()
기타 효과적인 Diff 형식
다양한 diff 형식을 실험해보고 싶다면, 테스트 결과 Aider의 다국어 벤치마크에서 사용되는 SEARCH/REPLACE diff 형식과 내부 이스케이프가 없는 의사 XML 형식 모두 성공률이 높은 것으로 나타났습니다.
이러한 다른 형식에는 두 가지 주요 공통점이 있습니다. (1) 줄 번호를 사용하지 않으며, (2) 대체할 정확한 코드와 대체하는 데 사용할 정확한 코드를 모두 제공하며 둘 사이에 명확한 구분 기호가 있습니다.
SEARCH_REPLACE_DIFF_EXAMPLE = """ path/to/file.py ``` >>>>>>> SEARCH def search(): pass ======= def search(): raise NotImplementedError() <<<<<<< REPLACE """ PSEUDO_XML_DIFF_EXAMPLE = """ <edit> <file> path/to/file.py </file> <old_code> def search(): pass </old_code> <new_code> def search(): raise NotImplementedError() </new_code> </edit> """
© 저작권 정책
이 글은 저작권이 있으며 무단으로 복제해서는 안 됩니다.
관련 문서
댓글 없음...