티스토리 뷰
LangChain 백엔드 운영 준비하기 — 로그, 관측, 디버깅을 어디서부터 붙여야 하나
여기서부터는 분위기가 조금 바뀝니다.
지금까지 우리는 꽤 많은 걸 만들었어요.
사내 도우미도 만들었고, FastAPI로 감쌌고, short-term memory도 붙였고, structured output도 넣었고, 스트리밍까지 해봤죠.
그런데 서비스는 “된다”에서 끝나지 않습니다.
운영에 들어가면 진짜 많이 듣는 말이 나와요.
- “왜 이 질문에서 이상한 답이 나왔죠?”
- “왜 이번엔 tool을 안 썼어요?”
- “왜 갑자기 느려졌어요?”
- “비용이 왜 늘었죠?”
- “어느 단계에서 실패한 거예요?”
이 질문들에 답하려면, 결국 관측 가능성(observability) 이 필요합니다.
LangSmith observability 문서는 이를 “개별 trace부터 production-wide 성능 지표까지 LLM 앱을 들여다보는 것”으로 설명하고, LangChain/LangGraph 문서도 agent behavior를 디버깅하고 tool calls를 추적하려면 LangSmith tracing을 쓰라고 계속 안내합니다. 또 trace 상세 화면에서는 입력, 출력, 지연 시간, 토큰 수, 에러, 메타데이터를 drill-down해서 볼 수 있다고 설명해요. (LangChain Docs)
저는 이 구간이 진짜 중요하다고 생각합니다.
왜냐면 여기서부터 AI 기능이 “멋진 데모”에서 “고칠 수 있는 시스템”으로 바뀌기 때문이에요.
왜 로그와 관측이 특히 LLM 백엔드에서 더 중요할까
일반 CRUD 서버에서도 로그는 중요합니다.
그런데 LLM 백엔드는 그보다 더 중요해요.
이유는 단순합니다.
중간 단계가 많기 때문입니다.
- 사용자의 원래 질문
- 시스템 프롬프트
- tool 호출 여부
- tool 인자
- tool 결과
- 모델 최종 응답
- 토큰 사용량
- 응답 시간
에이전트라면 더 많죠.
LangChain의 agent loop는 모델 호출과 tool execution의 반복으로 설명되고, streaming 문서는 step 단위 진행 상태까지 흘려보낼 수 있다고 말합니다. 즉, “최종 답 하나”만 보면 왜 그런 답이 나왔는지 놓치기 쉽습니다. (LangChain Docs)
그래서 운영에서는 늘 이 질문으로 시작합니다.
최종 답이 아니라, 그 답이 나오기까지 무슨 일이 있었는가?
이번 글에서 얻어갈 것
이번 글에서는 딱 이 네 가지를 잡겠습니다.
- FastAPI 서버에 최소한 어떤 로그를 남겨야 하는지
- LangChain/LangSmith tracing을 언제 켜야 하는지
- 어떤 단위를 보면 agent가 왜 틀렸는지 빨리 보이는지
- 처음 운영할 때 어디까지 붙이면 충분한지
너무 거대한 운영 플랫폼 얘기까지는 안 갈게요.
대신 지금 바로 붙일 수 있는 가장 현실적인 출발점으로 가겠습니다.
1. 최소한 남겨야 하는 서버 로그는 생각보다 단순하다
처음부터 너무 많은 로그를 남기면 오히려 못 봅니다.
그래서 저는 처음엔 아래만 꼭 남기면 충분하다고 봐요.
- 요청 ID
- session_id
- 들어온 question
- 응답 시간
- 성공/실패 여부
- 에러 메시지
FastAPI는 high-performance API framework이고, dependency와 middleware를 붙이기 쉬워서 이런 공통 로그를 심기 좋은 구조입니다. 공식 문서도 dependency system과 middleware 확장성이 강점이라고 설명합니다. (FastAPI)
즉, 운영의 첫걸음은 이런 느낌입니다.
“누가 어떤 질문을 했고, 얼마나 걸렸고, 실패했는지”는 서버 로그로 바로 보여야 한다.
이건 LLM이든 아니든 기본기인데,
AI 백엔드에선 특히 더 중요합니다.
2. FastAPI에서 요청 단위 로그를 먼저 붙이자
가장 쉬운 시작점은 request logging middleware입니다.
아래 코드는 요청 ID, 경로, 처리 시간, 상태 코드를 남기는 아주 작은 예제예요.
app/main.py
import logging
import time
import uuid
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
logger = logging.getLogger("app")
logging.basicConfig(level=logging.INFO)
app = FastAPI(
title="Internal Assistant API",
version="0.5.0",
)
@app.middleware("http")
async def log_requests(request: Request, call_next):
request_id = str(uuid.uuid4())
start = time.perf_counter()
try:
response = await call_next(request)
duration_ms = round((time.perf_counter() - start) * 1000, 2)
logger.info(
"request_completed request_id=%s method=%s path=%s status_code=%s duration_ms=%s",
request_id,
request.method,
request.url.path,
response.status_code,
duration_ms,
)
response.headers["X-Request-ID"] = request_id
return response
except Exception as e:
duration_ms = round((time.perf_counter() - start) * 1000, 2)
logger.exception(
"request_failed request_id=%s method=%s path=%s duration_ms=%s error=%s",
request_id,
request.method,
request.url.path,
duration_ms,
str(e),
)
return JSONResponse(
status_code=500,
content={"detail": "internal server error", "request_id": request_id},
)
이 코드는 화려하진 않지만 효과가 좋아요.
문제가 생겼을 때 “어느 요청이었는지”부터 잡을 수 있으니까요.
3. 그다음엔 애플리케이션 로그에 session_id를 꼭 넣자
이건 LLM 백엔드에서 특히 중요합니다.
왜냐면 같은 사용자가 여러 턴 대화를 이어가기 때문이죠.
LangChain short-term memory는 thread-scoped state라고 설명하고, thread_id가 상태 저장과 복원의 핵심 키라고 말합니다. 즉, session_id 또는 thread_id가 로그에 없으면 대화 흐름 추적이 급격히 어려워집니다. (LangChain Docs)
예를 들면 /assistant/ask에서 이런 로그를 남깁니다.
import logging
from fastapi import HTTPException
from .agent import ask_assistant
from .schemas import AskRequest, AskResponse
logger = logging.getLogger("app")
@app.post("/assistant/ask", response_model=AskResponse)
def ask(request: AskRequest) -> AskResponse:
logger.info(
"assistant_request session_id=%s question=%s",
request.session_id,
request.question,
)
try:
structured = ask_assistant(
session_id=request.session_id,
question=request.question,
)
logger.info(
"assistant_response session_id=%s response_type=%s used_tools=%s sources=%s",
request.session_id,
structured.response_type,
structured.used_tools,
structured.sources,
)
return AskResponse(
answer=structured.answer,
response_type=structured.response_type,
used_tools=structured.used_tools,
sources=structured.sources,
)
except Exception as e:
logger.exception(
"assistant_error session_id=%s error=%s",
request.session_id,
str(e),
)
raise HTTPException(status_code=500, detail=str(e))
이 정도만 있어도 디버깅이 훨씬 쉬워집니다.
- 어떤 세션에서
- 어떤 질문을 했고
- 어떤 유형의 답이 나왔고
- 어떤 tool을 썼는지
이게 서버 로그만으로도 보이니까요.
4. 그런데 서버 로그만으로는 agent 디버깅이 부족하다
여기서부터 LangSmith 얘기가 나옵니다.
서버 로그는 좋지만,
LLM/agent 내부의 세부 단계까지는 다 안 보이거든요.
예를 들어 이런 건 서버 로그만으로 부족합니다.
- 모델이 어떤 tool call을 만들었는지
- tool에 어떤 인자를 넣었는지
- intermediate output이 어땠는지
- 토큰 수가 왜 늘었는지
- 어느 node에서 오래 걸렸는지
LangSmith observability는 바로 이런 걸 보라고 있는 도구입니다. 공식 문서는 개별 trace부터 production-wide metrics까지 볼 수 있다고 설명하고, trace detail에서 input, output, timing, token counts, errors, metadata를 drill-down할 수 있다고 말합니다. LangChain agents와 LangGraph overview도 “each step of the loop를 trace하고 debug tool calls하라”고 안내해요. (LangChain Docs)
즉 역할을 나누면 이렇습니다.
- FastAPI 로그: 요청 단위 운영 로그
- LangSmith trace: agent 내부 단계 디버깅
이 둘을 같이 봐야 훨씬 선명해집니다.
5. LangSmith tracing은 처음부터 붙이는 게 좋을까
제 답은 대체로 “네”입니다.
특히 agent, tool calling, RAG가 섞이면 더 그렇습니다.
왜냐면 문제가 생겼을 때 뒤늦게 tracing을 붙이면,
이미 그 문제 상황을 다시 재현해야 하거든요.
LangGraph overview는 tracing을 시작하려면 LANGSMITH_TRACING=true와 API key를 설정하라고 바로 안내합니다. LangSmith observability 문서도 instrument your LLM application to investigate traces and monitor performance라고 설명하고요. (LangChain Docs)
최소 설정 예시는 보통 이런 흐름입니다.
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="your_langsmith_api_key"
export LANGSMITH_PROJECT="internal-assistant-dev"
이 상태로 LangChain/LangGraph 앱을 돌리면 trace가 쌓입니다.
이게 정말 좋은 이유는,
에이전트가 “왜 이상하게 행동했는지”를 말이 아니라 실행 흔적으로 볼 수 있기 때문입니다.
6. LangSmith에서 제일 먼저 봐야 하는 것
처음 tracing 화면을 열면 이것저것 많아 보여서 오히려 막막할 수 있어요.
그래서 저는 아래 순서로 봅니다.
첫째, total latency
느린 요청인지부터 확인합니다.
둘째, tool call sequence
tool을 예상대로 불렀는지 봅니다.
셋째, tool input / output
잘못된 인자를 넣었는지, tool 결과가 이상한지 봅니다.
넷째, final model output
검색/조회는 맞았는데 생성 단계가 이상한지 봅니다.
다섯째, token counts
유난히 비싼 요청이 무엇인지 봅니다.
LangSmith trace details 문서가 exactly 이 레이어를 설명합니다. 입력, 출력, timing, token counts, errors, metadata를 drill-down해서 보라고요. (LangChain Docs)
즉, trace를 볼 때는 “예쁜 UI”보다
어느 단계에서 기대와 달라졌는지 찾는 것이 더 중요합니다.
7. agent 디버깅은 “최종 답”보다 “중간 행동”을 봐야 한다
이건 꼭 강조하고 싶어요.
에이전트는 최종 답 하나만 보면 원인을 놓치기 쉽습니다.
예를 들어 최종 답이 틀렸다고 해봅시다.
그 원인은 여러 가지일 수 있어요.
- 문서 검색이 잘못됨
- 잘못된 tool을 부름
- tool 인자를 틀리게 넣음
- tool 결과는 맞는데 모델이 정리하면서 틀림
LangChain agent loop 문서는 모델 호출과 tool execution이 반복된다고 설명합니다. 그러니까 에이전트 디버깅은 “결과”가 아니라 루프의 단계별 행동을 봐야 합니다. (LangChain Docs)
이건 진짜 개발자 감각이 붙는 지점입니다.
오답이 아니라, 오답이 만들어진 경로를 본다.
8. 스트리밍을 붙였다면 스트림 이벤트도 로그 관점에서 봐야 한다
이전 글에서 스트리밍 endpoint를 만들었죠.
그때는 UX 관점으로 봤습니다.
이번에는 운영 관점으로 보면 좋습니다.
예를 들어 이런 이벤트를 남길 수 있어요.
- agent_started
- tool_called
- tool_finished
- final_emitted
FastAPI StreamingResponse는 chunk를 그대로 흘려보내므로, 서버 쪽에서 어떤 chunk를 언제 보냈는지 로깅하기 쉽습니다. LangChain streaming은 updates, messages, custom 같은 mode를 제공해서 agent progress와 token stream을 surface할 수 있다고 설명합니다. (LangChain Docs)
예를 들면:
async def stream_assistant(...):
logger.info("stream_started session_id=%s", session_id)
yield ...
async for chunk, metadata in agent.astream(..., stream_mode="messages"):
...
if text:
logger.info("stream_token session_id=%s size=%s", session_id, len(text))
yield ...
logger.info("stream_finished session_id=%s", session_id)
이런 로그는 스트림이 중간에 끊기거나,
유독 오래 걸리는 세션을 볼 때 도움이 됩니다.
9. 최소한의 운영 지표는 뭘 보면 좋을까
처음부터 거대한 대시보드를 만들 필요는 없습니다.
저는 보통 아래부터 봅니다.
- 총 요청 수
- 실패율
- 평균 응답 시간
- 평균 tool 호출 수
- 평균 token 사용량
- 가장 자주 쓰인 tool
- 가장 느린 endpoint / session
LangSmith observability는 production-wide performance metrics까지 볼 수 있다고 설명하고, LangSmith/Fleet comparison도 every agent run is traced and easy to debug performance at scale라고 말합니다. (LangChain Docs)
즉, 처음엔 완벽한 APM이 아니라
“문제가 생기면 바로 보이는 최소 지표”만 있어도 충분합니다.
10. 운영에서 가장 자주 만나는 문제 패턴
이건 실제로 꽤 자주 만납니다.
패턴 1. 유독 느린 요청
대개 trace를 보면 tool 하나가 느리거나, tool loop를 여러 번 돌았습니다. (LangChain Docs)
패턴 2. 잘못된 tool 사용
tool 설명이 애매하거나 system prompt가 느슨한 경우가 많습니다. LangChain tools 문서는 이름·설명·인자 정보가 모델의 tool use reasoning에 직접 영향을 준다고 설명합니다. (LangChain Docs)
패턴 3. token 비용 급증
대화 히스토리가 길어졌거나, tool 결과를 너무 길게 다시 모델에 넣고 있습니다. short-term memory와 message 관리 문서가 오래된/불필요한 내용 때문에 성능과 비용이 나빠질 수 있다고 설명합니다. (LangChain Docs)
패턴 4. 간헐적 실패
tool 에러 handling이나 timeout 처리 부족일 가능성이 큽니다. agent 운영에선 tool error handling과 guardrails가 중요합니다. (LangChain Docs)
이런 패턴을 빨리 보려면
결국 로그와 trace가 같이 있어야 합니다.
11. 운영 로그와 trace를 연결하는 작은 팁
개인적으로는 request_id와 session_id를 같이 쓰는 걸 추천합니다.
- request_id: 이번 HTTP 요청 식별
- session_id: 대화 thread 식별
그리고 가능하면 LangSmith trace metadata에도 같은 키를 남기면 좋아요.
trace 화면과 서버 로그를 같은 키로 이어볼 수 있기 때문입니다. LangSmith는 metadata 기반 관측과 trace drill-down을 지원합니다. (LangChain Docs)
예를 들어 로그는 이렇게:
logger.info(
"assistant_request request_id=%s session_id=%s question=%s",
request_id,
request.session_id,
request.question,
)
이런 습관이 나중에 정말 큰 차이를 만듭니다.
12. 그럼 어디까지 붙이면 “충분한 첫 운영 세트”일까
저는 보통 이 정도면 충분하다고 봅니다.
서버 쪽
- request logging middleware
- request_id
- session_id 포함 로그
- 에러 로그
- 응답 시간 로그
LangChain 쪽
- LangSmith tracing ON
- project 분리(dev/staging/prod)
- tool call sequence 확인 가능
- token/latency 확인 가능
API 쪽
- 일반 JSON endpoint
- streaming endpoint
- structured response model
이 세 가지만 있어도
“왜 안 되지?”에서 너무 오래 헤매지 않게 됩니다.
오늘 글에서 꼭 가져가야 할 한 문장
오늘 내용을 한 줄로 줄이면 이겁니다.
LLM 백엔드 운영의 시작은 정답률을 감으로 보는 게 아니라, request 로그로 외곽을 보고 trace로 agent 내부 단계를 들여다보는 구조를 먼저 만드는 것이다.
이 감각이 생기면
문제가 생겨도 훨씬 덜 막막해집니다.
마무리
저는 예전엔 운영을 너무 늦게 붙였어요.
기능부터 다 만들고 나서 로그를 보려고 했죠.
근데 AI 기능은 특히 그게 더 힘들더라고요.
문제가 생겼을 때 최종 답만 보면 어디서 틀어졌는지 알 수가 없으니까요.
그래서 지금은 생각이 좀 바뀌었습니다.
좋은 AI 백엔드는
처음부터 완벽하게 똑똑한 시스템이 아니라,
문제가 생겼을 때 왜 그랬는지 빨리 볼 수 있는 시스템에 더 가깝다고요.
그게 진짜 운영 준비라고 생각합니다.
다음 글 예고
다음 글에서는
LangChain 백엔드를 테스트 가능하게 만들기 — 프롬프트, tool, structured output을 어떻게 검증하나
로 이어가겠습니다.
이제부터는 “문제가 생기면 본다”를 넘어서,
“애초에 깨지기 전에 잡는” 쪽으로 넘어가게 될 거예요.
출처
- LangSmith Observability 문서: 개별 trace부터 production-wide 성능 지표까지 LLM 앱을 관측할 수 있다고 설명합니다. (LangChain Docs)
- LangSmith trace details 문서: trace 상세 보기에서 inputs, outputs, timing, token counts, errors, metadata를 drill-down할 수 있다고 설명합니다. (LangChain Docs)
- LangChain Agents 문서: agent loop 각 단계를 trace하고 tool calls를 디버깅하라고 안내합니다. (LangChain Docs)
- LangGraph overview 문서: LangSmith tracing을 켜서 agent behavior를 디버깅하라고 설명합니다. (LangChain Docs)
- LangChain Context Engineering 문서: 전형적인 agent loop는 model call과 tool execution의 반복이라고 설명합니다. (LangChain Docs)
- LangChain Streaming 문서: updates, messages, custom 모드로 agent progress와 token stream을 실시간으로 surface할 수 있다고 설명합니다. (LangChain Docs)
- FastAPI 공식 소개 및 튜토리얼: FastAPI는 standard Python type hints 기반의 API framework이고, dependency/middleware 확장성이 강하다고 설명합니다. (FastAPI)
- FastAPI custom/streaming response 문서: StreamingResponse는 chunk를 그대로 스트리밍하며, 긴 스트림에서는 cancellation handling이 중요하다고 설명합니다. (FastAPI)
LangChain, LangSmith, Observability, FastAPI, AI Agent, Tracing, 생성형AI, 백엔드운영, 디버깅, 주니어개발자
'study > langchain' 카테고리의 다른 글
- Total
- Today
- Yesterday
- 생성형AI
- DevOps
- Express
- 백엔드개발
- PostgreSQL
- 딥러닝
- 주니어개발자
- fastapi
- nodejs
- JAX
- 쿠버네티스
- CI/CD
- rag
- SEO최적화
- flax
- node.js
- Python
- NestJS
- Prisma
- seo 최적화 10개
- 웹개발
- LangChain
- kotlin
- llm
- ai철학
- JWT
- REACT
- 개발블로그
- Next.js
- nextJS
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |

