티스토리 뷰

반응형

LangChain memory와 RAG를 같이 쓸 때 품질은 어떻게 올릴까? 검색 우선순위, prompt 조합, 충돌 처리 전략

LangChain에서 memory와 RAG를 같이 붙이면 서비스가 확실히 더 쓸 만해집니다. 그런데 아무 규칙 없이 섞으면 오히려 품질이 흔들립니다. 공식 문서 기준으로 memory는 사용자/대화 상태를 다루고, RAG는 외부 문서를 검색해 근거를 가져오는 구조입니다. 또 LangChain은 retrieval에 similarity, mmr, similarity_score_threshold 같은 검색 전략을 제공하고, context engineering 문서에서는 시스템 프롬프트가 현재 상태, 메모리, 설정을 바탕으로 달라져야 한다고 설명합니다. Deep Agents 문서는 memory는 항상 주입되기 쉬워서 최소한으로 유지하라고도 권장합니다. 그래서 memory와 RAG를 함께 쓸 때 핵심은 “둘 다 많이 넣는 것”이 아니라 무엇을 먼저 믿고, 어떤 순서로 넣고, 서로 충돌하면 누가 우선인지 정하는 것입니다. (LangChain Docs)

저는 이 구간에서 진짜 많이 헤맸습니다.
처음엔 단순하게 생각했어요.

  • 사용자 선호도 넣고
  • 문서 검색 결과도 넣고
  • 프롬프트에 다 붙이면 더 똑똑해지겠지

근데 아니더라고요.

오히려 이런 일이 생깁니다.

  • 사용자가 “짧게 답해줘”라고 했는데 문서 근거가 너무 길어서 답이 장황해짐
  • RAG 문서엔 없는 내용을 memory 때문에 자신 있게 말함
  • 오래된 사용자 선호가 최신 질문 의도를 덮어버림
  • memory도 길고 문서도 길어서 context가 비대해짐

그래서 이번 글은
memory와 RAG를 같이 쓸 때 품질을 올리는 실제 기준을 정리해보겠습니다.


이 글에서 바로 답하는 질문

  • memory와 RAG를 같이 쓰면 왜 품질이 흔들릴까
  • memory와 RAG 중 무엇을 먼저 넣어야 할까
  • 둘이 충돌하면 누구를 우선해야 할까
  • prompt는 어떻게 나누면 좋을까
  • retriever 검색 품질은 어떻게 올릴까
  • FastAPI + LangChain 구조에선 어느 레이어에서 정리해야 할까

한 줄 요약

memory와 RAG를 같이 쓸 때 품질을 올리는 가장 현실적인 방법은 이겁니다.

사실 정보는 RAG를 우선하고, 답변 스타일과 사용자 선호는 memory를 우선하며, 둘을 프롬프트 안에서 섹션별로 분리하고, retrieval은 threshold/MMR 같은 검색 전략으로 노이즈를 줄이는 것. LangChain retrieval 문서는 VectorStoreRetriever가 similarity, mmr, similarity_score_threshold를 지원한다고 설명하고, context engineering 문서는 메모리·상태·설정을 현재 대화 단계에 맞게 시스템 프롬프트에 반영해야 한다고 설명합니다. Deep Agents 문서는 memory를 최소로 유지하라고 권합니다. (LangChain Docs)


왜 memory와 RAG를 같이 쓰면 품질이 흔들릴까

이유는 생각보다 단순합니다.

둘 다 “추가 문맥”이기 때문이에요.

  • memory도 모델에 넣는 문맥
  • RAG 문서도 모델에 넣는 문맥

그런데 이 둘의 역할은 다릅니다.

LangChain 메모리 문서는 short-term memory를 thread-scoped state, long-term memory를 세션을 넘어가는 user/application data로 설명합니다. 반면 retrieval/RAG 문서는 retrieval이 런타임에 관련 컨텍스트를 가져와 grounded answer를 만들기 위한 기반이라고 설명합니다. 즉 memory는 “사용자/상태”, RAG는 “외부 근거”인데, 둘 다 그냥 한 덩어리 문맥으로 던져버리면 모델 입장에선 구분이 흐려질 수 있습니다. (LangChain Docs)

쉽게 말하면 이런 거예요.

  • memory: “이 사용자는 짧은 답을 좋아해”
  • RAG: “출장비는 귀사 후 7일 이내 정산”

이 둘은 성격이 전혀 다른데,
프롬프트에 그냥 쭉 붙여놓으면 모델이
무엇이 사실, 무엇이 스타일, 무엇이 선호인지 덜 분명하게 볼 수 있습니다.

