튜토리얼: ProgramOfThought

튜토리얼: ProgramOfThought

dspy.ProgramOfThought는 하위(downstream) 작업을 해결하기 위한 Python 코드를 자동으로 생성하고 개선해요.

최신 DSPy를 pip install -U dspy로 설치하고 따라와 보세요.

출처: 문서

본문

1) LocalSandbox 사용하기 (Using LocalSandbox)

ProgramOfThought는 LM이 생성한 코드를 실행하기 위해 적응된 Python 샌드박스를 통합해요.

샌드박스가 어떻게 동작하는지 보여주는 간단한 예로, dspy.LocalSandbox 인스턴스를 만들고 ProgramOfThought의 내부 실행을 살펴볼게요.

import dspy
sandbox = dspy.LocalSandbox()
expr = "value = 2*5 + 4\nvalue"
answer = sandbox.execute(expr)
answer

2) ProgramOfThought 시연하기 (Demonstrating ProgramOfThought)

예로 들어, 입력 question과 출력 answer를 가진 signature를 정의할게요. 그런 다음 ProgramOfThought 프로그램을 만들고 호출해 볼 텐데, 이 프로그램은 LM을 사용해 먼저 질문을 나타내는 코드를 생성하고, 인터프리터를 사용해 코드를 실행한 뒤, 최종 결과를 질문에 대한 답변으로 출력합니다.

Meta의 Llama-3-70b-Instruct를 사용해 볼게요. 다른 제공자나 로컬 모델로 쉽게 바꿀 수 있어요.

llama31_70b = dspy.LM("openai/meta-llama/Meta-Llama-3-70b-Instruct", api_base="API_BASE", api_key="None")

dspy.configure(lm=llama31_70b)

이제 입력 question과 출력 answer를 지정하는 간단한 signature로 모듈을 정의해 볼게요. 그런 다음 signature에 ProgramOfThought를 호출하고 샘플 문제를 전달할 수 있어요.

class BasicGenerateAnswer(dspy.Signature):
    question = dspy.InputField()
    answer = dspy.OutputField()

pot = dspy.ProgramOfThought(BasicGenerateAnswer)
problem = "2*5 + 4"
pot(question=problem).answer
'14'

좋아요! 모듈이 동일한 정답을 성공적으로 만들어냈네요. LM을 사용해 정확히 어떻게 그렇게 했는지 살펴볼게요:

dspy.inspect_history()
[2025-01-06T21:58:40.879405]

System message:

Your input fields are:
1. `question` (str)
2. `final_generated_code` (str): python code that answers the question
3. `code_output` (str): output of previously-generated python code

Your output fields are:
1. `reasoning` (str)
2. `answer` (str)

...

