내장 미들웨어
내장 미들웨어 (Prebuilt middleware)
미들웨어(middleware)는 에이전트의 실행을 가로채 특정 동작을 자동으로 처리하게 해주는 강력한 장치예요. 그런데 매번 직접 만들려면 시간이 걸리겠죠. 다행히 LangChain과 Deep Agents는 흔한 사례를 위한 사전 빌드(prebuilt) 미들웨어를 제공해요. 각각은 프로덕션에서 쓸 수 있을 만큼 완성도가 높고, 필요에 맞게 구성할 수 있습니다. 이번 페이지에서는 어떤 것들이 있고 언제 쓰면 좋은지 살펴볼게요.
제공자 무관 미들웨어 (Provider-agnostic middleware)
다음 미들웨어들은 어떤 LLM 제공자와도 동작해요.
| 미들웨어 | 설명 |
|---|---|
| Tool error | 도구 실행 예외를 잡아 모델이 볼 수 있는 오류 메시지로 변환 |
| Tool retry | 실패한 도구 호출을 지수 백오프로 자동 재시도 |
| Model retry | 실패한 모델 호출을 지수 백오프로 자동 재시도 |
| Model fallback | 기본 모델이 실패하면 대체 모델로 자동 폴백 |
| Summarization | 토큰 한도에 가까워지면 대화 이력을 자동 요약 |
| Human-in-the-loop | 도구 호출의 인간 승인을 위해 실행을 일시 중지 |
| Model call limit | 과도한 비용을 막기 위해 모델 호출 수 제한 |
| Tool call limit | 호출 횟수를 제한해 도구 실행 제어 |
| PII detection | 개인식별정보(PII) 감지·처리 |
| To-do list | 에이전트에 작업 계획·추적 능력 부여 |
| LLM tool selector | 메인 모델 호출 전에 LLM으로 관련 도구 선택 |
| Provider tool search | 도구를 제공자의 서버측 도구 검색 뒤로 지연, 필요할 때 노출 |
| Shell tool | 명령 실행을 위해 영구 셸 세션을 에이전트에 노출 |
| Filesystem | 컨텍스트·장기 메모리 저장을 위한 파일시스템 제공 |
| Subagent | 서브에이전트 생성 능력 추가 |
| Rubric grading (Beta) | LLM-as-a-judge 채점으로 에이전트가 루브릭을 충족할 때까지 자기 평가·반복 |
| File search | 파일시스템 파일 위에 Glob·Grep 검색 도구 제공 |
| Context editing | 도구 사용을 트리밍·정리해 대화 컨텍스트 관리 |
| LLM tool emulator | 테스트 목적으로 LLM으로 도구 실행을 에뮬레이트 |
여기서는 대표적인 것들을 자세히 살펴볼게요.
Tool error
도구 실행 중 발생한 예외를 잡아, 에이전트 실행을 중단시키는 대신 모델이 보고 회복할 수 있는 오류 ToolMessage로 변환해요. Tool error가 유용한 상황:
- 실패한 도구 호출을 수정된 인자로 모델이 재시도하게 하기
- 원시 예외 세부 정보 대신 통제된·정화된 오류 메시지 표면화
- 예상치 못한 도구 예외가 에이전트를 크래시시키는 것 방지
Tool error 미들웨어는 실패한 호출을 자동으로 재시도하지 않아요. 재시도가 필요하면 middleware 리스트에서 안쪽(더 앞쪽)에 두고 on_failure="error"로 구성한 Tool retry 미들웨어와 조합해서, 예외가 tool error 미들웨어에 도달하게 하세요.
API 레퍼런스: ToolErrorMiddleware — langchain>=1.3.14 필요
from langchain.agents import create_agent
from langchain.agents.middleware import ToolErrorMiddleware
def on_error(exc: Exception, request: ToolCallRequest) -> str | None:
if isinstance(exc, ValueError):
return f"`{request.tool_call['name']}` failed with {type(exc).__name__}."
# propagate everything else
agent = create_agent(
model="gpt-5.5",
tools=[your_tools],
middleware=[ToolErrorMiddleware(on_error)],
)
구성 옵션:
on_error— 도구 실행에서 발생한 각 예외에 대해 호출되는 동기 핸들러. 콘텐츠(str또는 콘텐츠 블록 리스트)를 반환하면 예외를ToolMessage(status="error")로 변환하고,None을 반환하거나 생략하면 예외를 그대로 전파해요. 동기 경로와(aon_error가 주어지지 않으면) 비동기 경로에서 쓰입니다.aon_error— 선택적 비동기 핸들러. 비동기 실행 경로에서 사용. 주어지지 않으면on_error로 폴백.tools— 오류 처리를 적용할 도구 또는 도구 이름 리스트.None이면 모든 도구에 적용.
전체 예시 — 핸들러가 예외와 ToolCallRequest(도구 호출 dict에 name, args, call ID 포함)를 받아요. 처리하고 싶지 않은 예외에 대해 None을 반환하면 정상적으로 전파됩니다.
from langchain.agents import create_agent
from langchain.agents.middleware import ToolErrorMiddleware, ToolRetryMiddleware
def on_error(exc: Exception, request: ToolCallRequest) -> str | None:
# Surface ValueError to the model so it can correct the input
if isinstance(exc, ValueError):
return f"`{request.tool_call['name']}` failed: {type(exc).__name__}. Fix the input and retry."
# Let all other exceptions propagate (halts the run)
return None
# Async-only usage
async def aon_error(exc: Exception, request: ToolCallRequest) -> str | None:
if isinstance(exc, ConnectionError):
return f"Tool `{request.tool_call['name']}` encountered a connection error."
return None
agent = create_agent(
model="gpt-5.5",
tools=[search_tool, database_tool],
middleware=[
# Place retry inner so exceptions reach ToolErrorMiddleware after retries are exhausted
ToolRetryMiddleware(max_retries=3, on_failure="error"),
ToolErrorMiddleware(on_error=on_error, tools=["search_tool"]),
],
)
# Async-only: pass aon_error alone (do not pass on_error)
async_agent = create_agent(
model="gpt-5.5",
tools=[api_tool],
middleware=[ToolErrorMiddleware(aon_error=aon_error)],
)
원시 예외 메시지에는 민감하거나 내부적인 세부 정보가 담길 수 있으니, 예외 타입의 이름을 담은 콘텐츠를 반환하는 걸 권장해요. on_error 핸들러가 공개 범위를 통제합니다 — 원시 예외 메시지는 직접 포함시키지 않는 한 모델에 보내지지 않아요.
Tool retry
실패한 도구 호출을 구성 가능한 지수 백오프로 자동 재시도해요. 유용한 상황:
- 외부 API 호출의 일시적 실패 처리
- 네트워크 의존 도구의 신뢰성 향상
- 일시적 오류를 우아하게 처리하는 회복력 있는 에이전트 구축
API 레퍼런스: ToolRetryMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[search_tool, database_tool],
middleware=[
ToolRetryMiddleware(
max_retries=3,
backoff_factor=2.0,
initial_delay=1.0,
),
],
)
구성 옵션:
| 매개변수 | 기본값 | 설명 |
|---|---|---|
max_retries |
"2" |
초기 호출 이후 최대 재시도 횟수 (기본 총 3회 시도) |
tools |
— | 재시도를 적용할 도구·이름 리스트. None이면 모든 도구 |
retry_on |
"default_retry_on" |
재시도할 예외 타입 튜플, 또는 예외를 받아 재시도 여부(True/False)를 반환하는 콜러블. langchain>=1.3.16부터 기본값은 재시도 가능한 모델 오류와 분류되지 않은 모든 예외를 재시도하고, non-retryable로 표시된 모델 오류는 재시도하지 않음 |
on_failure |
"continue" |
모든 재시도가 소진된 뒤의 동작. 'continue'(기본, 오류 세부 정보를 담은 ToolMessage 반환), 'error'(예외 재발생), 커스텀 콜러블(예외를 받아 ToolMessage 콘텐츠 문자열 반환). 지원 중단: 'return_message'('continue'로), 'raise'('error'로) |
backoff_factor |
"2.0" |
지수 백오프 승수. 각 재시도는 initial_delay * (backoff_factor ** retry_number)초 대기. 상수 지연은 0.0 |
initial_delay |
"1.0" |
첫 재시도 전 초기 지연(초) |
max_delay |
"60.0" |
재시도 사이 최대 지연(초). 지수 백오프 증가 상한 |
jitter |
"true" |
무리 효과(thundering herd) 방지를 위해 지연에 무작위 지터(±25%) 추가 여부 |
Model retry
실패한 모델 호출을 구성 가능한 지수 백오프로 자동 재시도해요. Tool retry와 같은 방식이며 모델 호출에 적용되는 버전이에요.
API 레퍼런스: ModelRetryMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[search_tool, database_tool],
middleware=[
ModelRetryMiddleware(
max_retries=3,
backoff_factor=2.0,
initial_delay=1.0,
),
],
)
구성 옵션은 Tool retry와 거의 동일한데, on_failure의 기본 동작이 'continue'일 때 오류 세부 정보를 담은 AIMessage를 반환한다는 점만 달라요.
# Custom exception filtering
retry = ModelRetryMiddleware(
max_retries=4,
retry_on=(TimeoutError, ConnectionError),
backoff_factor=1.5,
)
# Return error message instead of raising
retry_continue = ModelRetryMiddleware(
max_retries=4,
on_failure="continue", # Return AIMessage with error instead of raising
)
# Raise exception on failure
strict_retry = ModelRetryMiddleware(
max_retries=2,
on_failure="error", # Re-raise exception instead of returning message
)
Model fallback
기본 모델이 실패하면 대체 모델로 자동 폴백해요. 유용한 상황:
- 모델 장애를 처리하는 회복력 있는 에이전트 구축
- 더 저렴한 모델로 폴백하는 비용 최적화
- OpenAI, Anthropic 등 제공자 이중화
API 레퍼런스: ModelFallbackMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ModelFallbackMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[],
middleware=[
ModelFallbackMiddleware(
"gpt-5.4-mini",
"claude-3-5-sonnet-20241022",
),
],
)
구성 옵션:
first_model(필수) — 기본 모델이 실패할 때 시도할 첫 폴백 모델. 모델 식별자 문자열(예:'openai:gpt-5.4-mini') 또는BaseChatModel인스턴스.*additional_models— 이전 모델들이 실패하면 순서대로 시도할 추가 폴백 모델.
Summarization
토큰 한도에 가까워질 때 대화 이력을 자동 요약해서, 최근 메시지는 보존하고 오래된 컨텍스트는 압축해요. 유용한 상황:
- 컨텍스트 윈도우를 초과하는 장수 대화
- 방대한 이력을 가진 다회전 대화
- 전체 대화 컨텍스트 보존이 중요한 애플리케이션
Summarization은 텍스트 중심의 컨텍스트 압축이에요. 이미지/오디오/비디오 페이로드를 리사이즈하거나 다운샘플하지 않아요. keep이 유지하는 최근 메시지는 원래 멀티모달 블록을 그대로 포함하고, 요약되는 오래된 멀티모달 메시지는 생성된 텍스트 요약으로만 표현됩니다. 이미지가 많은 애플리케이션이라면 미디어를 파일시스템이나 객체 스토어에 저장하고, 메시지 이력에는 URL이나 파일 참조를 전달하세요.
API 레퍼런스: SummarizationMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[your_weather_tool, your_calculator_tool],
middleware=[
SummarizationMiddleware(
model="gpt-5.4-mini",
trigger=("tokens", 4000),
keep=("messages", 20),
),
],
)
trigger와 keep에 사용되는 fraction 조건은 langchain>=1.1을 쓸 때 챗 모델의 프로필 데이터에 의존해요. 데이터를 쓸 수 없다면 다른 조건을 쓰거나 수동으로 지정하세요.
구성 옵션 (핵심):
model(필수) — 요약 생성 모델. 식별자 문자열 또는BaseChatModel인스턴스.trigger— 요약을 트리거할 조건. 단일ContextSize튜플(지정 임계값 충족 시), 단일TriggerClausedict(모든 임계값 충족 — AND 논리), 또는 둘을 섞은 리스트(임의 항목 충족 — OR 논리). 지원 임계값:fraction(모델 컨텍스트 크기의 비율, 0-1),tokens(절대 토큰 수),messages(메시지 수). 제공하지 않으면 자동 요약이 트리거되지 않아요.keep— 요약 후 보존할 컨텍스트 양.fraction,tokens,messages중 정확히 하나.token_counter— 커스텀 토큰 계산 함수. 기본은 문자 기반.summary_prompt— 커스텀 요약 프롬프트 템플릿. 대화 이력이 삽입될{messages}자리표시자를 포함해야 해요.trim_tokens_to_summarize(기본"4000") — 요약 생성 시 포함할 최대 토큰 수. 요약 전에 메시지를 이 한도로 트리밍.summary_prefix,max_tokens_before_summary,messages_to_keep— 지원 중단. 각각summary_prompt,trigger: ("tokens", value),keep: ("messages", value)로 대체.
Human-in-the-loop
도구 호출이 실행되기 전에 인간의 승인·수정·거부를 위해 에이전트 실행을 일시 중지해요. 아래 상황에 유용합니다:
- 인간 승인이 필요한 고위험 작업 (예: 데이터베이스 쓰기, 금융 거래)
- 인간 감독이 의무인 규정 준수 워크플로
- 인간 피드백이 에이전트를 안내하는 장수 대화
API 레퍼런스: HumanInTheLoopMiddleware
Human-in-the-loop 미들웨어는 인터럽트 간 상태를 유지하기 위해 체크포인터가 필요해요.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
def your_read_email_tool(email_id: str) -> str:
"""Mock function to read an email by its ID."""
return f"Email content for ID: {email_id}"
def your_send_email_tool(recipient: str, subject: str, body: str) -> str:
"""Mock function to send an email."""
return f"Email sent to {recipient} with subject '{subject}'"
agent = create_agent(
model="gpt-5.5",
tools=[your_read_email_tool, your_send_email_tool],
checkpointer=InMemorySaver(),
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"your_send_email_tool": {
"allowed_decisions": ["approve", "edit", "reject"],
},
"your_read_email_tool": False,
}
),
],
)
완전한 예시, 구성 옵션, 통합 패턴은 Human-in-the-loop 문서를 참고하세요.
Model call limit
무한 루프나 과도한 비용을 막기 위해 모델 호출 수를 제한해요. 유용한 상황:
- 요청을 너무 많이 만드는 폭주 에이전트 방지
- 프로덕션 배포의 비용 통제 강제
- 특정 호출 예산 안에서 에이전트 동작 테스트
API 레퍼런스: ModelCallLimitMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="gpt-5.5",
checkpointer=InMemorySaver(), # Required for thread limiting
tools=[],
middleware=[
ModelCallLimitMiddleware(
thread_limit=10,
run_limit=5,
exit_behavior="end",
),
],
)
구성 옵션:
thread_limit— 스레드 안의 모든 실행에서 최대 모델 호출 수. 기본 무제한.run_limit— 단일 호출당 최대 모델 호출 수. 기본 무제한.exit_behavior(기본"end") — 한도 도달 시 동작.'end'(우아한 종료) 또는'error'(예외 발생).
Tool call limit
전역으로든 특정 도구에 대해서든 도구 호출 수를 제한해 에이전트 실행을 제어해요. 유용한 상황:
- 비싼 외부 API에 대한 과도한 호출 방지
- 웹 검색이나 데이터베이스 쿼리 제한
- 특정 도구 사용에 대한 속도 제한 강제
- 폭주 에이전트 루프 방지
API 레퍼런스: ToolCallLimitMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[search_tool, database_tool],
middleware=[
# Global limit
ToolCallLimitMiddleware(thread_limit=20, run_limit=10),
# Tool-specific limit
ToolCallLimitMiddleware(
tool_name="search",
thread_limit=5,
run_limit=3,
),
],
)
구성 옵션:
tool_name— 제한할 특정 도구 이름. 제공하지 않으면 모든 도구에 전역 적용.thread_limit— 스레드(대화) 안의 모든 실행에서 최대 도구 호출 수. 같은 thread ID로 여러 호출에 걸쳐 영속. 상태 유지에 체크포인터 필요.None은 스레드 제한 없음.run_limit— 단일 호출(사용자 메시지 하나 → 응답 주기)당 최대 도구 호출 수. 새 사용자 메시지마다 리셋.None은 실행 제한 없음. 참고:thread_limit또는run_limit중 하나 이상을 지정해야 해요.exit_behavior(기본"continue") — 한도 도달 시 동작.'continue'(초과된 도구 호출을 오류 메시지로 차단, 다른 도구와 모델은 계속),'error'(ToolCallLimitExceededError예외 발생, 즉시 중단),'end'(초과된 도구 호출에 대해ToolMessage와 AI 메시지로 즉시 종료 — 단일 도구를 제한할 때만 동작하며, 다른 도구에 대기 중인 호출이 있으면NotImplementedError발생).
PII detection
구성 가능한 전략으로 대화에서 개인식별정보(PII)를 감지·처리해요. 유용한 상황:
- 규정 준수가 필요한 의료·금융 애플리케이션
- 로그를 정화해야 하는 고객 서비스 에이전트
- 민감한 사용자 데이터를 다루는 모든 애플리케이션
apply_to_output=True를 설정하면 PIIMiddleware가 등록된 스트림 트랜스포머를 통해 스트리밍 출력(텍스트 델타, 도구 호출 인자, 도구 출력, 상태 스냅샷)도 가립니다. langchain>=1.3.2 필요. 미들웨어에 트랜스포머 등록을 참고하세요.
API 레퍼런스: PIIMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[],
middleware=[
PIIMiddleware("email", strategy="redact", apply_to_input=True),
PIIMiddleware("credit_card", strategy="mask", apply_to_input=True),
],
)
커스텀 PII 유형 — detector 매개변수를 제공해 내장 유형을 넘어 사용 사례에 특화된 패턴을 감지할 수 있어요. 세 가지 방법이 있습니다.
- 정규식 패턴 문자열 — 단순 패턴 매칭 (
detector=r"sk-[a-zA-Z0-9]{32}") - 컴파일된 정규식 — 플래그가 필요할 때 (
re.compile(...)) - 커스텀 감지 함수 — 검증까지 하는 복잡한 감지 로직
커스텀 감지 함수는 문자열(콘텐츠)을 받아 PIIMatch 객체 리스트를 반환해요.
from langchain.agents.middleware import PIIMatch
def detector(content: str) -> list[PIIMatch]:
return [
{
"type": "custom_type",
"value": "matched_text",
"start": 0,
"end": 12,
},
# ... more matches
]
구성 옵션:
| 매개변수 | 기본값 | 설명 |
|---|---|---|
pii_type |
필수 | 감지할 PII 유형. 내장(email, credit_card, ip, mac_address, url) 또는 커스텀 유형 이름 |
strategy |
"redact" |
감지된 PII 처리. 'block'(예외), 'redact'([REDACTED_{PII_TYPE}]), 'mask'(부분 가림, 예: ****-****-****-1234), 'hash'(결정적 해시) |
detector |
— | 커스텀 감지 함수 또는 정규식. 주어지지 않으면 PII 유형의 내장 감지기 사용 |
apply_to_input |
True |
모델 호출 전에 사용자 메시지 검사 |
apply_to_output |
False |
모델 호출 후 AI 메시지 검사. langchain>=1.3.2부터는 스트리밍 출력도 가림 |
apply_to_tool_results |
False |
실행 후 도구 결과 메시지 검사 |
To-do list
복잡한 다단계 작업을 위해 에이전트에 작업 계획·추적 능력을 부여해요. 미들웨어는 에이전트에 write_todos 도구와 효과적인 작업 계획을 안내하는 시스템 프롬프트를 자동으로 제공합니다.
API 레퍼런스: TodoListMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
agent = create_agent(
model="gpt-5.5",
tools=[read_file, write_file, run_tests],
middleware=[TodoListMiddleware()],
)
Context editing
도구 사용을 트리밍·정리해 대화 컨텍스트를 관리해요. 가장 흔한 전략은 ClearToolUsesEdit인데, 오래된 도구 결과를 지우면서 최근 것들은 보존합니다.
API 레퍼런스: ContextEditingMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import ContextEditingMiddleware, ClearToolUsesEdit
agent = create_agent(
model="gpt-5.5",
tools=[],
middleware=[
ContextEditingMiddleware(
edits=[
ClearToolUsesEdit(
trigger=100000,
keep=3,
),
],
),
],
)
동작 방식: 1) 대화의 토큰 수를 모니터링 → 2) 임계값에 도달하면 오래된 도구 출력을 정리 → 3) 최근 N개 도구 결과를 유지 → 4) (선택) 컨텍스트를 위해 도구 호출 인자 보존.
ContextEditingMiddleware 옵션:
edits(기본[ClearToolUsesEdit()]) — 적용할ContextEdit전략 리스트token_count_method(기본"approximate") — 토큰 계산 방법.'approximate'또는'model'
ClearToolUsesEdit 옵션:
trigger(기본"100000") — 편집을 트리거하는 토큰 수. 대화가 이 토큰 수를 초과하면 오래된 도구 출력을 정리clear_at_least(기본"0") — 편집이 실행될 때 회수할 최소 토큰 수.0이면 필요한 만큼 정리keep(기본"3") — 보존해야 할 가장 최근 도구 결과 수. 절대 지워지지 않아요clear_tool_inputs(기본False) — AI 메시지의 원래 도구 호출 매개변수도 지울지 여부.True면 도구 호출 인자를 빈 객체로 교체exclude_tools(기본()) — 정리에서 제외할 도구 이름 리스트. 이 도구들의 출력은 절대 지워지지 않아요placeholder(기본"[cleared]") — 지워진 도구 출력 대신 삽입되는 자리표시자 텍스트. 원래 도구 메시지 콘텐츠를 대체
LLM tool emulator
테스트 목적으로 실제 도구 호출을 AI 생성 응답으로 대체해 LLM으로 도구 실행을 에뮬레이트해요. 유용한 상황:
- 실제 도구를 실행하지 않고 에이전트 동작 테스트
- 외부 도구가 없거나 비쌀 때 에이전트 개발
- 실제 도구 구현 전에 에이전트 워크플로 프로토타이핑
API 레퍼런스: LLMToolEmulator
from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolEmulator
agent = create_agent(
model="gpt-5.5",
tools=[get_weather, search_database, send_email],
middleware=[
LLMToolEmulator(), # Emulate all tools
],
)
구성 옵션:
tools— 에뮬레이트할 도구 이름(str) 또는BaseTool인스턴스 리스트.None(기본)이면 모든 도구를 에뮬레이트. 빈 리스트[]면 아무것도 에뮬레이트하지 않음. 이름/인스턴스 배열이면 그 도구들만 에뮬레이트.model— 에뮬레이트된 도구 응답 생성에 쓰는 모델. 지정하지 않으면 에이전트의 모델을 사용.
제공자별 미들웨어 (Provider-specific middleware)
위 미들웨어들은 특정 LLM 제공자에 최적화된 것들이에요. 전체 내용과 예시는 각 제공자의 문서를 참고하세요.
- Anthropic — Claude 모델용 프롬프트 캐싱, bash 도구, 텍스트 편집기, 메모리, 파일 검색 미들웨어. 문서
- AWS — Amazon Bedrock 모델용 프롬프트 캐싱 미들웨어. 문서
- OpenAI — OpenAI 모델용 콘텐츠 중재 미들웨어. 문서