8장 — LLM을 효율적으로 사용하는 방법

LLM을 ‘잘’ 쓰는 것은 곧 효율을 높이는 일입니다. 효율은 단순히 응답 속도를 높이거나 비용을 낮추는 문제가 아닙니다. 정확도, 일관성, 지연 시간, 비용, 재현성, 유지 보수성을 함께 관리하는 운영상의 능력을 의미합니다. 동일한 질문에 언제나 비슷한 품질의 답을 빠르게, 그리고 합리적 비용으로 얻을 수 있어야 실제 서비스에 투입됩니다.

효율이 중요한 이유는 셋입니다.

이유 내용
품질-비용 균형 뛰어난 답을 얻으려고 매번 대형 모델을 호출하면 금세 비용이 누적된다
예측 가능성 운영 환경에서는 답변의 편차가 리스크가 된다. 출력 형식과 길이를 통제해 재현 가능한 워크플로를 설계해야 한다
확장성 트래픽이 늘어날수록 작은 비효율이 큰 비용이 된다

책이 말하는 ‘효율적인 사용’은 다섯 가지입니다.

  1. 문제 정의를 명확히 한다 — 모델이 해야 할 일과 하지 말아야 할 일을 구분해 입력과 출력을 계약처럼 고정한다.
  2. 콘텍스트를 정밀하게 주입한다 — 불필요한 문맥을 줄이고 필요한 근거만 넣어 토큰을 절감하고 정확도를 높인다.
  3. 출력을 제어한다 — ‘수식만 반환’, ‘설명 없이 비율만’처럼 형식·길이·근거 요구를 명시해 변동성을 낮춘다.
  4. 모델을 적재적소에 배치한다 — 복잡한 추론은 대형 모델이, 요약·파싱·검색 해석 같은 단순 태스크는 경량 모델이 맡는다.
  5. 측정하고 개선한다usage_metadata와 로깅으로 품질·지연·비용을 계량화한다.

효율은 우연히 얻어지지 않습니다. ‘정확한 문제 정의 → 콘텍스트 최소화 → 출력 제약 → 모델 선택 → 계량 기반 개선’ 의 순서를 일관되게 적용할 때 품질, 속도, 비용이 함께 개선됩니다.

8.1 분할 정복

LLM 애플리케이션을 처음 개발할 때 많은 개발자가 ‘생각보다 답변이 잘 나오지 않는’ 어려움을 겪습니다. 원인은 대부분 하나의 프롬프트에 너무 많은 것을 담으려 하기 때문입니다.

자주 범하는 실수가 이런 프롬프트입니다.

bad_prompt = PromptTemplate(
    input_variables=["property_value", "ownership_period"],
    template="""당신은 종합부동산세 계산 전문가입니다. ...
다음 단계를 모두 고려해서 계산해주세요:
1. 과세표준액 계산 방법을 설명하고 계산해주세요
2. 공정시장가액비율을 적용해주세요
3. 공제액이 있다면 확인하고 적용해주세요
4. 과세표준을 계산해주세요
5. 세율을 확인하고 적용해주세요
6. 최종 세액을 계산해주세요
...""")

문제점이 넷입니다.

  • LLM이 한 번에 너무 많은 작업을 수행해야 한다.
  • 단계별로 오류가 발생할 때 디버깅이 어렵다.
  • 중간 결과를 검증하거나 수정하기가 어렵다.
  • 프롬프트가 길어질수록 LLM이 중요한 정보를 놓칠 가능성이 높아진다.

해법은 전체 작업을 작은 단위로 나누고 각각을 독립적인 프롬프트로 만드는 것, 즉 소프트웨어 개발의 분할 정복(divide and conquer) 전략입니다. 과세표준 계산, 공정시장가액비율 확인, 공제액 확인, 최종 세액 계산을 각각의 PromptTemplate으로 쪼갭니다.

이렇게 하면 모듈화·유지 보수성·디버깅 용이성·재사용성을 얻습니다. 그리고 원칙이 명확합니다.

각각의 프롬프트는 객체지향 프로그래밍 원칙처럼 단일 책임 원칙을 기준으로 작성해서 LLM이 한 번에 하나의 작업만 할 수 있도록 해야 합니다. 그리고 LangChain으로 각 단계를 체인 형태로 연결해 이전 단계의 출력이 다음 단계의 입력으로 사용되도록 해야 합니다.

