LLM에 접근하기 위한 Python 라이브러리 겸 CLI 도구인 LLM 0.32a0 알파 버전이 출시됐습니다. LLM 오랫동안 준비해온 굵직한 변경 사항들이 이번 릴리스에 담겼습니다.
이전 버전의 LLM은 프롬프트와 응답이라는 단순한 구조로 세상을 모델링했습니다. 텍스트 프롬프트를 모델에 보내면 텍스트 응답이 돌아오는 방식이었죠.
import llm model = llm.get_model("gpt-5.5") response = model.prompt("Capital of France?") print(response.text())
2023년 4월 이 라이브러리를 처음 만들 당시에는 그게 당연한 구조였습니다. 하지만 그 이후로 많은 것이 달라졌습니다.
LLM은 플러그인 시스템을 통해 수천 개의 모델을 추상화합니다. 그런데 '텍스트를 입력받아 텍스트를 반환한다'는 초기의 추상화 방식으로는 더 이상 필요한 모든 것을 표현할 수 없게 됐습니다.
시간이 지나면서 LLM은 이미지·오디오·비디오 입력을 처리하는 첨부 파일(attachments) 기능, 구조화된 JSON 출력을 위한 스키마(schemas), 그리고 툴 호출 실행을 위한 도구(tools) 기능을 차례로 갖추게 됐습니다. 한편 LLM 자체도 계속 진화하면서 추론 지원, 이미지 반환 등 다양하고 흥미로운 기능들이 추가됐습니다.
오늘날의 최신 모델들이 처리할 수 있는 다양한 입출력 타입을 LLM이 더 잘 다룰 수 있도록 발전시켜야 할 때가 됐습니다.
0.32a0 알파에는 두 가지 핵심 변경 사항이 있습니다. 모델 입력을 메시지 시퀀스로 표현할 수 있게 됐고, 모델 응답을 타입이 서로 다른 파트들의 스트림으로 구성할 수 있게 됐습니다.
LLM은 텍스트 입력을 받지만, ChatGPT가 양방향 대화 인터페이스의 가치를 증명한 이후로는 입력을 대화 턴(turn)의 연속으로 다루는 방식이 가장 일반적이 됐습니다.
첫 번째 턴은 이런 모습입니다:
user: Capital of France?
assistant:
(이후 모델이 어시스턴트의 답변 부분을 채우게 됩니다.)
그런데 이후 각 턴마다 그 시점까지의 대화 전체를 일종의 대본처럼 다시 전달해야 합니다:
user: Capital of France?
assistant: Paris
user: Germany?
assistant:
주요 벤더들의 JSON API 대부분이 이 패턴을 따릅니다. 다른 프로바이더들이 널리 모방한 OpenAI 채팅 완성(chat completions) API로 표현하면 이렇습니다:
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": "Capital of France?"
},
{
"role": "assistant",
"content": "Paris"
},
{
"role": "user",
"content": "Germany?"
}
]
}'0.32 이전의 LLM은 이를 대화(conversation)로 모델링했습니다:
model = llm.get_model("gpt-5.5") conversation = model.conversation() r1 = conversation.prompt("Capital of France?") print(r1.text()) # Outputs "Paris" r2 = conversation.prompt("Germany?") print(r2.text()) # Outputs "Berlin"
이 방식은 모델과 대화를 처음부터 직접 구성할 때는 잘 동작했지만, 이미 진행된 대화를 처음부터 주입하는 방법은 제공하지 않았습니다. 덕분에 OpenAI 채팅 완성 API의 에뮬레이션을 구축하는 것처럼 사실 간단해야 할 작업들이 필요 이상으로 복잡해졌습니다.
llm CLI 도구는 SQLite를 이용해 대화를 저장하고 복원하는 자체 메커니즘으로 이 문제를 우회했지만, 그것이 LLM API의 안정적인 일부가 된 적은 없었습니다. 또한 SQLite를 스토리지 레이어로 고집하지 않고 Python 라이브러리를 사용하고 싶은 상황도 많습니다.
새 알파 버전에서는 이제 이런 방식이 가능합니다:
import llm from llm import user, assistant model = llm.get_model("gpt-5.5") response = model.prompt(messages=[ user("Capital of France?"), assistant("Paris"), user("Germany?"), ]) print(response.text())
llm.user()와 llm.assistant()는 messages=[] 배열 안에서 사용하도록 설계된 새 빌더 함수입니다.
기존의 prompt= 옵션은 그대로 동작하며, LLM이 내부적으로 이를 단일 항목의 messages 배열로 변환합니다.
대화를 직접 구성하는 방식 외에, 이제 응답에 reply하는 방식도 지원합니다:
response2 = response.reply("How about Hungary?") print(response2) # Default __str__() calls .text()
알파의 또 다른 주요 새 인터페이스는 프롬프트 결과를 스트리밍으로 돌려받는 방식에 관한 것입니다.
이전에는 LLM의 스트리밍이 이렇게 동작했습니다:
response = model.prompt("Generate an SVG of a pelican riding a bicycle") for chunk in response: print(chunk, end="")
혹은 이런 비동기 방식으로도 사용할 수 있었습니다:
import asyncio import llm model = llm.get_async_model("gpt-5.5") response = model.prompt("Generate an SVG of a pelican riding a bicycle") async def run(): async for chunk in response: print(chunk, end="", flush=True) asyncio.run(run())
오늘날의 많은 모델은 여러 타입이 혼합된 콘텐츠를 반환합니다. 예를 들어 Claude에 프롬프트를 실행하면 추론 출력, 텍스트, 툴 호출을 위한 JSON 요청, 그리고 추가 텍스트가 차례로 반환될 수 있습니다.
일부 모델은 서버 측에서 도구를 직접 실행할 수도 있습니다. OpenAI의 코드 인터프리터 도구나 Anthropic의 웹 검색이 그 예입니다. 즉, 모델의 결과물이 텍스트, 툴 호출, 툴 출력 등 다양한 형식을 조합한 형태가 될 수 있습니다.
멀티모달 출력 모델도 등장하고 있어, 이미지나 오디오 조각이 스트리밍 응답 사이에 섞여 반환될 수 있습니다.
새로운 LLM 알파는 이를 타입이 지정된 메시지 파트의 스트림으로 모델링합니다. Python API 사용자 관점에서 보면 이렇습니다:
import asyncio import llm model = llm.get_model("gpt-5.5") prompt = "invent 3 cool dogs, first talk about your motivations" def describe_dog(name: str, bio: str) -> str: """Record the name and biography of a hypothetical dog.""" return f"{name}: {bio}" def sync_example(): response = model.prompt( prompt, tools=[describe_dog], ) for event in response.stream_events(): if event.type == "text": print(event.chunk, end="", flush=True) elif event.type == "tool_call_name": print(f"\nTool call: {event.chunk}(", end="", flush=True) elif event.type == "tool_call_args": print(event.chunk, end="", flush=True) async def async_example(): model = llm.get_async_model("gpt-5.5") response = model.prompt( prompt, tools=[describe_dog], ) async for event in response.astream_events(): if event.type == "text": print(event.chunk, end="", flush=True) elif event.type == "tool_call_name": print(f"\nTool call: {event.chunk}(", end="", flush=True) elif event.type == "tool_call_args": print(event.chunk, end="", flush=True) sync_example() asyncio.run(async_example())
출력 예시 (첫 번째 동기 예제 기준):
My motivation: create three memorable dogs with distinct “cool” styles—one cinematic, one adventurous, and one charmingly chaotic—so each feels like they could star in their own story.
Tool call: describe_dog({"name": "Nova Jetpaw", "bio": "A sleek silver-gray whippet who wears tiny aviator goggles and loves sprinting along moonlit beaches. Nova is fearless, elegant, and rumored to outrun drones just for fun."}
Tool call: describe_dog({"name": "Mochi Thunderbark", "bio": "A fluffy corgi with a dramatic black-and-gold bandana and the confidence of a rock star. Mochi is short, loud, loyal, and leads a neighborhood 'security patrol' made entirely of squirrels."}
Tool call: describe_dog({"name": "Atlas Snowfang", "bio": "A massive white husky with ice-blue eyes and a backpack full of trail snacks. Atlas is calm, heroic, and always knows the way home—even during blizzards, fog, or confusing camping trips."}
응답이 끝난 후 response.execute_tool_calls()를 호출해 요청된 함수들을 직접 실행하거나, response.reply()를 전송해 도구를 호출하고 그 반환값을 모델에 다시 전달할 수 있습니다:
print(response.reply("Tell me about the dogs"))
토큰 타입별 스트리밍이 가능해지면서, CLI 도구에서 "thinking" 텍스트를 최종 응답 텍스트와 다른 색상으로 표시할 수 있게 됐습니다. thinking 텍스트는 stderr로 출력되므로 다른 도구로 파이프할 때 결과에 영향을 주지 않습니다.
다음 예시는 Anthropic 모델이 추론 텍스트를 응답의 일부로 반환하기 때문에, 스트리밍 이벤트 방식으로 업데이트된 llm-anthropic 플러그인과 함께 Claude Sonnet 4.6을 사용합니다:
llm -m claude-sonnet-4.6 'Think about 3 cool dogs then describe them' \
-o thinking_display 1
새로 추가된 -R/--no-reasoning 플래그로 추론 토큰 출력을 숨길 수 있습니다. 이 릴리스에서 CLI에 노출된 변경 사항이 이것 하나뿐이라는 게 의외였습니다.
앞서 언급했듯이, 현재 LLM은 대화를 SQLite에 저장하는 코드가 상당히 경직되어 있습니다. 0.32a0에서는 Python API 사용자가 자체적인 대안을 구현할 수 있도록 새 메커니즘을 추가했습니다:
serializable = response.to_dict() # serializable is a JSON-style dictionary # store it anywhere you like, then inflate it: response = Response.from_dict(serializable)
이 메서드가 반환하는 딕셔너리는 사실 새 llm/serialization.py 모듈에 정의된 TypedDict입니다.
몇 가지 플러그인을 업그레이드하고 새 설계를 실제 환경에서 며칠간 검증하기 위해 우선 알파로 릴리스합니다. 알파 테스트 중 설계상의 결함이 발견되지 않는 한, 안정 버전 0.32는 이 알파와 크게 다르지 않을 것입니다.
아직 남은 큰 작업이 하나 있습니다. 새 추상화가 반환하는 더 세분화된 정보들을 더 잘 담아낼 수 있도록 SQLite 로깅 시스템을 재설계하고 싶습니다.
이상적으로는 그래프 구조로 모델링하고 싶습니다. OpenAI 스타일의 채팅 완성 API처럼 같은 대화가 매 프롬프트마다 계속 확장되고 반복되는 상황을 가장 잘 지원하기 위해서입니다. 데이터베이스에 중복 저장 없이 이를 보관할 수 있어야 합니다.
이 기능을 0.32에 포함할지, 아니면 0.33으로 미룰지는 아직 결정하지 못했습니다.
이 블로그의 장문 아티클만 보고 계신다면, /atom/everything/을 구독하면 모든 포스트를 받아보실 수 있습니다. 다른 구독 옵션도 확인해 보세요.