ProgramOfThought 튜토리얼
ProgramOfThought 튜토리얼
dspy.ProgramOfThought는 하위 작업을 풀기 위한 파이썬 코드를 자동으로 생성하고 다듬는 모듈이에요. 즉 LM에게 '답'을 바로 묻는 대신, 문제를 풀 코드를 짜게 하고 그 코드를 실제로 실행해 결과를 답으로 쓰는 방식이죠.
설치는 pip install -U dspy로 최신 DSPy를 받아서 따라오시면 돼요.
출처: ProgramOfThought
1) LocalSandbox 사용하기
ProgramOfThought는 LM이 생성한 코드를 실행하기 위해 파이썬 샌드박스를 통합해요. 샌드박스가 어떻게 동작하는지 간단히 보기 위해 dspy.LocalSandbox 인스턴스를 만들어 ProgramOfThought가 내부에서 실행하는 것과 같은 동작을 직접 확인해 볼게요.
import dspy
sandbox = dspy.LocalSandbox()
expr = "value = 2*5 + 4\nvalue"
answer = sandbox.execute(expr)
answer
여기서 sandbox.execute(expr)가 표현식 문자열을 받아 실행 결과를 돌려줘요. ProgramOfThought도 결국 이렇게 코드를 실행하고, 그 출력을 답에 반영해요.
2) ProgramOfThought 실연하기
입력 질문과 출력 답을 가진 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)
이제 입력 질문과 출력 답을 지정하는 간단한 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()로 확인해 볼게요.
시스템 메시지를 보면 입력 필드가 question, final_generated_code, code_output으로 구성되고, 출력 필드는 reasoning과 answer예요. 사용자 메시지에는 질문과 함께 LM이 생성한 파이썬 코드가 실려 있어요.
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)
이 코드는 calculate_expression 함수를 정의해 2*5를 먼저 곱하고 4를 더한 뒤 실행 결과를 출력해요. code_output으로 14가 나오고, LM은 reasoning과 함께 최종 답 14를 내놓아요. 보시다시피 생성된 파이썬 코드가 중간 계산을 위한 함수를 정의하고, LocalSandbox를 통해 실행하면서 정확한 답을 얻는 구조예요.
3) ChainOfThought와 비교하기
이번에는 더 복잡한 문제로 넘어가서 ProgramOfThought가 얼마나 유용한지 확인해 볼게요.
문제: 1과 30 사이의 소수의 합으로 12!을 나눠라.
꽤 어려운 계산이에요. 먼저 ChainOfThought가 어떻게 수행하는지 볼게요.
problem = "Compute 12! / sum of prime numbers between 1 and 30."
cot = dspy.ChainOfThought(BasicGenerateAnswer)
cot(question=problem).answer
ChainOfThought의 결과는 '3,710,009'예요. reasoning을 살펴보면 12! = 479,001,600 이고, 1~30 사이 소수(2,3,5,7,11,13,17,19,23,29)의 합이 129라는 것까지는 정확히 계산해요. 하지만 마지막 나눗셈 단계에서 실수해요. 실제 계산기로 확인해 보면 479,001,600 / 129 = 3713190.69767이 정답인데, ChainOfThought는 3,710,009라고 잘못 계산하죠.
이번엔 ProgramOfThought가 어떤 결과를 내는지 볼게요.
pot(question=problem).answer
결과는 '3713190.697674419'예요. LM이 생성한 코드를 보면 is_prime, sum_of_primes, factorial 함수를 정의해 각각 소수 판별, 구간 내 소수 합, 팩토리얼을 계산하고, fact_12 / sum_primes로 최종 결과를 냈어요.
def is_prime(n):
"""Check if a number is prime."""
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
def sum_of_primes(start, end):
"""Calculate the sum of prime numbers in a given range."""
return sum(num for num in range(start, end + 1) if is_prime(num))
def factorial(n):
"""Calculate the factorial of a number."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Calculate the factorial of 12
fact_12 = factorial(12)
# Calculate the sum of prime numbers between 1 and 30
sum_primes = sum_of_primes(1, 30)
# Calculate the final result
result = fact_12 / sum_primes
print(result)
파이썬 인터프리터가 코드를 정확히 실행하면서 ChainOfThought에서 실패했을 계산 오류를 ProgramOfThought가 흡수해 주는 거예요. 특히 숫자·논리 질의에서 정확도가 크게 올라가요.
4) 문맥 추론과 함께하는 계산
이번에는 복잡한 수학 서술형 문제에서 계산을 수행하는 더 어려운 예시를 볼게요.
Step 1: 위키백과 검색 헬퍼 함수 정의
ProgramOfThought 파이프라인 안에서 위키백과의 상위 결과를 가져오고 파싱하기 위해 dspy.ColBERTv2 서버를 사용할게요.
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]
Step 2: ProgramOfThought로 멀티 홉 검색
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)에 대한 문서가 포함돼 있어요. 검색 덕분에 '프랑스가 미국에 준 선물 = 자유의 여신상'이라는 걸 확인하고, 그 재질이 구리임을 알아내며, 단계적 추론으로 구리의 원자번호(29)를 얻어냈어요.
질문의 두 번째 부분은 파이썬 로직으로 분해돼, 첫 10개 소수의 자릿수 합을 프로그램적으로 계산했어요. 이 두 하위 문제를 합치면 올바른 최종 답 2025가 나와요.
더 알아보기 (Learn more)
- ProgramOfThought 모듈 레퍼런스 — 파라미터와 동작 상세
- Multi-Hop Search 튜토리얼 — 검색 기반 추론 예시