8.2 LCEL의 특징

LangChain은 체인들을 더 효율적으로 연결하는 LCEL(LangChain Expression Language) 을 제공합니다. 이점은 선언적 프로그래밍, 타입 안정성, 재사용성, 병렬처리입니다.

파이프라인 연산자

LCEL은 유닉스 파이프라인과 유사하게 | 연산자로 체인을 연결합니다. 한 체인의 출력이 다음 체인의 입력으로 자동 전달됩니다.

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

simple_chain = (
    ChatPromptTemplate.from_template("주어진 숫자 {number}를 2진수로 변환해주세요")
    | llm
    | ChatPromptTemplate.from_template("다음 2진수를 16진수로 변환해주세요: {text}")
    | llm
)
simple_chain.invoke({"number": "42"})
# 2진수 `101010`을 16진수로 변환하면 `2A`입니다.

병렬 실행 — 그런데 요즘은

RunnableParallel을 쓰면 여러 체인을 동시에 실행해, 서로 독립적인 작업의 실행 시간을 크게 단축할 수 있습니다. 하나의 텍스트에 대해 요약·감정 분석·키워드 추출을 한꺼번에 돌리는 식입니다.

다만 책의 단서가 솔직합니다.

RunnableParallel은 다음 장에서 언급할 LangGraph에서 병렬처리가 가능해지면서 사용 빈도가 크게 줄었습니다. 특히 LangChain은 순차적인 작업을 처리하는 데 특화되어 있어 병렬처리에서는 큰 힘을 발휘하지 못합니다.

중간 결과 활용

RunnablePassthrough를 쓰면 체인의 중간 결과를 후속 체인에서 활용할 수 있습니다. 원본 입력을 그대로 보존하면서 요약본을 함께 만들어, 둘을 비교하는 분석 리포트를 생성하는 식입니다.

analysis_chain = (
    {
        "original": RunnablePassthrough(),  # 원본 입력을 보존
        "summary": ChatPromptTemplate.from_template("{text}를 한 문장으로 요약해주세요") | llm,
    }
    | ChatPromptTemplate.from_template("""원본 텍스트: {original}
요약: {summary}

위 내용에 대한 분석 리포트를 작성해주세요.""")
    | llm
)

종합부동산세 계산을 체인으로 조립하기

이제 7장에서 전처리해 만든 tax-markdown 벡터 저장소를 기반으로, 작은 작업들을 체인으로 엮습니다.

vector_store = Chroma(
    embedding_function=embedding,
    collection_name='tax-markdown',
    persist_directory="./tax-markdown")
retriever = vector_store.as_retriever(search_kwargs={"k": 3})

llm = ChatOpenAI(model='gpt-4o')
small_llm = ChatOpenAI(model='gpt-4o-mini')

여기서 중요한 운영 조언이 나옵니다.

비용 최적화는 애플리케이션 개발 초반부터 진행하지 않고, gpt-4o 기반으로 작성한 프롬프트를 활용해 우리가 원하는 답변을 생성할 수 있는지 확인해야 합니다. 원하는 답변이 나오는 것을 확인한 후에 gpt-4o-mini와 같은 작은 모델들을 활용해서 최적화하는 것을 추천합니다.

① 과세표준 계산식 체인

LangChain 허브에서 2천만 번 이상 다운로드된 rlm/rag-prompt 를 가져다 씁니다. 다만 “‘이럴 때는 무조건 이 프롬프트를 써야 한다’는 규칙은 없습니다. 테스트를 해보고 가장 적합한 프롬프트를 선정하면 됩니다.”

rag_prompt = hub.pull("rlm/rag-prompt")

# 청크에 포함된 메타데이터를 제거하고, 답변 생성에 활용할 문서만 하나로 엮는다.
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

tax_base_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | StrOutputParser()
)

{context}{question} 중괄호는 프로그래밍 언어의 함수 파라미터와 유사한 개념입니다. retriever가 검색해 format_docs가 엮은 결과와 RunnablePassthrough()로 전달된 원본 질문이 각각 해당 위치에 자동 삽입됩니다.

첫 실행 결과는 “괜찮은 답변”이지만 장황했습니다. LLM에게 나중에 수학적 계산을 요청해야 하므로 계산 방법은 수식으로 표현되는 편이 낫습니다. 그래서 프롬프트를 고칩니다.

