TL;DR — 코드를 짜는 것과 운영하는 것은 다른 일입니다. LLM 앱은 입력이 같아도 출력이 흔들립니다. 호출마다 비용이 쌓이고 실패가 어느 단계에서 났는지 로그만으론 보이지 않습니다. LangChain 1.0은 이 운영 영역을
LangSmith로 표준화합니다. 트레이싱(LANGSMITH_TRACING=true · @traceable) · 콜백(동기/Async) · 평가(evaluate · LLM-as-judge) · 프롬프트 관리(pull_prompt) · 비용(usage_metadata · 캐싱)이 한 묶음입니다. 옛 langchain hub·langchain.evaluation은 classic으로 옮겨졌습니다.
이 글은 LangChain으로 만든 LLM 앱을 실제로 굴려야 하는 개발자가 운영 단계의 디버깅·품질·비용을 관측 가능한 형태로 다루는 방법을 이해하도록, 나아가 LangSmith 트레이싱·평가·비용 추적을 자기 코드에 붙일 기준을 얻도록 돕습니다.
이 글에서 다루는 것
- 트레이싱: 환경변수 한 줄 + @traceable로 임의 함수 추적
- 콜백: 동기 vs Async 핸들러 — 비동기 호출 시 함정
- 평가: evaluate/aevaluate · LLM-as-judge / 휴리스틱 / pairwise
- 프롬프트 관리(pull_prompt/push_prompt)와 토큰·비용·캐싱
이 글에서 다루지 않는 것
- LangSmith 대시보드 UI 사용법·요금제 상세
- LCEL·LangGraph·에이전트·RAG 본론 (앞선 1~5편에서 다룸)
1. 왜 관측·평가인가 — 운영은 코딩과 다른 일입니다
앞선 다섯 편으로 모델을 잡고(1편), 파이프로 잇고(2편), 상태를 가진 그래프를 그리고(3편), 에이전트를 세우고(4편), RAG로 외부 지식을 붙였습니다(5편). 여기까지는 동작하는 코드를 만드는 일입니다. 그런데 그 코드를 사람들이 실제로 쓰기 시작하면 성격이 다른 문제가 생깁니다.
LLM 앱의 운영이 일반 백엔드와 다른 지점은 세 가지입니다. 첫째, 출력이 비결정적입니다. 같은 입력에 다른 답이 나오니 버그와 그냥 다른 답을 구분하기 어렵습니다. 둘째, 호출마다 비용이 든다는 점입니다. 토큰 수가 곧 청구서입니다. 에이전트 한 번 돌면 LLM이 여러 번 호출됩니다. 셋째, 실패 지점이 깊다는 것입니다. 체인·검색·도구·에이전트가 겹친 호출에서 어느 단계가 잘못됐는지 텍스트 로그만으로는 거의 보이지 않습니다.
비유하면 LLM 앱 운영은 비행기 블랙박스와 같습니다. 무사히 착륙했을 때는 필요 없어 보이지만, 무언가 어긋났을 때 어느 고도에서 어떤 입력으로 무슨 판단을 했는지 되짚을 기록이 없으면 원인을 찾을 길이 없습니다. LangSmith는 그 블랙박스입니다 — 트레이싱은 비행 기록, 평가는 정기 점검, 비용 추적은 연료 계기판에 해당합니다. 이 글은 그 세 계기를 코드에 다는 방법입니다.
2. 트레이싱 — 환경변수 한 줄로 켜집니다
트레이싱의 장점은 코드를 거의 안 고쳐도 된다는 데 있습니다. 환경변수만 켜면 이후의 모든 체인·에이전트·검색·도구 호출이 자동으로 추적됩니다.
환경
- 언어/런타임: Python 3.10 이상(1.x 요구)
- 핵심: langchain · langchain-core · langsmith · 파트너 패키지(예: langchain-openai)
- 선택: langchain-community(SQLiteCache 등)
- 키: LangSmith API 키(트레이싱 전송용)
환경변수 두 줄이면 LangChain 컴포넌트 호출은 손대지 않아도 추적됩니다. 그렇게 쓴 이유는 트레이싱이 켜고 끄는 스위치여야 운영·개발 환경을 코드 변경 없이 오갈 수 있기 때문입니다. 다만 LangChain 바깥의 순수 함수(전처리·후처리·외부 API 호출 등)는 자동으로 잡히지 않으므로, 그 부분에 @traceable을 붙여 같은 트레이스에 묶습니다. 이렇게 하면 LLM 호출과 내 코드가 한 타임라인에서 보입니다.
📌 참고 — LANGSMITH_TRACING은 예전 LANGCHAIN_TRACING_V2 이름과 호환됩니다. 옛 자료가 후자를 쓰더라도 동작은 같으니, 새 코드는 현행 이름을 쓰는 편이 헷갈리지 않습니다.
3. 콜백 — 트레이싱·스트리밍의 기반, 그리고 Async 함정
트레이싱과 토큰 스트리밍이 실제로 동작하는 바탕에는 콜백(callback)이 있습니다. 콜백은 실행 도중 일어나는 사건(LLM 새 토큰·체인 시작·도구 호출·검색 시작 등)에 끼어들어 무언가를 하는 후크입니다. 직접 핸들러를 만들어 로깅·모니터링·UI 업데이트에 연결할 수 있습니다.
여기서 1.0 운영에서 가장 자주 밟는 함정이 하나 있습니다. 핸들러에는 동기용과 비동기용 두 종류가 있습니다. 호출 방식과 핸들러 종류가 어긋나면 이벤트 루프가 막혀 성능이 무너집니다. 두 갈래를 나란히 보겠습니다.
invoke / stream 등 동기 호출.def on_llm_new_token(self, tok, **kw): ...
ainvoke / astream 등 비동기 호출 → 필수.async def on_llm_new_token(self, tok, **kw): ...
async def 훅)를 써야 비차단으로 동작합니다.핸들러는 config={"callbacks": [handler]}로 주입합니다. 제공되는 훅은 토큰 단위(on_llm_new_token)부터 체인·도구·검색 생애주기(on_chain_* · on_tool_* · on_retriever_*)까지 있어, 내가 원하는 지점만 골라 가로챌 수 있습니다. 표준 훅 바깥의 사건을 흘리고 싶다면 dispatch_custom_event로 커스텀 이벤트를 발행합니다.
4. 평가 — "다른 답"과 "틀린 답"을 가르는 잣대
출력이 비결정적이라는 LLM 앱의 특성은, 프롬프트나 모델을 바꿨을 때 좋아졌는지 나빠졌는지를 감으로 판단하게 만듭니다. 평가(evaluation)는 이걸 데이터셋 기반 점수로 바꿉니다. 미리 만든 예시 묶음에 대해 내 함수를 돌린 뒤 각 출력을 평가자(evaluator)로 채점하는 오프라인 방식입니다.
핵심은 evaluate(target, data, evaluators=[...]) 한 줄입니다. 비동기 파이프라인이라면 aevaluate를 씁니다. 이렇게 데이터셋을 고정해두면 프롬프트·모델·검색 설정을 바꿀 때마다 같은 잣대로 회귀를 잴 수 있어, 변경이 개선인지 후퇴인지를 수치로 말할 수 있습니다. 평가자는 네 갈래로 나뉩니다.
| 평가자 유형 | 방식 | 언제 쓰나 |
|---|---|---|
| 휴리스틱 | 규칙·문자열 비교 등 코드 기반 | 정답이 명확·자동 채점 가능할 때 |
| LLM-as-judge | LLM이 출력을 채점 | 정답이 모호·서술형 품질을 볼 때 |
| pairwise | 두 출력(A/B) 중 우위 판정 | 프롬프트·모델 두 버전 비교 |
| human | 사람이 직접 채점 | 기준 보정·자동 평가 검증 |
LLM-as-judge는 강력하지만 채점자 자체가 LLM이라 비용과 편향이 생깁니다. 그래서 자동 채점이 가능한 항목은 휴리스틱으로 두고 서술형 품질에만 LLM-as-judge를 씁니다. 기준이 흔들릴 땐 human으로 보정하는 식으로 섞는 것이 현실적입니다. 이 평가 흐름은 이 저장소의 ch19 Evaluation 패턴(응답·궤적·세션 레벨, LLM-as-Judge, 드리프트 탐지)과 곧장 이어집니다.
5. 프롬프트 관리 — 코드에 박지 말고 버전으로
프롬프트를 코드 문자열에 박아두면, 문구 한 줄 바꾸는 데도 배포가 필요하고 누가 언제 무엇을 고쳤는지 추적되지 않습니다. LangSmith의 프롬프트 관리는 프롬프트를 버전이 붙은 리소스로 끌어와 쓰게 합니다.
pull_prompt로 저장소의 프롬프트를 가져온 뒤 push_prompt로 새 버전을 올립니다. 이렇게 분리해두면 프롬프트 개선을 코드 배포와 떼어 빠르게 반복하면서도 버전 이력을 남길 수 있습니다. 다만 이 부분은 옛 자료와 import 경로가 크게 달라진 지점이라 주의가 필요합니다.
🕰️ 레거시 — 옛 자료의 from langchain import hub(hub.pull)과 langchain.evaluation(QAEvalChain · load_evaluator)은 langchain-classic으로 이동했습니다. 현행은 LangSmith의 Client.pull_prompt와 langsmith.evaluate입니다. 옛 코드가 hub.pull(...)로 프롬프트를 받아왔다면 이 경로 교체가 먼저입니다.
6. 토큰·비용 — 청구서를 코드에서 들여다보기
호출당 비용을 가늠하려면 토큰을 세야 합니다. 1.0에서 호출별 토큰은 응답 메시지 자체에 들어 있습니다 — AIMessage.usage_metadata입니다.
usage_metadata에는 input_tokens · output_tokens · total_tokens가 들어 있어 호출 하나의 비용을 바로 가늠할 수 있습니다. 에이전트처럼 한 번 실행에 LLM이 여러 번 불리는 경우 호출마다 따로 보면 합산이 번거로우므로, UsageMetadataCallbackHandler를 콜백으로 붙여 누적합을 모읍니다 — 집계 결과는 cb.usage_metadata(모델ID별 input/output/total 합계)로 읽고, with get_usage_metadata_callback() as cb: 컨텍스트 매니저 형태도 동일하게 쓸 수 있습니다. 트레이싱을 켜뒀다면 LangSmith가 비용을 함께 기록하므로, 코드 레벨 집계와 대시보드 집계를 같이 쓸 수 있습니다.
7. 캐싱 — 같은 입력에 두 번 돈 내지 않기
비용을 가장 직접적으로 줄이는 방법은 중복 호출을 피하는 것입니다. 같은 입력에 같은 답이 나올 호출이라면, 결과를 캐시해 두 번째부터는 모델을 부르지 않습니다. 1.0에서는 전역 캐시를 한 번 설정하면 됩니다.
set_llm_cache에 SQLiteCache를 넘기면 캐시가 파일로 영속화되어 프로세스를 재시작해도 살아남습니다. 세션 동안만 필요하면 InMemoryCache를 씁니다. 한 가지 전제는, 캐시는 입력이 완전히 같을 때만 적중하므로 동적 컨텍스트가 많이 섞이는 호출에는 효과가 제한적이라는 점입니다.
한 단계 더 들어가면 프로바이더 네이티브 프롬프트 캐싱이 있습니다. 4편에서 본 미들웨어로 붙이는데, AnthropicPromptCachingMiddleware · BedrockPromptCachingMiddleware로 안정적인 시스템 프롬프트·도구 정의 부분을 모델 측에서 캐싱합니다. 전역 LLM 캐시가 응답 전체를 재사용한다면 프롬프트 캐싱은 반복되는 입력 앞부분을 재사용하는 결이라 둘은 보완 관계입니다.
시리즈: 01 시작하기 → 02 LCEL & Runnable → 03 LangGraph → 04 create_agent → 05 현대 RAG → 06 LangSmith 관측·평가 · 완결
8. 정리 · 시리즈 완결
관측·평가는 화려한 기능은 아니지만 코드를 돌아가는 것에서 운영할 수 있는 것으로 바꾸는 마지막 한 겹입니다. 운영 단계에서 손에 쥐어야 할 다섯 가지를 체크리스트로 정리합니다.
여섯 편을 돌아보면, 이 시리즈는 LangChain 1.0이라는 이사를 끝낸 집을 방마다 둘러본 여정이었습니다. 1편에서 집 구조(패키지 4계층)를 파악했습니다. 2편에서는 배관 역할의 파이프(LCEL)를 익혔고 3편에서 상태를 가진 그래프(LangGraph)를, 4편에서 그 위에 선 에이전트(create_agent)를, 5편에서 외부 지식을 끌어오는 RAG를 다뤘습니다. 그리고 이번 6편은 그 집에 블랙박스와 계기판(LangSmith)을 달아, 만든 것을 안심하고 운영할 수 있게 마감했습니다.
처음 목표였던 내가 보는 코드가 현행인지 레거시인지 구분하는 눈은 이제 현행 코드를 직접 짜고 운영하는 손으로 바뀌었을 것이라 생각합니다. 옛 자료를 만나도 어느 방으로 옮겨졌는지 알 수 있고 새 코드를 1.0 기준으로 세울 수 있다면 이 시리즈의 역할은 다한 셈입니다. 여기까지 함께 읽어 주셔서 고맙습니다.
'LangChain' 카테고리의 다른 글
| 현대 RAG — 로드·분할부터 하이브리드 검색과 Agentic RAG까지 (0) | 2026.06.30 |
|---|---|
| create_agent & 미들웨어 — LangChain 1.0 에이전트 표준 (0) | 2026.06.29 |
| LangGraph 입문 — LCEL로 안 되던 순환·기억·사람 개입을 그래프로 풀기 (0) | 2026.06.26 |
| 파이프(|) 하나로 invoke·batch·stream이 공짜로 따라오는 이유 — LangChain Runnable (0) | 2026.06.24 |
| LangChain 1.0 시작하기 (0) | 2026.06.21 |