[[ ## question ## ]]
2*5 + 4

[[ ## final_generated_code ## ]]
def calculate_expression():
    # Multiply 2 and 5
    multiplication_result = 2 * 5
    
    # Add 4 to the result
    final_result = multiplication_result + 4
    
    return final_result

# Execute the function to get the final answer
answer = calculate_expression()
print(answer)

[[ ## code_output ## ]]
14

...

생성된 Python 코드가 중간 계산을 위한 함수를 정의하고, LocalSandbox를 통한 실행으로 최종 답변을 반환해 정답을 얻는 것을 볼 수 있어요.

3) ChainOfThought와 비교하기 (Comparing with ChainOfThought)

이제 ProgramOfThought 모듈이 얼마나 유용한지 보여주는 더 복잡한 문제로 넘어가 볼게요.

문제: 12! / (1과 30 사이의 소수의 합)을 계산하세요.

상당히 도전적인 계산이에요. 먼저 ChainOfThought가 어떻게 수행하는지 살펴볼게요:

problem = "Compute 12! / sum of prime numbers between 1 and 30."

cot = dspy.ChainOfThought(BasicGenerateAnswer)
cot(question=problem).answer
'3,710,009'

ChainOfThought는 단계를 거쳐 추론하는 데 꽤 잘하며, 12!과 1-30 사이의 소수의 합에 대해 올바른 값을 얻는 모습을 볼 수 있어요.

하지만 마지막 나눗셈 단계에서 실패해, 479,001,600 / 129 = 3,710,009라고 잘못 계산했어요. (실제 계산기로 검증한) 올바른 답은 3713190.69767입니다!

ProgramOfThought는 어떨지 살펴볼게요:

pot(question=problem).answer
'3713190.697674419'

Python 인터프리터가 코드를 정확하게 실행하면서, ProgramOfThought는 ChainOfThought에서 실패할 수 있는 계산 오류를 완화해, 특히 수치적·논리적 질의에서 정확성을 개선해요.

3) 맥락 추론을 통한 계산 (Computation with Contextual Reasoning)

이제 복잡한 수학 응용문제에서 계산을 수행하는 더 복잡한 예제를 시도해 볼게요.

1단계: Wikipedia 검색을 위한 헬퍼 함수 정의

dspy.ColBERTv2 서버를 사용해 Wikipedia에서 상위 매치를 검색하고, ProgramOfThought 파이프라인 안에서 이를 파싱할 거예요.

def search_wikipedia(query: str):
    results = dspy.ColBERTv2(url='http://20.102.90.50:2017/wiki17_abstracts')(query, k=3)
    return [x['text'] for x in results]

2단계: ProgramOfThought를 이용한 다중 홉 검색 (Multi-Hop Search)

Multi-Hop Search 작업에서 아이디어를 얻어, 질문과 검색된 컨텍스트가 주어졌을 때 정확한 계산을 보장하기 위해 최종 generate_answer 계층을 ChainOfThought 대신 ProgramOfThought를 사용하도록 살짝 바꿀 거예요.

정보를 수집하기 위해 검색이 필요하고, 그 사실들을 이용해 계산을 수행해 최종 결과를 만들어내는 도전적인 응용문제를 제시해 볼게요.

class GenerateAnswer(dspy.Signature):
    """Answer questions with short factoid answers."""

    context = dspy.InputField(desc="may contain relevant facts")
    question = dspy.InputField()
    answer = dspy.OutputField(desc="often between 1 and 5 words")


class GenerateSearchQuery(dspy.Signature):
    """Write a simple search query that will help answer the non-numerical components of a complex question."""

    context = dspy.InputField(desc="may contain relevant facts")
    question = dspy.InputField()
    query = dspy.OutputField()

from dspy.dsp.utils import deduplicate

class MultiHopSearchWithPoT(dspy.Module):
    def __init__(self, num_hops):
        self.num_hops = num_hops
        self.generate_query = dspy.ChainOfThought(GenerateSearchQuery)
        self.generate_answer = dspy.ProgramOfThought(GenerateAnswer, max_iters=3)

    def forward(self, question):
        context = []
        for _ in range(self.num_hops):
            query = self.generate_query(context=context, question=question).query
            context = deduplicate(context + search_wikipedia(query))
        prediction = self.generate_answer(context=context, question=question)
        return dspy.Prediction(context=context, answer=prediction.answer)

multi_hop_pot = MultiHopSearchWithPoT(num_hops=2)
question = (
    "What is the square of the total sum of the atomic number of the metal "
    "that makes up the gift from France to the United States in the late "
    "19th century and the sum of the number of digits in the first 10 prime numbers?"
)
multi_hop_pot(question=question).answer
'2025'

검색된 컨텍스트가 Statue of Liberty와 구리(copper)에 관한 passages를 포함한다는 것을 주목하세요. 이 검색은 질문의 첫 부분에 답하는 데 도움을 주는데, 19세기 후반 프랑스가 미국에 준 선물이 자유의 여신상(Statue of Liberty)임을 식별하고, 그것이 구리로 만들어졌음을 알아내며, 단계별 추론을 통해 구리의 원자번호(29)를 찾아냅니다.

질문의 두 번째 부분은 Python 로직으로 분해되어, 처음 10개의 소수에 있는 자릿수의 합을 프로그래밍 방식으로 계산합니다.

이 두 하위 문제를 결합해, 솔루션은 결과를 올바르게 집계하고 최종 답변을 출력해요: 2025.

더 알아보기 (Learn more)