메인 콘텐츠로 건너뛰기
이 노트북은 대화형 노트북입니다. 로컬에서 실행하거나 아래 링크를 사용할 수 있습니다:

Weave 및 OpenAI를 사용한 코드 생성

적절한 구조, 문서화, 테스트를 갖춘 고품질 코드를 생성하는 것은 쉽지 않은 작업입니다. 이 가이드에서는 코드 생성 파이프라인을 구현하는 방법을 설명합니다. humaneval 테스트 스위트를 대상으로 고품질 Python 함수를 생성하는 코드 생성 파이프라인을 만드는 방법을 알아봅니다. 평가 비교와 추적에는 Weave를 사용하고, 구조화된 출력을 활용한 코드 생성에는 OpenAI의 GPT 모델을 사용합니다.
평가

왜 Weave를 사용해야 하나요?

이 튜토리얼에서는 Weave를 사용해 코드 생성 파이프라인을 구현하고 평가하는 방법을 알아봅니다. 다음 내용을 배우게 됩니다:
  1. LLM 파이프라인 추적: 코드 생성 과정의 입력, 출력, 중간 step을 기록합니다.
  2. LLM 출력 평가: 풍부한 디버깅 도구와 시각화를 활용해 생성한 코드의 평가를 만들고 비교합니다.

환경 설정

먼저, 환경을 설정하고 필요한 라이브러리를 임포트하겠습니다:
!pip install -qU autopep8 autoflake weave isort openai set-env-colab-kaggle-dotenv datasets
python
%%capture
# openai의 버그를 수정하기 위한 임시 해결 방법:
# TypeError: Client.__init__() got an unexpected keyword argument 'proxies'
# 참고: https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/15
!pip install "httpx<0.28"
python
import ast
import os
import re
import subprocess
import tempfile
import traceback

import autopep8
import isort
from autoflake import fix_code
from datasets import load_dataset
from openai import OpenAI
from pydantic import BaseModel
from set_env import set_env

import weave
from weave import Dataset, Evaluation

set_env("WANDB_API_KEY")
set_env("OPENAI_API_KEY")
python
WEAVE_PROJECT = "codegen-cookbook-example"
weave.init(WEAVE_PROJECT)
python
client = OpenAI()
python
human_eval = load_dataset("openai_humaneval")
selected_examples = human_eval["test"][:3]
Weave는 입력, 출력, 메타데이터를 포함한 OpenAI API 호출을 자동으로 추적합니다. 즉, OpenAI와 상호작용할 때 추가로 로깅 코드를 작성할 필요가 없습니다. Weave가 이를 백그라운드에서 자동으로 처리합니다.

구조화된 출력과 Pydantic 모델 활용하기

이 코드 생성 파이프라인에서는 OpenAI의 structured outputs mode와 Pydantic 모델을 사용해 언어 모델이 일관되고 형식이 잘 갖춰진 응답을 반환하도록 합니다. 이 접근 방식에는 다음과 같은 장점이 있습니다.
  1. 유형 안정성: 예상 출력에 맞춰 Pydantic 모델을 정의하면 생성된 코드, 프로그램 실행기, 단위 테스트에 엄격한 구조를 강제 적용할 수 있습니다.
  2. 더 쉬운 파싱: 구조화된 출력 모드를 사용하면 모델의 응답을 미리 정의한 Pydantic 모델로 직접 파싱할 수 있어 복잡한 후처리의 필요성이 줄어듭니다.
  3. 향상된 신뢰성: 기대하는 정확한 형식을 지정하면 언어 모델이 예기치 않거나 형식이 잘못된 출력을 생성할 가능성을 줄일 수 있습니다.
다음은 Pydantic 모델을 정의하고 이를 OpenAI의 구조화된 출력과 함께 사용하는 방법의 예입니다.
class GeneratedCode(BaseModel):
    function_signature: str
    function_args_with_docstring_within_triple_quotes: str
    code_logic: str

class FormattedGeneratedCode(BaseModel):
    full_code: str

코드 포매터 구현

