GraphQL API에 Retrieval-Augmented Generation (RAG)을 더하는 방법
hsystems
2026. 5. 31. 09:04
728x90
반응형
GraphQL
지금은 강연을 준비하고 있어요. CityJS 아테네에서 프런트엔드 및 풀스택 개발자를 위한 GenAI(생성 AI)에 대해 이야기할 예정이거든요. GenAI가 GraphQL API 맥락에 어떻게 어울릴지 궁금했어요.
기본적으로 이걸 잘하는 회사 중 하나는 오픈 소스 벡터 데이터베이스인 Weaviate인데요. Weaviate 문서를 훑어보면서 생성 검색 문서에서 사용자 지정 프롬프트로 호출할 수 있는 생성 확인자를 언급한 걸 봤어요. 제가 알기로는 이 기능은 모든 컬렉션에서 사용할 수 있다니, 정말 좋은 소식이죠?
생성 확인자는 프롬프트를 받아서, 쿼리를 통해 검색된 값을 프롬프트에 전달하는 방식이에요. 이건 Retrieval-Augmented Generation(RAG)에 대한 유연한 접근 방식이라고 할 수 있죠.
제 머릿속에 바로 떠오른 생각은, 이걸 어떻게 가능하게 만들 수 있을까 하는 거였어요. @neo4j/graphql 프로젝트에서 말이죠!
GraphQL의 GenAI
물론 LLM 공급자의 엔드포인트에 프롬프트를 보내서 라이브러리 없이도 이 작업을 수행할 수 있지만, 저는 바로 Langchain.js를 사용하는 게 좋겠다고 생각했어요. 이렇게 하면 애플리케이션이 미래에도 계속 대응할 수 있고, 나중에 사전 구축된 도구와 에이전트에 API가 공개될 테니까요.
그러던 와중에 놀랍게도 Langchain이 GraphQL API를 도구로 지원한다는 걸 알게 됐어요! LLM을 통해 GraphQL 엔드포인트를 쿼리할 수 있게 된 거죠. 하지만 해당 GraphQL 엔드포인트 내부에서 LLM을 사용하는 예는 없었어요.
기본 @neo4j/graphql 프로젝트 생성
GraphAcademy의 Neo4j 및 GraphQL 과정 소개는 처음부터 시작하거나 복제 또는 포크할 수 있는 훌륭한 출발점이에요. 코드를 작성하기 위한 이 저장소의 기본 분기도 참고해보세요.
GraphQL 프로젝트 설정이 이미 있다면, LangChain.js 설치 단계로 바로 넘어가도 좋아요.
영화 추천 데이터 세트를 예시로 해서 간단한 예제를 만들어볼게요. Neo4j Sandbox에서 직접 영화 추천 데이터 세트를 실행하거나, GraphAcademy의 Neo4j 및 GraphQL 과정 소개를 참고하면 좋을 거예요.
새 폴더를 만들고 npm init 명령어를 실행해서 새 프로젝트를 시작해볼까요?
mkdir neo4j-graphql-genai && cd $_
npm init es6 --yes
neo4j/graphql과 Apollo 서버 dependencies를 설치하려면 다음 명령어를 실행하세요.
Type definitions와 driver를 사용해서 새로운 Neo4jGraphQL instance를 생성할 수 있어요.
// Define schema
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
});
// Get the schema
const schema = await neoSchema.getSchema()
이제 schema를 사용해서 새 ApolloServer instance를 만들 거예요. 해당 서버를 @apollo/server/standalone에서 가져온 startStandaloneServer 함수에 전달해서 4000번 포트에서 listen하는 새 서버를 만들 수 있죠.
// Create server
const server = new ApolloServer({
schema,
});
// Listen
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ req }),
listen: { port: 4000 },
});
console.log(`🚀 Server ready at ${url}`);
node 명령어를 실행해서 서버를 시작해볼까요?
node index.js
localhost:4000
을 로드하면 Apollo Server UI가 열리고, 여기서 type definitions에 따라 영화와 배우를 검색할 수 있어요.
자, 이제 재밌는 부분이에요!
LangChain.js 설치
이 예시에서는 OpenAI와 함께 Langchain을 사용하고 있지만, 80개 이상의 지원되는 LLM 중 하나를 사용할 수도 있어요. 글을 쓰는 시점에 원하는 걸 선택하면 돼요.
Langchain 및 OpenAI에 대한 종속성을 설치하려면 다음 명령을 실행하세요.
npm i --save langchain @langchain/openai
OpenAI와 함께 LangChain을 사용하려면 platform.openai.com에서 API 키를 생성해야 해요. 키가 있다면 .env에 추가하세요.
OPENAI_API_KEY=sk-...
체인 만들기
다음으로는 을 만들어볼게요. LangChain에서 체인은 목표를 달성하기 위해 거치는 일련의 작업들을 말해요.
PromptTemplate은 특정 항목에 대한 placeholder가 있는 재사용 가능한 prompt를 만드는 데 사용돼요. LLM이 따라야 하는 지침 역할을 하는 거죠.
LangChain이 어떻게 작동하는지, 특히 Neo4j에서 어떻게 작동하는지 배우고 싶다면, Python 중심의 Neo4j 및 LLM 기초 과정과 좀 더 발전된 TypeScript를 사용하여 Neo4j 지원 Chatbot 구축 강의를 참고해보세요.
영화 데이터 세트이므로 LLM에게 냉소적인 영화 리뷰를 작성하도록 지시하는 템플릿을 만들어 볼게요. prompt는 영화 제목과 줄거리를 가져와 query의 일부로 입력된 별점을 기반으로 리뷰를 작성해요.
const prompt = PromptTemplate.fromTemplate(`<br/>
You are a sarcastic movie reviewer creating tongue-in-cheek<br/>
reviews of movies. Create a {stars} star movie review<br/>
for {title}.<br/><br/>
The plot of the movie is: {plot}.<br/><br/>
Remember to use at least one pun or to include a dad joke.<br/>
`)<br/>
const llm = new ChatOpenAI({<br/>
openAIApiKey: process.env.OPENAI_API_KEY,<br/>
})<br/><br/>
const chain = RunnableSequence.from([<br/>
prompt,<br/>
llm,<br/>
new StringOutputParser()<br/>
])
체인에서 .invoke 메소드를 호출해서 이 체인을 테스트할 수 있어요.
const text = await chain.invoke({<br/>
title: 'Toy Story',<br/>
plot: "A cowboy doll is profoundly threatened and jealous when a new spaceman figure supplants him as top toy in a boy's room.",<br/>
stars: 1<br/>
})
LLM은 끔찍한 1⭐️ 리뷰를 생성하네요…
음, 음, 음, 토이 스토리, 이 평범함의 걸작을 어디서부터 시작해야 할까요? 이 영화는 너무 믿기지 않아서 마치 불량품으로 가득 찬 장난감 상자에서 줄거리를 꺼낸 것과 같습니다. 우주인 장난감을 질투하는 카우보이 인형? 별을 향해 다가갔다가 은하계를 놓치는 것에 대해 이야기해 보세요.
GraphQL 리졸버에서 LLM 호출
GraphQL query에서 이 체인을 호출하려면 typeDefs를 수정해서 @customResolver 지시문으로 주석이 달린 Movie 유형에 새 필드를 추가해야 해요. 또한 이 필드에는 Int가 될 필수 인수인 star가 하나 필요해요.
type Movie {<br/>
title: String!<br/>
plot: String!<br/>
generateReview(stars: Int!): GeneratedResponse! @customResolver<br/>
}
생성된 응답 output은 자체 유형으로 정의되어야 해요. 이 경우 text property를 반환해서 나중에 프로세스에 추가 필드를 추가할 수 있는 가능성을 추가하는 거죠.
type GeneratedResponse {<br/>
text: String!<br/>
}
다음으로 실행하려면 맞춤 리졸버 기능이 필요해요.
const generateReview = async (source, args) => {
// <1> Define the prompt template
const prompt = PromptTemplate.fromTemplate(`
You are a sarcastic movie reviewer creating tongue-in-cheek
reviews of movies. Create a {stars} star movie review
for {title}.
The plot of the movie is: {plot}.
Remember to use at least one pun or to include a dad joke.
`)
// <2> Create an LLM instance
const llm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
})
// <3> Create a chain to invoke
const chain = RunnableSequence.from([
prompt,
llm,
new StringOutputParser()
])
// <4> Invoke the chain with the source and args
const text = await chain.invoke({ ...source, ...args })
// Return an GeneratedResponse
return { text }
}
이 함수는 두 가지 인수가 필요해요.
source — `Query`에서 요청된 영화의 속성을 가지고 있어요.
args — `generateReview` 리졸버를 호출하는 데 사용되는 인수인데, `{stars: number}`에 해당해야 해요.
마지막으로, `neoSchema`를 정의할 때 해당 리졸버를 `resolvers` 객체에 정의해야 해요. 각 키는 `type`에 응답하고, 후속 객체는 개별 `resolver`를 참조하죠.