그래서 품질이 흔들립니다.


memory와 RAG의 우선순위는 어떻게 잡아야 할까

이건 진짜 중요합니다.

저는 보통 이렇게 정리합니다.

1. 사실과 근거는 RAG 우선

공식 정책, 규정, 문서, 매뉴얼처럼 검증 가능한 내용은 RAG를 우선해야 합니다. RAG 문서는 retrieval이 grounded, context-aware answers를 만드는 핵심이라고 설명합니다. (LangChain Docs)

2. 스타일과 개인화는 memory 우선

답변 길이, 톤, 선호 언어, 반복되는 사용자 제약은 memory가 더 맞습니다. 메모리 문서는 long-term memory를 user-specific data across sessions로 설명합니다. (LangChain Docs)

3. 충돌 시 사실 > 선호

예를 들어 사용자가 “짧게 말해줘”라고 했더라도, 규정상 꼭 전달해야 하는 핵심 경고나 조건이 있으면 생략하면 안 됩니다. 이건 문서 기반 사실을 더 우선하는 게 맞습니다. retrieval의 목적이 grounded answers라는 점을 생각하면 자연스럽습니다. (LangChain Docs)

즉 정리하면:

  • 무엇이 맞는가? → RAG
  • 어떻게 말할까? → memory

이 기준이 있으면 훨씬 덜 흔들립니다.


prompt는 왜 “섹션 분리”가 중요할까

많은 분이 여기서 한 번 틀어집니다.
memory와 RAG를 둘 다 가져와 놓고 프롬프트에 그냥 합쳐 넣어요.

그런데 공식 context engineering 문서는 시스템 프롬프트가 메모리, 선호, 설정, 현재 상태를 바탕으로 적절히 구성돼야 한다고 말합니다. 다시 말해, 문맥을 어떻게 구조화해서 모델에 주느냐가 핵심이라는 뜻입니다. (LangChain Docs)

그래서 저는 프롬프트를 이렇게 나누는 편이 좋다고 봅니다.

[사용자 선호 / 메모리]
- 답변은 짧게
- 재무팀 사용자
- 영어보다 한국어 선호

[검색된 문서 근거]
- 출장비는 귀사 후 7일 이내 정산
- 영수증 첨부 필요

[답변 규칙]
- 사실은 문서 근거를 우선
- 선호는 표현 방식에만 반영
- 문서에 없는 내용은 추측하지 말 것

이 구조가 좋은 이유는 명확합니다.

  • memory는 personal context
  • RAG는 factual context
  • rule은 decision rule

역할이 눈에 보여요.


memory와 RAG가 충돌할 때는 어떻게 처리할까

실제 서비스에선 충돌이 꽤 자주 납니다.

예를 들면:

  • memory: “사용자는 짧은 답을 좋아함”
  • RAG: “정책상 예외 조건과 부가 조건이 길게 붙어 있음”

이럴 때 그냥 짧게만 답하면 사실을 놓칠 수 있고,
길게 다 쓰면 사용자 선호를 무시하게 됩니다.

그래서 저는 보통 이렇게 처리합니다.

전략 1. 핵심 사실은 유지, 표현만 압축

좋은 방식:

  • “출장비는 귀사 후 7일 이내 정산입니다. 영수증 첨부가 필요합니다.”

나쁜 방식:

  • “출장비는 정산하면 됩니다.”
    • 너무 짧아서 핵심 조건 누락

즉 선호는 형식에 반영하고,
문서 근거는 내용에 반영하는 게 좋습니다.

전략 2. 문서 근거가 약하면 memory를 더 약하게 본다

RAG가 관련 문서를 확실히 못 찾았을 때는, memory 때문에 그럴듯하게 말하면 hallucination으로 가기 쉽습니다. retrieval 문서는 relevant context at runtime이 핵심이라고 설명하므로, 검색이 약하면 답도 더 보수적이어야 합니다. (LangChain Docs)

전략 3. 최신 사용자 입력 > 오래된 memory

예를 들어 예전엔 “짧게 답해줘”라고 했지만, 이번 질문에서 “이번엔 자세히 설명해줘”라고 하면 현재 입력을 더 우선해야 합니다. context engineering 관점에서도 current state가 중요합니다. (LangChain Docs)


retrieval 품질을 올리면 memory와의 충돌도 줄어든다

반응형

이건 꽤 중요합니다.

많은 분이 충돌 문제를 프롬프트로만 해결하려고 해요.
그런데 retrieval이 지저분하면 프롬프트를 아무리 잘 써도 힘듭니다.

