- 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를 설치하려면 다음 명령어를 실행하세요.
npm install @neo4j/graphql graphql neo4j-driver @apollo/server dotenv
다음으로 index.js 파일에서 데이터 세트의 Movie와 Actor nodes를 설명하는 기본적인 type definitions를 정의할 거예요. 여기서는 너무 자세하게 다루진 않을게요. 간단한 정의만으로도 충분하답니다.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from "@neo4j/graphql";
import neo4j from "neo4j-driver";
const typeDefs = `#graphql
type Movie {
title: String!
plot: String!
actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN)
}
type Actor {
name: String
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
`;
Neo4j JavaScript driver instance도 지정해야 해요. Sandbox instance를 만들었다면, instance 세부 정보를 확장해서 연결 정보를 확인할 수 있을 거예요.
Bolt URL, 사용자 이름, 비밀번호를 복사해서 프로젝트 루트의 .env 파일에 붙여넣으세요.
NEO4J_URI="bolt+s://xxx.xxx.xxx.xxx:7687"
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="three-word-password"
그 다음 dotenv의 config() 함수를 사용해서 이 credentials를 로드하고, 이걸 사용해서 새 driver instance를 만들 거예요.
import { config } from "dotenv"
// Load .env file
config()
// Create driver instance
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(
process.env.NEO4J_USERNAME,
process.env.NEO4J_PASSWORD
)
);
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
자, 이제 재밌는 부분이에요!
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에서 체인은 목표를 달성하기 위해 거치는 일련의 작업들을 말해요.
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`를 참조하죠.
// Define schema
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
resolvers: {
Movie: {
generateReview,
},
}
});
그게 전부예요! 모든 것이 계획대로 진행되었다면 이제 별점을 기준으로 리뷰를 생성할 수 있어요.

일반 생성 리졸버
이 접근 방식은 애플리케이션에서 LLM이 사용되는 방식을 제한하려는 경우에 적합한데, 이게 바로 여러분이 원하시는 걸 수도 있죠.
하지만 이 게시물 시작 부분에서 언급한 예시처럼, 사용자가 자신만의 `prompt`를 보낼 수 있었어요. 사용자 정의 리졸버에 `$prompt` 인수를 추가해서 이걸 활성화하도록 코드를 수정할 수 있어요.
LLM 생성을 허용해야 하는 모든 `type`에 대한 인터페이스를 정의하는 것부터 시작할 수 있어요.
interface CanGenerate {
generate(prompt: String!): GeneratedResponse!
}
이 인터페이스는 동일한 `GeneratedResponse` 출력을 사용해서 생성이 포함된 텍스트 필드를 반환해요.
다음으로, 생성 리졸버를 구현하기 위해 생성(이 경우 `Movie`)을 허용해야 하는 `type`을 수정해요. 필드에는 `@customResolver` 지시문으로 주석을 달아야 하죠.
type Movie implements CanGenerate {
title: String!
plot: String!
generateReview(stars: Int!): GeneratedResponse! @customResolver
generate(prompt: String!): GeneratedResponse! @customResolver
}
type Actor implements CanGenerate {
name: String!
born: Date
actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
generate(prompt: String!): GeneratedResponse! @customResolver
}
사용자 지정 리졸버 함수는 `prompt` 값이 새 `PromptTemplate` 객체를 생성하는 데 사용되는 `args` 인수를 통과한다는 점을 제외하면 위와 유사해요.
const generate = async (source, args) => {
// Create prompt from arguments
const prompt = PromptTemplate.fromTemplate(args.prompt)
const model = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
model: args.model || 'gpt-4'
})
const output = new StringOutputParser()
const chain = RunnableSequence.from([
prompt,
model,
output,
])
// Stringify any objects
const input = Object.fromEntries(
Object.entries({...source, ...args}
)
.map(([ key, value ]) => [
key,
typeof value === 'object' ? JSON.stringify(value) : value
]))
// Invoke the chain
const res = await chain.invoke(input)
return { text: res }
}
그런 다음 함수는 `Movie` 및 `Actor` `type`에 대한 맞춤 리졸버로 정의되어야 해요.
const resolvers = {
Movie: {
generateReview,
generate,
},
Actor: {
generate,
},
};
// Define schema
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
resolvers,
});
잘 작동하는지 확인해 볼까요?
자동 상속
제가 하고 싶었지만 어려움을 겪었던 한 가지는 CanGenerate 인터페이스를 상속한 모든 타입을 감지하고 프로그래밍 방식으로 확인자를 적용하는 것이었어요. 하지만 graphql 라이브러리와 2시간 동안 씨름한 끝에 매핑 기능을 사용하기로 결정했죠.
// Assign resolver to many types
const withGenerateResolver = (types = [], existing = {}) =>
Object.fromEntries(
types.map(type => [
type,
{ ...existing[type], generate }
])
)
const resolvers = resolvers: withGenerateResolver(
['Movie', 'Actor'],
{
Movie: { generateReview, }
}
)
그래도 가능하다고 확신해요. 아이디어가 있으시면 언제든지 링크드인으로 문의해주세요.
결론
GraphQL은 데이터 검색에 대한 간단하고 유연한 접근 방식을 제공해서 맞춤형 요청이 가능하게 해줘요. LLM 생성을 지원하는 resolver를 추가하면 대규모로 동적 콘텐츠 생성 및 개인화가 가능해지죠.
좀 더 자세히 살펴보고 싶으시다면, 코드는 GitHub에서 확인할 수 있어요.
Knowledge Graph가 LLM이 환각을 피하는 데 어떻게 도움이 되는지 자세히 알아보려면 Neo4j GraphAcademy의 무료 LLM 과정을 확인해 보세요.
GraphAcademy에서 Neo4j 및 LLM 기초 과정을 수강하세요.
- API
- 생성 AI 솔루션
- GraphQL
- 랭체인JS
- neo4j-graphql
- Retrieval-Augmented Generation
에이치시스템즈의 LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.
'GraphRAG' 카테고리의 다른 글
| RAG 애플리케이션에서 텍스트 임베딩의 한계 (0) | 2026.05.31 |
|---|---|
| LlamaIndex에서 Property Graph Index를 내 입맛대로 커스터마이징하기 (0) | 2026.05.29 |
| 인공지능과 Machine Learning의 현재와 미래 (0) | 2026.05.29 |
| Neo4j 대규모 지식 그래프에서 예측 분석: GraphRAG와 Machine Learning 활용 (1) | 2026.05.28 |
| 2023년 그래프 기술 전망: Neo4j와 GraphRAG의 미래는? (1) | 2026.05.28 |
