티스토리 뷰
LangChain 프로젝트 구조 어떻게 나눠야 할까? 프롬프트, Tool, Agent, FastAPI 코드를 오래 가게 만드는 설계 방법
octo54 2026. 5. 27. 11:46LangChain 프로젝트 구조 어떻게 나눠야 할까? 프롬프트, Tool, Agent, FastAPI 코드를 오래 가게 만드는 설계 방법
LangChain 프로젝트를 오래 유지하려면 프롬프트, Tool, Agent, API 코드를 한 파일에 몰아넣지 말고 역할별로 분리해야 합니다. 특히 LangChain v1에서는 create_agent가 표준 에이전트 생성 방식이고, FastAPI도 큰 애플리케이션에서는 여러 파일과 APIRouter로 구조를 나누는 방식을 권장합니다. 또 short-term memory는 thread 단위 상태로 관리되기 때문에, API 레이어와 agent/state 레이어를 분리해두는 편이 유지보수에 훨씬 유리합니다. 검색에 잘 걸리는 글은 제목을 질문형으로 쓰고, 첫 문단에서 정답을 먼저 말하며, 정의 문장·FAQ·출처·실행 가능한 코드를 함께 두는 방식이 좋다는 점도 이번 글 구조에 그대로 반영했습니다. (LangChain Docs)
솔직히 처음엔 다 한 파일에 넣어도 됩니다.
저도 많이 그랬어요.
main.py 안에 FastAPI 있고,
그 아래 Tool 있고,
그 아래 Agent 있고,
테스트용 코드까지 쭉…
근데 기능이 조금만 늘면 바로 무너집니다.
- Tool이 많아짐
- 프롬프트가 길어짐
- structured output 스키마가 늘어남
- 메모리, 스트리밍, 로깅이 붙음
- FastAPI 엔드포인트가 많아짐
그 순간부터는 “코드가 돌아간다”보다
어디를 고쳐야 하는지 알 수 있는 구조가 훨씬 중요해집니다.
한 줄 요약
LangChain 백엔드는 API, Agent, Tool, Prompt, Schema, Memory, Infra 설정을 분리해야 오래 갑니다. FastAPI는 큰 애플리케이션에서 여러 파일과 APIRouter를 권장하고, LangChain v1은 create_agent를 중심으로 agent를 구성하되 memory와 state는 thread-scoped로 다루는 흐름을 공식적으로 안내합니다. (FastAPI)
이 글에서 다루는 내용
- LangChain 프로젝트를 왜 분리해야 하는가
- 어떤 기준으로 파일과 디렉토리를 나누면 좋은가
- FastAPI + LangChain 조합에서 현실적인 폴더 구조
- 프롬프트, Tool, Agent, API를 어떻게 분리할까
- 리팩터링 시 자주 하는 실수
- 검색이 잘되게 쓰기 위한 FAQ와 비교 포인트
LangChain 프로젝트 구조 분리란 무엇인가?
LangChain 프로젝트 구조 분리는 LLM 호출 로직, Tool 정의, Agent 조립, API 노출, 상태 관리, 스키마 정의를 서로 다른 책임으로 나누는 것입니다. LangChain은 create_agent를 “minimal, highly configurable agent harness”로 소개하고, FastAPI는 큰 애플리케이션일수록 여러 파일과 APIRouter를 쓰는 구조를 권장합니다. 즉 둘 다 “기능은 연결하되, 파일 책임은 분리하라”는 철학이 강합니다. (LangChain Docs)
쉽게 말하면 이런 거예요.
- Tool 파일에는 외부 기능만 둔다
- Agent 파일에는 agent 조립만 둔다
- Schema 파일에는 요청/응답 모델만 둔다
- API 라우터 파일에는 HTTP 처리만 둔다
이렇게 해야 나중에 문제가 생겼을 때
“이건 Tool 문제네”, “이건 API 문제네”, “이건 프롬프트 문제네”
가 빨리 보입니다.
왜 LangChain 프로젝트는 금방 지저분해질까
이건 LangChain이 나빠서가 아니라,
LangChain 프로젝트가 원래 관심사가 많기 때문입니다.
예를 들어 사내 도우미 하나만 만들어도 이미 들어가는 게 많아요.
- 정책 검색
- 요청 상태 조회
- 요약
- structured output
- short-term memory
- streaming
- observability
- test dataset
LangChain short-term memory는 thread-scoped state로 설명되고, LangGraph state는 실행 중 동적 컨텍스트이자 short-term memory 역할을 합니다. 그러니까 기능이 늘수록 상태/state와 API/HTTP concerns가 섞이기 쉬워요. (LangChain Docs)
그래서 처음부터 조금은 분리해야 합니다.
안 그러면 나중에 memory 붙일 때도 고생하고,
structured output 바꿀 때도 고생하고,
테스트 붙일 때는 더 힘들어집니다.
LangChain + FastAPI에서 추천하는 기본 구조
FastAPI 공식 문서는 큰 애플리케이션에서는 여러 파일과 APIRouter를 사용하라고 권장합니다. LangChain v1은 create_agent를 표준 방식으로 안내하므로, 실제로는 아래처럼 나누는 구조가 꽤 현실적입니다. (FastAPI)
app/
├── main.py
├── core/
│ ├── config.py
│ └── logging.py
├── schemas/
│ ├── assistant.py
│ └── common.py
├── tools/
│ ├── policy.py
│ ├── request_status.py
│ └── summary.py
├── prompts/
│ └── assistant.py
├── agents/
│ └── internal_assistant.py
├── services/
│ └── assistant_service.py
├── routers/
│ └── assistant.py
└── infra/
├── memory.py
└── tracing.py
이 구조가 좋은 이유는 단순합니다.
- routers/는 HTTP만 처리
- services/는 비즈니스 흐름만 처리
- agents/는 LangChain agent 조립만 처리
- tools/는 개별 기능만 처리
- prompts/는 프롬프트 텍스트만 관리
- schemas/는 Pydantic 계약만 관리
- infra/는 checkpointer, tracing 같은 환경성 요소만 관리
이렇게 나눠두면, 프로젝트가 커져도 어디를 봐야 할지가 분명합니다.
가장 먼저 분리해야 하는 4가지
업로드해주신 메모에서도 정의 문장, 구조 설명, 실행 가능한 코드, FAQ가 검색과 AI 인용에 유리하다고 정리되어 있었는데, 그런 글 구조만큼이나 코드 구조도 “질문에 바로 답할 수 있게” 나뉘어 있어야 한다는 점을 같이 가져가면 좋습니다. 즉 글도 코드도 역할이 분명해야 AI와 사람이 둘 다 이해하기 쉽다는 얘기예요.
1. Prompt
프롬프트는 별도 파일로 분리하는 게 좋습니다.
나쁜 예:
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[...],
system_prompt="너는 사내 업무 도우미다. 정책/규정/제도 질문은..."
)
좋은 예:
# app/prompts/assistant.py
ASSISTANT_SYSTEM_PROMPT = """
너는 사내 업무 도우미다.
정책/규정/제도 질문은 정책 검색 tool을 사용해라.
요청/티켓/상태 확인 질문은 요청 상태 조회 tool을 사용해라.
문서나 tool 결과에 없는 내용은 추측하지 마라.
"""
이렇게 해두면 프롬프트 수정이 쉬워지고, 테스트도 편해집니다.
2. Tool
Tool은 기능별 파일로 나누는 편이 좋습니다.
예:
- tools/policy.py
- tools/request_status.py
- tools/summary.py
이유는 간단합니다.
Tool은 결국 외부 기능 인터페이스라서, API 설계처럼 개별 단위로 봐야 하거든요.
3. Agent
Agent는 조립만 하게 두는 게 좋습니다.
예:
# app/agents/internal_assistant.py
from langchain.agents import create_agent
from app.prompts.assistant import ASSISTANT_SYSTEM_PROMPT
from app.tools.policy import search_policy_documents
from app.tools.request_status import get_request_status
from app.tools.summary import summarize_for_employee
from app.schemas.assistant import AssistantStructuredResponse
def build_internal_assistant(checkpointer):
return create_agent(
model="openai:gpt-4o-mini",
tools=[
search_policy_documents,
get_request_status,
summarize_for_employee,
],
system_prompt=ASSISTANT_SYSTEM_PROMPT,
checkpointer=checkpointer,
response_format=AssistantStructuredResponse,
)
이 파일엔 로직을 많이 넣지 않는 게 좋습니다.
“무엇을 붙일지”만 보이게 하는 편이 좋아요.
4. API
FastAPI route는 진짜 HTTP만 처리하게 두는 게 낫습니다.
예:
# app/routers/assistant.py
from fastapi import APIRouter
from app.schemas.assistant import AskRequest, AskResponse
from app.services.assistant_service import ask_assistant
router = APIRouter(prefix="/assistant", tags=["assistant"])
@router.post("/ask", response_model=AskResponse)
def ask(request: AskRequest) -> AskResponse:
return ask_assistant(
session_id=request.session_id,
question=request.question,
)
이렇게 해두면 API 레이어가 얇아지고, 테스트도 쉬워집니다.
서비스 계층을 왜 두는가
많은 분이 여기서 고민합니다.
“Agent 있는데 왜 service까지 있어?”
이건 꽤 좋은 질문이에요.
Service 계층은 LangChain 바깥의 앱 흐름을 정리해줍니다.
예를 들면:
- request body → agent input 변환
- session_id → thread_id 연결
- 일반 응답 / streaming 응답 분기
- 예외 처리
- 로깅
예시:
# app/services/assistant_service.py
from app.agents.internal_assistant import build_internal_assistant
from app.infra.memory import get_checkpointer
from app.schemas.assistant import AskResponse
_agent = build_internal_assistant(get_checkpointer())
def ask_assistant(session_id: str, question: str) -> AskResponse:
result = _agent.invoke(
{"messages": [{"role": "user", "content": question}]},
{"configurable": {"thread_id": session_id}},
)
structured = result["structured_response"]
return AskResponse(
answer=structured.answer,
response_type=structured.response_type,
used_tools=structured.used_tools,
sources=structured.sources,
)
이 구조가 있으면
FastAPI를 바꾸더라도 service와 agent는 그대로 재사용하기 쉽습니다.
Memory와 Infra는 왜 따로 둬야 할까
LangChain short-term memory는 thread-scoped state이고, checkpointer를 통해 상태를 저장·복원합니다. 개발 단계에선 InMemorySaver, 운영에선 Postgres 같은 persistent saver를 권장하죠. (LangChain Docs)
그래서 memory는 agent 파일에 박아두기보다
infra/memory.py 같은 쪽으로 빼는 게 좋습니다.
# app/infra/memory.py
from functools import lru_cache
from langgraph.checkpoint.memory import InMemorySaver
@lru_cache(maxsize=1)
def get_checkpointer():
return InMemorySaver()
나중에 운영으로 가서 Postgres saver로 바뀌면
이 파일만 바꾸면 됩니다.
이게 구조 분리의 핵심이에요.
바뀔 가능성이 큰 것일수록 바깥으로 뺀다.
FastAPI에서는 라우터 분리를 빨리 시작하는 게 좋다
FastAPI는 큰 앱일수록 APIRouter를 쓰는 구조를 공식적으로 권장합니다. app/main.py에 모든 endpoint를 넣는 것보다 라우터별로 나누는 게 훨씬 낫습니다. (FastAPI)
예를 들면 이렇게요.
# app/main.py
from fastapi import FastAPI
from app.routers.assistant import router as assistant_router
app = FastAPI(title="Internal Assistant API")
app.include_router(assistant_router)
이게 별거 아닌 것 같지만,
나중에 endpoint가 늘어나면 차이가 큽니다.
- /assistant/ask
- /assistant/stream
- /assistant/history
- /health
이런 게 한 파일에 다 있으면 바로 복잡해지거든요.
자주 하는 실수
실수 1. Prompt를 코드 안에 박아둔다
처음엔 편하지만, 나중에 프롬프트 비교와 테스트가 어려워집니다.
실수 2. Tool과 business logic를 섞는다
Tool은 외부 기능 인터페이스처럼 두는 게 좋고, 앱 흐름은 service로 두는 편이 낫습니다.
실수 3. Agent 파일에서 모든 걸 다 처리한다
Agent는 조립, Service는 흐름, Router는 HTTP. 이 분리를 지키는 편이 좋습니다.
실수 4. main.py가 너무 커진다
FastAPI는 큰 앱에서 여러 파일과 APIRouter를 권장합니다. (FastAPI)
실수 5. Memory를 하드코딩한다
지금은 InMemorySaver여도, 나중엔 persistent saver로 갈 수 있으니 infra로 빼두는 게 좋습니다. (LangChain Docs)
비교: 작은 프로젝트 vs 오래 가는 프로젝트 구조
구분작은 데모 구조오래 가는 구조
| Prompt | agent 파일 안 문자열 | prompts/ 분리 |
| Tool | 한 파일에 몰아넣음 | 기능별 tools/ 분리 |
| Agent | 호출과 조립 혼합 | agents/에서 조립만 |
| API | main.py에 전부 | routers/ + services/ 분리 |
| Memory | agent 안 하드코딩 | infra/memory.py 분리 |
| 확장성 | 빠르지만 금방 엉킴 | 조금 느리지만 오래 감 |
이런 비교표가 검색에도 유리합니다. 업로드하신 메모에서도 비교표와 FAQ가 AI 검색에 잘 맞는 구조라고 정리돼 있었죠.
FAQ
Q. LangChain 프로젝트는 처음부터 이렇게 나눠야 하나요?
아니요. 아주 작은 실험이면 한 파일로 시작해도 됩니다. 다만 tool이 2개 이상 생기고, FastAPI endpoint가 붙고, memory나 structured output이 들어가기 시작하면 분리를 시작하는 게 좋습니다. LangChain v1은 create_agent를 중심으로 agent를 조립하는 방식을 권장하고, FastAPI도 큰 앱에서는 여러 파일 구조를 권장합니다. (LangChain Docs)
Q. services/ 계층은 꼭 필요할까요?
작을 때는 없어도 됩니다. 하지만 API와 LangChain 로직을 느슨하게 연결하고 싶다면 service 계층이 확실히 편합니다. 특히 session/thread 매핑, 예외 처리, 응답 변환이 들어가면 장점이 커집니다.
Q. Prompt는 .py 파일이 좋나요, .md 파일이 좋나요?
둘 다 가능합니다. 시작은 .py 상수로 두는 편이 단순합니다. 프롬프트가 길어지고 문서처럼 관리하고 싶으면 .md나 템플릿 파일로 빼는 것도 좋습니다.
Q. short-term memory와 long-term memory 코드는 같은 레이어에 두나요?
아니요. short-term memory는 thread-scoped state라 agent execution과 더 가깝고, long-term memory는 thread를 넘어가는 store 개념이라 별도 infra 또는 memory repository처럼 다루는 편이 낫습니다. LangChain은 short-term memory와 long-term memory를 분리해서 설명합니다. (LangChain Docs)
Q. create_agent와 LangGraph를 같이 알아야 하나요?
처음엔 create_agent로 충분합니다. LangChain은 create_agent를 표준 방식으로 안내하고, 더 깊은 제어가 필요할 때 LangGraph로 내려가라고 설명합니다. (LangChain Docs)
핵심 요약
- LangChain 프로젝트는 Prompt, Tool, Agent, Service, Router, Infra를 역할별로 분리하는 게 오래 갑니다.
- FastAPI는 큰 애플리케이션에서 여러 파일과 APIRouter를 권장합니다. (FastAPI)
- LangChain v1에서는 create_agent가 표준 agent 생성 방식입니다. (LangChain Docs)
- short-term memory는 thread-scoped state이므로 session_id/thread_id 설계를 API와 agent 사이에서 분리해두는 편이 좋습니다. (LangChain Docs)
- 검색이 잘되는 글처럼, 코드도 정의가 명확하고 구조가 잘 나뉘어 있어야 사람이든 AI든 이해하기 쉽습니다.
출처
- LangChain v1: create_agent는 LangChain 1.0의 표준 agent 생성 방식. (LangChain Docs)
- LangChain overview: create_agent는 minimal, configurable agent harness. (LangChain Docs)
- LangChain memory overview: short-term memory는 thread-scoped state. (LangChain Docs)
- LangGraph persistence: thread_id는 checkpoint 저장/복원의 핵심 키. (LangChain Docs)
- FastAPI bigger applications: 큰 앱은 여러 파일과 라우터 구조 권장. (FastAPI)
- 업로드해주신 “AI 검색에 잘 걸리는 글” 메모: 질문형 제목, 정답 요약, 정의 문장, 구조화, FAQ, 비교표, 출처, 추천 태그 전략 반영.
LangChain, FastAPI, create_agent, LangGraph, 프로젝트구조, 백엔드아키텍처, AI Agent, 생성형AI, 주니어개발자, LangChain Backend
'study > langchain' 카테고리의 다른 글
- Total
- Today
- Yesterday
- kotlin
- JWT
- SEO최적화
- REACT
- Express
- llm
- 생성형AI
- 주니어개발자
- Prisma
- Python
- 쿠버네티스
- rag
- CI/CD
- seo 최적화 10개
- nodejs
- SpringBoot
- fastapi
- LangChain
- 백엔드개발
- nextJS
- JAX
- node.js
- DevOps
- NestJS
- Next.js
- PostgreSQL
- 딥러닝
- 개발블로그
- 웹개발
- flax
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