LangChain knowledge-base 문서는 retriever가 similarity, mmr, similarity_score_threshold 검색 타입을 지원한다고 설명합니다. 이건 memory와 RAG를 같이 쓸 때도 매우 중요해요. 왜냐면 RAG 컨텍스트가 너무 많거나 중복되면, memory까지 더해졌을 때 context overload가 더 심해지기 때문입니다. Deep Agents 문서는 memory를 minimal하게 유지하라고 분명히 말합니다. (LangChain Docs)

추천하는 retrieval 튜닝 포인트

1. similarity_score_threshold

관련도 낮은 문서는 아예 잘라내는 방식입니다.
문서 노이즈를 줄이는 데 꽤 좋습니다. (LangChain Docs)

2. mmr

비슷한 문서만 여러 개 오는 걸 줄이고, 좀 더 다양한 근거를 가져오게 도와줍니다. 문서 중복이 심할 때 유용합니다. (LangChain Docs)

3. k를 무조건 크게 하지 않기

문서도 많고 memory도 들어가는데 k까지 크면 context가 금방 무거워집니다. retrieval 문서와 context engineering 문서를 같이 보면, 결국 “필요한 정보만” 넣는 게 핵심입니다. (LangChain Docs)


memory는 왜 “최소한으로” 유지해야 할까

이건 공식 문서가 아주 직접적으로 말하는 부분이라 꼭 가져가고 싶습니다.

Deep Agents context engineering 문서는 memory is always injected되기 쉬우므로, minimal하게 유지하라고 설명합니다. detailed workflows나 domain content는 skills나 별도 구조로 두라고도 말해요. 즉 memory를 많이 쌓는 게 능사가 아니라는 뜻입니다. (LangChain Docs)

이게 memory + RAG 조합에서 특히 중요한 이유는:

  • RAG 문서도 context를 먹고
  • memory도 context를 먹고
  • 둘 다 많으면 모델이 핵심을 놓치기 쉽기 때문입니다

그래서 추천하는 원칙은 이렇습니다.

memory에 남길 것

  • 답변 스타일
  • 지속적 선호
  • 반복되는 사용자 제약
  • 꼭 다시 참고할 만한 장기 정보

memory에 남기지 말 것

  • 공식 규정 전문
  • 방금 대화에서만 필요한 세부사항
  • 긴 작업 절차
  • 외부 문서 원문

이런 건 RAG가 더 맞습니다.


같이 쓸 때 가장 현실적인 서비스 구조

FastAPI + LangChain 기준으로는 보통 이렇게 나누는 게 제일 편합니다.

services/
  assistant_service.py
memory/
  long_term.py
retrieval/
  policy_retriever.py
prompts/
  assistant.py

memory/long_term.py

  • 사용자 선호 검색
  • 장기 기억 저장
  • dedupe / policy

retrieval/policy_retriever.py

  • 벡터스토어 검색
  • threshold/MMR
  • source 정리

services/assistant_service.py

  • 둘 다 호출
  • 우선순위 정리
  • 충돌 처리
  • 최종 prompt 조립

이 구조가 좋은 이유는 단순합니다.

문제가 생겼을 때

  • memory 쪽 문제인지
  • retrieval 쪽 문제인지
  • prompt 조합 문제인지

빨리 보입니다.


최소 실행 코드: memory + RAG 우선순위 규칙 넣기

아래 코드는 아주 작은 예제입니다.

  • memory: 답변 스타일 선호
  • RAG: 정책 문서 검색
  • 충돌 처리: 사실은 문서 우선, 스타일은 memory 우선
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, List


@dataclass
class UserProfile:
    prefers_short_answers: bool = False


class MemoryStore:
    def __init__(self) -> None:
        self._profiles: Dict[str, UserProfile] = {}

    def get_profile(self, user_id: str) -> UserProfile:
        if user_id not in self._profiles:
            self._profiles[user_id] = UserProfile()
        return self._profiles[user_id]

    def update_short_answer_preference(self, user_id: str, prefers_short: bool) -> None:
        self.get_profile(user_id).prefers_short_answers = prefers_short


class PolicyRetriever:
    def __init__(self) -> None:
        self._docs: List[dict] = [
            {
                "title": "출장비 정산 정책",
                "content": "출장비는 귀사 후 7일 이내에 정산해야 하며, 영수증을 반드시 첨부해야 합니다.",
                "source": "policy-expense-travel",
            },
            {
                "title": "재택근무 정책",
                "content": "재택근무는 주 2회까지 가능하며, 예외는 팀장 승인 후 가능합니다.",
                "source": "policy-remote-work",
            },
        ]

    def search(self, query: str) -> dict | None:
        query_lower = query.lower()
        for doc in self._docs:
            haystack = f"{doc['title']} {doc['content']}".lower()
            if any(token in haystack for token in query_lower.split()):
                return doc
        return None


