LLM 앱은 실행 과정이 보여야 한다
일반적인 웹 애플리케이션은 오류 로그, 요청 시간, 데이터베이스 쿼리를 보면 많은 문제를 추적할 수 있다. LLM 애플리케이션은 여기에 몇 가지가 더 필요하다. 어떤 프롬프트가 들어갔는지, 어떤 문서가 검색됐는지, 모델이 어떤 도구를 호출하려 했는지, 최종 답변이 어떤 중간 결과에서 나왔는지 확인할 수 있어야 한다.
예를 들어 RAG 챗봇이 틀린 답을 했다고 하자. 원인은 여러 가지다.
사용자의 질문이 검색에 적합하지 않게 전달됐다.
리트리버가 관련 없는 문서를 가져왔다.
관련 문서는 있었지만 프롬프트에 잘못 포맷됐다.
모델이 문서에 없는 내용을 추측했다.
출력 파서나 후처리 코드가 응답을 잘못 변환했다.
최종 답변만 보면 이 중 무엇이 문제인지 알기 어렵다. 그래서 LangChain 앱은 처음부터 관찰 가능성, 즉 실행 과정을 들여다볼 수 있는 구조로 만들어야 한다. print()로 임시 확인하는 것은 실습에는 충분하지만, 실무에서는 실행 기록을 저장하고 비교하고 평가할 수 있어야 한다.
AI 생성 이미지
무엇을 추적해야 할까
체인이나 에이전트를 추적할 때는 최소한 다음 정보를 남기는 것이 좋다.
| 항목 | 확인하려는 것 |
|---|---|
| 사용자 입력 | 실제 요청이 무엇이었는가 |
| 프롬프트 | 모델에 전달된 최종 지시문과 변수 값은 무엇인가 |
| 검색 문서 | 어떤 문서 조각이 선택됐고 출처는 무엇인가 |
| 모델 응답 | 원본 응답과 파싱 후 결과가 어떻게 다른가 |
| 도구 호출 | 어떤 도구가 어떤 인자로 실행됐는가 |
| 오류 | 어느 단계에서 예외가 발생했는가 |
| 지연 시간 | 어느 단계가 느린가 |
| 토큰 사용량 | 비용이 어디서 많이 발생하는가 |
LangSmith는 LangChain 생태계에서 이런 실행 기록을 추적하고 분석하기 위한 도구다. 공식 문서에서도 LangSmith는 하나의 요청에서 입력부터 최종 출력까지 어떤 단계가 실행됐는지 trace로 기록해 LLM 애플리케이션의 실행 과정을 볼 수 있게 해 준다고 설명한다.1 여기서 하나의 사용자 요청 전체를 trace라고 보면 되고, 그 안에 프롬프트 렌더링, 모델 호출, 검색, 도구 호출 같은 하위 실행이 run 또는 span처럼 단계별로 기록된다.
다만 추적을 많이 남긴다고 무조건 좋은 것은 아니다. 운영 환경에서는 개인정보, API 키, 내부 문서의 민감한 부분이 trace에 그대로 저장되지 않도록 입력과 출력, 메타데이터를 마스킹하거나 수집 범위를 조정해야 한다. 관찰 가능성은 문제를 고치기 위한 장치이지, 민감정보를 더 많이 보관하기 위한 장치가 아니다.
LangSmith 추적 켜기
가장 단순한 설정은 환경변수로 추적을 켜는 것이다. 프로젝트의 .env 파일에 다음 값을 추가한다.
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_...
LANGSMITH_PROJECT=langchain-practice
OPENAI_API_KEY=sk-...LangSmith 계정의 리전이 기본값과 다르다면 LANGSMITH_ENDPOINT도 함께 설정해야 할 수 있다. 예를 들어 EU 리전 계정이라면 공식 문서의 리전별 endpoint 값을 확인해 .env에 추가한다. 예전 자료에서는 LANGCHAIN_TRACING_V2, LANGCHAIN_API_KEY, LANGCHAIN_PROJECT 같은 이름을 볼 수도 있지만, 새 프로젝트에서는 현재 문서와 SDK 버전에 맞춰 LANGSMITH_* 환경변수를 기준으로 설정하는 편이 안전하다.
Python 코드에서는 환경변수를 먼저 로드한 뒤 기존 체인을 그대로 실행하면 된다. LangChain의 Runnable, 체인, 에이전트 실행은 LangSmith에 trace로 기록된다.
from dotenv import load_dotenv
load_dotenv()
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "너는 초보 개발자를 돕는 Python 튜터다."),
("human", "{question}")
])
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
parser = StrOutputParser()
chain = prompt | model | parser
answer = chain.invoke({"question": "LCEL을 한 문단으로 설명해줘."})
print(answer)이제 LangSmith 화면에서 해당 실행을 열어 보면 입력, 프롬프트 렌더링 결과, 모델 호출, 출력, 실행 시간 등을 단계별로 확인할 수 있다. 프로젝트 이름을 나누면 개발, 테스트, 운영 실행을 분리해 볼 수 있다.
특정 실행에 태그나 메타데이터를 붙이면 나중에 필터링하기 쉽다.
result = chain.invoke(
{"question": "RAG에서 리트리버가 하는 일은?"},
config={
"tags": ["chapter-12", "basic-chain"],
"metadata": {"user_type": "student", "version": "prompt-v1"}
}
)운영 환경에서는 version, prompt_id, retriever_name, model_name, tenant_id 같은 값을 메타데이터로 남겨두면 좋다. 나중에 "프롬프트 v2로 바꾼 뒤 답변 품질이 좋아졌는가?" 같은 질문에 답할 수 있다. 단, tenant_id나 사용자 식별자는 원문 그대로 저장하기보다 내부 정책에 맞게 익명화하거나 해시 처리하는 것이 좋다.
trace를 읽는 순서
문제가 생겼을 때 trace를 아무렇게나 열어 보면 시간이 오래 걸린다. 다음 순서로 보는 습관을 들이면 원인을 빠르게 좁힐 수 있다.
최종 출력 확인: 사용자가 문제 삼은 답변이 실제로 기록되어 있는지 확인한다.
입력 확인: 사용자의 원래 질문이 예상한 형태로 들어왔는지 본다.
프롬프트 확인: 시스템 지시, 문맥, 출력 형식 안내가 빠지지 않았는지 확인한다.
검색 결과 확인: RAG라면 검색된 문서가 질문과 관련 있는지 본다.
도구 호출 확인: 에이전트라면 도구 이름과 인자가 적절했는지 본다.
오류와 재시도 확인: 파서 오류, API 오류, 타임아웃, 반복 한도 초과를 확인한다.
지연 시간과 토큰 확인: 느리거나 비싼 단계가 어디인지 찾는다.
LangSmith는 개별 trace를 UI나 API로 확인하고, 필터링·비교·공유할 수 있는 관찰 기능을 제공한다.2 이 기능은 단순히 "로그를 예쁘게 보는 도구"가 아니다. 같은 입력에 대해 프롬프트 버전, 모델, 리트리버 설정을 바꿨을 때 실행 결과가 어떻게 달라졌는지 비교하는 기반이 된다.
디버깅은 가설을 세우고 재현하는 일이다
LLM 앱 디버깅에서 흔한 실수는 프롬프트를 감으로 계속 고치는 것이다. 한두 번 좋아 보일 수는 있지만, 다른 입력에서 품질이 나빠질 수 있다. 디버깅은 다음 흐름으로 진행하는 것이 안전하다.
실패 사례를 trace에서 찾는다.
실패 원인을 하나의 가설로 적는다.
같은 입력을 재실행할 수 있게 저장한다.
프롬프트, 검색 설정, 도구 설명 중 하나만 바꾼다.
이전 결과와 새 결과를 비교한다.
좋아진 사례와 나빠진 사례를 함께 기록한다.
예를 들어 RAG 답변이 환각을 일으켰다면 바로 모델을 바꾸기보다 다음을 확인한다.
검색 문서에 정답이 있었는가?
정답 문서가 있었지만 순위가 낮았는가?
문서 본문이 너무 길거나 잡음이 많았는가?
프롬프트가 "문서에 없으면 모른다고 답하라"고 명확히 지시했는가?
답변 생성 단계에서 출처를 강제했는가?
문제가 검색이면 리트리버를 고쳐야 하고, 문제가 생성이면 프롬프트나 모델 설정을 고쳐야 한다. trace는 이 둘을 구분하게 해 준다.
평가 데이터셋 만들기
관찰은 한 번의 실행을 이해하는 일이고, 평가는 여러 입력에서 품질을 측정하는 일이다. 실무에서는 작은 평가 데이터셋부터 시작하는 것이 좋다. 처음부터 수백 개를 만들기보다 중요한 질문 10~30개를 모은다.
평가 예시는 보통 다음 형태를 가진다.
eval_examples = [
{
"inputs": {"question": "휴가 신청은 어디서 하나요?"},
"outputs": {
"answer": "사내 포털의 근태 관리 메뉴에서 신청합니다.",
"required_sources": ["hr_policy_2026.md"]
}
},
{
"inputs": {"question": "개인 API 키를 코드에 넣어도 되나요?"},
"outputs": {
"answer": "아니요. 환경변수나 비밀 관리 도구를 사용해야 합니다.",
"required_sources": ["security_guide.md"]
}
}
]좋은 평가셋에는 쉬운 질문만 있으면 안 된다. 실제 운영에서 문제가 될 질문을 포함해야 한다.
정답이 문서에 명확히 있는 질문
여러 문서를 함께 봐야 하는 질문
문서에 정답이 없어 "모른다"고 답해야 하는 질문
표현은 다르지만 같은 의도를 가진 질문
권한이나 보안상 답하면 안 되는 질문
LangSmith의 평가 흐름은 데이터셋의 각 예시를 대상 체인에 넣고, 출력과 평가 점수를 모아 실험 결과로 비교하는 방식으로 사용할 수 있다. 공식 문서에서도 데이터셋은 입력과 기준 출력으로 이루어진 예시들의 모음이고, 하나의 실험은 특정 애플리케이션 버전을 데이터셋에 대해 실행한 결과와 평가 점수를 담는다고 설명한다.3
세 가지 평가 방식
LLM 앱 평가는 하나의 점수로 끝나지 않는다. 작업 성격에 따라 다른 평가 방식을 섞어야 한다.
| 평가 방식 | 설명 | 장점 | 한계 |
|---|---|---|---|
| 정답 기반 평가 | 기대 답변, 정답 라벨, 필수 필드와 비교한다 | 빠르고 재현 가능하다 | 표현이 다양한 자연어 답변에는 엄격할 수 있다 |
| 기준표 평가 | 정확성, 완전성, 간결성, 안전성 같은 기준으로 사람이 채점한다 | 업무 기준을 반영하기 좋다 | 시간이 걸리고 평가자마다 차이가 날 수 있다 |
| LLM-as-a-judge | 평가용 LLM에게 답변 품질을 채점하게 한다 | 대량 평가와 주관적 기준 평가에 유용하다 | 비용이 들고 judge 모델도 틀릴 수 있다 |
정답이 명확한 분류, 라우팅, JSON 추출은 정답 기반 평가가 좋다. 예를 들어 문의 유형이 billing, technical, general 중 하나라면 모델 출력과 정답 라벨을 비교하면 된다.
반대로 고객 응대 답변처럼 표현이 다양한 작업은 기준표가 필요하다.
다음 기준으로 1~5점을 부여한다.
- 정확성: 문서 내용과 일치하는가?
- 완전성: 사용자의 질문에 필요한 내용을 빠뜨리지 않았는가?
- 간결성: 불필요하게 길지 않은가?
- 안전성: 민감정보나 허용되지 않은 조언을 포함하지 않는가?LLM-as-a-judge는 이 기준표를 모델에게 주고 채점하게 하는 방식이다. 편리하지만 최종 진실로 취급하면 안 된다. 중요한 변경 전에는 사람이 표본을 검토하고, judge 모델의 점수와 사람 평가가 대체로 맞는지 확인해야 한다.
RAG 평가는 세 부분으로 나눈다
RAG를 평가할 때 "답변이 좋은가?"만 보면 원인을 찾기 어렵다. 다음 세 부분으로 나눠야 한다.
1. 검색 정확도
검색 정확도는 리트리버가 필요한 문서를 가져왔는지 보는 지표다. 대표적으로 다음을 확인한다.
정답 문서가 top_k 안에 포함됐는가?
가장 관련 있는 문서가 상위에 있는가?
관련 없는 문서가 너무 많이 섞이지 않았는가?
메타데이터 필터가 올바르게 적용됐는가?
검색 단계에서 정답 문서가 없으면 생성 모델은 좋은 답을 하기 어렵다. 이 경우 프롬프트를 고치는 것보다 청크 크기, 임베딩 모델, top_k, 메타데이터, 하이브리드 검색을 점검해야 한다.
2. 근거 충실성
근거 충실성은 답변이 검색된 문서에 근거하는지 보는 기준이다. 문서에 없는 정책, 날짜, 숫자, 절차를 만들어내면 점수를 낮춰야 한다.
좋은 RAG 프롬프트는 다음 규칙을 포함한다.
제공된 문맥에 근거해서만 답하라.
문맥에 답이 없으면 모른다고 말하라.
중요한 주장에는 문서 번호를 표시하라.하지만 지시만으로 충분하지 않다. 평가 단계에서 답변 문장과 근거 문서를 대조해야 한다. 특히 숫자, 날짜, 법무·인사·보안 정책은 더 엄격하게 본다.
3. 답변 유용성
검색도 맞고 근거도 충실하지만 사용자가 이해하기 어려운 답변일 수 있다. 유용성은 답변이 사용자의 목적을 해결하는지 보는 기준이다.
바로 실행할 수 있는 절차를 제공하는가?
예외 조건이나 주의사항을 알려 주는가?
너무 많은 문서를 그대로 복사하지 않는가?
사용자의 질문 수준에 맞게 설명하는가?
RAG 품질 개선은 이 세 기준을 따로 기록해야 한다. 검색 정확도는 낮지만 답변이 그럴듯한 경우가 가장 위험하다. 반대로 검색은 잘됐지만 답변이 불친절하다면 생성 프롬프트를 개선하면 된다.
운영에서 볼 핵심 지표
서비스로 배포한 뒤에는 품질뿐 아니라 비용과 안정성도 봐야 한다.
평균 응답 시간과 p95 응답 시간
요청당 입력·출력 토큰 수
모델 호출 실패율
파서 오류율
도구 호출 실패율
검색 결과 없음 비율
사용자 재질문 비율
사람 검토로 넘겨진 비율
여기서 p95 응답 시간은 전체 요청 중 95%가 그 시간 안에 끝났다는 뜻이다. 평균만 보면 일부 사용자가 겪는 느린 응답이 가려질 수 있으므로, 운영에서는 평균과 p95를 함께 보는 것이 좋다.
이 지표는 제품 의사결정과 연결된다. 예를 들어 토큰 비용이 급증했다면 대화 기록을 너무 많이 넣고 있을 수 있다. 검색 결과 없음 비율이 높다면 문서 수집 파이프라인이나 질문 재작성 단계가 문제일 수 있다. 도구 호출 실패율이 높다면 도구 스키마가 모호하거나 외부 API 예외 처리가 부족할 수 있다.
개선 루프 만들기
LLM 앱은 한 번 만들고 끝나는 프로그램이 아니다. 운영 중에 실패 사례를 모으고, 평가셋에 추가하고, 다시 실험해야 한다.
권장 흐름은 단순하다.
운영 trace에서 실패 사례를 수집한다.
원인을 검색, 프롬프트, 모델, 도구, 파서, 권한 문제로 분류한다.
중요한 사례를 평가 데이터셋에 추가한다.
한 가지 변경만 적용한 새 버전을 만든다.
같은 평가셋으로 이전 버전과 비교한다.
품질, 비용, 지연 시간이 모두 허용 범위인지 확인한다.
배포 후 다시 trace와 지표를 관찰한다.
이 루프가 있어야 개선이 재현 가능해진다. "프롬프트를 조금 고쳤더니 좋아진 것 같다"가 아니라 "평가셋 50개에서 근거 충실성은 0.72에서 0.84로 올랐고, 평균 지연 시간은 1.1초 증가했다"처럼 판단할 수 있어야 한다.
마무리
LangChain 앱의 품질은 모델만으로 결정되지 않는다. 어떤 입력이 들어왔고, 어떤 문서가 검색됐고, 어떤 도구가 실행됐고, 어느 단계에서 비용과 오류가 발생했는지 볼 수 있어야 한다. LangSmith 같은 관찰 도구는 이 과정을 trace로 남기고, 평가 데이터셋은 변경의 효과를 재현 가능하게 비교하게 해 준다.
실무에서 중요한 원칙은 하나다. 눈에 보이지 않는 실행은 고칠 수 없다. 체인과 에이전트를 만들 때부터 추적, 평가, 디버깅을 함께 설계해야 한다.
참고
1Tracing quickstart - Docs by LangChain
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.