
요즘 제가 LLM 에이전트 만드는 재미에 살짝 빠져 있습니다. 재미라고 말하긴 했지만, 솔직히 반은 재미고 반은 야근이었어요. 특히 여러 단계를 거쳐서 판단하고 tool을 호출하는 에이전트는 겉으로 보면 똑똑해 보이는데, 안쪽을 들여다보면 꽤 자주 삐걱댑니다. 분명 한 번만 검색하면 될 일을 두세 번 반복한다든지, 이미 받은 결과를 못 본 척한다든지, 갑자기 토큰을 왕창 써버린다든지요. 처음엔 로그를 하나씩 까보면서 “아, 내가 이 나이에 또 printf 디버깅을 하고 있구나” 싶었습니다. 그러다 제대로 써보게 된 게 LangSmith였어요.
예전에는 LangSmith를 그냥 “LangChain 쓸 때 붙이는 로그 도구” 정도로 가볍게 봤는데, 막상 실무에서 에이전트 문제를 잡아보니 생각이 좀 바뀌었습니다. 이건 단순 로그 뷰어라기보다, 에이전트가 어디서 판단을 잘못했는지, 어느 단계에서 토큰을 많이 먹었는지, 어떤 프롬프트 변경이 실제로 비용을 줄였는지 보여주는 일종의 작업 현미경에 가깝더라고요. 오늘은 제가 직접 삽질하면서 알게 된 LangSmith 활용법을 친구에게 커피 한 잔 놓고 이야기하듯이 풀어보겠습니다.
LangSmith trace를 보면 에이전트가 어디서 길을 잃었는지 보입니다
LLM 에이전트 디버깅을 하다 보면 제일 답답한 순간이 있습니다. 결과는 이상한데, 왜 이상한지 알 수 없는 때예요. 코드 에러면 차라리 낫습니다. stack trace라도 있으니까요. 그런데 에이전트는 멀쩡히 실행됐는데 이상한 판단을 해버립니다. tool도 호출했고 응답도 했고 상태 코드도 정상인데, 사용자가 원한 답은 아니죠. 이럴 때 그냥 콘솔 로그만 보면 정신이 조금 아득해집니다.
LangSmith의 trace는 이런 상황에서 꽤 든든합니다. 에이전트가 실행된 전체 흐름을 span 단위로 쪼개서 보여주거든요. 각 단계마다 입력값, 출력값, 소요 시간, 토큰 수, 호출한 tool까지 한눈에 볼 수 있습니다. 뭐랄까, 에이전트 머릿속을 CCTV로 보는 느낌이랄까요.
제가 실제로 겪었던 문제는 이랬습니다. 사용자가 “최신 논문 찾아서 핵심만 정리해줘”라고 요청했는데, 에이전트가 검색 tool을 한 번만 호출하면 될 일을 계속 반복하더라고요. 처음엔 검색 API가 느려서 재시도하는 줄 알았습니다. 그런데 LangSmith trace를 보니 원인이 엉뚱한 데 있었습니다. tool description이 너무 길고 비슷비슷해서 LLM이 어떤 tool을 써야 할지 헷갈리고 있었던 것이었어요.
| 단계 | 호출한 tool | 입력 내용 | 토큰 사용량 | 상태 |
|---|---|---|---|---|
| 검색 요청 | search_docs | 최신 논문 검색 | 450 | 성공 |
| 본문 정리 | summarize | 검색 결과 전체 전달 | 2,100 | 응답 길이 초과 |
| 재시도 | retry_summarize | 요약 재시도 | 1,800 | 성공 |
이 표처럼 보면 어디서 문제가 터졌는지 바로 보입니다. 두 번째 단계에서 검색 결과 전체를 그대로 넘기면서 토큰이 확 늘어났고, 그 여파로 재시도까지 발생한 거죠. 저는 이걸 보고 max_tokens 설정을 줄이고, 검색 결과를 그대로 넘기지 않고 chunk 단위로 정리해서 전달하도록 바꿨습니다.
적용 전에는 한 번 요청에 평균 4,300토큰 정도를 썼는데, 수정 후에는 2,900토큰 안팎으로 내려갔습니다. 아주 드라마틱한 숫자는 아닐 수도 있지만, 하루에 수천 번 호출되는 서비스라면 이야기가 달라집니다. 토큰은 커피값처럼 보이다가 월말에 청구서로 보면 꽤 묵직하거든요.
토큰 절약은 감으로 하면 안 되고, LangSmith 숫자를 보고 해야 합니다
LLM 비용 줄이겠다고 프롬프트를 막 줄이는 분들이 있습니다. 저도 그랬고요. 그런데 프롬프트를 줄인다고 무조건 비용이 줄거나 품질이 좋아지는 건 아니더라고요. 필요한 맥락까지 같이 날려버리면 오히려 재시도가 늘고, hallucination이 늘고, 결국 더 비싸집니다. 좀 얄미운 구조죠.
그래서 저는 LangSmith에서 token usage를 보면서 줄일 부분과 남겨야 할 부분을 나눴습니다. 특히 효과가 컸던 건 대화 이력 관리였습니다. 처음 만든 에이전트는 사용자의 이전 대화를 거의 통째로 context에 넣고 있었습니다. 개발할 때는 편하거든요. “다 넣으면 알아서 잘 이해하겠지”라는 마음도 있고요. 그런데 trace를 보니 대화가 길어질수록 토큰 사용량이 계단식이 아니라 거의 눈덩이처럼 불어났습니다.
그래서 중간에 메모리 요약 단계를 하나 넣었습니다. 대화가 일정 길이를 넘으면 원문 전체를 들고 가지 않고, 다음 판단에 필요한 내용만 요약해서 넘기는 방식입니다. 여기서 중요한 건 그냥 “짧게 요약해줘”가 아니었습니다. 그렇게 하면 필요한 조건까지 날아갑니다. 저는 아래처럼 기준을 조금 더 구체적으로 잡았습니다.
- 사용자가 명시한 요구사항은 유지하기
- 이미 호출한 tool 결과는 중복 전달하지 않기
- 다음 단계 판단에 필요 없는 감탄사, 인사말, 반복 문장은 제거하기
- 숫자, 날짜, 파일명, API 이름은 원문 그대로 남기기
- 불확실한 내용은 추측해서 채우지 않기
실제로 제가 넣었던 구조는 대략 이런 느낌이었습니다. 코드 자체보다 방향만 봐주시면 됩니다.
if conversation_token_count > MEMORY_THRESHOLD:
summary = summarize_memory(
messages=conversation_history,
rules=[
"유저의 요구사항은 유지한다",
"tool 실행 결과는 핵심만 남긴다",
"숫자와 고유명사는 바꾸지 않는다",
"추측하지 않는다"
]
)
next_context = summary + recent_messages
else:
next_context = conversation_history
이걸 적용하고 나서 같은 시나리오를 LangSmith에서 비교해봤습니다. 이전에는 평균 6,000토큰 가까이 쓰던 요청이 3,200토큰 정도로 줄었습니다. 품질도 크게 떨어지지 않았고요. 오히려 이상한 잡음이 빠져서 답변이 더 안정적으로 나오는 경우도 있었습니다. 사실 이때 조금 반성했습니다. LLM에게 많은 정보를 주는 게 친절이라고 생각했는데, 때로는 잘 정리해서 주는 게 진짜 친절이더라고요.
프롬프트 튜닝은 예쁘게 쓰는 일이 아니라, 덜 헷갈리게 만드는 일입니다
프롬프트를 오래 만지다 보면 자꾸 문장을 예쁘게 쓰고 싶어집니다. 저도 처음에는 시스템 프롬프트에 이런 말을 많이 넣었습니다. “당신은 매우 유능하고 친절한 AI assistant입니다.” “사용자에게 최선의 답변을 제공하세요.” 이런 문장들요. 물론 나쁜 문장은 아닙니다. 그런데 실무 에이전트에서는 이런 문장이 큰 도움이 안 되는 경우가 많았습니다.
LangSmith로 span별 입력 프롬프트를 보다 보니, 진짜 토큰을 많이 차지하는 건 의외로 이런 장식 문장과 긴 tool 설명이었습니다. 특히 tool description은 길면 길수록 좋아 보이지만, LLM 입장에서는 비슷한 설명이 여러 개 있으면 선택 기준이 흐려집니다. 사람도 메뉴판이 너무 길면 결정이 늦어지잖아요. 에이전트도 비슷했습니다.
| 항목 | 수정 전 | 수정 후 | 느낀 점 |
|---|---|---|---|
| 시스템 프롬프트 | 역할, 태도, 말투 설명이 길게 포함 | 작업 목표와 금지 조건 중심 | 답변이 덜 장황해짐 |
| tool description | 사용 예시와 예외 상황까지 길게 작성 | 핵심 동사와 입력 조건만 작성 | tool 선택 오류가 줄어듦 |
| few-shot 예제 | 긴 예제 5개 | 짧고 대표적인 예제 2개 | 토큰 절약 효과가 큼 |
제가 요즘 선호하는 tool description은 이런 식입니다.
search_docs:
사용자 질문과 관련된 문서를 검색한다.
입력: 검색어
출력: 문서 제목, 요약, URL
summarize_docs:
검색된 문서를 핵심 주장과 근거 중심으로 요약한다.
입력: 문서 목록
출력: 5문장 이내 요약
별거 아닌 것 같지만 이런 단순한 정리가 꽤 잘 먹힙니다. 예전에는 tool마다 설명이 10줄 넘게 있었는데, 지금은 가능하면 3줄 안쪽으로 정리합니다. 그리고 이 변경이 실제로 효과가 있는지는 꼭 LangSmith의 compare runs로 봅니다. 감으로 “좋아진 것 같아”라고 말하는 건 개발자답지 않으니까요. 물론 저도 가끔 감으로 밀어붙입니다. 사람인데요, 뭐.
에이전트 무한 루프는 recursion limit만 믿으면 조금 위험합니다
에이전트가 무한 루프에 빠지는 상황, 한 번쯤은 다 겪어보셨을 겁니다. 검색하고, 판단하고, 다시 검색하고, 또 판단하고. 옆에서 보면 “그만해, 이제 충분해”라고 말해주고 싶어집니다. 코드에서는 recursion limit이나 max iterations를 걸어두면 일단 폭주는 막을 수 있습니다. 그런데 문제는 그게 왜 발생했는지까지 알려주지는 않는다는 겁니다.
저는 그래서 LangSmith trace에 iteration 정보를 같이 남깁니다. 별거 아닌데 나중에 분석할 때 정말 편합니다.
run.extra["iteration"] = step
run.extra["agent_state"] = current_state
run.extra["selected_tool"] = tool_name
이렇게 남겨두면 LangSmith에서 iteration이 비정상적으로 높은 run만 골라볼 수 있습니다. 제 경우에는 특정 질문 유형에서만 반복이 늘어나는 패턴을 발견했습니다. 사용자가 “비교해서 추천해줘”라고 요청하면 에이전트가 기준을 세우지 못하고 검색을 반복하더라고요. 그래서 프롬프트에 “비교 기준이 없으면 가격, 안정성, 유지보수성 기준으로 판단한다”는 문장을 추가했습니다.
재미있는 건, 이 문장 하나로 반복 호출이 꽤 줄었다는 겁니다. 거창한 모델 교체나 아키텍처 변경이 아니라, 판단 기준을 조금 선명하게 준 것뿐인데요. 이런 걸 보면 LLM 에이전트 개발은 개발이면서도 약간 사람 교육하는 느낌이 납니다. “이럴 땐 이렇게 해”라고 차분히 알려줘야 하더라고요.
callback 알림을 붙여두면 주말에 마음이 조금 편합니다
운영 환경에서 에이전트가 터지면 제일 곤란한 게 “어디서 터졌는지 모르는 상태”입니다. 사용자 입장에서는 그냥 답변이 늦거나 이상하게 보이지만, 개발자 입장에서는 그 뒤에 어떤 tool이 실패했는지, 어떤 입력이 문제였는지 알아야 하잖아요.
저는 LangSmith와 Callbacks를 같이 써서 주요 오류를 자동으로 기록하게 해뒀습니다. 예를 들어 Code Interpreter 쪽에서 실행 오류가 나거나, 외부 API가 500을 반환하거나, JSON 파싱이 깨지는 경우에는 trace URL과 함께 알림을 받도록 했습니다. Slack으로 받아도 되고, 사내 알림 시스템에 붙여도 됩니다.
- 외부 API timeout 발생 시 trace URL 기록
- LLM 응답이 JSON schema와 맞지 않을 때 원문 응답 저장
- tool 호출 실패가 2회 이상 반복되면 알림 전송
- 토큰 사용량이 기준치를 넘으면 별도 태그 추가
이걸 해두면 장애 대응할 때 “재현이 안 됩니다”라는 말을 조금 덜 하게 됩니다. trace에 당시 입력과 출력이 남아 있으니까요. 물론 민감한 데이터는 마스킹해야 합니다. 이건 정말 중요합니다. 고객 데이터나 개인정보가 들어가는 서비스라면 LangSmith에 남길 필드와 숨길 필드를 처음부터 분리해두는 게 좋습니다. 나중에 하려면 귀찮고, 귀찮으면 사고 납니다.
프롬프트 버전은 프로젝트별로 나눠야 나중에 덜 헷갈립니다
LangSmith를 쓰다 보면 프롬프트도 점점 여러 버전이 생깁니다. 처음에는 파일 하나에 대충 관리해도 괜찮습니다. 그런데 에이전트가 늘어나고, 팀원이 붙고, A/B 테스트를 하기 시작하면 금방 복잡해집니다. “이 프롬프트가 검색 에이전트용이었나, 번역 에이전트용이었나?” 같은 순간이 옵니다. 저는 그런 순간을 별로 좋아하지 않습니다. 이미 충분히 머리 아픈 일이 많거든요.
그래서 저는 LangSmith 프로젝트를 기능 단위로 나눠서 씁니다.
| 프로젝트 | 용도 | 주로 보는 지표 |
|---|---|---|
| 검색 에이전트 | 문서 검색과 근거 기반 답변 | tool 선택률, hallucination 비율, 검색 재시도 횟수 |
| 요약 에이전트 | 긴 문서 요약과 핵심 추출 | 토큰 사용량, 응답 길이, 누락 항목 |
| 번역 에이전트 | 문맥 유지 번역 | 용어 일관성, 재요청 비율, 사용자 수정률 |
이렇게 나눠두면 같은 base prompt를 조금씩 바꿔가며 비교하기가 좋습니다. 특히 compare runs 기능으로 동일한 입력을 여러 버전에 넣어보고 나란히 비교하면, 어떤 프롬프트가 더 짧고 안정적인지 감이 옵니다. 저는 여기서 “짧은데 성능이 비슷한 프롬프트”를 꽤 좋아합니다. 긴 프롬프트는 뭔가 든든해 보이지만, 운영비 입장에서는 조용히 지갑을 갉아먹습니다.
실제 사례: 고객사 데이터 분석 에이전트에서 hallucination을 줄인 방법
얼마 전에 고객사 내부 데이터를 분석하는 에이전트를 만들 일이 있었습니다. 요구사항은 단순해 보였습니다. 데이터를 읽고, 지표를 계산하고, 이상 징후를 설명하는 기능이었죠. 그런데 테스트 중에 에이전트가 자꾸 존재하지 않는 데이터를 “있다”고 말했습니다. 예를 들면 실제 데이터에는 없는 지역명을 언급하거나, 계산하지 않은 전월 대비 수치를 자연스럽게 만들어내는 식이었습니다. 이런 건 정말 위험합니다. 말투가 너무 그럴듯해서 더 문제예요.
처음에는 모델 문제라고 생각했습니다. 모델을 바꿔볼까, temperature를 낮춰볼까 고민했죠. 그런데 LangSmith trace를 따라가 보니 원인은 조금 달랐습니다. 세 번째 단계에서 context window가 꽉 차면서 중간 계산 결과가 잘려나가고 있었던 것이었습니다. 에이전트는 잘린 결과를 보고 빈칸을 추측으로 메우고 있었고요. 참 사람 같기도 하고, 그래서 더 무섭기도 했습니다.
이 문제를 잡기 위해 전처리 파이프라인을 하나 추가했습니다. 전체 데이터를 무작정 context에 넣는 대신, 질문과 관련된 컬럼, 기간, 지표만 먼저 추려낸 뒤 넘기도록 바꿨습니다. 그리고 중간 계산 결과는 자연어 설명보다 구조화된 JSON 형태로 넘겼습니다.
{
"metric": "monthly_active_users",
"period": "2024-01 ~ 2024-06",
"values": [1200, 1350, 1310, 1480, 1520, 1495],
"note": "원본 데이터에 없는 값은 생성하지 않는다"
}
이후 LangSmith에서 같은 입력을 다시 돌려봤습니다. hallucination이 완전히 사라졌다고 말하긴 어렵습니다. LLM을 쓰는 이상 0%라고 장담하는 건 좀 위험하니까요. 다만 테스트 케이스 기준으로 이상 응답이 90% 이상 줄었습니다. 특히 “데이터에 없는 내용은 확인할 수 없다고 말하기” 조건을 프롬프트와 후처리 양쪽에 넣은 게 효과가 있었습니다.
여기서 제가 얻은 교훈은 단순합니다. LLM에게 많은 데이터를 던져주는 것보다, 믿을 수 있는 형태로 적당히 정리해서 주는 게 훨씬 낫다는 겁니다. 이건 개발 오래 해도 자꾸 잊게 됩니다. 많이 주면 잘할 것 같지만, 실제로는 잘못 주면 더 자신 있게 틀립니다.
제가 LangSmith 설정할 때 거의 기본으로 넣는 체크리스트
요즘은 새 에이전트 프로젝트를 시작하면 아래 설정은 거의 기본으로 넣습니다. 처음부터 다 완벽하게 할 필요는 없지만, 최소한 이 정도만 해둬도 나중에 디버깅 시간이 꽤 줄어듭니다.
- trace 활성화: 개발 환경뿐 아니라 staging 환경에도 켜두기
- token usage 확인: 단계별 토큰 사용량을 보고 병목 구간 찾기
- iteration count 기록: 무한 루프나 반복 호출 패턴 잡기
- tool 호출 결과 저장: 실패한 tool과 입력값을 나중에 확인할 수 있게 하기
- 프롬프트 버전 관리: 변경 이유와 적용 날짜를 같이 남기기
- 민감정보 마스킹: 이름, 이메일, 전화번호, 고객 식별자는 trace에 남기지 않기
- compare runs 활용: 프롬프트 변경 전후를 같은 입력으로 비교하기
개인적으로 제일 중요하게 보는 건 token usage와 iteration count입니다. 이 두 개만 잘 봐도 “왜 비싼지”와 “왜 느린지”가 꽤 많이 설명됩니다. 그리고 이상하게 느린 에이전트는 대부분 쓸데없이 많이 생각하거나, 같은 tool을 반복 호출하고 있었습니다. 사람도 회의가 길어지면 결과가 좋아지는 게 아니잖아요. 에이전트도 비슷합니다.
이런 분들은 LangSmith를 꼭 한 번 제대로 써보면 좋겠습니다
LangSmith는 LLM 에이전트를 처음 만지는 분에게도 좋지만, 저는 오히려 어느 정도 만들어보고 한 번쯤 크게 데인 분들에게 더 잘 맞는 도구라고 생각합니다. “왜 비용이 이렇게 나오지?”, “왜 같은 질문인데 답이 달라지지?”, “왜 tool을 이상하게 고르지?” 같은 질문을 해본 분들이라면 trace를 보는 순간 꽤 반가울 겁니다.
특히 이런 분들에게 추천하고 싶습니다.
- 복잡한 multi-step agent를 운영하거나 개발 중인 개발자
- LLM API 비용이 슬슬 부담되기 시작한 스타트업 팀
- 프롬프트를 자주 바꾸는데 변경 효과를 숫자로 보고 싶은 분
- hallucination 원인을 감이 아니라 trace로 확인하고 싶은 분
- 에이전트가 tool을 반복 호출하거나 무한 루프에 빠져 고생한 분
제가 느끼기엔 LangSmith의 진짜 장점은 “문제를 멋지게 해결해준다”가 아닙니다. 문제를 피하지 못하게 보여준다는 쪽에 더 가깝습니다. 어디서 토큰을 낭비하는지, 어떤 프롬프트가 헷갈리는지, 어느 단계에서 context가 잘리는지 꽤 적나라하게 보여줍니다. 처음엔 조금 민망합니다. 내가 만든 에이전트가 이렇게 헤매고 있었나 싶거든요. 그런데 그걸 봐야 고칠 수 있습니다.
LLM 개발은 아직 정답지가 없는 영역이 많습니다. 그래서 더더욱 기록이 중요하다고 생각합니다. 감으로 튜닝하고, 느낌으로 배포하면 언젠가 꼭 다시 돌아옵니다. LangSmith trace를 붙이고, 토큰 사용량을 보고, 프롬프트 버전을 비교하는 습관만 들여도 에이전트 개발이 훨씬 덜 피곤해집니다. 저처럼 밤에 로그 붙잡고 혼잣말하기 싫다면, 다음 프로젝트에서는 LangSmith를 처음부터 옆에 두고 시작해보셔도 좋겠습니다.
댓글
댓글 쓰기