class AssistantService:
    def __init__(self, memory_store: MemoryStore, retriever: PolicyRetriever) -> None:
        self.memory_store = memory_store
        self.retriever = retriever

    def ask(self, user_id: str, question: str) -> str:
        q = question.strip()

        # 최신 사용자 지시가 오래된 memory보다 우선
        if "짧게 답해" in q or "짧게 말해" in q:
            self.memory_store.update_short_answer_preference(user_id, True)
            return "알겠습니다. 앞으로는 짧게 답변할게요."

        profile = self.memory_store.get_profile(user_id)
        doc = self.retriever.search(q)

        if doc is None:
            return "관련 문서를 찾지 못했습니다."

        # 사실 내용은 문서 기반
        full_answer = f"{doc['content']} (출처: {doc['source']})"

        # 스타일만 memory 기반
        if profile.prefers_short_answers:
            if "출장비" in doc["title"]:
                return "출장비는 귀사 후 7일 이내 정산, 영수증 첨부 필요."
            if "재택근무" in doc["title"]:
                return "재택근무는 주 2회까지, 예외는 팀장 승인."

        return full_answer


def main() -> None:
    memory_store = MemoryStore()
    retriever = PolicyRetriever()
    assistant = AssistantService(memory_store, retriever)

    user_id = "alice"

    print(assistant.ask(user_id, "앞으로 짧게 답해줘"))
    print(assistant.ask(user_id, "출장비 정책 알려줘"))
    print(assistant.ask(user_id, "재택근무 정책 알려줘"))


if __name__ == "__main__":
    main()

이 코드의 핵심은 되게 단순합니다.

  • 저장할 건 memory에
  • 사실 근거는 retriever에서
  • 충돌 시 최신 입력 > memory, 내용은 문서 우선

이 정도 규칙만 있어도 품질이 꽤 안정됩니다.


prompt 템플릿은 이렇게 짜면 덜 흔들린다

실제 LangChain prompt에선 보통 이런 식으로 나누는 게 좋습니다.

시스템 규칙:
- 사실과 규정은 검색된 문서를 우선
- 사용자의 선호는 답변 형식에만 반영
- 문서에 없는 내용은 추측하지 말 것

사용자 메모리:
- 사용자는 짧은 답변 선호
- 한국어 선호

검색된 문서:
- 출장비는 귀사 후 7일 이내 정산
- 영수증 첨부 필요

질문:
- 출장비 정책 알려줘

context engineering 문서가 말하는 핵심도 결국 이겁니다.
상태, 메모리, 설정을 현재 단계에 맞게 적절히 구조화해서 모델에 주는 것. (LangChain Docs)


자주 하는 실수

실수 1. memory를 너무 많이 넣는다

공식 문서도 minimal memory를 권장합니다. 특히 detailed domain content까지 memory에 넣으면 문맥이 과부하됩니다. (LangChain Docs)

실수 2. RAG 검색 결과를 너무 많이 넣는다

k를 크게 잡고 memory까지 넣으면 context가 금방 비대해집니다. threshold나 MMR를 고려하는 게 낫습니다. (LangChain Docs)

실수 3. 최신 사용자 지시보다 오래된 memory를 더 믿는다

현재 질문의 의도가 더 중요할 때가 많습니다. context engineering에서도 current state가 중요합니다. (LangChain Docs)

실수 4. 문서 근거와 사용자 선호를 같은 문장으로 섞는다

프롬프트 섹션을 나누는 편이 낫습니다.

실수 5. memory와 RAG를 한 저장소로 합치려 한다

공식 개념상 역할이 다르므로, 보통은 분리하는 편이 훨씬 자연스럽습니다. (LangChain Docs)


비교: memory 우선 vs RAG 우선 vs 역할 분리

방식장점단점추천도

memory 우선 개인화 강함 사실 왜곡 위험 낮음
RAG 우선만 근거 강함 개인화 약함 중간
역할 분리 개인화 + 근거 균형 설계 필요 높음

저는 거의 항상 마지막을 추천합니다.
처음엔 조금 번거롭지만, 결과가 훨씬 안정적이거든요.


FAQ

Q. LangChain memory와 RAG를 꼭 같이 써야 하나요?

