나만의 LM 엔진 만들기

나만의 LM 엔진 만들기 (Custom LM Engines)

DSPy 3.4 개발 API부터는 dspy.LM에 직접 만든 실행 엔진을 끼워 넣을 수 있어요. 이때 필요한 최소 동기 인터페이스는 다음처럼 생겼습니다.

class MyEngine:
    def complete(self, request: dspy.lm15.Request) -> dspy.lm15.Response:
        ...

DSPy가 프로그램의 입력을 받아 lm15 요청으로 바꾸고, 여러분의 엔진을 호출한 뒤 그 답을 Prediction으로 파싱해 줘요. BaseLM을 상속받거나 OpenAI SDK 객체를 돌려줄 필요가 없어요.

출처: Custom LM Engines

DSPy 3.5 컷오프

사용자 정의 BaseLM.forward()/aforward() 통합, LegacyEngine/AsyncLegacyEngine, complete_legacy() 단축어는 3.4에서 폐기(deprecated)됐고 3.5에서 제거될 예정이에요. 이 문서에 나온 request/response 엔진 계약대로 구현해 주세요. 레거시 래퍼를 쓰더라도 마이그레이션 시한이 늘어나진 않아요. OpenAI 스타일의 lm(messages=[...]) 호출도 사라질 예정이고요. lm("hello")는 여전히 리스트를 돌려주는 편의 함수로 남지만, 어댑터는 lm(Request(...))를 쓰고 Response를 직접 소비해요. 자세한 내용은 마이그레이션 가이드를 참고하세요.

예상되는 백엔드 실패는 dspy.lm15AuthErrorRateLimitError처럼 구체적인 오류로 던져야 해요. 그러면 DSPy가 이것들을 공개 LMError 계열로 바꾸고 재시도를 직접 관리해요. 예상 밖의 예외는 원래 원인을 그대로 보존하며, 메시지 문구만 보고 '다시 시도하면 되겠다'고 추측하지 않아요. errors and retry ownership을 참고하세요.

Pi CLI를 커스텀 엔진으로 감싸기

이 튜토리얼은 Pi CLI를 커스텀 엔진으로 감싸는 예시를 보여줘요. Pi는 자기만의 시스템 프롬프트와 도구를 그대로 유지하니까, DSPy 프로그램이 Pi에게 저장소를 조사해 달라고 부탁할 수 있어요. 도구 호출까지 포함한 에이전트의 전체 실행이 곧 DSPy LM 응답 하나가 되는 거예요.

준비 사항

  • 커스텀 engine= 인터페이스를 지원하는 DSPy 빌드
  • Pi가 설치되어 PATH에서 pi로 실행 가능하고 인증까지 되어 있는 상태
  • Pi 계정에서 쓸 수 있는 모델. 예시에서는 openai-codexgpt-6-astra를 쓰는데, 필요하면 이 두 CLI 인자만 바꾸면 돼요.
  • 조사할 로컬 Git 저장소

Pi에는 파일시스템과 셸 접근 권한이 있어요

이 예시는 Pi의 평소 도구·설정·발견된 리소스를 그대로 살려 두어요. Pi는 명령을 실행하고 파일을 수정할 수 있어요. "아무것도 수정하지 말라"고 부탁하는 건 지시일 뿐, 샌드박스가 아니에요. 반드시 신뢰할 수 있는 저장소에만 실행하고, 격리가 필요하면 OS 수준 샌드박스를 쓰세요.

엔진 정의하기

Pi의 print 모드는 자기 도구 루프를 돌리고 최종 어시스턴트 텍스트를 stdout으로 써요. 그 텍스트를 lm15 응답으로 감싸면 돼요. 이 데모에서는 이벤트 스트림 파싱이 필요 없어요.

DSPy의 시스템 지시문은 --append-system-prompt로 덧붙여요. 이 옵션은 Pi의 평소 시스템 프롬프트는 보존하면서, 답을 DSPy용으로 어떻게 포맷해야 하는지만 알려줘요.

import subprocess
import dspy
from dspy.lm15 import Message, Response, Usage