tax_base_question = "주택에 대한 종합부동산세 과세표준을 계산하는 방법을 수식으로 표현해서 수식만 반환해주세요. 부연설명을 하지 말아주세요"
'주택에 대한 종합부동산세 과세표준 = (주택의 공시가격 합산 - 공제금액) × 공정시장가액비율'

‘수식만 반환해주세요’라는 문구를 추가하지 않으면 장황한 부연 설명이 추가되면서 최종 답변을 생성하는 데 어려움이 발생할 수 있습니다. 따라서 프롬프트를 작성할 때는 ‘표로 작성해주세요’, ‘bullet point로 작성해주세요’ 등과 같이 출력 요구 사항을 최대한 정확하게 전달하는 것이 좋습니다.

② 공제액 체인

공제액 정보는 원본 문서에 있으므로 같은 방식의 RAG 체인을 하나 더 만듭니다. 결과는 “1세대 1주택자 12억 원, 법인 0원, 그 외 9억 원”입니다.

그다음 이 중간 결과를 입력으로 받는 체인을 만들어 사용자 상황(2주택)에 맞는 금액만 뽑습니다.

user_deduction_chain = (user_deduction_prompt_template | llm | StrOutputParser())
user_deduction = user_deduction_chain.invoke({
    'tax_deductible_response': tax_deductible_response,
    'question': question,
})
# '9억원'

LangChain의 모든 구성 요소가 Runnable이기 때문에 체인의 출력도 다른 체인의 입력이 됩니다. 이때 주의할 점이 있습니다.

프롬프트에 여러 개의 변수를 넘겨줘야 할 때에는 RunnablePassthrough()를 사용할 수 없고, invoke()를 호출할 때 Dictionary 형태로 변수를 전달해야 합니다.

③ 공정시장가액비율 — 웹 검색이 필요한 값

공정시장가액비율은 매년 변하기 때문에 문서가 아니라 웹 검색으로 파악해야 합니다. LangChain에서 가장 쉽게 쓸 수 있는 도구는 Tavily입니다. LangChain 창업자 해리슨 체이스가 Tavily 활용 강의를 낼 만큼 연동이 잘 되고, 매월 1천 개의 무료 API 호출을 제공해 PoC 단계에 좋습니다. 원하는 결과가 잘 안 나오면 Google Search를 추천합니다.

TAVILY_API_KEY라는 이름으로 저장하면 별도 설정 없이 쓸 수 있습니다.

search = TavilySearch(include_answer=True)

# datetime.now()로 현재 연도를 동적으로 가져와서 검색어에 포함한다.
market_value_rate_search = search.invoke(f"{datetime.now().year}년도 공정시장가액비율은?")
market_value_rate_search = market_value_rate_search['answer']

여기서도 비용 감각이 드러납니다.

Tavily가 답변을 영어로 생성하는 것을 굳이 우리말로 번역할 필요는 없습니다. 오히려 영어를 사용하는 경우 토큰 사용량이 줄어들어 비용과 답변 생성 시간을 고려할 때 우리말보다 유리합니다.

검색 결과에서 비율만 추출하도록 “별도의 설명 없이 공정시장가액비율만 알려주세요”라고 지시해 '공정시장가액비율: 60%'를 얻습니다.

④ 최종 세액 체인

네 가지 결과(과세표준 계산식, 공정시장가액비율, 공제액)를 system 메시지에 넣고, 세율 정보는 retriever로 가져와 합칩니다.

house_tax_prompt = ChatPromptTemplate.from_messages([
    ('system', f'''과세표준 계산방법: {tax_base_response}
공정시장가액비율: {market_value_rate}
공제액: {tax_deductible_response}

위의 공식과 아래 세율에 관한 정보를 활용해서 세금을 계산해주세요.
세율: {{tax_rate}}
'''),
    ('human', '{question}')
])

house_tax_chain = (
    {'tax_rate': retriever | format_docs, 'question': RunnablePassthrough()}
    | house_tax_prompt
    | llm
    | StrOutputParser()
)
house_tax = house_tax_chain.invoke(question)

드디어 제대로 된 계산이 나옵니다.

1. 공시가격 합산: 10억 + 10억 = 20억 원
2. 과세표준 = (20억 − 9억) × 0.6 = 11억 × 0.6 = 6.6억 원
3. 세액 = 360만 원 + (6천만 원 × 0.001) = 420만 원
→ 납부해야 할 종합부동산세는 420만 원