일관되고 깔끔한 코드 출력을 위해 Weave 오퍼레이션을 사용해 CodeFormatter 클래스를 구현합니다. 이 포매터는 생성된 코드, 프로그램 실행기, 단위 테스트에 다양한 린팅 및 스타일 규칙을 적용합니다.
class CodeFormatter(BaseModel):
    @weave.op()
    def lint_code(self, code: str) -> str:
        # 이스케이프된 개행 문자를 실제 개행 문자로 교체
        code = code.replace("\\n", "\n")

        # 사용되지 않는 임포트 및 변수 제거
        code = fix_code(
            code, remove_all_unused_imports=True, remove_unused_variables=True
        )

        # 임포트 정렬
        code = isort.code(code)

        # PEP 8 형식 적용
        code = autopep8.fix_code(code, options={"aggressive": 2})

        return code

    @weave.op()
    def add_imports(self, code: str) -> str:
        tree = ast.parse(code)
        from_imports = {}
        global_names = set()

        for node in ast.walk(tree):
            if isinstance(node, ast.Name) and node.id not in dir(__builtins__):
                global_names.add(node.id)

        # 실제로 사용되는 타이핑 임포트만 추가
        typing_imports = global_names.intersection(
            {"List", "Dict", "Tuple", "Set", "Optional", "Union"}
        )
        if typing_imports:
            from_imports["typing"] = typing_imports

        # 함수 내에서 정의된 이름 제거
        function_def = next(
            node for node in tree.body if isinstance(node, ast.FunctionDef)
        )
        local_names = {arg.arg for arg in function_def.args.args}
        local_names.update(
            node.id
            for node in ast.walk(function_def)
            if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)
        )

        global_names -= local_names
        global_names -= {"sorted"}  # 내장 함수 제거

        # 임포트 구문 구성
        import_statements = []
        for module, names in from_imports.items():
            names_str = ", ".join(sorted(names))
            import_statements.append(f"from {module} import {names_str}")

        return (
            "\n".join(import_statements) + ("\n\n" if import_statements else "") + code
        )

    @weave.op()
    def format_generated_code(
        self, generated_code: GeneratedCode
    ) -> FormattedGeneratedCode:
        # 코드 부분 결합
        full_code = f"{generated_code.function_signature}\n{generated_code.function_args_with_docstring_within_triple_quotes}\n{generated_code.code_logic}"

        # 올바른 들여쓰기 적용
        lines = full_code.split("\n")
        indented_lines = []
        for i, line in enumerate(lines):
            if i == 0:  # 함수 시그니처
                indented_lines.append(line)
            elif i == 1:  # 함수 인수 (docstring)
                indented_lines.append("    " + line)
            else:  # 함수 본문
                indented_lines.append("    " + line)
        full_code = "\n".join(indented_lines)

        # 코드 린트 적용
        full_code = self.lint_code(full_code)

        # 임포트 추가
        cleaned_code = self.add_imports(full_code)

        return FormattedGeneratedCode(full_code=cleaned_code)
CodeFormatter 클래스는 생성된 코드를 정리하고 형식을 맞추기 위한 여러 Weave 오퍼레이션을 제공합니다:
  • 이스케이프된 줄바꿈 문자를 실제 줄바꿈으로 바꾸기
  • 사용되지 않는 임포트와 변수 제거
  • 임포트 정렬
  • PEP 8 형식 적용
  • 누락된 임포트 추가

CodeGenerationPipeline 정의하기

코드 생성 파이프라인
이제 핵심 코드 생성 로직을 구현해 보겠습니다: 변경 시 자동으로 버전 관리되도록 weave.Model을 사용합니다. 또한 이를 가지고 실험하고 Weave에서 쉽게 diff 및 비교할 수 있도록 model_name을 속성으로 유지합니다. 입력값과 출력값이 로깅되어 오류 추적과 디버깅에 도움이 되도록 @weave.op으로 함수 호출을 추적합니다.
class CodeGenerationPipeline(weave.Model):
    model_name: str
    formatter: CodeFormatter

    def __init__(
        self, model_name: str = "gpt-4o", formatter: CodeFormatter | None = None
    ):
        if formatter is None:
            formatter = CodeFormatter()
        super().__init__(model_name=model_name, formatter=formatter)
        self.model_name = model_name
        self.formatter = formatter

    @weave.op()
    async def predict(self, prompt: str):
        generated_code = self.generate_code(prompt)
        formatted_generated_code = self.formatter.format_generated_code(generated_code)

        return formatted_generated_code.full_code

    @weave.op()
    def generate_code(self, prompt: str) -> GeneratedCode:
        completion = client.beta.chat.completions.parse(
            model=self.model_name,
            messages=[
                {
                    "role": "system",
                    "content": "당신은 전문 Python 코드 생성기입니다.",
                },
                {"role": "user", "content": prompt},
            ],
            response_format=GeneratedCode,
        )
        message = completion.choices[0].message
        if message.parsed:
            return message.parsed
        else:
            raise ValueError(message.refusal)
CodeGenerationPipeline 클래스는 코드 생성 로직을 Weave Model로 캡슐화하며, 다음과 같은 주요 이점을 제공합니다:
  1. 자동 실험 추적: Weave는 모델의 각 run에 대한 입력, 출력, 파라미터를 자동으로 캡처합니다.
  2. 버전 관리: 모델의 속성이나 코드 변경 사항은 자동으로 버전 관리되므로, 코드 생성 파이프라인이 시간에 따라 어떻게 발전하는지에 대한 명확한 이력이 생성됩니다.
  3. 재현성: 버전 관리와 추적 덕분에 코드 생성 파이프라인의 이전 결과나 설정을 쉽게 재현할 수 있습니다.
  4. 하이퍼파라미터 관리: model_name과 같은 모델 속성이 서로 다른 run 전반에서 명확하게 정의되고 추적되므로, 실험이 쉬워집니다.
  5. Weave 생태계와의 인테그레이션: weave.Model을 사용하면 평가 및 서빙 기능 같은 다른 Weave 도구와 원활하게 인테그레이션할 수 있습니다.

