언어 모델 미들웨어
언어 모델 미들웨어
모든 호출 지점마다 같은 로깅이나 캐싱, 가드레일 코드를 붙이는 건 금방 지저분해져요. 언어 모델 미들웨어는 모델에 대한 호출을 가로채고 수정해서 이런 공통 기능을 모델과 무관하게 얹는 방법이에요. 가드레일, RAG, 캐싱, 로깅을 언어 모델에게서 분리해 개발·배포할 수 있게 해 주죠.
출처: 공식문서
본문
미들웨어 사용하기
미들웨어는 wrapLanguageModel 함수로 감싸서 사용해요. 언어 모델과 미들웨어를 받아, 미들웨어가 적용된 새 언어 모델을 돌려줘요.
import { wrapLanguageModel, streamText } from 'ai';
const wrappedLanguageModel = wrapLanguageModel({
model: yourModel,
middleware: yourLanguageModelMiddleware,
});
감싼 모델은 다른 언어 모델처럼 그대로 쓸 수 있어요. 미들웨어는 여러 개를 배열로 넘겨 순서대로 적용돼요. [firstMiddleware, secondMiddleware]는 firstMiddleware(secondMiddleware(yourModel))처럼 중첩돼요.
내장 미들웨어
AI SDK는 바로 쓸 수 있는 여러 내장 미들웨어를 제공해요.
extractReasoningMiddleware— 생성된 텍스트에서 리저닝 정보를 뽑아 결과의reasoning프로퍼티로 노출해요.{ tagName: 'think' }으로 태그 이름을 정할 수 있고,startWithReasoning: true를 주면 리저닝 태그를 텍스트 앞에 붙이기도 해요(리저닝 태그를 응답 앞에 넣지 않는 모델용).extractJsonMiddleware— 마크다운 코드 펜스를 벗겨 텍스트에서 JSON을 추출해요. JSON을 코드 블록으로 감싸는 모델에서Output.object()와 함께 쓰기 좋아요. 다른 포맷을 쓰는 모델이라면transform함수로 커스텀할 수 있어요.simulateStreamingMiddleware— 비스트리밍 모델의 응답으로 스트리밍 동작을 흉내 내요. 완전한 응답만 주는 모델을 쓸 때도 일관된 스트리밍 인터페이스를 유지하려는 경우에 유용해요.defaultInstructionsMiddleware— 호출에 자체 지침이 없을 때 기본 지침을 적용해요. 호출에 직접 준 지침이 우선하고, 정규화된 프롬프트에 시스템 메시지가 있으면 기본값을 추가하지 않아요.defaultSettingsMiddleware— 언어 모델에 기본 설정을 적용해요.temperature,maxOutputTokens,providerOptions등을 일괄 지정할 수 있어요.addToolInputExamplesMiddleware—inputExamples프로퍼티를 네이티브로 지원하지 않는 프로바이더를 위해 도구 입력 예시를 도구 설명에 직렬화해 넣어요.prefix(기본'Input Examples:'),format,remove(기본true) 옵션을 줄 수 있어요.
커뮤니티 미들웨어
AI SDK는 언어 모델 미들웨어 명세를 제공해서, 커뮤니티가 이에 맞는 미들웨어를 개발해 생태계와 호환되게 만들 수 있어요. 대표적으로 @ai-sdk-tool/parser 패키지가 있어요. 이 미들웨어는 네이티브 함수 호출을 지원하지 않는 셀프호스팅·서드파티 모델에 도구 호출 기능을 더해요. 함수 스키마를 프롬프트 지시문으로 변환하고 모델 응답을 구조화된 함수 호출로 파싱해서, 모델에 관계없이 일관된 함수 호출 API를 제공하죠. createToolMiddleware, hermesToolMiddleware(Hermes·Qwen 포맷), gemmaToolMiddleware(Gemma 3 시리즈) 세 가지 변형이 있어요. 네이티브 함수 호출을 지원하는 모델에 이 미들웨어를 쓰면 성능이 저하될 수 있으니, 쓰기 전에 모델 지원 여부를 확인해야 해요.
미들웨어 직접 구현하기
미들웨어 구현은 고급 기능이라 언어 모델 명세에 대한 이해가 필요해요. 구현할 수 있는 함수는 세 가지예요.
transformParams—doGenerate와doStream양쪽에서 파라미터가 언어 모델에 전달되기 전에 변환해요.wrapGenerate— 언어 모델의doGenerate메서드를 감싸요. 파라미터를 수정하고, 모델을 호출하고, 결과를 수정할 수 있어요.wrapStream—doStream메서드를 감싸요. 파라미터 수정·호출·결과 수정이 모두 가능해요.
공식 문서에는 이 세 함수로 로깅, 캐싱, RAG, 가드레일을 구현하는 예시가 들어 있어요.
- 로깅 —
wrapGenerate에서 호출 전에 파라미터를, 호출 후에 생성된 텍스트를 출력하고,wrapStream에서는TransformStream으로text-start/text-delta/text-end파트를 누적해 생성 텍스트를 모아 출력한 뒤 그대로 통과시켜요. - 캐싱 —
wrapGenerate에서 파라미터를 직렬화한 값을 키로Map에 결과를 캐싱해요. - RAG —
transformParams에서 마지막 사용자 메시지 텍스트로 소스를 찾아 지시문을 만들고, 그것을 마지막 사용자 메시지에 덧붙여 파라미터를 반환해요. - 가드레일 —
wrapGenerate에서 생성 결과의 텍스트 파트를 필터링(예: 금칙어를<REDACTED>로 치환)해 반환해요. 스트리밍 가드레일은 스트림이 끝나야 전체 내용을 알 수 있어 구현이 어렵다는 점을 명심해야 해요.
요청별 커스텀 메타데이터
미들웨어에서 요청별 커스텀 메타데이터를 주고받으려면 providerOptions를 써요. 로깅 미들웨어에서 사용자 ID나 타임스탬프 같은 컨텍스트를 넘길 때 유용해요. params?.providerMetadata에서 자신의 미들웨어 키로 접근하면 돼요.
const { text } = await generateText({
model: wrapLanguageModel({
model: "xai/grok-4.6",
middleware: yourLogMiddleware,
}),
prompt: 'Invent a new holiday and describe its traditions.',
providerOptions: {
yourLogMiddleware: {
hello: 'world',
},
},
});
더 알아보기
- 도구 호출 —
addToolInputExamplesMiddleware가 붙는 도구 구조 - 리저닝 —
extractReasoningMiddleware의 배경 - 런타임과 도구 컨텍스트 —
providerOptions기반 메타데이터 흐름