6장의 5억 4천만 원, 7장의 2천만 원을 지나 420만 원에 도달했습니다. 데이터를 고치고(7장), 작업을 쪼개고 중간값을 주입한(8장) 결과입니다.

코드를 통해 작업을 작게 나누면 원하는 답변을 훨씬 더 수월하게 생성할 수 있습니다.

8.3 소규모 LLM을 활용한 비용 최적화

LLM 서비스 개발·운영에서 가장 자주 하는 고민은 모델 호출 비용입니다. GPT-5, Gemini 3 Pro, Claude Opus 4.5 같은 대형 모델은 성능이 뛰어나지만 단가가 높고 응답 속도도 상대적으로 느립니다.

모든 작업에 이러한 대형 모델을 사용하는 것은 효율적인 접근이 아닙니다. 이는 마치 작은 압정을 박으려고 대형 망치를 사용하는 것과 같은 상황입니다.

복잡한 추론이나 높은 정확도가 필요한 작업에는 여전히 대형 모델이 적합합니다. 하지만 단순한 정보 검색, 요약, 파싱, 포매팅에는 소규모 LLM만으로도 충분히 안정적입니다. 대표적인 경량 모델인 gpt-4o-mini나 Claude 3.5 Haiku는 업계에서 SLM이라 부르는 모델들보다는 파라미터가 많지만, 대규모 모델과 비교하면 추론 속도가 빠르고 비용이 최대 25배 저렴합니다.

이러한 이유로 프롬프트를 작성할 때부터 task를 최대한 작은 단위로 나누려고 노력한 것입니다.

실제로 얼마나 차이 나나

토큰 사용량을 보려면 체인 끝의 StrOutputParser()를 제거해 AIMessageusage_metadata를 확인합니다. 책의 두 실험 결과입니다.

실험 1 — 공정시장가액비율 체인 (입력 607토큰)

모델 출력 토큰 비용
gpt-4o 3 2.228원
gpt-4o-mini 11 0.141원

실험 2 — 과세표준 계산식 체인 (입력 2,613토큰)

모델 출력 토큰 비용
gpt-4o 26 9.781원
gpt-4o-mini 51 0.608원

두 실험의 해석이 다릅니다. 첫 번째는 절감 효과가 크지 않았습니다. LLM은 출력 토큰의 양이 많을수록 비용이 부과되는데, 공정시장가액비율 체인은 출력 토큰이 5분의 1 수준에 머물렀기 때문입니다. 반면 두 번째는 입력이 2,613토큰으로 크기 때문에 약 94%를 절감했습니다.

그리고 이 숫자의 의미를 이렇게 확장합니다.

gpt-4o-mini를 활용해도 원하는 결과를 얻을 수 있으며, 비용은 약 94%가량 줄일 수 있습니다. 간단한 케이스라서 적은 금액처럼 느껴질 수 있습니다. 하지만 ChatGPT처럼 주간 사용자 수가 300만 명에 달하는 서비스는 단순히 계산해도 매주 약 2,750만 원의 운영 비용을 절약할 수 있습니다.

모델 교체는 LangChain에서 한 줄입니다. llmsmall_llm으로 바꾸기만 하면 됩니다. LangChain의 추상화를 통해 사용하는 모델을 변경하면서 쉽게 디버깅이 가능합니다.

정리

  • 효율은 속도나 비용만이 아니라 정확도·일관성·재현성·유지보수성을 함께 관리하는 운영 능력이다.
  • 하나의 프롬프트에 다 담지 않는다. 단일 책임 원칙으로 쪼개고 LCEL로 잇는다.
  • 출력 요구 사항을 명시한다. ‘수식만 반환해주세요’ 한 줄이 이후 단계의 계산 가능성을 결정한다.
  • 매년 바뀌는 값은 문서가 아니라 웹 검색(Tavily) 으로 채운다. 검색 결과는 영어 그대로 쓰는 편이 토큰에 유리하다.
  • 비용 최적화는 마지막에 한다. 큰 모델로 원하는 답이 나오는 걸 확인한 뒤 경량 모델로 갈아탄다.
  • 절감 폭은 입력 토큰이 클수록 크다. 출력이 짧은 체인은 모델을 바꿔도 이득이 작다.