class PiEngine:
    def complete(self, request):
        result = subprocess.run(
            ["pi", "--print", "--no-session",
             "--provider", "openai-codex", "--model", "gpt-6-astra",
             "--append-system-prompt", request.system or ""],
            input=request.messages[-1].text,
            text=True, capture_output=True, check=True, timeout=120,
        )
        return Response(
            id=None, model=request.model,
            message=Message.assistant(result.stdout.strip()),
            finish_reason="stop", usage=Usage(),
        )

Usage()는 실행이 0토큰을 썼다는 게 아니라, 사용량이 DSPy에 보고되지 않았다는 뜻이에요. 이 최소 래퍼는 Pi의 도구 루프에 걸친 토큰 사용량이나 비용을 집계하지 않아요.

DSPy 프로그램 실행하기

작업 디렉토리가 저장소 루트인 노트북 셀에서 다음을 실행하세요. 마지막 표현식은 print() 없이도 prediction을 보여줘요.

dspy.configure(
    lm=dspy.LM("pi", engine=PiEngine(), cache=False, num_retries=0),
    adapter=dspy.ChatAdapter(use_json_adapter_fallback=False),
)

program = dspy.Predict("question -> answer")
program(
    question="Find the largest tracked file by byte size in this repository. "
             "Use a tool to check. Report its path and size; do not modify anything."
)

여기서 "pi"는 이력용 DSPy 모델 라벨이에요. 실제 프로바이더와 모델은 엔진의 CLI 인자가 정해요.

DSPy 저장소에 대한 실행 결과는 이런 모습일 수 있어요.

Prediction(
    answer='Largest tracked file: docs/docs/tutorials/observability/mlflow_trace_ui_navigation.gif\nSize: 8,704,325 bytes.'
)

결과는 여러분 저장소에 따라 달라져요. Pi는 여전히 도구를 쓰지만, print 모드는 도구 이벤트를 노출하지 않으니 이건 점진적 스트리밍(incremental streaming)이 아니에요.

데모의 한계

  • 최소 입력 처리: 마지막 사용자 텍스트 메시지와 평문 시스템 프롬프트만 전달해요. 그 이전 메시지, 생성 옵션, DSPy가 선언한 도구는 전달하거나 검증하지 않아요. 이 데모는 demonstration 없이, 대화 이력이나 미디어 없이 쓰세요. Pi 자체 도구는 활성 상태로 남아요.
  • 동기 전용: 비동기 대응이나 스트리밍 구현이 없어요.
  • Pi 세션 미저장: --no-session이 기록물 유지를 피하지만, Pi가 자기 설정이나 자격증명을 업데이트할 수는 있어요.
  • 자동 DSPy 재생 없음: 데모 프로그램에서는 캐싱, LM 재시도, 어댑터 폴백이 꺼져 있어요. Pi 자체의 재시도·압축 설정은 그대로 적용돼요.
  • 상세 원격 측정(telemetry) 없음: print 모드는 사용량·비용·도구 이벤트를 드러내지 않아요. 이 데모는 프로세스가 성공적으로 끝나면 stop으로 표시하는데, 토큰 한도로 끝난 건지 구분하지 못해요. 권위 있는 finish reason과 어시스턴트 턴마다의 회계가 필요하면 --mode json을 쓰세요.
  • 평범한 Pi 출력: 확장 기능이 stdout에 영향을 줄 수 있어요. 통제된 운영 프로토콜이 필요하면 JSON/RPC 모드를 쓰고 신뢰하는 확장만 골라 써요.
  • 버퍼드 서브프로세스 출력: stdout과 stderr를 메모리에 모아요. 타임아웃은 직접 실행한 Pi 프로세스까지만 제한해요. 전체 자식 프로세스 트리를 관리하지는 않아요.
  • 인자 속의 시스템 지시문: 덧붙인 프롬프트는 로컬 프로세스 검사에 드러나고 OS 인자 길이 제한을 받아요. 이 최소 예시에서는 민감한 시스템 지시문을 넣지 마세요.

이것들은 커스텀 엔진 인터페이스를 보여주려는 의도적인 단순화지, 운영용 서브프로세스 백엔드가 아니에요. 더 충실한 통합이 필요하면 메시지와 프로바이더 메타데이터를 온전히 보존하고, 취소·제한된 출력 처리를 구현하며, 각 생성 옵션을 명시적으로 매핑하거나 거부하세요.

더 알아보기 (Learn more)