5. 자료를 밖으로 보내지 않기

주소 한 줄만 바꾸면 된다

이 차시를 마치면

패키지를 바꾸지 않고 주소만 돌려 LangGraph 를 로컬 모델에 붙입니다.

LangGraph 에서 로컬 모델을 부릅니다. 이 차시의 핵심은 한 문장입니다.

핵심

패키지를 바꾸지 않습니다. langchain-openai 를 그대로 쓰고 base_url 만 로컬 서버로 돌립니다.

왜 이것이 가능한가

LM Studio 가 OpenAI 호환 API 를 내주기 때문입니다. 같은 경로(/v1/chat/completions), 같은 요청 형식, 같은 응답 형식입니다.

그래서 코드 관점에서 달라지는 것은 주소 하나입니다. 이것이 로컬 전환의 문턱을 크게 낮춥니다. 분석

외부 API로컬
패키지langchain-openai같음
클래스ChatOpenAI같음
base_url기본값(사업자 서버)http://localhost:1234/v1
api_key실제 키아무 문자열. 검사되지 않음
modelgpt-...google/gemma-4-e4b

코드

import json
import os

from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

BASE_URL = os.environ.get("LOCAL_LLM_BASE_URL", "http://host.docker.internal:1234/v1")
MODEL = "google/gemma-4-e4b"

# 추론 토큰을 쓰는 모델이므로 넉넉히 잡는다. 부족하면 본문이 빈 채로 돌아온다.
MAX_TOKENS = 1200

ACTION_SCHEMA = {
    "type": "object",
    "properties": {
        "actions": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "task": {"type": "string"},
                    "due": {"type": "string"},
                },
                "required": ["owner", "task", "due"],
            },
        }
    },
    "required": ["actions"],
}


class NoteState(TypedDict):
    note: str
    actions: list


def make_model():
    """로컬 모델. api_key 는 형식상 필요할 뿐 검사되지 않는다."""
    return ChatOpenAI(
        base_url=BASE_URL,
        api_key="lm-studio",
        model=MODEL,
        temperature=0,
        max_tokens=MAX_TOKENS,
    )


def extract_actions(state: NoteState) -> dict:
    """회의록에서 실행항목을 뽑는다. schema 를 지정해 형식을 고정한다."""
    model = make_model().bind(
        response_format={
            "type": "json_schema",
            "json_schema": {"name": "actions", "strict": True, "schema": ACTION_SCHEMA},
        }
    )
    reply = model.invoke(
        [
            ("system", "회의록에서 실행항목을 뽑아 JSON으로만 답한다."),
            ("user", state["note"]),
        ]
    )
    return {"actions": json.loads(reply.content)["actions"]}


def build_graph():
    builder = StateGraph(NoteState)
    builder.add_node("extract", extract_actions)
    builder.add_edge(START, "extract")
    builder.add_edge("extract", END)
    return builder.compile()

실행 결과

입력한 회의록입니다.

# 8월 2주차 회의록
참석: 김, 이, 박
- 김: 3분기 매출 자료 8월 20일까지 정리
- 이: 신규 채널 후보 3곳 조사, 8월 25일
- 박: 다음 회의 일정 공지 (8월 18일)

출력입니다.

base_url: http://host.docker.internal:1234/v1
model   : google/gemma-4-e4b

추출한 실행항목 3건
  - 김 | 3분기 매출 자료 정리 | 8월 20일
  - 이 | 신규 채널 후보 3곳 조사 | 8월 25일
  - 박 | 다음 회의 일정 공지 | 8월 18일

요청은 http://host.docker.internal:1234/v1 로만 갔습니다. 외부 사업자에게 가지 않았습니다.

2026-08-15 실행 결과입니다. 검증 환경은 Python 3.12.14 · langgraph 1.2.11 · langchain-openai 1.5.1 입니다.

Structured 출력이 4B 모델에서도 됩니다

주목할 결과입니다. 파라미터 4B 짜리 모델이 세 항목을 담당자·할 일·기한으로 정확히 나눴습니다.

비결은 response_formatjson_schema 입니다. 형식을 프롬프트로 부탁하는 것이 아니라 지정합니다. 2번 트랙에서 배운 "JSON 처럼 보이게 프롬프트만 요구하는 실수" 를 피하는 방법이 여기서 실제로 쓰입니다.

    model = make_model().bind(
        response_format={
            "type": "json_schema",
            "json_schema": {"name": "actions", "strict": True, "schema": ACTION_SCHEMA},
        }
    )

strict: True 가 형식을 강제합니다. 확인 필요 모든 로컬 모델이 이 기능을 지원하는 것은 아닙니다. 쓰기 전에 그 모델로 확인해야 합니다.

주소를 어디로 쓸 것인가

어디서 실행하느냐에 따라 다릅니다. 실측 결과입니다.

실행 위치주소결과
같은 기기(호스트)http://localhost:1234/v1동작
Docker 컨테이너http://host.docker.internal:1234/v1동작
Docker 컨테이너 안http://gateway.docker.internal:1234/v1실패 (URLError)
컨테이너 안에서 localhosthttp://localhost:1234/v1실패. 컨테이너 자신을 가리킵니다

컨테이너 안의 localhost컨테이너 자신입니다. 호스트를 가리키려면 host.docker.internal 을 써야 합니다. 이 교재의 검증은 컨테이너에서 돌리므로 그 주소를 씁니다.

코드에서 환경변수로 바꿀 수 있게 해 두었습니다.

LOCAL_LLM_BASE_URL=http://localhost:1234/v1 python3 local/connect_langgraph.py

api_key 는 왜 필요한가

ChatOpenAI 가 형식상 요구하기 때문입니다. 로컬 서버는 이 값을 검사하지 않으므로 아무 문자열이나 넣어도 됩니다.

여기서 헷갈리지 말아야 할 것이 있습니다. 이 값이 아무거나여도 된다는 것은 인증이 없다는 뜻입니다. 서버가 loopback 에만 열려 있어서 같은 기기에서만 접근할 수 있을 뿐입니다. 서버를 외부 주소로 열면 누구나 쓸 수 있게 됩니다. 분석

max_tokens 를 넉넉히 잡은 이유

코드에 MAX_TOKENS = 1200 이 있고 주석에 "부족하면 본문이 빈 채로 돌아온다" 고 적혀 있습니다.

이것은 이 모델 계열의 중요한 함정입니다. 6차시에서 계측 결과와 함께 다룹니다.

직접 실행하기

cd webapp/verify
docker run --rm -v "$PWD":/work -w /work llmwiki-verify python local/connect_langgraph.py

LM Studio 서버가 켜져 있어야 합니다. 응답에 시간이 걸릴 수 있습니다.

이해도 확인

문항을 불러오는 중입니다.