아닙니다. 문서 기반 QA만 필요하면 RAG만으로 충분할 수 있고, 개인 assistant면 memory만으로도 시작할 수 있습니다. 다만 사내 도우미나 고객 지원처럼 “근거 + 개인화”가 모두 필요한 경우에는 같이 쓰는 편이 자연스럽습니다. (LangChain Docs)

Q. memory와 RAG를 같은 prompt에 넣어도 되나요?

네, 가능합니다. 다만 섹션을 분리하고 역할 규칙을 같이 넣는 게 좋습니다. context engineering 문서도 시스템 프롬프트가 상태와 메모리를 현재 상황에 맞게 구조화해야 한다고 설명합니다. (LangChain Docs)

Q. memory와 RAG가 충돌하면 누구를 우선해야 하나요?

보통은 사실과 근거가 필요한 내용은 RAG를 우선하고, 답변 스타일과 형식은 memory를 우선하는 편이 좋습니다. grounded answers의 목적상 사실은 문서 쪽이 더 우선입니다. (LangChain Docs)

Q. retrieval search type은 뭘 쓰면 좋나요?

시작은 similarity로 충분하지만, 중복 문서가 많으면 mmr, 노이즈가 많으면 similarity_score_threshold를 고려할 수 있습니다. LangChain knowledge-base 문서가 이 검색 타입들을 설명합니다. (LangChain Docs)

Q. memory는 왜 최소한으로 유지하라고 하나요?

Deep Agents 문서는 memory는 항상 주입되기 쉬워서 context overload를 만들기 쉽다고 설명합니다. 그래서 minimal memory가 권장됩니다. (LangChain Docs)


핵심 요약

이번 글을 진짜 짧게 줄이면 이겁니다.

  • 사실과 근거는 RAG
  • 스타일과 선호는 memory
  • 최신 사용자 지시가 오래된 memory보다 우선
  • prompt는 memory / RAG / rules를 섹션으로 분리
  • retrieval은 threshold/MMR로 노이즈를 줄이기

이 정도 기준만 있어도 memory와 RAG를 같이 쓸 때 품질이 훨씬 덜 흔들립니다.


마무리

저는 예전엔 memory랑 RAG를 같이 붙이면 무조건 더 좋아질 줄 알았어요.
근데 막상 해보니 둘을 많이 넣는 게 중요한 게 아니더라고요.

더 중요한 건
둘의 역할을 헷갈리지 않는 것이었습니다.

memory는 사용자를 더 잘 이해하게 해주고,
RAG는 답변을 더 믿을 수 있게 해줍니다.

그리고 둘이 각자 자기 역할을 할 때
서비스가 제일 안정적으로 좋아졌어요.

그 감각이 생기고 나니까
프롬프트도 덜 복잡해지고,
retrieval도 덜 욕심내게 되고,
memory도 덜 과하게 넣게 되더라고요.

저는 그게 꽤 중요하다고 생각합니다.
잘 붙이는 기술보다, 적절히 나누는 감각이 더 오래 가거든요.


다음 글 예고

다음 글에서는
LangChain agent에 middleware는 왜 중요한가? dynamic prompt, tool 제한, 모델 선택을 한 곳에서 제어하는 방법
으로 이어가겠습니다.

이제부터는 memory와 RAG를 넘어서,
에이전트 전체 동작을 한 단계 위에서 제어하는 middleware 감각으로 넘어가게 될 거예요.


출처

  • LangChain Memory overview: short-term memory는 thread-scoped, long-term memory는 sessions across user/application-level data라고 설명. profile/collection, hot path/background도 함께 설명. (LangChain Docs)
  • LangChain Retrieval: retrieval은 relevant context를 runtime에 가져오고 grounded answers의 기반이 된다고 설명. (LangChain Docs)
  • LangChain RAG: external text source 기반 question answering과 RAG agent 흐름 설명. (LangChain Docs)
  • LangChain Context Engineering: 시스템 프롬프트는 상태, 메모리, 설정을 현재 상황에 맞게 반영해야 한다고 설명. (LangChain Docs)
  • LangChain Knowledge Base: VectorStoreRetriever는 similarity, mmr, similarity_score_threshold 검색 타입을 지원한다고 설명. (LangChain Docs)
  • Deep Agents Context Engineering: memory는 항상 주입되기 쉬워서 minimal하게 유지하라고 설명. (LangChain Docs)
  • LangGraph Add Memory: store + embedding index 기반 semantic search memory 예시 제공. (LangChain Docs)

LangChain, RAG, memory, middleware, context engineering, AI Agent, LangGraph, 사내도우미, 생성형AI, 주니어개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
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
글 보관함
반응형