Show HN: Outlines - LLM이 유효한 JSON 등 구조화 출력을 생성하도록 보장하는 도구
(github.com/normal-computing)- Outlines는 LLM을 위한 구조화 출력 도구로, 생성 중 원하는 출력 타입을 지정해 데이터가 해당 구조와 정확히 맞도록 보장함
- 파싱, 정규식, 깨지기 쉬운 코드로 생성 후 잘못된 출력을 고치는 방식 대신, 생성 단계에서 유효한 구조를 직접 강제함
- 사용 방식은
model(prompt, output_type)형태이며,Literal["Yes", "No"],int, Pydantic model 같은 Python 타입 체계와 맞춘 출력 지정이 가능함 - 지원 출력 유형은 사전 정의 선택지, 함수 시그니처 기반 Function Calls, JSON/Pydantic 스키마, 정규식 패턴, 문법(grammars) 기반 구조 강제임
- 모델 통합은 서버 지원의 vLLM·Ollama, 로컬 모델 지원의 transformers·llama.cpp, API 지원의 OpenAI·Gemini·Dottxt로 구분됨
- 예시 워크플로는 고객 이메일을 서비스 티켓으로 변환, 상품 설명을 카테고리 데이터로 변환, 불완전한 이벤트 설명에서 구조화 정보 또는
"I don't know"반환, 문서의 사전 정의 카테고리 분류, 자연어 회의 요청을 함수 파라미터로 변환하는 방식임 - 프롬프트 템플릿은 Jinja 기반
outlines.Template.from_string과 파일 로딩을 지원하며, 복잡한 프롬프트를 코드에서 분리하고 재사용하는 용도임 - 설치는
pip install outlines로 제공되며,.txt API는 현재 early access 상태로 표시됨
댓글과 토론
Hacker News 의견들
-
이 라이브러리는 메커니즘상 어휘 공간 일부를 마스킹하고 시간 단계별로 효율적으로 진행하는 단순한 아이디어를 쓴 것으로 보이며 훌륭함
다만 기본 LLM에 출력 구조를 씌우는 라이브러리를 써본 입장에서, Llama2 같은 기본 모델이 정말 잘 동작하는지 궁금함
내 경험으로는 “전혀 아님”에 가깝고, 실제로 동작하게 하려면 특정 용도에 맞춘 지시 튜닝이 꽤 필요했음
게다가 지시 튜닝된 모델에 생성 중 사후적으로 상태 공간을 마스킹하는 것은 결국 생성 분포를 바꾸는 셈이라, 지시 튜닝에 해로울 수도 있다는 점이 직관에 어긋나 보임- 굳이 원본 llama-2를 쓸 이유가 잘 모르겠음. HF Hub에는 llama-2를 지시 미세튜닝한 강력한 버전이 매우 많고, 그런 모델들이 훨씬 일을 잘할 것임. 예를 들면 Stability-AI의 Beluga-2가 있음: https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderb...
두 번째 요점에 대해서는, 목표가 예를 들어 모델이 JSON만 생성하게 만드는 것이라면, 어떤 출력 토큰을 쓸 수 있고 없는지 제한하는 방식으로 100% 가능함 - Llama 2 13B에는 꽤 인상받았고, 오래 써볼수록 로컬 LLM 장난감 이상으로 실제 쓸모가 있을지도 모르겠다는 생각이 듦
M2 Mac에서 GPU를 쓸 수 있어서 MLC 버전을 https://github.com/simonw/llm-mlc 플러그인으로 쓰고 있음 - 적어도 코드 생성에서는 토큰 수준 생성을 유도하면 기본 모델도 크게 개선될 수 있었음
논문 “Guiding Language Models of Code with Global Context using Monitors”(https://arxiv.org/abs/2306.10763)에서 Monitor Guided Decoding을 제안했는데, LLM을 정적 분석과 연결해 타입 일관성이 있는 코드를 생성하도록 유도함
어떤 미세튜닝도 없이 특정 지점에서 정적 분석으로 토큰 수준 생성을 유도하면, 컴파일 가능성과 정답과의 일치도 모두에서 생성 코드 품질이 크게 좋아졌음. 아주 작은 모델(1.1B)도 훨씬 큰 모델(175B)보다 컴파일 가능한 코드를 더 많이 만들면서 정답 일치도도 개선했음 - “지시 튜닝된 모델에서 생성 중 사후 마스킹이 생성 분포를 바꾸는 것”이라면, 그게 테스트 주도 개발에서 하던 일 아닌가 싶음
주된 차이는 생성 함수가 LLM이 아니라 사람이었다는 점뿐임. 중간의 사람을 빼면 안 될 이유가 없어 보임 - 지시 튜닝 자체는 “사소한” 편이고, 진짜 어려운 건 경계 조건을 다루는 부분임
전통적인 코드에서 경계 조건은 말 그대로 작은 특수 케이스에 가까웠지만, LLM에서는 무엇이 엉뚱한 방향으로 튀게 만들지 알 수 없고 파싱 코드는 그 혼돈을 감당해야 함
다르게 말하면, 경계 조건으로 취급해야 하는 케이스의 비율이 극적으로 올라간 느낌임
- 굳이 원본 llama-2를 쓸 이유가 잘 모르겠음. HF Hub에는 llama-2를 지시 미세튜닝한 강력한 버전이 매우 많고, 그런 모델들이 훨씬 일을 잘할 것임. 예를 들면 Stability-AI의 Beluga-2가 있음: https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderb...
-
GPT-4는 시스템 메시지에 예시만 넣어도 유효한 JSON을 반환하게 만들 수 있고, 열 번 중 아홉 번은 동작함
하지만 여전히 확률적이고, 열 번 중 아홉 번은 충분하지 않음
가끔{"key1": "value1", "key2": "value2" for i in range(n)}같은 환각 응답을 만들기도 함
파싱 오류 메시지와 함께 다시 프롬프트하면 보통 두 번째 시도에는 해결됨
하지만 큰따옴표와 줄바꿈 문자를 이스케이프하는 것은 덜 안정적임. 여러 예시를 줘도 절반 정도만 제대로 이스케이프하고, 이스케이프 오류를 다시 프롬프트해도 성공률이 약 50%에 머묾- 오류가 났을 때 다시 프롬프트하는 방식은 Microsoft의 새 라이브러리 TypeChat도 사용함: https://github.com/microsoft/TypeChat
해당 프롬프트는 여기 있음: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
여기서 보이는 문법 기반 접근이나 https://github.com/ggerganov/llama.cpp/pull/1773 같은 방식이 훨씬 더 우아한 해법이라고 봄 - JSON보다 XML 출력을 시키는 쪽이 더 잘 됐음. (1) XML에는 실제 언어와 의미를 더 담을 수 있고 LLM이 그런 것을 좋아하며, (2) 파서를 더 관대하게 만들 수 있음
사람들이 JSON을 원한다는 건 이해하지만, 내게는 고양이를 수영시키려는 것과 비슷함. 결국 성공할 수는 있어도 자연스러운 성향은 아님 - ChatGPT의 함수 호출을 쓰면, 프롬프트에 실수하지 않는 한 GPT-4에서 유효한 JSON을 100% 얻고 있음
주된 실수는 탈출구를 제공하지 않는 것임. LLM은 올바른 답을 찾으려 하기 때문에, 텍스트들을 넣고 구조화된 데이터를 반환하라고 했는데 어떤 텍스트가 비어 있으면 올바른 답을 정하기 어려워져 환각이 생김
해결책은 인자 중 하나로textIsMissing같은 불리언을 두는 식의 탈출구를 만드는 것임. 이런 실패 양상을 감안해두면 흠 없이 동작함 - GPT-4에게 임의의 JSON 안에 샘플 PHP 코드를 넣어 반환하라고 했더니 첫 시도부터 JSON 린터를 통과하지 못했음
여러 번 재시도하고 후속 수정까지 시켰지만 검증을 통과하지 못했고, 100% 유효한 JSON을 단 한 번도 생성하지 못해 결국 포기했음 - 문법 제약 생성에는 큰 장점이 두 가지 있음
첫째, 프롬프트에 너무 많은 예시를 넣을 필요가 없어 토큰을 덜 씀
둘째, 망각 문제의 영향을 덜 받음
작은 장점으로는 원하는 출력이 어디서 시작해야 하는지 정확히 제어할 수 있음. 다만 전체적으로는 좋은 부가 기능이지 아주 본질적이라고 보지는 않음
- 오류가 났을 때 다시 프롬프트하는 방식은 Microsoft의 새 라이브러리 TypeChat도 사용함: https://github.com/microsoft/TypeChat
-
LLM의 힘에서 큰 부분은 응답의 보정된 확률 분포인데, 이 기법은 아마 그 능력을 버리는 것 같음. 왜 이 정도면 충분한지 의문임
간단한 예로 LLM의 가능한 출력이 “hello world”, “food”, “hello”, “good day”뿐이고 프롬프트가 없을 때 모두 동일 확률이라고 해보자. 문법은 출력 어딘가에 공백이 있어야 한다는 제약만 둔다고 가정함
문법을 통과할 때까지 LLM 출력을 샘플링하면 “hello world”와 “good day”를 같은 확률로 받게 됨. 하지만 웹사이트의 기법을 적용하면 “hello world”가 “good day”보다 두 배 자주 나옴
핵심 문제는 어떤 답변 접두사가 유효한 응답으로 이어질 가능성이 극히 낮았을 수 있는데도, 이 기법은 성공한다고 가정하면 그 접두사로부터 유효한 응답을 만들어낸다는 점임. 적절한 곳에서 충분히 독립적이라면 괜찮겠지만, 자기회귀 모델에서는 상관된 오류가 빠르게 누적됨
JSON에 한정해 묻자면, 스키마를 지키지 못하는 응답을 만들 때 LLM이 사실 오류, 환각, 잘린 문자열, 주요 등장인물 누락 등을 더 많이 만들까 아니면 덜 만들까? 사실 오류율이 스키마 오류율과 비자명하게 관련된다면 이 경로는 보기보다 위험함. 특정 단어나 붙어버린 단어구가 LLM 출력에 큰 영향을 주는 것을 보면, 스키마 준수 같은 세부 사항도 출력의 다른 특성으로 번질 가능성이 커 보임- 이 경우처럼 객관식 생성이라면 가능한 출력 중 하나가 정규식에 맞지 않을 때 그냥 생성에서 제외하면 됨
“답변 접두사가 유효 응답으로 이어질 가능성이 매우 낮았지만, 기법이 어떻게든 유효 응답을 구성해 문제를 일으키는” 예를 생각해보려 했지만 잘 떠오르지 않음. 좋은 예가 있다면 흥미로운 연구 질문이 될 수 있음
- 이 경우처럼 객관식 생성이라면 가능한 출력 중 하나가 정규식에 맞지 않을 때 그냥 생성에서 제외하면 됨
-
관련해서 LLama.cpp가 지난달 문법 기반 샘플링을 구현했음
https://news.ycombinator.com/item?id=36819906
https://github.com/ggerganov/llama.cpp/pull/1773- 위 논문에서 설명한 것처럼 우리 접근도 문법 기반 샘플링으로 확장할 수 있음. 관련 PR: https://github.com/normal-computing/outlines/pull/178
우리 방식이 훨씬 효율적임. llama.cpp는 각 단계에서 마스크를 만들기 위해 전체 어휘 약 5만 토큰을 순회함
우리는 초기화 때 인덱스를 만들고, 각 단계에서 마스크를 구성할 때는 딕셔너리 조회만 하면 됨. 속도를 메모리와 맞바꾸는 방식이고, 샘플링은 표준 샘플링만큼 빠름 - 비슷한 시기에 우리도 문법 주도 유도 구현을 갖고 있었음: https://github.com/normal-computing/outlines/pull/131
관련 논문이 많았던 것을 보면 다른 곳도 여럿 있었을 것임. 여기와 현재 작업의 요점은 매우 낮은 비용의 유도를 제공하는 것이고, 정규식 사례에서는 한동안 구현되어 있었으며 이후 JSON으로 확장했음
- 위 논문에서 설명한 것처럼 우리 접근도 문법 기반 샘플링으로 확장할 수 있음. 관련 PR: https://github.com/normal-computing/outlines/pull/178
-
이런 걸 만들어줘서 고맙고, 동작 원리는 너무 당연한 아이디어라 1차 플랫폼들이 아직 하지 않았다는 점이 놀라움
JSON 외에도 구조화된 입력이 필요한 다른 작업에 어떻게 쓰일 수 있을지 궁금함- LLM 기술은 지금 매우 빠른 군비 경쟁 상태이고 몇 달마다 극적으로 바뀌고 있다고 이해하고 있었음
개발자 리소스가 제한된 결과일 수도 있음. 10년 된 기술에 이런 기본 기능이 빠져 있다면 놀랍겠지만, 군비 경쟁 중인 AI 기술에서 아직 편의 기능이 빠진 것은 납득 가능해 보임 - 이 접근을 문법 기반 샘플링으로 확장했고, 위에 링크된 논문에서 설명함. 관련 PR은 https://github.com/normal-computing/outlines/pull/178임
- https://arxiv.org/abs/2306.10763의 “Guiding Language Models of Code with Global Context using Monitors”는 언어 모델이 환각된 역참조 없이 코드를 생성하게 하는 방법을 보여줌
- LLM 기술은 지금 매우 빠른 군비 경쟁 상태이고 몇 달마다 극적으로 바뀌고 있다고 이해하고 있었음
-
이게 다음 프로젝트들과 어떻게 다른지 잘 모르겠음
https://github.com/1rgs/jsonformer
https://github.com/newhouseb/clownfish
https://github.com/mkuchnik/relm
https://github.com/ggerganov/llama.cpp/pull/1773
https://github.com/Shopify/torch-grammar
전반적으로 이런 로짓 기반 유도 시스템은 아주 많고, 크게 traction을 얻지 못하는 이유는 최고 성능 모델들이 이런 세밀한 접근을 허용하지 않는 REST API 뒤에 있기 때문임
그 모델들이 훨씬 더 성능이 좋아서, 사람들은 대체로 올바른 형식이 나올 때까지 다시 요청하는 방식에 만족함. GPT-4에서는 내 경험상 그런 실패가 꽤 드물기도 함- clownfish와 relm을 알려줘서 고마움. 내가 알기로 다른 라이브러리들은 생성의 매 단계마다 전체 어휘를 순회함
우리는 초기화 때 어휘를 한 번 순회해 인덱스를 만들고, 이후 생성은 표준 생성만큼 빠름
- clownfish와 relm을 알려줘서 고마움. 내가 알기로 다른 라이브러리들은 생성의 매 단계마다 전체 어휘를 순회함
-
다른 식으로 설명하면, LLM이 토큰을 하나 생성할 때마다 다음 토큰이 유효한 JSON 토큰만 되도록 로짓 편향 “마스크”를 갱신하는 건가? 아주 멋짐
- 전체 문자열이 유효해질 때까지 계속 생성해야 할 텐데, 만약 루프에 걸리면 어떻게 되는지 모르겠음
이 방식이 어떻게 정말 100%를 보장할 수 있는지 확신이 안 듦 - 맞음. 그리고 전체 어휘를 순회하지 않고 딕셔너리 조회로 마스크를 갱신할 수 있음. 전체 순회는 느림
- JSON 토큰이 로짓과 정확히 대응하지 않기 때문에, 어떤 형태의 빔 탐색이나 거절 샘플링도 필요함
수정: 논문에서 이 부분을 더 조심스럽게 설명하고 있음 - 사실 아주 오래된 기법이고, 많은 라이브러리가 이걸 함. 이 라이브러리에서 뭐가 그렇게 대단한지 잘 모르겠음
- 전체 문자열이 유효해질 때까지 계속 생성해야 할 텐데, 만약 루프에 걸리면 어떻게 되는지 모르겠음
-
이 Brandon Willard가 Detroit의 브레이크댄서 Brandon Willard인가?
수정: 맞음! https://brandonwillard.github.io/- 맞음. 꽤 먼 과거지만 정말 재미있었음
-
remilouf, 본인 배경이 “확률적, 관계형, 기호적 프로그래밍”이라고 했으니, 정규 문법이나 문맥 자유 문법, 사실상 어떤 수준의 문법에서도 텍스트를 생성하는 것이 문제가 아니라는 점은 이해하고 있을 것 같음
예를 들어 관계형 언어인 Prolog에서는 Definite Clause Grammars 표기법으로 문법만 주어지면 아주 쉽게 할 수 있음
내가 보기에는 이 접근은 사용자가 문법을 제공해야 함. 그렇다면 텍스트 생성을 위해 LLM을 쓰는 장점이 무엇인지 궁금함
그냥 문법을 생성기로 실행해서 원하는 텍스트를 만들면 안 되는 이유가 뭘까? 그러면 애초에 LLM을 훈련하는 큰 수고와 비용을 줄일 수 있음. 구조화된 텍스트만 생성하려는 것이라면 자연어 모델인 LLM이 왜 필요한지도 의문임- 그러면 완전히 무작위지만 유효한 출력이 나오지 않나? 여기서는 요청과 관련된 유효한 출력이 필요함
구조화된 텍스트만 원하더라도 LLM이 필요한 이유는, 사람이 쓴 비구조화 텍스트를 파싱해 기계가 쓸 수 있는 구조화 데이터를 반환하기 위해서임 - 목표는 문법에 맞는 아무 무작위 문자열을 생성하는 것이 아님
요청이 “파이의 첫 10자리 숫자는?”이고 응답을 정규식"[0-9]+\.[0-9]+"로 제한했다면, 단지 패턴에 맞는 “1.2346789” 같은 무작위 문자열이 아니라 실제 정답인 3.1415926535를 받는 것이 목표임 - IanCal이 다 말했음. 다만 miniKanren과 함께 LLM을 쓰는 대안적 접근은 https://arxiv.org/abs/1809.02840에서 볼 수 있음
- 그러면 완전히 무작위지만 유효한 출력이 나오지 않나? 여기서는 요청과 관련된 유효한 출력이 필요함
-
흥미롭고, 최근에 우리도 llama 출력이 TypeScript 인터페이스와 맞도록 제한하는 비슷한 도구를 만들었음[1]
출력 형식 보장은 LLM이 실제 비장난감 용도로 쓰이는 앞으로의 수십 년 동안 중요해질 것이라고 굳게 믿음
[1] https://github.com/ggerganov/llama.cpp/discussions/2494