평가 메트릭 구현

생성된 코드의 품질을 평가하기 위해 weave.Scorer 하위 클래스를 사용해 단순한 평가 메트릭을 구현하겠습니다. 이렇게 하면 데이터셋의 각 model_output에 대해 score를 실행합니다. model_outputweave.Modelpredict 함수 출력에서 가져옵니다. prompthuman-eval 데이터셋에서 가져옵니다.
CODE_TEMPLATE = """
{model_output}

{test}

if __name__ == "__main__":
    check({entry_point})
"""
python
@weave.op()
async def score_humaneval_test(test: str, entry_point: str, output: str):
    generated_code = output

    # 테스트 문자열에서 테스트 케이스 추출
    test_cases = re.findall(r"assert.*", test)
    test_cases_str = "\n            ".join(test_cases)

    # 전체 소스 코드 생성
    full_code = CODE_TEMPLATE.format(
        model_output=generated_code,
        test=test,
        test_cases=test_cases_str,
        entry_point=entry_point,
    )

    # 코드를 저장할 임시 파일 생성
    with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as tmp_file:
        # 생성된 코드를 임시 파일에 작성
        tmp_file.write(full_code.encode())
        tmp_file_path = tmp_file.name

    try:
        # 타임아웃을 설정하여 임시 Python 파일을 서브프로세스로 실행
        result = subprocess.run(
            ["python", tmp_file_path],
            capture_output=True,
            text=True,
            timeout=10,  # 타임아웃 10초
        )

        print(result)

        if result.returncode == 0:
            return {"correct": True}
        else:
            return {"correct": False, "error": result.stderr, "output": result.stdout}
    except subprocess.TimeoutExpired:
        return {"correct": False, "error": "TimeoutExpired"}
    except Exception as e:
        return {"correct": False, "error": traceback.format_exc()}
    finally:
        # 실행 후 임시 파일 삭제 보장
        os.remove(tmp_file_path)
이 평가 함수들은 생성된 코드를 실행한 뒤, 해당 코드가 데이터셋에 포함된 테스트를 통과했는지를 나타내는 불리언 값을 반환합니다.
평가

Weave 데이터셋 만들기 및 평가 실행

파이프라인을 평가하기 위해 Weave 데이터셋을 만들고 평가를 실행하겠습니다:
formatted_selected_examples = [
    {
        "task_id": task_id,
        "prompt": prompt,
        "canonical_solution": solution,
        "test": test,
        "entry_point": entry_point,
    }
    for task_id, prompt, solution, test, entry_point in zip(
        selected_examples["task_id"],
        selected_examples["prompt"],
        selected_examples["canonical_solution"],
        selected_examples["test"],
        selected_examples["entry_point"],
    )
]
python
prompt_dataset = Dataset(
    name="humaneval_code_gen_example",
    rows=[
        {
            "prompt": example["prompt"],
            "test": example["test"],
            "entry_point": example["entry_point"],
        }
        for example in formatted_selected_examples
    ],
)
weave.publish(prompt_dataset)
python
EVAL_RUN = True
python
for model_name in ["gpt-4o-2024-08-06"]:
    pipeline = CodeGenerationPipeline(model_name=model_name)
    if not EVAL_RUN:
        dataset = prompt_dataset.rows[2]
        result = await pipeline.predict(dataset["prompt"])
        score_result = await score_humaneval_test(
            dataset["test"], dataset["entry_point"], result["generated_code"].full_code
        )
    else:
        evaluation = Evaluation(
            name="minimal_code_gen_evaluation",
            dataset=prompt_dataset,
            scorers=[score_humaneval_test],
        )
        results = await evaluation.evaluate(pipeline)
이 코드는 샘플 프롬프트로 데이터셋을 생성하고, humaneval 테스트 scorer를 정의한 뒤, 코드 생성 파이프라인을 평가합니다.
최종 평가

결론

이 예제에서는 Weave와 OpenAI의 언어 모델을 사용해 코드 생성 파이프라인을 구현하는 방법을 살펴보았습니다. 구체적으로 다음을 확인했습니다.
  1. 코드 생성 프로세스의 각 step에 대한 Weave 오퍼레이션 생성
  2. 추적과 평가를 쉽게 할 수 있도록 파이프라인을 Weave Model로 래핑
  3. Weave 오퍼레이션을 사용한 맞춤형 평가 메트릭 구현
  4. 데이터셋을 생성하고 파이프라인 평가 실행
Weave의 매끄러운 인테그레이션을 통해 코드 생성 프로세스 전반에서 입력, 출력, 중간 step을 추적할 수 있으므로, LLM 애플리케이션을 디버그하고 최적화하며 평가하기가 더 쉬워집니다. Weave와 그 기능에 대한 자세한 내용은 Weave 문서를 확인하세요. 이 예제를 확장해 더 큰 데이터셋을 처리하거나, 더 정교한 평가 메트릭을 구현하거나, 다른 LLM 워크플로와 통합할 수 있습니다.